Playlists start working

This commit is contained in:
Thomas Forgione 2020-10-04 13:15:57 +02:00
parent 34f7ddb015
commit 72fe2c7c17
8 changed files with 314 additions and 79 deletions

View File

@ -6,11 +6,13 @@
"elm-version": "0.19.1", "elm-version": "0.19.1",
"dependencies": { "dependencies": {
"direct": { "direct": {
"STTR13/ziplist": "1.3.0",
"elm/browser": "1.0.2", "elm/browser": "1.0.2",
"elm/core": "1.0.5", "elm/core": "1.0.5",
"elm/html": "1.0.0", "elm/html": "1.0.0",
"elm/http": "2.0.0", "elm/http": "2.0.0",
"elm/json": "1.1.3", "elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0", "elm/url": "1.0.0",
"jims/html-parser": "1.0.0", "jims/html-parser": "1.0.0",
"mdgriffith/elm-ui": "1.1.8" "mdgriffith/elm-ui": "1.1.8"
@ -19,8 +21,8 @@
"elm/bytes": "1.0.8", "elm/bytes": "1.0.8",
"elm/file": "1.0.5", "elm/file": "1.0.5",
"elm/parser": "1.1.0", "elm/parser": "1.1.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2", "elm/virtual-dom": "1.0.2",
"elm-community/list-extra": "8.2.4",
"rtfeldman/elm-hex": "1.0.0" "rtfeldman/elm-hex": "1.0.0"
} }
}, },

28
src/Colors.elm Normal file
View File

@ -0,0 +1,28 @@
module Colors exposing (blackFont, greyBackground, primary, primaryOver, white)
import Element
primary : Element.Color
primary =
Element.rgb255 50 115 220
primaryOver : Element.Color
primaryOver =
Element.rgb255 35 102 209
white : Element.Color
white =
Element.rgb255 255 255 255
greyBackground : Element.Color
greyBackground =
Element.rgba255 0 0 0 0.7
blackFont : Element.Color
blackFont =
Element.rgb255 54 54 54

21
src/Consts.elm Normal file
View File

@ -0,0 +1,21 @@
module Consts exposing (homeFontSize, homePadding, name, url)
url : String
url =
"twitch.tforgione.fr"
name : String
name =
url
homeFontSize : Int
homeFontSize =
25
homePadding : Int
homePadding =
15

View File

@ -1,4 +1,4 @@
module Core exposing (Model(..), Msg(..), init) module Core exposing (FullModel(..), Model, Msg(..), Page(..), init, update)
import Browser.Navigation import Browser.Navigation
import Json.Decode as Decode import Json.Decode as Decode
@ -7,17 +7,38 @@ import Twitch
import Url import Url
type Model type FullModel
= Unloaded
| Loaded Model
type alias Model =
{ playlists : List Twitch.Playlist
, page : Page
}
type Page
= Home = Home
type Msg type Msg
= Noop = Noop
| ReceivedPlaylists (List Twitch.Playlist) | PlaylistsReceived (List Twitch.Playlist)
init : Decode.Value -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg ) init : Decode.Value -> Url.Url -> Browser.Navigation.Key -> ( FullModel, Cmd Msg )
init _ _ _ = init _ _ _ =
( Home ( Unloaded
, Task.perform ReceivedPlaylists Twitch.fetchPlaylists , Task.perform PlaylistsReceived Twitch.fetchPlaylists
) )
update : Msg -> FullModel -> ( FullModel, Cmd Msg )
update msg model =
case msg of
Noop ->
( model, Cmd.none )
PlaylistsReceived playlists ->
( Loaded { playlists = playlists, page = Home }, Cmd.none )

View File

@ -3,16 +3,15 @@ module Main exposing (main)
import Browser import Browser
import Core import Core
import Json.Decode as Decode import Json.Decode as Decode
import Updates
import Url import Url
import Views import Views
main : Program Decode.Value Core.Model Core.Msg main : Program Decode.Value Core.FullModel Core.Msg
main = main =
Browser.application Browser.application
{ init = Core.init { init = Core.init
, update = Updates.update , update = Core.update
, view = Views.view , view = Views.view
, subscriptions = \_ -> Sub.none , subscriptions = \_ -> Sub.none
, onUrlChange = onUrlChange , onUrlChange = onUrlChange

View File

@ -1,4 +1,9 @@
module Twitch exposing (Playlist, Video, fetchPlaylists) module Twitch exposing
( Playlist
, Video
, fetchPlaylists
, playlistMiniatureUrl
)
import Html.Parser import Html.Parser
import Http import Http
@ -7,7 +12,8 @@ import Task exposing (Task)
type alias Playlist = type alias Playlist =
{ name : String { url : String
, name : String
, videos : List Video , videos : List Video
} }
@ -15,7 +21,7 @@ type alias Playlist =
type alias Video = type alias Video =
{ name : String { name : String
, url : String , url : String
, duration : Float , duration : Int
} }
@ -31,6 +37,16 @@ get { url, resolver } =
} }
playlistMiniatureUrl : Playlist -> String
playlistMiniatureUrl playlist =
case List.head playlist.videos of
Just v ->
v.url ++ "miniature-050.png"
_ ->
""
fetchPlaylists : Task x (List Playlist) fetchPlaylists : Task x (List Playlist)
fetchPlaylists = fetchPlaylists =
fetchPlaylistPath |> Task.andThen fetchPlaylistsMapper fetchPlaylistPath |> Task.andThen fetchPlaylistsMapper
@ -40,25 +56,18 @@ fetchPlaylistPath : Task x (List String)
fetchPlaylistPath = fetchPlaylistPath =
get get
{ url = "/videos" { url = "/videos"
, resolver = Http.stringResolver parsePlaylistPath , resolver = Http.stringResolver parseHrefs
} }
fetchPlaylist : String -> Task x Playlist fetchPlaylist : String -> Task x Playlist
fetchPlaylist name = fetchPlaylist name =
get fetchPlaylistName name
{ url = "/videos/" ++ name ++ "/description.json"
, resolver = Http.stringResolver parsePlaylistName
}
|> Task.andThen |> Task.andThen
(\a -> (\a ->
Task.map (\b -> Playlist a b) fetchPlaylistVideoPaths name
(get |> Task.andThen (\c -> fetchVideos name c)
{ url = "/videos/" ++ name |> Task.map (Playlist name a)
, resolver = Http.stringResolver parsePlaylistVideoPaths
}
|> Task.andThen (\c -> fetchVideos name c)
)
) )
@ -69,7 +78,7 @@ fetchVideo playlist video =
"/videos/" ++ playlist ++ video "/videos/" ++ playlist ++ video
in in
get get
{ url = "/videos/" ++ playlist ++ "/" ++ video ++ "/description.json" { url = url ++ "description.json"
, resolver = Http.stringResolver (parseVideo url) , resolver = Http.stringResolver (parseVideo url)
} }
@ -79,6 +88,22 @@ fetchVideos playlist videos =
Task.sequence (List.map (fetchVideo playlist) videos) Task.sequence (List.map (fetchVideo playlist) videos)
fetchPlaylistName : String -> Task x String
fetchPlaylistName path =
get
{ url = "/videos/" ++ path ++ "description.json"
, resolver = Http.stringResolver parsePlaylistName
}
fetchPlaylistVideoPaths : String -> Task x (List String)
fetchPlaylistVideoPaths name =
get
{ url = "/videos/" ++ name
, resolver = Http.stringResolver parseHrefs
}
parseVideo : String -> Http.Response String -> Result x Video parseVideo : String -> Http.Response String -> Result x Video
parseVideo url result = parseVideo url result =
case result of case result of
@ -114,37 +139,8 @@ parsePlaylistName result =
Ok "" Ok ""
parsePlaylistVideoPaths : Http.Response String -> Result x (List String) parseHrefs : Http.Response String -> Result x (List String)
parsePlaylistVideoPaths result = parseHrefs result =
case result of
Http.GoodStatus_ _ content ->
let
withoutDoctype =
if String.startsWith "<!doctype" (String.toLower content) then
String.lines content |> List.drop 1 |> String.join "\n"
else
content
decoded =
Html.Parser.run withoutDoctype
hrefs =
Result.map findHrefs decoded
in
case hrefs of
Ok h ->
Ok (List.filter (String.endsWith "/") h)
_ ->
Ok []
_ ->
Ok []
parsePlaylistPath : Http.Response String -> Result x (List String)
parsePlaylistPath result =
case result of case result of
Http.GoodStatus_ _ content -> Http.GoodStatus_ _ content ->
let let
@ -211,4 +207,4 @@ decodeVideo : String -> Decode.Decoder Video
decodeVideo url = decodeVideo url =
Decode.map2 (\x y -> Video x url y) Decode.map2 (\x y -> Video x url y)
(Decode.field "title" Decode.string) (Decode.field "title" Decode.string)
(Decode.field "duration" Decode.float) (Decode.map Basics.round (Decode.field "duration" Decode.float))

View File

@ -1,17 +0,0 @@
module Updates exposing (update)
import Core
update : Core.Msg -> Core.Model -> ( Core.Model, Cmd Core.Msg )
update msg model =
case msg of
Core.Noop ->
( model, Cmd.none )
Core.ReceivedPlaylists playlists ->
let
_ =
Debug.log "p" playlists
in
( model, Cmd.none )

View File

@ -1,12 +1,197 @@
module Views exposing (view) module Views exposing (view)
import Browser import Browser
import Colors
import Consts
import Core import Core
import Element 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 Twitch
view : Core.Model -> Browser.Document Core.Msg view : Core.FullModel -> Browser.Document Core.Msg
view model = view model =
{ title = "twitch.tforgione.fr" { title = Consts.url
, body = [ Element.layout [] (Element.text "sup") ] , body = [ Element.layout [] (viewContent model) ]
} }
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
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 = Nothing
}
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.el
[ Element.alignBottom
, Element.alignRight
, Background.color Colors.greyBackground
, Border.rounded 5
, Element.padding 5
, Font.color Colors.white
]
(Element.text label)
|> 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.color Colors.blackFont ] [ Element.text playlist.name ]
]
button =
Input.button [ Element.width Element.fill, Element.alignTop ]
{ label = display
, onPress = Nothing
}
in
button
formatTime : Int -> String
formatTime time =
String.fromInt (toHours time)
++ ":"
++ String.fromInt (toMinutes time)
++ ":"
++ String.fromInt (toSeconds time)
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
acc
else
let
groupHead =
List.take size list
groupTail =
List.drop size list
in
groupAux size groupTail (groupHead :: acc)