Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0201bcd4a9 | |||
| df821e07af | |||
| 28f6c05acb | |||
| c28f3ef9ae | |||
| 360952706d | |||
| 53e96eaa48 | |||
| a6d73f094d | |||
| 1bb1f8f5c4 | |||
| cace6f09f8 | |||
| 5b902ec915 | |||
| 67e633d887 | |||
| 1621e5ac58 | |||
| ff5db2df3d | |||
| a61d2fabcd | |||
| 57bc1e0a9f | |||
| 2b79862e51 | |||
| a6c651bdd2 | |||
| e5c9bde291 | |||
| 2775eed077 | |||
| 948df5da9a | |||
| bf1d35c179 | |||
| 8c10e99cca | |||
| 082817a521 | |||
| c8664f2375 | |||
| 1ba2bbd8ce | |||
| f22e1c576b |
@@ -15,6 +15,7 @@
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.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"
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
+113
-26
@@ -4,46 +4,133 @@
|
||||
<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 rel="icon" type="image/ico" href="/favicon.ico"/>
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.6.7/plyr.css" />
|
||||
<link href="css/spinner.css" rel="stylesheet">
|
||||
<style>
|
||||
video, .plyr, .plyr__video-wrapper {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script src="js/vd.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.6.7/plyr.polyfilled.js"></script>
|
||||
<script src="https://cdn.rawgit.com/video-dev/hls.js/18bb552/dist/hls.min.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
<script>
|
||||
customElements.define('plyr-video',
|
||||
class PlyrVideo extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.videoElement = document.createElement('video');
|
||||
this.videoElement.setAttribute('controls', 'true');
|
||||
}
|
||||
|
||||
static get observedAttributes() { return []; }
|
||||
|
||||
connectedCallback() {
|
||||
this.appendChild(this.videoElement)
|
||||
|
||||
const hls = new Hls();
|
||||
hls.loadSource(this.getAttribute('src'));
|
||||
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
|
||||
|
||||
// Transform available levels into an array of integers (height values).
|
||||
const availableQualities = hls.levels.map((l) => l.height);
|
||||
availableQualities.unshift(0);
|
||||
|
||||
// Initialize here
|
||||
new Plyr(this.videoElement, {
|
||||
quality: {
|
||||
default: 0,
|
||||
options: availableQualities,
|
||||
// this ensures Plyr to use Hls to update quality level
|
||||
forced: true,
|
||||
onChange: updateQuality,
|
||||
debug: true,
|
||||
},
|
||||
fullscreen: {
|
||||
enabled: true,
|
||||
fallback: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) {
|
||||
var span = document.querySelector(".plyr__menu__container [data-plyr='quality'][value='0'] span");
|
||||
if (hls.autoLevelEnabled) {
|
||||
span.innerHTML = "Auto (" + hls.levels[data.level].height + "p)";
|
||||
} else {
|
||||
span.innerHTML = "Auto";
|
||||
}
|
||||
var x = document.querySelectorAll(".plyr__menu__container [data-plyr='settings'] span")[3];
|
||||
if (x.innerHTML.startsWith("Auto") || x.innerHTML === "0p") {
|
||||
x.innerHTML = span.innerHTML;
|
||||
}
|
||||
})
|
||||
|
||||
hls.attachMedia(this.videoElement);
|
||||
|
||||
function updateQuality(newQuality) {
|
||||
if (newQuality === 0) {
|
||||
hls.currentLevel = -1;
|
||||
} else {
|
||||
hls.levels.forEach((level, levelIndex) => {
|
||||
if (level.height === newQuality) {
|
||||
hls.currentLevel = levelIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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'))
|
||||
}
|
||||
});
|
||||
|
||||
var lastId;
|
||||
|
||||
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) {
|
||||
app.ports.eraseVideo.subscribe(function() {
|
||||
lastId = undefined;
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
||||
app.ports.darkMode.send(isDarkMode(e));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+11
-2
@@ -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));
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ const vd = (function() {
|
||||
|
||||
let vd = {};
|
||||
|
||||
function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
|
||||
function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
|
||||
const el = document.createElement(tagName);
|
||||
|
||||
Object.getOwnPropertyNames(properties).forEach(function(propName) {
|
||||
@@ -28,9 +28,9 @@ const vd = (function() {
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
||||
function findPosition(el) {
|
||||
function findPosition(el) {
|
||||
let box;
|
||||
|
||||
if (el.getBoundingClientRect && el.parentNode) {
|
||||
@@ -60,9 +60,9 @@ const vd = (function() {
|
||||
left: Math.round(left),
|
||||
top: Math.round(top)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getBoundingClientRect(el) {
|
||||
function getBoundingClientRect(el) {
|
||||
if (el && el.getBoundingClientRect && el.parentNode) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const result = {};
|
||||
@@ -83,9 +83,9 @@ const vd = (function() {
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPointerPosition(el, event) {
|
||||
function getPointerPosition(el, event) {
|
||||
const position = {};
|
||||
const box = findPosition(el);
|
||||
const boxW = el.offsetWidth;
|
||||
@@ -105,48 +105,19 @@ const vd = (function() {
|
||||
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
|
||||
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
function setupRepresentations(menuButton, player) {
|
||||
let vjs = {};
|
||||
vjs.Component = videojs.getComponent('Component');
|
||||
vjs.Menu = videojs.getComponent('Menu');
|
||||
vjs.MenuButton = videojs.getComponent('MenuButton');
|
||||
vjs.MenuItem = videojs.getComponent('MenuItem');
|
||||
|
||||
let hls = player.tech({IWillNotUseThisInPlugins: true}).hls;
|
||||
|
||||
if (hls === undefined) {
|
||||
setTimeout(() => setupRepresentations(menuButton, player), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
let representations = hls.representations();
|
||||
representations.sort((a, b) => b.height - a.height);
|
||||
|
||||
menuButton.menu.items = [];
|
||||
menuButton.menu.addAndRecordItem = function(item) {
|
||||
this.addItem(item);
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
menuButton.menu.addAndRecordItem(new MenuItem(player, { label: "auto", menuButton }));
|
||||
for (let representation of representations) {
|
||||
menuButton.menu.addAndRecordItem(new MenuItem(player, {
|
||||
label: representation.height + "p",
|
||||
representation,
|
||||
menuButton
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let vjs = {};
|
||||
|
||||
vjs.Component = videojs.getComponent('Component');
|
||||
vjs.Menu = videojs.getComponent('Menu');
|
||||
vjs.MenuButton = videojs.getComponent('MenuButton');
|
||||
vjs.MenuItem = videojs.getComponent('MenuItem');
|
||||
|
||||
class MenuButton extends vjs.MenuButton {
|
||||
class MenuButton extends vjs.MenuButton {
|
||||
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.updateLabel('auto');
|
||||
this.updateLabel(options === undefined ? "" : options.label || "");
|
||||
}
|
||||
|
||||
createEl() {
|
||||
@@ -188,9 +159,9 @@ const vd = (function() {
|
||||
this.labelEl_.innerHTML = newLabel;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class MenuItem extends vjs.MenuItem {
|
||||
class ResolutionItem extends vjs.MenuItem {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.representation = arguments[1].representation;
|
||||
@@ -214,9 +185,23 @@ const vd = (function() {
|
||||
}
|
||||
this.menuButton.updateLabel(this.label);
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedItem extends vjs.MenuItem {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.label = arguments[1].label;
|
||||
this.speed = arguments[1].speed;
|
||||
this.menuButton = arguments[1].menuButton;
|
||||
}
|
||||
|
||||
class Thumbnail extends vjs.Component {
|
||||
handleClick() {
|
||||
this.player().playbackRate(this.speed);
|
||||
this.menuButton.updateLabel(this.label);
|
||||
}
|
||||
}
|
||||
|
||||
class Thumbnail extends vjs.Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.thumbnails = arguments[1].thumbnails;
|
||||
@@ -243,10 +228,119 @@ const vd = (function() {
|
||||
update(ratio) {
|
||||
this.el().src = this.options_.thumbnails[Math.round(100 * ratio)];
|
||||
}
|
||||
}
|
||||
|
||||
function createRepresentationButtons(player, menuButton = undefined) {
|
||||
if (menuButton === undefined) {
|
||||
menuButton = new MenuButton(player);
|
||||
menuButton.updateLabel("auto");
|
||||
}
|
||||
|
||||
let hls = player.tech({IWillNotUseThisInPlugins: true}).hls;
|
||||
|
||||
vd.setup = function(video, args) {
|
||||
if (hls === undefined || hls.representations === undefined) {
|
||||
setTimeout(() => createRepresentationButtons(player, menuButton), 500);
|
||||
return menuButton;
|
||||
}
|
||||
|
||||
let representations = hls.representations();
|
||||
representations.sort((a, b) => b.height - a.height);
|
||||
|
||||
menuButton.menu.items = [];
|
||||
menuButton.menu.addAndRecordItem = function(item) {
|
||||
this.addItem(item);
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
menuButton.menu.addAndRecordItem(new ResolutionItem(player, { label: "auto", menuButton }));
|
||||
for (let representation of representations) {
|
||||
menuButton.menu.addAndRecordItem(new ResolutionItem(player, {
|
||||
label: representation.height + "p",
|
||||
representation,
|
||||
menuButton
|
||||
}));
|
||||
}
|
||||
|
||||
return menuButton;
|
||||
}
|
||||
|
||||
function createSpeedButtons(player) {
|
||||
let menuButton = new MenuButton(player);
|
||||
|
||||
let speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
||||
menuButton.updateLabel('x1');
|
||||
menuButton.menu.items = [];
|
||||
menuButton.menu.addAndRecordItem = function(item) {
|
||||
this.addItem(item);
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
for (let speed of speeds) {
|
||||
menuButton.menu.addAndRecordItem(new SpeedItem(player, {
|
||||
label: "x" + speed,
|
||||
menuButton,
|
||||
speed,
|
||||
}));
|
||||
}
|
||||
|
||||
return menuButton;
|
||||
}
|
||||
|
||||
vd.parseTime = function(t) {
|
||||
let parsed = 1 * t;
|
||||
|
||||
if (!isNaN(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Compute the split
|
||||
let split = t.split("h");
|
||||
|
||||
let hours;
|
||||
let minutes;
|
||||
let seconds;
|
||||
|
||||
switch (split.length) {
|
||||
case 1:
|
||||
hours = 0;
|
||||
split = split[0].split("m");
|
||||
break;
|
||||
case 2:
|
||||
hours = 1 * split[0];
|
||||
if (isNaN(hours)) {
|
||||
return NaN;
|
||||
}
|
||||
split = split[1].split("m");
|
||||
break;
|
||||
default:
|
||||
return NaN;
|
||||
}
|
||||
|
||||
switch (split.length) {
|
||||
case 1:
|
||||
minutes = 0;
|
||||
split = split[0].split("s");
|
||||
break;
|
||||
case 2:
|
||||
minutes = 1 * split[0];
|
||||
if (isNaN(minutes)) {
|
||||
return NaN;
|
||||
}
|
||||
split = split[1].split("s");
|
||||
break;
|
||||
default:
|
||||
return NaN;
|
||||
}
|
||||
|
||||
seconds = 1 * split[0];
|
||||
if ((split.length !== 1 && (! (split.length == 2 && split[1] === ""))) || isNaN(seconds)) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
return 3600 * hours + 60 * minutes + seconds;
|
||||
}
|
||||
|
||||
vd.setup = function(video, args) {
|
||||
|
||||
let src;
|
||||
|
||||
@@ -271,10 +365,16 @@ const vd = (function() {
|
||||
}
|
||||
|
||||
let player = videojs(video);
|
||||
if (src.endsWith('.mpd')) {
|
||||
player.src({
|
||||
src,
|
||||
type: 'application/dash+xml'
|
||||
});
|
||||
} else {
|
||||
player.src({
|
||||
src,
|
||||
});
|
||||
}
|
||||
|
||||
if (args.focus === true) {
|
||||
player.focus();
|
||||
@@ -311,7 +411,7 @@ const vd = (function() {
|
||||
};
|
||||
|
||||
if (args.t !== undefined) {
|
||||
let time = parseFloat(args.t);
|
||||
let time = vd.parseTime(args.t);
|
||||
if (!isNaN(time)) {
|
||||
player.currentTime(time);
|
||||
}
|
||||
@@ -324,12 +424,18 @@ const vd = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
let p = document.getElementById(player.id());
|
||||
if (p === null) {
|
||||
return;
|
||||
}
|
||||
p = typeof p.player === "function" ? p.player() : p.player;
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 37: e.preventDefault(); player.currentTime(player.currentTime() - 5); break;
|
||||
case 39: e.preventDefault(); player.currentTime(player.currentTime() + 5); break;
|
||||
case 32: e.preventDefault(); if (player.paused()) player.play(); else player.pause(); break;
|
||||
case 40: e.preventDefault(); player.volume(player.volume() - 0.1); break;
|
||||
case 38: e.preventDefault(); player.volume(player.volume() + 0.1); break;
|
||||
case 37: e.preventDefault(); p.currentTime(p.currentTime() - 5); break;
|
||||
case 39: e.preventDefault(); p.currentTime(p.currentTime() + 5); break;
|
||||
case 32: e.preventDefault(); if (p.paused()) p.play(); else p.pause(); break;
|
||||
case 40: e.preventDefault(); p.volume(p.volume() - 0.1); break;
|
||||
case 38: e.preventDefault(); p.volume(p.volume() + 0.1); break;
|
||||
}
|
||||
}, true);
|
||||
|
||||
@@ -338,20 +444,22 @@ const vd = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
let p = document.getElementById(player.id());
|
||||
if (p === null) {
|
||||
return;
|
||||
}
|
||||
p = typeof p.player === "function" ? p.player() : p.player;
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 74: e.preventDefault(); player.currentTime(player.currentTime() - 10); break; // J -> -10s
|
||||
case 76: e.preventDefault(); player.currentTime(player.currentTime() + 10); break; // L -> +10s
|
||||
case 75: e.preventDefault(); if (player.paused()) player.play(); else player.pause(); break; // K -> play/pause
|
||||
case 77: e.preventDefault(); player.muted(!player.muted()); break; // M -> mute
|
||||
case 74: e.preventDefault(); p.currentTime(p.currentTime() - 10); break; // J -> -10s
|
||||
case 76: e.preventDefault(); p.currentTime(p.currentTime() + 10); break; // L -> +10s
|
||||
case 75: e.preventDefault(); if (p.paused()) p.play(); else p.pause(); break; // K -> play/pause
|
||||
case 77: e.preventDefault(); p.muted(!p.muted()); break; // M -> mute
|
||||
|
||||
// F -> toggle fullscreen
|
||||
case 70:
|
||||
e.preventDefault();
|
||||
let p = document.getElementById(player.id());
|
||||
if (p === null) {
|
||||
break;
|
||||
}
|
||||
p = typeof p.player === "function" ? p.player() : p.player;
|
||||
|
||||
if (p.isFullscreen()) {
|
||||
p.exitFullscreen();
|
||||
} else {
|
||||
@@ -360,16 +468,16 @@ const vd = (function() {
|
||||
break;
|
||||
|
||||
// Seek shortcuts
|
||||
case 48: case 96: e.preventDefault(); player.currentTime(0); break;
|
||||
case 49: case 97: e.preventDefault(); player.currentTime( player.duration() / 10); break;
|
||||
case 50: case 98: e.preventDefault(); player.currentTime(2 * player.duration() / 10); break;
|
||||
case 51: case 99: e.preventDefault(); player.currentTime(3 * player.duration() / 10); break;
|
||||
case 52: case 100: e.preventDefault(); player.currentTime(4 * player.duration() / 10); break;
|
||||
case 53: case 101: e.preventDefault(); player.currentTime(5 * player.duration() / 10); break;
|
||||
case 54: case 102: e.preventDefault(); player.currentTime(6 * player.duration() / 10); break;
|
||||
case 55: case 103: e.preventDefault(); player.currentTime(7 * player.duration() / 10); break;
|
||||
case 56: case 104: e.preventDefault(); player.currentTime(8 * player.duration() / 10); break;
|
||||
case 57: case 105: e.preventDefault(); player.currentTime(9 * player.duration() / 10); break;
|
||||
case 48: case 96: e.preventDefault(); p.currentTime(0); break;
|
||||
case 49: case 97: e.preventDefault(); p.currentTime( p.duration() / 10); break;
|
||||
case 50: case 98: e.preventDefault(); p.currentTime(2 * p.duration() / 10); break;
|
||||
case 51: case 99: e.preventDefault(); p.currentTime(3 * p.duration() / 10); break;
|
||||
case 52: case 100: e.preventDefault(); p.currentTime(4 * p.duration() / 10); break;
|
||||
case 53: case 101: e.preventDefault(); p.currentTime(5 * p.duration() / 10); break;
|
||||
case 54: case 102: e.preventDefault(); p.currentTime(6 * p.duration() / 10); break;
|
||||
case 55: case 103: e.preventDefault(); p.currentTime(7 * p.duration() / 10); break;
|
||||
case 56: case 104: e.preventDefault(); p.currentTime(8 * p.duration() / 10); break;
|
||||
case 57: case 105: e.preventDefault(); p.currentTime(9 * p.duration() / 10); break;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
@@ -381,6 +489,8 @@ const vd = (function() {
|
||||
|
||||
if (player.getAttribute('disable-thumbnails') == undefined) {
|
||||
|
||||
player.poster(root + "poster.png");
|
||||
|
||||
let thumbnails = [];
|
||||
const MAX_INDEX = 100;
|
||||
for (let i = 0; i <= MAX_INDEX; i++) {
|
||||
@@ -422,12 +532,15 @@ const vd = (function() {
|
||||
let controlBar = player.getChild('controlBar');
|
||||
let fullscreenButton = controlBar.children()[controlBar.children().length - 1];
|
||||
controlBar.removeChild(fullscreenButton);
|
||||
let menuButton = new MenuButton(player);
|
||||
|
||||
let menuButton = createRepresentationButtons(player);
|
||||
if (player.getAttribute('disable-speed') == undefined) {
|
||||
let speedButton = createSpeedButtons(player);
|
||||
controlBar.addChild(speedButton, {});
|
||||
}
|
||||
controlBar.addChild(menuButton, {});
|
||||
controlBar.addChild(fullscreenButton, {});
|
||||
|
||||
setupRepresentations(menuButton, player);
|
||||
// videojs.Html5DashJS.hook('beforeinitialize', (p, mp) => setupRepresentations(menuButton, p, mp));
|
||||
window.player = player;
|
||||
|
||||
if (video.getAttribute('autoplay') != undefined) {
|
||||
@@ -435,15 +548,15 @@ const vd = (function() {
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
for (let element of document.getElementsByTagName('video')) {
|
||||
for (let element of document.getElementsByTagName('video')) {
|
||||
let src = element.getAttribute('data-dash-src');
|
||||
if (src != undefined) {
|
||||
vd.setup(element, src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return vd;
|
||||
|
||||
})();
|
||||
|
||||
+34
-8
@@ -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
|
||||
|
||||
+151
-52
@@ -1,50 +1,78 @@
|
||||
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
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ playlists : List Twitch.Playlist
|
||||
, zone : Time.Zone
|
||||
, 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 (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
|
||||
|
||||
|
||||
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 +80,109 @@ 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 : Model -> Sub Msg
|
||||
subscriptions _ =
|
||||
Events.onResize (\w h -> SizeReceived w h)
|
||||
Sub.batch
|
||||
[ Events.onResize (\w h -> SizeReceived w h)
|
||||
, Time.every 200 TimeReceived
|
||||
, Ports.darkMode DarkMode
|
||||
]
|
||||
|
||||
|
||||
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 Nothing ->
|
||||
( { model | page = Video p v (Just (Hover.hover v 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 _ ->
|
||||
( { model | page = Video p v 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 +199,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 +221,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
|
||||
@@ -143,31 +231,29 @@ update msg model =
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
( page, cmd ) =
|
||||
page =
|
||||
case ( playlist, video ) of
|
||||
( Just p, Just v ) ->
|
||||
( Video p v
|
||||
, Ports.registerVideo ( Twitch.videoId v, "videos/" ++ p.url ++ v.url, time )
|
||||
)
|
||||
Video p v Nothing
|
||||
|
||||
( Just p, Nothing ) ->
|
||||
( Playlist p, Cmd.none )
|
||||
Playlist p Nothing
|
||||
|
||||
_ ->
|
||||
( Home, Cmd.none )
|
||||
|
||||
extraCmd =
|
||||
case page of
|
||||
Video _ _ ->
|
||||
Cmd.none
|
||||
|
||||
_ ->
|
||||
Ports.eraseVideo ()
|
||||
Home Nothing
|
||||
in
|
||||
( Loaded { m | page = page }, Cmd.batch [ cmd, extraCmd ] )
|
||||
( { model | page = page }, Cmd.none )
|
||||
|
||||
_ ->
|
||||
( model, Cmd.none )
|
||||
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 )
|
||||
|
||||
|
||||
splitter : String -> Maybe ( String, String )
|
||||
@@ -183,3 +269,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
|
||||
|
||||
@@ -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 }
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,7 +1,13 @@
|
||||
port module Ports exposing (eraseVideo, registerVideo)
|
||||
port module Ports exposing (darkMode, eraseVideo, registerVideo, setDarkMode)
|
||||
|
||||
|
||||
port registerVideo : ( String, String, Maybe Int ) -> Cmd msg
|
||||
port registerVideo : ( String, String, Maybe String ) -> Cmd msg
|
||||
|
||||
|
||||
port eraseVideo : () -> Cmd msg
|
||||
|
||||
|
||||
port setDarkMode : Maybe Bool -> Cmd msg
|
||||
|
||||
|
||||
port darkMode : (Bool -> msg) -> Sub msg
|
||||
|
||||
+71
-7
@@ -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
@@ -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
|
||||
}
|
||||
+177
-102
@@ -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 Hover exposing (Hover)
|
||||
import Html
|
||||
import Html.Attributes
|
||||
import Json.Encode as Encode
|
||||
import Time
|
||||
import TimeUtils
|
||||
import Twitch
|
||||
import Ui
|
||||
|
||||
|
||||
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 hover ->
|
||||
videoView model.darkMode model.device model.zone model.currentDate model.time hover playlist video
|
||||
|
||||
|
||||
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,41 @@ 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")
|
||||
, Events.onMouseEnter (Core.HoverVideo video)
|
||||
, 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 }
|
||||
)
|
||||
@@ -243,53 +314,53 @@ 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
|
||||
[ videoMiniature currentDate time hover playlist video
|
||||
, videoDescription 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 ) =
|
||||
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 =
|
||||
Element.row [ Element.width Element.fill, Element.spacing 10 ]
|
||||
[ 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 ]
|
||||
(videoDescription zone video)
|
||||
(videoDescription darkMode zone video)
|
||||
]
|
||||
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 device zone playlist video =
|
||||
videoView : Bool -> Element.Device -> Time.Zone -> Time.Posix -> Time.Posix -> Maybe (Hover Twitch.Video) -> Twitch.Playlist -> Twitch.Video -> Element Core.Msg
|
||||
videoView darkMode device zone currentDate time hover playlist video =
|
||||
let
|
||||
( builder, contentPadding ) =
|
||||
case device.class of
|
||||
@@ -310,29 +381,11 @@ videoView device zone playlist video =
|
||||
, 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
|
||||
]
|
||||
[ Element.html
|
||||
(Html.node "plyr-video"
|
||||
[ Html.Attributes.attribute "src" ("videos/" ++ playlist.url ++ video.url ++ "manifest.m3u8") ]
|
||||
[]
|
||||
)
|
||||
)
|
||||
, Element.paragraph
|
||||
[ Font.size Consts.homeFontSize
|
||||
, Font.bold
|
||||
@@ -356,12 +409,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 =
|
||||
videoDescription : Bool -> Time.Zone -> Twitch.Video -> Element Core.Msg
|
||||
videoDescription darkMode zone video =
|
||||
Element.column [ Element.spacing 10 ]
|
||||
[ Element.paragraph
|
||||
[ Font.bold
|
||||
@@ -371,7 +424,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) ]
|
||||
|
||||
@@ -522,3 +575,25 @@ spinner =
|
||||
, Html.div [] []
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user