Compare commits

..

No commits in common. "master" and "indexify" have entirely different histories.

20 changed files with 2480 additions and 667 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
videos videos
elm-stuff elm-stuff
js js/main.js
js/main.tmp.js
js/main.min.js
deploy.sh deploy.sh

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "elm-video"]
path = elm-video
url = https://github.com/polymny/elm-video

View File

@ -18,13 +18,13 @@ dev: js/main.js
release: js/main.min.js release: js/main.min.js
js/main.js: src/** elm-video/src/** js/main.js: src/**
$(ELM) make src/Main.elm --output $(BUILD_DIR)/main.js $(ELM) make src/Main.elm --output $(BUILD_DIR)/main.js
js/main.min.js: js/main.tmp.js js/main.min.js: js/main.tmp.js
@$(UGLIFYJS) $(BUILD_DIR)/main.tmp.js --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle > $(BUILD_DIR)/main.min.js @$(UGLIFYJS) $(BUILD_DIR)/main.tmp.js --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle > $(BUILD_DIR)/main.min.js
js/main.tmp.js: src/** elm-video/src/** js/main.tmp.js: src/**
@$(ELM) make src/Main.elm --optimize --output $(BUILD_DIR)/main.tmp.js @$(ELM) make src/Main.elm --optimize --output $(BUILD_DIR)/main.tmp.js
watch: watch:

78
css/spinner.css Normal file
View File

@ -0,0 +1,78 @@
.lds-spinner {
color: official;
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-spinner div {
transform-origin: 40px 40px;
animation: lds-spinner 1.2s linear infinite;
}
.lds-spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: #cef;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

1664
css/video-js.css Normal file

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
Subproject commit 087c3d594b62b6617954fc31a58576f12752d75a

View File

@ -1,29 +1,24 @@
{ {
"type": "application", "type": "application",
"source-directories": [ "source-directories": [
"src", "src"
"elm-video/src"
], ],
"elm-version": "0.19.1", "elm-version": "0.19.1",
"dependencies": { "dependencies": {
"direct": { "direct": {
"andrewMacmurray/elm-simple-animation": "2.1.0", "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/svg": "1.0.1",
"elm/time": "1.0.0", "elm/time": "1.0.0",
"elm/url": "1.0.0", "elm/url": "1.0.0",
"icidasset/elm-material-icons": "9.0.0",
"jims/html-parser": "1.0.0", "jims/html-parser": "1.0.0",
"justinmimbs/timezone-data": "3.0.3",
"mdgriffith/elm-ui": "1.1.8", "mdgriffith/elm-ui": "1.1.8",
"rtfeldman/elm-iso8601-date-strings": "1.1.3" "rtfeldman/elm-iso8601-date-strings": "1.1.3"
}, },
"indirect": { "indirect": {
"avh4/elm-color": "1.0.0",
"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",

View File

@ -1,24 +0,0 @@
<!doctype HTML>
<html>
<head>
<title>twitch.tforgione.fr</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/ico" href="favicon.ico"/>
</head>
<body>
<div id="container"></div>
<script src="js/polymny-video-full.min.js"></script>
<script src="js/main.js"></script>
<script>
PolymnyVideo.fullpage({
node: document.getElementById('container'),
url: "videos/" + PolymnyVideo.getArgumentFromUrl("v") + "/manifest.m3u8",
autoplay: true,
startTime: PolymnyVideo.getArgumentFromUrl("t"),
enableMiniatures: true,
});
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -4,65 +4,46 @@
<title>twitch.tforgione.fr</title> <title>twitch.tforgione.fr</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/ico" href="favicon.ico"/> <link href="css/video-js.css" rel="stylesheet">
<link href="css/spinner.css" rel="stylesheet">
</head> </head>
<body> <body>
<div id="container"></div> <div id="container"></div>
<script src="js/polymny-video-elm.min.js"></script> <script src="js/vd.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script> <script>
function isDarkMode(e) {
var darkMode = JSON.parse(localStorage.getItem('darkMode'));
if (darkMode === null) {
if (e === undefined) {
e = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
}
darkMode = e.matches === true;
}
return darkMode;
}
var app = Elm.Main.init({ var app = Elm.Main.init({
node: document.getElementById('container'), node: document.getElementById('container'),
flags: { flags: { width: window.innerWidth, height: window.innerHeight }
width: window.innerWidth,
height: window.innerHeight,
darkMode: isDarkMode(),
darkSetting: JSON.parse(localStorage.getItem('darkMode'))
}
}); });
PolymnyVideo.setup(app); var lastId, player;
if (app.ports !== undefined) { if (app.ports !== undefined && app.ports.registerVideo !== undefined) {
if (app.ports.setDarkMode !== undefined) { app.ports.registerVideo.subscribe(function(args) {
app.ports.setDarkMode.subscribe(function(arg) { var time = parseInt(args[2], 10) || undefined;
if (arg === null) {
localStorage.removeItem('darkMode'); requestAnimationFrame(function() {
} else { if (args[0] !== lastId) {
localStorage.setItem('darkMode', arg); lastId = args[0];
player = vd.setup(args[0], {
v: args[1] + "/manifest.mpd",
t: parseInt(args[2], 10) || 0,
focus: true
});
} else if (time !== undefined ){
player.currentTime(time);
} }
app.ports.darkMode.send(isDarkMode());
}); });
}
}
if (app.ports !== undefined) {
if (app.ports.eraseVideo !== undefined) {
app.ports.eraseVideo.subscribe(function() {
window.scrollTo(0, 0);
lastId = undefined;
});
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
app.ports.darkMode.send(isDarkMode(e));
}); });
} }
if (app.ports !== undefined && app.ports.eraseVideo !== undefined) {
app.ports.eraseVideo.subscribe(function() {
lastId = undefined;
});
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -2,25 +2,16 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const VIDEO_DIR = "videos"; const VIDEO_DIR = "videos";
const DESCRIPTION_FILE = "description.json"; const DESCRIPTION_FILE = "description.json";
const INDEX_FILE = "index.json";
function isDirectory(path) {
return fs.lstatSync(path).isDirectory();
}
let info = []; let info = [];
for (let dir of fs.readdirSync(VIDEO_DIR)) { for (let dir of fs.readdirSync(VIDEO_DIR)) {
if (!isDirectory(path.join(VIDEO_DIR, dir))) {
continue;
}
let description = JSON.parse(fs.readFileSync(path.join(VIDEO_DIR, dir, DESCRIPTION_FILE))); let description = JSON.parse(fs.readFileSync(path.join(VIDEO_DIR, dir, DESCRIPTION_FILE)));
description.url = dir + "/"; description.url = dir + "/";
description.videos = []; description.videos = [];
for (let subdir of fs.readdirSync(path.join(VIDEO_DIR, dir))) { for (let subdir of fs.readdirSync(path.join(VIDEO_DIR, dir))) {
if (!isDirectory(path.join(VIDEO_DIR, dir, subdir))) { if (subdir === DESCRIPTION_FILE) {
continue; continue;
} }
@ -33,5 +24,5 @@ for (let dir of fs.readdirSync(VIDEO_DIR)) {
info.push(description); info.push(description);
} }
fs.writeFileSync(path.join(VIDEO_DIR, INDEX_FILE), JSON.stringify(info)); fs.writeFileSync(path.join(VIDEO_DIR, 'index.json'), JSON.stringify(info));

449
js/vd.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
module Colors exposing (..) module Colors exposing (blackFont, greyBackground, greyFont, primary, primaryOver, selected, white)
import Element import Element
@ -18,47 +18,21 @@ white =
Element.rgb255 255 255 255 Element.rgb255 255 255 255
red : Element.Color
red =
Element.rgb255 255 0 0
greyBackground : Element.Color greyBackground : Element.Color
greyBackground = greyBackground =
Element.rgba255 0 0 0 0.7 Element.rgba255 0 0 0 0.7
background : Bool -> Element.Color blackFont : Element.Color
background darkMode = blackFont =
if darkMode then Element.rgb255 54 54 54
Element.rgb255 47 49 54
else
Element.rgb255 245 245 245
font : Bool -> Element.Color greyFont : Element.Color
font darkMode = greyFont =
if darkMode then Element.rgb255 128 128 128
Element.rgb255 245 245 245
else
Element.rgb255 54 57 63
detailFont : Bool -> Element.Color selected : Element.Color
detailFont darkMode = selected =
if darkMode then Element.rgb255 223 233 250
Element.rgb255 160 160 160
else
Element.rgb255 128 128 128
selected : Bool -> Element.Color
selected darkMode =
if darkMode then
Element.rgb255 64 68 75
else
Element.rgb255 223 233 250

View File

@ -1,20 +1,19 @@
module Core exposing (Model, Msg(..), Page(..), init, subscriptions, update) module Core exposing (FullModel(..), Model, Msg(..), Page(..), init, subscriptions, update)
import Browser
import Browser.Events as Events import Browser.Events as Events
import Browser.Navigation as Nav import Browser.Navigation as Nav
import Dict exposing (Dict) import Dict exposing (Dict)
import Element import Element
import Hover exposing (Hover)
import Http import Http
import Ports import Ports
import Task
import Time import Time
import TimeZone
import Twitch import Twitch
import Url import Url
import Video
import Video.Events
type FullModel
= Unloaded Element.Device Url.Url Nav.Key
| Loaded Model
type alias Model = type alias Model =
@ -23,59 +22,29 @@ type alias Model =
, page : Page , page : Page
, key : Nav.Key , key : Nav.Key
, device : Element.Device , device : Element.Device
, time : Time.Posix
, currentDate : Time.Posix
, url : Url.Url
, darkMode : Bool
, darkSetting : Maybe Bool
} }
type Page type Page
= Home (Maybe (Hover Twitch.Playlist)) = Home
| Playlist Twitch.Playlist (Maybe (Hover Twitch.Video)) | Playlist Twitch.Playlist
| Video Twitch.Playlist Twitch.Video Video.Video (Maybe (Hover Twitch.Video)) | Video Twitch.Playlist Twitch.Video
type Msg type Msg
= Noop = Noop
| PlaylistsReceived (List Twitch.Playlist) | PlaylistsReceived ( List Twitch.Playlist, Time.Zone )
| HomeClicked | HomeClicked
| PlaylistClicked Twitch.Playlist | PlaylistClicked Twitch.Playlist
| VideoClicked Twitch.Playlist Twitch.Video | VideoClicked Twitch.Playlist Twitch.Video
| UrlReceived Url.Url | UrlReceived Url.Url
| UrlRequested Browser.UrlRequest
| SizeReceived Int Int | SizeReceived Int Int
| TimeZoneReceivedResult (Result TimeZone.Error ( String, Time.Zone ))
| TimeZoneReceived Time.Zone
| HoverPlaylist Twitch.Playlist
| HoverVideo Twitch.Video
| Unhover
| TimeReceived Time.Posix
| CurrentDateReceived Time.Posix
| DarkMode Bool
| DarkModeClicked
| VideoMsg Video.Msg
init : { width : Int, height : Int, darkMode : Bool, darkSetting : Maybe Bool } -> Url.Url -> Nav.Key -> ( Model, Cmd Msg ) init : { width : Int, height : Int } -> Url.Url -> Nav.Key -> ( FullModel, Cmd Msg )
init { width, height, darkMode, darkSetting } url key = init { width, height } url key =
( Model ( Unloaded (Element.classifyDevice { width = width, height = height }) url key
[] , Twitch.fetchPlaylists resultToMsg
Time.utc
(Home Nothing)
key
(Element.classifyDevice { width = width, height = height })
(Time.millisToPosix 0)
(Time.millisToPosix 0)
url
darkMode
darkSetting
, Cmd.batch
[ Task.attempt TimeZoneReceivedResult TimeZone.getZone
, Task.perform CurrentDateReceived Time.now
, Twitch.fetchPlaylists resultToMsg
]
) )
@ -83,218 +52,122 @@ resultToMsg : Result Http.Error (List Twitch.Playlist) -> Msg
resultToMsg result = resultToMsg result =
case result of case result of
Ok o -> Ok o ->
PlaylistsReceived o PlaylistsReceived ( o, Time.utc )
Err _ -> Err _ ->
Noop Noop
subscriptions : Model -> Sub Msg subscriptions : FullModel -> Sub Msg
subscriptions model = subscriptions _ =
Sub.batch Events.onResize (\w h -> SizeReceived w h)
[ Events.onResize (\w h -> SizeReceived w h)
, Time.every 200 TimeReceived
, Ports.darkMode DarkMode
, Sub.map VideoMsg
(case model.page of
Video _ _ v _ ->
Video.Events.subs v
_ ->
Sub.none
)
]
update : Msg -> Model -> ( Model, Cmd Msg ) update : Msg -> FullModel -> ( FullModel, Cmd Msg )
update msg model = update msg model =
case msg of case ( msg, model ) of
Noop -> ( Noop, _ ) ->
( model, Cmd.none ) ( model, Cmd.none )
TimeZoneReceived z -> ( SizeReceived w h, Loaded m ) ->
( { model | zone = z }, Cmd.none ) ( Loaded { m | device = Element.classifyDevice { width = w, height = h } }
TimeZoneReceivedResult (Ok ( _, zone )) ->
( { model | zone = zone }, Cmd.none )
TimeZoneReceivedResult (Err _) ->
( model, Task.perform TimeZoneReceived Time.here )
TimeReceived p ->
( { model | time = p }, Cmd.none )
CurrentDateReceived d ->
( { model | currentDate = d }, Cmd.none )
HoverPlaylist hover ->
case model.page of
Home Nothing ->
( { model | page = Home (Just (Hover.hover hover model.time)) }, Cmd.none )
_ ->
( model, Cmd.none )
HoverVideo hover ->
case model.page of
Playlist p Nothing ->
( { model | page = Playlist p (Just (Hover.hover hover model.time)) }, Cmd.none )
Video p v x Nothing ->
( { model | page = Video p v x (Just (Hover.hover hover model.time)) }, Cmd.none )
_ ->
( model, Cmd.none )
Unhover ->
case model.page of
Home _ ->
( { model | page = Home Nothing }, Cmd.none )
Playlist p _ ->
( { model | page = Playlist p Nothing }, Cmd.none )
Video p v x _ ->
( { model | page = Video p v x Nothing }, Cmd.none )
SizeReceived w h ->
( { model | device = Element.classifyDevice { width = w, height = h } }
, Cmd.none , Cmd.none
) )
PlaylistsReceived playlists -> ( PlaylistsReceived ( playlists, zone ), Unloaded device url key ) ->
update update
(UrlReceived model.url) (UrlReceived url)
{ model | playlists = playlists, page = Home Nothing } (Loaded { key = key, playlists = playlists, zone = zone, page = Home, device = device })
HomeClicked -> ( HomeClicked, Loaded m ) ->
( model ( model
, Nav.pushUrl model.key "#" , Nav.pushUrl m.key "#"
) )
DarkModeClicked -> ( PlaylistClicked playlist, Loaded m ) ->
( model
, Nav.pushUrl m.key ("#" ++ playlist.url)
)
( VideoClicked playlist video, Loaded m ) ->
( model
, Nav.pushUrl m.key ("#" ++ playlist.url ++ video.url)
)
( UrlReceived url, Loaded m ) ->
let let
next = splits =
nextDarkSetting model.darkSetting String.split "?" (Maybe.withDefault "" url.fragment)
in
( { model | darkSetting = next }, Ports.setDarkMode next )
PlaylistClicked playlist -> ( split, args ) =
( model case splits of
, Nav.pushUrl model.key ("#" ++ playlist.url) h1 :: h2 :: _ ->
) ( String.split "/" h1, parseQueryString h2 )
VideoClicked playlist video -> h1 :: _ ->
( model ( String.split "/" h1, Dict.empty )
, Nav.pushUrl model.key ("#" ++ playlist.url ++ video.url)
)
UrlReceived url ->
if List.isEmpty model.playlists then
( { model | url = url }, Cmd.none )
else
let
splits =
String.split "?" (Maybe.withDefault "" url.fragment)
( split, args ) =
case splits of
h1 :: h2 :: _ ->
( String.split "/" h1, parseQueryString h2 )
h1 :: _ ->
( String.split "/" h1, Dict.empty )
_ ->
( [], Dict.empty )
time =
case Dict.get "t" args of
Just "0" ->
Nothing
Just t ->
Just t
_ ->
Nothing
( playlistName, videoName ) =
case split of
p :: v :: _ ->
( Just (p ++ "/"), Just (v ++ "/") )
p :: _ ->
( Just (p ++ "/"), Nothing )
_ ->
( Nothing, Nothing )
playlist =
List.head (List.filter (\x -> Just x.url == playlistName) model.playlists)
video =
case playlist of
Just p ->
List.head (List.filter (\x -> Just x.url == videoName) p.videos)
_ ->
Nothing
( page, cmd ) =
case ( playlist, video ) of
( Just p, Just v ) ->
let
( el, videoCommand ) =
Video.fromConfig
{ url = "/videos/" ++ p.url ++ v.url ++ "/manifest.m3u8"
, id = "video"
, autoplay = True
, enableMiniatures = True
, startTime = time
, customElement = Nothing
, live = Just False
, miniaturesUrl = Nothing
, muted = False
}
in
( Video p v el Nothing, Cmd.map VideoMsg videoCommand )
( Just p, Nothing ) ->
( Playlist p Nothing, Cmd.none )
_ ->
( Home Nothing, Cmd.none )
in
( { model | page = page }, cmd )
UrlRequested u ->
case u of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External s ->
( model, Nav.load s )
DarkMode dark ->
( { model | darkMode = dark }, Cmd.none )
VideoMsg vMsg ->
let
( newPage, newCommand ) =
case model.page of
Video a b v c ->
let
( newVideo, cmd ) =
Video.update vMsg v
in
( Video a b newVideo c, cmd )
_ -> _ ->
( model.page, Cmd.none ) ( [], Dict.empty )
time =
case Maybe.map String.toInt (Dict.get "t" args) of
Just (Just 0) ->
Nothing
Just (Just t) ->
Just t
_ ->
Nothing
( playlistName, videoName ) =
case split of
p :: v :: _ ->
( Just (p ++ "/"), Just (v ++ "/") )
p :: _ ->
( Just (p ++ "/"), Nothing )
_ ->
( Nothing, Nothing )
playlist =
List.head (List.filter (\x -> Just x.url == playlistName) m.playlists)
video =
case playlist of
Just p ->
List.head (List.filter (\x -> Just x.url == videoName) p.videos)
_ ->
Nothing
( page, cmd ) =
case ( playlist, video ) of
( Just p, Just v ) ->
( Video p v
, Ports.registerVideo ( Twitch.videoId v, "videos/" ++ p.url ++ v.url, time )
)
( Just p, Nothing ) ->
( Playlist p, Cmd.none )
_ ->
( Home, Cmd.none )
extraCmd =
case page of
Video _ _ ->
Cmd.none
_ ->
Ports.eraseVideo ()
in in
( { model | page = newPage }, Cmd.map VideoMsg newCommand ) ( Loaded { m | page = page }, Cmd.batch [ cmd, extraCmd ] )
_ ->
( model, Cmd.none )
splitter : String -> Maybe ( String, String ) splitter : String -> Maybe ( String, String )
@ -310,16 +183,3 @@ splitter input =
parseQueryString : String -> Dict String String parseQueryString : String -> Dict String String
parseQueryString input = parseQueryString input =
Dict.fromList (List.filterMap splitter (String.split "&" input)) Dict.fromList (List.filterMap splitter (String.split "&" input))
nextDarkSetting : Maybe Bool -> Maybe Bool
nextDarkSetting current =
case current of
Nothing ->
Just True
Just True ->
Just False
Just False ->
Nothing

View File

@ -1,14 +0,0 @@
module Hover exposing (..)
import Time
type alias Hover a =
{ element : a
, time : Time.Posix
}
hover : a -> Time.Posix -> Hover a
hover element time =
{ element = element, time = time }

View File

@ -5,7 +5,7 @@ import Core
import Views import Views
main : Program { width : Int, height : Int, darkMode : Bool, darkSetting : Maybe Bool } Core.Model Core.Msg main : Program { width : Int, height : Int } Core.FullModel Core.Msg
main = main =
Browser.application Browser.application
{ init = Core.init { init = Core.init
@ -13,5 +13,5 @@ main =
, view = Views.view , view = Views.view
, subscriptions = Core.subscriptions , subscriptions = Core.subscriptions
, onUrlChange = Core.UrlReceived , onUrlChange = Core.UrlReceived
, onUrlRequest = Core.UrlRequested , onUrlRequest = \_ -> Core.Noop
} }

View File

@ -1,7 +1,7 @@
port module Ports exposing (darkMode, setDarkMode) port module Ports exposing (eraseVideo, registerVideo)
port setDarkMode : Maybe Bool -> Cmd msg port registerVideo : ( String, String, Maybe Int ) -> Cmd msg
port darkMode : (Bool -> msg) -> Sub msg port eraseVideo : () -> Cmd msg

View File

@ -3,14 +3,12 @@ module Twitch exposing
, Video , Video
, decodePlaylists , decodePlaylists
, fetchPlaylists , fetchPlaylists
, playlistDate
, playlistMiniatureUrl , playlistMiniatureUrl
, videoId , videoId
, videoMiniatureUrl , videoMiniatureUrl
, videoName , videoName
) )
import Hover exposing (Hover)
import Http import Http
import Iso8601 import Iso8601
import Json.Decode as Decode import Json.Decode as Decode
@ -51,32 +49,7 @@ decodePlaylist =
decodePlaylists : Decode.Decoder (List Playlist) decodePlaylists : Decode.Decoder (List Playlist)
decodePlaylists = decodePlaylists =
Decode.map (sortPlaylists >> List.reverse) (Decode.list decodePlaylist) Decode.map (List.sortBy .url >> List.reverse) (Decode.list decodePlaylist)
mostRecentVideo : List Video -> Maybe Time.Posix
mostRecentVideo videos =
case ( videos, List.map .date videos ) of
( _, (Just t) :: _ ) ->
Just t
( _ :: t, Nothing :: _ ) ->
mostRecentVideo t
_ ->
Nothing
playlistDate : Playlist -> Int
playlistDate playlist =
mostRecentVideo playlist.videos
|> Maybe.map Time.posixToMillis
|> Maybe.withDefault 0
sortPlaylists : List Playlist -> List Playlist
sortPlaylists list =
List.sortBy playlistDate list
fetchPlaylists : (Result Http.Error (List Playlist) -> msg) -> Cmd msg fetchPlaylists : (Result Http.Error (List Playlist) -> msg) -> Cmd msg
@ -97,22 +70,9 @@ videoId video =
String.dropLeft 1 video.url |> String.replace "/" "-" String.dropLeft 1 video.url |> String.replace "/" "-"
playlistMiniatureUrl : Time.Posix -> Maybe (Hover Playlist) -> Playlist -> String playlistMiniatureUrl : Playlist -> String
playlistMiniatureUrl currentTime hover playlist = playlistMiniatureUrl playlist =
let case List.head (List.reverse playlist.videos) of
skip =
case hover of
Nothing ->
0
Just { element, time } ->
if element == playlist then
modBy (List.length playlist.videos) ((Time.posixToMillis currentTime - Time.posixToMillis time) // 1000)
else
0
in
case List.head (List.drop skip playlist.videos) of
Just v -> Just v ->
"videos/" ++ playlist.url ++ v.url ++ "miniature-050.png" "videos/" ++ playlist.url ++ v.url ++ "miniature-050.png"
@ -120,30 +80,6 @@ playlistMiniatureUrl currentTime hover playlist =
"" ""
videoMiniatureUrl : Time.Posix -> Maybe (Hover Video) -> Playlist -> Video -> String videoMiniatureUrl : Playlist -> Video -> String
videoMiniatureUrl currentTime hover playlist video = videoMiniatureUrl playlist video =
let "videos/" ++ playlist.url ++ video.url ++ "miniature-050.png"
index =
case hover of
Nothing ->
5
Just { element, time } ->
if element == video then
modBy 11 ((Time.posixToMillis currentTime - Time.posixToMillis time) // 1000)
else
5
indexString =
case index of
10 ->
"100"
0 ->
"000"
n ->
"0" ++ String.fromInt n ++ "0"
in
"videos/" ++ playlist.url ++ video.url ++ "miniature-" ++ indexString ++ ".png"

View File

@ -1,12 +0,0 @@
module Ui exposing (link)
import Element exposing (Element)
import Element.Input as Input
link : List (Element.Attribute msg) -> { label : Element msg, url : String } -> Element msg
link attr data =
Input.button attr
{ label = Element.link [ Element.width Element.fill, Element.height Element.fill ] data
, onPress = Nothing
}

View File

@ -7,128 +7,106 @@ import Core
import Element exposing (Element) import Element exposing (Element)
import Element.Background as Background import Element.Background as Background
import Element.Border as Border import Element.Border as Border
import Element.Events as Events
import Element.Font as Font import Element.Font as Font
import Element.Input as Input import Element.Input as Input
import Element.Keyed as Keyed import Element.Keyed as Keyed
import Hover exposing (Hover) import Html
import Html.Attributes import Html.Attributes
import Json.Encode as Encode
import Time import Time
import TimeUtils import TimeUtils
import Twitch import Twitch
import Ui
import Video
import Video.Views
view : Core.Model -> Browser.Document Core.Msg view : Core.FullModel -> Browser.Document Core.Msg
view model = view model =
let let
element = element =
case model.playlists of case model of
[] -> Core.Unloaded _ _ _ ->
Element.el [ Element.padding 10, Element.centerX ] spinner Element.el [ Element.padding 10, Element.centerX ] spinner
_ -> Core.Loaded m ->
viewContent model viewContent m
in in
{ title = title model { title = title model
, body = , body =
[ Element.layout [ Element.layout
[ Font.color (Colors.font model.darkMode) [ Font.color Colors.blackFont
, Background.color (Colors.background model.darkMode)
, Font.size Consts.normalFontSize , Font.size Consts.normalFontSize
, Font.family [ Font.typeface "Cantarell" ] , Font.family [ Font.typeface "Cantarell" ]
] ]
(Element.column (Element.column
[ Element.width Element.fill, Element.height Element.fill ] [ Element.width Element.fill, Element.height Element.fill ]
[ topBar model.darkSetting, element ] [ topBar, element ]
) )
] ]
} }
title : Core.Model -> String title : Core.FullModel -> String
title model = title model =
case model.page of case model of
Core.Home _ -> Core.Unloaded _ _ _ ->
Consts.url Consts.url
Core.Playlist p _ -> Core.Loaded m ->
Consts.url ++ " - " ++ p.name case m.page of
Core.Home ->
Consts.url
Core.Video p v _ _ -> Core.Playlist p ->
Consts.url ++ " - " ++ p.name ++ " - " ++ v.name Consts.url ++ " - " ++ p.name
Core.Video p v ->
Consts.url ++ " - " ++ p.name ++ " - " ++ v.name
viewContent : Core.Model -> Element Core.Msg viewContent : Core.Model -> Element Core.Msg
viewContent model = viewContent model =
case model.page of case model.page of
Core.Home hover -> Core.Home ->
playlistsView model.device model.playlists model.currentDate model.time hover playlistsView model.device model.playlists
Core.Playlist playlist hover -> Core.Playlist playlist ->
videoMiniaturesView model.darkMode model.device model.zone model.currentDate model.time hover playlist videoMiniaturesView model.device model.zone playlist
Core.Video playlist video v hover -> Core.Video playlist video ->
videoView model.darkMode model.device model.zone model.currentDate model.time hover playlist video v videoView model.device model.zone playlist video
topBar : Maybe Bool -> Element Core.Msg topBar : Element Core.Msg
topBar darkSetting = topBar =
Element.row Element.row
[ Element.width Element.fill [ Element.width Element.fill
, Background.color Colors.primary , Background.color Colors.primary
, Font.color Colors.white , Font.color Colors.white
, Font.size Consts.homeFontSize , Font.size Consts.homeFontSize
] ]
[ homeButton, mode darkSetting ] [ homeButton ]
mode : Maybe Bool -> Element Core.Msg
mode current =
let
( label, icon ) =
case current of
Nothing ->
( "Par défaut", "🌓" )
Just True ->
( "Mode nuit", "🌑" )
Just False ->
( "Mode jour", "🌕" )
in
Input.button
[ Element.height Element.fill, Element.alignRight, Element.padding 10 ]
{ label =
Element.row [ Element.spacing 5 ]
[ Element.el [ Font.size Consts.titleFontSize ] (Element.text label)
, Element.el [ Font.size Consts.homeFontSize ] (Element.text icon)
]
, onPress = Just Core.DarkModeClicked
}
homeButton : Element Core.Msg homeButton : Element Core.Msg
homeButton = homeButton =
Ui.link Input.button
[ Element.height Element.fill [ Element.padding Consts.homePadding
, Element.height Element.fill
, Element.mouseOver [ Background.color Colors.primaryOver ]
, Font.bold , Font.bold
] ]
{ label = Element.el [ Element.padding 10 ] (Element.text Consts.name) { label = Element.text Consts.name
, url = "/" , onPress = Just Core.HomeClicked
} }
playlistsView : Element.Device -> List Twitch.Playlist -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Playlist) -> Element Core.Msg playlistsView : Element.Device -> List Twitch.Playlist -> Element Core.Msg
playlistsView device playlists currentDate time hover = playlistsView device playlists =
let let
empty = empty =
Element.el [ Element.width Element.fill ] Element.none Element.el [ Element.width Element.fill ] Element.none
views = views =
List.map (playlistView currentDate time hover) playlists List.map playlistView playlists
grouped = grouped =
group (numberOfVideosPerRow device) views group (numberOfVideosPerRow device) views
@ -144,32 +122,19 @@ playlistsView device playlists currentDate time hover =
final final
playlistView : Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Playlist) -> Twitch.Playlist -> Element Core.Msg playlistView : Twitch.Playlist -> Element Core.Msg
playlistView currentDate time hover playlist = playlistView playlist =
let let
key =
Twitch.playlistMiniatureUrl time Nothing playlist
src = src =
Twitch.playlistMiniatureUrl time hover playlist Twitch.playlistMiniatureUrl playlist
image = image =
Keyed.el Keyed.el [ Element.width Element.fill, Element.height Element.fill ]
[ Element.width Element.fill ( src
, Element.height (Element.px 0)
, Element.htmlAttribute (Html.Attributes.style "padding-top" "56.25%")
, Element.htmlAttribute (Html.Attributes.style "position" "relative")
, Events.onMouseEnter (Core.HoverPlaylist playlist)
, Events.onMouseLeave Core.Unhover
]
( key
, Element.image , Element.image
[ Element.width Element.fill [ Element.width Element.fill
, Element.htmlAttribute (Html.Attributes.style "position" "absolute") , Element.height Element.fill
, Element.htmlAttribute (Html.Attributes.style "top" "0")
, Element.htmlAttribute (Html.Attributes.style "height" "100%")
, Element.inFront inFront , Element.inFront inFront
, Element.inFront new
] ]
{ description = "", src = src } { description = "", src = src }
) )
@ -201,13 +166,6 @@ playlistView currentDate time hover playlist =
, Element.padding 5 , Element.padding 5
] ]
new =
if Time.posixToMillis currentDate - Twitch.playlistDate playlist > week then
Element.none
else
newBadge
display = display =
Element.column [ Element.width Element.fill, Element.spacing 10 ] Element.column [ Element.width Element.fill, Element.spacing 10 ]
[ image [ image
@ -218,25 +176,21 @@ playlistView currentDate time hover playlist =
button = button =
Input.button [ Element.width Element.fill, Element.alignTop ] Input.button [ Element.width Element.fill, Element.alignTop ]
{ label = { label = display
Ui.link [ Element.width Element.fill, Element.alignTop ] , onPress = Just (Core.PlaylistClicked playlist)
{ label = display
, url = "/#" ++ playlist.url
}
, onPress = Nothing
} }
in in
button button
videoMiniaturesView : Bool -> Element.Device -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Element Core.Msg videoMiniaturesView : Element.Device -> Time.Zone -> Twitch.Playlist -> Element Core.Msg
videoMiniaturesView darkMode device zone currentDate time hover playlist = videoMiniaturesView device zone playlist =
let let
empty = empty =
Element.el [ Element.width Element.fill ] Element.none Element.el [ Element.width Element.fill ] Element.none
views = views =
List.map (videoMiniatureView darkMode zone currentDate time hover playlist) playlist.videos List.map (videoMiniatureView zone playlist) playlist.videos
grouped = grouped =
group (numberOfVideosPerRow device) views group (numberOfVideosPerRow device) views
@ -252,8 +206,8 @@ videoMiniaturesView darkMode device zone currentDate time hover playlist =
final final
videoMiniature : Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg videoMiniature : Twitch.Playlist -> Twitch.Video -> Element Core.Msg
videoMiniature currentDate time hover playlist video = videoMiniature playlist video =
let let
inFront = inFront =
Element.text label Element.text label
@ -269,39 +223,16 @@ videoMiniature currentDate time hover playlist video =
, Element.padding 5 , Element.padding 5
] ]
date =
video.date
|> Maybe.map Time.posixToMillis
|> Maybe.withDefault 0
new =
if Time.posixToMillis currentDate - date > week then
Element.none
else
newBadge
key =
Twitch.videoMiniatureUrl time Nothing playlist video
src = src =
Twitch.videoMiniatureUrl time hover playlist video Twitch.videoMiniatureUrl playlist video
image = image =
Keyed.el Keyed.el [ Element.width Element.fill, Element.height Element.fill ]
[ Element.width Element.fill ( src
, Element.height (Element.px 0)
, Element.htmlAttribute (Html.Attributes.style "padding-top" "56.25%")
, Element.htmlAttribute (Html.Attributes.style "position" "relative")
]
( key
, Element.image , Element.image
[ Element.width Element.fill [ Element.width Element.fill
, Element.htmlAttribute (Html.Attributes.style "position" "absolute") , Element.height Element.fill
, Element.htmlAttribute (Html.Attributes.style "top" "0")
, Element.htmlAttribute (Html.Attributes.style "height" "100%")
, Element.inFront inFront , Element.inFront inFront
, Element.inFront new
] ]
{ description = "", src = src } { description = "", src = src }
) )
@ -312,63 +243,53 @@ videoMiniature currentDate time hover playlist video =
image image
videoMiniatureView : Bool -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg videoMiniatureView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
videoMiniatureView darkMode zone currentDate time hover playlist video = videoMiniatureView zone playlist video =
let let
display = display =
Element.column Element.column [ Element.width Element.fill, Element.spacing 10 ]
[ Element.width Element.fill [ videoMiniature playlist video
, Element.spacing 10 , videoDescription zone video
, Events.onMouseEnter (Core.HoverVideo video)
, Events.onMouseLeave Core.Unhover
]
[ videoMiniature currentDate time hover playlist video
, videoDate darkMode zone video
] ]
button = button =
Ui.link [ Element.width Element.fill, Element.alignTop ] Input.button [ Element.width Element.fill, Element.alignTop ]
{ label = display { label = display
, url = "/#" ++ playlist.url ++ video.url , onPress = Just (Core.VideoClicked playlist video)
} }
in in
button button
videoInList : Bool -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Twitch.Video -> Element Core.Msg videoInList : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Twitch.Video -> Element Core.Msg
videoInList darkMode zone currentDate time hover playlist activeVideo video = videoInList zone playlist activeVideo video =
let 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 = label =
Element.row Element.row [ Element.width Element.fill, Element.spacing 10 ]
[ Element.width Element.fill
, Element.spacing 10
, Events.onMouseEnter (Core.HoverVideo video)
, Events.onMouseLeave Core.Unhover
]
[ Element.el [ Element.width (Element.fillPortion 2) ] [ Element.el [ Element.width (Element.fillPortion 2) ]
(videoMiniature currentDate time hover playlist video) (videoMiniature playlist video)
, Element.el [ Element.width (Element.fillPortion 3), Element.paddingXY 0 10, Element.alignTop ] , Element.el [ Element.width (Element.fillPortion 3), Element.paddingXY 0 10, Element.alignTop ]
(videoDate darkMode zone video) (videoDescription zone video)
] ]
in in
if video == activeVideo then Input.button attr { label = label, onPress = msg }
Element.el
[ Element.width Element.fill
, Background.color (Colors.selected darkMode)
, Border.color Colors.primary
, Border.width 2
]
label
else
Ui.link [ Element.width Element.fill ]
{ label = label
, url = "/#" ++ playlist.url ++ video.url
}
videoView : Bool -> Element.Device -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Video.Video -> Element Core.Msg videoView : Element.Device -> Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
videoView darkMode device zone currentDate time hover playlist video v = videoView device zone playlist video =
let let
( builder, contentPadding ) = ( builder, contentPadding ) =
case device.class of case device.class of
@ -387,8 +308,31 @@ videoView darkMode device zone currentDate time hover playlist video v =
[ Element.width (Element.fillPortion 2) [ Element.width (Element.fillPortion 2)
, Element.spacing 10 , Element.spacing 10
, Element.alignTop , Element.alignTop
, Element.height Element.fill
] ]
[ Video.Views.embedElement v |> Element.map Core.VideoMsg [ 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 , Element.paragraph
[ Font.size Consts.homeFontSize [ Font.size Consts.homeFontSize
, Font.bold , Font.bold
@ -412,12 +356,12 @@ videoView darkMode device zone currentDate time hover playlist video v =
, Element.height Element.fill , Element.height Element.fill
, Element.scrollbarY , Element.scrollbarY
] ]
(List.map (videoInList darkMode zone currentDate time hover playlist video) playlist.videos) (List.map (videoInList zone playlist video) playlist.videos)
] ]
videoDate : Bool -> Time.Zone -> Twitch.Video -> Element Core.Msg videoDescription : Time.Zone -> Twitch.Video -> Element Core.Msg
videoDate darkMode zone video = videoDescription zone video =
Element.column [ Element.spacing 10 ] Element.column [ Element.spacing 10 ]
[ Element.paragraph [ Element.paragraph
[ Font.bold [ Font.bold
@ -427,7 +371,7 @@ videoDate darkMode zone video =
, case video.date of , case video.date of
Just date -> Just date ->
Element.paragraph Element.paragraph
[ Font.color (Colors.detailFont darkMode) [ Font.color Colors.greyFont
] ]
[ Element.text ("Diffusé le " ++ formatDate zone date) ] [ Element.text ("Diffusé le " ++ formatDate zone date) ]
@ -562,26 +506,19 @@ groupAux size list acc =
spinner : Element Core.Msg spinner : Element Core.Msg
spinner = spinner =
Element.none Element.html
(Html.div [ Html.Attributes.class "lds-spinner" ]
[ Html.div [] []
newBadge : Element Core.Msg , Html.div [] []
newBadge = , Html.div [] []
Element.text "NOUV." , Html.div [] []
|> Element.el , Html.div [] []
[ Background.color Colors.red , Html.div [] []
, Border.rounded 5 , Html.div [] []
, Element.padding 5 , Html.div [] []
, Font.color Colors.white , Html.div [] []
, Font.bold , Html.div [] []
, Html.div [] []
, Html.div [] []
] ]
|> Element.el )
[ Element.alignBottom
, Element.alignLeft
, Element.padding 5
]
week : Int
week =
1000 * 60 * 60 * 24 * 7