mirror of
https://gitea.invidious.io/iv-org/invidious.git
synced 2024-08-15 00:53:41 +00:00
Add support for Web notifications
This commit is contained in:
parent
b3788bc143
commit
0338fd42e1
22 changed files with 456 additions and 30 deletions
|
@ -1,4 +1,4 @@
|
|||
function get_playlist(plid, timeouts = 0) {
|
||||
function get_playlist(plid, timeouts) {
|
||||
if (timeouts > 10) {
|
||||
console.log('Failed to pull playlist');
|
||||
return;
|
||||
|
@ -53,7 +53,7 @@ function get_playlist(plid, timeouts = 0) {
|
|||
|
||||
xhr.ontimeout = function () {
|
||||
console.log('Pulling playlist timed out.');
|
||||
get_playlist(plid, timeouts + 1);
|
||||
get_playlist(plid, timeouts++);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
139
assets/js/notifications.js
Normal file
139
assets/js/notifications.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
var notifications, delivered;
|
||||
|
||||
function get_subscriptions(callback, failures) {
|
||||
if (failures >= 10) {
|
||||
return
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.timeout = 20000;
|
||||
xhr.open('GET', '/api/v1/auth/subscriptions', true);
|
||||
xhr.send(null);
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
subscriptions = xhr.response;
|
||||
callback(subscriptions);
|
||||
} else {
|
||||
console.log('Pulling subscriptions failed... ' + failures + '/10');
|
||||
get_subscriptions(callback, failures++)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xhr.ontimeout = function () {
|
||||
console.log('Pulling subscriptions failed... ' + failures + '/10');
|
||||
get_subscriptions(callback, failures++);
|
||||
}
|
||||
}
|
||||
|
||||
function create_notification_stream(subscriptions) {
|
||||
notifications = new SSE(
|
||||
'/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', {
|
||||
withCredentials: true,
|
||||
payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId }).join(','),
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
delivered = [];
|
||||
|
||||
var start_time = Math.round(new Date() / 1000);
|
||||
|
||||
notifications.onmessage = function (event) {
|
||||
if (!event.id) {
|
||||
return
|
||||
}
|
||||
|
||||
var notification = JSON.parse(event.data);
|
||||
console.log('Got notification:', notification);
|
||||
|
||||
if (start_time < notification.published && !delivered.includes(notification.videoId)) {
|
||||
if (Notification.permission === 'granted') {
|
||||
var system_notification =
|
||||
new Notification((notification.liveNow ? notification_data.live_now_text : notification_data.upload_text).replace('`x`', notification.author), {
|
||||
body: notification.title,
|
||||
icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname,
|
||||
img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname,
|
||||
tag: notification.videoId
|
||||
});
|
||||
|
||||
system_notification.onclick = function (event) {
|
||||
window.open('/watch?v=' + event.currentTarget.tag, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
delivered.push(notification.videoId);
|
||||
localStorage.setItem('notification_count', parseInt(localStorage.getItem('notification_count') || '0') + 1);
|
||||
var notification_ticker = document.getElementById('notification_ticker');
|
||||
|
||||
if (parseInt(localStorage.getItem('notification_count')) > 0) {
|
||||
notification_ticker.innerHTML =
|
||||
'<span id="notification_count">' + localStorage.getItem('notification_count') + '</span> <i class="icon ion-ios-notifications"></i>';
|
||||
} else {
|
||||
notification_ticker.innerHTML =
|
||||
'<i class="icon ion-ios-notifications-outline"></i>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifications.onerror = function (event) {
|
||||
console.log('Something went wrong with notifications, trying to reconnect...');
|
||||
notifications.close();
|
||||
get_subscriptions(create_notification_stream);
|
||||
}
|
||||
|
||||
notifications.ontimeout = function (event) {
|
||||
console.log('Something went wrong with notifications, trying to reconnect...');
|
||||
notifications.close();
|
||||
get_subscriptions(create_notification_stream);
|
||||
}
|
||||
|
||||
notifications.stream();
|
||||
}
|
||||
|
||||
window.addEventListener('storage', function (e) {
|
||||
if (e.key === 'stream' && !e.newValue) {
|
||||
if (notifications) {
|
||||
localStorage.setItem('stream', true);
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
if (!localStorage.getItem('stream')) {
|
||||
get_subscriptions(create_notification_stream);
|
||||
localStorage.setItem('stream', true);
|
||||
}
|
||||
}, Math.random() * 1000 + 10);
|
||||
}
|
||||
} else if (e.key === 'notification_count') {
|
||||
var notification_ticker = document.getElementById('notification_ticker');
|
||||
|
||||
if (parseInt(e.newValue) > 0) {
|
||||
notification_ticker.innerHTML =
|
||||
'<span id="notification_count">' + e.newValue + '</span> <i class="icon ion-ios-notifications"></i>';
|
||||
} else {
|
||||
notification_ticker.innerHTML =
|
||||
'<i class="icon ion-ios-notifications-outline"></i>';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('load', function (e) {
|
||||
localStorage.setItem('notification_count', document.getElementById('notification_count') ? document.getElementById('notification_count').innerText : '0');
|
||||
|
||||
if (localStorage.getItem('stream')) {
|
||||
localStorage.removeItem('stream');
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
if (!localStorage.getItem('stream')) {
|
||||
get_subscriptions(create_notification_stream);
|
||||
localStorage.setItem('stream', true);
|
||||
}
|
||||
}, Math.random() * 1000 + 10);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('unload', function (e) {
|
||||
if (notifications) {
|
||||
localStorage.removeItem('stream');
|
||||
}
|
||||
});
|
200
assets/js/sse.js
Normal file
200
assets/js/sse.js
Normal file
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* Copyright (C) 2016 Maxime Petazzoni <maxime.petazzoni@bulix.org>.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
var SSE = function (url, options) {
|
||||
if (!(this instanceof SSE)) {
|
||||
return new SSE(url, options);
|
||||
}
|
||||
|
||||
this.INITIALIZING = -1;
|
||||
this.CONNECTING = 0;
|
||||
this.OPEN = 1;
|
||||
this.CLOSED = 2;
|
||||
|
||||
this.url = url;
|
||||
|
||||
options = options || {};
|
||||
this.headers = options.headers || {};
|
||||
this.payload = options.payload !== undefined ? options.payload : '';
|
||||
this.method = options.method || (this.payload && 'POST' || 'GET');
|
||||
|
||||
this.FIELD_SEPARATOR = ':';
|
||||
this.listeners = {};
|
||||
|
||||
this.xhr = null;
|
||||
this.readyState = this.INITIALIZING;
|
||||
this.progress = 0;
|
||||
this.chunk = '';
|
||||
|
||||
this.addEventListener = function(type, listener) {
|
||||
if (this.listeners[type] === undefined) {
|
||||
this.listeners[type] = [];
|
||||
}
|
||||
|
||||
if (this.listeners[type].indexOf(listener) === -1) {
|
||||
this.listeners[type].push(listener);
|
||||
}
|
||||
};
|
||||
|
||||
this.removeEventListener = function(type, listener) {
|
||||
if (this.listeners[type] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var filtered = [];
|
||||
this.listeners[type].forEach(function(element) {
|
||||
if (element !== listener) {
|
||||
filtered.push(element);
|
||||
}
|
||||
});
|
||||
if (filtered.length === 0) {
|
||||
delete this.listeners[type];
|
||||
} else {
|
||||
this.listeners[type] = filtered;
|
||||
}
|
||||
};
|
||||
|
||||
this.dispatchEvent = function(e) {
|
||||
if (!e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
e.source = this;
|
||||
|
||||
var onHandler = 'on' + e.type;
|
||||
if (this.hasOwnProperty(onHandler)) {
|
||||
this[onHandler].call(this, e);
|
||||
if (e.defaultPrevented) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.listeners[e.type]) {
|
||||
return this.listeners[e.type].every(function(callback) {
|
||||
callback(e);
|
||||
return !e.defaultPrevented;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
this._setReadyState = function (state) {
|
||||
var event = new CustomEvent('readystatechange');
|
||||
event.readyState = state;
|
||||
this.readyState = state;
|
||||
this.dispatchEvent(event);
|
||||
};
|
||||
|
||||
this._onStreamFailure = function(e) {
|
||||
this.dispatchEvent(new CustomEvent('error'));
|
||||
this.close();
|
||||
}
|
||||
|
||||
this._onStreamProgress = function(e) {
|
||||
if (this.xhr.status !== 200) {
|
||||
this._onStreamFailure(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.readyState == this.CONNECTING) {
|
||||
this.dispatchEvent(new CustomEvent('open'));
|
||||
this._setReadyState(this.OPEN);
|
||||
}
|
||||
|
||||
var data = this.xhr.responseText.substring(this.progress);
|
||||
this.progress += data.length;
|
||||
data.split(/(\r\n|\r|\n){2}/g).forEach(function(part) {
|
||||
if (part.trim().length === 0) {
|
||||
this.dispatchEvent(this._parseEventChunk(this.chunk.trim()));
|
||||
this.chunk = '';
|
||||
} else {
|
||||
this.chunk += part;
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
this._onStreamLoaded = function(e) {
|
||||
this._onStreamProgress(e);
|
||||
|
||||
// Parse the last chunk.
|
||||
this.dispatchEvent(this._parseEventChunk(this.chunk));
|
||||
this.chunk = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a received SSE event chunk into a constructed event object.
|
||||
*/
|
||||
this._parseEventChunk = function(chunk) {
|
||||
if (!chunk || chunk.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var e = {'id': null, 'retry': null, 'data': '', 'event': 'message'};
|
||||
chunk.split(/\n|\r\n|\r/).forEach(function(line) {
|
||||
line = line.trimRight();
|
||||
var index = line.indexOf(this.FIELD_SEPARATOR);
|
||||
if (index <= 0) {
|
||||
// Line was either empty, or started with a separator and is a comment.
|
||||
// Either way, ignore.
|
||||
return;
|
||||
}
|
||||
|
||||
var field = line.substring(0, index);
|
||||
if (!(field in e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var value = line.substring(index + 1).trimLeft();
|
||||
if (field === 'data') {
|
||||
e[field] += value;
|
||||
} else {
|
||||
e[field] = value;
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
var event = new CustomEvent(e.event);
|
||||
event.data = e.data;
|
||||
event.id = e.id;
|
||||
return event;
|
||||
};
|
||||
|
||||
this._checkStreamClosed = function() {
|
||||
if (this.xhr.readyState === XMLHttpRequest.DONE) {
|
||||
this._setReadyState(this.CLOSED);
|
||||
}
|
||||
};
|
||||
|
||||
this.stream = function() {
|
||||
this._setReadyState(this.CONNECTING);
|
||||
|
||||
this.xhr = new XMLHttpRequest();
|
||||
this.xhr.addEventListener('progress', this._onStreamProgress.bind(this));
|
||||
this.xhr.addEventListener('load', this._onStreamLoaded.bind(this));
|
||||
this.xhr.addEventListener('readystatechange', this._checkStreamClosed.bind(this));
|
||||
this.xhr.addEventListener('error', this._onStreamFailure.bind(this));
|
||||
this.xhr.addEventListener('abort', this._onStreamFailure.bind(this));
|
||||
this.xhr.open(this.method, this.url);
|
||||
for (var header in this.headers) {
|
||||
this.xhr.setRequestHeader(header, this.headers[header]);
|
||||
}
|
||||
this.xhr.send(this.payload);
|
||||
};
|
||||
|
||||
this.close = function() {
|
||||
if (this.readyState === this.CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.xhr.abort();
|
||||
this.xhr = null;
|
||||
this._setReadyState(this.CLOSED);
|
||||
};
|
||||
};
|
||||
|
||||
// Export our SSE module for npm.js
|
||||
if (typeof exports !== 'undefined') {
|
||||
exports.SSE = SSE;
|
||||
}
|
|
@ -7,8 +7,8 @@ if (subscribe_button.getAttribute('data-type') === 'subscribe') {
|
|||
subscribe_button.onclick = unsubscribe;
|
||||
}
|
||||
|
||||
function subscribe(timeouts = 0) {
|
||||
if (timeouts > 10) {
|
||||
function subscribe(timeouts) {
|
||||
if (timeouts >= 10) {
|
||||
console.log('Failed to subscribe.');
|
||||
return;
|
||||
}
|
||||
|
@ -37,12 +37,12 @@ function subscribe(timeouts = 0) {
|
|||
|
||||
xhr.ontimeout = function () {
|
||||
console.log('Subscribing timed out.');
|
||||
subscribe(timeouts + 1);
|
||||
subscribe(timeouts++);
|
||||
}
|
||||
}
|
||||
|
||||
function unsubscribe(timeouts = 0) {
|
||||
if (timeouts > 10) {
|
||||
function unsubscribe(timeouts) {
|
||||
if (timeouts >= 10) {
|
||||
console.log('Failed to subscribe');
|
||||
return;
|
||||
}
|
||||
|
@ -71,6 +71,6 @@ function unsubscribe(timeouts = 0) {
|
|||
|
||||
xhr.ontimeout = function () {
|
||||
console.log('Unsubscribing timed out.');
|
||||
unsubscribe(timeouts + 1);
|
||||
unsubscribe(timeouts++);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "فقط اظهر اخر فيديو لم يتم رؤيتة من القناة: ",
|
||||
"Only show unwatched: ": "فقط اظهر الذى لم يتم رؤيتة: ",
|
||||
"Only show notifications (if there are any): ": "إظهار الإشعارات فقط (إذا كان هناك أي): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "إعدادات التفضيلات",
|
||||
"Clear watch history": "حذف سجل المشاهدة",
|
||||
"Import/export data": "إضافة\\إستخراج البيانات",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Nur neueste ungesehene Videos des Kanals anzeigen: ",
|
||||
"Only show unwatched: ": "Nur ungesehene anzeigen: ",
|
||||
"Only show notifications (if there are any): ": "Nur Benachrichtigungen anzeigen (wenn es welche gibt): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Dateneinstellungen",
|
||||
"Clear watch history": "Verlauf löschen",
|
||||
"Import/export data": "Daten im- exportieren",
|
||||
|
|
|
@ -91,6 +91,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Προβολή μόνο του τελευταίου μη-προβεβλημένου βίντεο του καναλιού: ",
|
||||
"Only show unwatched: ": "Προβολή μόνο μη-προβεβλημένων: ",
|
||||
"Only show notifications (if there are any): ": "Προβολή μόνο ειδοποιήσεων (αν υπάρχουν): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Προτιμήσεις δεδομένων",
|
||||
"Clear watch history": "Εκκαθάριση ιστορικού προβολής",
|
||||
"Import/export data": "Εισαγωγή/εξαγωγή δεδομένων",
|
||||
|
|
|
@ -91,6 +91,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Only show latest unwatched video from channel: ",
|
||||
"Only show unwatched: ": "Only show unwatched: ",
|
||||
"Only show notifications (if there are any): ": "Only show notifications (if there are any): ",
|
||||
"Enable web notifications": "Enable web notifications",
|
||||
"`x` uploaded a video": "`x` uploaded a video",
|
||||
"`x` is live": "`x` is live",
|
||||
"Data preferences": "Data preferences",
|
||||
"Clear watch history": "Clear watch history",
|
||||
"Import/export data": "Import/export data",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Nur montri pli novan malviditan videon el kanalo: ",
|
||||
"Only show unwatched: ": "Nur montri malviditajn: ",
|
||||
"Only show notifications (if there are any): ": "Nur montri sciigojn (se estas): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Datumagordoj",
|
||||
"Clear watch history": "Forigi vidohistorion",
|
||||
"Import/export data": "Importi/Eksporti datumojn",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Mostrar solo el último vídeo sin ver del canal: ",
|
||||
"Only show unwatched: ": "Mostrar solo los no vistos: ",
|
||||
"Only show notifications (if there are any): ": "Mostrar solo notificaciones (si hay alguna): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Preferencias de los datos",
|
||||
"Clear watch history": "Borrar el historial de reproducción",
|
||||
"Import/export data": "Importar/Exportar datos",
|
||||
|
@ -312,4 +315,4 @@
|
|||
"Videos": "Vídeos",
|
||||
"Playlists": "Listas de reproducción",
|
||||
"Current version: ": "Versión actual: "
|
||||
}
|
||||
}
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "",
|
||||
"Only show unwatched: ": "",
|
||||
"Only show notifications (if there are any): ": "",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "",
|
||||
"Clear watch history": "",
|
||||
"Import/export data": "",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Afficher uniquement la dernière vidéo de la chaîne non regardée : ",
|
||||
"Only show unwatched: ": "Afficher uniquement les vidéos non regardées : ",
|
||||
"Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a) : ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Préférences liées aux données",
|
||||
"Clear watch history": "Supprimer l'historique des vidéos regardées",
|
||||
"Import/export data": "Importer/exporter les données",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Mostra solo il video più recente non guardato del canale: ",
|
||||
"Only show unwatched: ": "Mostra solo i video non guardati: ",
|
||||
"Only show notifications (if there are any): ": "Mostra solo le notifiche (se presenti): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Preferenze dati",
|
||||
"Clear watch history": "Cancella la cronologia dei video guardati",
|
||||
"Import/export data": "Importazione/esportazione dati",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Kun vis siste usette video fra kanal: ",
|
||||
"Only show unwatched: ": "Kun vis usette: ",
|
||||
"Only show notifications (if there are any): ": "Kun vis merknader (hvis det er noen): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Datainnstillinger",
|
||||
"Clear watch history": "Tøm visningshistorikk",
|
||||
"Import/export data": "Importer/eksporter data",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Alleen nieuwste niet-bekeken video van kanaal tonen: ",
|
||||
"Only show unwatched: ": "Alleen niet-bekeken videos tonen: ",
|
||||
"Only show notifications (if there are any): ": "Alleen meldingen tonen (als die er zijn): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Gegevensinstellingen",
|
||||
"Clear watch history": "Kijkgeschiedenis wissen",
|
||||
"Import/export data": "Gegevens im-/exporteren",
|
||||
|
@ -312,4 +315,4 @@
|
|||
"Videos": "Video's",
|
||||
"Playlists": "Afspeellijsten",
|
||||
"Current version: ": "Huidige versie: "
|
||||
}
|
||||
}
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Pokazuj tylko najnowszy nie obejrzany film z kanału: ",
|
||||
"Only show unwatched: ": "Pokazuj tylko nie obejrzane: ",
|
||||
"Only show notifications (if there are any): ": "Pokazuj tylko powiadomienia (jeśli są): ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Preferencje danych",
|
||||
"Clear watch history": "Wyczyść historię",
|
||||
"Import/export data": "Import/Eksport danych",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Показывать только непросмотренные видео с каналов: ",
|
||||
"Only show unwatched: ": "Показывать только непросмотренные видео: ",
|
||||
"Only show notifications (if there are any): ": "Показывать только оповещения, если они есть: ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Настройки данных",
|
||||
"Clear watch history": "Очистить историю просмотров",
|
||||
"Import/export data": "Импорт/Экспорт данных",
|
||||
|
|
|
@ -85,6 +85,9 @@
|
|||
"Only show latest unwatched video from channel: ": "Показувати тільки непереглянуті відео з каналів: ",
|
||||
"Only show unwatched: ": "Показувати тільки непереглянуті відео: ",
|
||||
"Only show notifications (if there are any): ": "Показувати лише сповіщення, якщо вони є: ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "",
|
||||
"`x` is live": "",
|
||||
"Data preferences": "Налаштування даних",
|
||||
"Clear watch history": "Очистити історію переглядів",
|
||||
"Import/export data": "Імпорт і експорт даних",
|
||||
|
|
|
@ -2,24 +2,24 @@
|
|||
<html lang="<%= env.get("preferences").as(Preferences).locale %>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="thumbnail" content="<%= thumbnail %>">
|
||||
<%= rendered "components/player_sources" %>
|
||||
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
|
||||
<title><%= HTML.escape(video.title) %> - Invidious</title>
|
||||
<style>
|
||||
#player {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: -100;
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="thumbnail" content="<%= thumbnail %>">
|
||||
<%= rendered "components/player_sources" %>
|
||||
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
|
||||
<title><%= HTML.escape(video.title) %> - Invidious</title>
|
||||
<style>
|
||||
#player {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: -100;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -23,6 +23,20 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/js/notifications.js?v=<%= ASSET_COMMIT %>">notifications.js</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="/js/notifications.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/js/player.js?v=<%= ASSET_COMMIT %>">player.js</a>
|
||||
|
@ -51,6 +65,20 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/js/sse.js?v=<%= ASSET_COMMIT %>">sse.js</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="http://www.jclark.com/xml/copying.txt">Expat</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/mpetazzoni/sse.js"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/js/subscribe_widget.js?v=<%= ASSET_COMMIT %>">subscribe_widget.js</a>
|
||||
|
|
|
@ -165,6 +165,13 @@ function update_value(element) {
|
|||
<label for="notifications_only"><%= translate(locale, "Only show notifications (if there are any): ") %></label>
|
||||
<input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<% # Conditions for supporting web notifications %>
|
||||
<% if CONFIG.use_pubsub_feeds && (Kemal.config.ssl || config.https_only) %>
|
||||
<div class="pure-control-group">
|
||||
<a href="#" onclick="Notification.requestPermission()"><%= translate(locale, "Enable web notifications") %></a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if env.get?("user") && config.admins.includes? env.get?("user").as(User).email %>
|
||||
|
|
|
@ -51,10 +51,10 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
|
||||
<a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
|
||||
<% notification_count = env.get("user").as(User).notifications.size %>
|
||||
<% if notification_count > 0 %>
|
||||
<%= notification_count %> <i class="icon ion-ios-notifications"></i>
|
||||
<span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i>
|
||||
<% else %>
|
||||
<i class="icon ion-ios-notifications-outline"></i>
|
||||
<% end %>
|
||||
|
@ -151,6 +151,16 @@
|
|||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||
</div>
|
||||
<script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
<% if env.get? "user" %>
|
||||
<script src="/js/sse.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
<script>
|
||||
var notification_data = {
|
||||
upload_text: '<%= HTML.escape(translate(locale, "`x` uploaded a video")) %>',
|
||||
live_upload_text: '<%= HTML.escape(translate(locale, "`x` is live")) %>',
|
||||
}
|
||||
</script>
|
||||
<script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
<% end %>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue