Compare commits

...

34 Commits

Author SHA1 Message Date
Thomas Forgione 138433d32a Update to latest elm-video 2024-08-02 17:21:42 +02:00
Thomas Forgione a770755e67 Update elm-video 2024-08-02 16:47:10 +02:00
Thomas Forgione dcfc677331 Update to latest elm-video 2021-12-09 17:34:53 +01:00
Thomas Forgione 62004d9e56 Update elm-video 2021-07-02 10:23:30 +02:00
Thomas Forgione e706447e42 Fix miniatures 2021-06-22 09:21:37 +02:00
Thomas Forgione a8cba787bc Update makefile 2021-06-21 16:07:55 +02:00
Thomas Forgione b26cc79787 Update elm-video 2021-06-21 15:57:56 +02:00
Thomas Forgione ab510e4ec2 Update elm-video 2021-06-21 15:16:58 +02:00
Thomas Forgione 31cc8a1bb9 An attempt to work with elm-video 2021-06-21 11:30:31 +02:00
Thomas Forgione df821e07af Add support for dark mode 2021-04-03 23:33:01 +02:00
Thomas Forgione 28f6c05acb I don't like this way of doing record position 2021-03-24 08:18:46 +01:00
Thomas Forgione c28f3ef9ae Support for HLS 2021-03-24 08:17:36 +01:00
Thomas Forgione 360952706d Space is correctly allocated for images when unloaded 2021-03-13 11:31:31 +01:00
Thomas Forgione 53e96eaa48 Fix bug 2020-12-14 10:39:22 +01:00
Thomas Forgione a6d73f094d Update vd 2020-11-06 16:36:37 +01:00
Thomas Forgione 1bb1f8f5c4 Adds badge new 2020-11-06 12:19:42 +01:00
Thomas Forgione cace6f09f8 Clean 2020-11-06 11:43:28 +01:00
Thomas Forgione 5b902ec915 Sort playlists by last video date 2020-11-06 11:15:54 +01:00
Thomas Forgione 67e633d887 Fix bug 2020-11-05 17:34:03 +01:00
Thomas Forgione 1621e5ac58 Update vd 2020-11-05 17:32:58 +01:00
Thomas Forgione ff5db2df3d Adds favicon 2020-11-05 17:23:42 +01:00
Thomas Forgione a61d2fabcd Update video 2020-11-05 17:18:16 +01:00
Thomas Forgione 57bc1e0a9f Update vd.js 2020-11-05 16:17:05 +01:00
Thomas Forgione 2b79862e51 Remove useless comment 2020-10-28 17:03:11 +01:00
Thomas Forgione a6c651bdd2 Better timezone 2020-10-28 16:57:08 +01:00
Thomas Forgione e5c9bde291 Better padding for home button 2020-10-21 11:05:31 +02:00
Thomas Forgione 2775eed077 More robust indexify 2020-10-21 11:00:19 +02:00
Thomas Forgione 948df5da9a Change miniatures when hover 2020-10-16 12:03:07 +02:00
Thomas Forgione bf1d35c179 Fix timezone 2020-10-15 16:47:10 +02:00
Thomas Forgione 8c10e99cca Last miniature in playlist 2020-10-13 14:15:55 +02:00
Thomas Forgione 082817a521 Fix shortcuts 2020-10-08 17:25:17 +02:00
Thomas Forgione c8664f2375 Fix indexify 2020-10-08 16:33:30 +02:00
Thomas Forgione 1ba2bbd8ce Links instead of button 2020-10-08 16:18:22 +02:00
Thomas Forgione f22e1c576b Scroll up on page change 2020-10-06 18:32:10 +02:00
20 changed files with 670 additions and 2483 deletions

4
.gitignore vendored
View File

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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[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
js/main.js: src/**
js/main.js: src/** elm-video/src/**
$(ELM) make src/Main.elm --output $(BUILD_DIR)/main.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
js/main.tmp.js: src/**
js/main.tmp.js: src/** elm-video/src/**
@$(ELM) make src/Main.elm --optimize --output $(BUILD_DIR)/main.tmp.js
watch:

View File

@ -1,78 +0,0 @@
.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;
}
}

File diff suppressed because one or more lines are too long

1
elm-video Submodule

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

View File

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

24
embed.html Normal file
View File

@ -0,0 +1,24 @@
<!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>

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

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

View File

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

449
js/vd.js

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,19 +1,20 @@
module Core exposing (FullModel(..), Model, Msg(..), Page(..), init, subscriptions, update)
module Core exposing (Model, Msg(..), Page(..), init, subscriptions, update)
import Browser
import Browser.Events as Events
import Browser.Navigation as Nav
import Dict exposing (Dict)
import Element
import Hover exposing (Hover)
import Http
import Ports
import Task
import Time
import TimeZone
import Twitch
import Url
type FullModel
= Unloaded Element.Device Url.Url Nav.Key
| Loaded Model
import Video
import Video.Events
type alias Model =
@ -22,29 +23,59 @@ type alias Model =
, page : Page
, key : Nav.Key
, device : Element.Device
, time : Time.Posix
, currentDate : Time.Posix
, url : Url.Url
, darkMode : Bool
, darkSetting : Maybe Bool
}
type Page
= Home
| Playlist Twitch.Playlist
| Video Twitch.Playlist Twitch.Video
= Home (Maybe (Hover Twitch.Playlist))
| Playlist Twitch.Playlist (Maybe (Hover Twitch.Video))
| Video Twitch.Playlist Twitch.Video Video.Video (Maybe (Hover Twitch.Video))
type Msg
= Noop
| PlaylistsReceived ( List Twitch.Playlist, Time.Zone )
| PlaylistsReceived (List Twitch.Playlist)
| HomeClicked
| PlaylistClicked Twitch.Playlist
| VideoClicked Twitch.Playlist Twitch.Video
| UrlReceived Url.Url
| UrlRequested Browser.UrlRequest
| 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 } -> Url.Url -> Nav.Key -> ( FullModel, Cmd Msg )
init { width, height } url key =
( Unloaded (Element.classifyDevice { width = width, height = height }) url key
init : { width : Int, height : Int, darkMode : Bool, darkSetting : Maybe Bool } -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init { width, height, darkMode, darkSetting } url key =
( Model
[]
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
]
)
@ -52,49 +83,117 @@ resultToMsg : Result Http.Error (List Twitch.Playlist) -> Msg
resultToMsg result =
case result of
Ok o ->
PlaylistsReceived ( o, Time.utc )
PlaylistsReceived o
Err _ ->
Noop
subscriptions : FullModel -> Sub Msg
subscriptions _ =
Events.onResize (\w h -> SizeReceived w h)
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ 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 -> FullModel -> ( FullModel, Cmd Msg )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case ( msg, model ) of
( Noop, _ ) ->
case msg of
Noop ->
( model, Cmd.none )
( SizeReceived w h, Loaded m ) ->
( Loaded { m | device = Element.classifyDevice { width = w, height = h } }
TimeZoneReceived z ->
( { model | zone = z }, Cmd.none )
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
)
( PlaylistsReceived ( playlists, zone ), Unloaded device url key ) ->
PlaylistsReceived playlists ->
update
(UrlReceived url)
(Loaded { key = key, playlists = playlists, zone = zone, page = Home, device = device })
(UrlReceived model.url)
{ model | playlists = playlists, page = Home Nothing }
( HomeClicked, Loaded m ) ->
HomeClicked ->
( model
, Nav.pushUrl m.key "#"
, Nav.pushUrl model.key "#"
)
( PlaylistClicked playlist, Loaded m ) ->
DarkModeClicked ->
let
next =
nextDarkSetting model.darkSetting
in
( { model | darkSetting = next }, Ports.setDarkMode next )
PlaylistClicked playlist ->
( model
, Nav.pushUrl m.key ("#" ++ playlist.url)
, Nav.pushUrl model.key ("#" ++ playlist.url)
)
( VideoClicked playlist video, Loaded m ) ->
VideoClicked playlist video ->
( model
, Nav.pushUrl m.key ("#" ++ playlist.url ++ video.url)
, Nav.pushUrl model.key ("#" ++ playlist.url ++ video.url)
)
( UrlReceived url, Loaded m ) ->
UrlReceived url ->
if List.isEmpty model.playlists then
( { model | url = url }, Cmd.none )
else
let
splits =
String.split "?" (Maybe.withDefault "" url.fragment)
@ -111,11 +210,11 @@ update msg model =
( [], Dict.empty )
time =
case Maybe.map String.toInt (Dict.get "t" args) of
Just (Just 0) ->
case Dict.get "t" args of
Just "0" ->
Nothing
Just (Just t) ->
Just t ->
Just t
_ ->
@ -133,7 +232,7 @@ update msg model =
( Nothing, Nothing )
playlist =
List.head (List.filter (\x -> Just x.url == playlistName) m.playlists)
List.head (List.filter (\x -> Just x.url == playlistName) model.playlists)
video =
case playlist of
@ -146,28 +245,56 @@ update msg model =
( page, cmd ) =
case ( playlist, video ) of
( Just p, Just v ) ->
( Video p v
, Ports.registerVideo ( Twitch.videoId v, "videos/" ++ p.url ++ v.url, time )
)
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, Cmd.none )
( Playlist p Nothing, Cmd.none )
_ ->
( Home, Cmd.none )
extraCmd =
case page of
Video _ _ ->
Cmd.none
_ ->
Ports.eraseVideo ()
( Home Nothing, Cmd.none )
in
( Loaded { m | page = page }, Cmd.batch [ cmd, extraCmd ] )
( { 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, Cmd.none )
( model.page, Cmd.none )
in
( { model | page = newPage }, Cmd.map VideoMsg newCommand )
splitter : String -> Maybe ( String, String )
@ -183,3 +310,16 @@ splitter input =
parseQueryString : String -> Dict String String
parseQueryString 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

14
src/Hover.elm Normal file
View File

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

View File

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

View File

@ -3,12 +3,14 @@ module Twitch exposing
, Video
, decodePlaylists
, fetchPlaylists
, playlistDate
, playlistMiniatureUrl
, videoId
, videoMiniatureUrl
, videoName
)
import Hover exposing (Hover)
import Http
import Iso8601
import Json.Decode as Decode
@ -49,7 +51,32 @@ decodePlaylist =
decodePlaylists : Decode.Decoder (List Playlist)
decodePlaylists =
Decode.map (List.sortBy .url >> List.reverse) (Decode.list decodePlaylist)
Decode.map (sortPlaylists >> 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
@ -70,9 +97,22 @@ videoId video =
String.dropLeft 1 video.url |> String.replace "/" "-"
playlistMiniatureUrl : Playlist -> String
playlistMiniatureUrl playlist =
case List.head (List.reverse playlist.videos) of
playlistMiniatureUrl : Time.Posix -> Maybe (Hover Playlist) -> Playlist -> String
playlistMiniatureUrl currentTime hover playlist =
let
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 ->
"videos/" ++ playlist.url ++ v.url ++ "miniature-050.png"
@ -80,6 +120,30 @@ playlistMiniatureUrl playlist =
""
videoMiniatureUrl : Playlist -> Video -> String
videoMiniatureUrl playlist video =
"videos/" ++ playlist.url ++ video.url ++ "miniature-050.png"
videoMiniatureUrl : Time.Posix -> Maybe (Hover Video) -> Playlist -> Video -> String
videoMiniatureUrl currentTime hover playlist video =
let
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"

12
src/Ui.elm Normal file
View File

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