Compare commits
34 Commits
Author | SHA1 | Date |
---|---|---|
Thomas Forgione | 138433d32a | |
Thomas Forgione | a770755e67 | |
Thomas Forgione | dcfc677331 | |
Thomas Forgione | 62004d9e56 | |
Thomas Forgione | e706447e42 | |
Thomas Forgione | a8cba787bc | |
Thomas Forgione | b26cc79787 | |
Thomas Forgione | ab510e4ec2 | |
Thomas Forgione | 31cc8a1bb9 | |
Thomas Forgione | df821e07af | |
Thomas Forgione | 28f6c05acb | |
Thomas Forgione | c28f3ef9ae | |
Thomas Forgione | 360952706d | |
Thomas Forgione | 53e96eaa48 | |
Thomas Forgione | a6d73f094d | |
Thomas Forgione | 1bb1f8f5c4 | |
Thomas Forgione | cace6f09f8 | |
Thomas Forgione | 5b902ec915 | |
Thomas Forgione | 67e633d887 | |
Thomas Forgione | 1621e5ac58 | |
Thomas Forgione | ff5db2df3d | |
Thomas Forgione | a61d2fabcd | |
Thomas Forgione | 57bc1e0a9f | |
Thomas Forgione | 2b79862e51 | |
Thomas Forgione | a6c651bdd2 | |
Thomas Forgione | e5c9bde291 | |
Thomas Forgione | 2775eed077 | |
Thomas Forgione | 948df5da9a | |
Thomas Forgione | bf1d35c179 | |
Thomas Forgione | 8c10e99cca | |
Thomas Forgione | 082817a521 | |
Thomas Forgione | c8664f2375 | |
Thomas Forgione | 1ba2bbd8ce | |
Thomas Forgione | f22e1c576b |
|
@ -1,6 +1,4 @@
|
||||||
videos
|
videos
|
||||||
elm-stuff
|
elm-stuff
|
||||||
js/main.js
|
js
|
||||||
js/main.tmp.js
|
|
||||||
js/main.min.js
|
|
||||||
deploy.sh
|
deploy.sh
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "elm-video"]
|
||||||
|
path = elm-video
|
||||||
|
url = https://github.com/polymny/elm-video
|
4
Makefile
4
Makefile
|
@ -18,13 +18,13 @@ dev: js/main.js
|
||||||
|
|
||||||
release: js/main.min.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
|
$(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/**
|
js/main.tmp.js: src/** elm-video/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:
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
1664
css/video-js.css
1664
css/video-js.css
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 087c3d594b62b6617954fc31a58576f12752d75a
|
9
elm.json
9
elm.json
|
@ -1,24 +1,29 @@
|
||||||
{
|
{
|
||||||
"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": {
|
||||||
"STTR13/ziplist": "1.3.0",
|
"andrewMacmurray/elm-simple-animation": "2.1.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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
69
index.html
69
index.html
|
@ -4,46 +4,65 @@
|
||||||
<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 href="css/video-js.css" rel="stylesheet">
|
<link rel="icon" type="image/ico" href="favicon.ico"/>
|
||||||
<link href="css/spinner.css" rel="stylesheet">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container"></div>
|
<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 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: { width: window.innerWidth, height: window.innerHeight }
|
flags: {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
darkMode: isDarkMode(),
|
||||||
|
darkSetting: JSON.parse(localStorage.getItem('darkMode'))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var lastId, player;
|
PolymnyVideo.setup(app);
|
||||||
|
|
||||||
if (app.ports !== undefined && app.ports.registerVideo !== undefined) {
|
if (app.ports !== undefined) {
|
||||||
app.ports.registerVideo.subscribe(function(args) {
|
if (app.ports.setDarkMode !== undefined) {
|
||||||
var time = parseInt(args[2], 10) || undefined;
|
app.ports.setDarkMode.subscribe(function(arg) {
|
||||||
|
if (arg === null) {
|
||||||
requestAnimationFrame(function() {
|
localStorage.removeItem('darkMode');
|
||||||
if (args[0] !== lastId) {
|
} else {
|
||||||
lastId = args[0];
|
localStorage.setItem('darkMode', arg);
|
||||||
|
|
||||||
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>
|
||||||
|
|
13
indexify.js
13
indexify.js
|
@ -2,16 +2,25 @@ 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 (subdir === DESCRIPTION_FILE) {
|
if (!isDirectory(path.join(VIDEO_DIR, dir, subdir))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,5 +33,5 @@ for (let dir of fs.readdirSync(VIDEO_DIR)) {
|
||||||
info.push(description);
|
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));
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
module Colors exposing (blackFont, greyBackground, greyFont, primary, primaryOver, selected, white)
|
module Colors exposing (..)
|
||||||
|
|
||||||
import Element
|
import Element
|
||||||
|
|
||||||
|
@ -18,21 +18,47 @@ 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
|
||||||
|
|
||||||
|
|
||||||
blackFont : Element.Color
|
background : Bool -> Element.Color
|
||||||
blackFont =
|
background darkMode =
|
||||||
Element.rgb255 54 54 54
|
if darkMode then
|
||||||
|
Element.rgb255 47 49 54
|
||||||
|
|
||||||
|
else
|
||||||
|
Element.rgb255 245 245 245
|
||||||
|
|
||||||
|
|
||||||
greyFont : Element.Color
|
font : Bool -> Element.Color
|
||||||
greyFont =
|
font darkMode =
|
||||||
Element.rgb255 128 128 128
|
if darkMode then
|
||||||
|
Element.rgb255 245 245 245
|
||||||
|
|
||||||
|
else
|
||||||
|
Element.rgb255 54 57 63
|
||||||
|
|
||||||
|
|
||||||
selected : Element.Color
|
detailFont : Bool -> Element.Color
|
||||||
selected =
|
detailFont darkMode =
|
||||||
Element.rgb255 223 233 250
|
if darkMode then
|
||||||
|
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
|
||||||
|
|
358
src/Core.elm
358
src/Core.elm
|
@ -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.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 =
|
||||||
|
@ -22,29 +23,59 @@ 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
|
= Home (Maybe (Hover Twitch.Playlist))
|
||||||
| Playlist Twitch.Playlist
|
| Playlist Twitch.Playlist (Maybe (Hover Twitch.Video))
|
||||||
| Video Twitch.Playlist Twitch.Video
|
| Video Twitch.Playlist Twitch.Video Video.Video (Maybe (Hover Twitch.Video))
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= Noop
|
= Noop
|
||||||
| PlaylistsReceived ( List Twitch.Playlist, Time.Zone )
|
| PlaylistsReceived (List Twitch.Playlist)
|
||||||
| 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 } -> Url.Url -> Nav.Key -> ( FullModel, Cmd Msg )
|
init : { width : Int, height : Int, darkMode : Bool, darkSetting : Maybe Bool } -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
|
||||||
init { width, height } url key =
|
init { width, height, darkMode, darkSetting } url key =
|
||||||
( Unloaded (Element.classifyDevice { width = width, height = height }) url key
|
( Model
|
||||||
, 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
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,122 +83,218 @@ resultToMsg : Result Http.Error (List Twitch.Playlist) -> Msg
|
||||||
resultToMsg result =
|
resultToMsg result =
|
||||||
case result of
|
case result of
|
||||||
Ok o ->
|
Ok o ->
|
||||||
PlaylistsReceived ( o, Time.utc )
|
PlaylistsReceived o
|
||||||
|
|
||||||
Err _ ->
|
Err _ ->
|
||||||
Noop
|
Noop
|
||||||
|
|
||||||
|
|
||||||
subscriptions : FullModel -> Sub Msg
|
subscriptions : Model -> Sub Msg
|
||||||
subscriptions _ =
|
subscriptions model =
|
||||||
Events.onResize (\w h -> SizeReceived w h)
|
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 =
|
update msg model =
|
||||||
case ( msg, model ) of
|
case msg of
|
||||||
( Noop, _ ) ->
|
Noop ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none )
|
||||||
|
|
||||||
( SizeReceived w h, Loaded m ) ->
|
TimeZoneReceived z ->
|
||||||
( Loaded { m | device = Element.classifyDevice { width = w, height = h } }
|
( { 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
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
( PlaylistsReceived ( playlists, zone ), Unloaded device url key ) ->
|
PlaylistsReceived playlists ->
|
||||||
update
|
update
|
||||||
(UrlReceived url)
|
(UrlReceived model.url)
|
||||||
(Loaded { key = key, playlists = playlists, zone = zone, page = Home, device = device })
|
{ model | playlists = playlists, page = Home Nothing }
|
||||||
|
|
||||||
( HomeClicked, Loaded m ) ->
|
HomeClicked ->
|
||||||
( model
|
( model
|
||||||
, Nav.pushUrl m.key "#"
|
, Nav.pushUrl model.key "#"
|
||||||
)
|
)
|
||||||
|
|
||||||
( PlaylistClicked playlist, Loaded m ) ->
|
DarkModeClicked ->
|
||||||
( 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
|
||||||
splits =
|
next =
|
||||||
String.split "?" (Maybe.withDefault "" url.fragment)
|
nextDarkSetting model.darkSetting
|
||||||
|
|
||||||
( split, args ) =
|
|
||||||
case splits of
|
|
||||||
h1 :: h2 :: _ ->
|
|
||||||
( String.split "/" h1, parseQueryString h2 )
|
|
||||||
|
|
||||||
h1 :: _ ->
|
|
||||||
( String.split "/" h1, Dict.empty )
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
( [], 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
|
||||||
( Loaded { m | page = page }, Cmd.batch [ cmd, extraCmd ] )
|
( { model | darkSetting = next }, Ports.setDarkMode next )
|
||||||
|
|
||||||
_ ->
|
PlaylistClicked playlist ->
|
||||||
( model, Cmd.none )
|
( model
|
||||||
|
, Nav.pushUrl model.key ("#" ++ playlist.url)
|
||||||
|
)
|
||||||
|
|
||||||
|
VideoClicked playlist video ->
|
||||||
|
( model
|
||||||
|
, 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 )
|
||||||
|
in
|
||||||
|
( { model | page = newPage }, Cmd.map VideoMsg newCommand )
|
||||||
|
|
||||||
|
|
||||||
splitter : String -> Maybe ( String, String )
|
splitter : String -> Maybe ( String, String )
|
||||||
|
@ -183,3 +310,16 @@ 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
|
||||||
|
|
|
@ -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 }
|
|
@ -5,7 +5,7 @@ import Core
|
||||||
import Views
|
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 =
|
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.Noop
|
, onUrlRequest = Core.UrlRequested
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -3,12 +3,14 @@ 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
|
||||||
|
@ -49,7 +51,32 @@ decodePlaylist =
|
||||||
|
|
||||||
decodePlaylists : Decode.Decoder (List Playlist)
|
decodePlaylists : Decode.Decoder (List Playlist)
|
||||||
decodePlaylists =
|
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
|
fetchPlaylists : (Result Http.Error (List Playlist) -> msg) -> Cmd msg
|
||||||
|
@ -70,9 +97,22 @@ videoId video =
|
||||||
String.dropLeft 1 video.url |> String.replace "/" "-"
|
String.dropLeft 1 video.url |> String.replace "/" "-"
|
||||||
|
|
||||||
|
|
||||||
playlistMiniatureUrl : Playlist -> String
|
playlistMiniatureUrl : Time.Posix -> Maybe (Hover Playlist) -> Playlist -> String
|
||||||
playlistMiniatureUrl playlist =
|
playlistMiniatureUrl currentTime hover playlist =
|
||||||
case List.head (List.reverse playlist.videos) of
|
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 ->
|
Just v ->
|
||||||
"videos/" ++ playlist.url ++ v.url ++ "miniature-050.png"
|
"videos/" ++ playlist.url ++ v.url ++ "miniature-050.png"
|
||||||
|
|
||||||
|
@ -80,6 +120,30 @@ playlistMiniatureUrl playlist =
|
||||||
""
|
""
|
||||||
|
|
||||||
|
|
||||||
videoMiniatureUrl : Playlist -> Video -> String
|
videoMiniatureUrl : Time.Posix -> Maybe (Hover Video) -> Playlist -> Video -> String
|
||||||
videoMiniatureUrl playlist video =
|
videoMiniatureUrl currentTime hover playlist video =
|
||||||
"videos/" ++ playlist.url ++ video.url ++ "miniature-050.png"
|
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"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
317
src/Views.elm
317
src/Views.elm
|
@ -7,106 +7,128 @@ 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 Html
|
import Hover exposing (Hover)
|
||||||
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.FullModel -> Browser.Document Core.Msg
|
view : Core.Model -> Browser.Document Core.Msg
|
||||||
view model =
|
view model =
|
||||||
let
|
let
|
||||||
element =
|
element =
|
||||||
case model of
|
case model.playlists of
|
||||||
Core.Unloaded _ _ _ ->
|
[] ->
|
||||||
Element.el [ Element.padding 10, Element.centerX ] spinner
|
Element.el [ Element.padding 10, Element.centerX ] spinner
|
||||||
|
|
||||||
Core.Loaded m ->
|
_ ->
|
||||||
viewContent m
|
viewContent model
|
||||||
in
|
in
|
||||||
{ title = title model
|
{ title = title model
|
||||||
, body =
|
, body =
|
||||||
[ Element.layout
|
[ Element.layout
|
||||||
[ Font.color Colors.blackFont
|
[ Font.color (Colors.font model.darkMode)
|
||||||
|
, 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, element ]
|
[ topBar model.darkSetting, element ]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
title : Core.FullModel -> String
|
title : Core.Model -> String
|
||||||
title model =
|
title model =
|
||||||
case model of
|
case model.page of
|
||||||
Core.Unloaded _ _ _ ->
|
Core.Home _ ->
|
||||||
Consts.url
|
Consts.url
|
||||||
|
|
||||||
Core.Loaded m ->
|
Core.Playlist p _ ->
|
||||||
case m.page of
|
Consts.url ++ " - " ++ p.name
|
||||||
Core.Home ->
|
|
||||||
Consts.url
|
|
||||||
|
|
||||||
Core.Playlist p ->
|
Core.Video p v _ _ ->
|
||||||
Consts.url ++ " - " ++ p.name
|
Consts.url ++ " - " ++ p.name ++ " - " ++ v.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 ->
|
Core.Home hover ->
|
||||||
playlistsView model.device model.playlists
|
playlistsView model.device model.playlists model.currentDate model.time hover
|
||||||
|
|
||||||
Core.Playlist playlist ->
|
Core.Playlist playlist hover ->
|
||||||
videoMiniaturesView model.device model.zone playlist
|
videoMiniaturesView model.darkMode model.device model.zone model.currentDate model.time hover playlist
|
||||||
|
|
||||||
Core.Video playlist video ->
|
Core.Video playlist video v hover ->
|
||||||
videoView model.device model.zone playlist video
|
videoView model.darkMode model.device model.zone model.currentDate model.time hover playlist video v
|
||||||
|
|
||||||
|
|
||||||
topBar : Element Core.Msg
|
topBar : Maybe Bool -> Element Core.Msg
|
||||||
topBar =
|
topBar darkSetting =
|
||||||
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 ]
|
[ 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 : Element Core.Msg
|
||||||
homeButton =
|
homeButton =
|
||||||
Input.button
|
Ui.link
|
||||||
[ Element.padding Consts.homePadding
|
[ Element.height Element.fill
|
||||||
, Element.height Element.fill
|
|
||||||
, Element.mouseOver [ Background.color Colors.primaryOver ]
|
|
||||||
, Font.bold
|
, Font.bold
|
||||||
]
|
]
|
||||||
{ label = Element.text Consts.name
|
{ label = Element.el [ Element.padding 10 ] (Element.text Consts.name)
|
||||||
, onPress = Just Core.HomeClicked
|
, url = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
playlistsView : Element.Device -> List Twitch.Playlist -> Element Core.Msg
|
playlistsView : Element.Device -> List Twitch.Playlist -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Playlist) -> Element Core.Msg
|
||||||
playlistsView device playlists =
|
playlistsView device playlists currentDate time hover =
|
||||||
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 playlists
|
List.map (playlistView currentDate time hover) playlists
|
||||||
|
|
||||||
grouped =
|
grouped =
|
||||||
group (numberOfVideosPerRow device) views
|
group (numberOfVideosPerRow device) views
|
||||||
|
@ -122,19 +144,32 @@ playlistsView device playlists =
|
||||||
final
|
final
|
||||||
|
|
||||||
|
|
||||||
playlistView : Twitch.Playlist -> Element Core.Msg
|
playlistView : Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Playlist) -> Twitch.Playlist -> Element Core.Msg
|
||||||
playlistView playlist =
|
playlistView currentDate time hover playlist =
|
||||||
let
|
let
|
||||||
|
key =
|
||||||
|
Twitch.playlistMiniatureUrl time Nothing playlist
|
||||||
|
|
||||||
src =
|
src =
|
||||||
Twitch.playlistMiniatureUrl playlist
|
Twitch.playlistMiniatureUrl time hover playlist
|
||||||
|
|
||||||
image =
|
image =
|
||||||
Keyed.el [ Element.width Element.fill, Element.height Element.fill ]
|
Keyed.el
|
||||||
( src
|
[ 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.image
|
||||||
[ Element.width Element.fill
|
[ 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 inFront
|
||||||
|
, Element.inFront new
|
||||||
]
|
]
|
||||||
{ description = "", src = src }
|
{ description = "", src = src }
|
||||||
)
|
)
|
||||||
|
@ -166,6 +201,13 @@ playlistView 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
|
||||||
|
@ -176,21 +218,25 @@ playlistView playlist =
|
||||||
|
|
||||||
button =
|
button =
|
||||||
Input.button [ Element.width Element.fill, Element.alignTop ]
|
Input.button [ Element.width Element.fill, Element.alignTop ]
|
||||||
{ label = display
|
{ label =
|
||||||
, onPress = Just (Core.PlaylistClicked playlist)
|
Ui.link [ Element.width Element.fill, Element.alignTop ]
|
||||||
|
{ label = display
|
||||||
|
, url = "/#" ++ playlist.url
|
||||||
|
}
|
||||||
|
, onPress = Nothing
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
button
|
button
|
||||||
|
|
||||||
|
|
||||||
videoMiniaturesView : Element.Device -> Time.Zone -> Twitch.Playlist -> Element Core.Msg
|
videoMiniaturesView : Bool -> Element.Device -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Element Core.Msg
|
||||||
videoMiniaturesView device zone playlist =
|
videoMiniaturesView darkMode device zone currentDate time hover 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 zone playlist) playlist.videos
|
List.map (videoMiniatureView darkMode zone currentDate time hover playlist) playlist.videos
|
||||||
|
|
||||||
grouped =
|
grouped =
|
||||||
group (numberOfVideosPerRow device) views
|
group (numberOfVideosPerRow device) views
|
||||||
|
@ -206,8 +252,8 @@ videoMiniaturesView device zone playlist =
|
||||||
final
|
final
|
||||||
|
|
||||||
|
|
||||||
videoMiniature : Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
videoMiniature : Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||||
videoMiniature playlist video =
|
videoMiniature currentDate time hover playlist video =
|
||||||
let
|
let
|
||||||
inFront =
|
inFront =
|
||||||
Element.text label
|
Element.text label
|
||||||
|
@ -223,16 +269,39 @@ videoMiniature 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 playlist video
|
Twitch.videoMiniatureUrl time hover playlist video
|
||||||
|
|
||||||
image =
|
image =
|
||||||
Keyed.el [ Element.width Element.fill, Element.height Element.fill ]
|
Keyed.el
|
||||||
( src
|
[ 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.image
|
||||||
[ Element.width Element.fill
|
[ 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 inFront
|
||||||
|
, Element.inFront new
|
||||||
]
|
]
|
||||||
{ description = "", src = src }
|
{ description = "", src = src }
|
||||||
)
|
)
|
||||||
|
@ -243,53 +312,63 @@ videoMiniature playlist video =
|
||||||
image
|
image
|
||||||
|
|
||||||
|
|
||||||
videoMiniatureView : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
videoMiniatureView : Bool -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||||
videoMiniatureView zone playlist video =
|
videoMiniatureView darkMode zone currentDate time hover playlist video =
|
||||||
let
|
let
|
||||||
display =
|
display =
|
||||||
Element.column [ Element.width Element.fill, Element.spacing 10 ]
|
Element.column
|
||||||
[ videoMiniature playlist video
|
[ Element.width Element.fill
|
||||||
, videoDescription zone video
|
, Element.spacing 10
|
||||||
|
, Events.onMouseEnter (Core.HoverVideo video)
|
||||||
|
, Events.onMouseLeave Core.Unhover
|
||||||
|
]
|
||||||
|
[ videoMiniature currentDate time hover playlist video
|
||||||
|
, videoDate darkMode zone video
|
||||||
]
|
]
|
||||||
|
|
||||||
button =
|
button =
|
||||||
Input.button [ Element.width Element.fill, Element.alignTop ]
|
Ui.link [ Element.width Element.fill, Element.alignTop ]
|
||||||
{ label = display
|
{ label = display
|
||||||
, onPress = Just (Core.VideoClicked playlist video)
|
, url = "/#" ++ playlist.url ++ video.url
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
button
|
button
|
||||||
|
|
||||||
|
|
||||||
videoInList : Time.Zone -> Twitch.Playlist -> Twitch.Video -> Twitch.Video -> Element Core.Msg
|
videoInList : Bool -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Twitch.Video -> Element Core.Msg
|
||||||
videoInList zone playlist activeVideo video =
|
videoInList darkMode zone currentDate time hover 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.width Element.fill, Element.spacing 10 ]
|
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) ]
|
[ Element.el [ Element.width (Element.fillPortion 2) ]
|
||||||
(videoMiniature playlist video)
|
(videoMiniature currentDate time hover 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 ]
|
||||||
(videoDescription zone video)
|
(videoDate darkMode zone video)
|
||||||
]
|
]
|
||||||
in
|
in
|
||||||
Input.button attr { label = label, onPress = msg }
|
if video == activeVideo then
|
||||||
|
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 : Element.Device -> Time.Zone -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
videoView : Bool -> Element.Device -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Video.Video -> Element Core.Msg
|
||||||
videoView device zone playlist video =
|
videoView darkMode device zone currentDate time hover playlist video v =
|
||||||
let
|
let
|
||||||
( builder, contentPadding ) =
|
( builder, contentPadding ) =
|
||||||
case device.class of
|
case device.class of
|
||||||
|
@ -308,31 +387,8 @@ videoView device zone playlist video =
|
||||||
[ Element.width (Element.fillPortion 2)
|
[ Element.width (Element.fillPortion 2)
|
||||||
, Element.spacing 10
|
, Element.spacing 10
|
||||||
, Element.alignTop
|
, Element.alignTop
|
||||||
, Element.height Element.fill
|
|
||||||
]
|
]
|
||||||
[ Keyed.el
|
[ Video.Views.embedElement v |> Element.map Core.VideoMsg
|
||||||
[ 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
|
||||||
|
@ -356,12 +412,12 @@ videoView device zone playlist video =
|
||||||
, Element.height Element.fill
|
, Element.height Element.fill
|
||||||
, Element.scrollbarY
|
, 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
|
videoDate : Bool -> Time.Zone -> Twitch.Video -> Element Core.Msg
|
||||||
videoDescription zone video =
|
videoDate darkMode zone video =
|
||||||
Element.column [ Element.spacing 10 ]
|
Element.column [ Element.spacing 10 ]
|
||||||
[ Element.paragraph
|
[ Element.paragraph
|
||||||
[ Font.bold
|
[ Font.bold
|
||||||
|
@ -371,7 +427,7 @@ videoDescription zone video =
|
||||||
, case video.date of
|
, case video.date of
|
||||||
Just date ->
|
Just date ->
|
||||||
Element.paragraph
|
Element.paragraph
|
||||||
[ Font.color Colors.greyFont
|
[ Font.color (Colors.detailFont darkMode)
|
||||||
]
|
]
|
||||||
[ Element.text ("Diffusé le " ++ formatDate zone date) ]
|
[ Element.text ("Diffusé le " ++ formatDate zone date) ]
|
||||||
|
|
||||||
|
@ -506,19 +562,26 @@ groupAux size list acc =
|
||||||
|
|
||||||
spinner : Element Core.Msg
|
spinner : Element Core.Msg
|
||||||
spinner =
|
spinner =
|
||||||
Element.html
|
Element.none
|
||||||
(Html.div [ Html.Attributes.class "lds-spinner" ]
|
|
||||||
[ Html.div [] []
|
|
||||||
, 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 [] []
|
|
||||||
]
|
]
|
||||||
)
|
|> Element.el
|
||||||
|
[ Element.alignBottom
|
||||||
|
, Element.alignLeft
|
||||||
|
, Element.padding 5
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
week : Int
|
||||||
|
week =
|
||||||
|
1000 * 60 * 60 * 24 * 7
|
||||||
|
|
Loading…
Reference in New Issue