Playlists start working
This commit is contained in:
parent
34f7ddb015
commit
72fe2c7c17
4
elm.json
4
elm.json
|
@ -6,11 +6,13 @@
|
|||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"STTR13/ziplist": "1.3.0",
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/http": "2.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"jims/html-parser": "1.0.0",
|
||||
"mdgriffith/elm-ui": "1.1.8"
|
||||
|
@ -19,8 +21,8 @@
|
|||
"elm/bytes": "1.0.8",
|
||||
"elm/file": "1.0.5",
|
||||
"elm/parser": "1.1.0",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2",
|
||||
"elm-community/list-extra": "8.2.4",
|
||||
"rtfeldman/elm-hex": "1.0.0"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
|
@ -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
|
33
src/Core.elm
33
src/Core.elm
|
@ -1,4 +1,4 @@
|
|||
module Core exposing (Model(..), Msg(..), init)
|
||||
module Core exposing (FullModel(..), Model, Msg(..), Page(..), init, update)
|
||||
|
||||
import Browser.Navigation
|
||||
import Json.Decode as Decode
|
||||
|
@ -7,17 +7,38 @@ import Twitch
|
|||
import Url
|
||||
|
||||
|
||||
type Model
|
||||
type FullModel
|
||||
= Unloaded
|
||||
| Loaded Model
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ playlists : List Twitch.Playlist
|
||||
, page : Page
|
||||
}
|
||||
|
||||
|
||||
type Page
|
||||
= Home
|
||||
|
||||
|
||||
type Msg
|
||||
= 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 _ _ _ =
|
||||
( Home
|
||||
, Task.perform ReceivedPlaylists Twitch.fetchPlaylists
|
||||
( Unloaded
|
||||
, 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 )
|
||||
|
|
|
@ -3,16 +3,15 @@ module Main exposing (main)
|
|||
import Browser
|
||||
import Core
|
||||
import Json.Decode as Decode
|
||||
import Updates
|
||||
import Url
|
||||
import Views
|
||||
|
||||
|
||||
main : Program Decode.Value Core.Model Core.Msg
|
||||
main : Program Decode.Value Core.FullModel Core.Msg
|
||||
main =
|
||||
Browser.application
|
||||
{ init = Core.init
|
||||
, update = Updates.update
|
||||
, update = Core.update
|
||||
, view = Views.view
|
||||
, subscriptions = \_ -> Sub.none
|
||||
, onUrlChange = onUrlChange
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
module Twitch exposing (Playlist, Video, fetchPlaylists)
|
||||
module Twitch exposing
|
||||
( Playlist
|
||||
, Video
|
||||
, fetchPlaylists
|
||||
, playlistMiniatureUrl
|
||||
)
|
||||
|
||||
import Html.Parser
|
||||
import Http
|
||||
|
@ -7,7 +12,8 @@ import Task exposing (Task)
|
|||
|
||||
|
||||
type alias Playlist =
|
||||
{ name : String
|
||||
{ url : String
|
||||
, name : String
|
||||
, videos : List Video
|
||||
}
|
||||
|
||||
|
@ -15,7 +21,7 @@ type alias Playlist =
|
|||
type alias Video =
|
||||
{ name : 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 =
|
||||
fetchPlaylistPath |> Task.andThen fetchPlaylistsMapper
|
||||
|
@ -40,25 +56,18 @@ fetchPlaylistPath : Task x (List String)
|
|||
fetchPlaylistPath =
|
||||
get
|
||||
{ url = "/videos"
|
||||
, resolver = Http.stringResolver parsePlaylistPath
|
||||
, resolver = Http.stringResolver parseHrefs
|
||||
}
|
||||
|
||||
|
||||
fetchPlaylist : String -> Task x Playlist
|
||||
fetchPlaylist name =
|
||||
get
|
||||
{ url = "/videos/" ++ name ++ "/description.json"
|
||||
, resolver = Http.stringResolver parsePlaylistName
|
||||
}
|
||||
fetchPlaylistName name
|
||||
|> Task.andThen
|
||||
(\a ->
|
||||
Task.map (\b -> Playlist a b)
|
||||
(get
|
||||
{ url = "/videos/" ++ name
|
||||
, resolver = Http.stringResolver parsePlaylistVideoPaths
|
||||
}
|
||||
|> Task.andThen (\c -> fetchVideos name c)
|
||||
)
|
||||
fetchPlaylistVideoPaths name
|
||||
|> Task.andThen (\c -> fetchVideos name c)
|
||||
|> Task.map (Playlist name a)
|
||||
)
|
||||
|
||||
|
||||
|
@ -69,7 +78,7 @@ fetchVideo playlist video =
|
|||
"/videos/" ++ playlist ++ video
|
||||
in
|
||||
get
|
||||
{ url = "/videos/" ++ playlist ++ "/" ++ video ++ "/description.json"
|
||||
{ url = url ++ "description.json"
|
||||
, resolver = Http.stringResolver (parseVideo url)
|
||||
}
|
||||
|
||||
|
@ -79,6 +88,22 @@ fetchVideos 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 url result =
|
||||
case result of
|
||||
|
@ -114,37 +139,8 @@ parsePlaylistName result =
|
|||
Ok ""
|
||||
|
||||
|
||||
parsePlaylistVideoPaths : Http.Response String -> Result x (List String)
|
||||
parsePlaylistVideoPaths 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 =
|
||||
parseHrefs : Http.Response String -> Result x (List String)
|
||||
parseHrefs result =
|
||||
case result of
|
||||
Http.GoodStatus_ _ content ->
|
||||
let
|
||||
|
@ -211,4 +207,4 @@ decodeVideo : String -> Decode.Decoder Video
|
|||
decodeVideo url =
|
||||
Decode.map2 (\x y -> Video x url y)
|
||||
(Decode.field "title" Decode.string)
|
||||
(Decode.field "duration" Decode.float)
|
||||
(Decode.map Basics.round (Decode.field "duration" Decode.float))
|
||||
|
|
|
@ -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 )
|
193
src/Views.elm
193
src/Views.elm
|
@ -1,12 +1,197 @@
|
|||
module Views exposing (view)
|
||||
|
||||
import Browser
|
||||
import Colors
|
||||
import Consts
|
||||
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 =
|
||||
{ title = "twitch.tforgione.fr"
|
||||
, body = [ Element.layout [] (Element.text "sup") ]
|
||||
{ title = Consts.url
|
||||
, 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)
|
||||
|
|
Loading…
Reference in New Issue