Adds missing files, gets device
This commit is contained in:
parent
56749e29c4
commit
a72c5c867c
|
@ -12,7 +12,8 @@
|
|||
<script src="js/main.js"></script>
|
||||
<script>
|
||||
var app = Elm.Main.init({
|
||||
node: document.getElementById('container')
|
||||
node: document.getElementById('container'),
|
||||
flags: { width: window.innerWidth, height: window.innerHeight }
|
||||
});
|
||||
|
||||
app.ports.registerVideo.subscribe(function(args) {
|
||||
|
|
29
src/Core.elm
29
src/Core.elm
|
@ -1,7 +1,8 @@
|
|||
module Core exposing (FullModel(..), Model, Msg(..), Page(..), init, update)
|
||||
module Core exposing (FullModel(..), Model, Msg(..), Page(..), init, subscriptions, update)
|
||||
|
||||
import Browser.Events as Events
|
||||
import Browser.Navigation as Nav
|
||||
import Json.Decode as Decode
|
||||
import Element
|
||||
import Ports
|
||||
import Task
|
||||
import Time
|
||||
|
@ -10,7 +11,7 @@ import Url
|
|||
|
||||
|
||||
type FullModel
|
||||
= Unloaded Url.Url Nav.Key
|
||||
= Unloaded Element.Device Url.Url Nav.Key
|
||||
| Loaded Model
|
||||
|
||||
|
||||
|
@ -19,6 +20,7 @@ type alias Model =
|
|||
, zone : Time.Zone
|
||||
, page : Page
|
||||
, key : Nav.Key
|
||||
, device : Element.Device
|
||||
}
|
||||
|
||||
|
||||
|
@ -35,25 +37,36 @@ type Msg
|
|||
| PlaylistClicked Twitch.Playlist
|
||||
| VideoClicked Twitch.Playlist Twitch.Video
|
||||
| UrlReceived Url.Url
|
||||
| SizeReceived Int Int
|
||||
|
||||
|
||||
init : Decode.Value -> Url.Url -> Nav.Key -> ( FullModel, Cmd Msg )
|
||||
init _ url key =
|
||||
( Unloaded url key
|
||||
init : { width : Int, height : Int } -> Url.Url -> Nav.Key -> ( FullModel, Cmd Msg )
|
||||
init { width, height } url key =
|
||||
( Unloaded (Element.classifyDevice { width = width, height = height }) url key
|
||||
, Task.perform PlaylistsReceived Twitch.fetchPlaylists
|
||||
)
|
||||
|
||||
|
||||
subscriptions : FullModel -> Sub Msg
|
||||
subscriptions _ =
|
||||
Events.onResize (\w h -> SizeReceived w h)
|
||||
|
||||
|
||||
update : Msg -> FullModel -> ( FullModel, Cmd Msg )
|
||||
update msg model =
|
||||
case ( msg, model ) of
|
||||
( Noop, _ ) ->
|
||||
( model, Cmd.none )
|
||||
|
||||
( PlaylistsReceived ( playlists, zone ), Unloaded url key ) ->
|
||||
( SizeReceived w h, Loaded m ) ->
|
||||
( Loaded { m | device = Element.classifyDevice { width = w, height = h } }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
( PlaylistsReceived ( playlists, zone ), Unloaded device url key ) ->
|
||||
update
|
||||
(UrlReceived url)
|
||||
(Loaded { key = key, playlists = playlists, zone = zone, page = Home })
|
||||
(Loaded { key = key, playlists = playlists, zone = zone, page = Home, device = device })
|
||||
|
||||
( HomeClicked, Loaded m ) ->
|
||||
( model
|
||||
|
|
|
@ -2,17 +2,16 @@ module Main exposing (main)
|
|||
|
||||
import Browser
|
||||
import Core
|
||||
import Json.Decode as Decode
|
||||
import Views
|
||||
|
||||
|
||||
main : Program Decode.Value Core.FullModel Core.Msg
|
||||
main : Program { width : Int, height : Int } Core.FullModel Core.Msg
|
||||
main =
|
||||
Browser.application
|
||||
{ init = Core.init
|
||||
, update = Core.update
|
||||
, view = Views.view
|
||||
, subscriptions = \_ -> Sub.none
|
||||
, subscriptions = Core.subscriptions
|
||||
, onUrlChange = Core.UrlReceived
|
||||
, onUrlRequest = \_ -> Core.Noop
|
||||
}
|
||||
|
|
427
src/Views.elm
427
src/Views.elm
|
@ -4,18 +4,9 @@ import Browser
|
|||
import Colors
|
||||
import Consts
|
||||
import Core
|
||||
import Element exposing (Element)
|
||||
import Element.Background as Background
|
||||
import Element.Border as Border
|
||||
import Element
|
||||
import Element.Font as Font
|
||||
import Element.Input as Input
|
||||
import Element.Keyed as Keyed
|
||||
import Html
|
||||
import Html.Attributes
|
||||
import Json.Encode as Encode
|
||||
import Time
|
||||
import TimeUtils
|
||||
import Twitch
|
||||
import Views.Desktop as Desktop
|
||||
|
||||
|
||||
view : Core.FullModel -> Browser.Document Core.Msg
|
||||
|
@ -27,7 +18,7 @@ view model =
|
|||
, Font.size Consts.normalFontSize
|
||||
, Font.family [ Font.typeface "Cantarell" ]
|
||||
]
|
||||
(viewContent model)
|
||||
(Desktop.view model)
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -35,7 +26,7 @@ view model =
|
|||
title : Core.FullModel -> String
|
||||
title model =
|
||||
case model of
|
||||
Core.Unloaded _ _ ->
|
||||
Core.Unloaded _ _ _ ->
|
||||
Consts.url
|
||||
|
||||
Core.Loaded m ->
|
||||
|
@ -48,413 +39,3 @@ title model =
|
|||
|
||||
Core.Video p v ->
|
||||
Consts.url ++ " - " ++ p.name ++ " - " ++ v.name
|
||||
|
||||
|
||||
viewContent : Core.FullModel -> Element Core.Msg
|
||||
viewContent model =
|
||||
let
|
||||
content =
|
||||
case model of
|
||||
Core.Unloaded _ _ ->
|
||||
Element.none
|
||||
|
||||
Core.Loaded submodel ->
|
||||
mainView submodel
|
||||
in
|
||||
Element.column [ Element.width Element.fill ] [ topBar, content ]
|
||||
|
||||
|
||||
mainView : Core.Model -> Element Core.Msg
|
||||
mainView model =
|
||||
case model.page of
|
||||
Core.Home ->
|
||||
playlistsView model.playlists
|
||||
|
||||
Core.Playlist playlist ->
|
||||
videoMiniaturesView model.zone playlist
|
||||
|
||||
Core.Video playlist video ->
|
||||
videoView model.zone playlist video
|
||||
|
||||
|
||||
topBar : Element Core.Msg
|
||||
topBar =
|
||||
Element.row
|
||||
[ Element.width Element.fill
|
||||
, Background.color Colors.primary
|
||||
, Font.color Colors.white
|
||||
, Font.size Consts.homeFontSize
|
||||
]
|
||||
[ homeButton ]
|
||||
|
||||
|
||||
homeButton : Element Core.Msg
|
||||
homeButton =
|
||||
Input.button
|
||||
[ Element.padding Consts.homePadding
|
||||
, Element.height Element.fill
|
||||
, Element.mouseOver [ Background.color Colors.primaryOver ]
|
||||
, Font.bold
|
||||
]
|
||||
{ label = Element.text Consts.name
|
||||
, onPress = Just Core.HomeClicked
|
||||
}
|
||||
|
||||
|
||||
playlistsView : List Twitch.Playlist -> Element Core.Msg
|
||||
playlistsView playlists =
|
||||
let
|
||||
empty =
|
||||
Element.el [ Element.width Element.fill ] Element.none
|
||||
|
||||
views =
|
||||
List.map playlistView playlists
|
||||
|
||||
grouped =
|
||||
group 4 views
|
||||
|
||||
rows =
|
||||
grouped
|
||||
|> List.map (\x -> List.map (Maybe.withDefault empty) x)
|
||||
|> List.map (Element.row [ Element.spacing 10, Element.width Element.fill ])
|
||||
|
||||
final =
|
||||
Element.column [ Element.padding 10, Element.spacing 10, Element.width Element.fill ] rows
|
||||
in
|
||||
final
|
||||
|
||||
|
||||
playlistView : Twitch.Playlist -> Element Core.Msg
|
||||
playlistView playlist =
|
||||
let
|
||||
image =
|
||||
Element.image
|
||||
[ Element.width Element.fill
|
||||
, Element.height Element.fill
|
||||
, Element.inFront inFront
|
||||
]
|
||||
{ description = "", src = Twitch.playlistMiniatureUrl playlist }
|
||||
|
||||
length =
|
||||
List.length playlist.videos
|
||||
|
||||
label =
|
||||
String.fromInt length
|
||||
++ " video"
|
||||
++ (if length > 1 then
|
||||
"s"
|
||||
|
||||
else
|
||||
""
|
||||
)
|
||||
|
||||
inFront =
|
||||
Element.text label
|
||||
|> Element.el
|
||||
[ Background.color Colors.greyBackground
|
||||
, Border.rounded 5
|
||||
, Element.padding 5
|
||||
, Font.color Colors.white
|
||||
]
|
||||
|> Element.el
|
||||
[ Element.alignBottom
|
||||
, Element.alignRight
|
||||
, Element.padding 5
|
||||
]
|
||||
|
||||
display =
|
||||
Element.column [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ image
|
||||
, Element.paragraph
|
||||
[ Font.bold, Font.size Consts.titleFontSize ]
|
||||
[ Element.text playlist.name ]
|
||||
]
|
||||
|
||||
button =
|
||||
Input.button [ Element.width Element.fill, Element.alignTop ]
|
||||
{ label = display
|
||||
, onPress = Just (Core.PlaylistClicked playlist)
|
||||
}
|
||||
in
|
||||
button
|
||||
|
||||
|
||||
videoMiniaturesView : Time.Zone -> Twitch.Playlist -> Element Core.Msg
|
||||
videoMiniaturesView zone playlist =
|
||||
let
|
||||
empty =
|
||||
Element.el [ Element.width Element.fill ] Element.none
|
||||
|
||||
views =
|
||||
List.map (videoMiniatureView zone playlist) playlist.videos
|
||||
|
||||
grouped =
|
||||
group 4 views
|
||||
|
||||
rows =
|
||||
grouped
|
||||
|> List.map (\x -> List.map (Maybe.withDefault empty) x)
|
||||
|> List.map (Element.row [ Element.spacing 10, Element.width Element.fill ])
|
||||
|
||||
final =
|
||||
Element.column [ Element.padding 10, Element.spacing 10, Element.width Element.fill ] rows
|
||||
in
|
||||
final
|
||||
|
||||
|
||||
videoMiniature : Twitch.Video -> Element Core.Msg
|
||||
videoMiniature video =
|
||||
let
|
||||
inFront =
|
||||
Element.text label
|
||||
|> Element.el
|
||||
[ Background.color Colors.greyBackground
|
||||
, Border.rounded 5
|
||||
, Element.padding 5
|
||||
, Font.color Colors.white
|
||||
]
|
||||
|> Element.el
|
||||
[ Element.alignBottom
|
||||
, Element.alignRight
|
||||
, Element.padding 5
|
||||
]
|
||||
|
||||
image =
|
||||
Element.image
|
||||
[ Element.width Element.fill
|
||||
, Element.height Element.fill
|
||||
, Element.inFront inFront
|
||||
]
|
||||
{ description = "", src = Twitch.videoMiniatureUrl video }
|
||||
|
||||
label =
|
||||
formatTime video.duration
|
||||
in
|
||||
image
|
||||
|
||||
|
||||
videoMiniatureView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||
videoMiniatureView zone playlist video =
|
||||
let
|
||||
display =
|
||||
Element.column [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ videoMiniature video
|
||||
, videoDescription zone video
|
||||
]
|
||||
|
||||
button =
|
||||
Input.button [ Element.width Element.fill, Element.alignTop ]
|
||||
{ label = display
|
||||
, onPress = Just (Core.VideoClicked playlist video)
|
||||
}
|
||||
in
|
||||
button
|
||||
|
||||
|
||||
videoInList : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Twitch.Video -> Element Core.Msg
|
||||
videoInList zone playlist activeVideo video =
|
||||
let
|
||||
( msg, attr ) =
|
||||
if video == activeVideo then
|
||||
( Nothing
|
||||
, [ Element.width Element.fill
|
||||
, Background.color Colors.selected
|
||||
, Border.color Colors.primary
|
||||
, Border.width 2
|
||||
]
|
||||
)
|
||||
|
||||
else
|
||||
( Just (Core.VideoClicked playlist video), [ Element.width Element.fill ] )
|
||||
|
||||
label =
|
||||
Element.row [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ Element.el [ Element.width (Element.fillPortion 2) ]
|
||||
(videoMiniature video)
|
||||
, Element.el [ Element.width (Element.fillPortion 3), Element.paddingXY 0 10, Element.alignTop ]
|
||||
(videoDescription zone video)
|
||||
]
|
||||
in
|
||||
Input.button attr { label = label, onPress = msg }
|
||||
|
||||
|
||||
videoView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||
videoView zone playlist video =
|
||||
Element.row [ Element.padding 10, Element.width Element.fill, Element.spacing 10 ]
|
||||
[ Element.column
|
||||
[ Element.width (Element.fillPortion 2)
|
||||
, Element.spacing 10
|
||||
, Element.alignTop
|
||||
]
|
||||
[ Keyed.el
|
||||
[ Element.width Element.fill
|
||||
, Element.height (Element.px 0)
|
||||
, Element.htmlAttribute (Html.Attributes.style "padding-top" "56.25%")
|
||||
, Element.htmlAttribute (Html.Attributes.style "position" "relative")
|
||||
]
|
||||
( video.url
|
||||
, Element.html
|
||||
(Html.video
|
||||
[ Html.Attributes.id (Twitch.videoId video)
|
||||
, Html.Attributes.class "video-js"
|
||||
, Html.Attributes.class "vjs-default-skin"
|
||||
, Html.Attributes.class "wf"
|
||||
, Html.Attributes.property "data-setup" (Encode.string "{\"fluid\": true}")
|
||||
, Html.Attributes.style "position" "absolute"
|
||||
, Html.Attributes.style "top" "0"
|
||||
, Html.Attributes.style "height" "100%"
|
||||
, Html.Attributes.controls True
|
||||
, Html.Attributes.autoplay True
|
||||
]
|
||||
[]
|
||||
)
|
||||
)
|
||||
, Element.paragraph
|
||||
[ Font.size Consts.homeFontSize
|
||||
, Font.bold
|
||||
, Element.paddingEach { top = 10, left = 0, bottom = 0, right = 0 }
|
||||
]
|
||||
[ Element.text video.name ]
|
||||
, case video.date of
|
||||
Just date ->
|
||||
Element.paragraph
|
||||
[ Font.size Consts.titleFontSize ]
|
||||
[ Element.text ("Diffusé le " ++ formatDate zone date) ]
|
||||
|
||||
_ ->
|
||||
Element.none
|
||||
]
|
||||
, Element.column [ Element.alignTop, Element.spacing 10, Element.width (Element.fillPortion 1) ]
|
||||
(List.map (videoInList zone playlist video) playlist.videos)
|
||||
]
|
||||
|
||||
|
||||
videoDescription : Time.Zone -> Twitch.Video -> Element Core.Msg
|
||||
videoDescription zone video =
|
||||
Element.column [ Element.spacing 10 ]
|
||||
[ Element.paragraph
|
||||
[ Font.bold
|
||||
, Font.size Consts.titleFontSize
|
||||
]
|
||||
[ Element.text video.name ]
|
||||
, case video.date of
|
||||
Just date ->
|
||||
Element.paragraph
|
||||
[ Font.color Colors.greyFont
|
||||
]
|
||||
[ Element.text ("Diffusé le " ++ formatDate zone date) ]
|
||||
|
||||
_ ->
|
||||
Element.none
|
||||
]
|
||||
|
||||
|
||||
formatDate : Time.Zone -> Time.Posix -> String
|
||||
formatDate zone time =
|
||||
let
|
||||
day =
|
||||
Time.toDay zone time |> String.fromInt |> TimeUtils.pad2
|
||||
|
||||
month =
|
||||
Time.toMonth zone time |> TimeUtils.monthToString
|
||||
|
||||
year =
|
||||
Time.toYear zone time |> String.fromInt |> TimeUtils.pad2
|
||||
|
||||
hours =
|
||||
Time.toHour zone time |> String.fromInt
|
||||
|
||||
minutes =
|
||||
Time.toMinute zone time |> String.fromInt |> TimeUtils.pad2
|
||||
in
|
||||
day ++ "/" ++ month ++ "/" ++ year ++ " à " ++ hours ++ "h" ++ minutes
|
||||
|
||||
|
||||
formatTime : Int -> String
|
||||
formatTime time =
|
||||
let
|
||||
hours =
|
||||
toHours time
|
||||
|
||||
minutes =
|
||||
toMinutes time
|
||||
|
||||
seconds =
|
||||
toSeconds time
|
||||
|
||||
hoursString =
|
||||
String.fromInt hours
|
||||
|
||||
minutesString =
|
||||
if minutes < 10 then
|
||||
"0" ++ String.fromInt minutes
|
||||
|
||||
else
|
||||
String.fromInt minutes
|
||||
|
||||
secondsString =
|
||||
if seconds < 10 then
|
||||
"0" ++ String.fromInt seconds
|
||||
|
||||
else
|
||||
String.fromInt seconds
|
||||
in
|
||||
hoursString
|
||||
++ ":"
|
||||
++ minutesString
|
||||
++ ":"
|
||||
++ secondsString
|
||||
|
||||
|
||||
toHours : Int -> Int
|
||||
toHours i =
|
||||
i // 3600
|
||||
|
||||
|
||||
toMinutes : Int -> Int
|
||||
toMinutes i =
|
||||
modBy 3600 i // 60
|
||||
|
||||
|
||||
toSeconds : Int -> Int
|
||||
toSeconds i =
|
||||
modBy 60 i
|
||||
|
||||
|
||||
group : Int -> List a -> List (List (Maybe a))
|
||||
group size list =
|
||||
let
|
||||
grouped =
|
||||
List.map (List.map Just) (groupAux size list [])
|
||||
|
||||
groupedRev =
|
||||
List.reverse grouped
|
||||
|
||||
( firstFixed, tail ) =
|
||||
case groupedRev of
|
||||
h :: t ->
|
||||
( List.concat [ h, List.repeat (size - List.length h) Nothing ], t )
|
||||
|
||||
[] ->
|
||||
( [], [] )
|
||||
|
||||
fixed =
|
||||
(firstFixed :: tail) |> List.reverse
|
||||
in
|
||||
fixed
|
||||
|
||||
|
||||
groupAux : Int -> List a -> List (List a) -> List (List a)
|
||||
groupAux size list acc =
|
||||
if List.isEmpty list then
|
||||
List.reverse acc
|
||||
|
||||
else
|
||||
let
|
||||
groupHead =
|
||||
List.take size list
|
||||
|
||||
groupTail =
|
||||
List.drop size list
|
||||
in
|
||||
groupAux size groupTail (groupHead :: acc)
|
||||
|
|
|
@ -0,0 +1,427 @@
|
|||
module Views.Desktop exposing (view)
|
||||
|
||||
import Colors
|
||||
import Consts
|
||||
import Core
|
||||
import Element exposing (Element)
|
||||
import Element.Background as Background
|
||||
import Element.Border as Border
|
||||
import Element.Font as Font
|
||||
import Element.Input as Input
|
||||
import Element.Keyed as Keyed
|
||||
import Html
|
||||
import Html.Attributes
|
||||
import Json.Encode as Encode
|
||||
import Time
|
||||
import TimeUtils
|
||||
import Twitch
|
||||
|
||||
|
||||
view : Core.FullModel -> Element Core.Msg
|
||||
view model =
|
||||
let
|
||||
content =
|
||||
case model of
|
||||
Core.Unloaded _ _ _ ->
|
||||
Element.none
|
||||
|
||||
Core.Loaded submodel ->
|
||||
mainView submodel
|
||||
in
|
||||
Element.column [ Element.width Element.fill ] [ topBar, content ]
|
||||
|
||||
|
||||
mainView : Core.Model -> Element Core.Msg
|
||||
mainView model =
|
||||
case model.page of
|
||||
Core.Home ->
|
||||
playlistsView model.playlists
|
||||
|
||||
Core.Playlist playlist ->
|
||||
videoMiniaturesView model.zone playlist
|
||||
|
||||
Core.Video playlist video ->
|
||||
videoView model.zone playlist video
|
||||
|
||||
|
||||
topBar : Element Core.Msg
|
||||
topBar =
|
||||
Element.row
|
||||
[ Element.width Element.fill
|
||||
, Background.color Colors.primary
|
||||
, Font.color Colors.white
|
||||
, Font.size Consts.homeFontSize
|
||||
]
|
||||
[ homeButton ]
|
||||
|
||||
|
||||
homeButton : Element Core.Msg
|
||||
homeButton =
|
||||
Input.button
|
||||
[ Element.padding Consts.homePadding
|
||||
, Element.height Element.fill
|
||||
, Element.mouseOver [ Background.color Colors.primaryOver ]
|
||||
, Font.bold
|
||||
]
|
||||
{ label = Element.text Consts.name
|
||||
, onPress = Just Core.HomeClicked
|
||||
}
|
||||
|
||||
|
||||
playlistsView : List Twitch.Playlist -> Element Core.Msg
|
||||
playlistsView playlists =
|
||||
let
|
||||
empty =
|
||||
Element.el [ Element.width Element.fill ] Element.none
|
||||
|
||||
views =
|
||||
List.map playlistView playlists
|
||||
|
||||
grouped =
|
||||
group 4 views
|
||||
|
||||
rows =
|
||||
grouped
|
||||
|> List.map (\x -> List.map (Maybe.withDefault empty) x)
|
||||
|> List.map (Element.row [ Element.spacing 10, Element.width Element.fill ])
|
||||
|
||||
final =
|
||||
Element.column [ Element.padding 10, Element.spacing 10, Element.width Element.fill ] rows
|
||||
in
|
||||
final
|
||||
|
||||
|
||||
playlistView : Twitch.Playlist -> Element Core.Msg
|
||||
playlistView playlist =
|
||||
let
|
||||
image =
|
||||
Element.image
|
||||
[ Element.width Element.fill
|
||||
, Element.height Element.fill
|
||||
, Element.inFront inFront
|
||||
]
|
||||
{ description = "", src = Twitch.playlistMiniatureUrl playlist }
|
||||
|
||||
length =
|
||||
List.length playlist.videos
|
||||
|
||||
label =
|
||||
String.fromInt length
|
||||
++ " video"
|
||||
++ (if length > 1 then
|
||||
"s"
|
||||
|
||||
else
|
||||
""
|
||||
)
|
||||
|
||||
inFront =
|
||||
Element.text label
|
||||
|> Element.el
|
||||
[ Background.color Colors.greyBackground
|
||||
, Border.rounded 5
|
||||
, Element.padding 5
|
||||
, Font.color Colors.white
|
||||
]
|
||||
|> Element.el
|
||||
[ Element.alignBottom
|
||||
, Element.alignRight
|
||||
, Element.padding 5
|
||||
]
|
||||
|
||||
display =
|
||||
Element.column [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ image
|
||||
, Element.paragraph
|
||||
[ Font.bold, Font.size Consts.titleFontSize ]
|
||||
[ Element.text playlist.name ]
|
||||
]
|
||||
|
||||
button =
|
||||
Input.button [ Element.width Element.fill, Element.alignTop ]
|
||||
{ label = display
|
||||
, onPress = Just (Core.PlaylistClicked playlist)
|
||||
}
|
||||
in
|
||||
button
|
||||
|
||||
|
||||
videoMiniaturesView : Time.Zone -> Twitch.Playlist -> Element Core.Msg
|
||||
videoMiniaturesView zone playlist =
|
||||
let
|
||||
empty =
|
||||
Element.el [ Element.width Element.fill ] Element.none
|
||||
|
||||
views =
|
||||
List.map (videoMiniatureView zone playlist) playlist.videos
|
||||
|
||||
grouped =
|
||||
group 4 views
|
||||
|
||||
rows =
|
||||
grouped
|
||||
|> List.map (\x -> List.map (Maybe.withDefault empty) x)
|
||||
|> List.map (Element.row [ Element.spacing 10, Element.width Element.fill ])
|
||||
|
||||
final =
|
||||
Element.column [ Element.padding 10, Element.spacing 10, Element.width Element.fill ] rows
|
||||
in
|
||||
final
|
||||
|
||||
|
||||
videoMiniature : Twitch.Video -> Element Core.Msg
|
||||
videoMiniature video =
|
||||
let
|
||||
inFront =
|
||||
Element.text label
|
||||
|> Element.el
|
||||
[ Background.color Colors.greyBackground
|
||||
, Border.rounded 5
|
||||
, Element.padding 5
|
||||
, Font.color Colors.white
|
||||
]
|
||||
|> Element.el
|
||||
[ Element.alignBottom
|
||||
, Element.alignRight
|
||||
, Element.padding 5
|
||||
]
|
||||
|
||||
image =
|
||||
Element.image
|
||||
[ Element.width Element.fill
|
||||
, Element.height Element.fill
|
||||
, Element.inFront inFront
|
||||
]
|
||||
{ description = "", src = Twitch.videoMiniatureUrl video }
|
||||
|
||||
label =
|
||||
formatTime video.duration
|
||||
in
|
||||
image
|
||||
|
||||
|
||||
videoMiniatureView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||
videoMiniatureView zone playlist video =
|
||||
let
|
||||
display =
|
||||
Element.column [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ videoMiniature video
|
||||
, videoDescription zone video
|
||||
]
|
||||
|
||||
button =
|
||||
Input.button [ Element.width Element.fill, Element.alignTop ]
|
||||
{ label = display
|
||||
, onPress = Just (Core.VideoClicked playlist video)
|
||||
}
|
||||
in
|
||||
button
|
||||
|
||||
|
||||
videoInList : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Twitch.Video -> Element Core.Msg
|
||||
videoInList zone playlist activeVideo video =
|
||||
let
|
||||
( msg, attr ) =
|
||||
if video == activeVideo then
|
||||
( Nothing
|
||||
, [ Element.width Element.fill
|
||||
, Background.color Colors.selected
|
||||
, Border.color Colors.primary
|
||||
, Border.width 2
|
||||
]
|
||||
)
|
||||
|
||||
else
|
||||
( Just (Core.VideoClicked playlist video), [ Element.width Element.fill ] )
|
||||
|
||||
label =
|
||||
Element.row [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ Element.el [ Element.width (Element.fillPortion 2) ]
|
||||
(videoMiniature video)
|
||||
, Element.el [ Element.width (Element.fillPortion 3), Element.paddingXY 0 10, Element.alignTop ]
|
||||
(videoDescription zone video)
|
||||
]
|
||||
in
|
||||
Input.button attr { label = label, onPress = msg }
|
||||
|
||||
|
||||
videoView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||
videoView zone playlist video =
|
||||
Element.row [ Element.padding 10, Element.width Element.fill, Element.spacing 10 ]
|
||||
[ Element.column
|
||||
[ Element.width (Element.fillPortion 2)
|
||||
, Element.spacing 10
|
||||
, Element.alignTop
|
||||
]
|
||||
[ Keyed.el
|
||||
[ Element.width Element.fill
|
||||
, Element.height (Element.px 0)
|
||||
, Element.htmlAttribute (Html.Attributes.style "padding-top" "56.25%")
|
||||
, Element.htmlAttribute (Html.Attributes.style "position" "relative")
|
||||
]
|
||||
( video.url
|
||||
, Element.html
|
||||
(Html.video
|
||||
[ Html.Attributes.id (Twitch.videoId video)
|
||||
, Html.Attributes.class "video-js"
|
||||
, Html.Attributes.class "vjs-default-skin"
|
||||
, Html.Attributes.class "wf"
|
||||
, Html.Attributes.property "data-setup" (Encode.string "{\"fluid\": true}")
|
||||
, Html.Attributes.style "position" "absolute"
|
||||
, Html.Attributes.style "top" "0"
|
||||
, Html.Attributes.style "height" "100%"
|
||||
, Html.Attributes.controls True
|
||||
, Html.Attributes.autoplay True
|
||||
]
|
||||
[]
|
||||
)
|
||||
)
|
||||
, Element.paragraph
|
||||
[ Font.size Consts.homeFontSize
|
||||
, Font.bold
|
||||
, Element.paddingEach { top = 10, left = 0, bottom = 0, right = 0 }
|
||||
]
|
||||
[ Element.text video.name ]
|
||||
, case video.date of
|
||||
Just date ->
|
||||
Element.paragraph
|
||||
[ Font.size Consts.titleFontSize ]
|
||||
[ Element.text ("Diffusé le " ++ formatDate zone date) ]
|
||||
|
||||
_ ->
|
||||
Element.none
|
||||
]
|
||||
, Element.column [ Element.alignTop, Element.spacing 10, Element.width (Element.fillPortion 1) ]
|
||||
(List.map (videoInList zone playlist video) playlist.videos)
|
||||
]
|
||||
|
||||
|
||||
videoDescription : Time.Zone -> Twitch.Video -> Element Core.Msg
|
||||
videoDescription zone video =
|
||||
Element.column [ Element.spacing 10 ]
|
||||
[ Element.paragraph
|
||||
[ Font.bold
|
||||
, Font.size Consts.titleFontSize
|
||||
]
|
||||
[ Element.text video.name ]
|
||||
, case video.date of
|
||||
Just date ->
|
||||
Element.paragraph
|
||||
[ Font.color Colors.greyFont
|
||||
]
|
||||
[ Element.text ("Diffusé le " ++ formatDate zone date) ]
|
||||
|
||||
_ ->
|
||||
Element.none
|
||||
]
|
||||
|
||||
|
||||
formatDate : Time.Zone -> Time.Posix -> String
|
||||
formatDate zone time =
|
||||
let
|
||||
day =
|
||||
Time.toDay zone time |> String.fromInt |> TimeUtils.pad2
|
||||
|
||||
month =
|
||||
Time.toMonth zone time |> TimeUtils.monthToString
|
||||
|
||||
year =
|
||||
Time.toYear zone time |> String.fromInt |> TimeUtils.pad2
|
||||
|
||||
hours =
|
||||
Time.toHour zone time |> String.fromInt
|
||||
|
||||
minutes =
|
||||
Time.toMinute zone time |> String.fromInt |> TimeUtils.pad2
|
||||
in
|
||||
day ++ "/" ++ month ++ "/" ++ year ++ " à " ++ hours ++ "h" ++ minutes
|
||||
|
||||
|
||||
formatTime : Int -> String
|
||||
formatTime time =
|
||||
let
|
||||
hours =
|
||||
toHours time
|
||||
|
||||
minutes =
|
||||
toMinutes time
|
||||
|
||||
seconds =
|
||||
toSeconds time
|
||||
|
||||
hoursString =
|
||||
String.fromInt hours
|
||||
|
||||
minutesString =
|
||||
if minutes < 10 then
|
||||
"0" ++ String.fromInt minutes
|
||||
|
||||
else
|
||||
String.fromInt minutes
|
||||
|
||||
secondsString =
|
||||
if seconds < 10 then
|
||||
"0" ++ String.fromInt seconds
|
||||
|
||||
else
|
||||
String.fromInt seconds
|
||||
in
|
||||
hoursString
|
||||
++ ":"
|
||||
++ minutesString
|
||||
++ ":"
|
||||
++ secondsString
|
||||
|
||||
|
||||
toHours : Int -> Int
|
||||
toHours i =
|
||||
i // 3600
|
||||
|
||||
|
||||
toMinutes : Int -> Int
|
||||
toMinutes i =
|
||||
modBy 3600 i // 60
|
||||
|
||||
|
||||
toSeconds : Int -> Int
|
||||
toSeconds i =
|
||||
modBy 60 i
|
||||
|
||||
|
||||
group : Int -> List a -> List (List (Maybe a))
|
||||
group size list =
|
||||
let
|
||||
grouped =
|
||||
List.map (List.map Just) (groupAux size list [])
|
||||
|
||||
groupedRev =
|
||||
List.reverse grouped
|
||||
|
||||
( firstFixed, tail ) =
|
||||
case groupedRev of
|
||||
h :: t ->
|
||||
( List.concat [ h, List.repeat (size - List.length h) Nothing ], t )
|
||||
|
||||
[] ->
|
||||
( [], [] )
|
||||
|
||||
fixed =
|
||||
(firstFixed :: tail) |> List.reverse
|
||||
in
|
||||
fixed
|
||||
|
||||
|
||||
groupAux : Int -> List a -> List (List a) -> List (List a)
|
||||
groupAux size list acc =
|
||||
if List.isEmpty list then
|
||||
List.reverse acc
|
||||
|
||||
else
|
||||
let
|
||||
groupHead =
|
||||
List.take size list
|
||||
|
||||
groupTail =
|
||||
List.drop size list
|
||||
in
|
||||
groupAux size groupTail (groupHead :: acc)
|
|
@ -0,0 +1,427 @@
|
|||
module Views.Mobile exposing (view)
|
||||
|
||||
import Colors
|
||||
import Consts
|
||||
import Core
|
||||
import Element exposing (Element)
|
||||
import Element.Background as Background
|
||||
import Element.Border as Border
|
||||
import Element.Font as Font
|
||||
import Element.Input as Input
|
||||
import Element.Keyed as Keyed
|
||||
import Html
|
||||
import Html.Attributes
|
||||
import Json.Encode as Encode
|
||||
import Time
|
||||
import TimeUtils
|
||||
import Twitch
|
||||
|
||||
|
||||
view : Core.FullModel -> Element Core.Msg
|
||||
view model =
|
||||
let
|
||||
content =
|
||||
case model of
|
||||
Core.Unloaded _ _ ->
|
||||
Element.none
|
||||
|
||||
Core.Loaded submodel ->
|
||||
mainView submodel
|
||||
in
|
||||
Element.column [ Element.width Element.fill ] [ topBar, content ]
|
||||
|
||||
|
||||
mainView : Core.Model -> Element Core.Msg
|
||||
mainView model =
|
||||
case model.page of
|
||||
Core.Home ->
|
||||
playlistsView model.playlists
|
||||
|
||||
Core.Playlist playlist ->
|
||||
videoMiniaturesView model.zone playlist
|
||||
|
||||
Core.Video playlist video ->
|
||||
videoView model.zone playlist video
|
||||
|
||||
|
||||
topBar : Element Core.Msg
|
||||
topBar =
|
||||
Element.row
|
||||
[ Element.width Element.fill
|
||||
, Background.color Colors.primary
|
||||
, Font.color Colors.white
|
||||
, Font.size Consts.homeFontSize
|
||||
]
|
||||
[ homeButton ]
|
||||
|
||||
|
||||
homeButton : Element Core.Msg
|
||||
homeButton =
|
||||
Input.button
|
||||
[ Element.padding Consts.homePadding
|
||||
, Element.height Element.fill
|
||||
, Element.mouseOver [ Background.color Colors.primaryOver ]
|
||||
, Font.bold
|
||||
]
|
||||
{ label = Element.text Consts.name
|
||||
, onPress = Just Core.HomeClicked
|
||||
}
|
||||
|
||||
|
||||
playlistsView : List Twitch.Playlist -> Element Core.Msg
|
||||
playlistsView playlists =
|
||||
let
|
||||
empty =
|
||||
Element.el [ Element.width Element.fill ] Element.none
|
||||
|
||||
views =
|
||||
List.map playlistView playlists
|
||||
|
||||
grouped =
|
||||
group 4 views
|
||||
|
||||
rows =
|
||||
grouped
|
||||
|> List.map (\x -> List.map (Maybe.withDefault empty) x)
|
||||
|> List.map (Element.row [ Element.spacing 10, Element.width Element.fill ])
|
||||
|
||||
final =
|
||||
Element.column [ Element.padding 10, Element.spacing 10, Element.width Element.fill ] rows
|
||||
in
|
||||
final
|
||||
|
||||
|
||||
playlistView : Twitch.Playlist -> Element Core.Msg
|
||||
playlistView playlist =
|
||||
let
|
||||
image =
|
||||
Element.image
|
||||
[ Element.width Element.fill
|
||||
, Element.height Element.fill
|
||||
, Element.inFront inFront
|
||||
]
|
||||
{ description = "", src = Twitch.playlistMiniatureUrl playlist }
|
||||
|
||||
length =
|
||||
List.length playlist.videos
|
||||
|
||||
label =
|
||||
String.fromInt length
|
||||
++ " video"
|
||||
++ (if length > 1 then
|
||||
"s"
|
||||
|
||||
else
|
||||
""
|
||||
)
|
||||
|
||||
inFront =
|
||||
Element.text label
|
||||
|> Element.el
|
||||
[ Background.color Colors.greyBackground
|
||||
, Border.rounded 5
|
||||
, Element.padding 5
|
||||
, Font.color Colors.white
|
||||
]
|
||||
|> Element.el
|
||||
[ Element.alignBottom
|
||||
, Element.alignRight
|
||||
, Element.padding 5
|
||||
]
|
||||
|
||||
display =
|
||||
Element.column [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ image
|
||||
, Element.paragraph
|
||||
[ Font.bold, Font.size Consts.titleFontSize ]
|
||||
[ Element.text playlist.name ]
|
||||
]
|
||||
|
||||
button =
|
||||
Input.button [ Element.width Element.fill, Element.alignTop ]
|
||||
{ label = display
|
||||
, onPress = Just (Core.PlaylistClicked playlist)
|
||||
}
|
||||
in
|
||||
button
|
||||
|
||||
|
||||
videoMiniaturesView : Time.Zone -> Twitch.Playlist -> Element Core.Msg
|
||||
videoMiniaturesView zone playlist =
|
||||
let
|
||||
empty =
|
||||
Element.el [ Element.width Element.fill ] Element.none
|
||||
|
||||
views =
|
||||
List.map (videoMiniatureView zone playlist) playlist.videos
|
||||
|
||||
grouped =
|
||||
group 4 views
|
||||
|
||||
rows =
|
||||
grouped
|
||||
|> List.map (\x -> List.map (Maybe.withDefault empty) x)
|
||||
|> List.map (Element.row [ Element.spacing 10, Element.width Element.fill ])
|
||||
|
||||
final =
|
||||
Element.column [ Element.padding 10, Element.spacing 10, Element.width Element.fill ] rows
|
||||
in
|
||||
final
|
||||
|
||||
|
||||
videoMiniature : Twitch.Video -> Element Core.Msg
|
||||
videoMiniature video =
|
||||
let
|
||||
inFront =
|
||||
Element.text label
|
||||
|> Element.el
|
||||
[ Background.color Colors.greyBackground
|
||||
, Border.rounded 5
|
||||
, Element.padding 5
|
||||
, Font.color Colors.white
|
||||
]
|
||||
|> Element.el
|
||||
[ Element.alignBottom
|
||||
, Element.alignRight
|
||||
, Element.padding 5
|
||||
]
|
||||
|
||||
image =
|
||||
Element.image
|
||||
[ Element.width Element.fill
|
||||
, Element.height Element.fill
|
||||
, Element.inFront inFront
|
||||
]
|
||||
{ description = "", src = Twitch.videoMiniatureUrl video }
|
||||
|
||||
label =
|
||||
formatTime video.duration
|
||||
in
|
||||
image
|
||||
|
||||
|
||||
videoMiniatureView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||
videoMiniatureView zone playlist video =
|
||||
let
|
||||
display =
|
||||
Element.column [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ videoMiniature video
|
||||
, videoDescription zone video
|
||||
]
|
||||
|
||||
button =
|
||||
Input.button [ Element.width Element.fill, Element.alignTop ]
|
||||
{ label = display
|
||||
, onPress = Just (Core.VideoClicked playlist video)
|
||||
}
|
||||
in
|
||||
button
|
||||
|
||||
|
||||
videoInList : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Twitch.Video -> Element Core.Msg
|
||||
videoInList zone playlist activeVideo video =
|
||||
let
|
||||
( msg, attr ) =
|
||||
if video == activeVideo then
|
||||
( Nothing
|
||||
, [ Element.width Element.fill
|
||||
, Background.color Colors.selected
|
||||
, Border.color Colors.primary
|
||||
, Border.width 2
|
||||
]
|
||||
)
|
||||
|
||||
else
|
||||
( Just (Core.VideoClicked playlist video), [ Element.width Element.fill ] )
|
||||
|
||||
label =
|
||||
Element.row [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ Element.el [ Element.width (Element.fillPortion 2) ]
|
||||
(videoMiniature video)
|
||||
, Element.el [ Element.width (Element.fillPortion 3), Element.paddingXY 0 10, Element.alignTop ]
|
||||
(videoDescription zone video)
|
||||
]
|
||||
in
|
||||
Input.button attr { label = label, onPress = msg }
|
||||
|
||||
|
||||
videoView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||
videoView zone playlist video =
|
||||
Element.row [ Element.padding 10, Element.width Element.fill, Element.spacing 10 ]
|
||||
[ Element.column
|
||||
[ Element.width (Element.fillPortion 2)
|
||||
, Element.spacing 10
|
||||
, Element.alignTop
|
||||
]
|
||||
[ Keyed.el
|
||||
[ Element.width Element.fill
|
||||
, Element.height (Element.px 0)
|
||||
, Element.htmlAttribute (Html.Attributes.style "padding-top" "56.25%")
|
||||
, Element.htmlAttribute (Html.Attributes.style "position" "relative")
|
||||
]
|
||||
( video.url
|
||||
, Element.html
|
||||
(Html.video
|
||||
[ Html.Attributes.id (Twitch.videoId video)
|
||||
, Html.Attributes.class "video-js"
|
||||
, Html.Attributes.class "vjs-default-skin"
|
||||
, Html.Attributes.class "wf"
|
||||
, Html.Attributes.property "data-setup" (Encode.string "{\"fluid\": true}")
|
||||
, Html.Attributes.style "position" "absolute"
|
||||
, Html.Attributes.style "top" "0"
|
||||
, Html.Attributes.style "height" "100%"
|
||||
, Html.Attributes.controls True
|
||||
, Html.Attributes.autoplay True
|
||||
]
|
||||
[]
|
||||
)
|
||||
)
|
||||
, Element.paragraph
|
||||
[ Font.size Consts.homeFontSize
|
||||
, Font.bold
|
||||
, Element.paddingEach { top = 10, left = 0, bottom = 0, right = 0 }
|
||||
]
|
||||
[ Element.text video.name ]
|
||||
, case video.date of
|
||||
Just date ->
|
||||
Element.paragraph
|
||||
[ Font.size Consts.titleFontSize ]
|
||||
[ Element.text ("Diffusé le " ++ formatDate zone date) ]
|
||||
|
||||
_ ->
|
||||
Element.none
|
||||
]
|
||||
, Element.column [ Element.alignTop, Element.spacing 10, Element.width (Element.fillPortion 1) ]
|
||||
(List.map (videoInList zone playlist video) playlist.videos)
|
||||
]
|
||||
|
||||
|
||||
videoDescription : Time.Zone -> Twitch.Video -> Element Core.Msg
|
||||
videoDescription zone video =
|
||||
Element.column [ Element.spacing 10 ]
|
||||
[ Element.paragraph
|
||||
[ Font.bold
|
||||
, Font.size Consts.titleFontSize
|
||||
]
|
||||
[ Element.text video.name ]
|
||||
, case video.date of
|
||||
Just date ->
|
||||
Element.paragraph
|
||||
[ Font.color Colors.greyFont
|
||||
]
|
||||
[ Element.text ("Diffusé le " ++ formatDate zone date) ]
|
||||
|
||||
_ ->
|
||||
Element.none
|
||||
]
|
||||
|
||||
|
||||
formatDate : Time.Zone -> Time.Posix -> String
|
||||
formatDate zone time =
|
||||
let
|
||||
day =
|
||||
Time.toDay zone time |> String.fromInt |> TimeUtils.pad2
|
||||
|
||||
month =
|
||||
Time.toMonth zone time |> TimeUtils.monthToString
|
||||
|
||||
year =
|
||||
Time.toYear zone time |> String.fromInt |> TimeUtils.pad2
|
||||
|
||||
hours =
|
||||
Time.toHour zone time |> String.fromInt
|
||||
|
||||
minutes =
|
||||
Time.toMinute zone time |> String.fromInt |> TimeUtils.pad2
|
||||
in
|
||||
day ++ "/" ++ month ++ "/" ++ year ++ " à " ++ hours ++ "h" ++ minutes
|
||||
|
||||
|
||||
formatTime : Int -> String
|
||||
formatTime time =
|
||||
let
|
||||
hours =
|
||||
toHours time
|
||||
|
||||
minutes =
|
||||
toMinutes time
|
||||
|
||||
seconds =
|
||||
toSeconds time
|
||||
|
||||
hoursString =
|
||||
String.fromInt hours
|
||||
|
||||
minutesString =
|
||||
if minutes < 10 then
|
||||
"0" ++ String.fromInt minutes
|
||||
|
||||
else
|
||||
String.fromInt minutes
|
||||
|
||||
secondsString =
|
||||
if seconds < 10 then
|
||||
"0" ++ String.fromInt seconds
|
||||
|
||||
else
|
||||
String.fromInt seconds
|
||||
in
|
||||
hoursString
|
||||
++ ":"
|
||||
++ minutesString
|
||||
++ ":"
|
||||
++ secondsString
|
||||
|
||||
|
||||
toHours : Int -> Int
|
||||
toHours i =
|
||||
i // 3600
|
||||
|
||||
|
||||
toMinutes : Int -> Int
|
||||
toMinutes i =
|
||||
modBy 3600 i // 60
|
||||
|
||||
|
||||
toSeconds : Int -> Int
|
||||
toSeconds i =
|
||||
modBy 60 i
|
||||
|
||||
|
||||
group : Int -> List a -> List (List (Maybe a))
|
||||
group size list =
|
||||
let
|
||||
grouped =
|
||||
List.map (List.map Just) (groupAux size list [])
|
||||
|
||||
groupedRev =
|
||||
List.reverse grouped
|
||||
|
||||
( firstFixed, tail ) =
|
||||
case groupedRev of
|
||||
h :: t ->
|
||||
( List.concat [ h, List.repeat (size - List.length h) Nothing ], t )
|
||||
|
||||
[] ->
|
||||
( [], [] )
|
||||
|
||||
fixed =
|
||||
(firstFixed :: tail) |> List.reverse
|
||||
in
|
||||
fixed
|
||||
|
||||
|
||||
groupAux : Int -> List a -> List (List a) -> List (List a)
|
||||
groupAux size list acc =
|
||||
if List.isEmpty list then
|
||||
List.reverse acc
|
||||
|
||||
else
|
||||
let
|
||||
groupHead =
|
||||
List.take size list
|
||||
|
||||
groupTail =
|
||||
List.drop size list
|
||||
in
|
||||
groupAux size groupTail (groupHead :: acc)
|
Loading…
Reference in New Issue