From 14f72baf162a9fba23cb648c3ac13e1620d3ac47 Mon Sep 17 00:00:00 2001 From: Mike Abrahall Date: Fri, 21 Feb 2020 16:08:23 +1100 Subject: [PATCH 0001/2481] Added extra playback keybindings, following YouTube's spec at https://support.google.com/youtube/answer/7631406?hl=en --- src/controllers/playback/videoosd.js | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/controllers/playback/videoosd.js b/src/controllers/playback/videoosd.js index 27652958d..3c3e88bf7 100644 --- a/src/controllers/playback/videoosd.js +++ b/src/controllers/playback/videoosd.js @@ -1119,6 +1119,14 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med playbackManager.playPause(currentPlayer); showOsd(); break; + case "ArrowUp": + case "Up": + playbackManager.volumeUp(currentPlayer); + break; + case "ArrowDown": + case "Down": + playbackManager.volumeDown(currentPlayer); + break; case "l": case "ArrowRight": case "Right": @@ -1141,6 +1149,18 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med playbackManager.toggleMute(currentPlayer); showOsd(); break; + case "p": + case "P": + if (e.shiftKey) { + playbackManager.previousTrack(currentPlayer); + } + break; + case "n": + case "N": + if (e.shiftKey) { + playbackManager.nextTrack(currentPlayer); + } + break; case "NavigationLeft": case "GamepadDPadLeft": case "GamepadLeftThumbstickLeft": @@ -1158,6 +1178,29 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med playbackManager.fastForward(currentPlayer); showOsd(); } + // + case "Home": + playbackManager.seekPercent(0, currentPlayer); + break; + case "End": + // Honestly this binding seems kind of pointless, just use 'nextTrack', + // but YouTube has this one and that's the standard I'm aiming for + playbackManager.seekPercent(100, currentPlayer); + break; + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + var percent = parseInt(key); + percent *= 10; + playbackManager.seekPercent(percent, currentPlayer); + break; } } From 02baff1fe0c5a22d9b1563f7101d0a01824e34cd Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Thu, 9 Apr 2020 00:35:54 +0800 Subject: [PATCH 0002/2481] add more separate hw decoding toggles --- src/controllers/dashboard/dashboard.js | 12 +++--------- src/controllers/encodingsettings.js | 2 ++ src/encodingsettings.html | 23 ++++++++++++++++------- src/strings/en-us.json | 4 +++- src/strings/zh-cn.json | 4 +++- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/controllers/dashboard/dashboard.js b/src/controllers/dashboard/dashboard.js index 78f5cdca0..addcfc6e7 100644 --- a/src/controllers/dashboard/dashboard.js +++ b/src/controllers/dashboard/dashboard.js @@ -258,12 +258,6 @@ define(["datetime", "events", "itemHelper", "serverNotifications", "dom", "globa html += ""; html += ""; - if (session.TranscodingInfo && session.TranscodingInfo.Framerate) { - html += '
' + session.TranscodingInfo.Framerate + " fps
"; - } else { - html += '
'; - } - html += '
'; var nowPlayingName = DashboardPage.getNowPlayingName(session); html += '
'; @@ -398,9 +392,9 @@ define(["datetime", "events", "itemHelper", "serverNotifications", "dom", "globa } else if (displayPlayMethod === "Transcode") { html += globalize.translate("Transcoding"); - if (session.TranscodingInfo && session.TranscodingInfo.Framerate) { - html += " (" + session.TranscodingInfo.Framerate + " fps)"; - } + if (session.TranscodingInfo && session.TranscodingInfo.Framerate) { + html += " (" + session.TranscodingInfo.Framerate + " fps)"; + } showTranscodingInfo = true; } else if (displayPlayMethod === "DirectPlay") { diff --git a/src/controllers/encodingsettings.js b/src/controllers/encodingsettings.js index 1b2718866..5a540a5e2 100644 --- a/src/controllers/encodingsettings.js +++ b/src/controllers/encodingsettings.js @@ -5,6 +5,7 @@ define(["jQuery", "loading", "globalize", "dom", "libraryMenu"], function ($, lo Array.prototype.forEach.call(page.querySelectorAll(".chkDecodeCodec"), function (c) { c.checked = -1 !== (config.HardwareDecodingCodecs || []).indexOf(c.getAttribute("data-codec")); }); + page.querySelector("#chkDecodingColorDepth10").checked = config.EnableDecodingColorDepth10; page.querySelector("#chkHardwareEncoding").checked = config.EnableHardwareEncoding; $("#selectVideoDecoder", page).val(config.HardwareAccelerationType); $("#selectThreadCount", page).val(config.EncodingThreadCount); @@ -67,6 +68,7 @@ define(["jQuery", "loading", "globalize", "dom", "libraryMenu"], function ($, lo }), function (c) { return c.getAttribute("data-codec"); }); + config.EnableDecodingColorDepth10 = form.querySelector("#chkDecodingColorDepth10").checked; config.EnableHardwareEncoding = form.querySelector("#chkHardwareEncoding").checked; ApiClient.updateNamedConfiguration("encoding", config).then(function () { updateEncoder(form); diff --git a/src/encodingsettings.html b/src/encodingsettings.html index 5a005d800..b3c9f42b2 100644 --- a/src/encodingsettings.html +++ b/src/encodingsettings.html @@ -35,36 +35,44 @@

${LabelEnableHardwareDecodingFor}

+
+ +
${EnableDecodingColorDepth10Help}
+
+
+ diff --git a/src/strings/en-us.json b/src/strings/en-us.json index d5fc40ef0..a3b4257ae 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1496,5 +1496,7 @@ "EveryXMinutes": "Every {0} minutes", "EveryHour": "Every hour", "EveryXHours": "Every {0} hours", - "OnApplicationStartup": "On application startup" + "OnApplicationStartup": "On application startup", + "EnableDecodingColorDepth10": "Enable 10-Bit hardware decoding", + "EnableDecodingColorDepth10Help" : "Enable 10-Bit hardware decoding on supported hardware. Only works for HEVC and VP9 formats. Turn this off if you experience playback issues." } diff --git a/src/strings/zh-cn.json b/src/strings/zh-cn.json index acdfb2d5e..80ab75908 100644 --- a/src/strings/zh-cn.json +++ b/src/strings/zh-cn.json @@ -1502,5 +1502,7 @@ "YadifBob": "Yadif Bob", "Yadif": "Yadif", "LabelDeinterlaceMethod": "反交错方法:", - "DeinterlaceMethodHelp": "选择对隔行扫描内容进行转码时所用的反交错方法。" + "DeinterlaceMethodHelp": "选择对隔行扫描内容进行转码时所用的反交错方法。", + "EnableDecodingColorDepth10": "启用 10-Bit 硬件解码", + "EnableDecodingColorDepth10Help" : "在支持的硬件上启用 10-Bit 硬件解码。仅对 HEVC 和 VP9 格式起作用。如果你遇到了播放问题,请关闭这个选项。" } From 45ced60cc1340a1c25640ead454e225352db69ac Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Thu, 9 Apr 2020 01:15:43 +0800 Subject: [PATCH 0003/2481] fix lint --- src/controllers/dashboard/dashboard.js | 6 +++--- src/encodingsettings.html | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/controllers/dashboard/dashboard.js b/src/controllers/dashboard/dashboard.js index addcfc6e7..2901a6966 100644 --- a/src/controllers/dashboard/dashboard.js +++ b/src/controllers/dashboard/dashboard.js @@ -392,9 +392,9 @@ define(["datetime", "events", "itemHelper", "serverNotifications", "dom", "globa } else if (displayPlayMethod === "Transcode") { html += globalize.translate("Transcoding"); - if (session.TranscodingInfo && session.TranscodingInfo.Framerate) { - html += " (" + session.TranscodingInfo.Framerate + " fps)"; - } + if (session.TranscodingInfo && session.TranscodingInfo.Framerate) { + html += " (" + session.TranscodingInfo.Framerate + " fps)"; + } showTranscodingInfo = true; } else if (displayPlayMethod === "DirectPlay") { diff --git a/src/encodingsettings.html b/src/encodingsettings.html index b3c9f42b2..8e8c8fc5a 100644 --- a/src/encodingsettings.html +++ b/src/encodingsettings.html @@ -177,4 +177,3 @@ - From b43adb740625f76304423a6c4e563a290981c07a Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Sat, 11 Apr 2020 16:24:40 -0500 Subject: [PATCH 0004/2481] Add quick connect --- src/controllers/auth/login.js | 48 ++++++++++++++++++++++++ src/libraries/apiclient/apiclientcore.js | 23 +++++++++++- src/login.html | 4 ++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/controllers/auth/login.js b/src/controllers/auth/login.js index 4296b8bfb..746384a64 100644 --- a/src/controllers/auth/login.js +++ b/src/controllers/auth/login.js @@ -150,6 +150,53 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout }); } + function loginQuickConnect() { + var apiClient = getApiClient(); + var identifier = "" + var interval = 0; + var friendlyName = "test"; + $.get('/QuickConnect/Initiate?FriendlyName=' + friendlyName).then(json => { + if (!json.Secret || !json.Code) { + Dashboard.alert({ + message: json.Error, + title: "Error" + }); + return; + } + + Dashboard.alert({ + message: "Authorize request " + json.Code + " to continue", + title: "Quick Connect Code" + }); + + loading.show(); + + identifier = json.Secret; + interval = setInterval(() => { + $.get('/QuickConnect/Connect?Secret=' + identifier).then(x => { + if(x.Authenticated) { + apiClient.quickConnect(x.Authentication).then((result) => { + var user = result.User; + var serverId = getParameterByName("serverid"); + var newUrl; + + if (user.Policy.IsAdministrator && !serverId) { + newUrl = "dashboard.html"; + } else { + newUrl = "home.html"; + } + + loading.hide(); + Dashboard.onServerChanged(user.Id, result.AccessToken, apiClient); + Dashboard.navigate(newUrl); + clearInterval(interval); + }); + } + }); + }, 5000); + }); + } + view.querySelector("#divUsers").addEventListener("click", function (e) { var card = dom.parentWithClass(e.target, "card"); var cardContent = card ? card.querySelector(".cardContent") : null; @@ -183,6 +230,7 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout Dashboard.navigate("forgotpassword.html"); }); view.querySelector(".btnCancel").addEventListener("click", showVisualForm); + view.querySelector(".btnQuick").addEventListener("click", loginQuickConnect); view.querySelector(".btnManual").addEventListener("click", function () { view.querySelector("#txtManualName").value = ""; showManualForm(view, true); diff --git a/src/libraries/apiclient/apiclientcore.js b/src/libraries/apiclient/apiclientcore.js index 557a4e103..80be946a6 100644 --- a/src/libraries/apiclient/apiclientcore.js +++ b/src/libraries/apiclient/apiclientcore.js @@ -356,7 +356,28 @@ define(["events", "appStorage"], function(events, appStorage) { instance.onAuthenticated ? instance.onAuthenticated(instance, result).then(afterOnAuthenticated) : afterOnAuthenticated() }, reject) }) - }, ApiClient.prototype.ensureWebSocket = function() { + }, ApiClient.prototype.quickConnect = function (token) { + if (!token) return Promise.reject(); + var url = this.getUrl("Users/AuthenticateWithQuickConnect"), + instance = this; + return new Promise(function(resolve, reject) { + var postData = { + Token: token + }; + instance.ajax({ + type: "POST", + url: url, + data: JSON.stringify(postData), + dataType: "json", + contentType: "application/json" + }).then(function(result) { + var afterOnAuthenticated = function() { + redetectBitrate(instance), resolve(result) + }; + instance.onAuthenticated ? instance.onAuthenticated(instance, result).then(afterOnAuthenticated) : afterOnAuthenticated() + }, reject) + }) + }, ApiClient.prototype.ensureWebSocket = function() { if (!this.isWebSocketOpenOrConnecting() && this.isWebSocketSupported()) try { this.openWebSocket() } catch (err) { diff --git a/src/login.html b/src/login.html index 8e48901c1..343b85906 100644 --- a/src/login.html +++ b/src/login.html @@ -42,6 +42,10 @@ + + + + Incoming login requests: +
+
    +
  • Failed to load incoming requests
  • +
+
+ diff --git a/src/quickconnect.html b/src/quickconnect.html new file mode 100644 index 000000000..770796a10 --- /dev/null +++ b/src/quickconnect.html @@ -0,0 +1,29 @@ +
+
+
+
+
+

Quick Connect

+
+
+ + Quick connect is: Failed to load status + +
+ + +
If unchecked, users will have to click the Activate button in their profile before initiating a quick connect login.
+
+ + +
+
+
\ No newline at end of file diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index 11c89b01d..930df8be4 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -349,6 +349,12 @@ define(["dom", "layoutManager", "inputManager", "connectionManager", "events", " href: "devices.html", pageIds: ["devicesPage", "devicePage"], icon: "devices" + }); + links.push({ + name: "Quick Connect", + href: "quickconnect.html", + pageIds: ["quickConnectPage", "quickConnectPage"], + icon: "devices" }); links.push({ name: globalize.translate("HeaderActivity"), diff --git a/src/scripts/routes.js b/src/scripts/routes.js index 9c3db58a7..1520e9663 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -72,6 +72,12 @@ define([ transition: "fade", controller: "user/subtitles" }); + defineRoute({ + path: "/mypreferencesquickconnect.html", + autoFocus: false, + transition: "fade", + controller: "user/quickconnect" + }); defineRoute({ path: "/dashboard.html", @@ -103,6 +109,12 @@ define([ roles: "admin", controller: "device" }); + defineRoute({ + path: "/quickconnect.html", + autoFocus: false, + roles: "admin", + controller: "quickconnect" + }); defineRoute({ path: "/dlnaprofile.html", autoFocus: false, diff --git a/src/scripts/site.js b/src/scripts/site.js index 3b992eaf2..311478c01 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -829,6 +829,7 @@ var AppInfo = {}; define("displaySettings", [componentsPath + "/displaysettings/displaysettings"], returnFirstDependency); define("playbackSettings", [componentsPath + "/playbacksettings/playbacksettings"], returnFirstDependency); define("homescreenSettings", [componentsPath + "/homescreensettings/homescreensettings"], returnFirstDependency); + define("quickConnectSettings", [componentsPath + "/quickconnectsettings/quickconnectsettings"], returnFirstDependency); define("playbackManager", [componentsPath + "/playback/playbackmanager"], getPlaybackManager); define("layoutManager", [componentsPath + "/layoutManager", "apphost"], getLayoutManager); define("homeSections", [componentsPath + "/homesections/homesections"], returnFirstDependency); From fdfdcd60fe3d7a66cfc334f0692978fc3bb8e27c Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Mon, 13 Apr 2020 16:23:16 -0500 Subject: [PATCH 0006/2481] Switch off of jQuery --- CONTRIBUTORS.md | 1 + .../quickconnectsettings/quickconnectsettings.js | 8 +++++--- src/controllers/auth/login.js | 12 ++++++------ src/controllers/quickconnect.js | 16 +++++++++------- src/controllers/user/menu.js | 2 +- src/controllers/user/quickconnect.js | 9 +++++++++ src/mypreferencesquickconnect.html | 6 +++--- src/quickconnect.html | 2 +- 8 files changed, 35 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index aa3ec707e..ffbf05482 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -34,6 +34,7 @@ - [Ryan Hartzell](https://github.com/ryan-hartzell) - [Thibault Nocchi](https://github.com/ThibaultNocchi) - [MrTimscampi](https://github.com/MrTimscampi) + - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) # Emby Contributors diff --git a/src/components/quickconnectsettings/quickconnectsettings.js b/src/components/quickconnectsettings/quickconnectsettings.js index a30db441e..ed7985dcf 100644 --- a/src/components/quickconnectsettings/quickconnectsettings.js +++ b/src/components/quickconnectsettings/quickconnectsettings.js @@ -15,12 +15,14 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa } function list(apiClient) { - var elem = $("#quickConnectIncoming"); - elem.html(""); + console.debug("getting json"); apiClient.getJSON("/QuickConnect/List").then(json => { - console.debug("raw json", json); + var elem = $("#quickConnectIncoming"); + elem.html(""); + console.debug("raw json", json, "length is", json.length); for(var i = 0; i < json.length; i++) { var current = json[i]; + console.debug("current is", current); var html = "
  • " + current.Code + " - " + current.FriendlyName + " - "; if(!current.Authenticated) { diff --git a/src/controllers/auth/login.js b/src/controllers/auth/login.js index f5dc9b487..845ae9a38 100644 --- a/src/controllers/auth/login.js +++ b/src/controllers/auth/login.js @@ -153,10 +153,10 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout function loginQuickConnect() { var apiClient = getApiClient(); var friendlyName = "test"; - - var url = apiClient.getUrl("/QuickConnect/Initiate?FriendlyName=" + friendlyName); - apiClient.getJSON(url) - .then(json => { + + var url = apiClient.getUrl("/QuickConnect/Initiate?FriendlyName=" + friendlyName); + apiClient.getJSON(url) + .then(json => { if (!json.Secret || !json.Code) { Dashboard.alert({ message: json.Error, @@ -173,9 +173,9 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout loading.show(); var interval = setInterval(() => { - var url = apiClient.getUrl('/QuickConnect/Connect?Secret=' + json.Secret); + var url = apiClient.getUrl('/QuickConnect/Connect?Secret=' + json.Secret); apiClient.getJSON(url) - .then(data => { + .then(data => { if(data.Authenticated) { apiClient.quickConnect(data.Authentication).then((result) => { var user = result.User; diff --git a/src/controllers/quickconnect.js b/src/controllers/quickconnect.js index e3d1f7605..5c3c8f9a1 100644 --- a/src/controllers/quickconnect.js +++ b/src/controllers/quickconnect.js @@ -2,13 +2,15 @@ define(["jQuery", "loading", "libraryMenu", "fnchecked"], function ($, loading, "use strict"; function loadPage(page, status) { + console.debug("status is \"" + status + "\""); + var active = (status == "Active"); var available = (status == "Available") || active; - - $("#quickConnectStatus").text(status.toLocaleLowerCase()); - $("#chkQuickConnectAvailable").checked(available); - $("#chkQuickConnectActive").checked(active); - + + page.querySelector("#quickConnectStatus").textContent = status.toLocaleLowerCase(); + page.querySelector("#chkQuickConnectAvailable").checked = available; + page.querySelector("#chkQuickConnectActive").checked = active; + loading.hide(); } @@ -44,8 +46,8 @@ define(["jQuery", "loading", "libraryMenu", "fnchecked"], function ($, loading, } $(document).on("pageinit", "#quickConnectPage", function () { - $("#quickConnectPage").off("submit", onSubmit).on("submit", onSubmit); - $("#btnQuickConnectSubmit").click(onSubmit); + document.querySelector("#quickConnectPage").onsubmit = onSubmit; + document.querySelector("#btnQuickConnectSubmit").onclick = onSubmit; }).on("pageshow", "#quickConnectPage", function () { loading.show(); var page = this; diff --git a/src/controllers/user/menu.js b/src/controllers/user/menu.js index ff8db7268..f5d05c5be 100644 --- a/src/controllers/user/menu.js +++ b/src/controllers/user/menu.js @@ -24,7 +24,7 @@ define(["apphost", "connectionManager", "layoutManager", "listViewStyle", "emby- page.querySelector(".lnkHomePreferences").setAttribute("href", "mypreferenceshome.html?userId=" + userId); page.querySelector(".lnkPlaybackPreferences").setAttribute("href", "mypreferencesplayback.html?userId=" + userId); page.querySelector(".lnkSubtitlePreferences").setAttribute("href", "mypreferencessubtitles.html?userId=" + userId); - page.querySelector(".lnkQuickConnectPreferences").setAttribute("href", "mypreferencesquickconnect.html?userId=" + userId); + page.querySelector(".lnkQuickConnectPreferences").setAttribute("href", "mypreferencesquickconnect.html?userId=" + userId); if (window.NativeShell && window.NativeShell.AppHost.supports("clientsettings")) { page.querySelector(".clientSettings").classList.remove("hide"); diff --git a/src/controllers/user/quickconnect.js b/src/controllers/user/quickconnect.js index d6f2ae25c..6e63bcdc0 100644 --- a/src/controllers/user/quickconnect.js +++ b/src/controllers/user/quickconnect.js @@ -2,6 +2,13 @@ define(["quickConnectSettings", "dom", "globalize", "loading", "userSettings", " "use strict"; return function (view, params) { + function notImplemented() { + Dashboard.alert({ + message: "This button is not implemented yet, you must check the checkbox labeled \"Always accept quick connect login requests\" in the dashboard", + title: "Not implemented" + }); + } + var quickConnectSettingsInstance = null; var hasChanges; var userId = params.userId || ApiClient.getCurrentUserId(); @@ -9,6 +16,8 @@ define(["quickConnectSettings", "dom", "globalize", "loading", "userSettings", " view.addEventListener("viewshow", function () { console.debug("defining instance"); + $("#btnQuickConnectActivate").click(notImplemented); + quickConnectSettingsInstance = new QuickConnectSettings({ serverId: ApiClient.serverId(), userId: userId, diff --git a/src/mypreferencesquickconnect.html b/src/mypreferencesquickconnect.html index 326ce51b4..b9ffdfacc 100644 --- a/src/mypreferencesquickconnect.html +++ b/src/mypreferencesquickconnect.html @@ -1,12 +1,12 @@
    - Incoming login requests:
      -
    • Failed to load incoming requests
    • -
    +
  • Failed to load incoming requests
  • + diff --git a/src/quickconnect.html b/src/quickconnect.html index 770796a10..875716285 100644 --- a/src/quickconnect.html +++ b/src/quickconnect.html @@ -16,7 +16,7 @@
    If unchecked, users will have to click the Activate button in their profile before initiating a quick connect login.
    From c37e8f2f1bcb827893fe32a3fcf77107964a853e Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Sat, 18 Apr 2020 19:20:15 -0500 Subject: [PATCH 0007/2481] Implement auto refresh and activate button --- .../quickconnectsettings.js | 85 +++++++++++++------ src/controllers/auth/login.js | 75 ++++++++-------- src/controllers/quickconnect.js | 54 +++++++----- src/controllers/user/quickconnect.js | 63 +++++++------- src/mypreferencesmenu.html | 4 +- src/mypreferencesquickconnect.html | 2 +- src/quickconnect.html | 6 +- src/scripts/librarymenu.js | 2 +- src/scripts/site.js | 2 +- 9 files changed, 170 insertions(+), 123 deletions(-) diff --git a/src/components/quickconnectsettings/quickconnectsettings.js b/src/components/quickconnectsettings/quickconnectsettings.js index ed7985dcf..fc167802d 100644 --- a/src/components/quickconnectsettings/quickconnectsettings.js +++ b/src/components/quickconnectsettings/quickconnectsettings.js @@ -1,11 +1,10 @@ -define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loading', 'connectionManager', 'homeSections', 'dom', 'events', 'listViewStyle', 'emby-select', 'emby-checkbox'], function (require, appHost, layoutManager, focusManager, globalize, loading, connectionManager, homeSections, dom, events) { +define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loading', 'connectionManager', 'listViewStyle', 'emby-select', 'emby-checkbox'], function (require, appHost, layoutManager, focusManager, globalize, loading, connectionManager) { "use strict"; function authorizeRequest(event) { var lookup = event.data.lookup; - var apiClient = event.data.apiClient; var url = ApiClient.getUrl("/QuickConnect/Authorize"); - apiClient.ajax({ + ApiClient.ajax({ type: "POST", url: url, data: { @@ -13,46 +12,72 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa } }, true); } - - function list(apiClient) { - console.debug("getting json"); - apiClient.getJSON("/QuickConnect/List").then(json => { - var elem = $("#quickConnectIncoming"); + + QuickConnectSettings.prototype.list = function(argPage) { + ApiClient.getJSON("/QuickConnect/List").then(json => { + var elem = $(argPage.querySelector("#quickConnectIncoming")); elem.html(""); - console.debug("raw json", json, "length is", json.length); - for(var i = 0; i < json.length; i++) { + for (var i = 0; i < json.length; i++) { var current = json[i]; - console.debug("current is", current); var html = "
  • " + current.Code + " - " + current.FriendlyName + " - "; - - if(!current.Authenticated) { + + if (!current.Authenticated) { html += "authorize"; - } - else { + } else { html += " (already authorized)"; } - + html += "
  • "; elem.append(html); - $("#qc" + current.Lookup).click({ lookup: current.Lookup, apiClient: apiClient}, authorizeRequest); + $("#qc" + current.Lookup).click({ lookup: current.Lookup }, authorizeRequest); } + + return true; + }).catch((e) => { + console.error("Unable to get quick connect login requests. error:", e); }); - } + }; + + QuickConnectSettings.prototype.activate = function() { + var url = ApiClient.getUrl("/QuickConnect/Activate"); + ApiClient.ajax({ + type: "POST", + url: url, + contentType: "application/json", + dataType: "json" + }).then((json) => { + let message = json.Error; + + console.log("message is \"" + message + "\""); + if (message && message !== "") { + console.error("Error activating quick connect. Error: ", json.Error); + + Dashboard.alert({ + title: "Unable to activate quick connect", + message: message + }); + + return false; + } + + Dashboard.alert({ + message: "Already active" + }); + + return true; + }).catch((e) => { + console.error("Error activating quick connect. Error:", e); + throw e; + }); + }; function QuickConnectSettings(options) { this.options = options; } QuickConnectSettings.prototype.loadData = function () { - loading.show(); - - var apiClient = connectionManager.getApiClient(this.options.serverId); - - list(apiClient); - - console.debug("request list finished"); - - loading.hide(); + this.options.interval = setInterval(this.list, 5000, this.options.page); + this.list(this.options.page); }; QuickConnectSettings.prototype.submit = function () { @@ -60,8 +85,14 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa }; QuickConnectSettings.prototype.destroy = function () { + console.debug("clearing refresh interval", this.options.interval); + clearInterval(this.options.interval); this.options = null; }; + QuickConnectSettings.prototype.interval = function (interval) { + this.options.interval = interval; + }; + return QuickConnectSettings; }); diff --git a/src/controllers/auth/login.js b/src/controllers/auth/login.js index 845ae9a38..94fa15d41 100644 --- a/src/controllers/auth/login.js +++ b/src/controllers/auth/login.js @@ -152,51 +152,54 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout function loginQuickConnect() { var apiClient = getApiClient(); - var friendlyName = "test"; - + var friendlyName = navigator.userAgent; // TODO: what should this be changed to? + var url = apiClient.getUrl("/QuickConnect/Initiate?FriendlyName=" + friendlyName); apiClient.getJSON(url) - .then(json => { - if (!json.Secret || !json.Code) { + .then(json => { + if (!json.Secret || !json.Code) { + Dashboard.alert({ + message: json.Error, + title: "Error" + }); + + return false; + } + Dashboard.alert({ - message: json.Error, - title: "Error" + message: "Authorize request " + json.Code + " to continue", + title: "Quick Connect Code" }); - return; - } - Dashboard.alert({ - message: "Authorize request " + json.Code + " to continue", - title: "Quick Connect Code" - }); + loading.show(); - loading.show(); + var interval = setInterval(async function() { + let connectUrl = apiClient.getUrl('/QuickConnect/Connect?Secret=' + json.Secret); + let data = await apiClient.getJSON(connectUrl); + if (data.Authenticated) { + let result = await apiClient.quickConnect(data.Authentication); + var user = result.User; + var serverId = getParameterByName("serverid"); + var newUrl = "home.html"; - var interval = setInterval(() => { - var url = apiClient.getUrl('/QuickConnect/Connect?Secret=' + json.Secret); - apiClient.getJSON(url) - .then(data => { - if(data.Authenticated) { - apiClient.quickConnect(data.Authentication).then((result) => { - var user = result.User; - var serverId = getParameterByName("serverid"); - var newUrl; + if (user.Policy.IsAdministrator && !serverId) { + newUrl = "dashboard.html"; + } - if (user.Policy.IsAdministrator && !serverId) { - newUrl = "dashboard.html"; - } else { - newUrl = "home.html"; - } + loading.hide(); + Dashboard.onServerChanged(user.Id, result.AccessToken, apiClient); + Dashboard.navigate(newUrl); + clearInterval(interval); - loading.hide(); - Dashboard.onServerChanged(user.Id, result.AccessToken, apiClient); - Dashboard.navigate(newUrl); - clearInterval(interval); - }); + return true; } - }); - }, 5000); - }); + return false; + }, 5000); + + return true; + }).catch((e) => { + console.error("Unable to initiate quick connect login request. Error:", e); + }); } view.querySelector("#divUsers").addEventListener("click", function (e) { @@ -240,7 +243,7 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout view.querySelector(".btnSelectServer").addEventListener("click", function () { Dashboard.selectServer(); }); - view.addEventListener("viewshow", function (e) { + view.addEventListener("viewshow", function () { loading.show(); if (!appHost.supports('multiserver')) { diff --git a/src/controllers/quickconnect.js b/src/controllers/quickconnect.js index 5c3c8f9a1..a6ebc381c 100644 --- a/src/controllers/quickconnect.js +++ b/src/controllers/quickconnect.js @@ -1,9 +1,8 @@ -define(["jQuery", "loading", "libraryMenu", "fnchecked"], function ($, loading, libraryMenu) { +define(["jQuery", "loading", "fnchecked"], function ($, loading) { "use strict"; + var page; function loadPage(page, status) { - console.debug("status is \"" + status + "\""); - var active = (status == "Active"); var available = (status == "Available") || active; @@ -16,44 +15,53 @@ define(["jQuery", "loading", "libraryMenu", "fnchecked"], function ($, loading, function onSubmit() { loading.show(); - - var available = $("#chkQuickConnectAvailable").is(":checked") ? "Available" : "Unavailable"; + + var newStatus = page.querySelector("#chkQuickConnectAvailable").checked ? "Available" : "Unavailable"; + if (newStatus && page.querySelector("#chkQuickConnectActive").checked) { + newStatus = "Active"; + } + var url = ApiClient.getUrl("/QuickConnect/Available"); - + ApiClient.ajax({ type: "POST", data: { - "Status": available + "Status": newStatus }, url: url }, true).then(() => { - if($("#chkQuickConnectActive").is(":checked")) { - url = ApiClient.getUrl("/QuickConnect/Activate"); - ApiClient.ajax({ - type: "POST", - url: url - }, true); - } - Dashboard.alert({ message: "Settings saved", title: "Saved" }); + + setTimeout(updatePage, 500); + + return true; + }).catch((e) => { + console.error("Unable to set quick connect status. error:", e); }); - + loading.hide(); return false; } - $(document).on("pageinit", "#quickConnectPage", function () { - document.querySelector("#quickConnectPage").onsubmit = onSubmit; - document.querySelector("#btnQuickConnectSubmit").onclick = onSubmit; - }).on("pageshow", "#quickConnectPage", function () { - loading.show(); - var page = this; + function updatePage() { var promise1 = ApiClient.getQuickConnect("Status"); - Promise.all([promise1]).then(function (responses) { + Promise.all([promise1]).then((responses) => { loadPage(page, responses[0]); + return true; + }).catch((e) => { + console.error("Unable to get quick connect status. error:", e); }); + } + + $(document).on("pageshow", "#quickConnectPage", function () { + loading.show(); + page = this; + + page.querySelector("#btnQuickConnectSubmit").onclick = onSubmit; + + updatePage(); }); }); diff --git a/src/controllers/user/quickconnect.js b/src/controllers/user/quickconnect.js index 6e63bcdc0..fae5d4986 100644 --- a/src/controllers/user/quickconnect.js +++ b/src/controllers/user/quickconnect.js @@ -1,50 +1,55 @@ define(["quickConnectSettings", "dom", "globalize", "loading", "userSettings", "autoFocuser", "listViewStyle"], function (QuickConnectSettings, dom, globalize, loading, userSettings, autoFocuser) { "use strict"; - return function (view, params) { - function notImplemented() { - Dashboard.alert({ - message: "This button is not implemented yet, you must check the checkbox labeled \"Always accept quick connect login requests\" in the dashboard", - title: "Not implemented" - }); - } - + return function (view) { var quickConnectSettingsInstance = null; - var hasChanges; - var userId = params.userId || ApiClient.getCurrentUserId(); - var currentSettings = userId === ApiClient.getCurrentUserId() ? userSettings : new userSettings(); + view.addEventListener("viewshow", function () { - console.debug("defining instance"); - - $("#btnQuickConnectActivate").click(notImplemented); - quickConnectSettingsInstance = new QuickConnectSettings({ - serverId: ApiClient.serverId(), - userId: userId, - element: view.querySelector(".quickConnectSettingsContainer"), - userSettings: currentSettings, - enableSaveButton: false, - enableSaveConfirmation: false, - autoFocus: autoFocuser.isEnabled() + page: view, + interval: 0 }); - + + view.querySelector("#btnQuickConnectActivate").addEventListener("click", () => { + quickConnectSettingsInstance.activate(quickConnectSettingsInstance); + }); + quickConnectSettingsInstance.loadData(); - }); - view.addEventListener("change", function () { - hasChanges = true; + + ApiClient.getQuickConnect("Status").then((status) => { + let btn = view.querySelector("#btnQuickConnectActivate"); + + if (status === "Unavailable") { + btn.textContent = "Quick connect is not available on this server"; + btn.disabled = true; + return false; + } + + else if (status === "Available") { + return false; + } + + btn.style.display = "none"; + return true; + }).catch((e) => { + throw e; + }); }); view.addEventListener("viewbeforehide", function () { - hasChanges = false; - if (quickConnectSettingsInstance) { quickConnectSettingsInstance.submit(); } + onDestroy(); }); view.addEventListener("viewdestroy", function () { + onDestroy(); + }); + + function onDestroy() { if (quickConnectSettingsInstance) { quickConnectSettingsInstance.destroy(); quickConnectSettingsInstance = null; } - }); + } }; }); diff --git a/src/mypreferencesmenu.html b/src/mypreferencesmenu.html index c8db1d17f..bfc5e793e 100644 --- a/src/mypreferencesmenu.html +++ b/src/mypreferencesmenu.html @@ -51,13 +51,13 @@
    - +
    Quick Connect
    - +
    diff --git a/src/mypreferencesquickconnect.html b/src/mypreferencesquickconnect.html index b9ffdfacc..d92e49a42 100644 --- a/src/mypreferencesquickconnect.html +++ b/src/mypreferencesquickconnect.html @@ -2,7 +2,7 @@ - + Incoming login requests:
      diff --git a/src/quickconnect.html b/src/quickconnect.html index 875716285..9a7dfdfb3 100644 --- a/src/quickconnect.html +++ b/src/quickconnect.html @@ -6,9 +6,9 @@

      Quick Connect

    - + Quick connect is: Failed to load status - +
    If unchecked, users will have to click the Activate button in their profile before initiating a quick connect login.
    - + diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index 930df8be4..2accf4948 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -350,7 +350,7 @@ define(["dom", "layoutManager", "inputManager", "connectionManager", "events", " pageIds: ["devicesPage", "devicePage"], icon: "devices" }); - links.push({ + links.push({ name: "Quick Connect", href: "quickconnect.html", pageIds: ["quickConnectPage", "quickConnectPage"], diff --git a/src/scripts/site.js b/src/scripts/site.js index 311478c01..3b8abf2bf 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -829,7 +829,7 @@ var AppInfo = {}; define("displaySettings", [componentsPath + "/displaysettings/displaysettings"], returnFirstDependency); define("playbackSettings", [componentsPath + "/playbacksettings/playbacksettings"], returnFirstDependency); define("homescreenSettings", [componentsPath + "/homescreensettings/homescreensettings"], returnFirstDependency); - define("quickConnectSettings", [componentsPath + "/quickconnectsettings/quickconnectsettings"], returnFirstDependency); + define("quickConnectSettings", [componentsPath + "/quickconnectsettings/quickconnectsettings"], returnFirstDependency); define("playbackManager", [componentsPath + "/playback/playbackmanager"], getPlaybackManager); define("layoutManager", [componentsPath + "/layoutManager", "apphost"], getLayoutManager); define("homeSections", [componentsPath + "/homesections/homesections"], returnFirstDependency); From 43d01146a23b9ee9c4044a5d0b545ee6fe6ce692 Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Sun, 19 Apr 2020 01:39:05 -0500 Subject: [PATCH 0008/2481] Fix linting --- .../quickconnectsettings/quickconnectsettings.js | 2 +- src/controllers/quickconnect.js | 4 ++-- src/controllers/user/quickconnect.js | 4 +--- src/libraries/apiclient/apiclientcore.js | 7 ++++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/quickconnectsettings/quickconnectsettings.js b/src/components/quickconnectsettings/quickconnectsettings.js index fc167802d..c65782caf 100644 --- a/src/components/quickconnectsettings/quickconnectsettings.js +++ b/src/components/quickconnectsettings/quickconnectsettings.js @@ -61,7 +61,7 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa } Dashboard.alert({ - message: "Already active" + message: "Successfully activated" }); return true; diff --git a/src/controllers/quickconnect.js b/src/controllers/quickconnect.js index a6ebc381c..35e5516f4 100644 --- a/src/controllers/quickconnect.js +++ b/src/controllers/quickconnect.js @@ -2,7 +2,7 @@ define(["jQuery", "loading", "fnchecked"], function ($, loading) { "use strict"; var page; - function loadPage(page, status) { + function loadPage(status) { var active = (status == "Active"); var available = (status == "Available") || active; @@ -49,7 +49,7 @@ define(["jQuery", "loading", "fnchecked"], function ($, loading) { function updatePage() { var promise1 = ApiClient.getQuickConnect("Status"); Promise.all([promise1]).then((responses) => { - loadPage(page, responses[0]); + loadPage(responses[0]); return true; }).catch((e) => { console.error("Unable to get quick connect status. error:", e); diff --git a/src/controllers/user/quickconnect.js b/src/controllers/user/quickconnect.js index fae5d4986..2b03dec41 100644 --- a/src/controllers/user/quickconnect.js +++ b/src/controllers/user/quickconnect.js @@ -23,9 +23,7 @@ define(["quickConnectSettings", "dom", "globalize", "loading", "userSettings", " btn.textContent = "Quick connect is not available on this server"; btn.disabled = true; return false; - } - - else if (status === "Available") { + } else if (status === "Available") { return false; } diff --git a/src/libraries/apiclient/apiclientcore.js b/src/libraries/apiclient/apiclientcore.js index 04421b837..e1892505f 100644 --- a/src/libraries/apiclient/apiclientcore.js +++ b/src/libraries/apiclient/apiclientcore.js @@ -358,8 +358,8 @@ define(["events", "appStorage"], function(events, appStorage) { }) }, ApiClient.prototype.quickConnect = function (token) { if (!token) return Promise.reject(); - var url = this.getUrl("Users/AuthenticateWithQuickConnect"), - instance = this; + var url = this.getUrl("Users/AuthenticateWithQuickConnect"); + var instance = this; return new Promise(function(resolve, reject) { var postData = { Token: token @@ -372,7 +372,8 @@ define(["events", "appStorage"], function(events, appStorage) { contentType: "application/json" }).then(function(result) { var afterOnAuthenticated = function() { - redetectBitrate(instance), resolve(result) + redetectBitrate(instance); + return resolve(result); }; instance.onAuthenticated ? instance.onAuthenticated(instance, result).then(afterOnAuthenticated) : afterOnAuthenticated() }, reject) From bf03a7ba57af89f1a38c41457dbe7c70f501a04d Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Sat, 25 Apr 2020 15:46:22 -0500 Subject: [PATCH 0009/2481] Improve UI --- .../quickconnectsettings.js | 42 ++++++++++++++----- src/controllers/auth/login.js | 42 ++++++++++++------- src/controllers/quickconnect.js | 12 ++---- src/mypreferencesmenu.html | 2 +- src/mypreferencesquickconnect.html | 5 +-- src/quickconnect.html | 5 --- src/scripts/librarymenu.js | 2 +- 7 files changed, 65 insertions(+), 45 deletions(-) diff --git a/src/components/quickconnectsettings/quickconnectsettings.js b/src/components/quickconnectsettings/quickconnectsettings.js index c65782caf..59250df76 100644 --- a/src/components/quickconnectsettings/quickconnectsettings.js +++ b/src/components/quickconnectsettings/quickconnectsettings.js @@ -11,25 +11,45 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa "Lookup": lookup } }, true); + + require(["toast"], function (toast) { + toast("Request authorized"); + }); + + // prevent bubbling + return false; } QuickConnectSettings.prototype.list = function(argPage) { ApiClient.getJSON("/QuickConnect/List").then(json => { + let found = false; var elem = $(argPage.querySelector("#quickConnectIncoming")); - elem.html(""); - for (var i = 0; i < json.length; i++) { - var current = json[i]; - var html = "
  • " + current.Code + " - " + current.FriendlyName + " - "; + elem.text("No pending login requests"); - if (!current.Authenticated) { - html += "authorize"; - } else { - html += " (already authorized)"; + for (var i = 0; i < json.length; i++) { + if (!found) { + elem.html(""); + found = true; } - html += "
  • "; + var current = json[i]; + + let html = '
    '; + html += '
    ' + current.Code + '
    '; + html += '
    ' + current.FriendlyName + '
    '; + html += '
    '; + + if (!current.Authenticated) { + html += 'authorize'; + } else { + html += " (authorized)"; + } + + html += '
    '; elem.append(html); + $("#qc" + current.Lookup).click({ lookup: current.Lookup }, authorizeRequest); + $("#div" + current.Lookup).click({ lookup: current.Lookup }, authorizeRequest); } return true; @@ -60,8 +80,8 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa return false; } - Dashboard.alert({ - message: "Successfully activated" + require(["toast"], function (toast) { + toast("Successfully activated"); }); return true; diff --git a/src/controllers/auth/login.js b/src/controllers/auth/login.js index 94fa15d41..ff36939fb 100644 --- a/src/controllers/auth/login.js +++ b/src/controllers/auth/login.js @@ -152,7 +152,7 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout function loginQuickConnect() { var apiClient = getApiClient(); - var friendlyName = navigator.userAgent; // TODO: what should this be changed to? + var friendlyName = navigator.userAgent; var url = apiClient.getUrl("/QuickConnect/Initiate?FriendlyName=" + friendlyName); apiClient.getJSON(url) @@ -174,25 +174,37 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout loading.show(); var interval = setInterval(async function() { - let connectUrl = apiClient.getUrl('/QuickConnect/Connect?Secret=' + json.Secret); - let data = await apiClient.getJSON(connectUrl); - if (data.Authenticated) { - let result = await apiClient.quickConnect(data.Authentication); - var user = result.User; - var serverId = getParameterByName("serverid"); - var newUrl = "home.html"; + try { + let connectUrl = apiClient.getUrl('/QuickConnect/Connect?Secret=' + json.Secret); + let data = await apiClient.getJSON(connectUrl); + if (data.Authenticated) { + let result = await apiClient.quickConnect(data.Authentication); + var user = result.User; + var serverId = getParameterByName("serverid"); + var newUrl = "home.html"; - if (user.Policy.IsAdministrator && !serverId) { - newUrl = "dashboard.html"; + if (user.Policy.IsAdministrator && !serverId) { + newUrl = "dashboard.html"; + } + + loading.hide(); + Dashboard.onServerChanged(user.Id, result.AccessToken, apiClient); + Dashboard.navigate(newUrl); + clearInterval(interval); + + return true; } + } catch (e) { + Dashboard.alert({ + message: "Quick connect was deactivated before the login request could be approved", + title: "Unexpected error" + }); - loading.hide(); - Dashboard.onServerChanged(user.Id, result.AccessToken, apiClient); - Dashboard.navigate(newUrl); + console.error("Unable to login with quick connect", e); clearInterval(interval); - - return true; + loading.hide(); } + return false; }, 5000); diff --git a/src/controllers/quickconnect.js b/src/controllers/quickconnect.js index 35e5516f4..3432e7923 100644 --- a/src/controllers/quickconnect.js +++ b/src/controllers/quickconnect.js @@ -3,12 +3,10 @@ define(["jQuery", "loading", "fnchecked"], function ($, loading) { var page; function loadPage(status) { - var active = (status == "Active"); - var available = (status == "Available") || active; + var available = status === "Available" || status === "Active"; page.querySelector("#quickConnectStatus").textContent = status.toLocaleLowerCase(); page.querySelector("#chkQuickConnectAvailable").checked = available; - page.querySelector("#chkQuickConnectActive").checked = active; loading.hide(); } @@ -17,9 +15,6 @@ define(["jQuery", "loading", "fnchecked"], function ($, loading) { loading.show(); var newStatus = page.querySelector("#chkQuickConnectAvailable").checked ? "Available" : "Unavailable"; - if (newStatus && page.querySelector("#chkQuickConnectActive").checked) { - newStatus = "Active"; - } var url = ApiClient.getUrl("/QuickConnect/Available"); @@ -30,9 +25,8 @@ define(["jQuery", "loading", "fnchecked"], function ($, loading) { }, url: url }, true).then(() => { - Dashboard.alert({ - message: "Settings saved", - title: "Saved" + require(["toast"], function (toast) { + toast("Settings saved"); }); setTimeout(updatePage, 500); diff --git a/src/mypreferencesmenu.html b/src/mypreferencesmenu.html index bfc5e793e..6f753e555 100644 --- a/src/mypreferencesmenu.html +++ b/src/mypreferencesmenu.html @@ -51,7 +51,7 @@
    - + tap_and_play
    Quick Connect
    diff --git a/src/mypreferencesquickconnect.html b/src/mypreferencesquickconnect.html index d92e49a42..b58ca8074 100644 --- a/src/mypreferencesquickconnect.html +++ b/src/mypreferencesquickconnect.html @@ -3,10 +3,9 @@ Activate - Incoming login requests:
    -
      -
    • Failed to load incoming requests
    • +
      +
      Failed to load incoming requests
    diff --git a/src/quickconnect.html b/src/quickconnect.html index 9a7dfdfb3..7d7ec879a 100644 --- a/src/quickconnect.html +++ b/src/quickconnect.html @@ -14,11 +14,6 @@ Enable quick connect on this server - -
    If unchecked, users will have to click the Activate button in their profile before initiating a quick connect login.
    '; + html += ''; } } @@ -1426,7 +1426,7 @@ import 'programStyles'; } if (options.overlayMoreButton) { - overlayButtons += ''; + overlayButtons += ''; } } @@ -1580,7 +1580,7 @@ import 'programStyles'; html += ''; } - html += ''; + html += ''; html += ''; html += ''; diff --git a/src/components/guide/tvguide.template.html b/src/components/guide/tvguide.template.html index ef3c4b58c..1babf5aac 100644 --- a/src/components/guide/tvguide.template.html +++ b/src/components/guide/tvguide.template.html @@ -10,7 +10,7 @@
    diff --git a/src/components/metadataeditor/metadataeditor.template.html b/src/components/metadataeditor/metadataeditor.template.html index 8e36a824b..246757768 100644 --- a/src/components/metadataeditor/metadataeditor.template.html +++ b/src/components/metadataeditor/metadataeditor.template.html @@ -9,7 +9,7 @@ ${Save}
    diff --git a/src/controllers/dashboard/plugins/installed.js b/src/controllers/dashboard/plugins/installed.js index c381b2409..d87413bb5 100644 --- a/src/controllers/dashboard/plugins/installed.js +++ b/src/controllers/dashboard/plugins/installed.js @@ -54,7 +54,7 @@ define(["loading", "libraryMenu", "dom", "globalize", "cardStyle", "emby-button" html += ""; html += '
    '; html += '
    '; - html += ''; + html += ''; html += "
    "; html += "
    "; html += configPage ? configPage.DisplayName || plugin.Name : plugin.Name; diff --git a/src/controllers/devices.js b/src/controllers/devices.js index 8dd665f7f..7401e17c0 100644 --- a/src/controllers/devices.js +++ b/src/controllers/devices.js @@ -89,7 +89,7 @@ define(["loading", "dom", "libraryMenu", "globalize", "scripts/imagehelper", "da if (canEdit || canDelete(device.Id)) { deviceHtml += '
    '; - deviceHtml += ''; + deviceHtml += ''; deviceHtml += "
    "; } diff --git a/src/controllers/livetvstatus.js b/src/controllers/livetvstatus.js index aee5876a4..adc436070 100644 --- a/src/controllers/livetvstatus.js +++ b/src/controllers/livetvstatus.js @@ -30,7 +30,7 @@ define(["jQuery", "globalize", "scripts/taskbutton", "dom", "libraryMenu", "layo html += "
    "; html += "
    "; html += '
    "; - html += ''; + html += ''; html += ""; } diff --git a/src/controllers/medialibrarypage.js b/src/controllers/medialibrarypage.js index d838c2145..b94c8600f 100644 --- a/src/controllers/medialibrarypage.js +++ b/src/controllers/medialibrarypage.js @@ -298,7 +298,7 @@ define(["jQuery", "apphost", "scripts/taskbutton", "loading", "libraryMenu", "gl if (virtualFolder.showMenu !== false) { html += '
    '; - html += ''; + html += ''; html += "
    "; } diff --git a/src/controllers/userprofilespage.js b/src/controllers/userprofilespage.js index 180d0e62a..90609fa06 100644 --- a/src/controllers/userprofilespage.js +++ b/src/controllers/userprofilespage.js @@ -115,7 +115,7 @@ define(["loading", "dom", "globalize", "date-fns", "dfnshelper", "paper-icon-but html += '
    '; html += user.Name; html += "
    "; - html += ''; + html += ''; html += ""; html += '
    '; var lastSeen = getLastSeenText(user.LastActivityDate); @@ -189,7 +189,7 @@ define(["loading", "dom", "globalize", "date-fns", "dfnshelper", "paper-icon-but html += "
    "; html += '
    '; html += '
    '; - html += ''; + html += ''; html += "
    "; html += '
    '; html += user.UserName; diff --git a/src/list.html b/src/list.html index 375653c12..48214df35 100644 --- a/src/list.html +++ b/src/list.html @@ -42,10 +42,10 @@
    From aa12a796256c7dcecb471d8c34ffda6391d0eac1 Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Sun, 26 Apr 2020 16:19:58 -0500 Subject: [PATCH 0011/2481] Apply suggestions from code review --- .../quickconnectsettings.js | 19 ++++++++----------- src/controllers/auth/login.js | 4 ++-- src/controllers/user/quickconnect.js | 2 +- src/login.html | 2 +- src/mypreferencesmenu.html | 2 +- src/mypreferencesquickconnect.html | 4 ++-- src/quickconnect.html | 6 +++--- src/scripts/librarymenu.js | 2 +- src/strings/en-us.json | 10 ++++++++++ 9 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/components/quickconnectsettings/quickconnectsettings.js b/src/components/quickconnectsettings/quickconnectsettings.js index 59250df76..6010fab5c 100644 --- a/src/components/quickconnectsettings/quickconnectsettings.js +++ b/src/components/quickconnectsettings/quickconnectsettings.js @@ -2,8 +2,8 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa "use strict"; function authorizeRequest(event) { - var lookup = event.data.lookup; - var url = ApiClient.getUrl("/QuickConnect/Authorize"); + let lookup = event.data.lookup; + let url = ApiClient.getUrl("/QuickConnect/Authorize"); ApiClient.ajax({ type: "POST", url: url, @@ -23,12 +23,12 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa QuickConnectSettings.prototype.list = function(argPage) { ApiClient.getJSON("/QuickConnect/List").then(json => { let found = false; - var elem = $(argPage.querySelector("#quickConnectIncoming")); - elem.text("No pending login requests"); + var elem = argPage.querySelector('#quickConnectIncoming'); + elem.innerText = globalize.translate('QuickConnectNoPending'); for (var i = 0; i < json.length; i++) { if (!found) { - elem.html(""); + elem.innerHTML = ""; found = true; } @@ -40,13 +40,11 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa html += '
    '; if (!current.Authenticated) { - html += 'authorize'; - } else { - html += " (authorized)"; + html += '' + globalize.translate('Authorize') + ''; } html += '
    '; - elem.append(html); + elem.innerHTML += html; $("#qc" + current.Lookup).click({ lookup: current.Lookup }, authorizeRequest); $("#div" + current.Lookup).click({ lookup: current.Lookup }, authorizeRequest); @@ -68,7 +66,6 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa }).then((json) => { let message = json.Error; - console.log("message is \"" + message + "\""); if (message && message !== "") { console.error("Error activating quick connect. Error: ", json.Error); @@ -81,7 +78,7 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa } require(["toast"], function (toast) { - toast("Successfully activated"); + toast(globalize.translate("QuickConnectActivationSuccessful")); }); return true; diff --git a/src/controllers/auth/login.js b/src/controllers/auth/login.js index ff36939fb..86e8874e7 100644 --- a/src/controllers/auth/login.js +++ b/src/controllers/auth/login.js @@ -167,8 +167,8 @@ define(["apphost", "appSettings", "dom", "connectionManager", "loading", "layout } Dashboard.alert({ - message: "Authorize request " + json.Code + " to continue", - title: "Quick Connect Code" + message: Globalize.translate('QuickConnectAuthorizeCode', json.Code), + title: Globalize.translate('QuickConnect') }); loading.show(); diff --git a/src/controllers/user/quickconnect.js b/src/controllers/user/quickconnect.js index 2b03dec41..035aef8f2 100644 --- a/src/controllers/user/quickconnect.js +++ b/src/controllers/user/quickconnect.js @@ -20,7 +20,7 @@ define(["quickConnectSettings", "dom", "globalize", "loading", "userSettings", " let btn = view.querySelector("#btnQuickConnectActivate"); if (status === "Unavailable") { - btn.textContent = "Quick connect is not available on this server"; + btn.textContent = globalize.translate("QuickConnectNotAvailable"); btn.disabled = true; return false; } else if (status === "Available") { diff --git a/src/login.html b/src/login.html index 343b85906..49472d881 100644 --- a/src/login.html +++ b/src/login.html @@ -44,7 +44,7 @@
    -
    Failed to load incoming requests
    +
    ${MessagePleaseWait}
    diff --git a/src/quickconnect.html b/src/quickconnect.html index 7d7ec879a..dff74eb83 100644 --- a/src/quickconnect.html +++ b/src/quickconnect.html @@ -3,16 +3,16 @@
    -

    Quick Connect

    +

    ${QuickConnect}

    - Quick connect is: Failed to load status + ${LabelCurrentStatus}
    diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index 131dcf747..e3ef65f22 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -351,7 +351,7 @@ define(["dom", "layoutManager", "inputManager", "connectionManager", "events", " icon: "devices" }); links.push({ - name: "Quick Connect", + name: globalize.translate("QuickConnect"), href: "quickconnect.html", pageIds: ["quickConnectPage", "quickConnectPage"], icon: "tap_and_play" diff --git a/src/strings/en-us.json b/src/strings/en-us.json index d5fc40ef0..ad390a0fe 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -48,6 +48,7 @@ "AuthProviderHelp": "Select an Authentication Provider to be used to authenticate this user's password.", "Auto": "Auto", "AutoBasedOnLanguageSetting": "Auto (based on language setting)", + "Authorize": "authorize", "Backdrop": "Backdrop", "Backdrops": "Backdrops", "Banner": "Banner", @@ -63,6 +64,7 @@ "Browse": "Browse", "BrowsePluginCatalogMessage": "Browse our plugin catalog to view available plugins.", "BurnSubtitlesHelp": "Determines if the server should burn in subtitles when transcoding videos. Avoiding this will greatly improve performance. Select Auto to burn image based formats (VOBSUB, PGS, SUB, IDX) and certain ASS or SSA subtitles.", + "ButtonActivate": "Activate", "ButtonAdd": "Add", "ButtonAddImage": "Add Image", "ButtonAddMediaLibrary": "Add Media Library", @@ -140,6 +142,7 @@ "ButtonTrailer": "Trailer", "ButtonUninstall": "Uninstall", "ButtonUp": "Up", + "ButtonUseQuickConnect": "Use Quick Connect", "ButtonViewWebsite": "View website", "ButtonWebsite": "Website", "CancelRecording": "Cancel recording", @@ -231,6 +234,7 @@ "EnableNextVideoInfoOverlayHelp": "At the end of a video, display info about the next video coming up in the current playlist.", "EnablePhotos": "Display photos", "EnablePhotosHelp": "Images will be detected and displayed alongside other media files.", + "EnableQuickConnect": "Enable quick connect on this server", "EnableStreamLooping": "Auto-loop live streams", "EnableStreamLoopingHelp": "Enable this if live streams only contain a few seconds of data and need to be continuously requested. Enabling this when not needed may cause problems.", "EnableThemeSongs": "Theme songs", @@ -587,6 +591,7 @@ "LabelCountry": "Country:", "LabelCriticRating": "Critic rating:", "LabelCurrentPassword": "Current password:", + "LabelCurrentStatus": "Current status:", "LabelCustomCertificatePath": "Custom SSL certificate path:", "LabelCustomCertificatePathHelp": "Path to a PKCS #12 file containing a certificate and private key to enable TLS support on a custom domain.", "LabelCustomCss": "Custom CSS:", @@ -1260,6 +1265,11 @@ "Programs": "Programs", "Quality": "Quality", "QueueAllFromHere": "Queue all from here", + "QuickConnect": "Quick Connect", + "QuickConnectActivationSuccessful": "Successfully activated", + "QuickConnectAuthorizeCode": "Authorize request {0} to continue", + "QuickConnectNoPending": "No pending login requests", + "QuickConnectNotAvailable": "Quick connect is not available on this server", "Raised": "Raised", "Rate": "Rate", "RecentlyWatched": "Recently watched", From ac62b8282858296e185db434509ae59ed3fef585 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 26 Apr 2020 17:17:59 -0400 Subject: [PATCH 0012/2481] Clean up network settings page - remove HTTPS mode dropdown and split it into two checkboxes: EnableHttps and RequireHttps (IsBehindProxy is no longer a setting on the server) - Reorganize settings on page - Create a fieldset for HTTPS settings - Create a fieldset for remote connection settings - Move baseUrl out of remote connection settings at closer to the top of the page --- src/controllers/dashboard/networking.js | 74 +++----------- src/networking.html | 130 +++++++++++++----------- src/strings/en-us.json | 12 ++- 3 files changed, 95 insertions(+), 121 deletions(-) diff --git a/src/controllers/dashboard/networking.js b/src/controllers/dashboard/networking.js index c0e4d76d0..ec73fb2e9 100644 --- a/src/controllers/dashboard/networking.js +++ b/src/controllers/dashboard/networking.js @@ -9,7 +9,7 @@ define(["loading", "libraryMenu", "globalize", "emby-checkbox", "emby-select"], var validationResult = getValidationAlert(form); if (validationResult) { - alertText(validationResult); + showAlertText(validationResult); return; } @@ -29,35 +29,10 @@ define(["loading", "libraryMenu", "globalize", "emby-checkbox", "emby-select"], config.IsRemoteIPFilterBlacklist = "blacklist" === form.querySelector("#selectExternalAddressFilterMode").value; config.PublicPort = form.querySelector("#txtPublicPort").value; config.PublicHttpsPort = form.querySelector("#txtPublicHttpsPort").value; - var httpsMode = form.querySelector("#selectHttpsMode").value; - - switch (httpsMode) { - case "proxy": - config.EnableHttps = true; - config.RequireHttps = false; - config.IsBehindProxy = true; - break; - - case "required": - config.EnableHttps = true; - config.RequireHttps = true; - config.IsBehindProxy = false; - break; - - case "enabled": - config.EnableHttps = true; - config.RequireHttps = false; - config.IsBehindProxy = false; - break; - - default: - config.EnableHttps = false; - config.RequireHttps = false; - config.IsBehindProxy = false; - } - - config.HttpsPortNumber = form.querySelector("#txtHttpsPort").value; config.HttpServerPortNumber = form.querySelector("#txtPortNumber").value; + config.HttpsPortNumber = form.querySelector("#txtHttpsPort").value; + config.EnableHttps = form.querySelector("#chkEnableHttps").checked; + config.RequireHttps = form.querySelector("#chkRequireHttps").checked; config.EnableUPnP = enableUpnp; config.BaseUrl = form.querySelector("#txtBaseUrl").value; config.EnableRemoteAccess = form.querySelector("#chkRemoteAccess").checked; @@ -90,23 +65,20 @@ define(["loading", "libraryMenu", "globalize", "emby-checkbox", "emby-select"], } function validateHttps(form) { - var remoteAccess = form.querySelector("#chkRemoteAccess").checked; var certPath = form.querySelector("#txtCertificatePath").value || null; - var httpsMode = form.querySelector("#selectHttpsMode").value; + var httpsEnabled = form.querySelector("#chkEnableHttps").checked; - if (!remoteAccess || ("enabled" !== httpsMode && "required" !== httpsMode || certPath)) { - return Promise.resolve(); - } - - return new Promise(function (resolve, reject) { - return alertText({ + if (httpsEnabled && !certPath) { + return showAlertText({ title: globalize.translate("TitleHostingSettings"), text: globalize.translate("HttpsRequiresCert") - }).then(reject, reject); - }); + }).then(Promise.reject); + } + + return Promise.resolve(); } - function alertText(options) { + function showAlertText(options) { return new Promise(function (resolve, reject) { require(["alert"], function (alert) { alert(options).then(resolve, reject); @@ -116,7 +88,7 @@ define(["loading", "libraryMenu", "globalize", "emby-checkbox", "emby-select"], function confirmSelections(localAddress, enableUpnp, callback) { if (localAddress || !enableUpnp) { - alertText({ + showAlertText({ title: globalize.translate("TitleHostingSettings"), text: globalize.translate("SettingsWarning") }).then(callback); @@ -135,19 +107,9 @@ define(["loading", "libraryMenu", "globalize", "emby-checkbox", "emby-select"], page.querySelector("#txtExternalAddressFilter").value = (config.RemoteIPFilter || []).join(", "); page.querySelector("#selectExternalAddressFilterMode").value = config.IsRemoteIPFilterBlacklist ? "blacklist" : "whitelist"; page.querySelector("#chkRemoteAccess").checked = null == config.EnableRemoteAccess || config.EnableRemoteAccess; - var selectHttpsMode = page.querySelector("#selectHttpsMode"); - - if (config.IsBehindProxy) { - selectHttpsMode.value = "proxy"; - } else if (config.RequireHttps) { - selectHttpsMode.value = "required"; - } else if (config.EnableHttps) { - selectHttpsMode.value = "enabled"; - } else { - selectHttpsMode.value = "disabled"; - } - page.querySelector("#txtHttpsPort").value = config.HttpsPortNumber; + page.querySelector("#chkEnableHttps").checked = config.EnableHttps; + page.querySelector("#chkRequireHttps").checked = config.RequireHttps; page.querySelector("#txtBaseUrl").value = config.BaseUrl || ""; var txtCertificatePath = page.querySelector("#txtCertificatePath"); txtCertificatePath.value = config.CertificatePath || ""; @@ -163,18 +125,12 @@ define(["loading", "libraryMenu", "globalize", "emby-checkbox", "emby-select"], view.querySelector(".fldExternalAddressFilterMode").classList.remove("hide"); view.querySelector(".fldPublicPort").classList.remove("hide"); view.querySelector(".fldPublicHttpsPort").classList.remove("hide"); - view.querySelector(".fldCertificatePath").classList.remove("hide"); - view.querySelector(".fldCertPassword").classList.remove("hide"); - view.querySelector(".fldHttpsMode").classList.remove("hide"); view.querySelector(".fldEnableUpnp").classList.remove("hide"); } else { view.querySelector(".fldExternalAddressFilter").classList.add("hide"); view.querySelector(".fldExternalAddressFilterMode").classList.add("hide"); view.querySelector(".fldPublicPort").classList.add("hide"); view.querySelector(".fldPublicHttpsPort").classList.add("hide"); - view.querySelector(".fldCertificatePath").classList.add("hide"); - view.querySelector(".fldCertPassword").classList.add("hide"); - view.querySelector(".fldHttpsMode").classList.add("hide"); view.querySelector(".fldEnableUpnp").classList.add("hide"); } }); diff --git a/src/networking.html b/src/networking.html index 1352e0ef5..b885e9b89 100644 --- a/src/networking.html +++ b/src/networking.html @@ -20,73 +20,89 @@
    ${LabelLocalHttpServerPortNumberHelp}
    -
    - -
    ${LabelHttpsPortHelp}
    -
    - -
    - -
    ${AllowRemoteAccessHelp}
    -
    -
    - -
    ${AllowedRemoteAddressesHelp}
    -
    -
    - -
    -
    - -
    ${LabelPublicHttpPortHelp}
    -
    -
    - -
    ${LabelPublicHttpsPortHelp}
    -
    ${LabelBaseUrlHelp}
    -
    -
    -
    - -
    - +
    +

    ${HeaderHttpsSettings}

    + +
    + +
    ${LabelEnableHttpsHelp}
    -
    ${LabelCustomCertificatePathHelp}
    -
    -
    - -
    ${LabelCertificatePasswordHelp}
    -
    +
    + +
    ${LabelRequireHttpsHelp}
    +
    -
    - -
    +
    + +
    ${LabelHttpsPortHelp}
    +
    -
    - -
    ${LabelEnableAutomaticPortMapHelp}
    -
    +
    +
    +
    + +
    + +
    +
    ${LabelCustomCertificatePathHelp}
    +
    + +
    + +
    ${LabelCertificatePasswordHelp}
    +
    + + +
    +

    ${HeaderRemoteAccessSettings}

    + +
    + +
    ${AllowRemoteAccessHelp}
    +
    +
    + +
    ${AllowedRemoteAddressesHelp}
    +
    +
    + +
    + +
    + +
    ${LabelEnableAutomaticPortMapHelp}
    +
    +
    + +
    ${LabelPublicHttpPortHelp}
    +
    +
    + +
    ${LabelPublicHttpsPortHelp}
    +
    +
    '; html += '

    '; - var moreIcon = ""; + var moreIcon = ''; html += ''; selectionCommandsPanel.innerHTML = html; From 8b71ebc35f40375037fb8b075d215cfdfaff08ba Mon Sep 17 00:00:00 2001 From: ferferga Date: Thu, 30 Apr 2020 00:05:44 +0200 Subject: [PATCH 0017/2481] more_vert in itemdetailpage.js --- src/controllers/itemdetailpage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/itemdetailpage.js b/src/controllers/itemdetailpage.js index e9242c511..992b86e4e 100644 --- a/src/controllers/itemdetailpage.js +++ b/src/controllers/itemdetailpage.js @@ -2078,7 +2078,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti bindAll(view, ".btnCancelTimer", "click", onCancelTimerClick); bindAll(view, ".btnDeleteItem", "click", onDeleteClick); bindAll(view, ".btnDownload", "click", onDownloadClick); - view.querySelector(".btnMoreCommands i").innerHTML = ""; + view.querySelector(".btnMoreCommands i").innerHTML = ""; view.querySelector(".trackSelections").addEventListener("submit", onTrackSelectionsSubmit); view.querySelector(".btnSplitVersions").addEventListener("click", function () { splitVersions(self, view, apiClient, params); From 84a22e9c3f4562a9c58d4309e752a413d8fe0428 Mon Sep 17 00:00:00 2001 From: Cromefire_ Date: Thu, 30 Apr 2020 00:31:34 +0200 Subject: [PATCH 0018/2481] Added more debug logging --- src/components/pluginManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pluginManager.js b/src/components/pluginManager.js index c87dc003e..9f3d70c06 100644 --- a/src/components/pluginManager.js +++ b/src/components/pluginManager.js @@ -54,7 +54,7 @@ define(['events', 'globalize', 'appRouter'], function (events, globalize, appRou } if (typeof pluginSpec === "string") { - console.debug('Loading plugin: ' + pluginSpec); + console.debug('Loading plugin (via deprecated requirejs method): ' + pluginSpec); return new Promise(function (resolve, reject) { require([pluginSpec], (pluginFactory) => { @@ -97,6 +97,7 @@ define(['events', 'globalize', 'appRouter'], function (events, globalize, appRou return pluginSpec.then(pluginBuilder => { return pluginBuilder(); }).then(plugin => { + console.debug(`Plugin loaded: ${plugin.id}`); return registerPlugin(plugin); }); } else { From 8655de4469c3aade0e1f949169c8f536a43e210b Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Fri, 1 May 2020 13:49:06 -0500 Subject: [PATCH 0019/2481] Rename to camel case --- .../quickConnectSettings.js} | 0 src/controllers/{quickconnect.js => quickConnect.js} | 0 src/controllers/user/{quickconnect.js => quickConnect.js} | 0 ...referencesquickconnect.html => myPreferencesQuickConnect.html} | 0 src/{quickconnect.html => quickConnect.html} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename src/components/{quickconnectsettings/quickconnectsettings.js => quickConnectSettings/quickConnectSettings.js} (100%) rename src/controllers/{quickconnect.js => quickConnect.js} (100%) rename src/controllers/user/{quickconnect.js => quickConnect.js} (100%) rename src/{mypreferencesquickconnect.html => myPreferencesQuickConnect.html} (100%) rename src/{quickconnect.html => quickConnect.html} (100%) diff --git a/src/components/quickconnectsettings/quickconnectsettings.js b/src/components/quickConnectSettings/quickConnectSettings.js similarity index 100% rename from src/components/quickconnectsettings/quickconnectsettings.js rename to src/components/quickConnectSettings/quickConnectSettings.js diff --git a/src/controllers/quickconnect.js b/src/controllers/quickConnect.js similarity index 100% rename from src/controllers/quickconnect.js rename to src/controllers/quickConnect.js diff --git a/src/controllers/user/quickconnect.js b/src/controllers/user/quickConnect.js similarity index 100% rename from src/controllers/user/quickconnect.js rename to src/controllers/user/quickConnect.js diff --git a/src/mypreferencesquickconnect.html b/src/myPreferencesQuickConnect.html similarity index 100% rename from src/mypreferencesquickconnect.html rename to src/myPreferencesQuickConnect.html diff --git a/src/quickconnect.html b/src/quickConnect.html similarity index 100% rename from src/quickconnect.html rename to src/quickConnect.html From d8cfe064e60379033fdf61a963efbc27296130af Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Fri, 1 May 2020 14:18:04 -0500 Subject: [PATCH 0020/2481] Changed code to camel case --- src/controllers/user/menu.js | 2 +- src/scripts/librarymenu.js | 2 +- src/scripts/routes.js | 8 ++++---- src/scripts/site.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/user/menu.js b/src/controllers/user/menu.js index f5d05c5be..9482b26cb 100644 --- a/src/controllers/user/menu.js +++ b/src/controllers/user/menu.js @@ -24,7 +24,7 @@ define(["apphost", "connectionManager", "layoutManager", "listViewStyle", "emby- page.querySelector(".lnkHomePreferences").setAttribute("href", "mypreferenceshome.html?userId=" + userId); page.querySelector(".lnkPlaybackPreferences").setAttribute("href", "mypreferencesplayback.html?userId=" + userId); page.querySelector(".lnkSubtitlePreferences").setAttribute("href", "mypreferencessubtitles.html?userId=" + userId); - page.querySelector(".lnkQuickConnectPreferences").setAttribute("href", "mypreferencesquickconnect.html?userId=" + userId); + page.querySelector(".lnkQuickConnectPreferences").setAttribute("href", "myPreferencesQuickConnect.html?userId=" + userId); if (window.NativeShell && window.NativeShell.AppHost.supports("clientsettings")) { page.querySelector(".clientSettings").classList.remove("hide"); diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index e3ef65f22..d123395ca 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -352,7 +352,7 @@ define(["dom", "layoutManager", "inputManager", "connectionManager", "events", " }); links.push({ name: globalize.translate("QuickConnect"), - href: "quickconnect.html", + href: "quickConnect.html", pageIds: ["quickConnectPage", "quickConnectPage"], icon: "tap_and_play" }); diff --git a/src/scripts/routes.js b/src/scripts/routes.js index 1520e9663..f0806f56e 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -73,10 +73,10 @@ define([ controller: "user/subtitles" }); defineRoute({ - path: "/mypreferencesquickconnect.html", + path: "/myPreferencesQuickConnect.html", autoFocus: false, transition: "fade", - controller: "user/quickconnect" + controller: "user/quickConnect" }); defineRoute({ @@ -110,10 +110,10 @@ define([ controller: "device" }); defineRoute({ - path: "/quickconnect.html", + path: "/quickConnect.html", autoFocus: false, roles: "admin", - controller: "quickconnect" + controller: "quickConnect" }); defineRoute({ path: "/dlnaprofile.html", diff --git a/src/scripts/site.js b/src/scripts/site.js index 3b8abf2bf..ca8a828b0 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -829,7 +829,7 @@ var AppInfo = {}; define("displaySettings", [componentsPath + "/displaysettings/displaysettings"], returnFirstDependency); define("playbackSettings", [componentsPath + "/playbacksettings/playbacksettings"], returnFirstDependency); define("homescreenSettings", [componentsPath + "/homescreensettings/homescreensettings"], returnFirstDependency); - define("quickConnectSettings", [componentsPath + "/quickconnectsettings/quickconnectsettings"], returnFirstDependency); + define("quickConnectSettings", [componentsPath + "/quickConnectSettings/quickConnectSettings"], returnFirstDependency); define("playbackManager", [componentsPath + "/playback/playbackmanager"], getPlaybackManager); define("layoutManager", [componentsPath + "/layoutManager", "apphost"], getLayoutManager); define("homeSections", [componentsPath + "/homesections/homesections"], returnFirstDependency); From 257ce4974ebee7c0977ae95cba4269805f6f8b77 Mon Sep 17 00:00:00 2001 From: Christoph Potas Date: Sun, 3 May 2020 20:30:35 +0200 Subject: [PATCH 0021/2481] ~ migrate subtitlesettings.js to es6 ~ migrate subtitleappearancehelper.js to es6 ~ replace duplicated "populateLanguages" function ( playbacksettings.js:18 ) ~ replace duplicated "onSubmit" function ( playbacksettings.js:266 ) - remove empty function getWindowStyles Signed-off-by: Christoph Potas --- package.json | 2 + .../subtitleappearancehelper.js | 242 ++++++------- .../subtitlesettings/subtitlesettings.js | 330 +++++++++--------- 3 files changed, 276 insertions(+), 298 deletions(-) diff --git a/package.json b/package.json index 53f3d225a..fda29b810 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,8 @@ "test": [ "src/components/autoFocuser.js", "src/components/cardbuilder/cardBuilder.js", + "src/components/subtitlesettings/subtitlesettings.js", + "src/components/subtitlesettings/subtitleappearancehelper.js", "src/scripts/dom.js", "src/components/filedownloader.js", "src/scripts/filesystem.js", diff --git a/src/components/subtitlesettings/subtitleappearancehelper.js b/src/components/subtitlesettings/subtitleappearancehelper.js index fdc64f0df..7bf473783 100644 --- a/src/components/subtitlesettings/subtitleappearancehelper.js +++ b/src/components/subtitlesettings/subtitleappearancehelper.js @@ -1,159 +1,149 @@ -define([], function () { - "use strict"; +function getTextStyles(settings, isCue) { - function getTextStyles(settings, isCue) { + let list = []; - var list = []; + if (isCue) { + switch (settings.textSize || '') { - if (isCue) { - switch (settings.textSize || '') { - - case 'smaller': - list.push({ name: 'font-size', value: '.5em' }); - break; - case 'small': - list.push({ name: 'font-size', value: '.7em' }); - break; - case 'large': - list.push({ name: 'font-size', value: '1.3em' }); - break; - case 'larger': - list.push({ name: 'font-size', value: '1.72em' }); - break; - case 'extralarge': - list.push({ name: 'font-size', value: '2em' }); - break; - default: - case 'medium': - break; - } - } else { - switch (settings.textSize || '') { - - case 'smaller': - list.push({ name: 'font-size', value: '.8em' }); - break; - case 'small': - list.push({ name: 'font-size', value: 'inherit' }); - break; - case 'larger': - list.push({ name: 'font-size', value: '2em' }); - break; - case 'extralarge': - list.push({ name: 'font-size', value: '2.2em' }); - break; - case 'large': - list.push({ name: 'font-size', value: '1.72em' }); - break; - default: - case 'medium': - list.push({ name: 'font-size', value: '1.36em' }); - break; - } - } - - switch (settings.dropShadow || '') { - - case 'raised': - list.push({ name: 'text-shadow', value: '-1px -1px white, 0px -1px white, -1px 0px white, 1px 1px black, 0px 1px black, 1px 0px black' }); + case 'smaller': + list.push({ name: 'font-size', value: '.5em' }); break; - case 'depressed': - list.push({ name: 'text-shadow', value: '1px 1px white, 0px 1px white, 1px 0px white, -1px -1px black, 0px -1px black, -1px 0px black' }); + case 'small': + list.push({ name: 'font-size', value: '.7em' }); break; - case 'uniform': - list.push({ name: 'text-shadow', value: '-1px 0px #000000, 0px 1px #000000, 1px 0px #000000, 0px -1px #000000' }); + case 'large': + list.push({ name: 'font-size', value: '1.3em' }); break; - case 'none': - list.push({ name: 'text-shadow', value: 'none' }); + case 'larger': + list.push({ name: 'font-size', value: '1.72em' }); + break; + case 'extralarge': + list.push({ name: 'font-size', value: '2em' }); break; default: - case 'dropshadow': - list.push({ name: 'text-shadow', value: '#000000 0px 0px 7px' }); + case 'medium': break; } + } else { + switch (settings.textSize || '') { - var background = settings.textBackground || 'transparent'; - if (background) { - list.push({ name: 'background-color', value: background }); - } - - var textColor = settings.textColor || '#ffffff'; - if (textColor) { - list.push({ name: 'color', value: textColor }); - } - - switch (settings.font || '') { - - case 'typewriter': - list.push({ name: 'font-family', value: '"Courier New",monospace' }); - list.push({ name: 'font-variant', value: 'none' }); + case 'smaller': + list.push({ name: 'font-size', value: '.8em' }); break; - case 'print': - list.push({ name: 'font-family', value: 'Georgia,Times New Roman,Arial,Helvetica,serif' }); - list.push({ name: 'font-variant', value: 'none' }); + case 'small': + list.push({ name: 'font-size', value: 'inherit' }); break; - case 'console': - list.push({ name: 'font-family', value: 'Consolas,Lucida Console,Menlo,Monaco,monospace' }); - list.push({ name: 'font-variant', value: 'none' }); + case 'larger': + list.push({ name: 'font-size', value: '2em' }); break; - case 'cursive': - list.push({ name: 'font-family', value: 'Lucida Handwriting,Brush Script MT,Segoe Script,cursive,Quintessential,system-ui,-apple-system,BlinkMacSystemFont,sans-serif' }); - list.push({ name: 'font-variant', value: 'none' }); + case 'extralarge': + list.push({ name: 'font-size', value: '2.2em' }); break; - case 'casual': - list.push({ name: 'font-family', value: 'Gabriola,Segoe Print,Comic Sans MS,Chalkboard,Short Stack,system-ui,-apple-system,BlinkMacSystemFont,sans-serif' }); - list.push({ name: 'font-variant', value: 'none' }); - break; - case 'smallcaps': - list.push({ name: 'font-family', value: 'Copperplate Gothic,Copperplate Gothic Bold,Copperplate,system-ui,-apple-system,BlinkMacSystemFont,sans-serif' }); - list.push({ name: 'font-variant', value: 'small-caps' }); + case 'large': + list.push({ name: 'font-size', value: '1.72em' }); break; default: - list.push({ name: 'font-family', value: 'inherit' }); - list.push({ name: 'font-variant', value: 'none' }); + case 'medium': + list.push({ name: 'font-size', value: '1.36em' }); break; } - - return list; } - function getWindowStyles(settings) { + switch (settings.dropShadow || '') { - return []; + case 'raised': + list.push({ name: 'text-shadow', value: '-1px -1px white, 0px -1px white, -1px 0px white, 1px 1px black, 0px 1px black, 1px 0px black' }); + break; + case 'depressed': + list.push({ name: 'text-shadow', value: '1px 1px white, 0px 1px white, 1px 0px white, -1px -1px black, 0px -1px black, -1px 0px black' }); + break; + case 'uniform': + list.push({ name: 'text-shadow', value: '-1px 0px #000000, 0px 1px #000000, 1px 0px #000000, 0px -1px #000000' }); + break; + case 'none': + list.push({ name: 'text-shadow', value: 'none' }); + break; + default: + case 'dropshadow': + list.push({ name: 'text-shadow', value: '#000000 0px 0px 7px' }); + break; } - function getStyles(settings, isCue) { - - return { - text: getTextStyles(settings, isCue), - window: getWindowStyles(settings) - }; + const background = settings.textBackground || 'transparent'; + if (background) { + list.push({ name: 'background-color', value: background }); } - function applyStyleList(styles, elem) { - - for (var i = 0, length = styles.length; i < length; i++) { - - var style = styles[i]; - - elem.style[style.name] = style.value; - } + const textColor = settings.textColor || '#ffffff'; + if (textColor) { + list.push({ name: 'color', value: textColor }); } - function applyStyles(elements, appearanceSettings) { + switch (settings.font || '') { - var styles = getStyles(appearanceSettings); - - if (elements.text) { - applyStyleList(styles.text, elements.text); - } - if (elements.window) { - applyStyleList(styles.window, elements.window); - } + case 'typewriter': + list.push({ name: 'font-family', value: '"Courier New",monospace' }); + list.push({ name: 'font-variant', value: 'none' }); + break; + case 'print': + list.push({ name: 'font-family', value: 'Georgia,Times New Roman,Arial,Helvetica,serif' }); + list.push({ name: 'font-variant', value: 'none' }); + break; + case 'console': + list.push({ name: 'font-family', value: 'Consolas,Lucida Console,Menlo,Monaco,monospace' }); + list.push({ name: 'font-variant', value: 'none' }); + break; + case 'cursive': + list.push({ name: 'font-family', value: 'Lucida Handwriting,Brush Script MT,Segoe Script,cursive,Quintessential,system-ui,-apple-system,BlinkMacSystemFont,sans-serif' }); + list.push({ name: 'font-variant', value: 'none' }); + break; + case 'casual': + list.push({ name: 'font-family', value: 'Gabriola,Segoe Print,Comic Sans MS,Chalkboard,Short Stack,system-ui,-apple-system,BlinkMacSystemFont,sans-serif' }); + list.push({ name: 'font-variant', value: 'none' }); + break; + case 'smallcaps': + list.push({ name: 'font-family', value: 'Copperplate Gothic,Copperplate Gothic Bold,Copperplate,system-ui,-apple-system,BlinkMacSystemFont,sans-serif' }); + list.push({ name: 'font-variant', value: 'small-caps' }); + break; + default: + list.push({ name: 'font-family', value: 'inherit' }); + list.push({ name: 'font-variant', value: 'none' }); + break; } + return list; +} + +export function getStyles(settings, isCue) { + return { - getStyles: getStyles, - applyStyles: applyStyles + text: getTextStyles(settings, isCue), + window: [] }; -}); +} + +function applyStyleList(styles, elem) { + + for (let i = 0, length = styles.length; i < length; i++) { + + let style = styles[i]; + + elem.style[style.name] = style.value; + } +} + +export function applyStyles(elements, appearanceSettings) { + + let styles = getStyles(appearanceSettings); + + if (elements.text) { + applyStyleList(styles.text, elements.text); + } + if (elements.window) { + applyStyleList(styles.window, elements.window); + } +} +export default { + getStyles: getStyles, + applyStyles: applyStyles +}; diff --git a/src/components/subtitlesettings/subtitlesettings.js b/src/components/subtitlesettings/subtitlesettings.js index 2c8692919..8f5038fd6 100644 --- a/src/components/subtitlesettings/subtitlesettings.js +++ b/src/components/subtitlesettings/subtitlesettings.js @@ -1,213 +1,199 @@ -define(['require', 'globalize', 'appSettings', 'apphost', 'focusManager', 'loading', 'connectionManager', 'subtitleAppearanceHelper', 'dom', 'events', 'listViewStyle', 'emby-select', 'emby-input', 'emby-checkbox', 'flexStyles'], function (require, globalize, appSettings, appHost, focusManager, loading, connectionManager, subtitleAppearanceHelper, dom, events) { - "use strict"; +import require from 'require'; +import globalize from 'globalize'; +import appHost from 'apphost'; +import appSettings from 'appSettings'; +import focusManager from 'focusManager'; +import loading from 'loading'; +import connectionManager from 'connectionmanager'; +import subtitleAppearanceHelper from 'subtitleappearancehelper'; +import playbacksettings from 'playbacksettings'; +import dom from 'dom'; +import events from 'events'; +import 'listViewStyle'; +import 'emby-select'; +import 'emby-input'; +import 'emby-checkbox'; +import 'flexStyles'; - function populateLanguages(select, languages) { - var html = ""; +function getSubtitleAppearanceObject(context) { + let appearanceSettings = {}; - html += ""; - for (var i = 0, length = languages.length; i < length; i++) { - var culture = languages[i]; - html += ""; + appearanceSettings.textSize = context.querySelector('#selectTextSize').value; + appearanceSettings.dropShadow = context.querySelector('#selectDropShadow').value; + appearanceSettings.font = context.querySelector('#selectFont').value; + appearanceSettings.textBackground = context.querySelector('#inputTextBackground').value; + appearanceSettings.textColor = context.querySelector('#inputTextColor').value; + + return appearanceSettings; +} + +function loadForm(context, user, userSettings, appearanceSettings, apiClient) { + + apiClient.getCultures().then(function (allCultures) { + + if (appHost.supports('subtitleburnsettings') && user.Policy.EnableVideoPlaybackTranscoding) { + context.querySelector('.fldBurnIn').classList.remove('hide'); } - select.innerHTML = html; - } + let selectSubtitleLanguage = context.querySelector( '#selectSubtitleLanguage' ); - function getSubtitleAppearanceObject(context) { - var appearanceSettings = {}; + playbacksettings.populateLanguages(selectSubtitleLanguage, allCultures); - appearanceSettings.textSize = context.querySelector('#selectTextSize').value; - appearanceSettings.dropShadow = context.querySelector('#selectDropShadow').value; - appearanceSettings.font = context.querySelector('#selectFont').value; - appearanceSettings.textBackground = context.querySelector('#inputTextBackground').value; - appearanceSettings.textColor = context.querySelector('#inputTextColor').value; + selectSubtitleLanguage.value = user.Configuration.SubtitleLanguagePreference || ""; + context.querySelector('#selectSubtitlePlaybackMode').value = user.Configuration.SubtitleMode || ""; - return appearanceSettings; - } + context.querySelector('#selectSubtitlePlaybackMode').dispatchEvent(new CustomEvent('change', {})); - function loadForm(context, user, userSettings, appearanceSettings, apiClient) { + context.querySelector('#selectTextSize').value = appearanceSettings.textSize || ''; + context.querySelector('#selectDropShadow').value = appearanceSettings.dropShadow || ''; + context.querySelector('#inputTextBackground').value = appearanceSettings.textBackground || 'transparent'; + context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff'; + context.querySelector('#selectFont').value = appearanceSettings.font || ''; - apiClient.getCultures().then(function (allCultures) { + context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || ''; - if (appHost.supports('subtitleburnsettings') && user.Policy.EnableVideoPlaybackTranscoding) { - context.querySelector('.fldBurnIn').classList.remove('hide'); - } + onAppearanceFieldChange({ + target: context.querySelector('#selectTextSize') + }); - var selectSubtitleLanguage = context.querySelector('#selectSubtitleLanguage'); + loading.hide(); + }); +} - populateLanguages(selectSubtitleLanguage, allCultures); +function saveUser(context, user, userSettingsInstance, appearanceKey, apiClient) { - selectSubtitleLanguage.value = user.Configuration.SubtitleLanguagePreference || ""; - context.querySelector('#selectSubtitlePlaybackMode').value = user.Configuration.SubtitleMode || ""; + let appearanceSettings = userSettingsInstance.getSubtitleAppearanceSettings( appearanceKey ); + appearanceSettings = Object.assign(appearanceSettings, getSubtitleAppearanceObject(context)); - context.querySelector('#selectSubtitlePlaybackMode').dispatchEvent(new CustomEvent('change', {})); + userSettingsInstance.setSubtitleAppearanceSettings(appearanceSettings, appearanceKey); - context.querySelector('#selectTextSize').value = appearanceSettings.textSize || ''; - context.querySelector('#selectDropShadow').value = appearanceSettings.dropShadow || ''; - context.querySelector('#inputTextBackground').value = appearanceSettings.textBackground || 'transparent'; - context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff'; - context.querySelector('#selectFont').value = appearanceSettings.font || ''; + user.Configuration.SubtitleLanguagePreference = context.querySelector('#selectSubtitleLanguage').value; + user.Configuration.SubtitleMode = context.querySelector('#selectSubtitlePlaybackMode').value; - context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || ''; + return apiClient.updateUserConfiguration(user.Id, user.Configuration); +} - onAppearanceFieldChange({ - target: context.querySelector('#selectTextSize') - }); +export function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) { + + loading.show(); + + appSettings.set('subtitleburnin', context.querySelector('#selectSubtitleBurnIn').value); + + apiClient.getUser(userId).then(function (user) { + + saveUser(context, user, userSettings, instance.appearanceKey, apiClient).then(function () { loading.hide(); + if (enableSaveConfirmation) { + require(['toast'], function (toast) { + toast(globalize.translate('SettingsSaved')); + }); + } + + events.trigger(instance, 'saved'); + + }, function () { + loading.hide(); }); + }); +} + +function onSubtitleModeChange(e) { + + let view = dom.parentWithClass( e.target, 'subtitlesettings' ); + + let subtitlesHelp = view.querySelectorAll( '.subtitlesHelp' ); + for (let i = 0, length = subtitlesHelp.length; i < length; i++) { + subtitlesHelp[i].classList.add('hide'); } + view.querySelector('.subtitles' + this.value + 'Help').classList.remove('hide'); +} - function saveUser(context, user, userSettingsInstance, appearanceKey, apiClient) { +function onAppearanceFieldChange(e) { - var appearanceSettings = userSettingsInstance.getSubtitleAppearanceSettings(appearanceKey); - appearanceSettings = Object.assign(appearanceSettings, getSubtitleAppearanceObject(context)); + let view = dom.parentWithClass( e.target, 'subtitlesettings' ); - userSettingsInstance.setSubtitleAppearanceSettings(appearanceSettings, appearanceKey); + let appearanceSettings = getSubtitleAppearanceObject( view ); - user.Configuration.SubtitleLanguagePreference = context.querySelector('#selectSubtitleLanguage').value; - user.Configuration.SubtitleMode = context.querySelector('#selectSubtitlePlaybackMode').value; + let elements = { + window: view.querySelector( '.subtitleappearance-preview-window' ), + text: view.querySelector( '.subtitleappearance-preview-text' ) + }; - return apiClient.updateUserConfiguration(user.Id, user.Configuration); - } + subtitleAppearanceHelper.applyStyles(elements, appearanceSettings); +} - function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) { +export function embed(options, self) { - loading.show(); + require(['text!./subtitlesettings.template.html'], function (template) { - appSettings.set('subtitleburnin', context.querySelector('#selectSubtitleBurnIn').value); + options.element.classList.add('subtitlesettings'); + options.element.innerHTML = globalize.translateDocument(template, 'core'); - apiClient.getUser(userId).then(function (user) { + options.element.querySelector('form').addEventListener('submit', playbacksettings.OnSubmit.bind(self)); - saveUser(context, user, userSettings, instance.appearanceKey, apiClient).then(function () { + options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange); + options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange); + options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange); + options.element.querySelector('#selectFont').addEventListener('change', onAppearanceFieldChange); + options.element.querySelector('#inputTextColor').addEventListener('change', onAppearanceFieldChange); + options.element.querySelector('#inputTextBackground').addEventListener('change', onAppearanceFieldChange); - loading.hide(); - if (enableSaveConfirmation) { - require(['toast'], function (toast) { - toast(globalize.translate('SettingsSaved')); - }); - } + if (options.enableSaveButton) { + options.element.querySelector('.btnSave').classList.remove('hide'); + } - events.trigger(instance, 'saved'); + if (appHost.supports('subtitleappearancesettings')) { + options.element.querySelector('.subtitleAppearanceSection').classList.remove('hide'); + } - }, function () { - loading.hide(); - }); - }); - } + self.loadData(); - function onSubmit(e) { - var self = this; - var apiClient = connectionManager.getApiClient(self.options.serverId); - var userId = self.options.userId; - var userSettings = self.options.userSettings; + if (options.autoFocus) { + focusManager.autoFocus(options.element); + } + }); +} +export function SubtitleSettings(options) { + + this.options = options; + + embed(options, this); +} + +SubtitleSettings.prototype.loadData = function () { + + let self = this; + let context = self.options.element; + + loading.show(); + + let userId = self.options.userId; + let apiClient = connectionManager.getApiClient( self.options.serverId ); + let userSettings = self.options.userSettings; + + apiClient.getUser(userId).then(function (user) { userSettings.setUserInfo(userId, apiClient).then(function () { - var enableSaveConfirmation = self.options.enableSaveConfirmation; - save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation); + self.dataLoaded = true; + + let appearanceSettings = userSettings.getSubtitleAppearanceSettings( self.options.appearanceKey ); + + loadForm(context, user, userSettings, appearanceSettings, apiClient); }); + }); +}; - // Disable default form submission - if (e) { - e.preventDefault(); - } +SubtitleSettings.prototype.submit = function () { + playbacksettings.onSubmit.call(this); +}; - return false; - } +SubtitleSettings.prototype.destroy = function () { + this.options = null; +}; - function onSubtitleModeChange(e) { - - var view = dom.parentWithClass(e.target, 'subtitlesettings'); - - var subtitlesHelp = view.querySelectorAll('.subtitlesHelp'); - for (var i = 0, length = subtitlesHelp.length; i < length; i++) { - subtitlesHelp[i].classList.add('hide'); - } - view.querySelector('.subtitles' + this.value + 'Help').classList.remove('hide'); - } - - function onAppearanceFieldChange(e) { - - var view = dom.parentWithClass(e.target, 'subtitlesettings'); - - var appearanceSettings = getSubtitleAppearanceObject(view); - - var elements = { - window: view.querySelector('.subtitleappearance-preview-window'), - text: view.querySelector('.subtitleappearance-preview-text') - }; - - subtitleAppearanceHelper.applyStyles(elements, appearanceSettings); - } - - function embed(options, self) { - - require(['text!./subtitlesettings.template.html'], function (template) { - - options.element.classList.add('subtitlesettings'); - options.element.innerHTML = globalize.translateDocument(template, 'core'); - - options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self)); - - options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange); - options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange); - options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange); - options.element.querySelector('#selectFont').addEventListener('change', onAppearanceFieldChange); - options.element.querySelector('#inputTextColor').addEventListener('change', onAppearanceFieldChange); - options.element.querySelector('#inputTextBackground').addEventListener('change', onAppearanceFieldChange); - - if (options.enableSaveButton) { - options.element.querySelector('.btnSave').classList.remove('hide'); - } - - if (appHost.supports('subtitleappearancesettings')) { - options.element.querySelector('.subtitleAppearanceSection').classList.remove('hide'); - } - - self.loadData(); - - if (options.autoFocus) { - focusManager.autoFocus(options.element); - } - }); - } - - function SubtitleSettings(options) { - - this.options = options; - - embed(options, this); - } - - SubtitleSettings.prototype.loadData = function () { - - var self = this; - var context = self.options.element; - - loading.show(); - - var userId = self.options.userId; - var apiClient = connectionManager.getApiClient(self.options.serverId); - var userSettings = self.options.userSettings; - - apiClient.getUser(userId).then(function (user) { - userSettings.setUserInfo(userId, apiClient).then(function () { - self.dataLoaded = true; - - var appearanceSettings = userSettings.getSubtitleAppearanceSettings(self.options.appearanceKey); - - loadForm(context, user, userSettings, appearanceSettings, apiClient); - }); - }); - }; - - SubtitleSettings.prototype.submit = function () { - onSubmit.call(this); - }; - - SubtitleSettings.prototype.destroy = function () { - this.options = null; - }; - - return SubtitleSettings; -}); +export default { + save: save, + embed: embed, + SubtitleSettings: SubtitleSettings +}; From 6f0843cc6d30629c6e8f190558389785b9037e3a Mon Sep 17 00:00:00 2001 From: Christoph Potas Date: Mon, 4 May 2020 03:20:38 +0200 Subject: [PATCH 0022/2481] ~ convert subtitlesettings to class ~ use base import name instead of relative file path ~ fix "new" calling Signed-off-by: Christoph Potas --- package.json | 1 + src/components/settingshelper.js | 24 ++++ .../subtitleappearancehelper.js | 4 + .../subtitlesettings/subtitlesettings.js | 112 +++++++++++------- src/controllers/user/subtitles.js | 2 +- 5 files changed, 97 insertions(+), 46 deletions(-) create mode 100644 src/components/settingshelper.js diff --git a/package.json b/package.json index fda29b810..c11b6db91 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "src/components/cardbuilder/cardBuilder.js", "src/components/subtitlesettings/subtitlesettings.js", "src/components/subtitlesettings/subtitleappearancehelper.js", + "src/components/settingshelper.js", "src/scripts/dom.js", "src/components/filedownloader.js", "src/scripts/filesystem.js", diff --git a/src/components/settingshelper.js b/src/components/settingshelper.js new file mode 100644 index 000000000..0c942ed04 --- /dev/null +++ b/src/components/settingshelper.js @@ -0,0 +1,24 @@ +import globalize from 'globalize'; +/** + * Helper for handling settings + * @module components/settingsHelper + */ +export function populateLanguages(select, languages) { + + let html = ""; + + html += ""; + + for (let i = 0, length = languages.length; i < length; i++) { + + const culture = languages[i]; + + html += ""; + } + + select.innerHTML = html; +} + +export default { + populateLanguages: populateLanguages +}; diff --git a/src/components/subtitlesettings/subtitleappearancehelper.js b/src/components/subtitlesettings/subtitleappearancehelper.js index 7bf473783..d1c55e1ae 100644 --- a/src/components/subtitlesettings/subtitleappearancehelper.js +++ b/src/components/subtitlesettings/subtitleappearancehelper.js @@ -1,3 +1,7 @@ +/** + * Subtitle settings visual helper + * @module components/subtitleSettings/subtitleAppearanceHelper + */ function getTextStyles(settings, isCue) { let list = []; diff --git a/src/components/subtitlesettings/subtitlesettings.js b/src/components/subtitlesettings/subtitlesettings.js index 8f5038fd6..3e41da953 100644 --- a/src/components/subtitlesettings/subtitlesettings.js +++ b/src/components/subtitlesettings/subtitlesettings.js @@ -4,9 +4,9 @@ import appHost from 'apphost'; import appSettings from 'appSettings'; import focusManager from 'focusManager'; import loading from 'loading'; -import connectionManager from 'connectionmanager'; -import subtitleAppearanceHelper from 'subtitleappearancehelper'; -import playbacksettings from 'playbacksettings'; +import connectionManager from 'connectionManager'; +import subtitleAppearanceHelper from 'subtitleAppearanceHelper'; +import settingsHelper from '../settingshelper'; import dom from 'dom'; import events from 'events'; import 'listViewStyle'; @@ -15,6 +15,11 @@ import 'emby-input'; import 'emby-checkbox'; import 'flexStyles'; +/** + * Subtitle settings + * @module components/subtitleSettings/subtitleSettings + */ + function getSubtitleAppearanceObject(context) { let appearanceSettings = {}; @@ -37,7 +42,7 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) { let selectSubtitleLanguage = context.querySelector( '#selectSubtitleLanguage' ); - playbacksettings.populateLanguages(selectSubtitleLanguage, allCultures); + settingsHelper.populateLanguages(selectSubtitleLanguage, allCultures); selectSubtitleLanguage.value = user.Configuration.SubtitleLanguagePreference || ""; context.querySelector('#selectSubtitlePlaybackMode').value = user.Configuration.SubtitleMode || ""; @@ -73,7 +78,7 @@ function saveUser(context, user, userSettingsInstance, appearanceKey, apiClient) return apiClient.updateUserConfiguration(user.Id, user.Configuration); } -export function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) { +function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) { loading.show(); @@ -123,14 +128,14 @@ function onAppearanceFieldChange(e) { subtitleAppearanceHelper.applyStyles(elements, appearanceSettings); } -export function embed(options, self) { +function embed(options, self) { require(['text!./subtitlesettings.template.html'], function (template) { options.element.classList.add('subtitlesettings'); options.element.innerHTML = globalize.translateDocument(template, 'core'); - options.element.querySelector('form').addEventListener('submit', playbacksettings.OnSubmit.bind(self)); + options.element.querySelector('form').addEventListener('submit', self.onSubmit ); options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange); options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange); @@ -155,45 +160,62 @@ export function embed(options, self) { }); } -export function SubtitleSettings(options) { +export class SubtitleSettings { - this.options = options; + constructor(options) { - embed(options, this); + this.options = options; + + embed(options, this); + } + + loadData() { + let self = this; + let context = self.options.element; + + loading.show(); + + let userId = self.options.userId; + let apiClient = connectionManager.getApiClient( self.options.serverId ); + let userSettings = self.options.userSettings; + + apiClient.getUser(userId).then(function (user) { + userSettings.setUserInfo(userId, apiClient).then(function () { + self.dataLoaded = true; + + let appearanceSettings = userSettings.getSubtitleAppearanceSettings( self.options.appearanceKey ); + + loadForm(context, user, userSettings, appearanceSettings, apiClient); + }); + }); + } + + submit() { + this.onSubmit( null ); + } + + destroy() { + this.options = null; + } + + onSubmit( e ) { + const self = this; + let apiClient = connectionManager.getApiClient(self.options.serverId); + let userId = self.options.userId; + let userSettings = self.options.userSettings; + + userSettings.setUserInfo(userId, apiClient).then(function () { + + let enableSaveConfirmation = self.options.enableSaveConfirmation; + save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation); + }); + + // Disable default form submission + if (e) { + e.preventDefault(); + } + return false; + } } -SubtitleSettings.prototype.loadData = function () { - - let self = this; - let context = self.options.element; - - loading.show(); - - let userId = self.options.userId; - let apiClient = connectionManager.getApiClient( self.options.serverId ); - let userSettings = self.options.userSettings; - - apiClient.getUser(userId).then(function (user) { - userSettings.setUserInfo(userId, apiClient).then(function () { - self.dataLoaded = true; - - let appearanceSettings = userSettings.getSubtitleAppearanceSettings( self.options.appearanceKey ); - - loadForm(context, user, userSettings, appearanceSettings, apiClient); - }); - }); -}; - -SubtitleSettings.prototype.submit = function () { - playbacksettings.onSubmit.call(this); -}; - -SubtitleSettings.prototype.destroy = function () { - this.options = null; -}; - -export default { - save: save, - embed: embed, - SubtitleSettings: SubtitleSettings -}; +export default SubtitleSettings; diff --git a/src/controllers/user/subtitles.js b/src/controllers/user/subtitles.js index e2b98dc2d..f449b655c 100644 --- a/src/controllers/user/subtitles.js +++ b/src/controllers/user/subtitles.js @@ -18,7 +18,7 @@ define(["subtitleSettings", "userSettings", "autoFocuser"], function (SubtitleSe if (subtitleSettingsInstance) { subtitleSettingsInstance.loadData(); } else { - subtitleSettingsInstance = new SubtitleSettings({ + subtitleSettingsInstance = new SubtitleSettings.default({ serverId: ApiClient.serverId(), userId: userId, element: view.querySelector(".settingsContainer"), From ea18f97e8b68ba8d7e6041dfe507912d94fd6485 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Mon, 4 May 2020 18:09:10 +0200 Subject: [PATCH 0023/2481] Fix change requests in videoOsd --- src/controllers/playback/videoosd.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/controllers/playback/videoosd.js b/src/controllers/playback/videoosd.js index 3c3e88bf7..b5bf250aa 100644 --- a/src/controllers/playback/videoosd.js +++ b/src/controllers/playback/videoosd.js @@ -1178,13 +1178,10 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med playbackManager.fastForward(currentPlayer); showOsd(); } - // case "Home": playbackManager.seekPercent(0, currentPlayer); break; case "End": - // Honestly this binding seems kind of pointless, just use 'nextTrack', - // but YouTube has this one and that's the standard I'm aiming for playbackManager.seekPercent(100, currentPlayer); break; case "0": @@ -1197,8 +1194,7 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med case "7": case "8": case "9": - var percent = parseInt(key); - percent *= 10; + var percent = parseInt(key, 10) * 10; playbackManager.seekPercent(percent, currentPlayer); break; } From adb8f4114ba216b9665a3de4011cc226fcf02079 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Mon, 4 May 2020 18:13:34 +0200 Subject: [PATCH 0024/2481] Add missing break in videoOsd --- src/controllers/playback/videoosd.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/playback/videoosd.js b/src/controllers/playback/videoosd.js index b5bf250aa..05fbac7bb 100644 --- a/src/controllers/playback/videoosd.js +++ b/src/controllers/playback/videoosd.js @@ -1178,6 +1178,7 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med playbackManager.fastForward(currentPlayer); showOsd(); } + break; case "Home": playbackManager.seekPercent(0, currentPlayer); break; From 2655b4dca3da200f9dc720f37b4d6e7f32b4d839 Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Mon, 4 May 2020 17:23:57 -0500 Subject: [PATCH 0025/2481] Migrate API changes to apiclient repo --- src/libraries/apiclient/apiclientcore.js | 25 ------------------------ 1 file changed, 25 deletions(-) diff --git a/src/libraries/apiclient/apiclientcore.js b/src/libraries/apiclient/apiclientcore.js index e1892505f..557a4e103 100644 --- a/src/libraries/apiclient/apiclientcore.js +++ b/src/libraries/apiclient/apiclientcore.js @@ -356,31 +356,6 @@ define(["events", "appStorage"], function(events, appStorage) { instance.onAuthenticated ? instance.onAuthenticated(instance, result).then(afterOnAuthenticated) : afterOnAuthenticated() }, reject) }) - }, ApiClient.prototype.quickConnect = function (token) { - if (!token) return Promise.reject(); - var url = this.getUrl("Users/AuthenticateWithQuickConnect"); - var instance = this; - return new Promise(function(resolve, reject) { - var postData = { - Token: token - }; - instance.ajax({ - type: "POST", - url: url, - data: JSON.stringify(postData), - dataType: "json", - contentType: "application/json" - }).then(function(result) { - var afterOnAuthenticated = function() { - redetectBitrate(instance); - return resolve(result); - }; - instance.onAuthenticated ? instance.onAuthenticated(instance, result).then(afterOnAuthenticated) : afterOnAuthenticated() - }, reject) - }) - }, ApiClient.prototype.getQuickConnect = function(verb) { - var url = this.getUrl("/QuickConnect/" + verb); - return this.getJSON(url); }, ApiClient.prototype.ensureWebSocket = function() { if (!this.isWebSocketOpenOrConnecting() && this.isWebSocketSupported()) try { this.openWebSocket() From 5b40232b7bf96f5468429f17f928d26eb169076f Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Thu, 7 May 2020 09:45:49 +0300 Subject: [PATCH 0026/2481] Use built-in swiper classes --- src/components/slideshow/slideshow.js | 5 ++--- src/components/slideshow/style.css | 21 --------------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/src/components/slideshow/slideshow.js b/src/components/slideshow/slideshow.js index 4716135ce..b92d956ff 100644 --- a/src/components/slideshow/slideshow.js +++ b/src/components/slideshow/slideshow.js @@ -260,8 +260,7 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f loop: false, zoom: { minRatio: 1, - toggle: true, - containerClass: 'slider-zoom-container' + toggle: true }, autoplay: !options.interactive, keyboard: { @@ -328,7 +327,7 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f function getSwiperSlideHtmlFromSlide(item) { var html = ''; html += '
    '; - html += '
    '; + html += '
    '; html += ''; html += '
    '; if (item.title || item.subtitle) { diff --git a/src/components/slideshow/style.css b/src/components/slideshow/style.css index f1fea508d..400c47566 100644 --- a/src/components/slideshow/style.css +++ b/src/components/slideshow/style.css @@ -40,16 +40,6 @@ text-shadow: 3px 3px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; } -.swiper-slide-img { - max-height: 100%; - max-width: 100%; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - margin: auto; -} - .slideshowButtonIcon { color: #fff; opacity: 0.7; @@ -134,14 +124,3 @@ .slideSubtitle { color: #ccc; } - -.swiper-slide { - display: flex; - flex-direction: column; -} - -.slider-zoom-container { - margin: auto; - max-height: 100%; - max-width: 100%; -} From f516bee14798ddd151d5d2c105b5ad845ce48737 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Thu, 7 May 2020 23:11:19 +0300 Subject: [PATCH 0027/2481] Fix iOS blurry zoomed image --- src/components/slideshow/slideshow.js | 44 ++++++++++++++++++++++++++- src/components/slideshow/style.css | 16 ++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/components/slideshow/slideshow.js b/src/components/slideshow/slideshow.js index b92d956ff..e7302b734 100644 --- a/src/components/slideshow/slideshow.js +++ b/src/components/slideshow/slideshow.js @@ -2,9 +2,14 @@ * Image viewer component * @module components/slideshow/slideshow */ -define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'focusManager', 'browser', 'apphost', 'css!./style', 'material-icons', 'paper-icon-button-light'], function (dialogHelper, inputManager, connectionManager, layoutManager, focusManager, browser, appHost) { +define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'focusManager', 'browser', 'apphost', 'dom', 'css!./style', 'material-icons', 'paper-icon-button-light'], function (dialogHelper, inputManager, connectionManager, layoutManager, focusManager, browser, appHost, dom) { 'use strict'; + /** + * Name of transition event. + */ + const transitionEndEventName = dom.whichTransitionEvent(); + /** * Retrieves an item's image URL from the API. * @param {object|string} item - Item used to generate the image URL. @@ -240,6 +245,41 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f } } + /** + * Handles zoom changes. + */ + function onZoomChange(scale, imageEl, slideEl) { + const zoomImage = slideEl.querySelector('.swiper-zoom-fakeimg'); + + if (zoomImage) { + zoomImage.style.width = zoomImage.style.height = scale * 100 + '%'; + + if (scale > 1) { + if (zoomImage.classList.contains('swiper-zoom-fakeimg-hidden')) { + // Await for Swiper style changes + setTimeout(() => { + const callback = () => { + imageEl.removeEventListener(transitionEndEventName, callback); + zoomImage.classList.remove('swiper-zoom-fakeimg-hidden'); + }; + + // Swiper set 'transition-duration: 300ms' for auto zoom + // and 'transition-duration: 0s' for touch zoom + const transitionDuration = parseFloat(imageEl.style.transitionDuration.replace(/[a-z]/i, '')); + + if (transitionDuration > 0) { + imageEl.addEventListener(transitionEndEventName, callback); + } else { + callback(); + } + }, 0); + } + } else { + zoomImage.classList.add('swiper-zoom-fakeimg-hidden'); + } + } + } + /** * Initializes the Swiper instance and binds the relevant events. * @param {HTMLElement} dialog - Element containing the dialog. @@ -287,6 +327,7 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f swiperInstance.on('autoplayStart', onAutoplayStart); swiperInstance.on('autoplayStop', onAutoplayStop); + swiperInstance.on('zoomChange', onZoomChange); }); } @@ -328,6 +369,7 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f var html = ''; html += '
    '; html += '
    '; + html += `
    `; html += ''; html += '
    '; if (item.title || item.subtitle) { diff --git a/src/components/slideshow/style.css b/src/components/slideshow/style.css index 400c47566..af50eb9cd 100644 --- a/src/components/slideshow/style.css +++ b/src/components/slideshow/style.css @@ -124,3 +124,19 @@ .slideSubtitle { color: #ccc; } + +.swiper-zoom-fakeimg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-position: 50% 50%; + background-repeat: no-repeat; + background-size: contain; + z-index: 1; + pointer-events: none; +} + +.swiper-zoom-fakeimg-hidden { + display: none; +} From 1fe17007d6ba2db4553919351967988380e435b0 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Fri, 8 May 2020 01:23:17 +0300 Subject: [PATCH 0028/2481] Enable fake zoom image on Safari (WebKit) --- src/components/slideshow/slideshow.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/slideshow/slideshow.js b/src/components/slideshow/slideshow.js index e7302b734..8e3f0d873 100644 --- a/src/components/slideshow/slideshow.js +++ b/src/components/slideshow/slideshow.js @@ -10,6 +10,12 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f */ const transitionEndEventName = dom.whichTransitionEvent(); + /** + * Flag to use fake image to fix blurry zoomed image. + * At least WebKit doesn't restore quality for zoomed images. + */ + const useFakeZoomImage = browser.safari; + /** * Retrieves an item's image URL from the API. * @param {object|string} item - Item used to generate the image URL. @@ -327,7 +333,10 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f swiperInstance.on('autoplayStart', onAutoplayStart); swiperInstance.on('autoplayStop', onAutoplayStop); - swiperInstance.on('zoomChange', onZoomChange); + + if (useFakeZoomImage) { + swiperInstance.on('zoomChange', onZoomChange); + } }); } @@ -369,7 +378,9 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f var html = ''; html += '
    '; html += '
    '; - html += `
    `; + if (useFakeZoomImage) { + html += `
    `; + } html += ''; html += '
    '; if (item.title || item.subtitle) { From 658710e982db7a435d5396549d5712b955fb320b Mon Sep 17 00:00:00 2001 From: Christoph Potas Date: Sat, 9 May 2020 01:20:32 +0200 Subject: [PATCH 0029/2481] + added settingshelper to site defines ~ convert subtitles controller to es6 module + added support for class controller Signed-off-by: Christoph Potas --- .../subtitlesettings/subtitlesettings.js | 2 +- src/components/viewManager/viewManager.js | 4 + src/controllers/user/subtitles.js | 102 ++++++++++-------- src/scripts/site.js | 1 + 4 files changed, 64 insertions(+), 45 deletions(-) diff --git a/src/components/subtitlesettings/subtitlesettings.js b/src/components/subtitlesettings/subtitlesettings.js index 3e41da953..e03c718fc 100644 --- a/src/components/subtitlesettings/subtitlesettings.js +++ b/src/components/subtitlesettings/subtitlesettings.js @@ -6,7 +6,7 @@ import focusManager from 'focusManager'; import loading from 'loading'; import connectionManager from 'connectionManager'; import subtitleAppearanceHelper from 'subtitleAppearanceHelper'; -import settingsHelper from '../settingshelper'; +import settingsHelper from 'settingsHelper'; import dom from 'dom'; import events from 'events'; import 'listViewStyle'; diff --git a/src/components/viewManager/viewManager.js b/src/components/viewManager/viewManager.js index a8e514e06..f39c4995f 100644 --- a/src/components/viewManager/viewManager.js +++ b/src/components/viewManager/viewManager.js @@ -25,6 +25,10 @@ define(['viewContainer', 'focusManager', 'queryString', 'layoutManager'], functi // Use controller method var controller = new options.controllerFactory(newView, eventDetail.detail.params); + } else if (typeof options.controllerFactory === 'object') { + + // Use controller class + var controller = new options.controllerFactory.default(newView, eventDetail.detail.params); } if (!options.controllerFactory || dispatchPageEvents) { diff --git a/src/controllers/user/subtitles.js b/src/controllers/user/subtitles.js index f449b655c..f1efe0c21 100644 --- a/src/controllers/user/subtitles.js +++ b/src/controllers/user/subtitles.js @@ -1,49 +1,63 @@ -define(["subtitleSettings", "userSettings", "autoFocuser"], function (SubtitleSettings, userSettings, autoFocuser) { - "use strict"; +import subtitleSettings from 'subtitleSettings'; +import * as userSettings from 'userSettings'; +import autoFocuser from 'autoFocuser'; - return function (view, params) { - function onBeforeUnload(e) { - if (hasChanges) { - e.returnValue = "You currently have unsaved changes. Are you sure you wish to leave?"; - } +export class SubtitleController { + constructor(view, params) { + this.userId = params.userId || ApiClient.getCurrentUserId(); + this.currentSettings = this.userId === ApiClient.getCurrentUserId() ? userSettings : new userSettings(); + this.hasChanges = false; + this.subtitleSettingsInstance = null; + this.view = view; + + view.addEventListener("viewshow", this.viewShow.bind(this)); + view.addEventListener("change", this.change.bind(this)); + view.addEventListener("viewbeforehide", this.viewBeforeHide.bind(this)); + view.addEventListener("viewdestroy", this.viewDestroy.bind(this)); + } + + viewShow() { + window.addEventListener("beforeunload", this.beforeUnload.bind(this)); + + if (this.subtitleSettingsInstance) { + this.subtitleSettingsInstance.loadData(); + } else { + this.subtitleSettingsInstance = new subtitleSettings({ + serverId: ApiClient.serverId(), + userId: this.userId, + element: this.view.querySelector(".settingsContainer"), + userSettings: this.currentSettings, + enableSaveButton: false, + enableSaveConfirmation: false, + autoFocus: autoFocuser.isEnabled() + }); } + } - var subtitleSettingsInstance; - var hasChanges; - var userId = params.userId || ApiClient.getCurrentUserId(); - var currentSettings = userId === ApiClient.getCurrentUserId() ? userSettings : new userSettings(); - view.addEventListener("viewshow", function () { - window.addEventListener("beforeunload", onBeforeUnload); + viewDestroy() { + if (this.subtitleSettingsInstance) { + this.subtitleSettingsInstance.destroy(); + this.subtitleSettingsInstance = null; + } + } - if (subtitleSettingsInstance) { - subtitleSettingsInstance.loadData(); - } else { - subtitleSettingsInstance = new SubtitleSettings.default({ - serverId: ApiClient.serverId(), - userId: userId, - element: view.querySelector(".settingsContainer"), - userSettings: currentSettings, - enableSaveButton: false, - enableSaveConfirmation: false, - autoFocus: autoFocuser.isEnabled() - }); - } - }); - view.addEventListener("change", function () { - hasChanges = true; - }); - view.addEventListener("viewbeforehide", function () { - hasChanges = false; + viewBeforeHide() { + this.hasChanges = false; - if (subtitleSettingsInstance) { - subtitleSettingsInstance.submit(); - } - }); - view.addEventListener("viewdestroy", function () { - if (subtitleSettingsInstance) { - subtitleSettingsInstance.destroy(); - subtitleSettingsInstance = null; - } - }); - }; -}); + if (this.subtitleSettingsInstance) { + this.subtitleSettingsInstance.submit(); + } + } + + change() { + this.hasChanges = true; + } + + beforeUnload(e) { + if (this.hasChanges) { + e.returnValue = "You currently have unsaved changes. Are you sure you wish to leave?"; + } + } +} + +export default SubtitleController; diff --git a/src/scripts/site.js b/src/scripts/site.js index 5ce093e62..37832af5c 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -831,6 +831,7 @@ var AppInfo = {}; define("upNextDialog", [componentsPath + "/upnextdialog/upnextdialog"], returnFirstDependency); define("subtitleAppearanceHelper", [componentsPath + "/subtitlesettings/subtitleappearancehelper"], returnFirstDependency); define("subtitleSettings", [componentsPath + "/subtitlesettings/subtitlesettings"], returnFirstDependency); + define("settingsHelper", [componentsPath + "/settingshelper"], returnFirstDependency); define("displaySettings", [componentsPath + "/displaysettings/displaysettings"], returnFirstDependency); define("playbackSettings", [componentsPath + "/playbacksettings/playbacksettings"], returnFirstDependency); define("homescreenSettings", [componentsPath + "/homescreensettings/homescreensettings"], returnFirstDependency); From e5bf9bc074d59af7b5e76c734e0af1c6741add52 Mon Sep 17 00:00:00 2001 From: Christoph Potas Date: Sat, 9 May 2020 01:53:13 +0200 Subject: [PATCH 0030/2481] + add controller to es6 module list Signed-off-by: Christoph Potas --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c11b6db91..4d3d3047b 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "src/components/subtitlesettings/subtitlesettings.js", "src/components/subtitlesettings/subtitleappearancehelper.js", "src/components/settingshelper.js", + "src/controllers/user/subtitles.js", "src/scripts/dom.js", "src/components/filedownloader.js", "src/scripts/filesystem.js", From 6c18b655e0b4823e4b7a6377cc1ebc4fbc27ab12 Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 1 Apr 2020 17:53:14 +0200 Subject: [PATCH 0031/2481] Implement syncplay frontend --- src/assets/css/videoosd.css | 2 +- src/components/htmlaudioplayer/plugin.js | 42 ++ src/components/htmlvideoplayer/plugin.js | 25 + src/components/playback/playbackmanager.js | 21 + src/components/playerstats/playerstats.js | 31 +- src/components/serverNotifications.js | 6 +- src/components/syncplay/groupSelectionMenu.js | 140 ++++ src/components/syncplay/syncplaymanager.js | 600 ++++++++++++++++++ src/scripts/librarymenu.js | 36 +- src/scripts/site.js | 1 + src/strings/en-us.json | 15 + 11 files changed, 915 insertions(+), 4 deletions(-) create mode 100644 src/components/syncplay/groupSelectionMenu.js create mode 100644 src/components/syncplay/syncplaymanager.js diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index f4f198325..50cb41021 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -30,7 +30,7 @@ opacity: 0; } -.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton) { +.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton):not(.headerSyncButton) { display: none; } diff --git a/src/components/htmlaudioplayer/plugin.js b/src/components/htmlaudioplayer/plugin.js index c3a5484ac..258048105 100644 --- a/src/components/htmlaudioplayer/plugin.js +++ b/src/components/htmlaudioplayer/plugin.js @@ -171,6 +171,7 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp elem.addEventListener('pause', onPause); elem.addEventListener('playing', onPlaying); elem.addEventListener('play', onPlay); + elem.addEventListener('waiting', onWaiting); } function unBindEvents(elem) { @@ -180,6 +181,7 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp elem.removeEventListener('pause', onPause); elem.removeEventListener('playing', onPlaying); elem.removeEventListener('play', onPlay); + elem.removeEventListener('waiting', onWaiting); } self.stop = function (destroyPlayer) { @@ -294,6 +296,10 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp events.trigger(self, 'pause'); } + function onWaiting() { + events.trigger(self, 'waiting'); + } + function onError() { var errorCode = this.error ? (this.error.code || 0) : 0; @@ -450,6 +456,21 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp return false; }; + HtmlAudioPlayer.prototype.setPlaybackRate = function (value) { + var mediaElement = this._mediaElement; + if (mediaElement) { + mediaElement.playbackRate = value; + } + }; + + HtmlAudioPlayer.prototype.getPlaybackRate = function () { + var mediaElement = this._mediaElement; + if (mediaElement) { + return mediaElement.playbackRate; + } + return null; + }; + HtmlAudioPlayer.prototype.setVolume = function (val) { var mediaElement = this._mediaElement; if (mediaElement) { @@ -493,5 +514,26 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp }; + var supportedFeatures; + + function getSupportedFeatures() { + var list = []; + var audio = document.createElement('audio'); + + if (typeof audio.playbackRate === "number") { + list.push("PlaybackRate"); + } + + return list; + } + + HtmlAudioPlayer.prototype.supports = function (feature) { + if (!supportedFeatures) { + supportedFeatures = getSupportedFeatures(); + } + + return supportedFeatures.indexOf(feature) !== -1; + }; + return HtmlAudioPlayer; }); diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js index f87fd1946..064e4155e 100644 --- a/src/components/htmlvideoplayer/plugin.js +++ b/src/components/htmlvideoplayer/plugin.js @@ -785,6 +785,7 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa videoElement.removeEventListener('play', onPlay); videoElement.removeEventListener('click', onClick); videoElement.removeEventListener('dblclick', onDblClick); + videoElement.removeEventListener('waiting', onWaiting); videoElement.parentNode.removeChild(videoElement); } @@ -915,6 +916,10 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa events.trigger(self, 'pause'); } + function onWaiting() { + events.trigger(self, 'waiting'); + } + function onError() { var errorCode = this.error ? (this.error.code || 0) : 0; var errorMessage = this.error ? (this.error.message || '') : ''; @@ -1348,6 +1353,7 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa videoElement.addEventListener('play', onPlay); videoElement.addEventListener('click', onClick); videoElement.addEventListener('dblclick', onDblClick); + videoElement.addEventListener('waiting', onWaiting); document.body.insertBefore(dlg, document.body.firstChild); videoDialog = dlg; @@ -1436,6 +1442,10 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa list.push('AirPlay'); } + if (typeof video.playbackRate === "number") { + list.push("PlaybackRate"); + } + list.push('SetBrightness'); list.push('SetAspectRatio'); @@ -1656,6 +1666,21 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa return false; }; + HtmlVideoPlayer.prototype.setPlaybackRate = function (value) { + var mediaElement = this._mediaElement; + if (mediaElement) { + mediaElement.playbackRate = value; + } + }; + + HtmlVideoPlayer.prototype.getPlaybackRate = function () { + var mediaElement = this._mediaElement; + if (mediaElement) { + return mediaElement.playbackRate; + } + return null; + }; + HtmlVideoPlayer.prototype.setVolume = function (val) { var mediaElement = this._mediaElement; if (mediaElement) { diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index ee85f9acb..923092cd3 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -3777,6 +3777,24 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla } }; + PlaybackManager.prototype.setPlaybackRate = function (value, player) { + player = player || this._currentPlayer; + + if (player) { + player.setPlaybackRate(value); + } + }; + + PlaybackManager.prototype.getPlaybackRate = function (player) { + player = player || this._currentPlayer; + + if (player) { + return player.getPlaybackRate(val); + } + + return null; + }; + PlaybackManager.prototype.instantMix = function (item, player) { player = player || this._currentPlayer; @@ -3887,6 +3905,9 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla if (player.supports('SetAspectRatio')) { list.push('SetAspectRatio'); } + if (player.supports('PlaybackRate')) { + list.push('PlaybackRate'); + } } return list; diff --git a/src/components/playerstats/playerstats.js b/src/components/playerstats/playerstats.js index c0fb369c6..ad4ce960b 100644 --- a/src/components/playerstats/playerstats.js +++ b/src/components/playerstats/playerstats.js @@ -1,4 +1,4 @@ -define(['events', 'globalize', 'playbackManager', 'connectionManager', 'playMethodHelper', 'layoutManager', 'serverNotifications', 'paper-icon-button-light', 'css!./playerstats'], function (events, globalize, playbackManager, connectionManager, playMethodHelper, layoutManager, serverNotifications) { +define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplayManager', 'playMethodHelper', 'layoutManager', 'serverNotifications', 'paper-icon-button-light', 'css!./playerstats'], function (events, globalize, playbackManager, connectionManager, syncplayManager, playMethodHelper, layoutManager, serverNotifications) { 'use strict'; function init(instance) { @@ -327,6 +327,28 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'playMeth return sessionStats; } + function getSyncplayStats() { + var syncStats = []; + var stats = syncplayManager.getStats(); + + syncStats.push({ + label: globalize.translate("LabelSyncplayTimeDiff"), + value: stats.TimeDiff + "ms" + }); + + syncStats.push({ + label: globalize.translate("LabelSyncplayPlaybackDiff"), + value: stats.PlaybackDiff + "ms" + }); + + syncStats.push({ + label: globalize.translate("LabelSyncplaySyncMethod"), + value: stats.SyncMethod + }); + + return syncStats; + } + function getStats(instance, player) { var statsPromise = player.getStats ? player.getStats() : Promise.resolve({}); @@ -383,6 +405,13 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'playMeth name: 'Original Media Info' }); + if (syncplayManager.isSyncplayEnabled()) { + categories.push({ + stats: getSyncplayStats(), + name: 'Syncplay Info' + }); + } + return Promise.resolve(categories); }); } diff --git a/src/components/serverNotifications.js b/src/components/serverNotifications.js index e60f98475..9776c88bd 100644 --- a/src/components/serverNotifications.js +++ b/src/components/serverNotifications.js @@ -1,4 +1,4 @@ -define(['connectionManager', 'playbackManager', 'events', 'inputManager', 'focusManager', 'appRouter'], function (connectionManager, playbackManager, events, inputManager, focusManager, appRouter) { +define(['connectionManager', 'playbackManager', 'syncplayManager', 'events', 'inputManager', 'focusManager', 'appRouter'], function (connectionManager, playbackManager, syncplayManager, events, inputManager, focusManager, appRouter) { 'use strict'; var serverNotifications = {}; @@ -187,6 +187,10 @@ define(['connectionManager', 'playbackManager', 'events', 'inputManager', 'focus events.trigger(serverNotifications, 'UserDataChanged', [apiClient, msg.Data.UserDataList[i]]); } } + } else if (msg.MessageType === "SyncplayCommand") { + syncplayManager.processCommand(msg.Data, apiClient); + } else if (msg.MessageType === "SyncplayGroupUpdate") { + syncplayManager.processGroupUpdate(msg.Data, apiClient); } else { events.trigger(serverNotifications, msg.MessageType, [apiClient, msg.Data]); } diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js new file mode 100644 index 000000000..01141741c --- /dev/null +++ b/src/components/syncplay/groupSelectionMenu.js @@ -0,0 +1,140 @@ +define(['events', 'loading', 'connectionManager', 'playbackManager', 'syncplayManager', 'globalize', 'datetime'], function (events, loading, connectionManager, playbackManager, syncplayManager, globalize, datetime) { + 'use strict'; + + function getActivePlayerId() { + var info = playbackManager.getPlayerInfo(); + return info ? info.id : null; + } + + function emptyCallback() { + // avoid console logs about uncaught promises + } + + function showNewJoinGroupSelection(button) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + sessionId = sessionId ? sessionId : "none"; + + loading.show(); + + apiClient.sendSyncplayCommand(sessionId, "ListGroups").then(function (response) { + response.json().then(function (groups) { + var inSession = sessionId !== "none"; + + var menuItems = groups.map(function (group) { + var name = datetime.getDisplayRunningTime(group.PositionTicks); + if (!inSession) { + name = group.PlayingItemName + } + return { + name: name, + icon: "group", + id: group.GroupId, + selected: false, + secondaryText: group.Partecipants.join(", ") + } + }); + + if (inSession) { + menuItems.push({ + name: globalize.translate('LabelSyncplayNewGroup'), + icon: "add", + id: "new-group", + selected: true, + secondaryText: globalize.translate('LabelSyncplayNewGroupDescription') + }); + } + + if (menuItems.length === 0) { + require(['toast'], function (alert) { + alert({ + title: globalize.translate('MessageSyncplayNoGroupsAvailable'), + text: globalize.translate('MessageSyncplayNoGroupsAvailable') + }); + }); + loading.hide(); + return; + } + + require(['actionsheet'], function (actionsheet) { + + loading.hide(); + + var menuOptions = { + title: globalize.translate('HeaderSyncplaySelectGroup'), + items: menuItems, + positionTo: button, + resolveOnClick: true, + border: true + }; + + actionsheet.show(menuOptions).then(function (id) { + + if (id == "new-group") { + apiClient.sendSyncplayCommand(sessionId, "NewGroup"); + } else { + apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { + GroupId: id + }); + } + }, emptyCallback); + }); + }); + }).catch(function (error) { + loading.hide(); + console.error(error); + }); + } + + function showLeaveGroupSelection(button) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + + loading.show(); + + var menuItems = [{ + name: globalize.translate('LabelSyncplayLeaveGroup'), + icon: "meeting_room", + id: "leave-group", + selected: true, + secondaryText: globalize.translate('LabelSyncplayLeaveGroupDescription') + }]; + + require(['actionsheet'], function (actionsheet) { + + loading.hide(); + + var menuOptions = { + title: globalize.translate('HeaderSyncplayEnabled'), + items: menuItems, + positionTo: button, + resolveOnClick: true, + border: true + }; + + actionsheet.show(menuOptions).then(function (id) { + if (id == "leave-group") { + apiClient.sendSyncplayCommand(sessionId, "LeaveGroup"); + } + }, emptyCallback); + }); + } + + function showGroupSelection(button) { + if (syncplayEnabled) { + showLeaveGroupSelection(button); + } else { + showNewJoinGroupSelection(button); + } + } + + var syncplayEnabled = false; + + events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) { + syncplayEnabled = enabled; + }); + + return { + show: showGroupSelection + }; +}); diff --git a/src/components/syncplay/syncplaymanager.js b/src/components/syncplay/syncplaymanager.js new file mode 100644 index 000000000..bde47a9d4 --- /dev/null +++ b/src/components/syncplay/syncplaymanager.js @@ -0,0 +1,600 @@ +define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager'], function (events, globalize, loading, connectionManager, playbackManager) { + 'use strict'; + + function waitForEvent(emitter, eventType) { + return new Promise(function (resolve) { + var callback = function () { + events.off(emitter, eventType, callback); + resolve(arguments); + } + events.on(emitter, eventType, callback); + }); + } + + function displaySyncplayUpdate(message) { + require(['toast'], function (alert) { + alert({ + title: message.Header, + text: message.Text + }); + }); + } + + function getActivePlayerId() { + var info = playbackManager.getPlayerInfo(); + return info ? info.id : null; + } + + function millisecondsToTicks(milliseconds) { + return milliseconds * 10000; + } + + function ticksToMilliseconds(ticks) { + return ticks / 10000; + } + + function SyncplayManager() { + + var self = this; + + function onPlayerChange() { + bindToPlayer(playbackManager.getCurrentPlayer()); + events.trigger(self, "PlayerChange", [self.currentPlayer]); + } + + function onPlayPauseStateChanged(e) { + events.trigger(self, "PlayPauseStateChange", [self.currentPlayer]); + } + + self.playbackRateSupported = false; + self.syncEnabled = false; + self.maxAcceptedDelaySpeedToSync = 50; // milliseconds + self.maxAcceptedDelaySkipToSync = 300; // milliseconds + self.syncMethodThreshold = 2000; // milliseconds + self.speedUpToSyncTime = 1000; // milliseconds + self.playbackDiffMillis = 0; // used for stats + self.syncMethod = "None"; // used for stats + + function onTimeUpdate(e) { + events.trigger(self, "TimeUpdate", [e]); + + if (self.lastCommand && self.lastCommand.Command === 'Play' && !self.isBuffering()) { + var currentTime = new Date(); + var playAtTime = self.lastCommand.When; + + var state = playbackManager.getPlayerState().PlayState; + // Estimate PositionTicks on server + var ServerPositionTicks = self.lastCommand.PositionTicks + ((currentTime - playAtTime) - self.timeDiff) * 10000; + // Measure delay that needs to be recovered + // diff might be caused by the player internally starting the playback + var diff = ServerPositionTicks - state.PositionTicks; + var diffMillis = diff / 10000; + + self.playbackDiffMillis = diffMillis; + + // console.debug("Syncplay onTimeUpdate", diffMillis, state.PositionTicks, ServerPositionTicks); + + if (self.syncEnabled) { + var absDiffMillis = Math.abs(diffMillis); + // TODO: SpeedToSync sounds bad on songs + if (self.playbackRateSupported && absDiffMillis > self.maxAcceptedDelaySpeedToSync && absDiffMillis < self.syncMethodThreshold) { + // SpeedToSync method + var speed = 1 + diffMillis / self.speedUpToSyncTime; + + self.currentPlayer.setPlaybackRate(speed); + self.syncEnabled = false; + self.showSyncIcon("SpeedToSync (x" + speed + ")"); + + self.syncTimeout = setTimeout(() => { + self.currentPlayer.setPlaybackRate(1); + self.syncEnabled = true; + self.clearSyncIcon(); + }, self.speedUpToSyncTime); + } else if (absDiffMillis > self.maxAcceptedDelaySkipToSync) { + // SkipToSync method + playbackManager.syncplay_seek(ServerPositionTicks); + self.syncEnabled = false; + self.showSyncIcon("SkipToSync"); + + self.syncTimeout = setTimeout(() => { + self.syncEnabled = true; + self.clearSyncIcon(); + }, self.syncMethodThreshold / 2); + } + } + } + } + + self.lastPlaybackWaiting = null; // used to determine if player's buffering + self.minBufferingThresholdMillis = 1000; + + // TODO: implement group wait + function onPlaying() { + self.lastPlaybackWaiting = null; + events.trigger(self, "PlayerPlaying"); + } + + // TODO: implement group wait + function onWaiting() { + if (!self.lastPlaybackWaiting) { + self.lastPlaybackWaiting = new Date(); + } + events.trigger(self, "PlayerWaiting"); + } + + self.isBuffering = function () { + if (self.lastPlaybackWaiting === null) return false; + return (new Date() - self.lastPlaybackWaiting) > self.minBufferingThresholdMillis; + } + + function bindToPlayer(player) { + if (player !== self.currentPlayer) { + releaseCurrentPlayer(); + self.currentPlayer = player; + if (!player) return; + } + events.on(player, "pause", onPlayPauseStateChanged); + events.on(player, "unpause", onPlayPauseStateChanged); + events.on(player, "timeupdate", onTimeUpdate); + events.on(player, "playing", onPlaying); + events.on(player, "waiting", onWaiting); + self.playbackRateSupported = player.supports("PlaybackRate"); + } + + function releaseCurrentPlayer() { + var player = self.currentPlayer; + if (player) { + events.off(player, "pause", onPlayPauseStateChanged); + events.off(player, "unpause", onPlayPauseStateChanged); + events.off(player, "timeupdate", onTimeUpdate); + events.off(player, "playing", onPlaying); + events.off(player, "waiting", onWaiting); + if (self.playbackRateSupported) { + player.setPlaybackRate(1); + } + self.currentPlayer = null; + self.playbackRateSupported = false; + } + } + + self.currentPlayer = null; + + events.on(playbackManager, "playerchange", onPlayerChange); + bindToPlayer(playbackManager.getCurrentPlayer()); + + self.syncplayEnabledAt = null; // Server time of when Syncplay has been enabled + self.syncplayReady = false; // Syncplay is ready after first ping to server + + self.processGroupUpdate = function (cmd, apiClient) { + switch (cmd.Type) { + case 'PrepareSession': + var serverId = apiClient.serverInfo().Id; + playbackManager.play({ + ids: cmd.Data.ItemIds, + startPositionTicks: cmd.Data.StartPositionTicks, + mediaSourceId: cmd.Data.MediaSourceId, + audioStreamIndex: cmd.Data.AudioStreamIndex, + subtitleStreamIndex: cmd.Data.SubtitleStreamIndex, + startIndex: cmd.Data.StartIndex, + serverId: serverId + }).then(function () { + waitForEvent(self, "PlayerChange").then(function () { + playbackManager.pause(); + var sessionId = getActivePlayerId(); + if (!sessionId) { + console.error("Missing sessionId!"); + displaySyncplayUpdate({ + Text: "Failed to enable Syncplay!" + }); + return; + } + // Sometimes JoinGroup fails, maybe because server hasn't been updated yet + setTimeout(() => { + apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { + GroupId: cmd.GroupId + }); + }, 500); + }); + }); + break; + case 'UserJoined': + displaySyncplayUpdate({ + Text: globalize.translate('MessageSyncplayUserJoined', cmd.Data), + }); + break; + case 'UserLeft': + displaySyncplayUpdate({ + Text: globalize.translate('MessageSyncplayUserLeft', cmd.Data), + }); + break; + case 'GroupJoined': + displaySyncplayUpdate({ + Text: globalize.translate('MessageSyncplayEnabled'), + }); + // Enable Syncplay + self.syncplayEnabledAt = new Date(cmd.Data); + self.syncplayReady = false; + events.trigger(self, "SyncplayEnabled", [true]); + waitForEvent(self, "SyncplayReady").then(function () { + self.processCommand(self.queuedCommand, apiClient); + self.queuedCommand = null; + }); + self.injectPlaybackManager(); + self.startPing(); + break; + case 'NotInGroup': + case 'GroupLeft': + displaySyncplayUpdate({ + Text: globalize.translate('MessageSyncplayDisabled'), + }); + // Disable Syncplay + self.syncplayEnabledAt = null; + self.syncplayReady = false; + events.trigger(self, "SyncplayEnabled", [false]); + self.restorePlaybackManager(); + self.stopPing(); + break; + case 'GroupWait': + displaySyncplayUpdate({ + Text: globalize.translate('MessageSyncplayGroupWait', cmd.Data), + }); + break; + case 'KeepAlive': + break; + default: + console.error('processSyncplayGroupUpdate does not recognize: ' + cmd.Type); + break; + } + } + + self.lastCommand = null; + self.queuedCommand = null; + + self.processCommand = function (cmd, apiClient) { + if (cmd === null) return; + + if (!self.isSyncplayEnabled()) { + console.debug("Syncplay processCommand: ignoring command", cmd); + return; + } + + if (!self.syncplayReady) { + console.debug("Syncplay processCommand: queued command", cmd); + self.queuedCommand = cmd; + return; + } + + cmd.When = new Date(cmd.When); + + if (cmd.When < self.syncplayEnabledAt) { + console.debug("Syncplay processCommand: ignoring old command", cmd); + return; + } + + // Check if new command differs from last one + if (self.lastCommand && + self.lastCommand.When === cmd.When && + self.lastCommand.PositionTicks === cmd.PositionTicks && + self.Command === cmd.Command + ) { + console.debug("Syncplay processCommand: ignoring duplicate command", cmd); + return; + } + + self.lastCommand = cmd; + console.log("Syncplay will", cmd.Command, "at", cmd.When, "PositionTicks", cmd.PositionTicks); + + switch (cmd.Command) { + case 'Play': + self.schedulePlay(cmd.When, cmd.PositionTicks); + break; + case 'Pause': + self.schedulePause(cmd.When, cmd.PositionTicks); + break; + case 'Seek': + self.scheduleSeek(cmd.When, cmd.PositionTicks); + break; + default: + console.error('processSyncplayCommand does not recognize: ' + cmd.Type); + break; + } + } + + self.scheduledCommand = null; + self.syncTimeout = null; + + self.schedulePlay = function (playAtTime, positionTicks) { + self.clearScheduledCommand(); + var currentTime = new Date(); + var playAtTimeLocal = self.serverDateToLocal(playAtTime); + + if (playAtTimeLocal > currentTime) { + var playTimeout = (playAtTimeLocal - currentTime) - self.playerDelay; + playbackManager.syncplay_seek(positionTicks); + + self.scheduledCommand = setTimeout(() => { + playbackManager.syncplay_unpause(); + + self.syncTimeout = setTimeout(() => { + self.syncEnabled = true + }, self.syncMethodThreshold / 2); + + }, playTimeout); + + // console.debug("Syncplay schedulePlay:", playTimeout); + } else { + // Group playback already started + var serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000; + playbackManager.syncplay_unpause(); + playbackManager.syncplay_seek(serverPositionTicks); + + self.syncTimeout = setTimeout(() => { + self.syncEnabled = true + }, self.syncMethodThreshold / 2); + } + } + + self.schedulePause = function (pauseAtTime, positionTicks) { + self.clearScheduledCommand(); + var currentTime = new Date(); + var pauseAtTimeLocal = self.serverDateToLocal(pauseAtTime); + + if (pauseAtTimeLocal > currentTime) { + var pauseTimeout = (pauseAtTimeLocal - currentTime) - self.playerDelay; + + self.scheduledCommand = setTimeout(() => { + playbackManager.syncplay_pause(); + setTimeout(() => { + playbackManager.syncplay_seek(positionTicks); + }, 800); + + }, pauseTimeout); + } else { + playbackManager.syncplay_pause(); + setTimeout(() => { + playbackManager.syncplay_seek(positionTicks); + }, 800); + } + } + + self.scheduleSeek = function (seekAtTime, positionTicks) { + self.schedulePause(seekAtTime, positionTicks); + } + + self.clearScheduledCommand = function () { + clearTimeout(self.scheduledCommand); + clearTimeout(self.syncTimeout); + + self.syncEnabled = false; + if (self.currentPlayer) { + self.currentPlayer.setPlaybackRate(1); + } + self.clearSyncIcon(); + } + + self.injectPlaybackManager = function () { + if (!self.isSyncplayEnabled()) return; + if (playbackManager.syncplayEnabled) return; + + playbackManager.syncplay_unpause = playbackManager.unpause; + playbackManager.syncplay_pause = playbackManager.pause; + playbackManager.syncplay_seek = playbackManager.seek; + + playbackManager.unpause = self.playRequest; + playbackManager.pause = self.pauseRequest; + playbackManager.seek = self.seekRequest; + playbackManager.syncplayEnabled = true; + } + + self.restorePlaybackManager = function () { + if (self.isSyncplayEnabled()) return; + if (!playbackManager.syncplayEnabled) return; + + playbackManager.unpause = playbackManager.syncplay_unpause; + playbackManager.pause = playbackManager.syncplay_pause; + playbackManager.seek = playbackManager.syncplay_seek; + playbackManager.syncplayEnabled = false; + } + + self.playRequest = function (player) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + apiClient.sendSyncplayCommand(sessionId, "PlayRequest"); + } + + self.pauseRequest = function (player) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + apiClient.sendSyncplayCommand(sessionId, "PauseRequest"); + // Pause locally as well, to give the user some little control + playbackManager.syncplay_pause(); + } + + self.seekRequest = function (PositionTicks, player) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + apiClient.sendSyncplayCommand(sessionId, "SeekRequest", { + PositionTicks: PositionTicks + }); + } + + self.pingIntervalTimeoutGreedy = 1000; + self.pingIntervalTimeoutLowProfile = 60000; + self.greedyPingCount = 3; + + self.pingStop = true; + self.pingIntervalTimeout = self.pingIntervalTimeoutGreedy; + self.pingInterval = null; + self.initTimeDiff = 0; // number of pings + self.timeDiff = 0; // local time minus server time + self.roundTripDuration = 0; + self.notifySyncplayReady = false; + + self.updateTimeDiff = function (pingStartTime, pingEndTime, serverTime) { + self.roundTripDuration = (pingEndTime - pingStartTime); + // The faster the response, the closer we are to the real timeDiff value + // localTime = pingStartTime + roundTripDuration / 2 + // newTimeDiff = localTime - serverTime + var newTimeDiff = (pingStartTime - serverTime) + (self.roundTripDuration / 2); + + // Initial setup + if (self.initTimeDiff === 0) { + self.timeDiff = newTimeDiff; + self.initTimeDiff++ + return; + } + + // As response time gets better, absolute value should decrease + var distanceFromZero = Math.abs(newTimeDiff); + var oldDistanceFromZero = Math.abs(self.timeDiff); + if (distanceFromZero < oldDistanceFromZero) { + self.timeDiff = newTimeDiff; + } + + // Avoid overloading server + if (self.initTimeDiff >= self.greedyPingCount) { + self.pingIntervalTimeout = self.pingIntervalTimeoutLowProfile; + } else { + self.initTimeDiff++; + } + + // console.debug("Syncplay updateTimeDiff:", serverTime, self.timeDiff, self.roundTripDuration, newTimeDiff); + } + + self.requestPing = function () { + if (self.pingInterval === null && !self.pingStop) { + self.pingInterval = setTimeout(() => { + self.pingInterval = null; + + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + + var pingStartTime = new Date(); + apiClient.sendSyncplayCommand(sessionId, "GetUtcTime").then(function (response) { + var pingEndTime = new Date(); + response.text().then(function (utcTime) { + var serverTime = new Date(utcTime); + self.updateTimeDiff(pingStartTime, pingEndTime, serverTime); + + // Alert user that ping is high + if (Math.abs(self.roundTripDuration) >= 1000) { + events.trigger(self, "SyncplayError", [true]); + } else { + events.trigger(self, "SyncplayError", [false]); + } + + // Notify server of ping + apiClient.sendSyncplayCommand(sessionId, "KeepAlive", { + Ping: (self.roundTripDuration / 2) + self.playerDelay + }); + + if (self.notifySyncplayReady) { + self.syncplayReady = true; + events.trigger(self, "SyncplayReady"); + self.notifySyncplayReady = false; + } + + self.requestPing(); + }); + }); + + }, self.pingIntervalTimeout); + } + } + + self.startPing = function () { + self.notifySyncplayReady = true; + self.pingStop = false; + self.initTimeDiff = self.initTimeDiff > self.greedyPingCount ? 1 : self.initTimeDiff; + self.pingIntervalTimeout = self.pingIntervalTimeoutGreedy; + + self.requestPing(); + } + + self.stopPing = function () { + self.pingStop = true; + if (self.pingInterval !== null) { + clearTimeout(self.pingInterval); + self.pingInterval = null; + } + } + + self.serverDateToLocal = function (server) { + // local - server = diff + return new Date(server.getTime() + self.timeDiff); + } + + self.localDateToServer = function (local) { + // local - server = diff + return new Date(local.getTime() - self.timeDiff); + } + + // THIS FEATURE IS CURRENTLY DISABLED + // Mainly because SpeedToSync seems to do the job + // Also because the delay is unreliable and different every time + self.playerDelay = 0; + self.playerDelayMeasured = true; // disable this feature + self.measurePlayerDelay = function (positionTicks) { + if (self.playerDelayMeasured) { + playbackManager.syncplay_seek(positionTicks); + } else { + // Measure playerDelay by issuing a play command + // followed by a pause command after one second + // PositionTicks should be at 1 second minus two times the player delay + loading.show(); + self.currentPlayer.setPlaybackRate(1); + playbackManager.syncplay_seek(0); + // Wait for player to seek + setTimeout(() => { + playbackManager.syncplay_unpause(); + // Play one second of media + setTimeout(() => { + playbackManager.syncplay_pause(); + // Wait for state to get update + setTimeout(() => { + var state = playbackManager.getPlayerState().PlayState; + var delayTicks = millisecondsToTicks(1000) - state.PositionTicks; + var delayMillis = ticksToMilliseconds(delayTicks); + self.playerDelay = delayMillis / 2; + // Make sure delay is not negative + self.playerDelay = self.playerDelay > 0 ? self.playerDelay : 0; + self.playerDelayMeasured = true; + // console.debug("Syncplay PlayerDelay:", self.playerDelay); + // Restore player + setTimeout(() => { + playbackManager.syncplay_seek(positionTicks); + loading.hide(); + }, 800); + }, 1000); + }, 1000); + }, 2000); + } + } + + // Stats + self.isSyncplayEnabled = function () { + return self.syncplayEnabledAt !== null ? true : false; + } + + self.getStats = function () { + return { + TimeDiff: self.timeDiff, + PlaybackDiff: self.playbackDiffMillis, + SyncMethod: self.syncMethod + } + } + + // UI + self.showSyncIcon = function (syncMethod) { + self.syncMethod = syncMethod; + events.trigger(self, "SyncplayError", [true]); + } + + self.clearSyncIcon = function () { + self.syncMethod = "None"; + events.trigger(self, "SyncplayError", [false]); + } + } + + return new SyncplayManager(); +}); diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index 81a381bff..8afe5f10f 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -1,4 +1,4 @@ -define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, browser, globalize, imageHelper) { +define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'syncplayManager', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, syncplayManager, browser, globalize, imageHelper) { 'use strict'; function renderHeader() { @@ -12,6 +12,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' html += '
    '; html += '
    '; html += ''; + html += ''; html += ''; html += ''; html += ''; @@ -30,6 +31,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' headerCastButton = skinHeader.querySelector('.headerCastButton'); headerAudioPlayerButton = skinHeader.querySelector('.headerAudioPlayerButton'); headerSearchButton = skinHeader.querySelector('.headerSearchButton'); + headerSyncButton = skinHeader.querySelector('.headerSyncButton'); lazyLoadViewMenuBarImages(); bindMenuEvents(); @@ -93,6 +95,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } } + headerSyncButton.classList.remove("hide"); + requiresUserRefresh = false; } @@ -147,6 +151,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } headerAudioPlayerButton.addEventListener('click', showAudioPlayer); + headerSyncButton.addEventListener('click', onSyncButtonClicked); if (layoutManager.mobile) { initHeadRoom(skinHeader); @@ -177,6 +182,32 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' }); } + function onSyncButtonClicked() { + var btn = this; + + require(["groupSelectionMenu"], function (groupSelectionMenu) { + groupSelectionMenu.show(btn); + }); + } + + function updateSyncplayIcon(event, enabled) { + var icon = headerSyncButton.querySelector("i"); + if (enabled) { + icon.innerHTML = "sync"; + } else { + icon.innerHTML = "sync_disabled"; + } + } + + function updateSyncplayErrorIcon(event, show_error) { + var icon = headerSyncButton.querySelector("i"); + if (show_error) { + icon.innerHTML = "sync_problem"; + } else { + icon.innerHTML = "sync"; + } + } + function getItemHref(item, context) { return appRouter.getRouteUrl(item, { context: context @@ -799,6 +830,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' var headerCastButton; var headerSearchButton; var headerAudioPlayerButton; + var headerSyncButton; var enableLibraryNavDrawer = layoutManager.desktop; var skinHeader = document.querySelector('.skinHeader'); var requiresUserRefresh = true; @@ -931,6 +963,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' updateUserInHeader(); }); events.on(playbackManager, 'playerchange', updateCastIcon); + events.on(syncplayManager, 'SyncplayEnabled', updateSyncplayIcon); + events.on(syncplayManager, 'SyncplayError', updateSyncplayErrorIcon); loadNavDrawer(); return LibraryMenu; }); diff --git a/src/scripts/site.js b/src/scripts/site.js index c00169d22..3a2ba1315 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -817,6 +817,7 @@ var AppInfo = {}; define('playbackSettings', [componentsPath + '/playbacksettings/playbacksettings'], returnFirstDependency); define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency); define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager); + define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnFirstDependency); define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager); define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency); define('playMenu', [componentsPath + '/playmenu'], returnFirstDependency); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 9895bbc05..c7611333f 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -491,6 +491,8 @@ "HeaderSubtitleProfile": "Subtitle Profile", "HeaderSubtitleProfiles": "Subtitle Profiles", "HeaderSubtitleProfilesHelp": "Subtitle profiles describe the subtitle formats supported by the device.", + "HeaderSyncplaySelectGroup": "Join a group", + "HeaderSyncplayEnabled": "Syncplay enabled", "HeaderSystemDlnaProfiles": "System Profiles", "HeaderTags": "Tags", "HeaderTaskTriggers": "Task Triggers", @@ -853,6 +855,13 @@ "LabelSubtitlePlaybackMode": "Subtitle mode:", "LabelSubtitles": "Subtitles", "LabelSupportedMediaTypes": "Supported Media Types:", + "LabelSyncplayTimeDiff": "Time difference with server:", + "LabelSyncplayPlaybackDiff": "Playback time difference:", + "LabelSyncplaySyncMethod": "Sync method:", + "LabelSyncplayNewGroup": "New group", + "LabelSyncplayNewGroupDescription": "Create a new group", + "LabelSyncplayLeaveGroup": "Leave group", + "LabelSyncplayLeaveGroupDescription": "Disable Syncplay", "LabelTVHomeScreen": "TV mode home screen:", "LabelTag": "Tag:", "LabelTagline": "Tagline:", @@ -1016,6 +1025,12 @@ "MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.", "MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.", "MessageYouHaveVersionInstalled": "You currently have version {0} installed.", + "MessageSyncplayEnabled": "Syncplay enabled.", + "MessageSyncplayDisabled": "Syncplay disabled.", + "MessageSyncplayUserJoined": "{0} joined group.", + "MessageSyncplayUserLeft": "{0} left group.", + "MessageSyncplayGroupWait": "{0} is buffering...", + "MessageSyncplayNoGroupsAvailable": "No groups available.", "Metadata": "Metadata", "MetadataManager": "Metadata Manager", "MetadataSettingChangeHelp": "Changing metadata settings will affect new content that is added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.", From 460c2a1f77dbc805ba364b7f7e932e9d9d79486b Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 1 Apr 2020 19:27:38 +0200 Subject: [PATCH 0032/2481] Fix lint errors --- src/components/playback/playbackmanager.js | 2 +- src/components/playerstats/playerstats.js | 2 +- src/components/syncplay/groupSelectionMenu.js | 4 +- src/components/syncplay/syncplaymanager.js | 76 +++++++++---------- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 923092cd3..e4ce40cf4 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -3789,7 +3789,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla player = player || this._currentPlayer; if (player) { - return player.getPlaybackRate(val); + return player.getPlaybackRate(); } return null; diff --git a/src/components/playerstats/playerstats.js b/src/components/playerstats/playerstats.js index ad4ce960b..4bd49ba5c 100644 --- a/src/components/playerstats/playerstats.js +++ b/src/components/playerstats/playerstats.js @@ -345,7 +345,7 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplay label: globalize.translate("LabelSyncplaySyncMethod"), value: stats.SyncMethod }); - + return syncStats; } diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js index 01141741c..8fb0523c3 100644 --- a/src/components/syncplay/groupSelectionMenu.js +++ b/src/components/syncplay/groupSelectionMenu.js @@ -24,7 +24,7 @@ define(['events', 'loading', 'connectionManager', 'playbackManager', 'syncplayMa var menuItems = groups.map(function (group) { var name = datetime.getDisplayRunningTime(group.PositionTicks); if (!inSession) { - name = group.PlayingItemName + name = group.PlayingItemName; } return { name: name, @@ -32,7 +32,7 @@ define(['events', 'loading', 'connectionManager', 'playbackManager', 'syncplayMa id: group.GroupId, selected: false, secondaryText: group.Partecipants.join(", ") - } + }; }); if (inSession) { diff --git a/src/components/syncplay/syncplaymanager.js b/src/components/syncplay/syncplaymanager.js index bde47a9d4..995c140fd 100644 --- a/src/components/syncplay/syncplaymanager.js +++ b/src/components/syncplay/syncplaymanager.js @@ -6,7 +6,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' var callback = function () { events.off(emitter, eventType, callback); resolve(arguments); - } + }; events.on(emitter, eventType, callback); }); } @@ -80,11 +80,11 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' if (self.playbackRateSupported && absDiffMillis > self.maxAcceptedDelaySpeedToSync && absDiffMillis < self.syncMethodThreshold) { // SpeedToSync method var speed = 1 + diffMillis / self.speedUpToSyncTime; - + self.currentPlayer.setPlaybackRate(speed); self.syncEnabled = false; self.showSyncIcon("SpeedToSync (x" + speed + ")"); - + self.syncTimeout = setTimeout(() => { self.currentPlayer.setPlaybackRate(1); self.syncEnabled = true; @@ -95,7 +95,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' playbackManager.syncplay_seek(ServerPositionTicks); self.syncEnabled = false; self.showSyncIcon("SkipToSync"); - + self.syncTimeout = setTimeout(() => { self.syncEnabled = true; self.clearSyncIcon(); @@ -105,7 +105,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' } } - self.lastPlaybackWaiting = null; // used to determine if player's buffering + self.lastPlaybackWaiting = null; // used to determine if player's buffering self.minBufferingThresholdMillis = 1000; // TODO: implement group wait @@ -119,13 +119,13 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' if (!self.lastPlaybackWaiting) { self.lastPlaybackWaiting = new Date(); } - events.trigger(self, "PlayerWaiting"); + events.trigger(self, "PlayerWaiting"); } self.isBuffering = function () { if (self.lastPlaybackWaiting === null) return false; return (new Date() - self.lastPlaybackWaiting) > self.minBufferingThresholdMillis; - } + }; function bindToPlayer(player) { if (player !== self.currentPlayer) { @@ -165,7 +165,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' self.syncplayEnabledAt = null; // Server time of when Syncplay has been enabled self.syncplayReady = false; // Syncplay is ready after first ping to server - self.processGroupUpdate = function (cmd, apiClient) { + self.processGroupUpdate = function (cmd, apiClient) { switch (cmd.Type) { case 'PrepareSession': var serverId = apiClient.serverInfo().Id; @@ -199,17 +199,17 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' break; case 'UserJoined': displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayUserJoined', cmd.Data), + Text: globalize.translate('MessageSyncplayUserJoined', cmd.Data) }); break; case 'UserLeft': displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayUserLeft', cmd.Data), + Text: globalize.translate('MessageSyncplayUserLeft', cmd.Data) }); break; case 'GroupJoined': displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayEnabled'), + Text: globalize.translate('MessageSyncplayEnabled') }); // Enable Syncplay self.syncplayEnabledAt = new Date(cmd.Data); @@ -225,7 +225,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' case 'NotInGroup': case 'GroupLeft': displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayDisabled'), + Text: globalize.translate('MessageSyncplayDisabled') }); // Disable Syncplay self.syncplayEnabledAt = null; @@ -236,7 +236,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' break; case 'GroupWait': displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayGroupWait', cmd.Data), + Text: globalize.translate('MessageSyncplayGroupWait', cmd.Data) }); break; case 'KeepAlive': @@ -245,7 +245,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' console.error('processSyncplayGroupUpdate does not recognize: ' + cmd.Type); break; } - } + }; self.lastCommand = null; self.queuedCommand = null; @@ -283,7 +283,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' self.lastCommand = cmd; console.log("Syncplay will", cmd.Command, "at", cmd.When, "PositionTicks", cmd.PositionTicks); - + switch (cmd.Command) { case 'Play': self.schedulePlay(cmd.When, cmd.PositionTicks); @@ -298,7 +298,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' console.error('processSyncplayCommand does not recognize: ' + cmd.Type); break; } - } + }; self.scheduledCommand = null; self.syncTimeout = null; @@ -332,7 +332,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' self.syncEnabled = true }, self.syncMethodThreshold / 2); } - } + }; self.schedulePause = function (pauseAtTime, positionTicks) { self.clearScheduledCommand(); @@ -355,11 +355,11 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' playbackManager.syncplay_seek(positionTicks); }, 800); } - } + }; self.scheduleSeek = function (seekAtTime, positionTicks) { self.schedulePause(seekAtTime, positionTicks); - } + }; self.clearScheduledCommand = function () { clearTimeout(self.scheduledCommand); @@ -370,7 +370,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' self.currentPlayer.setPlaybackRate(1); } self.clearSyncIcon(); - } + }; self.injectPlaybackManager = function () { if (!self.isSyncplayEnabled()) return; @@ -379,12 +379,12 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' playbackManager.syncplay_unpause = playbackManager.unpause; playbackManager.syncplay_pause = playbackManager.pause; playbackManager.syncplay_seek = playbackManager.seek; - + playbackManager.unpause = self.playRequest; playbackManager.pause = self.pauseRequest; playbackManager.seek = self.seekRequest; playbackManager.syncplayEnabled = true; - } + }; self.restorePlaybackManager = function () { if (self.isSyncplayEnabled()) return; @@ -394,13 +394,13 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' playbackManager.pause = playbackManager.syncplay_pause; playbackManager.seek = playbackManager.syncplay_seek; playbackManager.syncplayEnabled = false; - } + }; self.playRequest = function (player) { var apiClient = connectionManager.currentApiClient(); var sessionId = getActivePlayerId(); apiClient.sendSyncplayCommand(sessionId, "PlayRequest"); - } + }; self.pauseRequest = function (player) { var apiClient = connectionManager.currentApiClient(); @@ -408,7 +408,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' apiClient.sendSyncplayCommand(sessionId, "PauseRequest"); // Pause locally as well, to give the user some little control playbackManager.syncplay_pause(); - } + }; self.seekRequest = function (PositionTicks, player) { var apiClient = connectionManager.currentApiClient(); @@ -416,7 +416,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' apiClient.sendSyncplayCommand(sessionId, "SeekRequest", { PositionTicks: PositionTicks }); - } + }; self.pingIntervalTimeoutGreedy = 1000; self.pingIntervalTimeoutLowProfile = 60000; @@ -459,7 +459,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' } // console.debug("Syncplay updateTimeDiff:", serverTime, self.timeDiff, self.roundTripDuration, newTimeDiff); - } + }; self.requestPing = function () { if (self.pingInterval === null && !self.pingStop) { @@ -493,14 +493,14 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' events.trigger(self, "SyncplayReady"); self.notifySyncplayReady = false; } - + self.requestPing(); }); }); }, self.pingIntervalTimeout); } - } + }; self.startPing = function () { self.notifySyncplayReady = true; @@ -509,7 +509,7 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' self.pingIntervalTimeout = self.pingIntervalTimeoutGreedy; self.requestPing(); - } + }; self.stopPing = function () { self.pingStop = true; @@ -517,17 +517,17 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' clearTimeout(self.pingInterval); self.pingInterval = null; } - } + }; self.serverDateToLocal = function (server) { // local - server = diff return new Date(server.getTime() + self.timeDiff); - } + }; self.localDateToServer = function (local) { // local - server = diff return new Date(local.getTime() - self.timeDiff); - } + }; // THIS FEATURE IS CURRENTLY DISABLED // Mainly because SpeedToSync seems to do the job @@ -569,12 +569,12 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' }, 1000); }, 2000); } - } + }; // Stats self.isSyncplayEnabled = function () { return self.syncplayEnabledAt !== null ? true : false; - } + }; self.getStats = function () { return { @@ -582,18 +582,18 @@ define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager' PlaybackDiff: self.playbackDiffMillis, SyncMethod: self.syncMethod } - } + }; // UI self.showSyncIcon = function (syncMethod) { self.syncMethod = syncMethod; events.trigger(self, "SyncplayError", [true]); - } + }; self.clearSyncIcon = function () { self.syncMethod = "None"; events.trigger(self, "SyncplayError", [false]); - } + }; } return new SyncplayManager(); From 56ee678fc222bd50ea9e540fa8ffe0698dd67e7c Mon Sep 17 00:00:00 2001 From: gion Date: Fri, 3 Apr 2020 18:49:19 +0200 Subject: [PATCH 0033/2481] Port to ES6 --- package.json | 4 +- src/components/syncplay/groupSelectionMenu.js | 239 +++--- src/components/syncplay/syncplayManager.js | 704 ++++++++++++++++++ src/components/syncplay/syncplaymanager.js | 600 --------------- src/scripts/site.js | 9 +- 5 files changed, 840 insertions(+), 716 deletions(-) create mode 100644 src/components/syncplay/syncplayManager.js delete mode 100644 src/components/syncplay/syncplaymanager.js diff --git a/package.json b/package.json index ffbbc0295..39330cf60 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,9 @@ "src/scripts/keyboardnavigation.js", "src/scripts/settings/appSettings.js", "src/scripts/settings/userSettings.js", - "src/scripts/settings/webSettings.js" + "src/scripts/settings/webSettings.js", + "src/components/syncplay/groupSelectionMenu.js", + "src/components/syncplay/syncplayManager.js" ], "plugins": [ "@babel/plugin-transform-modules-amd" diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js index 8fb0523c3..671b76d5e 100644 --- a/src/components/syncplay/groupSelectionMenu.js +++ b/src/components/syncplay/groupSelectionMenu.js @@ -1,111 +1,78 @@ -define(['events', 'loading', 'connectionManager', 'playbackManager', 'syncplayManager', 'globalize', 'datetime'], function (events, loading, connectionManager, playbackManager, syncplayManager, globalize, datetime) { - 'use strict'; +import events from 'events'; +import connectionManager from 'connectionManager'; +import playbackManager from 'playbackManager'; +import syncplayManager from 'syncplayManager'; +import loading from 'loading'; +import datetime from 'datetime'; +import toast from 'toast'; +import actionsheet from 'actionsheet'; +import globalize from 'globalize'; - function getActivePlayerId() { - var info = playbackManager.getPlayerInfo(); - return info ? info.id : null; - } +/** + * Gets active player id. + * @returns {string} The player's id. + */ +function getActivePlayerId() { + var info = playbackManager.getPlayerInfo(); + return info ? info.id : null; +} - function emptyCallback() { - // avoid console logs about uncaught promises - } +/** + * Used to avoid console logs about uncaught promises + */ +function emptyCallback() { + // avoid console logs about uncaught promises +} - function showNewJoinGroupSelection(button) { - var apiClient = connectionManager.currentApiClient(); - var sessionId = getActivePlayerId(); - sessionId = sessionId ? sessionId : "none"; +/** + * Used when user needs to join a group. + * @param {HTMLElement} button - Element where to place the menu. + */ +function showNewJoinGroupSelection(button) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + sessionId = sessionId ? sessionId : "none"; - loading.show(); + loading.show(); - apiClient.sendSyncplayCommand(sessionId, "ListGroups").then(function (response) { - response.json().then(function (groups) { - var inSession = sessionId !== "none"; + apiClient.sendSyncplayCommand(sessionId, "ListGroups").then(function (response) { + response.json().then(function (groups) { + var inSession = sessionId !== "none"; - var menuItems = groups.map(function (group) { - var name = datetime.getDisplayRunningTime(group.PositionTicks); - if (!inSession) { - name = group.PlayingItemName; - } - return { - name: name, - icon: "group", - id: group.GroupId, - selected: false, - secondaryText: group.Partecipants.join(", ") - }; - }); - - if (inSession) { - menuItems.push({ - name: globalize.translate('LabelSyncplayNewGroup'), - icon: "add", - id: "new-group", - selected: true, - secondaryText: globalize.translate('LabelSyncplayNewGroupDescription') - }); + var menuItems = groups.map(function (group) { + var name = datetime.getDisplayRunningTime(group.PositionTicks); + if (!inSession) { + name = group.PlayingItemName; } - - if (menuItems.length === 0) { - require(['toast'], function (alert) { - alert({ - title: globalize.translate('MessageSyncplayNoGroupsAvailable'), - text: globalize.translate('MessageSyncplayNoGroupsAvailable') - }); - }); - loading.hide(); - return; - } - - require(['actionsheet'], function (actionsheet) { - - loading.hide(); - - var menuOptions = { - title: globalize.translate('HeaderSyncplaySelectGroup'), - items: menuItems, - positionTo: button, - resolveOnClick: true, - border: true - }; - - actionsheet.show(menuOptions).then(function (id) { - - if (id == "new-group") { - apiClient.sendSyncplayCommand(sessionId, "NewGroup"); - } else { - apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { - GroupId: id - }); - } - }, emptyCallback); - }); + return { + name: name, + icon: "group", + id: group.GroupId, + selected: false, + secondaryText: group.Partecipants.join(", ") + }; }); - }).catch(function (error) { - loading.hide(); - console.error(error); - }); - } - function showLeaveGroupSelection(button) { - var apiClient = connectionManager.currentApiClient(); - var sessionId = getActivePlayerId(); + if (inSession) { + menuItems.push({ + name: globalize.translate('LabelSyncplayNewGroup'), + icon: "add", + id: "new-group", + selected: true, + secondaryText: globalize.translate('LabelSyncplayNewGroupDescription') + }); + } - loading.show(); - - var menuItems = [{ - name: globalize.translate('LabelSyncplayLeaveGroup'), - icon: "meeting_room", - id: "leave-group", - selected: true, - secondaryText: globalize.translate('LabelSyncplayLeaveGroupDescription') - }]; - - require(['actionsheet'], function (actionsheet) { - - loading.hide(); + if (menuItems.length === 0) { + toast({ + text: globalize.translate('MessageSyncplayNoGroupsAvailable') + }); + loading.hide(); + return; + } var menuOptions = { - title: globalize.translate('HeaderSyncplayEnabled'), + title: globalize.translate('HeaderSyncplaySelectGroup'), items: menuItems, positionTo: button, resolveOnClick: true, @@ -113,28 +80,72 @@ define(['events', 'loading', 'connectionManager', 'playbackManager', 'syncplayMa }; actionsheet.show(menuOptions).then(function (id) { - if (id == "leave-group") { - apiClient.sendSyncplayCommand(sessionId, "LeaveGroup"); + if (id == "new-group") { + apiClient.sendSyncplayCommand(sessionId, "NewGroup"); + } else { + apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { + GroupId: id + }); } }, emptyCallback); + + loading.hide(); }); - } - - function showGroupSelection(button) { - if (syncplayEnabled) { - showLeaveGroupSelection(button); - } else { - showNewJoinGroupSelection(button); - } - } - - var syncplayEnabled = false; - - events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) { - syncplayEnabled = enabled; + }).catch(function (error) { + loading.hide(); + console.error(error); }); +} - return { - show: showGroupSelection +/** + * Used when user has joined a group. + * @param {HTMLElement} button - Element where to place the menu. + */ +function showLeaveGroupSelection(button) { + const apiClient = connectionManager.currentApiClient(); + const sessionId = getActivePlayerId(); + + loading.show(); + + const menuItems = [{ + name: globalize.translate('LabelSyncplayLeaveGroup'), + icon: "meeting_room", + id: "leave-group", + selected: true, + secondaryText: globalize.translate('LabelSyncplayLeaveGroupDescription') + }]; + + var menuOptions = { + title: globalize.translate('HeaderSyncplayEnabled'), + items: menuItems, + positionTo: button, + resolveOnClick: true, + border: true }; + + actionsheet.show(menuOptions).then(function (id) { + if (id == "leave-group") { + apiClient.sendSyncplayCommand(sessionId, "LeaveGroup"); + } + }, emptyCallback); + + loading.hide(); +} + +// Register to Syncplay events +let syncplayEnabled = false; +events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) { + syncplayEnabled = enabled; }); + +/** + * Shows a menu to handle Syncplay groups. + * @param {HTMLElement} button - Element where to place the menu. + */ +export function show(button) { + if (syncplayEnabled) { + showLeaveGroupSelection(button); + } else { + showNewJoinGroupSelection(button); + } +} diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js new file mode 100644 index 000000000..bc0fe7bc6 --- /dev/null +++ b/src/components/syncplay/syncplayManager.js @@ -0,0 +1,704 @@ +/* eslint-disable indent */ + +/** + * Module that manages the Syncplay feature. + * @module components/syncplay/syncplayManager + */ + +import events from 'events'; +import connectionManager from 'connectionManager'; +import playbackManager from 'playbackManager'; +import toast from 'toast'; +import globalize from 'globalize'; + +/** + * Waits for an event to be triggered on an object. + * @param {Object} emitter Object on which to listen for events. + * @param {string} eventType Event name to listen for. + * @returns {Promise} A promise that resolves when the event is triggered. + */ +function waitForEvent(emitter, eventType) { + return new Promise((resolve) => { + var callback = () => { + events.off(emitter, eventType, callback); + resolve(arguments); + }; + events.on(emitter, eventType, callback); + }); +} + +/** + * Gets active player id. + * @returns {string} The player's id. + */ +function getActivePlayerId() { + var info = playbackManager.getPlayerInfo(); + return info ? info.id : null; +} + +/** + * Playback synchronization + */ +const MaxAcceptedDelaySpeedToSync = 50; // milliseconds, delay after which SpeedToSync is enabled +const MaxAcceptedDelaySkipToSync = 300; // milliseconds, delay after which SkipToSync is enabled +const SyncMethodThreshold = 2000; // milliseconds, switches between SpeedToSync or SkipToSync +const SpeedUpToSyncTime = 1000; // milliseconds, duration in which the playback is sped up + +/** + * Time estimation + */ +const PingIntervalTimeoutGreedy = 1000; // milliseconds +const PingIntervalTimeoutLowProfile = 60000; // milliseconds +const GreedyPingCount = 3; + +/** + * Class that manages the Syncplay feature. + */ +class SyncplayManager { + constructor() { + this.playbackRateSupported = false; + this.syncEnabled = false; + this.playbackDiffMillis = 0; // used for stats + this.syncMethod = "None"; // used for stats + + this.lastPlaybackWaiting = null; // used to determine if player's buffering + this.minBufferingThresholdMillis = 1000; + + this.currentPlayer = null; + + this.syncplayEnabledAt = null; // Server time of when Syncplay has been enabled + this.syncplayReady = false; // Syncplay is ready after first ping to server + + this.lastCommand = null; + this.queuedCommand = null; + + this.scheduledCommand = null; + this.syncTimeout = null; + + this.pingStop = true; + this.pingIntervalTimeout = PingIntervalTimeoutGreedy; + this.pingInterval = null; + this.initTimeDiff = 0; // number of pings + this.timeDiff = 0; // local time minus server time + this.roundTripDuration = 0; + this.notifySyncplayReady = false; + + events.on(playbackManager, "playerchange", () => { + this.onPlayerChange(); + }); + this.bindToPlayer(playbackManager.getCurrentPlayer()); + } + + /** + * Called when the player changes. + */ + onPlayerChange () { + this.bindToPlayer(playbackManager.getCurrentPlayer()); + events.trigger(this, "PlayerChange", [this.currentPlayer]); + } + + /** + * Called on playback state changes. + * @param {Object} e The playback state change event. + */ + onPlayPauseStateChanged (e) { + events.trigger(this, "PlayPauseStateChange", [this.currentPlayer]); + } + + /** + * Called on playback progress. + * @param {Object} e The time update event. + */ + onTimeUpdate (e) { + events.trigger(this, "TimeUpdate", [e]); + + if (this.lastCommand && this.lastCommand.Command === 'Play' && !this.isBuffering()) { + var currentTime = new Date(); + var playAtTime = this.lastCommand.When; + + var state = playbackManager.getPlayerState().PlayState; + // Estimate PositionTicks on server + var ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) - this.timeDiff) * 10000; + // Measure delay that needs to be recovered + // diff might be caused by the player internally starting the playback + var diff = ServerPositionTicks - state.PositionTicks; + var diffMillis = diff / 10000; + + this.playbackDiffMillis = diffMillis; + + // console.debug("Syncplay onTimeUpdate", diffMillis, state.PositionTicks, ServerPositionTicks); + + if (this.syncEnabled) { + var absDiffMillis = Math.abs(diffMillis); + // TODO: SpeedToSync sounds bad on songs + if (this.playbackRateSupported && absDiffMillis > MaxAcceptedDelaySpeedToSync && absDiffMillis < SyncMethodThreshold) { + // SpeedToSync method + var speed = 1 + diffMillis / SpeedUpToSyncTime; + + this.currentPlayer.setPlaybackRate(speed); + this.syncEnabled = false; + this.showSyncIcon("SpeedToSync (x" + speed + ")"); + + this.syncTimeout = setTimeout(() => { + this.currentPlayer.setPlaybackRate(1); + this.syncEnabled = true; + this.clearSyncIcon(); + }, SpeedUpToSyncTime); + } else if (absDiffMillis > MaxAcceptedDelaySkipToSync) { + // SkipToSync method + playbackManager.syncplay_seek(ServerPositionTicks); + this.syncEnabled = false; + this.showSyncIcon("SkipToSync"); + + this.syncTimeout = setTimeout(() => { + this.syncEnabled = true; + this.clearSyncIcon(); + }, this.syncMethodThreshold / 2); + } + } + } + } + + /** + * Called when playback is resumed. + */ + onPlaying () { + // TODO: implement group wait + this.lastPlaybackWaiting = null; + events.trigger(this, "PlayerPlaying"); + } + + /** + * Called when playback is buffering. + */ + onWaiting () { + // TODO: implement group wait + if (!this.lastPlaybackWaiting) { + this.lastPlaybackWaiting = new Date(); + } + events.trigger(this, "PlayerWaiting"); + } + + /** + * Gets playback buffering status. + * @returns {boolean} _true_ if player is buffering, _false_ otherwise. + */ + isBuffering () { + if (this.lastPlaybackWaiting === null) return false; + return (new Date() - this.lastPlaybackWaiting) > this.minBufferingThresholdMillis; + } + + /** + * Binds to the player's events. + * @param {Object} player The player. + */ + bindToPlayer (player) { + if (player !== this.currentPlayer) { + this.releaseCurrentPlayer(); + this.currentPlayer = player; + if (!player) return; + } + + // TODO: remove this extra functions + const self = this; + this._onPlayPauseStateChanged = () => { + self.onPlayPauseStateChanged(); + }; + + this._onPlayPauseStateChanged = (e) => { + self.onPlayPauseStateChanged(e); + }; + + this._onTimeUpdate = (e) => { + self.onTimeUpdate(e); + }; + + this._onPlaying = () => { + self.onPlaying(); + }; + + this._onWaiting = () => { + self.onWaiting(); + }; + + events.on(player, "pause", this._onPlayPauseStateChanged); + events.on(player, "unpause", this._onPlayPauseStateChanged); + events.on(player, "timeupdate", this._onTimeUpdate); + events.on(player, "playing", this._onPlaying); + events.on(player, "waiting", this._onWaiting); + this.playbackRateSupported = player.supports("PlaybackRate"); + } + + /** + * Removes the bindings to the current player's events. + */ + releaseCurrentPlayer () { + var player = this.currentPlayer; + if (player) { + events.off(player, "pause", this._onPlayPauseStateChanged); + events.off(player, "unpause", this._onPlayPauseStateChanged); + events.off(player, "timeupdate", this._onTimeUpdate); + events.off(player, "playing", this._onPlaying); + events.off(player, "waiting", this._onWaiting); + if (this.playbackRateSupported) { + player.setPlaybackRate(1); + } + this.currentPlayer = null; + this.playbackRateSupported = false; + } + } + + /** + * Handles a group update from the server. + * @param {Object} cmd The group update. + * @param {Object} apiClient The ApiClient. + */ + processGroupUpdate (cmd, apiClient) { + switch (cmd.Type) { + case 'PrepareSession': + var serverId = apiClient.serverInfo().Id; + playbackManager.play({ + ids: cmd.Data.ItemIds, + startPositionTicks: cmd.Data.StartPositionTicks, + mediaSourceId: cmd.Data.MediaSourceId, + audioStreamIndex: cmd.Data.AudioStreamIndex, + subtitleStreamIndex: cmd.Data.SubtitleStreamIndex, + startIndex: cmd.Data.StartIndex, + serverId: serverId + }).then(() => { + waitForEvent(this, "PlayerChange").then(() => { + playbackManager.pause(); + var sessionId = getActivePlayerId(); + if (!sessionId) { + console.error("Missing sessionId!"); + toast({ + text: "Failed to enable Syncplay!" + }); + return; + } + // Sometimes JoinGroup fails, maybe because server hasn't been updated yet + setTimeout(() => { + apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { + GroupId: cmd.GroupId + }); + }, 500); + }); + }); + break; + case 'UserJoined': + toast({ + text: globalize.translate('MessageSyncplayUserJoined', cmd.Data) + }); + break; + case 'UserLeft': + toast({ + text: globalize.translate('MessageSyncplayUserLeft', cmd.Data) + }); + break; + case 'GroupJoined': + toast({ + text: globalize.translate('MessageSyncplayEnabled') + }); + // Enable Syncplay + this.syncplayEnabledAt = new Date(cmd.Data); + this.syncplayReady = false; + events.trigger(this, "SyncplayEnabled", [true]); + waitForEvent(this, "SyncplayReady").then(() => { + this.processCommand(this.queuedCommand, apiClient); + this.queuedCommand = null; + }); + this.injectPlaybackManager(); + this.startPing(); + break; + case 'NotInGroup': + case 'GroupLeft': + toast({ + text: globalize.translate('MessageSyncplayDisabled') + }); + // Disable Syncplay + this.syncplayEnabledAt = null; + this.syncplayReady = false; + events.trigger(this, "SyncplayEnabled", [false]); + this.restorePlaybackManager(); + this.stopPing(); + break; + case 'GroupWait': + toast({ + text: globalize.translate('MessageSyncplayGroupWait', cmd.Data) + }); + break; + case 'KeepAlive': + break; + default: + console.error('processSyncplayGroupUpdate does not recognize: ' + cmd.Type); + break; + } + } + + /** + * Handles a playback command from the server. + * @param {Object} cmd The playback command. + * @param {Object} apiClient The ApiClient. + */ + processCommand (cmd, apiClient) { + if (cmd === null) return; + + if (!this.isSyncplayEnabled()) { + console.debug("Syncplay processCommand: ignoring command", cmd); + return; + } + + if (!this.syncplayReady) { + console.debug("Syncplay processCommand: queued command", cmd); + this.queuedCommand = cmd; + return; + } + + cmd.When = new Date(cmd.When); + + if (cmd.When < this.syncplayEnabledAt) { + console.debug("Syncplay processCommand: ignoring old command", cmd); + return; + } + + // Check if new command differs from last one + if (this.lastCommand && + this.lastCommand.When === cmd.When && + this.lastCommand.PositionTicks === cmd.PositionTicks && + this.Command === cmd.Command + ) { + console.debug("Syncplay processCommand: ignoring duplicate command", cmd); + return; + } + + this.lastCommand = cmd; + console.log("Syncplay will", cmd.Command, "at", cmd.When, "PositionTicks", cmd.PositionTicks); + + switch (cmd.Command) { + case 'Play': + this.schedulePlay(cmd.When, cmd.PositionTicks); + break; + case 'Pause': + this.schedulePause(cmd.When, cmd.PositionTicks); + break; + case 'Seek': + this.scheduleSeek(cmd.When, cmd.PositionTicks); + break; + default: + console.error('processSyncplayCommand does not recognize: ' + cmd.Type); + break; + } + } + + /** + * Schedules a resume playback on the player at the specified clock time. + * @param {Date} playAtTime The server's UTC time at which to resume playback. + * @param {number} positionTicks The PositionTicks from where to resume. + */ + schedulePlay (playAtTime, positionTicks) { + this.clearScheduledCommand(); + var currentTime = new Date(); + var playAtTimeLocal = this.serverDateToLocal(playAtTime); + + if (playAtTimeLocal > currentTime) { + var playTimeout = playAtTimeLocal - currentTime; + playbackManager.syncplay_seek(positionTicks); + + this.scheduledCommand = setTimeout(() => { + playbackManager.syncplay_unpause(); + + this.syncTimeout = setTimeout(() => { + this.syncEnabled = true + }, this.syncMethodThreshold / 2); + + }, playTimeout); + + // console.debug("Syncplay schedulePlay:", playTimeout); + } else { + // Group playback already started + var serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000; + playbackManager.syncplay_unpause(); + playbackManager.syncplay_seek(serverPositionTicks); + + this.syncTimeout = setTimeout(() => { + this.syncEnabled = true + }, this.syncMethodThreshold / 2); + } + } + + /** + * Schedules a pause playback on the player at the specified clock time. + * @param {Date} pauseAtTime The server's UTC time at which to pause playback. + * @param {number} positionTicks The PositionTicks where player will be paused. + */ + schedulePause (pauseAtTime, positionTicks) { + this.clearScheduledCommand(); + var currentTime = new Date(); + var pauseAtTimeLocal = this.serverDateToLocal(pauseAtTime); + + if (pauseAtTimeLocal > currentTime) { + var pauseTimeout = pauseAtTimeLocal - currentTime; + + this.scheduledCommand = setTimeout(() => { + playbackManager.syncplay_pause(); + setTimeout(() => { + playbackManager.syncplay_seek(positionTicks); + }, 800); + + }, pauseTimeout); + } else { + playbackManager.syncplay_pause(); + setTimeout(() => { + playbackManager.syncplay_seek(positionTicks); + }, 800); + } + } + + /** + * Schedules a seek playback on the player at the specified clock time. + * @param {Date} pauseAtTime The server's UTC time at which to seek playback. + * @param {number} positionTicks The PositionTicks where player will be seeked. + */ + scheduleSeek (seekAtTime, positionTicks) { + this.schedulePause(seekAtTime, positionTicks); + } + + /** + * Clears the current scheduled command. + */ + clearScheduledCommand () { + clearTimeout(this.scheduledCommand); + clearTimeout(this.syncTimeout); + + this.syncEnabled = false; + if (this.currentPlayer) { + this.currentPlayer.setPlaybackRate(1); + } + this.clearSyncIcon(); + } + + /** + * Overrides some PlaybackManager's methods to intercept playback commands. + */ + injectPlaybackManager () { + if (!this.isSyncplayEnabled()) return; + if (playbackManager.syncplayEnabled) return; + + // TODO: make this less hacky + playbackManager.syncplay_unpause = playbackManager.unpause; + playbackManager.syncplay_pause = playbackManager.pause; + playbackManager.syncplay_seek = playbackManager.seek; + + playbackManager.unpause = this.playRequest; + playbackManager.pause = this.pauseRequest; + playbackManager.seek = this.seekRequest; + playbackManager.syncplayEnabled = true; + } + + /** + * Restores original PlaybackManager's methods. + */ + restorePlaybackManager () { + if (this.isSyncplayEnabled()) return; + if (!playbackManager.syncplayEnabled) return; + + playbackManager.unpause = playbackManager.syncplay_unpause; + playbackManager.pause = playbackManager.syncplay_pause; + playbackManager.seek = playbackManager.syncplay_seek; + playbackManager.syncplayEnabled = false; + } + + /** + * Overrides PlaybackManager's unpause method. + */ + playRequest (player) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + apiClient.sendSyncplayCommand(sessionId, "PlayRequest"); + } + + /** + * Overrides PlaybackManager's pause method. + */ + pauseRequest (player) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + apiClient.sendSyncplayCommand(sessionId, "PauseRequest"); + // Pause locally as well, to give the user some little control + playbackManager.syncplay_pause(); + } + + /** + * Overrides PlaybackManager's seek method. + */ + seekRequest (PositionTicks, player) { + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + apiClient.sendSyncplayCommand(sessionId, "SeekRequest", { + PositionTicks: PositionTicks + }); + } + + /** + * Computes time difference between this client's time and server's time. + * @param {Date} pingStartTime Local time when ping request started. + * @param {Date} pingEndTime Local time when ping request ended. + * @param {Date} serverTime Server UTC time at ping request. + */ + updateTimeDiff (pingStartTime, pingEndTime, serverTime) { + this.roundTripDuration = (pingEndTime - pingStartTime); + // The faster the response, the closer we are to the real timeDiff value + // localTime = pingStartTime + roundTripDuration / 2 + // newTimeDiff = localTime - serverTime + var newTimeDiff = (pingStartTime - serverTime) + (this.roundTripDuration / 2); + + // Initial setup + if (this.initTimeDiff === 0) { + this.timeDiff = newTimeDiff; + this.initTimeDiff++ + return; + } + + // As response time gets better, absolute value should decrease + var distanceFromZero = Math.abs(newTimeDiff); + var oldDistanceFromZero = Math.abs(this.timeDiff); + if (distanceFromZero < oldDistanceFromZero) { + this.timeDiff = newTimeDiff; + } + + // Avoid overloading server + if (this.initTimeDiff >= GreedyPingCount) { + this.pingIntervalTimeout = PingIntervalTimeoutLowProfile; + } else { + this.initTimeDiff++; + } + + // console.debug("Syncplay updateTimeDiff:", serverTime, this.timeDiff, this.roundTripDuration, newTimeDiff); + } + + /** + * Schedules a ping request to the server. Used to compute time difference between client and server. + */ + requestPing () { + if (this.pingInterval === null && !this.pingStop) { + this.pingInterval = setTimeout(() => { + this.pingInterval = null; + + var apiClient = connectionManager.currentApiClient(); + var sessionId = getActivePlayerId(); + + var pingStartTime = new Date(); + apiClient.sendSyncplayCommand(sessionId, "GetUtcTime").then((response) => { + var pingEndTime = new Date(); + response.text().then((utcTime) => { + var serverTime = new Date(utcTime); + this.updateTimeDiff(pingStartTime, pingEndTime, serverTime); + + // Alert user that ping is high + if (Math.abs(this.roundTripDuration) >= 1000) { + events.trigger(this, "SyncplayError", [true]); + } else { + events.trigger(this, "SyncplayError", [false]); + } + + // Notify server of ping + apiClient.sendSyncplayCommand(sessionId, "KeepAlive", { + Ping: this.roundTripDuration / 2 + }); + + if (this.notifySyncplayReady) { + this.syncplayReady = true; + events.trigger(this, "SyncplayReady"); + this.notifySyncplayReady = false; + } + + this.requestPing(); + }); + }); + + }, this.pingIntervalTimeout); + } + } + + /** + * Starts the keep alive poller. + */ + startPing () { + this.notifySyncplayReady = true; + this.pingStop = false; + this.initTimeDiff = this.initTimeDiff > this.greedyPingCount ? 1 : this.initTimeDiff; + this.pingIntervalTimeout = this.pingIntervalTimeoutGreedy; + + this.requestPing(); + } + + /** + * Stops the keep alive poller. + */ + stopPing () { + this.pingStop = true; + if (this.pingInterval !== null) { + clearTimeout(this.pingInterval); + this.pingInterval = null; + } + } + + /** + * Converts server time to local time. + * @param {Date} server + * @returns {Date} Local time. + */ + serverDateToLocal (server) { + // local - server = diff + return new Date(server.getTime() + this.timeDiff); + } + + /** + * Converts local time to server time. + * @param {Date} local + * @returns {Date} Server time. + */ + localDateToServer (local) { + // local - server = diff + return new Date(local.getTime() - this.timeDiff); + } + + /** + * Gets Syncplay status. + * @returns {boolean} _true_ is user joined a group, _false_ otherwise. + */ + isSyncplayEnabled () { + return this.syncplayEnabledAt !== null ? true : false; + } + + /** + * Gets Syncplay stats. + * @returns {Object} The Syncplay stats. + */ + getStats () { + return { + TimeDiff: this.timeDiff, + PlaybackDiff: this.playbackDiffMillis, + SyncMethod: this.syncMethod + } + } + + /** + * Emits an event to update the Syncplay status icon. + */ + showSyncIcon (syncMethod) { + this.syncMethod = syncMethod; + events.trigger(this, "SyncplayError", [true]); + } + + /** + * Emits an event to clear the Syncplay status icon. + */ + clearSyncIcon () { + this.syncMethod = "None"; + events.trigger(this, "SyncplayError", [false]); + } +} + +/** SyncplayManager singleton. */ +export default new SyncplayManager(); diff --git a/src/components/syncplay/syncplaymanager.js b/src/components/syncplay/syncplaymanager.js deleted file mode 100644 index 995c140fd..000000000 --- a/src/components/syncplay/syncplaymanager.js +++ /dev/null @@ -1,600 +0,0 @@ -define(['events', 'globalize', 'loading', 'connectionManager', 'playbackManager'], function (events, globalize, loading, connectionManager, playbackManager) { - 'use strict'; - - function waitForEvent(emitter, eventType) { - return new Promise(function (resolve) { - var callback = function () { - events.off(emitter, eventType, callback); - resolve(arguments); - }; - events.on(emitter, eventType, callback); - }); - } - - function displaySyncplayUpdate(message) { - require(['toast'], function (alert) { - alert({ - title: message.Header, - text: message.Text - }); - }); - } - - function getActivePlayerId() { - var info = playbackManager.getPlayerInfo(); - return info ? info.id : null; - } - - function millisecondsToTicks(milliseconds) { - return milliseconds * 10000; - } - - function ticksToMilliseconds(ticks) { - return ticks / 10000; - } - - function SyncplayManager() { - - var self = this; - - function onPlayerChange() { - bindToPlayer(playbackManager.getCurrentPlayer()); - events.trigger(self, "PlayerChange", [self.currentPlayer]); - } - - function onPlayPauseStateChanged(e) { - events.trigger(self, "PlayPauseStateChange", [self.currentPlayer]); - } - - self.playbackRateSupported = false; - self.syncEnabled = false; - self.maxAcceptedDelaySpeedToSync = 50; // milliseconds - self.maxAcceptedDelaySkipToSync = 300; // milliseconds - self.syncMethodThreshold = 2000; // milliseconds - self.speedUpToSyncTime = 1000; // milliseconds - self.playbackDiffMillis = 0; // used for stats - self.syncMethod = "None"; // used for stats - - function onTimeUpdate(e) { - events.trigger(self, "TimeUpdate", [e]); - - if (self.lastCommand && self.lastCommand.Command === 'Play' && !self.isBuffering()) { - var currentTime = new Date(); - var playAtTime = self.lastCommand.When; - - var state = playbackManager.getPlayerState().PlayState; - // Estimate PositionTicks on server - var ServerPositionTicks = self.lastCommand.PositionTicks + ((currentTime - playAtTime) - self.timeDiff) * 10000; - // Measure delay that needs to be recovered - // diff might be caused by the player internally starting the playback - var diff = ServerPositionTicks - state.PositionTicks; - var diffMillis = diff / 10000; - - self.playbackDiffMillis = diffMillis; - - // console.debug("Syncplay onTimeUpdate", diffMillis, state.PositionTicks, ServerPositionTicks); - - if (self.syncEnabled) { - var absDiffMillis = Math.abs(diffMillis); - // TODO: SpeedToSync sounds bad on songs - if (self.playbackRateSupported && absDiffMillis > self.maxAcceptedDelaySpeedToSync && absDiffMillis < self.syncMethodThreshold) { - // SpeedToSync method - var speed = 1 + diffMillis / self.speedUpToSyncTime; - - self.currentPlayer.setPlaybackRate(speed); - self.syncEnabled = false; - self.showSyncIcon("SpeedToSync (x" + speed + ")"); - - self.syncTimeout = setTimeout(() => { - self.currentPlayer.setPlaybackRate(1); - self.syncEnabled = true; - self.clearSyncIcon(); - }, self.speedUpToSyncTime); - } else if (absDiffMillis > self.maxAcceptedDelaySkipToSync) { - // SkipToSync method - playbackManager.syncplay_seek(ServerPositionTicks); - self.syncEnabled = false; - self.showSyncIcon("SkipToSync"); - - self.syncTimeout = setTimeout(() => { - self.syncEnabled = true; - self.clearSyncIcon(); - }, self.syncMethodThreshold / 2); - } - } - } - } - - self.lastPlaybackWaiting = null; // used to determine if player's buffering - self.minBufferingThresholdMillis = 1000; - - // TODO: implement group wait - function onPlaying() { - self.lastPlaybackWaiting = null; - events.trigger(self, "PlayerPlaying"); - } - - // TODO: implement group wait - function onWaiting() { - if (!self.lastPlaybackWaiting) { - self.lastPlaybackWaiting = new Date(); - } - events.trigger(self, "PlayerWaiting"); - } - - self.isBuffering = function () { - if (self.lastPlaybackWaiting === null) return false; - return (new Date() - self.lastPlaybackWaiting) > self.minBufferingThresholdMillis; - }; - - function bindToPlayer(player) { - if (player !== self.currentPlayer) { - releaseCurrentPlayer(); - self.currentPlayer = player; - if (!player) return; - } - events.on(player, "pause", onPlayPauseStateChanged); - events.on(player, "unpause", onPlayPauseStateChanged); - events.on(player, "timeupdate", onTimeUpdate); - events.on(player, "playing", onPlaying); - events.on(player, "waiting", onWaiting); - self.playbackRateSupported = player.supports("PlaybackRate"); - } - - function releaseCurrentPlayer() { - var player = self.currentPlayer; - if (player) { - events.off(player, "pause", onPlayPauseStateChanged); - events.off(player, "unpause", onPlayPauseStateChanged); - events.off(player, "timeupdate", onTimeUpdate); - events.off(player, "playing", onPlaying); - events.off(player, "waiting", onWaiting); - if (self.playbackRateSupported) { - player.setPlaybackRate(1); - } - self.currentPlayer = null; - self.playbackRateSupported = false; - } - } - - self.currentPlayer = null; - - events.on(playbackManager, "playerchange", onPlayerChange); - bindToPlayer(playbackManager.getCurrentPlayer()); - - self.syncplayEnabledAt = null; // Server time of when Syncplay has been enabled - self.syncplayReady = false; // Syncplay is ready after first ping to server - - self.processGroupUpdate = function (cmd, apiClient) { - switch (cmd.Type) { - case 'PrepareSession': - var serverId = apiClient.serverInfo().Id; - playbackManager.play({ - ids: cmd.Data.ItemIds, - startPositionTicks: cmd.Data.StartPositionTicks, - mediaSourceId: cmd.Data.MediaSourceId, - audioStreamIndex: cmd.Data.AudioStreamIndex, - subtitleStreamIndex: cmd.Data.SubtitleStreamIndex, - startIndex: cmd.Data.StartIndex, - serverId: serverId - }).then(function () { - waitForEvent(self, "PlayerChange").then(function () { - playbackManager.pause(); - var sessionId = getActivePlayerId(); - if (!sessionId) { - console.error("Missing sessionId!"); - displaySyncplayUpdate({ - Text: "Failed to enable Syncplay!" - }); - return; - } - // Sometimes JoinGroup fails, maybe because server hasn't been updated yet - setTimeout(() => { - apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { - GroupId: cmd.GroupId - }); - }, 500); - }); - }); - break; - case 'UserJoined': - displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayUserJoined', cmd.Data) - }); - break; - case 'UserLeft': - displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayUserLeft', cmd.Data) - }); - break; - case 'GroupJoined': - displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayEnabled') - }); - // Enable Syncplay - self.syncplayEnabledAt = new Date(cmd.Data); - self.syncplayReady = false; - events.trigger(self, "SyncplayEnabled", [true]); - waitForEvent(self, "SyncplayReady").then(function () { - self.processCommand(self.queuedCommand, apiClient); - self.queuedCommand = null; - }); - self.injectPlaybackManager(); - self.startPing(); - break; - case 'NotInGroup': - case 'GroupLeft': - displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayDisabled') - }); - // Disable Syncplay - self.syncplayEnabledAt = null; - self.syncplayReady = false; - events.trigger(self, "SyncplayEnabled", [false]); - self.restorePlaybackManager(); - self.stopPing(); - break; - case 'GroupWait': - displaySyncplayUpdate({ - Text: globalize.translate('MessageSyncplayGroupWait', cmd.Data) - }); - break; - case 'KeepAlive': - break; - default: - console.error('processSyncplayGroupUpdate does not recognize: ' + cmd.Type); - break; - } - }; - - self.lastCommand = null; - self.queuedCommand = null; - - self.processCommand = function (cmd, apiClient) { - if (cmd === null) return; - - if (!self.isSyncplayEnabled()) { - console.debug("Syncplay processCommand: ignoring command", cmd); - return; - } - - if (!self.syncplayReady) { - console.debug("Syncplay processCommand: queued command", cmd); - self.queuedCommand = cmd; - return; - } - - cmd.When = new Date(cmd.When); - - if (cmd.When < self.syncplayEnabledAt) { - console.debug("Syncplay processCommand: ignoring old command", cmd); - return; - } - - // Check if new command differs from last one - if (self.lastCommand && - self.lastCommand.When === cmd.When && - self.lastCommand.PositionTicks === cmd.PositionTicks && - self.Command === cmd.Command - ) { - console.debug("Syncplay processCommand: ignoring duplicate command", cmd); - return; - } - - self.lastCommand = cmd; - console.log("Syncplay will", cmd.Command, "at", cmd.When, "PositionTicks", cmd.PositionTicks); - - switch (cmd.Command) { - case 'Play': - self.schedulePlay(cmd.When, cmd.PositionTicks); - break; - case 'Pause': - self.schedulePause(cmd.When, cmd.PositionTicks); - break; - case 'Seek': - self.scheduleSeek(cmd.When, cmd.PositionTicks); - break; - default: - console.error('processSyncplayCommand does not recognize: ' + cmd.Type); - break; - } - }; - - self.scheduledCommand = null; - self.syncTimeout = null; - - self.schedulePlay = function (playAtTime, positionTicks) { - self.clearScheduledCommand(); - var currentTime = new Date(); - var playAtTimeLocal = self.serverDateToLocal(playAtTime); - - if (playAtTimeLocal > currentTime) { - var playTimeout = (playAtTimeLocal - currentTime) - self.playerDelay; - playbackManager.syncplay_seek(positionTicks); - - self.scheduledCommand = setTimeout(() => { - playbackManager.syncplay_unpause(); - - self.syncTimeout = setTimeout(() => { - self.syncEnabled = true - }, self.syncMethodThreshold / 2); - - }, playTimeout); - - // console.debug("Syncplay schedulePlay:", playTimeout); - } else { - // Group playback already started - var serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000; - playbackManager.syncplay_unpause(); - playbackManager.syncplay_seek(serverPositionTicks); - - self.syncTimeout = setTimeout(() => { - self.syncEnabled = true - }, self.syncMethodThreshold / 2); - } - }; - - self.schedulePause = function (pauseAtTime, positionTicks) { - self.clearScheduledCommand(); - var currentTime = new Date(); - var pauseAtTimeLocal = self.serverDateToLocal(pauseAtTime); - - if (pauseAtTimeLocal > currentTime) { - var pauseTimeout = (pauseAtTimeLocal - currentTime) - self.playerDelay; - - self.scheduledCommand = setTimeout(() => { - playbackManager.syncplay_pause(); - setTimeout(() => { - playbackManager.syncplay_seek(positionTicks); - }, 800); - - }, pauseTimeout); - } else { - playbackManager.syncplay_pause(); - setTimeout(() => { - playbackManager.syncplay_seek(positionTicks); - }, 800); - } - }; - - self.scheduleSeek = function (seekAtTime, positionTicks) { - self.schedulePause(seekAtTime, positionTicks); - }; - - self.clearScheduledCommand = function () { - clearTimeout(self.scheduledCommand); - clearTimeout(self.syncTimeout); - - self.syncEnabled = false; - if (self.currentPlayer) { - self.currentPlayer.setPlaybackRate(1); - } - self.clearSyncIcon(); - }; - - self.injectPlaybackManager = function () { - if (!self.isSyncplayEnabled()) return; - if (playbackManager.syncplayEnabled) return; - - playbackManager.syncplay_unpause = playbackManager.unpause; - playbackManager.syncplay_pause = playbackManager.pause; - playbackManager.syncplay_seek = playbackManager.seek; - - playbackManager.unpause = self.playRequest; - playbackManager.pause = self.pauseRequest; - playbackManager.seek = self.seekRequest; - playbackManager.syncplayEnabled = true; - }; - - self.restorePlaybackManager = function () { - if (self.isSyncplayEnabled()) return; - if (!playbackManager.syncplayEnabled) return; - - playbackManager.unpause = playbackManager.syncplay_unpause; - playbackManager.pause = playbackManager.syncplay_pause; - playbackManager.seek = playbackManager.syncplay_seek; - playbackManager.syncplayEnabled = false; - }; - - self.playRequest = function (player) { - var apiClient = connectionManager.currentApiClient(); - var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, "PlayRequest"); - }; - - self.pauseRequest = function (player) { - var apiClient = connectionManager.currentApiClient(); - var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, "PauseRequest"); - // Pause locally as well, to give the user some little control - playbackManager.syncplay_pause(); - }; - - self.seekRequest = function (PositionTicks, player) { - var apiClient = connectionManager.currentApiClient(); - var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, "SeekRequest", { - PositionTicks: PositionTicks - }); - }; - - self.pingIntervalTimeoutGreedy = 1000; - self.pingIntervalTimeoutLowProfile = 60000; - self.greedyPingCount = 3; - - self.pingStop = true; - self.pingIntervalTimeout = self.pingIntervalTimeoutGreedy; - self.pingInterval = null; - self.initTimeDiff = 0; // number of pings - self.timeDiff = 0; // local time minus server time - self.roundTripDuration = 0; - self.notifySyncplayReady = false; - - self.updateTimeDiff = function (pingStartTime, pingEndTime, serverTime) { - self.roundTripDuration = (pingEndTime - pingStartTime); - // The faster the response, the closer we are to the real timeDiff value - // localTime = pingStartTime + roundTripDuration / 2 - // newTimeDiff = localTime - serverTime - var newTimeDiff = (pingStartTime - serverTime) + (self.roundTripDuration / 2); - - // Initial setup - if (self.initTimeDiff === 0) { - self.timeDiff = newTimeDiff; - self.initTimeDiff++ - return; - } - - // As response time gets better, absolute value should decrease - var distanceFromZero = Math.abs(newTimeDiff); - var oldDistanceFromZero = Math.abs(self.timeDiff); - if (distanceFromZero < oldDistanceFromZero) { - self.timeDiff = newTimeDiff; - } - - // Avoid overloading server - if (self.initTimeDiff >= self.greedyPingCount) { - self.pingIntervalTimeout = self.pingIntervalTimeoutLowProfile; - } else { - self.initTimeDiff++; - } - - // console.debug("Syncplay updateTimeDiff:", serverTime, self.timeDiff, self.roundTripDuration, newTimeDiff); - }; - - self.requestPing = function () { - if (self.pingInterval === null && !self.pingStop) { - self.pingInterval = setTimeout(() => { - self.pingInterval = null; - - var apiClient = connectionManager.currentApiClient(); - var sessionId = getActivePlayerId(); - - var pingStartTime = new Date(); - apiClient.sendSyncplayCommand(sessionId, "GetUtcTime").then(function (response) { - var pingEndTime = new Date(); - response.text().then(function (utcTime) { - var serverTime = new Date(utcTime); - self.updateTimeDiff(pingStartTime, pingEndTime, serverTime); - - // Alert user that ping is high - if (Math.abs(self.roundTripDuration) >= 1000) { - events.trigger(self, "SyncplayError", [true]); - } else { - events.trigger(self, "SyncplayError", [false]); - } - - // Notify server of ping - apiClient.sendSyncplayCommand(sessionId, "KeepAlive", { - Ping: (self.roundTripDuration / 2) + self.playerDelay - }); - - if (self.notifySyncplayReady) { - self.syncplayReady = true; - events.trigger(self, "SyncplayReady"); - self.notifySyncplayReady = false; - } - - self.requestPing(); - }); - }); - - }, self.pingIntervalTimeout); - } - }; - - self.startPing = function () { - self.notifySyncplayReady = true; - self.pingStop = false; - self.initTimeDiff = self.initTimeDiff > self.greedyPingCount ? 1 : self.initTimeDiff; - self.pingIntervalTimeout = self.pingIntervalTimeoutGreedy; - - self.requestPing(); - }; - - self.stopPing = function () { - self.pingStop = true; - if (self.pingInterval !== null) { - clearTimeout(self.pingInterval); - self.pingInterval = null; - } - }; - - self.serverDateToLocal = function (server) { - // local - server = diff - return new Date(server.getTime() + self.timeDiff); - }; - - self.localDateToServer = function (local) { - // local - server = diff - return new Date(local.getTime() - self.timeDiff); - }; - - // THIS FEATURE IS CURRENTLY DISABLED - // Mainly because SpeedToSync seems to do the job - // Also because the delay is unreliable and different every time - self.playerDelay = 0; - self.playerDelayMeasured = true; // disable this feature - self.measurePlayerDelay = function (positionTicks) { - if (self.playerDelayMeasured) { - playbackManager.syncplay_seek(positionTicks); - } else { - // Measure playerDelay by issuing a play command - // followed by a pause command after one second - // PositionTicks should be at 1 second minus two times the player delay - loading.show(); - self.currentPlayer.setPlaybackRate(1); - playbackManager.syncplay_seek(0); - // Wait for player to seek - setTimeout(() => { - playbackManager.syncplay_unpause(); - // Play one second of media - setTimeout(() => { - playbackManager.syncplay_pause(); - // Wait for state to get update - setTimeout(() => { - var state = playbackManager.getPlayerState().PlayState; - var delayTicks = millisecondsToTicks(1000) - state.PositionTicks; - var delayMillis = ticksToMilliseconds(delayTicks); - self.playerDelay = delayMillis / 2; - // Make sure delay is not negative - self.playerDelay = self.playerDelay > 0 ? self.playerDelay : 0; - self.playerDelayMeasured = true; - // console.debug("Syncplay PlayerDelay:", self.playerDelay); - // Restore player - setTimeout(() => { - playbackManager.syncplay_seek(positionTicks); - loading.hide(); - }, 800); - }, 1000); - }, 1000); - }, 2000); - } - }; - - // Stats - self.isSyncplayEnabled = function () { - return self.syncplayEnabledAt !== null ? true : false; - }; - - self.getStats = function () { - return { - TimeDiff: self.timeDiff, - PlaybackDiff: self.playbackDiffMillis, - SyncMethod: self.syncMethod - } - }; - - // UI - self.showSyncIcon = function (syncMethod) { - self.syncMethod = syncMethod; - events.trigger(self, "SyncplayError", [true]); - }; - - self.clearSyncIcon = function () { - self.syncMethod = "None"; - events.trigger(self, "SyncplayError", [false]); - }; - } - - return new SyncplayManager(); -}); diff --git a/src/scripts/site.js b/src/scripts/site.js index 3a2ba1315..421cdae32 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -314,6 +314,13 @@ var AppInfo = {}; return obj; } + function returnDefault(obj) { + if (obj.default === null) { + throw new Error("Object has no default!"); + } + return obj.default; + } + function getBowerPath() { return 'libraries'; } @@ -817,7 +824,7 @@ var AppInfo = {}; define('playbackSettings', [componentsPath + '/playbacksettings/playbacksettings'], returnFirstDependency); define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency); define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager); - define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnFirstDependency); + define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnDefault); define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager); define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency); define('playMenu', [componentsPath + '/playmenu'], returnFirstDependency); From a18bca9d8c4a15383985d7a403b5564b88008921 Mon Sep 17 00:00:00 2001 From: gion Date: Sat, 4 Apr 2020 18:20:39 +0200 Subject: [PATCH 0034/2481] Implement syncplay permissions for a user --- src/components/syncplay/groupSelectionMenu.js | 55 +++++++++++++------ src/components/syncplay/syncplayManager.js | 6 +- src/controllers/useredit.js | 2 + src/scripts/librarymenu.js | 8 ++- src/strings/en-us.json | 6 ++ src/useredit.html | 10 ++++ 6 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js index 671b76d5e..046900d8e 100644 --- a/src/components/syncplay/groupSelectionMenu.js +++ b/src/components/syncplay/groupSelectionMenu.js @@ -27,18 +27,17 @@ function emptyCallback() { /** * Used when user needs to join a group. * @param {HTMLElement} button - Element where to place the menu. + * @param {Object} user - Current user. + * @param {Object} apiClient - ApiClient. */ -function showNewJoinGroupSelection(button) { - var apiClient = connectionManager.currentApiClient(); - var sessionId = getActivePlayerId(); +function showNewJoinGroupSelection(button, user, apiClient) { + let sessionId = getActivePlayerId(); sessionId = sessionId ? sessionId : "none"; - - loading.show(); + const inSession = sessionId !== "none"; + const policy = user.localUser ? user.localUser.Policy : {}; apiClient.sendSyncplayCommand(sessionId, "ListGroups").then(function (response) { - response.json().then(function (groups) { - var inSession = sessionId !== "none"; - + response.json().then(function (groups) { var menuItems = groups.map(function (group) { var name = datetime.getDisplayRunningTime(group.PositionTicks); if (!inSession) { @@ -53,7 +52,7 @@ function showNewJoinGroupSelection(button) { }; }); - if (inSession) { + if (inSession && policy.SyncplayAccess === "CreateAndJoinGroups") { menuItems.push({ name: globalize.translate('LabelSyncplayNewGroup'), icon: "add", @@ -64,9 +63,15 @@ function showNewJoinGroupSelection(button) { } if (menuItems.length === 0) { + if (inSession && policy.SyncplayAccess === "JoinGroups") { + toast({ + text: globalize.translate('MessageSyncplayPermissionRequired') + }); + } else { toast({ text: globalize.translate('MessageSyncplayNoGroupsAvailable') }); + } loading.hide(); return; } @@ -92,20 +97,23 @@ function showNewJoinGroupSelection(button) { loading.hide(); }); }).catch(function (error) { - loading.hide(); console.error(error); + loading.hide(); + toast({ + text: globalize.translate('MessageSyncplayNoGroupsAvailable') + }); }); } /** * Used when user has joined a group. * @param {HTMLElement} button - Element where to place the menu. + * @param {Object} user - Current user. + * @param {Object} apiClient - ApiClient. */ -function showLeaveGroupSelection(button) { - const apiClient = connectionManager.currentApiClient(); +function showLeaveGroupSelection(button, user, apiClient) { const sessionId = getActivePlayerId(); - loading.show(); const menuItems = [{ name: globalize.translate('LabelSyncplayLeaveGroup'), @@ -143,9 +151,20 @@ events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) { * @param {HTMLElement} button - Element where to place the menu. */ export function show(button) { - if (syncplayEnabled) { - showLeaveGroupSelection(button); - } else { - showNewJoinGroupSelection(button); - } + loading.show(); + + const apiClient = connectionManager.currentApiClient(); + connectionManager.user(apiClient).then((user) => { + if (syncplayEnabled) { + showLeaveGroupSelection(button, user, apiClient); + } else { + showNewJoinGroupSelection(button, user, apiClient); + } + }).catch((error) => { + console.error(error); + loading.hide(); + toast({ + text: globalize.translate('MessageSyncplayNoGroupsAvailable') + }); + }); } diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js index bc0fe7bc6..8619508e2 100644 --- a/src/components/syncplay/syncplayManager.js +++ b/src/components/syncplay/syncplayManager.js @@ -645,7 +645,7 @@ class SyncplayManager { /** * Converts server time to local time. - * @param {Date} server + * @param {Date} server The time to convert. * @returns {Date} Local time. */ serverDateToLocal (server) { @@ -655,7 +655,7 @@ class SyncplayManager { /** * Converts local time to server time. - * @param {Date} local + * @param {Date} local The time to convert. * @returns {Date} Server time. */ localDateToServer (local) { @@ -665,7 +665,7 @@ class SyncplayManager { /** * Gets Syncplay status. - * @returns {boolean} _true_ is user joined a group, _false_ otherwise. + * @returns {boolean} _true_ if user joined a group, _false_ otherwise. */ isSyncplayEnabled () { return this.syncplayEnabledAt !== null ? true : false; diff --git a/src/controllers/useredit.js b/src/controllers/useredit.js index 21ed60cfa..cbe5acd36 100644 --- a/src/controllers/useredit.js +++ b/src/controllers/useredit.js @@ -104,6 +104,7 @@ define(['jQuery', 'loading', 'libraryMenu', 'globalize', 'fnchecked'], function $('#chkEnableSharing', page).checked(user.Policy.EnablePublicSharing); $('#txtRemoteClientBitrateLimit', page).val(user.Policy.RemoteClientBitrateLimit / 1e6 || ''); $('#txtLoginAttemptsBeforeLockout', page).val(user.Policy.LoginAttemptsBeforeLockout || '0'); + $('#selectSyncplayAccess').val(user.Policy.SyncplayAccess); loading.hide(); } @@ -145,6 +146,7 @@ define(['jQuery', 'loading', 'libraryMenu', 'globalize', 'fnchecked'], function }).map(function (c) { return c.getAttribute('data-id'); }); + user.Policy.SyncplayAccess = page.querySelector('#selectSyncplayAccess').value; ApiClient.updateUser(user).then(function () { ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { onSaveComplete(page, user); diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index 8afe5f10f..1daa200b8 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -86,6 +86,12 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' if (!layoutManager.tv) { headerCastButton.classList.remove('hide'); } + + var policy = user.Policy ? user.Policy : user.localUser.Policy; + + if (headerSyncButton && policy && policy.SyncplayAccess !== "None") { + headerSyncButton.classList.remove("hide"); + } } else { headerHomeButton.classList.add('hide'); headerCastButton.classList.add('hide'); @@ -95,8 +101,6 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } } - headerSyncButton.classList.remove("hide"); - requiresUserRefresh = false; } diff --git a/src/strings/en-us.json b/src/strings/en-us.json index c7611333f..83890426c 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -862,6 +862,10 @@ "LabelSyncplayNewGroupDescription": "Create a new group", "LabelSyncplayLeaveGroup": "Leave group", "LabelSyncplayLeaveGroupDescription": "Disable Syncplay", + "LabelSyncplayAccessCreateAndJoinGroups": "Allow user to create and join groups", + "LabelSyncplayAccessJoinGroups": "Allow user to join groups", + "LabelSyncplayAccessNone": "Disabled for this user", + "LabelSyncplayAccess": "Syncplay access", "LabelTVHomeScreen": "TV mode home screen:", "LabelTag": "Tag:", "LabelTagline": "Tagline:", @@ -1031,6 +1035,7 @@ "MessageSyncplayUserLeft": "{0} left group.", "MessageSyncplayGroupWait": "{0} is buffering...", "MessageSyncplayNoGroupsAvailable": "No groups available.", + "MessageSyncplayPermissionRequired": "Permission required to create a group.", "Metadata": "Metadata", "MetadataManager": "Metadata Manager", "MetadataSettingChangeHelp": "Changing metadata settings will affect new content that is added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.", @@ -1379,6 +1384,7 @@ "Suggestions": "Suggestions", "Sunday": "Sunday", "Sync": "Sync", + "SyncplayAccessHelp": "Select the level of access this user has to the Syncplay feature. Syncplay enables to sync playback with other users.", "SystemDlnaProfilesHelp": "System profiles are read-only. Changes to a system profile will be saved to a new custom profile.", "TV": "TV", "TabAccess": "Access", diff --git a/src/useredit.html b/src/useredit.html index 0de3069dc..5f759c973 100644 --- a/src/useredit.html +++ b/src/useredit.html @@ -104,6 +104,16 @@
    ${LabelUserRemoteClientBitrateLimitHelp}
    +
    +
    + +
    ${SyncplayAccessHelp}
    +
    +

    ${HeaderAllowMediaDeletionFrom}

    From 9fabbd5746c90b2115ae5b634159fb79d60abc5f Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 15 Apr 2020 18:09:34 +0200 Subject: [PATCH 0035/2481] Capture playback permission --- gulpfile.js | 2 +- package.json | 7 +-- src/assets/audio/silence.wav | Bin 0 -> 88244 bytes src/components/syncplay/groupSelectionMenu.js | 11 ++++ .../syncplay/playbackPermissionManager.js | 51 ++++++++++++++++++ src/scripts/site.js | 1 + src/strings/en-us.json | 1 + webpack.dev.js | 4 ++ webpack.prod.js | 4 ++ 9 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/assets/audio/silence.wav create mode 100644 src/components/syncplay/playbackPermissionManager.js diff --git a/gulpfile.js b/gulpfile.js index 6c3316738..ad77d9a67 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -45,7 +45,7 @@ const options = { query: ['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'] }, copy: { - query: ['src/**/*.json', 'src/**/*.ico'] + query: ['src/**/*.json', 'src/**/*.ico', 'src/**/*.wav'] }, injectBundle: { query: 'src/index.html' diff --git a/package.json b/package.json index 39330cf60..19d3b05a4 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,9 @@ "src/components/playback/mediasession.js", "src/components/sanatizefilename.js", "src/components/scrollManager.js", + "src/components/syncplay/playbackPermissionManager.js", + "src/components/syncplay/groupSelectionMenu.js", + "src/components/syncplay/syncplayManager.js", "src/scripts/dfnshelper.js", "src/scripts/dom.js", "src/scripts/filesystem.js", @@ -105,9 +108,7 @@ "src/scripts/keyboardnavigation.js", "src/scripts/settings/appSettings.js", "src/scripts/settings/userSettings.js", - "src/scripts/settings/webSettings.js", - "src/components/syncplay/groupSelectionMenu.js", - "src/components/syncplay/syncplayManager.js" + "src/scripts/settings/webSettings.js" ], "plugins": [ "@babel/plugin-transform-modules-amd" diff --git a/src/assets/audio/silence.wav b/src/assets/audio/silence.wav new file mode 100644 index 0000000000000000000000000000000000000000..63f253da845e5177e59bcbc05a49ecb118f5e97a GIT binary patch literal 88244 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 Sj#^Qp#u#I*axXE^2mT*$^iC20 literal 0 HcmV?d00001 diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js index 046900d8e..1b490db80 100644 --- a/src/components/syncplay/groupSelectionMenu.js +++ b/src/components/syncplay/groupSelectionMenu.js @@ -7,6 +7,7 @@ import datetime from 'datetime'; import toast from 'toast'; import actionsheet from 'actionsheet'; import globalize from 'globalize'; +import playbackPermissionManager from 'playbackPermissionManager'; /** * Gets active player id. @@ -153,6 +154,16 @@ events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) { export function show(button) { loading.show(); + // TODO: should feature be disabled if playback permission is missing? + playbackPermissionManager.check().then(() => { + console.debug("Playback is allowed."); + }).catch((error) => { + console.error("Playback not allowed!", error); + toast({ + text: globalize.translate("MessageSyncplayPlaybackPermissionRequired") + }); + }); + const apiClient = connectionManager.currentApiClient(); connectionManager.user(apiClient).then((user) => { if (syncplayEnabled) { diff --git a/src/components/syncplay/playbackPermissionManager.js b/src/components/syncplay/playbackPermissionManager.js new file mode 100644 index 000000000..df16545b3 --- /dev/null +++ b/src/components/syncplay/playbackPermissionManager.js @@ -0,0 +1,51 @@ +/** + * Creates an audio element that plays a silent sound. + * @returns {HTMLMediaElement} The audio element. + */ +function createTestMediaElement () { + + const elem = document.createElement('audio'); + elem.classList.add('testMediaPlayerAudio'); + elem.classList.add('hide'); + + document.body.appendChild(elem); + + elem.volume = 1; // Volume should not be zero to trigger proper permissions + elem.src = "assets/audio/silence.wav"; // Silent sound + + return elem; +} + +/** + * Destroys a media element. + * @param {HTMLMediaElement} elem The element to destroy. + */ +function destroyTestMediaElement (elem) { + elem.pause(); + elem.remove(); +} + +/** + * Class that manages the playback permission. + */ +class PlaybackPermissionManager { + /** + * Tests playback permission. Grabs the permission when called inside a click event (or any other valid user interaction). + * @returns {Promise} Promise that resolves succesfully if playback permission is allowed. + */ + check () { + return new Promise((resolve, reject) => { + const media = createTestMediaElement(); + media.play().then(() => { + resolve(); + }).catch((error) => { + reject(error); + }).finally(() => { + destroyTestMediaElement(media); + }); + }); + } +} + +/** PlaybackPermissionManager singleton. */ +export default new PlaybackPermissionManager(); diff --git a/src/scripts/site.js b/src/scripts/site.js index 421cdae32..3954b153a 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -825,6 +825,7 @@ var AppInfo = {}; define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency); define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager); define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnDefault); + define('playbackPermissionManager', [componentsPath + '/syncplay/playbackPermissionManager'], returnDefault); define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager); define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency); define('playMenu', [componentsPath + '/playmenu'], returnFirstDependency); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 83890426c..136a01543 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1036,6 +1036,7 @@ "MessageSyncplayGroupWait": "{0} is buffering...", "MessageSyncplayNoGroupsAvailable": "No groups available.", "MessageSyncplayPermissionRequired": "Permission required to create a group.", + "MessageSyncplayPlaybackPermissionRequired": "Playback permission required.", "Metadata": "Metadata", "MetadataManager": "Metadata Manager", "MetadataSettingChangeHelp": "Changing metadata settings will affect new content that is added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.", diff --git a/webpack.dev.js b/webpack.dev.js index 76a1a7a75..d8879fe80 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -44,6 +44,10 @@ module.exports = merge(common, { use: [ 'file-loader' ] + }, + { + test: /\.(wav)$/i, + use: ["file-loader"] } ] } diff --git a/webpack.prod.js b/webpack.prod.js index f5c7accd0..cc4c57b9f 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -37,6 +37,10 @@ module.exports = merge(common, { use: [ 'file-loader' ] + }, + { + test: /\.(wav)$/i, + use: ["file-loader"] } ] } From 06e6c99c03f70023e40e35bd2804e3916e2ec0de Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 15 Apr 2020 18:15:28 +0200 Subject: [PATCH 0036/2481] Disable syncing after several attempts Refactor syncplay manager Attempt fixing Safari issues (timeupdate event not firing) --- src/components/syncplay/groupSelectionMenu.js | 29 +- src/components/syncplay/syncplayManager.js | 428 +++++++++++++----- src/workers/syncplay/syncplay.worker.js | 90 ++++ webpack.dev.js | 4 + webpack.prod.js | 4 + 5 files changed, 437 insertions(+), 118 deletions(-) create mode 100644 src/workers/syncplay/syncplay.worker.js diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js index 1b490db80..c907d338a 100644 --- a/src/components/syncplay/groupSelectionMenu.js +++ b/src/components/syncplay/groupSelectionMenu.js @@ -13,7 +13,7 @@ import playbackPermissionManager from 'playbackPermissionManager'; * Gets active player id. * @returns {string} The player's id. */ -function getActivePlayerId() { +function getActivePlayerId () { var info = playbackManager.getPlayerInfo(); return info ? info.id : null; } @@ -21,7 +21,7 @@ function getActivePlayerId() { /** * Used to avoid console logs about uncaught promises */ -function emptyCallback() { +function emptyCallback () { // avoid console logs about uncaught promises } @@ -31,15 +31,23 @@ function emptyCallback() { * @param {Object} user - Current user. * @param {Object} apiClient - ApiClient. */ -function showNewJoinGroupSelection(button, user, apiClient) { +function showNewJoinGroupSelection (button, user, apiClient) { let sessionId = getActivePlayerId(); sessionId = sessionId ? sessionId : "none"; const inSession = sessionId !== "none"; const policy = user.localUser ? user.localUser.Policy : {}; + let playingItemId; + try { + const playState = playbackManager.getPlayerState(); + playingItemId = playState.NowPlayingItem.Id; + } catch (error) { + playingItemId = ""; + } apiClient.sendSyncplayCommand(sessionId, "ListGroups").then(function (response) { response.json().then(function (groups) { var menuItems = groups.map(function (group) { + // TODO: update running time if group is playing? var name = datetime.getDisplayRunningTime(group.PositionTicks); if (!inSession) { name = group.PlayingItemName; @@ -90,7 +98,8 @@ function showNewJoinGroupSelection(button, user, apiClient) { apiClient.sendSyncplayCommand(sessionId, "NewGroup"); } else { apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { - GroupId: id + GroupId: id, + PlayingItemId: playingItemId }); } }, emptyCallback); @@ -112,8 +121,16 @@ function showNewJoinGroupSelection(button, user, apiClient) { * @param {Object} user - Current user. * @param {Object} apiClient - ApiClient. */ -function showLeaveGroupSelection(button, user, apiClient) { +function showLeaveGroupSelection (button, user, apiClient) { const sessionId = getActivePlayerId(); + if (!sessionId) { + syncplayManager.signalError(); + toast({ + // TODO: translate + text: "Syncplay error occured." + }); + return; + } const menuItems = [{ @@ -151,7 +168,7 @@ events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) { * Shows a menu to handle Syncplay groups. * @param {HTMLElement} button - Element where to place the menu. */ -export function show(button) { +export function show (button) { loading.show(); // TODO: should feature be disabled if playback permission is missing? diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js index 8619508e2..86b7ec34a 100644 --- a/src/components/syncplay/syncplayManager.js +++ b/src/components/syncplay/syncplayManager.js @@ -32,7 +32,7 @@ function waitForEvent(emitter, eventType) { * @returns {string} The player's id. */ function getActivePlayerId() { - var info = playbackManager.getPlayerInfo(); + var info = playbackManager.getPlayerInfo(); return info ? info.id : null; } @@ -42,7 +42,9 @@ function getActivePlayerId() { const MaxAcceptedDelaySpeedToSync = 50; // milliseconds, delay after which SpeedToSync is enabled const MaxAcceptedDelaySkipToSync = 300; // milliseconds, delay after which SkipToSync is enabled const SyncMethodThreshold = 2000; // milliseconds, switches between SpeedToSync or SkipToSync -const SpeedUpToSyncTime = 1000; // milliseconds, duration in which the playback is sped up +const SpeedToSyncTime = 1000; // milliseconds, duration in which the playback is sped up +const MaxAttemptsSpeedToSync = 3; // attempts before disabling SpeedToSync +const MaxAttemptsSync = 5; // attempts before disabling syncing at all /** * Time estimation @@ -60,6 +62,9 @@ class SyncplayManager { this.syncEnabled = false; this.playbackDiffMillis = 0; // used for stats this.syncMethod = "None"; // used for stats + this.syncAttempts = 0; + this.lastSyncTime = new Date(); + this.syncWatcherTimeout = null; // interval that watches playback time and syncs it this.lastPlaybackWaiting = null; // used to determine if player's buffering this.minBufferingThresholdMillis = 1000; @@ -86,7 +91,16 @@ class SyncplayManager { events.on(playbackManager, "playerchange", () => { this.onPlayerChange(); }); + + events.on(playbackManager, "playbackstart", (player, state) => { + events.trigger(this, 'PlaybackStart', [player, state]); + }); + this.bindToPlayer(playbackManager.getCurrentPlayer()); + + events.on(this, "TimeUpdate", (event) => { + this.syncPlaybackTime(); + }); } /** @@ -110,53 +124,9 @@ class SyncplayManager { * @param {Object} e The time update event. */ onTimeUpdate (e) { + // NOTICE: this event is unreliable, at least in Safari + // which just stops firing the event after a while. events.trigger(this, "TimeUpdate", [e]); - - if (this.lastCommand && this.lastCommand.Command === 'Play' && !this.isBuffering()) { - var currentTime = new Date(); - var playAtTime = this.lastCommand.When; - - var state = playbackManager.getPlayerState().PlayState; - // Estimate PositionTicks on server - var ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) - this.timeDiff) * 10000; - // Measure delay that needs to be recovered - // diff might be caused by the player internally starting the playback - var diff = ServerPositionTicks - state.PositionTicks; - var diffMillis = diff / 10000; - - this.playbackDiffMillis = diffMillis; - - // console.debug("Syncplay onTimeUpdate", diffMillis, state.PositionTicks, ServerPositionTicks); - - if (this.syncEnabled) { - var absDiffMillis = Math.abs(diffMillis); - // TODO: SpeedToSync sounds bad on songs - if (this.playbackRateSupported && absDiffMillis > MaxAcceptedDelaySpeedToSync && absDiffMillis < SyncMethodThreshold) { - // SpeedToSync method - var speed = 1 + diffMillis / SpeedUpToSyncTime; - - this.currentPlayer.setPlaybackRate(speed); - this.syncEnabled = false; - this.showSyncIcon("SpeedToSync (x" + speed + ")"); - - this.syncTimeout = setTimeout(() => { - this.currentPlayer.setPlaybackRate(1); - this.syncEnabled = true; - this.clearSyncIcon(); - }, SpeedUpToSyncTime); - } else if (absDiffMillis > MaxAcceptedDelaySkipToSync) { - // SkipToSync method - playbackManager.syncplay_seek(ServerPositionTicks); - this.syncEnabled = false; - this.showSyncIcon("SkipToSync"); - - this.syncTimeout = setTimeout(() => { - this.syncEnabled = true; - this.clearSyncIcon(); - }, this.syncMethodThreshold / 2); - } - } - } } /** @@ -256,34 +226,7 @@ class SyncplayManager { processGroupUpdate (cmd, apiClient) { switch (cmd.Type) { case 'PrepareSession': - var serverId = apiClient.serverInfo().Id; - playbackManager.play({ - ids: cmd.Data.ItemIds, - startPositionTicks: cmd.Data.StartPositionTicks, - mediaSourceId: cmd.Data.MediaSourceId, - audioStreamIndex: cmd.Data.AudioStreamIndex, - subtitleStreamIndex: cmd.Data.SubtitleStreamIndex, - startIndex: cmd.Data.StartIndex, - serverId: serverId - }).then(() => { - waitForEvent(this, "PlayerChange").then(() => { - playbackManager.pause(); - var sessionId = getActivePlayerId(); - if (!sessionId) { - console.error("Missing sessionId!"); - toast({ - text: "Failed to enable Syncplay!" - }); - return; - } - // Sometimes JoinGroup fails, maybe because server hasn't been updated yet - setTimeout(() => { - apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { - GroupId: cmd.GroupId - }); - }, 500); - }); - }); + this.prepareSession(apiClient, cmd.GroupId, cmd.Data); break; case 'UserJoined': toast({ @@ -296,31 +239,12 @@ class SyncplayManager { }); break; case 'GroupJoined': - toast({ - text: globalize.translate('MessageSyncplayEnabled') - }); - // Enable Syncplay - this.syncplayEnabledAt = new Date(cmd.Data); - this.syncplayReady = false; - events.trigger(this, "SyncplayEnabled", [true]); - waitForEvent(this, "SyncplayReady").then(() => { - this.processCommand(this.queuedCommand, apiClient); - this.queuedCommand = null; - }); - this.injectPlaybackManager(); - this.startPing(); + const enabledAt = new Date(cmd.Data); + this.enableSyncplay(apiClient, enabledAt, true); break; case 'NotInGroup': case 'GroupLeft': - toast({ - text: globalize.translate('MessageSyncplayDisabled') - }); - // Disable Syncplay - this.syncplayEnabledAt = null; - this.syncplayReady = false; - events.trigger(this, "SyncplayEnabled", [false]); - this.restorePlaybackManager(); - this.stopPing(); + this.disableSyncplay(true); break; case 'GroupWait': toast({ @@ -355,8 +279,9 @@ class SyncplayManager { } cmd.When = new Date(cmd.When); + cmd.EmittedAt = new Date(cmd.EmitttedAt); - if (cmd.When < this.syncplayEnabledAt) { + if (cmd.EmitttedAt < this.syncplayEnabledAt) { console.debug("Syncplay processCommand: ignoring old command", cmd); return; } @@ -390,6 +315,114 @@ class SyncplayManager { } } + /** + * Prepares this client to join a group by loading the required content. + * @param {Object} apiClient The ApiClient. + * @param {string} groupId The group to join. + * @param {Object} sessionData Info about the content to load. + */ + prepareSession (apiClient, groupId, sessionData) { + var serverId = apiClient.serverInfo().Id; + playbackManager.play({ + ids: sessionData.ItemIds, + startPositionTicks: sessionData.StartPositionTicks, + mediaSourceId: sessionData.MediaSourceId, + audioStreamIndex: sessionData.AudioStreamIndex, + subtitleStreamIndex: sessionData.SubtitleStreamIndex, + startIndex: sessionData.StartIndex, + serverId: serverId + }).then(() => { + // TODO: switch to PlaybackStart maybe? + waitForEvent(this, "PlayerChange").then(() => { + playbackManager.pause(); + var sessionId = getActivePlayerId(); + if (!sessionId) { + console.error("Missing sessionId!"); + toast({ + // TODO: translate + text: "Failed to enable Syncplay! Missing session id." + }); + return; + } + // Get playing item id + let playingItemId; + try { + const playState = playbackManager.getPlayerState(); + playingItemId = playState.NowPlayingItem.Id; + } catch (error) { + playingItemId = ""; + } + // Sometimes JoinGroup fails, maybe because server hasn't been updated yet + setTimeout(() => { + apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { + GroupId: groupId, + PlayingItemId: playingItemId + }); + }, 500); + }); + }).catch((error) => { + console.error(error); + toast({ + // TODO: translate + text: "Failed to enable Syncplay! Media error." + }); + }); + } + + /** + * Enables Syncplay. + * @param {Object} apiClient The ApiClient. + * @param {Date} enabledAt When Syncplay has been enabled. Server side date. + * @param {boolean} showMessage Display message. + */ + enableSyncplay (apiClient, enabledAt, showMessage = false) { + this.syncplayEnabledAt = enabledAt; + this.syncplayReady = false; + events.trigger(this, "SyncplayEnabled", [true]); + waitForEvent(this, "SyncplayReady").then(() => { + this.processCommand(this.queuedCommand, apiClient); + this.queuedCommand = null; + }); + this.injectPlaybackManager(); + this.startPing(); + + if (showMessage) { + toast({ + text: globalize.translate('MessageSyncplayEnabled') + }); + } + } + + /** + * Disables Syncplay. + * @param {boolean} showMessage Display message. + */ + disableSyncplay (showMessage = false) { + this.syncplayEnabledAt = null; + this.syncplayReady = false; + this.lastCommand = null; + this.queuedCommand = null; + this.syncEnabled = false; + events.trigger(this, "SyncplayEnabled", [false]); + this.restorePlaybackManager(); + this.stopPing(); + this.stopSyncWatcher(); + + if (showMessage) { + toast({ + text: globalize.translate('MessageSyncplayDisabled') + }); + } + } + + /** + * Gets Syncplay status. + * @returns {boolean} _true_ if user joined a group, _false_ otherwise. + */ + isSyncplayEnabled () { + return this.syncplayEnabledAt !== null ? true : false; + } + /** * Schedules a resume playback on the player at the specified clock time. * @param {Date} playAtTime The server's UTC time at which to resume playback. @@ -408,8 +441,9 @@ class SyncplayManager { playbackManager.syncplay_unpause(); this.syncTimeout = setTimeout(() => { - this.syncEnabled = true - }, this.syncMethodThreshold / 2); + this.syncEnabled = true; + this.startSyncWatcher(); + }, SyncMethodThreshold / 2); }, playTimeout); @@ -421,8 +455,9 @@ class SyncplayManager { playbackManager.syncplay_seek(serverPositionTicks); this.syncTimeout = setTimeout(() => { - this.syncEnabled = true - }, this.syncMethodThreshold / 2); + this.syncEnabled = true; + this.startSyncWatcher(); + }, SyncMethodThreshold / 2); } } @@ -471,6 +506,7 @@ class SyncplayManager { clearTimeout(this.syncTimeout); this.syncEnabled = false; + this.stopSyncWatcher(); if (this.currentPlayer) { this.currentPlayer.setPlaybackRate(1); } @@ -587,6 +623,15 @@ class SyncplayManager { var apiClient = connectionManager.currentApiClient(); var sessionId = getActivePlayerId(); + if (!sessionId) { + this.signalError(); + toast({ + // TODO: translate + text: "Syncplay error occured." + }); + return; + } + var pingStartTime = new Date(); apiClient.sendSyncplayCommand(sessionId, "GetUtcTime").then((response) => { var pingEndTime = new Date(); @@ -614,6 +659,13 @@ class SyncplayManager { this.requestPing(); }); + }).catch((error) => { + console.error(error); + this.signalError(); + toast({ + // TODO: translate + text: "Syncplay error occured." + }); }); }, this.pingIntervalTimeout); @@ -627,7 +679,7 @@ class SyncplayManager { this.notifySyncplayReady = true; this.pingStop = false; this.initTimeDiff = this.initTimeDiff > this.greedyPingCount ? 1 : this.initTimeDiff; - this.pingIntervalTimeout = this.pingIntervalTimeoutGreedy; + this.pingIntervalTimeout = PingIntervalTimeoutGreedy; this.requestPing(); } @@ -643,6 +695,159 @@ class SyncplayManager { } } + /** + * Attempts to sync playback time with estimated server time. + * + * When sync is enabled, the following will be checked: + * - check if local playback time is close enough to the server playback time + * If it is not, then a playback time sync will be attempted. + * Two methods of syncing are available: + * - SpeedToSync: speeds up the media for some time to catch up (default is one second) + * - SkipToSync: seeks the media to the estimated correct time + * SpeedToSync aims to reduce the delay as much as possible, whereas SkipToSync is less pretentious. + */ + syncPlaybackTime () { + // Attempt to sync only when media is playing. + if (!this.lastCommand || this.lastCommand.Command !== 'Play' || this.isBuffering()) return; + + const currentTime = new Date(); + + // Avoid overloading the browser + const elapsed = currentTime - this.lastSyncTime; + if (elapsed < SyncMethodThreshold / 2) return; + this.lastSyncTime = currentTime; + this.notifySyncWatcher(); + + const playAtTime = this.lastCommand.When; + + const CurrentPositionTicks = playbackManager.currentTime(); + // Estimate PositionTicks on server + const ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) - this.timeDiff) * 10000; + // Measure delay that needs to be recovered + // diff might be caused by the player internally starting the playback + const diff = ServerPositionTicks - CurrentPositionTicks; + const diffMillis = diff / 10000; + + this.playbackDiffMillis = diffMillis; + + // console.debug("Syncplay onTimeUpdate", diffMillis, CurrentPositionTicks, ServerPositionTicks); + + if (this.syncEnabled) { + const absDiffMillis = Math.abs(diffMillis); + // TODO: SpeedToSync sounds bad on songs + // TODO: SpeedToSync is failing on Safari (Mojave); even if playbackRate is supported, some delay seems to exist + if (this.playbackRateSupported && absDiffMillis > MaxAcceptedDelaySpeedToSync && absDiffMillis < SyncMethodThreshold) { + // Disable SpeedToSync if it keeps failing + if (this.syncAttempts > MaxAttemptsSpeedToSync) { + this.playbackRateSupported = false; + } + // SpeedToSync method + const speed = 1 + diffMillis / SpeedToSyncTime; + + this.currentPlayer.setPlaybackRate(speed); + this.syncEnabled = false; + this.syncAttempts++; + this.showSyncIcon("SpeedToSync (x" + speed + ")"); + + this.syncTimeout = setTimeout(() => { + this.currentPlayer.setPlaybackRate(1); + this.syncEnabled = true; + this.clearSyncIcon(); + }, SpeedToSyncTime); + } else if (absDiffMillis > MaxAcceptedDelaySkipToSync) { + // Disable SkipToSync if it keeps failing + if (this.syncAttempts > MaxAttemptsSync) { + this.syncEnabled = false; + this.showSyncIcon("Sync disabled (too many attempts)"); + } + // SkipToSync method + playbackManager.syncplay_seek(ServerPositionTicks); + this.syncEnabled = false; + this.syncAttempts++; + this.showSyncIcon("SkipToSync (" + this.syncAttempts + ")"); + + this.syncTimeout = setTimeout(() => { + this.syncEnabled = true; + this.clearSyncIcon(); + }, SyncMethodThreshold / 2); + } else { + // Playback is synced + if (this.syncAttempts > 0) { + // console.debug("Playback has been synced after", this.syncAttempts, "attempts."); + } + this.syncAttempts = 0; + } + } + } + + /** + * Signals the worker to start watching sync. Also creates the worker if needed. + * + * This additional fail-safe has been added because on Safari the timeupdate event fails after a while. + */ + startSyncWatcher () { + // SPOILER ALERT: this idea fails too on Safari... Keeping it here for future investigations + return; + if (window.Worker) { + // Start worker if needed + if (!this.worker) { + this.worker = new Worker("workers/syncplay/syncplay.worker.js"); + this.worker.onmessage = (event) => { + const message = event.data; + switch (message.type) { + case "TriggerSync": + // TODO: player state might not reflect the real playback position, + // thus calling syncPlaybackTime outside a timeupdate event might not really sync to the right point + this.syncPlaybackTime(); + break; + default: + console.error("Syncplay: unknown message from worker:", message.type); + break; + } + }; + this.worker.onerror = (event) => { + console.error("Syncplay: worker error", event); + }; + this.worker.onmessageerror = (event) => { + console.error("Syncplay: worker message error", event); + }; + } + // Start watcher + this.worker.postMessage({ + type: "StartSyncWatcher", + data: { + interval: SyncMethodThreshold / 2, + threshold: SyncMethodThreshold + } + }); + } else { + console.debug("Syncplay: workers not supported."); + } + } + + /** + * Signals the worker to stop watching sync. + */ + stopSyncWatcher () { + if (this.worker) { + this.worker.postMessage({ + type: "StopSyncWatcher" + }); + } + } + + /** + * Signals new state to worker. + */ + notifySyncWatcher () { + if (this.worker) { + this.worker.postMessage({ + type: "UpdateLastSyncTime", + data: this.lastSyncTime + }); + } + } + /** * Converts server time to local time. * @param {Date} server The time to convert. @@ -663,14 +868,6 @@ class SyncplayManager { return new Date(local.getTime() - this.timeDiff); } - /** - * Gets Syncplay status. - * @returns {boolean} _true_ if user joined a group, _false_ otherwise. - */ - isSyncplayEnabled () { - return this.syncplayEnabledAt !== null ? true : false; - } - /** * Gets Syncplay stats. * @returns {Object} The Syncplay stats. @@ -698,6 +895,13 @@ class SyncplayManager { this.syncMethod = "None"; events.trigger(this, "SyncplayError", [false]); } + + /** + * Signals an error state, which disables and resets Syncplay for a new session. + */ + signalError () { + this.disableSyncplay(); + } } /** SyncplayManager singleton. */ diff --git a/src/workers/syncplay/syncplay.worker.js b/src/workers/syncplay/syncplay.worker.js new file mode 100644 index 000000000..bd8d2bd60 --- /dev/null +++ b/src/workers/syncplay/syncplay.worker.js @@ -0,0 +1,90 @@ +var SyncTimeThreshold = 2000; // milliseconds, overwritten by startSyncWatcher +var SyncWatcherInterval = 1000; // milliseconds, overwritten by startSyncWatcher +var lastSyncTime = new Date(); // internal state +var syncWatcher; // holds value from setInterval + +/** + * Sends a message to the UI worker. + * @param {string} type + * @param {*} data + */ +function sendMessage (type, data) { + postMessage({ + type: type, + data: data + }); + +} + +/** + * Updates the state. + * @param {Date} syncTime The new state. + */ +function updateLastSyncTime (syncTime) { + lastSyncTime = syncTime; +} + +/** + * Starts sync watcher. + * @param {Object} options Additional options to configure the watcher, like _interval_ and _threshold_. + */ +function startSyncWatcher(options) { + stopSyncWatcher(); + if (options) { + if (options.interval) { + SyncWatcherInterval = options.interval; + } + if (options.threshold) { + SyncTimeThreshold = options.threshold; + } + } + syncWatcher = setInterval(syncWatcherCallback, SyncWatcherInterval); +} + +/** + * Stops sync watcher. + */ +function stopSyncWatcher () { + if (syncWatcher) { + clearInterval(syncWatcher); + syncWatcher = null; + } +} + +/** + * Oversees playback sync and makes sure that it gets called regularly. + */ +function syncWatcherCallback () { + const currentTime = new Date(); + const elapsed = currentTime - lastSyncTime; + if (elapsed > SyncTimeThreshold) { + sendMessage("TriggerSync"); + } +} + +/** + * Handles messages from UI worker. + * @param {MessageEvent} event The message to handle. + */ +function handleMessage (event) { + const message = event.data; + switch (message.type) { + case "UpdateLastSyncTime": + updateLastSyncTime(message.data); + break; + case "StartSyncWatcher": + startSyncWatcher(message.data); + break; + case "StopSyncWatcher": + stopSyncWatcher(); + break; + default: + console.error("Unknown message type:", message.type); + break; + } +} + +// Listen for messages +addEventListener("message", function (event) { + handleMessage(event); +}); diff --git a/webpack.dev.js b/webpack.dev.js index d8879fe80..544d32d63 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -48,6 +48,10 @@ module.exports = merge(common, { { test: /\.(wav)$/i, use: ["file-loader"] + }, + { + test: /\.worker.js$/, + use: ["worker"] } ] } diff --git a/webpack.prod.js b/webpack.prod.js index cc4c57b9f..7b5f2ff6c 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -41,6 +41,10 @@ module.exports = merge(common, { { test: /\.(wav)$/i, use: ["file-loader"] + }, + { + test: /\.worker.js$/, + use: ["worker"] } ] } From 5342b90a56a7aeeacbf9edaa464ab74e9ecf8aca Mon Sep 17 00:00:00 2001 From: gion Date: Thu, 16 Apr 2020 16:05:04 +0200 Subject: [PATCH 0037/2481] Implement NTP like time sync --- src/components/playerstats/playerstats.js | 4 +- src/components/syncplay/syncplayManager.js | 176 +++-------------- src/components/syncplay/timeSyncManager.js | 209 +++++++++++++++++++++ src/scripts/site.js | 1 + src/strings/en-us.json | 2 +- 5 files changed, 236 insertions(+), 156 deletions(-) create mode 100644 src/components/syncplay/timeSyncManager.js diff --git a/src/components/playerstats/playerstats.js b/src/components/playerstats/playerstats.js index 4bd49ba5c..404baab7e 100644 --- a/src/components/playerstats/playerstats.js +++ b/src/components/playerstats/playerstats.js @@ -332,8 +332,8 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplay var stats = syncplayManager.getStats(); syncStats.push({ - label: globalize.translate("LabelSyncplayTimeDiff"), - value: stats.TimeDiff + "ms" + label: globalize.translate("LabelSyncplayTimeOffset"), + value: stats.TimeOffset + "ms" }); syncStats.push({ diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js index 86b7ec34a..de1424b89 100644 --- a/src/components/syncplay/syncplayManager.js +++ b/src/components/syncplay/syncplayManager.js @@ -8,6 +8,7 @@ import events from 'events'; import connectionManager from 'connectionManager'; import playbackManager from 'playbackManager'; +import timeSyncManager from 'timeSyncManager'; import toast from 'toast'; import globalize from 'globalize'; @@ -80,11 +81,7 @@ class SyncplayManager { this.scheduledCommand = null; this.syncTimeout = null; - this.pingStop = true; - this.pingIntervalTimeout = PingIntervalTimeoutGreedy; - this.pingInterval = null; - this.initTimeDiff = 0; // number of pings - this.timeDiff = 0; // local time minus server time + this.timeOffsetWithServer = 0; // server time minus local time this.roundTripDuration = 0; this.notifySyncplayReady = false; @@ -101,6 +98,17 @@ class SyncplayManager { events.on(this, "TimeUpdate", (event) => { this.syncPlaybackTime(); }); + + events.on(timeSyncManager, "Update", (event, timeOffset, ping) => { + this.timeOffsetWithServer = timeOffset; + this.roundTripDuration = ping * 2; + + if (this.notifySyncplayReady) { + this.syncplayReady = true; + events.trigger(this, "SyncplayReady"); + this.notifySyncplayReady = false; + } + }); } /** @@ -377,14 +385,17 @@ class SyncplayManager { */ enableSyncplay (apiClient, enabledAt, showMessage = false) { this.syncplayEnabledAt = enabledAt; - this.syncplayReady = false; + this.injectPlaybackManager(); events.trigger(this, "SyncplayEnabled", [true]); + waitForEvent(this, "SyncplayReady").then(() => { this.processCommand(this.queuedCommand, apiClient); this.queuedCommand = null; }); - this.injectPlaybackManager(); - this.startPing(); + this.syncplayReady = false; + this.notifySyncplayReady = true; + + timeSyncManager.forceUpdate(); if (showMessage) { toast({ @@ -405,7 +416,6 @@ class SyncplayManager { this.syncEnabled = false; events.trigger(this, "SyncplayEnabled", [false]); this.restorePlaybackManager(); - this.stopPing(); this.stopSyncWatcher(); if (showMessage) { @@ -431,7 +441,7 @@ class SyncplayManager { schedulePlay (playAtTime, positionTicks) { this.clearScheduledCommand(); var currentTime = new Date(); - var playAtTimeLocal = this.serverDateToLocal(playAtTime); + var playAtTimeLocal = timeSyncManager.serverDateToLocal(playAtTime); if (playAtTimeLocal > currentTime) { var playTimeout = playAtTimeLocal - currentTime; @@ -469,7 +479,7 @@ class SyncplayManager { schedulePause (pauseAtTime, positionTicks) { this.clearScheduledCommand(); var currentTime = new Date(); - var pauseAtTimeLocal = this.serverDateToLocal(pauseAtTime); + var pauseAtTimeLocal = timeSyncManager.serverDateToLocal(pauseAtTime); if (pauseAtTimeLocal > currentTime) { var pauseTimeout = pauseAtTimeLocal - currentTime; @@ -575,126 +585,6 @@ class SyncplayManager { }); } - /** - * Computes time difference between this client's time and server's time. - * @param {Date} pingStartTime Local time when ping request started. - * @param {Date} pingEndTime Local time when ping request ended. - * @param {Date} serverTime Server UTC time at ping request. - */ - updateTimeDiff (pingStartTime, pingEndTime, serverTime) { - this.roundTripDuration = (pingEndTime - pingStartTime); - // The faster the response, the closer we are to the real timeDiff value - // localTime = pingStartTime + roundTripDuration / 2 - // newTimeDiff = localTime - serverTime - var newTimeDiff = (pingStartTime - serverTime) + (this.roundTripDuration / 2); - - // Initial setup - if (this.initTimeDiff === 0) { - this.timeDiff = newTimeDiff; - this.initTimeDiff++ - return; - } - - // As response time gets better, absolute value should decrease - var distanceFromZero = Math.abs(newTimeDiff); - var oldDistanceFromZero = Math.abs(this.timeDiff); - if (distanceFromZero < oldDistanceFromZero) { - this.timeDiff = newTimeDiff; - } - - // Avoid overloading server - if (this.initTimeDiff >= GreedyPingCount) { - this.pingIntervalTimeout = PingIntervalTimeoutLowProfile; - } else { - this.initTimeDiff++; - } - - // console.debug("Syncplay updateTimeDiff:", serverTime, this.timeDiff, this.roundTripDuration, newTimeDiff); - } - - /** - * Schedules a ping request to the server. Used to compute time difference between client and server. - */ - requestPing () { - if (this.pingInterval === null && !this.pingStop) { - this.pingInterval = setTimeout(() => { - this.pingInterval = null; - - var apiClient = connectionManager.currentApiClient(); - var sessionId = getActivePlayerId(); - - if (!sessionId) { - this.signalError(); - toast({ - // TODO: translate - text: "Syncplay error occured." - }); - return; - } - - var pingStartTime = new Date(); - apiClient.sendSyncplayCommand(sessionId, "GetUtcTime").then((response) => { - var pingEndTime = new Date(); - response.text().then((utcTime) => { - var serverTime = new Date(utcTime); - this.updateTimeDiff(pingStartTime, pingEndTime, serverTime); - - // Alert user that ping is high - if (Math.abs(this.roundTripDuration) >= 1000) { - events.trigger(this, "SyncplayError", [true]); - } else { - events.trigger(this, "SyncplayError", [false]); - } - - // Notify server of ping - apiClient.sendSyncplayCommand(sessionId, "KeepAlive", { - Ping: this.roundTripDuration / 2 - }); - - if (this.notifySyncplayReady) { - this.syncplayReady = true; - events.trigger(this, "SyncplayReady"); - this.notifySyncplayReady = false; - } - - this.requestPing(); - }); - }).catch((error) => { - console.error(error); - this.signalError(); - toast({ - // TODO: translate - text: "Syncplay error occured." - }); - }); - - }, this.pingIntervalTimeout); - } - } - - /** - * Starts the keep alive poller. - */ - startPing () { - this.notifySyncplayReady = true; - this.pingStop = false; - this.initTimeDiff = this.initTimeDiff > this.greedyPingCount ? 1 : this.initTimeDiff; - this.pingIntervalTimeout = PingIntervalTimeoutGreedy; - - this.requestPing(); - } - - /** - * Stops the keep alive poller. - */ - stopPing () { - this.pingStop = true; - if (this.pingInterval !== null) { - clearTimeout(this.pingInterval); - this.pingInterval = null; - } - } - /** * Attempts to sync playback time with estimated server time. * @@ -722,7 +612,7 @@ class SyncplayManager { const CurrentPositionTicks = playbackManager.currentTime(); // Estimate PositionTicks on server - const ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) - this.timeDiff) * 10000; + const ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000; // Measure delay that needs to be recovered // diff might be caused by the player internally starting the playback const diff = ServerPositionTicks - CurrentPositionTicks; @@ -848,33 +738,13 @@ class SyncplayManager { } } - /** - * Converts server time to local time. - * @param {Date} server The time to convert. - * @returns {Date} Local time. - */ - serverDateToLocal (server) { - // local - server = diff - return new Date(server.getTime() + this.timeDiff); - } - - /** - * Converts local time to server time. - * @param {Date} local The time to convert. - * @returns {Date} Server time. - */ - localDateToServer (local) { - // local - server = diff - return new Date(local.getTime() - this.timeDiff); - } - /** * Gets Syncplay stats. * @returns {Object} The Syncplay stats. */ getStats () { return { - TimeDiff: this.timeDiff, + TimeOffset: this.timeOffsetWithServer, PlaybackDiff: this.playbackDiffMillis, SyncMethod: this.syncMethod } diff --git a/src/components/syncplay/timeSyncManager.js b/src/components/syncplay/timeSyncManager.js new file mode 100644 index 000000000..e526aa5fa --- /dev/null +++ b/src/components/syncplay/timeSyncManager.js @@ -0,0 +1,209 @@ +/* eslint-disable indent */ + +/** + * Module that manages time syncing with server. + * @module components/syncplay/timeSyncManager + */ + +import events from 'events'; +import connectionManager from 'connectionManager'; + +/** + * Time estimation + */ +const NumberOfTrackedMeasurements = 8; +const PollingIntervalGreedy = 1000; // milliseconds +const PollingIntervalLowProfile = 60000; // milliseconds +const GreedyPingCount = 3; + +/** + * Class that stores measurement data. + */ +class Measurement { + /** + * Creates a new measurement. + * @param {Date} t0 Client's timestamp of the request transmission + * @param {Date} t1 Server's timestamp of the request reception + * @param {Date} t2 Server's timestamp of the response transmission + * @param {Date} t3 Client's timestamp of the response reception + */ + constructor(t0, t1, t2, t3) { + this.t0 = t0.getTime(); + this.t1 = t1.getTime(); + this.t2 = t2.getTime(); + this.t3 = t3.getTime(); + } + + /** + * Time offset from server. + */ + getOffset () { + return ((this.t1 - this.t0) + (this.t2 - this.t3)) / 2; + } + + /** + * Get round-trip delay. + */ + getDelay () { + return (this.t3 - this.t0) - (this.t2 - this.t1); + } + + /** + * Get ping time. + */ + getPing () { + return this.getDelay() / 2; + } +} + +/** + * Class that manages time syncing with server. + */ +class TimeSyncManager { + constructor() { + this.pingStop = true; + this.pollingInterval = PollingIntervalGreedy; + this.poller = null; + this.pings = 0; // number of pings + this.measurement = null; // current time sync + this.measurements = []; + + this.startPing(); + } + + /** + * Gets status of time sync. + * @returns {boolean} _true_ if a measurement has been done, _false_ otherwise. + */ + isReady() { + return this.measurement ? true : false; + } + + /** + * Gets time offset with server. + * @returns {number} The time offset. + */ + getTimeOffset () { + return this.measurement ? this.measurement.getOffset() : 0; + } + + /** + * Gets ping time to server. + * @returns {number} The ping time. + */ + getPing () { + return this.measurement ? this.measurement.getPing() : 0; + } + + /** + * Updates time offset between server and client. + * @param {Measurement} measurement The new measurement. + */ + updateTimeOffset(measurement) { + this.measurements.push(measurement); + if (this.measurements.length > NumberOfTrackedMeasurements) { + this.measurements.shift(); + } + + // Pick measurement with minimum delay + const sortedMeasurements = this.measurements.slice(0); + sortedMeasurements.sort((a, b) => a.getDelay() - b.getDelay()); + this.measurement = sortedMeasurements[0]; + } + + /** + * Schedules a ping request to the server. Triggers time offset update. + */ + requestPing() { + if (!this.poller) { + this.poller = setTimeout(() => { + this.poller = null; + const apiClient = connectionManager.currentApiClient(); + const t0 = new Date(); // pingStartTime + apiClient.getServerTime().then((response) => { + const t3 = new Date(); // pingEndTime + response.json().then((data) => { + const t1 = new Date(data.RequestReceptionTime); // request received + const t2 = new Date(data.ResponseTransmissionTime); // response sent + + const measurement = new Measurement(t0, t1, t2, t3); + this.updateTimeOffset(measurement); + + // Avoid overloading server + if (this.pings >= GreedyPingCount) { + this.pollingInterval = PollingIntervalLowProfile; + } else { + this.pings++; + } + + events.trigger(this, "Update", [this.getTimeOffset(), this.getPing()]); + }); + }).catch((error) => { + console.error(error); + events.trigger(this, "Error", [error]); + }).finally(() => { + this.requestPing(); + }); + + }, this.pollingInterval); + } + } + + /** + * Drops accumulated measurements. + */ + resetMeasurements () { + this.measurement = null; + this.measurements = []; + } + + /** + * Starts the time poller. + */ + startPing() { + this.requestPing(); + } + + /** + * Stops the time poller. + */ + stopPing() { + if (this.poller) { + clearTimeout(this.poller); + this.poller = null; + } + } + + /** + * Resets poller into greedy mode. + */ + forceUpdate() { + this.stopPing(); + this.pollingInterval = PollingIntervalGreedy; + this.pings = 0; + this.startPing(); + } + + /** + * Converts server time to local time. + * @param {Date} server The time to convert. + * @returns {Date} Local time. + */ + serverDateToLocal(server) { + // server - local = offset + return new Date(server.getTime() + this.getTimeOffset()); + } + + /** + * Converts local time to server time. + * @param {Date} local The time to convert. + * @returns {Date} Server time. + */ + localDateToServer(local) { + // server - local = offset + return new Date(local.getTime() - this.getTimeOffset()); + } +} + +/** TimeSyncManager singleton. */ +export default new TimeSyncManager(); diff --git a/src/scripts/site.js b/src/scripts/site.js index 3954b153a..ecfeb2734 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -824,6 +824,7 @@ var AppInfo = {}; define('playbackSettings', [componentsPath + '/playbacksettings/playbacksettings'], returnFirstDependency); define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency); define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager); + define('timeSyncManager', [componentsPath + '/syncplay/timeSyncManager'], returnDefault); define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnDefault); define('playbackPermissionManager', [componentsPath + '/syncplay/playbackPermissionManager'], returnDefault); define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 136a01543..43b5f1414 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -855,7 +855,7 @@ "LabelSubtitlePlaybackMode": "Subtitle mode:", "LabelSubtitles": "Subtitles", "LabelSupportedMediaTypes": "Supported Media Types:", - "LabelSyncplayTimeDiff": "Time difference with server:", + "LabelSyncplayTimeOffset": "Time offset with server:", "LabelSyncplayPlaybackDiff": "Playback time difference:", "LabelSyncplaySyncMethod": "Sync method:", "LabelSyncplayNewGroup": "New group", From 9839dcd02ad8d46ef19ad6b24b859552e816dd65 Mon Sep 17 00:00:00 2001 From: gion Date: Fri, 17 Apr 2020 13:42:46 +0200 Subject: [PATCH 0038/2481] Update session ping --- package.json | 1 + src/components/syncplay/syncplayManager.js | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 19d3b05a4..33a7d2b00 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "src/components/scrollManager.js", "src/components/syncplay/playbackPermissionManager.js", "src/components/syncplay/groupSelectionMenu.js", + "src/components/syncplay/timeSyncManager.js", "src/components/syncplay/syncplayManager.js", "src/scripts/dfnshelper.js", "src/scripts/dom.js", diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js index de1424b89..167f79c2a 100644 --- a/src/components/syncplay/syncplayManager.js +++ b/src/components/syncplay/syncplayManager.js @@ -99,7 +99,7 @@ class SyncplayManager { this.syncPlaybackTime(); }); - events.on(timeSyncManager, "Update", (event, timeOffset, ping) => { + events.on(timeSyncManager, "Update", (event, timeOffset, ping) => { this.timeOffsetWithServer = timeOffset; this.roundTripDuration = ping * 2; @@ -108,6 +108,25 @@ class SyncplayManager { events.trigger(this, "SyncplayReady"); this.notifySyncplayReady = false; } + + // Report ping + if (this.syncEnabled) { + const apiClient = connectionManager.currentApiClient(); + const sessionId = getActivePlayerId(); + + if (!sessionId) { + this.signalError(); + toast({ + // TODO: translate + text: "Syncplay error occured." + }); + return; + } + + apiClient.sendSyncplayCommand(sessionId, "UpdatePing", { + Ping: ping + }); + } }); } From 4eedbe57427d43a96ca78f2ca27b4ff043ec8a1c Mon Sep 17 00:00:00 2001 From: gion Date: Fri, 17 Apr 2020 19:41:02 +0200 Subject: [PATCH 0039/2481] Fix sign in date conversion --- src/components/syncplay/timeSyncManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/syncplay/timeSyncManager.js b/src/components/syncplay/timeSyncManager.js index e526aa5fa..74c98820c 100644 --- a/src/components/syncplay/timeSyncManager.js +++ b/src/components/syncplay/timeSyncManager.js @@ -191,7 +191,7 @@ class TimeSyncManager { */ serverDateToLocal(server) { // server - local = offset - return new Date(server.getTime() + this.getTimeOffset()); + return new Date(server.getTime() - this.getTimeOffset()); } /** @@ -201,7 +201,7 @@ class TimeSyncManager { */ localDateToServer(local) { // server - local = offset - return new Date(local.getTime() - this.getTimeOffset()); + return new Date(local.getTime() + this.getTimeOffset()); } } From 71f72f836c508acb758bd18f06d43e4d9bf5857a Mon Sep 17 00:00:00 2001 From: gion Date: Mon, 20 Apr 2020 20:03:27 +0200 Subject: [PATCH 0040/2481] Remove syncplay worker This was an attempt to fix issues on Safari --- src/components/syncplay/syncplayManager.js | 78 ------------------- src/workers/syncplay/syncplay.worker.js | 90 ---------------------- webpack.dev.js | 4 - webpack.prod.js | 4 - 4 files changed, 176 deletions(-) delete mode 100644 src/workers/syncplay/syncplay.worker.js diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js index 167f79c2a..0ca7c1314 100644 --- a/src/components/syncplay/syncplayManager.js +++ b/src/components/syncplay/syncplayManager.js @@ -89,10 +89,6 @@ class SyncplayManager { this.onPlayerChange(); }); - events.on(playbackManager, "playbackstart", (player, state) => { - events.trigger(this, 'PlaybackStart', [player, state]); - }); - this.bindToPlayer(playbackManager.getCurrentPlayer()); events.on(this, "TimeUpdate", (event) => { @@ -359,7 +355,6 @@ class SyncplayManager { startIndex: sessionData.StartIndex, serverId: serverId }).then(() => { - // TODO: switch to PlaybackStart maybe? waitForEvent(this, "PlayerChange").then(() => { playbackManager.pause(); var sessionId = getActivePlayerId(); @@ -435,7 +430,6 @@ class SyncplayManager { this.syncEnabled = false; events.trigger(this, "SyncplayEnabled", [false]); this.restorePlaybackManager(); - this.stopSyncWatcher(); if (showMessage) { toast({ @@ -471,7 +465,6 @@ class SyncplayManager { this.syncTimeout = setTimeout(() => { this.syncEnabled = true; - this.startSyncWatcher(); }, SyncMethodThreshold / 2); }, playTimeout); @@ -485,7 +478,6 @@ class SyncplayManager { this.syncTimeout = setTimeout(() => { this.syncEnabled = true; - this.startSyncWatcher(); }, SyncMethodThreshold / 2); } } @@ -535,7 +527,6 @@ class SyncplayManager { clearTimeout(this.syncTimeout); this.syncEnabled = false; - this.stopSyncWatcher(); if (this.currentPlayer) { this.currentPlayer.setPlaybackRate(1); } @@ -625,7 +616,6 @@ class SyncplayManager { const elapsed = currentTime - this.lastSyncTime; if (elapsed < SyncMethodThreshold / 2) return; this.lastSyncTime = currentTime; - this.notifySyncWatcher(); const playAtTime = this.lastCommand.When; @@ -689,74 +679,6 @@ class SyncplayManager { } } - /** - * Signals the worker to start watching sync. Also creates the worker if needed. - * - * This additional fail-safe has been added because on Safari the timeupdate event fails after a while. - */ - startSyncWatcher () { - // SPOILER ALERT: this idea fails too on Safari... Keeping it here for future investigations - return; - if (window.Worker) { - // Start worker if needed - if (!this.worker) { - this.worker = new Worker("workers/syncplay/syncplay.worker.js"); - this.worker.onmessage = (event) => { - const message = event.data; - switch (message.type) { - case "TriggerSync": - // TODO: player state might not reflect the real playback position, - // thus calling syncPlaybackTime outside a timeupdate event might not really sync to the right point - this.syncPlaybackTime(); - break; - default: - console.error("Syncplay: unknown message from worker:", message.type); - break; - } - }; - this.worker.onerror = (event) => { - console.error("Syncplay: worker error", event); - }; - this.worker.onmessageerror = (event) => { - console.error("Syncplay: worker message error", event); - }; - } - // Start watcher - this.worker.postMessage({ - type: "StartSyncWatcher", - data: { - interval: SyncMethodThreshold / 2, - threshold: SyncMethodThreshold - } - }); - } else { - console.debug("Syncplay: workers not supported."); - } - } - - /** - * Signals the worker to stop watching sync. - */ - stopSyncWatcher () { - if (this.worker) { - this.worker.postMessage({ - type: "StopSyncWatcher" - }); - } - } - - /** - * Signals new state to worker. - */ - notifySyncWatcher () { - if (this.worker) { - this.worker.postMessage({ - type: "UpdateLastSyncTime", - data: this.lastSyncTime - }); - } - } - /** * Gets Syncplay stats. * @returns {Object} The Syncplay stats. diff --git a/src/workers/syncplay/syncplay.worker.js b/src/workers/syncplay/syncplay.worker.js deleted file mode 100644 index bd8d2bd60..000000000 --- a/src/workers/syncplay/syncplay.worker.js +++ /dev/null @@ -1,90 +0,0 @@ -var SyncTimeThreshold = 2000; // milliseconds, overwritten by startSyncWatcher -var SyncWatcherInterval = 1000; // milliseconds, overwritten by startSyncWatcher -var lastSyncTime = new Date(); // internal state -var syncWatcher; // holds value from setInterval - -/** - * Sends a message to the UI worker. - * @param {string} type - * @param {*} data - */ -function sendMessage (type, data) { - postMessage({ - type: type, - data: data - }); - -} - -/** - * Updates the state. - * @param {Date} syncTime The new state. - */ -function updateLastSyncTime (syncTime) { - lastSyncTime = syncTime; -} - -/** - * Starts sync watcher. - * @param {Object} options Additional options to configure the watcher, like _interval_ and _threshold_. - */ -function startSyncWatcher(options) { - stopSyncWatcher(); - if (options) { - if (options.interval) { - SyncWatcherInterval = options.interval; - } - if (options.threshold) { - SyncTimeThreshold = options.threshold; - } - } - syncWatcher = setInterval(syncWatcherCallback, SyncWatcherInterval); -} - -/** - * Stops sync watcher. - */ -function stopSyncWatcher () { - if (syncWatcher) { - clearInterval(syncWatcher); - syncWatcher = null; - } -} - -/** - * Oversees playback sync and makes sure that it gets called regularly. - */ -function syncWatcherCallback () { - const currentTime = new Date(); - const elapsed = currentTime - lastSyncTime; - if (elapsed > SyncTimeThreshold) { - sendMessage("TriggerSync"); - } -} - -/** - * Handles messages from UI worker. - * @param {MessageEvent} event The message to handle. - */ -function handleMessage (event) { - const message = event.data; - switch (message.type) { - case "UpdateLastSyncTime": - updateLastSyncTime(message.data); - break; - case "StartSyncWatcher": - startSyncWatcher(message.data); - break; - case "StopSyncWatcher": - stopSyncWatcher(); - break; - default: - console.error("Unknown message type:", message.type); - break; - } -} - -// Listen for messages -addEventListener("message", function (event) { - handleMessage(event); -}); diff --git a/webpack.dev.js b/webpack.dev.js index 544d32d63..d8879fe80 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -48,10 +48,6 @@ module.exports = merge(common, { { test: /\.(wav)$/i, use: ["file-loader"] - }, - { - test: /\.worker.js$/, - use: ["worker"] } ] } diff --git a/webpack.prod.js b/webpack.prod.js index 7b5f2ff6c..cc4c57b9f 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -41,10 +41,6 @@ module.exports = merge(common, { { test: /\.(wav)$/i, use: ["file-loader"] - }, - { - test: /\.worker.js$/, - use: ["worker"] } ] } From a2ba96ab820c5b3e8d3050e77952d48eee2be776 Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 22 Apr 2020 22:48:26 +0200 Subject: [PATCH 0041/2481] Handle error messages --- src/components/syncplay/groupSelectionMenu.js | 4 ++-- src/components/syncplay/syncplayManager.js | 20 ++++++++++++++++++- src/strings/en-us.json | 7 +++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js index c907d338a..be54d9221 100644 --- a/src/components/syncplay/groupSelectionMenu.js +++ b/src/components/syncplay/groupSelectionMenu.js @@ -57,7 +57,7 @@ function showNewJoinGroupSelection (button, user, apiClient) { icon: "group", id: group.GroupId, selected: false, - secondaryText: group.Partecipants.join(", ") + secondaryText: group.Participants.join(", ") }; }); @@ -74,7 +74,7 @@ function showNewJoinGroupSelection (button, user, apiClient) { if (menuItems.length === 0) { if (inSession && policy.SyncplayAccess === "JoinGroups") { toast({ - text: globalize.translate('MessageSyncplayPermissionRequired') + text: globalize.translate('MessageSyncplayCreateGroupDenied') }); } else { toast({ diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js index 0ca7c1314..0de232108 100644 --- a/src/components/syncplay/syncplayManager.js +++ b/src/components/syncplay/syncplayManager.js @@ -274,7 +274,25 @@ class SyncplayManager { text: globalize.translate('MessageSyncplayGroupWait', cmd.Data) }); break; - case 'KeepAlive': + case 'GroupNotJoined': + toast({ + text: globalize.translate('MessageSyncplayGroupNotJoined', cmd.Data) + }); + break; + case 'CreateGroupDenied': + toast({ + text: globalize.translate('MessageSyncplayCreateGroupDenied', cmd.Data) + }); + break; + case 'JoinGroupDenied': + toast({ + text: globalize.translate('MessageSyncplayJoinGroupDenied', cmd.Data) + }); + break; + case 'LibraryAccessDenied': + toast({ + text: globalize.translate('MessageSyncplayLibraryAccessDenied', cmd.Data) + }); break; default: console.error('processSyncplayGroupUpdate does not recognize: ' + cmd.Type); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 43b5f1414..fb72e8881 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1034,9 +1034,12 @@ "MessageSyncplayUserJoined": "{0} joined group.", "MessageSyncplayUserLeft": "{0} left group.", "MessageSyncplayGroupWait": "{0} is buffering...", - "MessageSyncplayNoGroupsAvailable": "No groups available.", - "MessageSyncplayPermissionRequired": "Permission required to create a group.", + "MessageSyncplayNoGroupsAvailable": "No groups available. Start playing something first.", "MessageSyncplayPlaybackPermissionRequired": "Playback permission required.", + "MessageSyncplayGroupNotJoined": "Failed to join requested group.", + "MessageSyncplayCreateGroupDenied": "Permission required to create a group.", + "MessageSyncplayJoinGroupDenied": "Permission required to use Syncplay.", + "MessageSyncplayLibraryAccessDenied": "Access to this content is restricted.", "Metadata": "Metadata", "MetadataManager": "Metadata Manager", "MetadataSettingChangeHelp": "Changing metadata settings will affect new content that is added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.", From 11f6217bb22072b3196b1406ad2b321f01ea3839 Mon Sep 17 00:00:00 2001 From: gion Date: Tue, 5 May 2020 12:01:43 +0200 Subject: [PATCH 0042/2481] Fix code issues --- gulpfile.js | 2 +- src/assets/audio/silence.mp3 | Bin 0 -> 4890 bytes src/assets/audio/silence.wav | Bin 88244 -> 0 bytes src/components/htmlaudioplayer/plugin.js | 4 +- src/components/htmlvideoplayer/plugin.js | 4 +- src/components/playback/playbackmanager.js | 19 +- src/components/playerstats/playerstats.js | 10 +- src/components/serverNotifications.js | 4 +- src/components/syncplay/groupSelectionMenu.js | 77 ++-- .../syncplay/playbackPermissionManager.js | 4 +- src/components/syncplay/syncplayManager.js | 356 +++++++++++------- src/components/syncplay/timeSyncManager.js | 40 +- src/scripts/librarymenu.js | 36 +- src/scripts/site.js | 5 +- src/strings/en-us.json | 13 +- webpack.dev.js | 4 +- webpack.prod.js | 4 +- 17 files changed, 340 insertions(+), 242 deletions(-) create mode 100644 src/assets/audio/silence.mp3 delete mode 100644 src/assets/audio/silence.wav diff --git a/gulpfile.js b/gulpfile.js index ad77d9a67..538497d4d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -45,7 +45,7 @@ const options = { query: ['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'] }, copy: { - query: ['src/**/*.json', 'src/**/*.ico', 'src/**/*.wav'] + query: ['src/**/*.json', 'src/**/*.ico', 'src/**/*.mp3'] }, injectBundle: { query: 'src/index.html' diff --git a/src/assets/audio/silence.mp3 b/src/assets/audio/silence.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..29dbef21856b7e607cab432056a8fa10315b496b GIT binary patch literal 4890 zcmd^?dr(wW9LLXkeKcmi5QiKgf>i+va1=&`l&2ssmzRQ~IJBh2q(cfi4$8`^VHm#2 zVH|Q%gjT^oCDM3ZaZ>UD>H?xck`=7DlMPT^VsGc}VXr^D?6O$>)tzDQ-QPL;``qvE z{C>Z4V0#bLpck*G_cyG=wh8#p4k5E?Xw)c6OIur8dwVA*Cl?ntH#bjDKR>_V;PCM9 z=;+wk*!cK_goMPzEnBv1-=3D1mY$xOnR(<$c6RoO6Q@s~K6|#Lq@=8@qN1Xvrna`W zuCAe>;o-x!wzl?mjYiYe#V`!ZVwQW5XRz;F$2rcc8A zKk$FKv%M2*5LJalTCCU_p~Y1Qjby>q_eRT6K5&uz1!l0FZ27B$OyTEp4=hdud%2#o z2*q;&9x!8&FA$=v49SZa@4$1E|JwBrU4oa?mP#9w?U-Uq-sK>o1~CB_tTts;k=>gE zEUJC24!ccu34i)vef`>;&5Ba-$I$AXIe4KF74P;Eum0}!^q`UxhZX~CLHKQZ$1D5FCcdWP z*M|!a$9cr->0cj%anlU|ugt=Yo0RW1#EhR{F?mEQ7+}{d73M()Rs9#KpA|AG4x6#K zL0{k2#)T=&=(A>G2XAO^x^>xtk#`I9bWpTZq|1Y)L*xM$fah1fQt{6GM&HL0apK13 zR<;*m<2Dec)hgDMj%i7rJ_n#!hK=y&Ir=)qdL%?o zplSK01TK3ToBVHPr3Zx^odv9H-{-4&4)@wz9&}Q40nxKUrVF28Jrqoz#)+)BxVe*!Zs_{yY}F*Ru6CNe;K1I@N&q_J9e8hN z+-xh6^P`JAN3Z9Joc@Dw0*v)h;7*l$%-#2^;{1l`W2%ciAH{;99(A67(oO%Qs(2xX zqU)&XoSu*&^9^`pRh@C(3%oYGk_wCpS{b=5bc*ZfvBG}PE~k$ZJswD0g>gpX2Ar}_ z?@al^Y8kW?-0|Jz=o(E+y3GZ8Fy|YQHTI9(khBWl4CjGv1p4j1F!nc%@Aa@HsleLj zy(yFt_8pvWDJ0+4AYFemz!5DbzL>7w@9|UT;iTD@-nv$nO02q1ChT&Yv~m3uXNg0e zKo9OaH?QJ+TTvBbVx7XSz^a-uT+Fioj6E5faJ}+~fTN+VNJt(;kUBM>hc`o?y%j5K z@`0gMrBr<=_MO=a0(YWD3h-#U7ez`<4hL}3m=jk4_SEC9ec@7r^ZvVl0BFH&0z_dJo z944xGWxz+N&=k<<^Pu^l4bRcX4X@8UvY+~q^)apoy43EG{Hd-4mit@p2i~=sHg%@O zT%M!XgX`xw(Gy}?KOpGSk(;Qx*DaDN;567M_IPyPn!oDb;$ literal 0 HcmV?d00001 diff --git a/src/assets/audio/silence.wav b/src/assets/audio/silence.wav deleted file mode 100644 index 63f253da845e5177e59bcbc05a49ecb118f5e97a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88244 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 Sj#^Qp#u#I*axXE^2mT*$^iC20 diff --git a/src/components/htmlaudioplayer/plugin.js b/src/components/htmlaudioplayer/plugin.js index 258048105..672bd06b8 100644 --- a/src/components/htmlaudioplayer/plugin.js +++ b/src/components/htmlaudioplayer/plugin.js @@ -520,8 +520,8 @@ define(['events', 'browser', 'require', 'apphost', 'appSettings', 'htmlMediaHelp var list = []; var audio = document.createElement('audio'); - if (typeof audio.playbackRate === "number") { - list.push("PlaybackRate"); + if (typeof audio.playbackRate === 'number') { + list.push('PlaybackRate'); } return list; diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js index 064e4155e..60f39c5ec 100644 --- a/src/components/htmlvideoplayer/plugin.js +++ b/src/components/htmlvideoplayer/plugin.js @@ -1442,8 +1442,8 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa list.push('AirPlay'); } - if (typeof video.playbackRate === "number") { - list.push("PlaybackRate"); + if (typeof video.playbackRate === 'number') { + list.push('PlaybackRate'); } list.push('SetBrightness'); diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index e4ce40cf4..ec0ee4140 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -54,6 +54,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla if (!serverId) { // Not a server item // We can expand on this later and possibly report them + events.trigger(playbackManagerInstance, 'reportplayback', [false]); return; } @@ -77,7 +78,11 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla } var apiClient = connectionManager.getApiClient(serverId); - apiClient[method](info); + var reportPlaybackPromise = apiClient[method](info); + // Notify that report has been sent + reportPlaybackPromise.then(() => { + events.trigger(playbackManagerInstance, 'reportplayback', [true]); + }); } function getPlaylistSync(playbackManagerInstance, player) { @@ -3777,18 +3782,14 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla } }; - PlaybackManager.prototype.setPlaybackRate = function (value, player) { - player = player || this._currentPlayer; - - if (player) { + PlaybackManager.prototype.setPlaybackRate = function (value, player = this._currentPlayer) { + if (player && player.setPlaybackRate) { player.setPlaybackRate(value); } }; - PlaybackManager.prototype.getPlaybackRate = function (player) { - player = player || this._currentPlayer; - - if (player) { + PlaybackManager.prototype.getPlaybackRate = function (player = this._currentPlayer) { + if (player && player.getPlaybackRate) { return player.getPlaybackRate(); } diff --git a/src/components/playerstats/playerstats.js b/src/components/playerstats/playerstats.js index 404baab7e..07fcd7070 100644 --- a/src/components/playerstats/playerstats.js +++ b/src/components/playerstats/playerstats.js @@ -332,17 +332,17 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplay var stats = syncplayManager.getStats(); syncStats.push({ - label: globalize.translate("LabelSyncplayTimeOffset"), - value: stats.TimeOffset + "ms" + label: globalize.translate('LabelSyncplayTimeOffset'), + value: stats.TimeOffset + globalize.translate('MillisecondsUnit') }); syncStats.push({ - label: globalize.translate("LabelSyncplayPlaybackDiff"), - value: stats.PlaybackDiff + "ms" + label: globalize.translate('LabelSyncplayPlaybackDiff'), + value: stats.PlaybackDiff + globalize.translate('MillisecondsUnit') }); syncStats.push({ - label: globalize.translate("LabelSyncplaySyncMethod"), + label: globalize.translate('LabelSyncplaySyncMethod'), value: stats.SyncMethod }); diff --git a/src/components/serverNotifications.js b/src/components/serverNotifications.js index 9776c88bd..876c3f7e7 100644 --- a/src/components/serverNotifications.js +++ b/src/components/serverNotifications.js @@ -187,9 +187,9 @@ define(['connectionManager', 'playbackManager', 'syncplayManager', 'events', 'in events.trigger(serverNotifications, 'UserDataChanged', [apiClient, msg.Data.UserDataList[i]]); } } - } else if (msg.MessageType === "SyncplayCommand") { + } else if (msg.MessageType === 'SyncplayCommand') { syncplayManager.processCommand(msg.Data, apiClient); - } else if (msg.MessageType === "SyncplayGroupUpdate") { + } else if (msg.MessageType === 'SyncplayGroupUpdate') { syncplayManager.processGroupUpdate(msg.Data, apiClient); } else { events.trigger(serverNotifications, msg.MessageType, [apiClient, msg.Data]); diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js index be54d9221..af08f9277 100644 --- a/src/components/syncplay/groupSelectionMenu.js +++ b/src/components/syncplay/groupSelectionMenu.js @@ -3,7 +3,6 @@ import connectionManager from 'connectionManager'; import playbackManager from 'playbackManager'; import syncplayManager from 'syncplayManager'; import loading from 'loading'; -import datetime from 'datetime'; import toast from 'toast'; import actionsheet from 'actionsheet'; import globalize from 'globalize'; @@ -18,13 +17,6 @@ function getActivePlayerId () { return info ? info.id : null; } -/** - * Used to avoid console logs about uncaught promises - */ -function emptyCallback () { - // avoid console logs about uncaught promises -} - /** * Used when user needs to join a group. * @param {HTMLElement} button - Element where to place the menu. @@ -32,47 +24,43 @@ function emptyCallback () { * @param {Object} apiClient - ApiClient. */ function showNewJoinGroupSelection (button, user, apiClient) { - let sessionId = getActivePlayerId(); - sessionId = sessionId ? sessionId : "none"; - const inSession = sessionId !== "none"; + const sessionId = getActivePlayerId() || 'none'; + const inSession = sessionId !== 'none'; const policy = user.localUser ? user.localUser.Policy : {}; let playingItemId; try { const playState = playbackManager.getPlayerState(); playingItemId = playState.NowPlayingItem.Id; + console.debug('Item', playingItemId, 'is currently playing.'); } catch (error) { - playingItemId = ""; + playingItemId = ''; + console.debug('No item is currently playing.'); } - apiClient.sendSyncplayCommand(sessionId, "ListGroups").then(function (response) { - response.json().then(function (groups) { + apiClient.sendSyncplayCommand(sessionId, 'ListGroups').then(function (response) { + response.json().then(function (groups) { var menuItems = groups.map(function (group) { - // TODO: update running time if group is playing? - var name = datetime.getDisplayRunningTime(group.PositionTicks); - if (!inSession) { - name = group.PlayingItemName; - } return { - name: name, - icon: "group", + name: group.PlayingItemName, + icon: 'group', id: group.GroupId, selected: false, - secondaryText: group.Participants.join(", ") + secondaryText: group.Participants.join(', ') }; }); - if (inSession && policy.SyncplayAccess === "CreateAndJoinGroups") { + if (inSession && policy.SyncplayAccess === 'CreateAndJoinGroups') { menuItems.push({ name: globalize.translate('LabelSyncplayNewGroup'), - icon: "add", - id: "new-group", + icon: 'add', + id: 'new-group', selected: true, secondaryText: globalize.translate('LabelSyncplayNewGroupDescription') }); } if (menuItems.length === 0) { - if (inSession && policy.SyncplayAccess === "JoinGroups") { + if (inSession && policy.SyncplayAccess === 'JoinGroups') { toast({ text: globalize.translate('MessageSyncplayCreateGroupDenied') }); @@ -94,15 +82,17 @@ function showNewJoinGroupSelection (button, user, apiClient) { }; actionsheet.show(menuOptions).then(function (id) { - if (id == "new-group") { - apiClient.sendSyncplayCommand(sessionId, "NewGroup"); + if (id == 'new-group') { + apiClient.sendSyncplayCommand(sessionId, 'NewGroup'); } else { - apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { + apiClient.sendSyncplayCommand(sessionId, 'JoinGroup', { GroupId: id, PlayingItemId: playingItemId }); } - }, emptyCallback); + }).catch((error) => { + console.error('Syncplay: unexpected error listing groups:', error); + }); loading.hide(); }); @@ -110,7 +100,7 @@ function showNewJoinGroupSelection (button, user, apiClient) { console.error(error); loading.hide(); toast({ - text: globalize.translate('MessageSyncplayNoGroupsAvailable') + text: globalize.translate('MessageSyncplayErrorAccessingGroups') }); }); } @@ -126,17 +116,16 @@ function showLeaveGroupSelection (button, user, apiClient) { if (!sessionId) { syncplayManager.signalError(); toast({ - // TODO: translate - text: "Syncplay error occured." + text: globalize.translate('MessageSyncplayErrorNoActivePlayer') }); + showNewJoinGroupSelection(button, user, apiClient); return; } - const menuItems = [{ name: globalize.translate('LabelSyncplayLeaveGroup'), - icon: "meeting_room", - id: "leave-group", + icon: 'meeting_room', + id: 'leave-group', selected: true, secondaryText: globalize.translate('LabelSyncplayLeaveGroupDescription') }]; @@ -150,17 +139,19 @@ function showLeaveGroupSelection (button, user, apiClient) { }; actionsheet.show(menuOptions).then(function (id) { - if (id == "leave-group") { - apiClient.sendSyncplayCommand(sessionId, "LeaveGroup"); + if (id == 'leave-group') { + apiClient.sendSyncplayCommand(sessionId, 'LeaveGroup'); } - }, emptyCallback); + }).catch((error) => { + console.error('Syncplay: unexpected error showing group menu:', error); + }); loading.hide(); } // Register to Syncplay events let syncplayEnabled = false; -events.on(syncplayManager, 'SyncplayEnabled', function (e, enabled) { +events.on(syncplayManager, 'enabled', function (e, enabled) { syncplayEnabled = enabled; }); @@ -173,11 +164,11 @@ export function show (button) { // TODO: should feature be disabled if playback permission is missing? playbackPermissionManager.check().then(() => { - console.debug("Playback is allowed."); + console.debug('Playback is allowed.'); }).catch((error) => { - console.error("Playback not allowed!", error); + console.error('Playback not allowed!', error); toast({ - text: globalize.translate("MessageSyncplayPlaybackPermissionRequired") + text: globalize.translate('MessageSyncplayPlaybackPermissionRequired') }); }); diff --git a/src/components/syncplay/playbackPermissionManager.js b/src/components/syncplay/playbackPermissionManager.js index df16545b3..3c258ad18 100644 --- a/src/components/syncplay/playbackPermissionManager.js +++ b/src/components/syncplay/playbackPermissionManager.js @@ -11,7 +11,7 @@ function createTestMediaElement () { document.body.appendChild(elem); elem.volume = 1; // Volume should not be zero to trigger proper permissions - elem.src = "assets/audio/silence.wav"; // Silent sound + elem.src = 'assets/audio/silence.mp3'; // Silent sound return elem; } @@ -30,7 +30,7 @@ function destroyTestMediaElement (elem) { */ class PlaybackPermissionManager { /** - * Tests playback permission. Grabs the permission when called inside a click event (or any other valid user interaction). + * Tests playback permission. Grabs the permission when called inside a click event (or any other valid user interaction). * @returns {Promise} Promise that resolves succesfully if playback permission is allowed. */ check () { diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncplayManager.js index 0de232108..b5694c88f 100644 --- a/src/components/syncplay/syncplayManager.js +++ b/src/components/syncplay/syncplayManager.js @@ -1,5 +1,3 @@ -/* eslint-disable indent */ - /** * Module that manages the Syncplay feature. * @module components/syncplay/syncplayManager @@ -13,15 +11,25 @@ import toast from 'toast'; import globalize from 'globalize'; /** - * Waits for an event to be triggered on an object. + * Waits for an event to be triggered on an object. An optional timeout can specified after which the promise is rejected. * @param {Object} emitter Object on which to listen for events. * @param {string} eventType Event name to listen for. + * @param {number} timeout Time in milliseconds before rejecting promise if event does not trigger. * @returns {Promise} A promise that resolves when the event is triggered. */ -function waitForEvent(emitter, eventType) { - return new Promise((resolve) => { - var callback = () => { +function waitForEventOnce(emitter, eventType, timeout) { + return new Promise((resolve, reject) => { + let rejectTimeout; + if (timeout) { + rejectTimeout = setTimeout(() => { + reject('Timed out.'); + }, timeout); + } + const callback = () => { events.off(emitter, eventType, callback); + if (rejectTimeout) { + clearTimeout(rejectTimeout); + } resolve(arguments); }; events.on(emitter, eventType, callback); @@ -33,7 +41,7 @@ function waitForEvent(emitter, eventType) { * @returns {string} The player's id. */ function getActivePlayerId() { - var info = playbackManager.getPlayerInfo(); + var info = playbackManager.getPlayerInfo(); return info ? info.id : null; } @@ -48,11 +56,10 @@ const MaxAttemptsSpeedToSync = 3; // attempts before disabling SpeedToSync const MaxAttemptsSync = 5; // attempts before disabling syncing at all /** - * Time estimation + * Other constants */ -const PingIntervalTimeoutGreedy = 1000; // milliseconds -const PingIntervalTimeoutLowProfile = 60000; // milliseconds -const GreedyPingCount = 3; +const WaitForEventDefaultTimeout = 30000; // milliseconds +const WaitForPlayerEventTimeout = 500; // milliseconds /** * Class that manages the Syncplay feature. @@ -62,7 +69,7 @@ class SyncplayManager { this.playbackRateSupported = false; this.syncEnabled = false; this.playbackDiffMillis = 0; // used for stats - this.syncMethod = "None"; // used for stats + this.syncMethod = 'None'; // used for stats this.syncAttempts = 0; this.lastSyncTime = new Date(); this.syncWatcherTimeout = null; // interval that watches playback time and syncs it @@ -71,6 +78,7 @@ class SyncplayManager { this.minBufferingThresholdMillis = 1000; this.currentPlayer = null; + this.localPlayerPlaybackRate = 1.0; // used to restore user PlaybackRate this.syncplayEnabledAt = null; // Server time of when Syncplay has been enabled this.syncplayReady = false; // Syncplay is ready after first ping to server @@ -84,24 +92,37 @@ class SyncplayManager { this.timeOffsetWithServer = 0; // server time minus local time this.roundTripDuration = 0; this.notifySyncplayReady = false; - - events.on(playbackManager, "playerchange", () => { + + events.on(playbackManager, 'playbackstart', (player, state) => { + this.onPlaybackStart(player, state); + }); + + events.on(playbackManager, 'playbackstop', (stopInfo) => { + this.onPlaybackStop(stopInfo); + }); + + events.on(playbackManager, 'playerchange', () => { this.onPlayerChange(); }); this.bindToPlayer(playbackManager.getCurrentPlayer()); - events.on(this, "TimeUpdate", (event) => { + events.on(this, 'timeupdate', (event) => { this.syncPlaybackTime(); }); - events.on(timeSyncManager, "Update", (event, timeOffset, ping) => { + events.on(timeSyncManager, 'update', (event, error, timeOffset, ping) => { + if (error) { + console.debug('Syncplay, time update issue', error); + return; + } + this.timeOffsetWithServer = timeOffset; this.roundTripDuration = ping * 2; if (this.notifySyncplayReady) { this.syncplayReady = true; - events.trigger(this, "SyncplayReady"); + events.trigger(this, 'ready'); this.notifySyncplayReady = false; } @@ -113,33 +134,55 @@ class SyncplayManager { if (!sessionId) { this.signalError(); toast({ - // TODO: translate - text: "Syncplay error occured." + text: globalize.translate('MessageSyncplayErrorMissingSession') }); return; } - apiClient.sendSyncplayCommand(sessionId, "UpdatePing", { + apiClient.sendSyncplayCommand(sessionId, 'UpdatePing', { Ping: ping }); } }); } + /** + * Called when playback starts. + */ + onPlaybackStart (player, state) { + events.trigger(this, 'playbackstart', [player, state]); + } + + /** + * Called when playback stops. + */ + onPlaybackStop (stopInfo) { + events.trigger(this, 'playbackstop', [stopInfo]); + if (this.isSyncplayEnabled()) { + this.disableSyncplay(false); + } + } + /** * Called when the player changes. */ onPlayerChange () { this.bindToPlayer(playbackManager.getCurrentPlayer()); - events.trigger(this, "PlayerChange", [this.currentPlayer]); + events.trigger(this, 'playerchange', [this.currentPlayer]); } /** - * Called on playback state changes. - * @param {Object} e The playback state change event. + * Called when playback unpauses. */ - onPlayPauseStateChanged (e) { - events.trigger(this, "PlayPauseStateChange", [this.currentPlayer]); + onPlayerUnpause () { + events.trigger(this, 'unpause', [this.currentPlayer]); + } + + /** + * Called when playback pauses. + */ + onPlayerPause() { + events.trigger(this, 'pause', [this.currentPlayer]); } /** @@ -149,7 +192,7 @@ class SyncplayManager { onTimeUpdate (e) { // NOTICE: this event is unreliable, at least in Safari // which just stops firing the event after a while. - events.trigger(this, "TimeUpdate", [e]); + events.trigger(this, 'timeupdate', [e]); } /** @@ -158,7 +201,7 @@ class SyncplayManager { onPlaying () { // TODO: implement group wait this.lastPlaybackWaiting = null; - events.trigger(this, "PlayerPlaying"); + events.trigger(this, 'playing'); } /** @@ -169,7 +212,7 @@ class SyncplayManager { if (!this.lastPlaybackWaiting) { this.lastPlaybackWaiting = new Date(); } - events.trigger(this, "PlayerWaiting"); + events.trigger(this, 'waiting'); } /** @@ -191,15 +234,18 @@ class SyncplayManager { this.currentPlayer = player; if (!player) return; } - - // TODO: remove this extra functions + + // FIXME: the following are needed because the 'events' module + // is changing the scope when executing the callbacks. + // For instance, calling 'onPlayerUnpause' from the wrong scope breaks things because 'this' + // points to 'player' (the event emitter) instead of pointing to the SyncplayManager singleton. const self = this; - this._onPlayPauseStateChanged = () => { - self.onPlayPauseStateChanged(); + this._onPlayerUnpause = () => { + self.onPlayerUnpause(); }; - this._onPlayPauseStateChanged = (e) => { - self.onPlayPauseStateChanged(e); + this._onPlayerPause = () => { + self.onPlayerPause(); }; this._onTimeUpdate = (e) => { @@ -214,12 +260,17 @@ class SyncplayManager { self.onWaiting(); }; - events.on(player, "pause", this._onPlayPauseStateChanged); - events.on(player, "unpause", this._onPlayPauseStateChanged); - events.on(player, "timeupdate", this._onTimeUpdate); - events.on(player, "playing", this._onPlaying); - events.on(player, "waiting", this._onWaiting); - this.playbackRateSupported = player.supports("PlaybackRate"); + events.on(player, 'unpause', this._onPlayerUnpause); + events.on(player, 'pause', this._onPlayerPause); + events.on(player, 'timeupdate', this._onTimeUpdate); + events.on(player, 'playing', this._onPlaying); + events.on(player, 'waiting', this._onWaiting); + this.playbackRateSupported = player.supports('PlaybackRate'); + + // Save player current PlaybackRate value + if (this.playbackRateSupported) { + this.localPlayerPlaybackRate = player.getPlaybackRate(); + } } /** @@ -228,13 +279,15 @@ class SyncplayManager { releaseCurrentPlayer () { var player = this.currentPlayer; if (player) { - events.off(player, "pause", this._onPlayPauseStateChanged); - events.off(player, "unpause", this._onPlayPauseStateChanged); - events.off(player, "timeupdate", this._onTimeUpdate); - events.off(player, "playing", this._onPlaying); - events.off(player, "waiting", this._onWaiting); + events.off(player, 'unpause', this._onPlayerUnpause); + events.off(player, 'pause', this._onPlayerPause); + events.off(player, 'timeupdate', this._onTimeUpdate); + events.off(player, 'playing', this._onPlaying); + events.off(player, 'waiting', this._onWaiting); + // Restore player original PlaybackRate value if (this.playbackRateSupported) { - player.setPlaybackRate(1); + player.setPlaybackRate(this.localPlayerPlaybackRate); + this.localPlayerPlaybackRate = 1.0; } this.currentPlayer = null; this.playbackRateSupported = false; @@ -262,8 +315,7 @@ class SyncplayManager { }); break; case 'GroupJoined': - const enabledAt = new Date(cmd.Data); - this.enableSyncplay(apiClient, enabledAt, true); + this.enableSyncplay(apiClient, new Date(cmd.Data), true); break; case 'NotInGroup': case 'GroupLeft': @@ -274,28 +326,28 @@ class SyncplayManager { text: globalize.translate('MessageSyncplayGroupWait', cmd.Data) }); break; - case 'GroupNotJoined': + case 'GroupDoesNotExist': toast({ - text: globalize.translate('MessageSyncplayGroupNotJoined', cmd.Data) + text: globalize.translate('MessageSyncplayGroupDoesNotExist') }); break; case 'CreateGroupDenied': toast({ - text: globalize.translate('MessageSyncplayCreateGroupDenied', cmd.Data) + text: globalize.translate('MessageSyncplayCreateGroupDenied') }); break; case 'JoinGroupDenied': toast({ - text: globalize.translate('MessageSyncplayJoinGroupDenied', cmd.Data) + text: globalize.translate('MessageSyncplayJoinGroupDenied') }); break; case 'LibraryAccessDenied': toast({ - text: globalize.translate('MessageSyncplayLibraryAccessDenied', cmd.Data) + text: globalize.translate('MessageSyncplayLibraryAccessDenied') }); break; default: - console.error('processSyncplayGroupUpdate does not recognize: ' + cmd.Type); + console.error('processSyncplayGroupUpdate: command is not recognised: ' + cmd.Type); break; } } @@ -309,12 +361,12 @@ class SyncplayManager { if (cmd === null) return; if (!this.isSyncplayEnabled()) { - console.debug("Syncplay processCommand: ignoring command", cmd); + console.debug('Syncplay processCommand: SyncPlay not enabled, ignoring command', cmd); return; } if (!this.syncplayReady) { - console.debug("Syncplay processCommand: queued command", cmd); + console.debug('Syncplay processCommand: SyncPlay not ready, queued command', cmd); this.queuedCommand = cmd; return; } @@ -323,7 +375,7 @@ class SyncplayManager { cmd.EmittedAt = new Date(cmd.EmitttedAt); if (cmd.EmitttedAt < this.syncplayEnabledAt) { - console.debug("Syncplay processCommand: ignoring old command", cmd); + console.debug('Syncplay processCommand: ignoring old command', cmd); return; } @@ -333,12 +385,12 @@ class SyncplayManager { this.lastCommand.PositionTicks === cmd.PositionTicks && this.Command === cmd.Command ) { - console.debug("Syncplay processCommand: ignoring duplicate command", cmd); + console.debug('Syncplay processCommand: ignoring duplicate command', cmd); return; } this.lastCommand = cmd; - console.log("Syncplay will", cmd.Command, "at", cmd.When, "PositionTicks", cmd.PositionTicks); + console.log('Syncplay will', cmd.Command, 'at', cmd.When, 'PositionTicks', cmd.PositionTicks); switch (cmd.Command) { case 'Play': @@ -351,7 +403,7 @@ class SyncplayManager { this.scheduleSeek(cmd.When, cmd.PositionTicks); break; default: - console.error('processSyncplayCommand does not recognize: ' + cmd.Type); + console.error('processCommand: command is not recognised: ' + cmd.Type); break; } } @@ -363,7 +415,7 @@ class SyncplayManager { * @param {Object} sessionData Info about the content to load. */ prepareSession (apiClient, groupId, sessionData) { - var serverId = apiClient.serverInfo().Id; + const serverId = apiClient.serverInfo().Id; playbackManager.play({ ids: sessionData.ItemIds, startPositionTicks: sessionData.StartPositionTicks, @@ -373,14 +425,12 @@ class SyncplayManager { startIndex: sessionData.StartIndex, serverId: serverId }).then(() => { - waitForEvent(this, "PlayerChange").then(() => { - playbackManager.pause(); + waitForEventOnce(this, 'playbackstart', WaitForEventDefaultTimeout).then(() => { var sessionId = getActivePlayerId(); if (!sessionId) { - console.error("Missing sessionId!"); + console.error('Missing sessionId!'); toast({ - // TODO: translate - text: "Failed to enable Syncplay! Missing session id." + text: globalize.translate('MessageSyncplayErrorMissingSession') }); return; } @@ -390,21 +440,38 @@ class SyncplayManager { const playState = playbackManager.getPlayerState(); playingItemId = playState.NowPlayingItem.Id; } catch (error) { - playingItemId = ""; + playingItemId = ''; } - // Sometimes JoinGroup fails, maybe because server hasn't been updated yet - setTimeout(() => { - apiClient.sendSyncplayCommand(sessionId, "JoinGroup", { + // Make sure the server has received the player state + waitForEventOnce(playbackManager, 'reportplayback', WaitForEventDefaultTimeout).then((success) => { + this.localPause(); + if (!success) { + console.warning('Error reporting playback state to server. Joining group will fail.'); + } + apiClient.sendSyncplayCommand(sessionId, 'JoinGroup', { GroupId: groupId, PlayingItemId: playingItemId }); - }, 500); + }).catch(() => { + console.error('Timed out while waiting for `reportplayback` event!'); + toast({ + text: globalize.translate('MessageSyncplayErrorMedia') + }); + return; + }); + }).catch(() => { + console.error('Timed out while waiting for `playbackstart` event!'); + if (!this.isSyncplayEnabled()) { + toast({ + text: globalize.translate('MessageSyncplayErrorMedia') + }); + } + return; }); }).catch((error) => { console.error(error); toast({ - // TODO: translate - text: "Failed to enable Syncplay! Media error." + text: globalize.translate('MessageSyncplayErrorMedia') }); }); } @@ -418,12 +485,13 @@ class SyncplayManager { enableSyncplay (apiClient, enabledAt, showMessage = false) { this.syncplayEnabledAt = enabledAt; this.injectPlaybackManager(); - events.trigger(this, "SyncplayEnabled", [true]); + events.trigger(this, 'enabled', [true]); - waitForEvent(this, "SyncplayReady").then(() => { + waitForEventOnce(this, 'ready').then(() => { this.processCommand(this.queuedCommand, apiClient); this.queuedCommand = null; }); + this.syncplayReady = false; this.notifySyncplayReady = true; @@ -446,7 +514,7 @@ class SyncplayManager { this.lastCommand = null; this.queuedCommand = null; this.syncEnabled = false; - events.trigger(this, "SyncplayEnabled", [false]); + events.trigger(this, 'enabled', [false]); this.restorePlaybackManager(); if (showMessage) { @@ -461,7 +529,7 @@ class SyncplayManager { * @returns {boolean} _true_ if user joined a group, _false_ otherwise. */ isSyncplayEnabled () { - return this.syncplayEnabledAt !== null ? true : false; + return this.syncplayEnabledAt !== null; } /** @@ -471,15 +539,15 @@ class SyncplayManager { */ schedulePlay (playAtTime, positionTicks) { this.clearScheduledCommand(); - var currentTime = new Date(); - var playAtTimeLocal = timeSyncManager.serverDateToLocal(playAtTime); + const currentTime = new Date(); + const playAtTimeLocal = timeSyncManager.serverDateToLocal(playAtTime); if (playAtTimeLocal > currentTime) { - var playTimeout = playAtTimeLocal - currentTime; - playbackManager.syncplay_seek(positionTicks); + const playTimeout = playAtTimeLocal - currentTime; + this.localSeek(positionTicks); this.scheduledCommand = setTimeout(() => { - playbackManager.syncplay_unpause(); + this.localUnpause(); this.syncTimeout = setTimeout(() => { this.syncEnabled = true; @@ -487,12 +555,14 @@ class SyncplayManager { }, playTimeout); - // console.debug("Syncplay schedulePlay:", playTimeout); + console.debug('Scheduled play in', playTimeout / 1000.0, 'seconds.'); } else { // Group playback already started - var serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000; - playbackManager.syncplay_unpause(); - playbackManager.syncplay_seek(serverPositionTicks); + const serverPositionTicks = positionTicks + (currentTime - playAtTimeLocal) * 10000; + waitForEventOnce(this, 'unpause').then(() => { + this.localSeek(serverPositionTicks); + }); + this.localUnpause(); this.syncTimeout = setTimeout(() => { this.syncEnabled = true; @@ -507,24 +577,26 @@ class SyncplayManager { */ schedulePause (pauseAtTime, positionTicks) { this.clearScheduledCommand(); - var currentTime = new Date(); - var pauseAtTimeLocal = timeSyncManager.serverDateToLocal(pauseAtTime); + const currentTime = new Date(); + const pauseAtTimeLocal = timeSyncManager.serverDateToLocal(pauseAtTime); + + const callback = () => { + waitForEventOnce(this, 'pause', WaitForPlayerEventTimeout).then(() => { + this.localSeek(positionTicks); + }).catch(() => { + // Player was already paused, seeking + this.localSeek(positionTicks); + }); + this.localPause(); + }; if (pauseAtTimeLocal > currentTime) { - var pauseTimeout = pauseAtTimeLocal - currentTime; + const pauseTimeout = pauseAtTimeLocal - currentTime; + this.scheduledCommand = setTimeout(callback, pauseTimeout); - this.scheduledCommand = setTimeout(() => { - playbackManager.syncplay_pause(); - setTimeout(() => { - playbackManager.syncplay_seek(positionTicks); - }, 800); - - }, pauseTimeout); + console.debug('Scheduled pause in', pauseTimeout / 1000.0, 'seconds.'); } else { - playbackManager.syncplay_pause(); - setTimeout(() => { - playbackManager.syncplay_seek(positionTicks); - }, 800); + callback(); } } @@ -558,10 +630,10 @@ class SyncplayManager { if (!this.isSyncplayEnabled()) return; if (playbackManager.syncplayEnabled) return; - // TODO: make this less hacky - playbackManager.syncplay_unpause = playbackManager.unpause; - playbackManager.syncplay_pause = playbackManager.pause; - playbackManager.syncplay_seek = playbackManager.seek; + // TODO: make this less hacky + playbackManager._localUnpause = playbackManager.unpause; + playbackManager._localPause = playbackManager.pause; + playbackManager._localSeek = playbackManager.seek; playbackManager.unpause = this.playRequest; playbackManager.pause = this.pauseRequest; @@ -576,9 +648,9 @@ class SyncplayManager { if (this.isSyncplayEnabled()) return; if (!playbackManager.syncplayEnabled) return; - playbackManager.unpause = playbackManager.syncplay_unpause; - playbackManager.pause = playbackManager.syncplay_pause; - playbackManager.seek = playbackManager.syncplay_seek; + playbackManager.unpause = playbackManager._localUnpause; + playbackManager.pause = playbackManager._localPause; + playbackManager.seek = playbackManager._localSeek; playbackManager.syncplayEnabled = false; } @@ -588,7 +660,7 @@ class SyncplayManager { playRequest (player) { var apiClient = connectionManager.currentApiClient(); var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, "PlayRequest"); + apiClient.sendSyncplayCommand(sessionId, 'PlayRequest'); } /** @@ -597,9 +669,9 @@ class SyncplayManager { pauseRequest (player) { var apiClient = connectionManager.currentApiClient(); var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, "PauseRequest"); + apiClient.sendSyncplayCommand(sessionId, 'PauseRequest'); // Pause locally as well, to give the user some little control - playbackManager.syncplay_pause(); + playbackManager._localUnpause(player); } /** @@ -608,14 +680,47 @@ class SyncplayManager { seekRequest (PositionTicks, player) { var apiClient = connectionManager.currentApiClient(); var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, "SeekRequest", { + apiClient.sendSyncplayCommand(sessionId, 'SeekRequest', { PositionTicks: PositionTicks }); } + /** + * Calls original PlaybackManager's unpause method. + */ + localUnpause(player) { + if (playbackManager.syncplayEnabled) { + playbackManager._localUnpause(player); + } else { + playbackManager.unpause(player); + } + } + + /** + * Calls original PlaybackManager's pause method. + */ + localPause(player) { + if (playbackManager.syncplayEnabled) { + playbackManager._localPause(player); + } else { + playbackManager.pause(player); + } + } + + /** + * Calls original PlaybackManager's seek method. + */ + localSeek(PositionTicks, player) { + if (playbackManager.syncplayEnabled) { + playbackManager._localSeek(PositionTicks, player); + } else { + playbackManager.seek(PositionTicks, player); + } + } + /** * Attempts to sync playback time with estimated server time. - * + * * When sync is enabled, the following will be checked: * - check if local playback time is close enough to the server playback time * If it is not, then a playback time sync will be attempted. @@ -637,18 +742,15 @@ class SyncplayManager { const playAtTime = this.lastCommand.When; - const CurrentPositionTicks = playbackManager.currentTime(); + const currentPositionTicks = playbackManager.currentTime(); // Estimate PositionTicks on server - const ServerPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000; + const serverPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000; // Measure delay that needs to be recovered // diff might be caused by the player internally starting the playback - const diff = ServerPositionTicks - CurrentPositionTicks; - const diffMillis = diff / 10000; + const diffMillis = (serverPositionTicks - currentPositionTicks) / 10000.0; this.playbackDiffMillis = diffMillis; - // console.debug("Syncplay onTimeUpdate", diffMillis, CurrentPositionTicks, ServerPositionTicks); - if (this.syncEnabled) { const absDiffMillis = Math.abs(diffMillis); // TODO: SpeedToSync sounds bad on songs @@ -664,7 +766,7 @@ class SyncplayManager { this.currentPlayer.setPlaybackRate(speed); this.syncEnabled = false; this.syncAttempts++; - this.showSyncIcon("SpeedToSync (x" + speed + ")"); + this.showSyncIcon('SpeedToSync (x' + speed + ')'); this.syncTimeout = setTimeout(() => { this.currentPlayer.setPlaybackRate(1); @@ -675,13 +777,13 @@ class SyncplayManager { // Disable SkipToSync if it keeps failing if (this.syncAttempts > MaxAttemptsSync) { this.syncEnabled = false; - this.showSyncIcon("Sync disabled (too many attempts)"); + this.showSyncIcon('Sync disabled (too many attempts)'); } // SkipToSync method - playbackManager.syncplay_seek(ServerPositionTicks); + this.localSeek(serverPositionTicks); this.syncEnabled = false; this.syncAttempts++; - this.showSyncIcon("SkipToSync (" + this.syncAttempts + ")"); + this.showSyncIcon('SkipToSync (' + this.syncAttempts + ')'); this.syncTimeout = setTimeout(() => { this.syncEnabled = true; @@ -690,7 +792,7 @@ class SyncplayManager { } else { // Playback is synced if (this.syncAttempts > 0) { - // console.debug("Playback has been synced after", this.syncAttempts, "attempts."); + console.debug('Playback has been synced after', this.syncAttempts, 'attempts.'); } this.syncAttempts = 0; } @@ -706,7 +808,7 @@ class SyncplayManager { TimeOffset: this.timeOffsetWithServer, PlaybackDiff: this.playbackDiffMillis, SyncMethod: this.syncMethod - } + }; } /** @@ -714,15 +816,15 @@ class SyncplayManager { */ showSyncIcon (syncMethod) { this.syncMethod = syncMethod; - events.trigger(this, "SyncplayError", [true]); + events.trigger(this, 'syncing', [true, this.syncMethod]); } /** * Emits an event to clear the Syncplay status icon. */ clearSyncIcon () { - this.syncMethod = "None"; - events.trigger(this, "SyncplayError", [false]); + this.syncMethod = 'None'; + events.trigger(this, 'syncing', [false, this.syncMethod]); } /** diff --git a/src/components/syncplay/timeSyncManager.js b/src/components/syncplay/timeSyncManager.js index 74c98820c..ca9293957 100644 --- a/src/components/syncplay/timeSyncManager.js +++ b/src/components/syncplay/timeSyncManager.js @@ -1,5 +1,3 @@ -/* eslint-disable indent */ - /** * Module that manages time syncing with server. * @module components/syncplay/timeSyncManager @@ -22,30 +20,30 @@ const GreedyPingCount = 3; class Measurement { /** * Creates a new measurement. - * @param {Date} t0 Client's timestamp of the request transmission - * @param {Date} t1 Server's timestamp of the request reception - * @param {Date} t2 Server's timestamp of the response transmission - * @param {Date} t3 Client's timestamp of the response reception + * @param {Date} requestSent Client's timestamp of the request transmission + * @param {Date} requestReceived Server's timestamp of the request reception + * @param {Date} responseSent Server's timestamp of the response transmission + * @param {Date} responseReceived Client's timestamp of the response reception */ - constructor(t0, t1, t2, t3) { - this.t0 = t0.getTime(); - this.t1 = t1.getTime(); - this.t2 = t2.getTime(); - this.t3 = t3.getTime(); + constructor(requestSent, requestReceived, responseSent, responseReceived) { + this.requestSent = requestSent.getTime(); + this.requestReceived = requestReceived.getTime(); + this.responseSent = responseSent.getTime(); + this.responseReceived = responseReceived.getTime(); } /** * Time offset from server. */ getOffset () { - return ((this.t1 - this.t0) + (this.t2 - this.t3)) / 2; + return ((this.requestReceived - this.requestSent) + (this.responseSent - this.responseReceived)) / 2; } /** * Get round-trip delay. */ getDelay () { - return (this.t3 - this.t0) - (this.t2 - this.t1); + return (this.responseReceived - this.requestSent) - (this.responseSent - this.requestReceived); } /** @@ -76,7 +74,7 @@ class TimeSyncManager { * @returns {boolean} _true_ if a measurement has been done, _false_ otherwise. */ isReady() { - return this.measurement ? true : false; + return !!this.measurement; } /** @@ -119,14 +117,14 @@ class TimeSyncManager { this.poller = setTimeout(() => { this.poller = null; const apiClient = connectionManager.currentApiClient(); - const t0 = new Date(); // pingStartTime + const requestSent = new Date(); apiClient.getServerTime().then((response) => { - const t3 = new Date(); // pingEndTime + const responseReceived = new Date(); response.json().then((data) => { - const t1 = new Date(data.RequestReceptionTime); // request received - const t2 = new Date(data.ResponseTransmissionTime); // response sent + const requestReceived = new Date(data.RequestReceptionTime); + const responseSent = new Date(data.ResponseTransmissionTime); - const measurement = new Measurement(t0, t1, t2, t3); + const measurement = new Measurement(requestSent, requestReceived, responseSent, responseReceived); this.updateTimeOffset(measurement); // Avoid overloading server @@ -136,11 +134,11 @@ class TimeSyncManager { this.pings++; } - events.trigger(this, "Update", [this.getTimeOffset(), this.getPing()]); + events.trigger(this, 'update', [null, this.getTimeOffset(), this.getPing()]); }); }).catch((error) => { console.error(error); - events.trigger(this, "Error", [error]); + events.trigger(this, 'update', [error, null, null]); }).finally(() => { this.requestPing(); }); diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index 1daa200b8..3113031ea 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -1,4 +1,4 @@ -define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'syncplayManager', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, syncplayManager, browser, globalize, imageHelper) { +define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'syncplayManager', 'groupSelectionMenu', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, syncplayManager, groupSelectionMenu, browser, globalize, imageHelper) { 'use strict'; function renderHeader() { @@ -89,12 +89,13 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' var policy = user.Policy ? user.Policy : user.localUser.Policy; - if (headerSyncButton && policy && policy.SyncplayAccess !== "None") { - headerSyncButton.classList.remove("hide"); + if (headerSyncButton && policy && policy.SyncplayAccess !== 'None') { + headerSyncButton.classList.remove('hide'); } } else { headerHomeButton.classList.add('hide'); headerCastButton.classList.add('hide'); + headerSyncButton.classList.add('hide'); if (headerSearchButton) { headerSearchButton.classList.add('hide'); @@ -188,27 +189,26 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' function onSyncButtonClicked() { var btn = this; - - require(["groupSelectionMenu"], function (groupSelectionMenu) { - groupSelectionMenu.show(btn); - }); + groupSelectionMenu.show(btn); } - function updateSyncplayIcon(event, enabled) { - var icon = headerSyncButton.querySelector("i"); + function onSyncplayEnabled(event, enabled) { + var icon = headerSyncButton.querySelector('span'); + icon.classList.remove('sync', 'sync_disabled', 'sync_problem'); if (enabled) { - icon.innerHTML = "sync"; + icon.classList.add('sync'); } else { - icon.innerHTML = "sync_disabled"; + icon.classList.add('sync_disabled'); } } - function updateSyncplayErrorIcon(event, show_error) { - var icon = headerSyncButton.querySelector("i"); - if (show_error) { - icon.innerHTML = "sync_problem"; + function onSyncplaySyncing(event, is_syncing, syncMethod) { + var icon = headerSyncButton.querySelector('span'); + icon.classList.remove('sync', 'sync_disabled', 'sync_problem'); + if (is_syncing) { + icon.classList.add('sync_problem'); } else { - icon.innerHTML = "sync"; + icon.classList.add('sync'); } } @@ -967,8 +967,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' updateUserInHeader(); }); events.on(playbackManager, 'playerchange', updateCastIcon); - events.on(syncplayManager, 'SyncplayEnabled', updateSyncplayIcon); - events.on(syncplayManager, 'SyncplayError', updateSyncplayErrorIcon); + events.on(syncplayManager, 'enabled', onSyncplayEnabled); + events.on(syncplayManager, 'syncing', onSyncplaySyncing); loadNavDrawer(); return LibraryMenu; }); diff --git a/src/scripts/site.js b/src/scripts/site.js index ecfeb2734..a07062ab3 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -316,7 +316,7 @@ var AppInfo = {}; function returnDefault(obj) { if (obj.default === null) { - throw new Error("Object has no default!"); + throw new Error('Object has no default!'); } return obj.default; } @@ -825,7 +825,8 @@ var AppInfo = {}; define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency); define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager); define('timeSyncManager', [componentsPath + '/syncplay/timeSyncManager'], returnDefault); - define('syncplayManager', [componentsPath + '/syncplay/syncplaymanager'], returnDefault); + define('groupSelectionMenu', [componentsPath + '/syncplay/groupSelectionMenu'], returnFirstDependency); + define('syncplayManager', [componentsPath + '/syncplay/syncplayManager'], returnDefault); define('playbackPermissionManager', [componentsPath + '/syncplay/playbackPermissionManager'], returnDefault); define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager); define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index fb72e8881..4759b670d 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -855,7 +855,8 @@ "LabelSubtitlePlaybackMode": "Subtitle mode:", "LabelSubtitles": "Subtitles", "LabelSupportedMediaTypes": "Supported Media Types:", - "LabelSyncplayTimeOffset": "Time offset with server:", + "LabelSyncplayTimeOffset": "Time offset with the server:", + "MillisecondsUnit": "ms", "LabelSyncplayPlaybackDiff": "Playback time difference:", "LabelSyncplaySyncMethod": "Sync method:", "LabelSyncplayNewGroup": "New group", @@ -1031,15 +1032,19 @@ "MessageYouHaveVersionInstalled": "You currently have version {0} installed.", "MessageSyncplayEnabled": "Syncplay enabled.", "MessageSyncplayDisabled": "Syncplay disabled.", - "MessageSyncplayUserJoined": "{0} joined group.", - "MessageSyncplayUserLeft": "{0} left group.", + "MessageSyncplayUserJoined": "{0} has joined the group.", + "MessageSyncplayUserLeft": "{0} has left the group.", "MessageSyncplayGroupWait": "{0} is buffering...", "MessageSyncplayNoGroupsAvailable": "No groups available. Start playing something first.", "MessageSyncplayPlaybackPermissionRequired": "Playback permission required.", - "MessageSyncplayGroupNotJoined": "Failed to join requested group.", + "MessageSyncplayGroupDoesNotExist": "Failed to join group because it does not exist.", "MessageSyncplayCreateGroupDenied": "Permission required to create a group.", "MessageSyncplayJoinGroupDenied": "Permission required to use Syncplay.", "MessageSyncplayLibraryAccessDenied": "Access to this content is restricted.", + "MessageSyncplayErrorAccessingGroups": "An error occurred while accessing groups list.", + "MessageSyncplayErrorNoActivePlayer": "No active player found. Syncplay has been disabled.", + "MessageSyncplayErrorMissingSession": "Failed to enable Syncplay! Missing session.", + "MessageSyncplayErrorMedia": "Failed to enable Syncplay! Media error.", "Metadata": "Metadata", "MetadataManager": "Metadata Manager", "MetadataSettingChangeHelp": "Changing metadata settings will affect new content that is added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.", diff --git a/webpack.dev.js b/webpack.dev.js index d8879fe80..b86914775 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -46,8 +46,8 @@ module.exports = merge(common, { ] }, { - test: /\.(wav)$/i, - use: ["file-loader"] + test: /\.(mp3)$/i, + use: ['file-loader'] } ] } diff --git a/webpack.prod.js b/webpack.prod.js index cc4c57b9f..2f5315ea7 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -39,8 +39,8 @@ module.exports = merge(common, { ] }, { - test: /\.(wav)$/i, - use: ["file-loader"] + test: /\.(mp3)$/i, + use: ['file-loader'] } ] } From 36d097291ee9ab1cfb841d2a5752e7f9026a7798 Mon Sep 17 00:00:00 2001 From: gion Date: Wed, 6 May 2020 23:41:54 +0200 Subject: [PATCH 0043/2481] Rename Syncplay to SyncPlay --- package.json | 2 +- src/components/playerstats/playerstats.js | 18 +-- src/components/serverNotifications.js | 10 +- src/components/syncplay/groupSelectionMenu.js | 56 +++---- ...{syncplayManager.js => syncPlayManager.js} | 150 +++++++++--------- src/controllers/useredit.js | 4 +- src/scripts/librarymenu.js | 12 +- src/scripts/site.js | 2 +- src/strings/en-us.json | 58 +++---- src/useredit.html | 12 +- 10 files changed, 162 insertions(+), 162 deletions(-) rename src/components/syncplay/{syncplayManager.js => syncPlayManager.js} (86%) diff --git a/package.json b/package.json index 33a7d2b00..1beb16499 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "src/components/syncplay/playbackPermissionManager.js", "src/components/syncplay/groupSelectionMenu.js", "src/components/syncplay/timeSyncManager.js", - "src/components/syncplay/syncplayManager.js", + "src/components/syncplay/syncPlayManager.js", "src/scripts/dfnshelper.js", "src/scripts/dom.js", "src/scripts/filesystem.js", diff --git a/src/components/playerstats/playerstats.js b/src/components/playerstats/playerstats.js index 07fcd7070..a65baf355 100644 --- a/src/components/playerstats/playerstats.js +++ b/src/components/playerstats/playerstats.js @@ -1,4 +1,4 @@ -define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplayManager', 'playMethodHelper', 'layoutManager', 'serverNotifications', 'paper-icon-button-light', 'css!./playerstats'], function (events, globalize, playbackManager, connectionManager, syncplayManager, playMethodHelper, layoutManager, serverNotifications) { +define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncPlayManager', 'playMethodHelper', 'layoutManager', 'serverNotifications', 'paper-icon-button-light', 'css!./playerstats'], function (events, globalize, playbackManager, connectionManager, syncPlayManager, playMethodHelper, layoutManager, serverNotifications) { 'use strict'; function init(instance) { @@ -327,22 +327,22 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplay return sessionStats; } - function getSyncplayStats() { + function getSyncPlayStats() { var syncStats = []; - var stats = syncplayManager.getStats(); + var stats = syncPlayManager.getStats(); syncStats.push({ - label: globalize.translate('LabelSyncplayTimeOffset'), + label: globalize.translate('LabelSyncPlayTimeOffset'), value: stats.TimeOffset + globalize.translate('MillisecondsUnit') }); syncStats.push({ - label: globalize.translate('LabelSyncplayPlaybackDiff'), + label: globalize.translate('LabelSyncPlayPlaybackDiff'), value: stats.PlaybackDiff + globalize.translate('MillisecondsUnit') }); syncStats.push({ - label: globalize.translate('LabelSyncplaySyncMethod'), + label: globalize.translate('LabelSyncPlaySyncMethod'), value: stats.SyncMethod }); @@ -405,10 +405,10 @@ define(['events', 'globalize', 'playbackManager', 'connectionManager', 'syncplay name: 'Original Media Info' }); - if (syncplayManager.isSyncplayEnabled()) { + if (syncPlayManager.isSyncPlayEnabled()) { categories.push({ - stats: getSyncplayStats(), - name: 'Syncplay Info' + stats: getSyncPlayStats(), + name: 'SyncPlay Info' }); } diff --git a/src/components/serverNotifications.js b/src/components/serverNotifications.js index 876c3f7e7..2553c284f 100644 --- a/src/components/serverNotifications.js +++ b/src/components/serverNotifications.js @@ -1,4 +1,4 @@ -define(['connectionManager', 'playbackManager', 'syncplayManager', 'events', 'inputManager', 'focusManager', 'appRouter'], function (connectionManager, playbackManager, syncplayManager, events, inputManager, focusManager, appRouter) { +define(['connectionManager', 'playbackManager', 'syncPlayManager', 'events', 'inputManager', 'focusManager', 'appRouter'], function (connectionManager, playbackManager, syncPlayManager, events, inputManager, focusManager, appRouter) { 'use strict'; var serverNotifications = {}; @@ -187,10 +187,10 @@ define(['connectionManager', 'playbackManager', 'syncplayManager', 'events', 'in events.trigger(serverNotifications, 'UserDataChanged', [apiClient, msg.Data.UserDataList[i]]); } } - } else if (msg.MessageType === 'SyncplayCommand') { - syncplayManager.processCommand(msg.Data, apiClient); - } else if (msg.MessageType === 'SyncplayGroupUpdate') { - syncplayManager.processGroupUpdate(msg.Data, apiClient); + } else if (msg.MessageType === 'SyncPlayCommand') { + syncPlayManager.processCommand(msg.Data, apiClient); + } else if (msg.MessageType === 'SyncPlayGroupUpdate') { + syncPlayManager.processGroupUpdate(msg.Data, apiClient); } else { events.trigger(serverNotifications, msg.MessageType, [apiClient, msg.Data]); } diff --git a/src/components/syncplay/groupSelectionMenu.js b/src/components/syncplay/groupSelectionMenu.js index af08f9277..067100ad7 100644 --- a/src/components/syncplay/groupSelectionMenu.js +++ b/src/components/syncplay/groupSelectionMenu.js @@ -1,7 +1,7 @@ import events from 'events'; import connectionManager from 'connectionManager'; import playbackManager from 'playbackManager'; -import syncplayManager from 'syncplayManager'; +import syncPlayManager from 'syncPlayManager'; import loading from 'loading'; import toast from 'toast'; import actionsheet from 'actionsheet'; @@ -37,7 +37,7 @@ function showNewJoinGroupSelection (button, user, apiClient) { console.debug('No item is currently playing.'); } - apiClient.sendSyncplayCommand(sessionId, 'ListGroups').then(function (response) { + apiClient.sendSyncPlayCommand(sessionId, 'ListGroups').then(function (response) { response.json().then(function (groups) { var menuItems = groups.map(function (group) { return { @@ -49,24 +49,24 @@ function showNewJoinGroupSelection (button, user, apiClient) { }; }); - if (inSession && policy.SyncplayAccess === 'CreateAndJoinGroups') { + if (inSession && policy.SyncPlayAccess === 'CreateAndJoinGroups') { menuItems.push({ - name: globalize.translate('LabelSyncplayNewGroup'), + name: globalize.translate('LabelSyncPlayNewGroup'), icon: 'add', id: 'new-group', selected: true, - secondaryText: globalize.translate('LabelSyncplayNewGroupDescription') + secondaryText: globalize.translate('LabelSyncPlayNewGroupDescription') }); } if (menuItems.length === 0) { - if (inSession && policy.SyncplayAccess === 'JoinGroups') { + if (inSession && policy.SyncPlayAccess === 'JoinGroups') { toast({ - text: globalize.translate('MessageSyncplayCreateGroupDenied') + text: globalize.translate('MessageSyncPlayCreateGroupDenied') }); } else { toast({ - text: globalize.translate('MessageSyncplayNoGroupsAvailable') + text: globalize.translate('MessageSyncPlayNoGroupsAvailable') }); } loading.hide(); @@ -74,7 +74,7 @@ function showNewJoinGroupSelection (button, user, apiClient) { } var menuOptions = { - title: globalize.translate('HeaderSyncplaySelectGroup'), + title: globalize.translate('HeaderSyncPlaySelectGroup'), items: menuItems, positionTo: button, resolveOnClick: true, @@ -83,15 +83,15 @@ function showNewJoinGroupSelection (button, user, apiClient) { actionsheet.show(menuOptions).then(function (id) { if (id == 'new-group') { - apiClient.sendSyncplayCommand(sessionId, 'NewGroup'); + apiClient.sendSyncPlayCommand(sessionId, 'NewGroup'); } else { - apiClient.sendSyncplayCommand(sessionId, 'JoinGroup', { + apiClient.sendSyncPlayCommand(sessionId, 'JoinGroup', { GroupId: id, PlayingItemId: playingItemId }); } }).catch((error) => { - console.error('Syncplay: unexpected error listing groups:', error); + console.error('SyncPlay: unexpected error listing groups:', error); }); loading.hide(); @@ -100,7 +100,7 @@ function showNewJoinGroupSelection (button, user, apiClient) { console.error(error); loading.hide(); toast({ - text: globalize.translate('MessageSyncplayErrorAccessingGroups') + text: globalize.translate('MessageSyncPlayErrorAccessingGroups') }); }); } @@ -114,24 +114,24 @@ function showNewJoinGroupSelection (button, user, apiClient) { function showLeaveGroupSelection (button, user, apiClient) { const sessionId = getActivePlayerId(); if (!sessionId) { - syncplayManager.signalError(); + syncPlayManager.signalError(); toast({ - text: globalize.translate('MessageSyncplayErrorNoActivePlayer') + text: globalize.translate('MessageSyncPlayErrorNoActivePlayer') }); showNewJoinGroupSelection(button, user, apiClient); return; } const menuItems = [{ - name: globalize.translate('LabelSyncplayLeaveGroup'), + name: globalize.translate('LabelSyncPlayLeaveGroup'), icon: 'meeting_room', id: 'leave-group', selected: true, - secondaryText: globalize.translate('LabelSyncplayLeaveGroupDescription') + secondaryText: globalize.translate('LabelSyncPlayLeaveGroupDescription') }]; var menuOptions = { - title: globalize.translate('HeaderSyncplayEnabled'), + title: globalize.translate('HeaderSyncPlayEnabled'), items: menuItems, positionTo: button, resolveOnClick: true, @@ -140,23 +140,23 @@ function showLeaveGroupSelection (button, user, apiClient) { actionsheet.show(menuOptions).then(function (id) { if (id == 'leave-group') { - apiClient.sendSyncplayCommand(sessionId, 'LeaveGroup'); + apiClient.sendSyncPlayCommand(sessionId, 'LeaveGroup'); } }).catch((error) => { - console.error('Syncplay: unexpected error showing group menu:', error); + console.error('SyncPlay: unexpected error showing group menu:', error); }); loading.hide(); } -// Register to Syncplay events -let syncplayEnabled = false; -events.on(syncplayManager, 'enabled', function (e, enabled) { - syncplayEnabled = enabled; +// Register to SyncPlay events +let syncPlayEnabled = false; +events.on(syncPlayManager, 'enabled', function (e, enabled) { + syncPlayEnabled = enabled; }); /** - * Shows a menu to handle Syncplay groups. + * Shows a menu to handle SyncPlay groups. * @param {HTMLElement} button - Element where to place the menu. */ export function show (button) { @@ -168,13 +168,13 @@ export function show (button) { }).catch((error) => { console.error('Playback not allowed!', error); toast({ - text: globalize.translate('MessageSyncplayPlaybackPermissionRequired') + text: globalize.translate('MessageSyncPlayPlaybackPermissionRequired') }); }); const apiClient = connectionManager.currentApiClient(); connectionManager.user(apiClient).then((user) => { - if (syncplayEnabled) { + if (syncPlayEnabled) { showLeaveGroupSelection(button, user, apiClient); } else { showNewJoinGroupSelection(button, user, apiClient); @@ -183,7 +183,7 @@ export function show (button) { console.error(error); loading.hide(); toast({ - text: globalize.translate('MessageSyncplayNoGroupsAvailable') + text: globalize.translate('MessageSyncPlayNoGroupsAvailable') }); }); } diff --git a/src/components/syncplay/syncplayManager.js b/src/components/syncplay/syncPlayManager.js similarity index 86% rename from src/components/syncplay/syncplayManager.js rename to src/components/syncplay/syncPlayManager.js index b5694c88f..f5c9ac446 100644 --- a/src/components/syncplay/syncplayManager.js +++ b/src/components/syncplay/syncPlayManager.js @@ -1,6 +1,6 @@ /** - * Module that manages the Syncplay feature. - * @module components/syncplay/syncplayManager + * Module that manages the SyncPlay feature. + * @module components/syncplay/syncPlayManager */ import events from 'events'; @@ -62,9 +62,9 @@ const WaitForEventDefaultTimeout = 30000; // milliseconds const WaitForPlayerEventTimeout = 500; // milliseconds /** - * Class that manages the Syncplay feature. + * Class that manages the SyncPlay feature. */ -class SyncplayManager { +class SyncPlayManager { constructor() { this.playbackRateSupported = false; this.syncEnabled = false; @@ -80,8 +80,8 @@ class SyncplayManager { this.currentPlayer = null; this.localPlayerPlaybackRate = 1.0; // used to restore user PlaybackRate - this.syncplayEnabledAt = null; // Server time of when Syncplay has been enabled - this.syncplayReady = false; // Syncplay is ready after first ping to server + this.syncPlayEnabledAt = null; // Server time of when SyncPlay has been enabled + this.syncPlayReady = false; // SyncPlay is ready after first ping to server this.lastCommand = null; this.queuedCommand = null; @@ -91,7 +91,7 @@ class SyncplayManager { this.timeOffsetWithServer = 0; // server time minus local time this.roundTripDuration = 0; - this.notifySyncplayReady = false; + this.notifySyncPlayReady = false; events.on(playbackManager, 'playbackstart', (player, state) => { this.onPlaybackStart(player, state); @@ -113,17 +113,17 @@ class SyncplayManager { events.on(timeSyncManager, 'update', (event, error, timeOffset, ping) => { if (error) { - console.debug('Syncplay, time update issue', error); + console.debug('SyncPlay, time update issue', error); return; } this.timeOffsetWithServer = timeOffset; this.roundTripDuration = ping * 2; - if (this.notifySyncplayReady) { - this.syncplayReady = true; + if (this.notifySyncPlayReady) { + this.syncPlayReady = true; events.trigger(this, 'ready'); - this.notifySyncplayReady = false; + this.notifySyncPlayReady = false; } // Report ping @@ -134,12 +134,12 @@ class SyncplayManager { if (!sessionId) { this.signalError(); toast({ - text: globalize.translate('MessageSyncplayErrorMissingSession') + text: globalize.translate('MessageSyncPlayErrorMissingSession') }); return; } - apiClient.sendSyncplayCommand(sessionId, 'UpdatePing', { + apiClient.sendSyncPlayCommand(sessionId, 'UpdatePing', { Ping: ping }); } @@ -158,8 +158,8 @@ class SyncplayManager { */ onPlaybackStop (stopInfo) { events.trigger(this, 'playbackstop', [stopInfo]); - if (this.isSyncplayEnabled()) { - this.disableSyncplay(false); + if (this.isSyncPlayEnabled()) { + this.disableSyncPlay(false); } } @@ -238,7 +238,7 @@ class SyncplayManager { // FIXME: the following are needed because the 'events' module // is changing the scope when executing the callbacks. // For instance, calling 'onPlayerUnpause' from the wrong scope breaks things because 'this' - // points to 'player' (the event emitter) instead of pointing to the SyncplayManager singleton. + // points to 'player' (the event emitter) instead of pointing to the SyncPlayManager singleton. const self = this; this._onPlayerUnpause = () => { self.onPlayerUnpause(); @@ -306,48 +306,48 @@ class SyncplayManager { break; case 'UserJoined': toast({ - text: globalize.translate('MessageSyncplayUserJoined', cmd.Data) + text: globalize.translate('MessageSyncPlayUserJoined', cmd.Data) }); break; case 'UserLeft': toast({ - text: globalize.translate('MessageSyncplayUserLeft', cmd.Data) + text: globalize.translate('MessageSyncPlayUserLeft', cmd.Data) }); break; case 'GroupJoined': - this.enableSyncplay(apiClient, new Date(cmd.Data), true); + this.enableSyncPlay(apiClient, new Date(cmd.Data), true); break; case 'NotInGroup': case 'GroupLeft': - this.disableSyncplay(true); + this.disableSyncPlay(true); break; case 'GroupWait': toast({ - text: globalize.translate('MessageSyncplayGroupWait', cmd.Data) + text: globalize.translate('MessageSyncPlayGroupWait', cmd.Data) }); break; case 'GroupDoesNotExist': toast({ - text: globalize.translate('MessageSyncplayGroupDoesNotExist') + text: globalize.translate('MessageSyncPlayGroupDoesNotExist') }); break; case 'CreateGroupDenied': toast({ - text: globalize.translate('MessageSyncplayCreateGroupDenied') + text: globalize.translate('MessageSyncPlayCreateGroupDenied') }); break; case 'JoinGroupDenied': toast({ - text: globalize.translate('MessageSyncplayJoinGroupDenied') + text: globalize.translate('MessageSyncPlayJoinGroupDenied') }); break; case 'LibraryAccessDenied': toast({ - text: globalize.translate('MessageSyncplayLibraryAccessDenied') + text: globalize.translate('MessageSyncPlayLibraryAccessDenied') }); break; default: - console.error('processSyncplayGroupUpdate: command is not recognised: ' + cmd.Type); + console.error('processSyncPlayGroupUpdate: command is not recognised: ' + cmd.Type); break; } } @@ -360,13 +360,13 @@ class SyncplayManager { processCommand (cmd, apiClient) { if (cmd === null) return; - if (!this.isSyncplayEnabled()) { - console.debug('Syncplay processCommand: SyncPlay not enabled, ignoring command', cmd); + if (!this.isSyncPlayEnabled()) { + console.debug('SyncPlay processCommand: SyncPlay not enabled, ignoring command', cmd); return; } - if (!this.syncplayReady) { - console.debug('Syncplay processCommand: SyncPlay not ready, queued command', cmd); + if (!this.syncPlayReady) { + console.debug('SyncPlay processCommand: SyncPlay not ready, queued command', cmd); this.queuedCommand = cmd; return; } @@ -374,8 +374,8 @@ class SyncplayManager { cmd.When = new Date(cmd.When); cmd.EmittedAt = new Date(cmd.EmitttedAt); - if (cmd.EmitttedAt < this.syncplayEnabledAt) { - console.debug('Syncplay processCommand: ignoring old command', cmd); + if (cmd.EmitttedAt < this.syncPlayEnabledAt) { + console.debug('SyncPlay processCommand: ignoring old command', cmd); return; } @@ -385,12 +385,12 @@ class SyncplayManager { this.lastCommand.PositionTicks === cmd.PositionTicks && this.Command === cmd.Command ) { - console.debug('Syncplay processCommand: ignoring duplicate command', cmd); + console.debug('SyncPlay processCommand: ignoring duplicate command', cmd); return; } this.lastCommand = cmd; - console.log('Syncplay will', cmd.Command, 'at', cmd.When, 'PositionTicks', cmd.PositionTicks); + console.log('SyncPlay will', cmd.Command, 'at', cmd.When, 'PositionTicks', cmd.PositionTicks); switch (cmd.Command) { case 'Play': @@ -430,7 +430,7 @@ class SyncplayManager { if (!sessionId) { console.error('Missing sessionId!'); toast({ - text: globalize.translate('MessageSyncplayErrorMissingSession') + text: globalize.translate('MessageSyncPlayErrorMissingSession') }); return; } @@ -448,22 +448,22 @@ class SyncplayManager { if (!success) { console.warning('Error reporting playback state to server. Joining group will fail.'); } - apiClient.sendSyncplayCommand(sessionId, 'JoinGroup', { + apiClient.sendSyncPlayCommand(sessionId, 'JoinGroup', { GroupId: groupId, PlayingItemId: playingItemId }); }).catch(() => { console.error('Timed out while waiting for `reportplayback` event!'); toast({ - text: globalize.translate('MessageSyncplayErrorMedia') + text: globalize.translate('MessageSyncPlayErrorMedia') }); return; }); }).catch(() => { console.error('Timed out while waiting for `playbackstart` event!'); - if (!this.isSyncplayEnabled()) { + if (!this.isSyncPlayEnabled()) { toast({ - text: globalize.translate('MessageSyncplayErrorMedia') + text: globalize.translate('MessageSyncPlayErrorMedia') }); } return; @@ -471,19 +471,19 @@ class SyncplayManager { }).catch((error) => { console.error(error); toast({ - text: globalize.translate('MessageSyncplayErrorMedia') + text: globalize.translate('MessageSyncPlayErrorMedia') }); }); } /** - * Enables Syncplay. + * Enables SyncPlay. * @param {Object} apiClient The ApiClient. - * @param {Date} enabledAt When Syncplay has been enabled. Server side date. + * @param {Date} enabledAt When SyncPlay has been enabled. Server side date. * @param {boolean} showMessage Display message. */ - enableSyncplay (apiClient, enabledAt, showMessage = false) { - this.syncplayEnabledAt = enabledAt; + enableSyncPlay (apiClient, enabledAt, showMessage = false) { + this.syncPlayEnabledAt = enabledAt; this.injectPlaybackManager(); events.trigger(this, 'enabled', [true]); @@ -492,25 +492,25 @@ class SyncplayManager { this.queuedCommand = null; }); - this.syncplayReady = false; - this.notifySyncplayReady = true; + this.syncPlayReady = false; + this.notifySyncPlayReady = true; timeSyncManager.forceUpdate(); if (showMessage) { toast({ - text: globalize.translate('MessageSyncplayEnabled') + text: globalize.translate('MessageSyncPlayEnabled') }); } } /** - * Disables Syncplay. + * Disables SyncPlay. * @param {boolean} showMessage Display message. */ - disableSyncplay (showMessage = false) { - this.syncplayEnabledAt = null; - this.syncplayReady = false; + disableSyncPlay (showMessage = false) { + this.syncPlayEnabledAt = null; + this.syncPlayReady = false; this.lastCommand = null; this.queuedCommand = null; this.syncEnabled = false; @@ -519,17 +519,17 @@ class SyncplayManager { if (showMessage) { toast({ - text: globalize.translate('MessageSyncplayDisabled') + text: globalize.translate('MessageSyncPlayDisabled') }); } } /** - * Gets Syncplay status. + * Gets SyncPlay status. * @returns {boolean} _true_ if user joined a group, _false_ otherwise. */ - isSyncplayEnabled () { - return this.syncplayEnabledAt !== null; + isSyncPlayEnabled () { + return this.syncPlayEnabledAt !== null; } /** @@ -627,8 +627,8 @@ class SyncplayManager { * Overrides some PlaybackManager's methods to intercept playback commands. */ injectPlaybackManager () { - if (!this.isSyncplayEnabled()) return; - if (playbackManager.syncplayEnabled) return; + if (!this.isSyncPlayEnabled()) return; + if (playbackManager.syncPlayEnabled) return; // TODO: make this less hacky playbackManager._localUnpause = playbackManager.unpause; @@ -638,20 +638,20 @@ class SyncplayManager { playbackManager.unpause = this.playRequest; playbackManager.pause = this.pauseRequest; playbackManager.seek = this.seekRequest; - playbackManager.syncplayEnabled = true; + playbackManager.syncPlayEnabled = true; } /** * Restores original PlaybackManager's methods. */ restorePlaybackManager () { - if (this.isSyncplayEnabled()) return; - if (!playbackManager.syncplayEnabled) return; + if (this.isSyncPlayEnabled()) return; + if (!playbackManager.syncPlayEnabled) return; playbackManager.unpause = playbackManager._localUnpause; playbackManager.pause = playbackManager._localPause; playbackManager.seek = playbackManager._localSeek; - playbackManager.syncplayEnabled = false; + playbackManager.syncPlayEnabled = false; } /** @@ -660,7 +660,7 @@ class SyncplayManager { playRequest (player) { var apiClient = connectionManager.currentApiClient(); var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, 'PlayRequest'); + apiClient.sendSyncPlayCommand(sessionId, 'PlayRequest'); } /** @@ -669,7 +669,7 @@ class SyncplayManager { pauseRequest (player) { var apiClient = connectionManager.currentApiClient(); var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, 'PauseRequest'); + apiClient.sendSyncPlayCommand(sessionId, 'PauseRequest'); // Pause locally as well, to give the user some little control playbackManager._localUnpause(player); } @@ -680,7 +680,7 @@ class SyncplayManager { seekRequest (PositionTicks, player) { var apiClient = connectionManager.currentApiClient(); var sessionId = getActivePlayerId(); - apiClient.sendSyncplayCommand(sessionId, 'SeekRequest', { + apiClient.sendSyncPlayCommand(sessionId, 'SeekRequest', { PositionTicks: PositionTicks }); } @@ -689,7 +689,7 @@ class SyncplayManager { * Calls original PlaybackManager's unpause method. */ localUnpause(player) { - if (playbackManager.syncplayEnabled) { + if (playbackManager.syncPlayEnabled) { playbackManager._localUnpause(player); } else { playbackManager.unpause(player); @@ -700,7 +700,7 @@ class SyncplayManager { * Calls original PlaybackManager's pause method. */ localPause(player) { - if (playbackManager.syncplayEnabled) { + if (playbackManager.syncPlayEnabled) { playbackManager._localPause(player); } else { playbackManager.pause(player); @@ -711,7 +711,7 @@ class SyncplayManager { * Calls original PlaybackManager's seek method. */ localSeek(PositionTicks, player) { - if (playbackManager.syncplayEnabled) { + if (playbackManager.syncPlayEnabled) { playbackManager._localSeek(PositionTicks, player); } else { playbackManager.seek(PositionTicks, player); @@ -800,8 +800,8 @@ class SyncplayManager { } /** - * Gets Syncplay stats. - * @returns {Object} The Syncplay stats. + * Gets SyncPlay stats. + * @returns {Object} The SyncPlay stats. */ getStats () { return { @@ -812,7 +812,7 @@ class SyncplayManager { } /** - * Emits an event to update the Syncplay status icon. + * Emits an event to update the SyncPlay status icon. */ showSyncIcon (syncMethod) { this.syncMethod = syncMethod; @@ -820,7 +820,7 @@ class SyncplayManager { } /** - * Emits an event to clear the Syncplay status icon. + * Emits an event to clear the SyncPlay status icon. */ clearSyncIcon () { this.syncMethod = 'None'; @@ -828,12 +828,12 @@ class SyncplayManager { } /** - * Signals an error state, which disables and resets Syncplay for a new session. + * Signals an error state, which disables and resets SyncPlay for a new session. */ signalError () { - this.disableSyncplay(); + this.disableSyncPlay(); } } -/** SyncplayManager singleton. */ -export default new SyncplayManager(); +/** SyncPlayManager singleton. */ +export default new SyncPlayManager(); diff --git a/src/controllers/useredit.js b/src/controllers/useredit.js index cbe5acd36..125c57136 100644 --- a/src/controllers/useredit.js +++ b/src/controllers/useredit.js @@ -104,7 +104,7 @@ define(['jQuery', 'loading', 'libraryMenu', 'globalize', 'fnchecked'], function $('#chkEnableSharing', page).checked(user.Policy.EnablePublicSharing); $('#txtRemoteClientBitrateLimit', page).val(user.Policy.RemoteClientBitrateLimit / 1e6 || ''); $('#txtLoginAttemptsBeforeLockout', page).val(user.Policy.LoginAttemptsBeforeLockout || '0'); - $('#selectSyncplayAccess').val(user.Policy.SyncplayAccess); + $('#selectSyncPlayAccess').val(user.Policy.SyncPlayAccess); loading.hide(); } @@ -146,7 +146,7 @@ define(['jQuery', 'loading', 'libraryMenu', 'globalize', 'fnchecked'], function }).map(function (c) { return c.getAttribute('data-id'); }); - user.Policy.SyncplayAccess = page.querySelector('#selectSyncplayAccess').value; + user.Policy.SyncPlayAccess = page.querySelector('#selectSyncPlayAccess').value; ApiClient.updateUser(user).then(function () { ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { onSaveComplete(page, user); diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index 3113031ea..a7aaeaa79 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -1,4 +1,4 @@ -define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'syncplayManager', 'groupSelectionMenu', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, syncplayManager, groupSelectionMenu, browser, globalize, imageHelper) { +define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', 'viewManager', 'libraryBrowser', 'appRouter', 'apphost', 'playbackManager', 'syncPlayManager', 'groupSelectionMenu', 'browser', 'globalize', 'scripts/imagehelper', 'paper-icon-button-light', 'material-icons', 'scrollStyles', 'flexStyles'], function (dom, layoutManager, inputManager, connectionManager, events, viewManager, libraryBrowser, appRouter, appHost, playbackManager, syncPlayManager, groupSelectionMenu, browser, globalize, imageHelper) { 'use strict'; function renderHeader() { @@ -89,7 +89,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' var policy = user.Policy ? user.Policy : user.localUser.Policy; - if (headerSyncButton && policy && policy.SyncplayAccess !== 'None') { + if (headerSyncButton && policy && policy.SyncPlayAccess !== 'None') { headerSyncButton.classList.remove('hide'); } } else { @@ -192,7 +192,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' groupSelectionMenu.show(btn); } - function onSyncplayEnabled(event, enabled) { + function onSyncPlayEnabled(event, enabled) { var icon = headerSyncButton.querySelector('span'); icon.classList.remove('sync', 'sync_disabled', 'sync_problem'); if (enabled) { @@ -202,7 +202,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' } } - function onSyncplaySyncing(event, is_syncing, syncMethod) { + function onSyncPlaySyncing(event, is_syncing, syncMethod) { var icon = headerSyncButton.querySelector('span'); icon.classList.remove('sync', 'sync_disabled', 'sync_problem'); if (is_syncing) { @@ -967,8 +967,8 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' updateUserInHeader(); }); events.on(playbackManager, 'playerchange', updateCastIcon); - events.on(syncplayManager, 'enabled', onSyncplayEnabled); - events.on(syncplayManager, 'syncing', onSyncplaySyncing); + events.on(syncPlayManager, 'enabled', onSyncPlayEnabled); + events.on(syncPlayManager, 'syncing', onSyncPlaySyncing); loadNavDrawer(); return LibraryMenu; }); diff --git a/src/scripts/site.js b/src/scripts/site.js index a07062ab3..3cf605460 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -826,7 +826,7 @@ var AppInfo = {}; define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager); define('timeSyncManager', [componentsPath + '/syncplay/timeSyncManager'], returnDefault); define('groupSelectionMenu', [componentsPath + '/syncplay/groupSelectionMenu'], returnFirstDependency); - define('syncplayManager', [componentsPath + '/syncplay/syncplayManager'], returnDefault); + define('syncPlayManager', [componentsPath + '/syncplay/syncPlayManager'], returnDefault); define('playbackPermissionManager', [componentsPath + '/syncplay/playbackPermissionManager'], returnDefault); define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager); define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 4759b670d..495f90edb 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -491,8 +491,8 @@ "HeaderSubtitleProfile": "Subtitle Profile", "HeaderSubtitleProfiles": "Subtitle Profiles", "HeaderSubtitleProfilesHelp": "Subtitle profiles describe the subtitle formats supported by the device.", - "HeaderSyncplaySelectGroup": "Join a group", - "HeaderSyncplayEnabled": "Syncplay enabled", + "HeaderSyncPlaySelectGroup": "Join a group", + "HeaderSyncPlayEnabled": "SyncPlay enabled", "HeaderSystemDlnaProfiles": "System Profiles", "HeaderTags": "Tags", "HeaderTaskTriggers": "Task Triggers", @@ -855,18 +855,18 @@ "LabelSubtitlePlaybackMode": "Subtitle mode:", "LabelSubtitles": "Subtitles", "LabelSupportedMediaTypes": "Supported Media Types:", - "LabelSyncplayTimeOffset": "Time offset with the server:", + "LabelSyncPlayTimeOffset": "Time offset with the server:", "MillisecondsUnit": "ms", - "LabelSyncplayPlaybackDiff": "Playback time difference:", - "LabelSyncplaySyncMethod": "Sync method:", - "LabelSyncplayNewGroup": "New group", - "LabelSyncplayNewGroupDescription": "Create a new group", - "LabelSyncplayLeaveGroup": "Leave group", - "LabelSyncplayLeaveGroupDescription": "Disable Syncplay", - "LabelSyncplayAccessCreateAndJoinGroups": "Allow user to create and join groups", - "LabelSyncplayAccessJoinGroups": "Allow user to join groups", - "LabelSyncplayAccessNone": "Disabled for this user", - "LabelSyncplayAccess": "Syncplay access", + "LabelSyncPlayPlaybackDiff": "Playback time difference:", + "LabelSyncPlaySyncMethod": "Sync method:", + "LabelSyncPlayNewGroup": "New group", + "LabelSyncPlayNewGroupDescription": "Create a new group", + "LabelSyncPlayLeaveGroup": "Leave group", + "LabelSyncPlayLeaveGroupDescription": "Disable SyncPlay", + "LabelSyncPlayAccessCreateAndJoinGroups": "Allow user to create and join groups", + "LabelSyncPlayAccessJoinGroups": "Allow user to join groups", + "LabelSyncPlayAccessNone": "Disabled for this user", + "LabelSyncPlayAccess": "SyncPlay access", "LabelTVHomeScreen": "TV mode home screen:", "LabelTag": "Tag:", "LabelTagline": "Tagline:", @@ -1030,21 +1030,21 @@ "MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.", "MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.", "MessageYouHaveVersionInstalled": "You currently have version {0} installed.", - "MessageSyncplayEnabled": "Syncplay enabled.", - "MessageSyncplayDisabled": "Syncplay disabled.", - "MessageSyncplayUserJoined": "{0} has joined the group.", - "MessageSyncplayUserLeft": "{0} has left the group.", - "MessageSyncplayGroupWait": "{0} is buffering...", - "MessageSyncplayNoGroupsAvailable": "No groups available. Start playing something first.", - "MessageSyncplayPlaybackPermissionRequired": "Playback permission required.", - "MessageSyncplayGroupDoesNotExist": "Failed to join group because it does not exist.", - "MessageSyncplayCreateGroupDenied": "Permission required to create a group.", - "MessageSyncplayJoinGroupDenied": "Permission required to use Syncplay.", - "MessageSyncplayLibraryAccessDenied": "Access to this content is restricted.", - "MessageSyncplayErrorAccessingGroups": "An error occurred while accessing groups list.", - "MessageSyncplayErrorNoActivePlayer": "No active player found. Syncplay has been disabled.", - "MessageSyncplayErrorMissingSession": "Failed to enable Syncplay! Missing session.", - "MessageSyncplayErrorMedia": "Failed to enable Syncplay! Media error.", + "MessageSyncPlayEnabled": "SyncPlay enabled.", + "MessageSyncPlayDisabled": "SyncPlay disabled.", + "MessageSyncPlayUserJoined": "{0} has joined the group.", + "MessageSyncPlayUserLeft": "{0} has left the group.", + "MessageSyncPlayGroupWait": "{0} is buffering...", + "MessageSyncPlayNoGroupsAvailable": "No groups available. Start playing something first.", + "MessageSyncPlayPlaybackPermissionRequired": "Playback permission required.", + "MessageSyncPlayGroupDoesNotExist": "Failed to join group because it does not exist.", + "MessageSyncPlayCreateGroupDenied": "Permission required to create a group.", + "MessageSyncPlayJoinGroupDenied": "Permission required to use SyncPlay.", + "MessageSyncPlayLibraryAccessDenied": "Access to this content is restricted.", + "MessageSyncPlayErrorAccessingGroups": "An error occurred while accessing groups list.", + "MessageSyncPlayErrorNoActivePlayer": "No active player found. SyncPlay has been disabled.", + "MessageSyncPlayErrorMissingSession": "Failed to enable SyncPlay! Missing session.", + "MessageSyncPlayErrorMedia": "Failed to enable SyncPlay! Media error.", "Metadata": "Metadata", "MetadataManager": "Metadata Manager", "MetadataSettingChangeHelp": "Changing metadata settings will affect new content that is added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.", @@ -1393,7 +1393,7 @@ "Suggestions": "Suggestions", "Sunday": "Sunday", "Sync": "Sync", - "SyncplayAccessHelp": "Select the level of access this user has to the Syncplay feature. Syncplay enables to sync playback with other users.", + "SyncPlayAccessHelp": "Select the level of access this user has to the SyncPlay feature. SyncPlay enables to sync playback with other users.", "SystemDlnaProfilesHelp": "System profiles are read-only. Changes to a system profile will be saved to a new custom profile.", "TV": "TV", "TabAccess": "Access", diff --git a/src/useredit.html b/src/useredit.html index 5f759c973..2a8f17af4 100644 --- a/src/useredit.html +++ b/src/useredit.html @@ -105,13 +105,13 @@
    -
    - + + + -
    ${SyncplayAccessHelp}
    +
    ${SyncPlayAccessHelp}
    From 9469c208e19f34b97fda261ca58e7201c1359aed Mon Sep 17 00:00:00 2001 From: Christoph Potas Date: Sat, 9 May 2020 15:42:37 +0200 Subject: [PATCH 0044/2481] ~ switch all strings to single quotes to match ESLint requirements Signed-off-by: Christoph Potas --- src/components/settingshelper.js | 6 +++--- src/controllers/user/subtitles.js | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/settingshelper.js b/src/components/settingshelper.js index 0c942ed04..3bbff9d8e 100644 --- a/src/components/settingshelper.js +++ b/src/components/settingshelper.js @@ -5,15 +5,15 @@ import globalize from 'globalize'; */ export function populateLanguages(select, languages) { - let html = ""; + let html = ''; - html += ""; + html += "'; for (let i = 0, length = languages.length; i < length; i++) { const culture = languages[i]; - html += ""; + html += "'; } select.innerHTML = html; diff --git a/src/controllers/user/subtitles.js b/src/controllers/user/subtitles.js index f1efe0c21..e88109cc6 100644 --- a/src/controllers/user/subtitles.js +++ b/src/controllers/user/subtitles.js @@ -10,14 +10,14 @@ export class SubtitleController { this.subtitleSettingsInstance = null; this.view = view; - view.addEventListener("viewshow", this.viewShow.bind(this)); - view.addEventListener("change", this.change.bind(this)); - view.addEventListener("viewbeforehide", this.viewBeforeHide.bind(this)); - view.addEventListener("viewdestroy", this.viewDestroy.bind(this)); + view.addEventListener('viewshow', this.viewShow.bind(this)); + view.addEventListener('change', this.change.bind(this)); + view.addEventListener('viewbeforehide', this.viewBeforeHide.bind(this)); + view.addEventListener('viewdestroy', this.viewDestroy.bind(this)); } viewShow() { - window.addEventListener("beforeunload", this.beforeUnload.bind(this)); + window.addEventListener('beforeunload', this.beforeUnload.bind(this)); if (this.subtitleSettingsInstance) { this.subtitleSettingsInstance.loadData(); @@ -25,7 +25,7 @@ export class SubtitleController { this.subtitleSettingsInstance = new subtitleSettings({ serverId: ApiClient.serverId(), userId: this.userId, - element: this.view.querySelector(".settingsContainer"), + element: this.view.querySelector('.settingsContainer'), userSettings: this.currentSettings, enableSaveButton: false, enableSaveConfirmation: false, @@ -55,7 +55,7 @@ export class SubtitleController { beforeUnload(e) { if (this.hasChanges) { - e.returnValue = "You currently have unsaved changes. Are you sure you wish to leave?"; + e.returnValue = 'You currently have unsaved changes. Are you sure you wish to leave?'; } } } From 7e7613d5de732bc5b59164613252b680895fba77 Mon Sep 17 00:00:00 2001 From: Guilherme Danno Date: Fri, 8 May 2020 19:06:44 -0300 Subject: [PATCH 0045/2481] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2eae7e693..65bb24e9f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -35,6 +35,7 @@ - [Thibault Nocchi](https://github.com/ThibaultNocchi) - [MrTimscampi](https://github.com/MrTimscampi) - [Sarab Singh](https://github.com/sarab97) + - [GuilhermeHideki](https://github.com/GuilhermeHideki) # Emby Contributors From 934797e07409e7e56cbed10f67504eaf4f0c9d41 Mon Sep 17 00:00:00 2001 From: Guilherme Danno Date: Sun, 3 May 2020 20:45:14 -0300 Subject: [PATCH 0046/2481] feat: add mixins file --- src/styles/_mixins.scss | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/styles/_mixins.scss diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss new file mode 100644 index 000000000..ed23ab083 --- /dev/null +++ b/src/styles/_mixins.scss @@ -0,0 +1,23 @@ +@mixin background-cover($position) { + background-position: $position; + background-repeat: no-repeat; + background-size: cover; +} + +@mixin circle($size) { + @include square($size); + border-radius: 100%; +} + +@mixin position($position, $top: null, $right: null, $bottom: null, $left: null) { + position: $position; + top: $top; + right: $right; + bottom: $bottom; + left: $left; +} + +@mixin square($size) { + height: $size; + width: $size; +} From bdda2df665fb7652d90ef30b37441786f4eacc7d Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 10 May 2020 18:49:01 -0400 Subject: [PATCH 0047/2481] Apply suggestions from code review --- src/networking.html | 54 +++++++++++++++++++++++------------------- src/strings/en-us.json | 5 ++-- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/networking.html b/src/networking.html index b885e9b89..991bcc291 100644 --- a/src/networking.html +++ b/src/networking.html @@ -8,26 +8,13 @@ ${Help}
    -
    - -
    ${LanNetworksHelp}
    -
    -
    - -
    ${LabelBindToLocalNetworkAddressHelp}
    -
    -
    - -
    ${LabelLocalHttpServerPortNumberHelp}
    -
    - -
    - -
    ${LabelBaseUrlHelp}
    -
    -
    -

    ${HeaderHttpsSettings}

    +

    ${HeaderServerAddressSettings}

    + +
    + +
    ${LabelLocalHttpServerPortNumberHelp}
    +
    +
    + +
    ${LabelHttpsPortHelp}
    +
    + +
    + +
    ${LabelBaseUrlHelp}
    +
    + +
    + +
    ${LabelBindToLocalNetworkAddressHelp}
    +
    + +
    + +
    ${LanNetworksHelp}
    +
    +
    + +
    +

    ${HeaderHttpsSettings}

    +
    -
    - -
    ${LabelHttpsPortHelp}
    -
    -
    diff --git a/src/strings/en-us.json b/src/strings/en-us.json index da2900f43..a7191557a 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -473,6 +473,7 @@ "HeaderSeries": "Series", "HeaderSeriesOptions": "Series Options", "HeaderSeriesStatus": "Series Status", + "HeaderServerAddressSettings": "Server Address Settings", "HeaderServerSettings": "Server Settings", "HeaderSettings": "Settings", "HeaderSetupLibrary": "Setup your media libraries", @@ -627,7 +628,7 @@ "LabelEmbedAlbumArtDidl": "Embed album art in Didl", "LabelEmbedAlbumArtDidlHelp": "Some devices prefer this method for obtaining album art. Others may fail to play with this option enabled.", "LabelEnableAutomaticPortMap": "Enable automatic port mapping", - "LabelEnableAutomaticPortMapHelp": "Attempt to automatically map the public ports to local ports via UPnP. This may not work with some router models. Changes will not apply until after a server restart.", + "LabelEnableAutomaticPortMapHelp": "Automatically forward public ports on your router to local ports on your server via UPnP. This may not work with some router models or network configurations. Changes will not apply until after a server restart.", "LabelEnableBlastAliveMessages": "Blast alive messages", "LabelEnableBlastAliveMessagesHelp": "Enable this if the server is not detected reliably by other UPnP devices on your network.", "LabelEnableDlnaClientDiscoveryInterval": "Client discovery interval (seconds)", @@ -650,7 +651,7 @@ "LabelEvent": "Event:", "LabelEveryXMinutes": "Every:", "LabelBaseUrl": "Base URL:", - "LabelBaseUrlHelp": "You can add a custom subdirectory here to access the server from a more unique URL.", + "LabelBaseUrlHelp": "Adds a custom subdirectory to the server URL. For example: http://example.com/<baseurl>", "LabelExtractChaptersDuringLibraryScan": "Extract chapter images during the library scan", "LabelExtractChaptersDuringLibraryScanHelp": "Generate chapter images when videos are imported during the library scan. Otherwise, they will be extracted during the chapter images scheduled task, allowing the regular library scan to complete faster.", "LabelFailed": "Failed", From 467bda4ed76c0d22095fcd472aeb1ce642bf3056 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Mon, 11 May 2020 21:43:41 +0200 Subject: [PATCH 0048/2481] Make dialogs more bearable --- src/components/dialogHelper/dialoghelper.css | 15 ---- .../directorybrowser/directorybrowser.js | 2 +- src/components/formdialog.css | 18 ++++- .../imagedownloader/imagedownloader.js | 2 +- src/components/imageeditor/imageeditor.js | 2 +- .../imageoptionseditor/imageoptionseditor.js | 2 +- src/components/imageuploader/imageuploader.js | 2 +- .../itemidentifier/itemidentifier.js | 4 +- .../medialibrarycreator.js | 2 +- .../medialibraryeditor/medialibraryeditor.js | 2 +- .../metadataeditor/metadataeditor.js | 68 +++------------- .../metadataeditor.template.html | 78 +++++++++++-------- src/components/metadataeditor/personeditor.js | 2 +- src/dlnaprofile.html | 16 ++-- src/elements/emby-input/emby-input.css | 21 +++++ src/scheduledtask.html | 2 +- src/strings/en-us.json | 1 + 17 files changed, 112 insertions(+), 127 deletions(-) diff --git a/src/components/dialogHelper/dialoghelper.css b/src/components/dialogHelper/dialoghelper.css index df2adf075..98cfef1c5 100644 --- a/src/components/dialogHelper/dialoghelper.css +++ b/src/components/dialogHelper/dialoghelper.css @@ -126,25 +126,10 @@ } @media all and (min-width: 80em) and (min-height: 45em) { - .dialog-medium { - width: 80%; - height: 80%; - } - - .dialog-medium-tall { - width: 80%; - height: 90%; - } - .dialog-small { width: 60%; height: 80%; } - - .dialog-fullscreen-border { - width: 90%; - height: 90%; - } } .noScroll { diff --git a/src/components/directorybrowser/directorybrowser.js b/src/components/directorybrowser/directorybrowser.js index 7f13f89ef..3b1bd4ea8 100644 --- a/src/components/directorybrowser/directorybrowser.js +++ b/src/components/directorybrowser/directorybrowser.js @@ -253,7 +253,7 @@ define(['loading', 'dialogHelper', 'dom', 'globalize', 'listViewStyle', 'emby-in var systemInfo = responses[0]; var initialPath = responses[1]; var dlg = dialogHelper.createDialog({ - size: 'medium-tall', + size: 'small', removeOnClose: true, scrollY: false }); diff --git a/src/components/formdialog.css b/src/components/formdialog.css index 788331da9..2b2df601c 100644 --- a/src/components/formdialog.css +++ b/src/components/formdialog.css @@ -19,6 +19,10 @@ margin-bottom: 0; } +.formDialogHeaderTitle:first-child { + margin-left: 1em; +} + .formDialogContent:not(.no-grow) { flex-grow: 1; } @@ -46,10 +50,16 @@ right: 0; display: flex; position: absolute; - padding: 1.25em 1em; + padding: 1em 1em; /* Without this emby-checkbox is able to appear on top */ z-index: 1; + align-items: flex-end; + justify-content: end; + flex-wrap: wrap; +} + +.layout-tv .formDialogFooter { align-items: center; justify-content: center; flex-wrap: wrap; @@ -69,8 +79,12 @@ .formDialogFooterItem { margin: 0.5em !important; - flex-grow: 1; text-align: center; + flex-basis: 12em; +} + +.layout-tv .formDialogFooterItem { + flex-grow: 1; flex-basis: 0; } diff --git a/src/components/imagedownloader/imagedownloader.js b/src/components/imagedownloader/imagedownloader.js index 3e0d00ac2..184526c76 100644 --- a/src/components/imagedownloader/imagedownloader.js +++ b/src/components/imagedownloader/imagedownloader.js @@ -334,7 +334,7 @@ define(['dom', 'loading', 'apphost', 'dialogHelper', 'connectionManager', 'image if (layoutManager.tv) { dialogOptions.size = 'fullscreen'; } else { - dialogOptions.size = 'fullscreen-border'; + dialogOptions.size = 'small'; } var dlg = dialogHelper.createDialog(dialogOptions); diff --git a/src/components/imageeditor/imageeditor.js b/src/components/imageeditor/imageeditor.js index 606cdfa71..228060b8b 100644 --- a/src/components/imageeditor/imageeditor.js +++ b/src/components/imageeditor/imageeditor.js @@ -457,7 +457,7 @@ define(['dialogHelper', 'connectionManager', 'loading', 'dom', 'layoutManager', if (layoutManager.tv) { dialogOptions.size = 'fullscreen'; } else { - dialogOptions.size = 'fullscreen-border'; + dialogOptions.size = 'small'; } var dlg = dialogHelper.createDialog(dialogOptions); diff --git a/src/components/imageoptionseditor/imageoptionseditor.js b/src/components/imageoptionseditor/imageoptionseditor.js index 73a6d119d..09cbc25a0 100644 --- a/src/components/imageoptionseditor/imageoptionseditor.js +++ b/src/components/imageoptionseditor/imageoptionseditor.js @@ -87,7 +87,7 @@ define(['globalize', 'dom', 'dialogHelper', 'emby-checkbox', 'emby-select', 'emb xhr.onload = function (e) { var template = this.response; var dlg = dialogHelper.createDialog({ - size: 'medium-tall', + size: 'small', removeOnClose: true, scrollY: false }); diff --git a/src/components/imageuploader/imageuploader.js b/src/components/imageuploader/imageuploader.js index 87ecc1bc3..509bf32d6 100644 --- a/src/components/imageuploader/imageuploader.js +++ b/src/components/imageuploader/imageuploader.js @@ -137,7 +137,7 @@ define(['dialogHelper', 'connectionManager', 'dom', 'loading', 'scrollHelper', ' if (layoutManager.tv) { dialogOptions.size = 'fullscreen'; } else { - dialogOptions.size = 'fullscreen-border'; + dialogOptions.size = 'small'; } var dlg = dialogHelper.createDialog(dialogOptions); diff --git a/src/components/itemidentifier/itemidentifier.js b/src/components/itemidentifier/itemidentifier.js index 871093602..b335d1dfd 100644 --- a/src/components/itemidentifier/itemidentifier.js +++ b/src/components/itemidentifier/itemidentifier.js @@ -348,7 +348,7 @@ define(['dialogHelper', 'loading', 'connectionManager', 'require', 'globalize', currentItemType = currentItem.Type; var dialogOptions = { - size: 'fullscreen-border', + size: 'small', removeOnClose: true, scrollY: false }; @@ -429,7 +429,7 @@ define(['dialogHelper', 'loading', 'connectionManager', 'require', 'globalize', require(['text!./itemidentifier.template.html'], function (template) { var dialogOptions = { - size: 'fullscreen-border', + size: 'small', removeOnClose: true, scrollY: false }; diff --git a/src/components/medialibrarycreator/medialibrarycreator.js b/src/components/medialibrarycreator/medialibrarycreator.js index 450f5a64e..42e26c252 100644 --- a/src/components/medialibrarycreator/medialibrarycreator.js +++ b/src/components/medialibrarycreator/medialibrarycreator.js @@ -187,7 +187,7 @@ define(['loading', 'dialogHelper', 'dom', 'jQuery', 'components/libraryoptionsed xhr.onload = function (e) { var template = this.response; var dlg = dialogHelper.createDialog({ - size: 'medium-tall', + size: 'small', modal: false, removeOnClose: true, scrollY: false diff --git a/src/components/medialibraryeditor/medialibraryeditor.js b/src/components/medialibraryeditor/medialibraryeditor.js index 314150962..1a2a5623b 100644 --- a/src/components/medialibraryeditor/medialibraryeditor.js +++ b/src/components/medialibraryeditor/medialibraryeditor.js @@ -204,7 +204,7 @@ define(['jQuery', 'loading', 'dialogHelper', 'dom', 'components/libraryoptionsed xhr.onload = function (e) { var template = this.response; var dlg = dialogHelper.createDialog({ - size: 'medium-tall', + size: 'small', modal: false, removeOnClose: true, scrollY: false diff --git a/src/components/metadataeditor/metadataeditor.js b/src/components/metadataeditor/metadataeditor.js index 7f1e50b5f..3b656abab 100644 --- a/src/components/metadataeditor/metadataeditor.js +++ b/src/components/metadataeditor/metadataeditor.js @@ -245,50 +245,6 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi }); } - function showMoreMenu(context, button, user) { - - require(['itemContextMenu'], function (itemContextMenu) { - - var item = currentItem; - - itemContextMenu.show({ - - item: item, - positionTo: button, - edit: false, - editImages: true, - editSubtitles: true, - sync: false, - share: false, - play: false, - queue: false, - user: user - - }).then(function (result) { - - if (result.deleted) { - afterDeleted(context, item); - - } else if (result.updated) { - reload(context, item.Id, item.ServerId); - } - }); - }); - } - - function afterDeleted(context, item) { - - var parentId = item.ParentId || item.SeasonId || item.SeriesId; - - if (parentId) { - reload(context, parentId, item.ServerId); - } else { - require(['appRouter'], function (appRouter) { - appRouter.goHome(); - }); - } - } - function onEditorClick(e) { var btnRemoveFromEditorList = dom.parentWithClass(e.target, 'btnRemoveFromEditorList'); @@ -307,6 +263,12 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi return connectionManager.getApiClient(currentItem.ServerId); } + function bindAll(elems, eventName, fn) { + for (var i = 0, length = elems.length; i < length; i++) { + elems[i].addEventListener(eventName, fn); + } + } + function init(context, apiClient) { context.querySelector('.externalIds').addEventListener('click', function (e) { @@ -322,19 +284,11 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi } }); - context.querySelector('.btnCancel').addEventListener('click', function () { - + bindAll(context.querySelectorAll('.btnCancel'), 'click', function (event) { + event.preventDefault(); closeDialog(false); }); - context.querySelector('.btnMore').addEventListener('click', function (e) { - - getApiClient().getCurrentUser().then(function (user) { - showMoreMenu(context, e.target, user); - }); - - }); - context.querySelector('.btnHeaderSave').addEventListener('click', function (e) { context.querySelector('.btnSave').click(); @@ -349,8 +303,8 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi } }); - context.removeEventListener('click', onEditorClick); - context.addEventListener('click', onEditorClick); + context.removeEventListener('submit', onEditorClick); + context.addEventListener('submit', onEditorClick); var form = context.querySelector('form'); form.removeEventListener('submit', onSubmit); @@ -1081,7 +1035,7 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi if (layoutManager.tv) { dialogOptions.size = 'fullscreen'; } else { - dialogOptions.size = 'medium-tall'; + dialogOptions.size = 'small'; } var dlg = dialogHelper.createDialog(dialogOptions); diff --git a/src/components/metadataeditor/metadataeditor.template.html b/src/components/metadataeditor/metadataeditor.template.html index 39894d4f6..4f693da46 100644 --- a/src/components/metadataeditor/metadataeditor.template.html +++ b/src/components/metadataeditor/metadataeditor.template.html @@ -1,5 +1,4 @@
    -

    ${Edit}

    @@ -8,8 +7,8 @@ ${Save} -
    @@ -57,11 +56,13 @@
    -
    - -
    -
    - +
    +
    + +
    +
    + +
    @@ -129,24 +130,28 @@
    -
    - +
    +
    + +
    +
    + +
    -
    - -
    -
    - -
    -
    - +
    +
    + +
    +
    + +
    @@ -160,14 +165,16 @@

    ${HeaderSpecialEpisodeInfo}

    -
    - -
    -
    - -
    -
    - +
    +
    + +
    +
    + +
    +
    + +
    @@ -240,8 +247,11 @@

    +
    diff --git a/src/components/metadataeditor/personeditor.js b/src/components/metadataeditor/personeditor.js index 9fb6fdec6..511e54807 100644 --- a/src/components/metadataeditor/personeditor.js +++ b/src/components/metadataeditor/personeditor.js @@ -21,7 +21,7 @@ define(['dialogHelper', 'layoutManager', 'globalize', 'require', 'paper-icon-but if (layoutManager.tv) { dialogOptions.size = 'fullscreen'; } else { - dialogOptions.size = 'medium-tall'; + dialogOptions.size = 'small'; } var dlg = dialogHelper.createDialog(dialogOptions); diff --git a/src/dlnaprofile.html b/src/dlnaprofile.html index 45182fa59..edaee0e6f 100644 --- a/src/dlnaprofile.html +++ b/src/dlnaprofile.html @@ -272,7 +272,7 @@
    -
    +

    ${HeaderDirectPlayProfile}

    @@ -312,7 +312,7 @@
    -
    +

    ${HeaderTranscodingProfile}

    @@ -394,7 +394,7 @@
    -
    +

    ${HeaderContainerProfile}

    @@ -426,7 +426,7 @@
    -
    +

    ${HeaderCodecProfile}

    @@ -455,7 +455,7 @@
    -
    +

    ${HeaderResponseProfile}

    @@ -495,7 +495,7 @@
    -
    +

    ${HeaderIdentificationHeader}

    @@ -525,7 +525,7 @@
    -
    +

    ${HeaderXmlDocumentAttribute}

    @@ -548,7 +548,7 @@
    -
    +

    ${HeaderSubtitleProfile}

    diff --git a/src/elements/emby-input/emby-input.css b/src/elements/emby-input/emby-input.css index 18ad37a87..195e68027 100644 --- a/src/elements/emby-input/emby-input.css +++ b/src/elements/emby-input/emby-input.css @@ -29,6 +29,27 @@ margin-bottom: 1.8em; } +.inlineForm { + display: flex; +} + +.inlineForm .inputContainer, +.inlineForm .selectContainer { + flex-basis: 0; + flex-grow: 1; + margin: 0 0.5em 1.8em; +} + +.inlineForm .inputContainer:first-child, +.inlineForm .selectContainer:first-child { + margin-left: 0; +} + +.inlineForm .inputContainer:last-child, +.inlineForm .selectContainer:last-child { + margin-right: 0; +} + .inputLabel { display: inline-block; margin-bottom: 0.25em; diff --git a/src/scheduledtask.html b/src/scheduledtask.html index 6cfc8ace4..6789e5990 100644 --- a/src/scheduledtask.html +++ b/src/scheduledtask.html @@ -20,7 +20,7 @@
    -
    +

    ${HeaderAddScheduledTaskTrigger}

    diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 9895bbc05..ee47d676a 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1303,6 +1303,7 @@ "Runtime": "Runtime", "Saturday": "Saturday", "Save": "Save", + "SaveChanges": "Save changes", "SaveSubtitlesIntoMediaFolders": "Save subtitles into media folders", "SaveSubtitlesIntoMediaFoldersHelp": "Storing subtitles next to video files will allow them to be more easily managed.", "ScanForNewAndUpdatedFiles": "Scan for new and updated files", From 88706e794ab9f682fc70038cac4c7b9750e27b3c Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Mon, 11 May 2020 22:23:08 +0200 Subject: [PATCH 0049/2481] Add TV and Mobile compatibility to the new metadata dialog --- src/components/metadataeditor/metadataeditor.js | 5 +++++ src/components/metadataeditor/metadataeditor.template.html | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/metadataeditor/metadataeditor.js b/src/components/metadataeditor/metadataeditor.js index 3b656abab..41d400062 100644 --- a/src/components/metadataeditor/metadataeditor.js +++ b/src/components/metadataeditor/metadataeditor.js @@ -284,6 +284,11 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi } }); + if (!layoutManager.desktop) { + context.querySelector('.btnBack').classList.remove('hide'); + context.querySelector('.btnClose').classList.add('hide'); + } + bindAll(context.querySelectorAll('.btnCancel'), 'click', function (event) { event.preventDefault(); closeDialog(false); diff --git a/src/components/metadataeditor/metadataeditor.template.html b/src/components/metadataeditor/metadataeditor.template.html index 4f693da46..4b4e2cf96 100644 --- a/src/components/metadataeditor/metadataeditor.template.html +++ b/src/components/metadataeditor/metadataeditor.template.html @@ -1,4 +1,5 @@
    +

    ${Edit}

    @@ -7,7 +8,7 @@ ${Save} -
    From 8d7b8104ff35d786ac3969c5d736751a1202ffa3 Mon Sep 17 00:00:00 2001 From: ferferga Date: Thu, 14 May 2020 11:58:13 +0200 Subject: [PATCH 0050/2481] Overflow with scrollbar size --- src/assets/css/site.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/assets/css/site.css b/src/assets/css/site.css index e59b639f4..4ef017f7b 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.css @@ -3,6 +3,7 @@ html { margin: 0; padding: 0; height: 100%; + overflow-y: scroll; } .material-icons { From 3250100fad772e87107f941800f4b3dfbb730371 Mon Sep 17 00:00:00 2001 From: ferferga Date: Thu, 14 May 2020 14:11:11 +0200 Subject: [PATCH 0051/2481] Apply style once an skin is loaded --- src/assets/css/site.css | 1 - src/components/skinManager.js | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/assets/css/site.css b/src/assets/css/site.css index 963630d85..627145abc 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.css @@ -3,7 +3,6 @@ html { margin: 0; padding: 0; height: 100%; - overflow-y: scroll; } .clipForScreenReader { diff --git a/src/components/skinManager.js b/src/components/skinManager.js index 871b6d999..0b954cac5 100644 --- a/src/components/skinManager.js +++ b/src/components/skinManager.js @@ -137,6 +137,7 @@ define(['apphost', 'userSettings', 'browser', 'events', 'pluginManager', 'backdr function onViewBeforeShow(e) { if (e.detail && e.detail.type === 'video-osd') { + document.getElementsByTagName('body')[0].style = null; return; } @@ -155,6 +156,7 @@ define(['apphost', 'userSettings', 'browser', 'events', 'pluginManager', 'backdr } } } + document.getElementsByTagName('body')[0].style = 'overflow-y: scroll'; } document.addEventListener('viewshow', onViewBeforeShow); From 6250f5bca857eeefdd1cdb784c70a19aee20f89d Mon Sep 17 00:00:00 2001 From: ferferga Date: Thu, 14 May 2020 14:18:29 +0200 Subject: [PATCH 0052/2481] Hide scrollbar while the videoosd component is loading --- src/components/htmlvideoplayer/plugin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js index a1e51b44f..8a7175200 100644 --- a/src/components/htmlvideoplayer/plugin.js +++ b/src/components/htmlvideoplayer/plugin.js @@ -109,6 +109,7 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa function hidePrePlaybackPage() { let animatedPage = document.querySelector('.page:not(.hide)'); animatedPage.classList.add('hide'); + document.getElementsByTagName('body')[0].style = null; } function zoomIn(elem) { From 26267e382732d1d73ff429aad4bc7c5a6225bbe6 Mon Sep 17 00:00:00 2001 From: ferferga Date: Thu, 14 May 2020 14:18:50 +0200 Subject: [PATCH 0053/2481] Add comments for future reference --- src/components/htmlvideoplayer/plugin.js | 1 + src/components/skinManager.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js index 8a7175200..987117d17 100644 --- a/src/components/htmlvideoplayer/plugin.js +++ b/src/components/htmlvideoplayer/plugin.js @@ -109,6 +109,7 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa function hidePrePlaybackPage() { let animatedPage = document.querySelector('.page:not(.hide)'); animatedPage.classList.add('hide'); + // At this point, we must hide the scrollbar placeholder, so it's not being displayed while the item is being loaded document.getElementsByTagName('body')[0].style = null; } diff --git a/src/components/skinManager.js b/src/components/skinManager.js index 0b954cac5..fe64df1f2 100644 --- a/src/components/skinManager.js +++ b/src/components/skinManager.js @@ -137,6 +137,7 @@ define(['apphost', 'userSettings', 'browser', 'events', 'pluginManager', 'backdr function onViewBeforeShow(e) { if (e.detail && e.detail.type === 'video-osd') { + // This removes the space that the scrollbar takes while playing a video document.getElementsByTagName('body')[0].style = null; return; } @@ -156,6 +157,8 @@ define(['apphost', 'userSettings', 'browser', 'events', 'pluginManager', 'backdr } } } + // This keeps the scrollbar always present in all pages, so we avoid clipping while switching between pages + // that need the scrollbar and pages that don't. document.getElementsByTagName('body')[0].style = 'overflow-y: scroll'; } From d2bd631cbe93768d36f9bb797be1e1d4a2e9b5b8 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Thu, 14 May 2020 23:25:22 +0200 Subject: [PATCH 0054/2481] Fix more sonarqube bugs --- src/components/apphost.js | 40 ++++++++++++++----- src/components/htmlvideoplayer/plugin.js | 15 ------- .../libraryoptionseditor.js | 7 +++- src/components/skinManager.js | 1 - .../dashboard/metadataimagespage.js | 12 ++++-- .../dashboard/notifications/notification.js | 3 -- src/controllers/playback/videoosd.js | 2 +- src/dlnaprofiles.html | 2 +- src/elements/emby-slider/emby-slider.js | 2 +- src/scripts/browser.js | 22 +++------- src/scripts/browserdeviceprofile.js | 17 -------- src/scripts/editorsidebar.js | 4 +- src/scripts/librarymenu.js | 21 +++++----- src/scripts/playlists.js | 3 +- src/scripts/site.js | 17 +++----- 15 files changed, 72 insertions(+), 96 deletions(-) diff --git a/src/components/apphost.js b/src/components/apphost.js index 75e8ba17f..2fe0cbd33 100644 --- a/src/components/apphost.js +++ b/src/components/apphost.js @@ -5,7 +5,7 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g var disableHlsVideoAudioCodecs = []; if (item && htmlMediaHelper.enableHlsJsPlayer(item.RunTimeTicks, item.MediaType)) { - if (browser.edge || browser.msie) { + if (browser.edge) { disableHlsVideoAudioCodecs.push('mp3'); } @@ -93,18 +93,36 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g function getDeviceName() { var deviceName; - deviceName = browser.tizen ? 'Samsung Smart TV' : browser.web0s ? 'LG Smart TV' : browser.operaTv ? 'Opera TV' : browser.xboxOne ? 'Xbox One' : browser.ps4 ? 'Sony PS4' : browser.chrome ? 'Chrome' : browser.edge ? 'Edge' : browser.firefox ? 'Firefox' : browser.msie ? 'Internet Explorer' : browser.opera ? 'Opera' : browser.safari ? 'Safari' : 'Web Browser'; + if (browser.tizen) { + deviceName = 'Samsung Smart TV'; + } else if (browser.web0s) { + deviceName = 'LG Smart TV'; + } else if (browser.operaTv) { + deviceName = 'Opera TV'; + } else if (browser.xboxOne) { + deviceName = 'Xbox One'; + } else if (browser.ps4) { + deviceName = 'Sony PS4'; + } else if (browser.chrome) { + deviceName = 'Chrome'; + } else if (browser.edge) { + deviceName = 'Edge'; + } else if (browser.firefox) { + deviceName = 'Firefox'; + } else if (browser.opera) { + deviceName = 'Opera'; + } else if (browser.safari) { + deviceName = 'Safari'; + } else { + deviceName = 'Web Browser'; + } if (browser.ipad) { deviceName += ' iPad'; - } else { - if (browser.iphone) { - deviceName += ' iPhone'; - } else { - if (browser.android) { - deviceName += ' Android'; - } - } + } else if (browser.iphone) { + deviceName += ' iPhone'; + } else if (browser.android) { + deviceName += ' Android'; } return deviceName; @@ -267,7 +285,7 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g if (enabled) features.push('multiserver'); }); - if (!browser.orsay && !browser.msie && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) { + if (!browser.orsay && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) { features.push('subtitleappearancesettings'); } diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js index a1e51b44f..bab41eb36 100644 --- a/src/components/htmlvideoplayer/plugin.js +++ b/src/components/htmlvideoplayer/plugin.js @@ -279,14 +279,6 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa } self.play = function (options) { - - if (browser.msie) { - if (options.playMethod === 'Transcode' && !window.MediaSource) { - alert('Playback of this content is not supported in Internet Explorer. For a better experience, try a modern browser such as Microsoft Edge, Google Chrome, Firefox or Opera.'); - return Promise.reject(); - } - } - self._started = false; self._timeUpdated = false; @@ -1411,13 +1403,6 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa var video = document.createElement('video'); if (video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function' || document.pictureInPictureEnabled) { list.push('PictureInPicture'); - } else if (browser.ipad) { - // Unfortunately this creates a false positive on devices where its' not actually supported - if (navigator.userAgent.toLowerCase().indexOf('os 9') === -1) { - if (video.webkitSupportsPresentationMode && video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function') { - list.push('PictureInPicture'); - } - } } else if (window.Windows) { if (Windows.UI.ViewManagement.ApplicationView.getForCurrentView().isViewModeSupported(Windows.UI.ViewManagement.ApplicationViewMode.compactOverlay)) { list.push('PictureInPicture'); diff --git a/src/components/libraryoptionseditor/libraryoptionseditor.js b/src/components/libraryoptionseditor/libraryoptionseditor.js index fec465640..832a6ffc5 100644 --- a/src/components/libraryoptionseditor/libraryoptionseditor.js +++ b/src/components/libraryoptionseditor/libraryoptionseditor.js @@ -120,7 +120,12 @@ define(['globalize', 'dom', 'emby-checkbox', 'emby-select', 'emby-input'], funct html += plugin.Name; html += ''; html += '
    '; - index > 0 ? html += '' : plugins.length > 1 && (html += ''), html += '
    '; + if (index > 0) { + html += ''; + } else if (plugins.length > 1) { + html += ''; + } + html += '
    '; }); html += '
    '; diff --git a/src/components/skinManager.js b/src/components/skinManager.js index 871b6d999..5c9fe71a5 100644 --- a/src/components/skinManager.js +++ b/src/components/skinManager.js @@ -57,7 +57,6 @@ define(['apphost', 'userSettings', 'browser', 'events', 'pluginManager', 'backdr var selectedTheme; for (var i = 0, length = themes.length; i < length; i++) { - var theme = themes[i]; if (theme[isDefaultProperty]) { defaultTheme = theme; diff --git a/src/controllers/dashboard/metadataimagespage.js b/src/controllers/dashboard/metadataimagespage.js index 277eecb42..3047736a6 100644 --- a/src/controllers/dashboard/metadataimagespage.js +++ b/src/controllers/dashboard/metadataimagespage.js @@ -29,14 +29,18 @@ define(['jQuery', 'dom', 'loading', 'libraryMenu', 'globalize', 'listViewStyle'] var promises = [ApiClient.getServerConfiguration(), populateLanguages(page.querySelector('#selectLanguage')), populateCountries(page.querySelector('#selectCountry'))]; Promise.all(promises).then(function(responses) { var config = responses[0]; - page.querySelector('#selectLanguage').value = config.PreferredMetadataLanguage || '', page.querySelector('#selectCountry').value = config.MetadataCountryCode || '', loading.hide(); + page.querySelector('#selectLanguage').value = config.PreferredMetadataLanguage || ''; + page.querySelector('#selectCountry').value = config.MetadataCountryCode || ''; + loading.hide(); }); } function onSubmit() { var form = this; return loading.show(), ApiClient.getServerConfiguration().then(function(config) { - config.PreferredMetadataLanguage = form.querySelector('#selectLanguage').value, config.MetadataCountryCode = form.querySelector('#selectCountry').value, ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult); + config.PreferredMetadataLanguage = form.querySelector('#selectLanguage').value; + config.MetadataCountryCode = form.querySelector('#selectCountry').value; + ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult); }), !1; } @@ -59,6 +63,8 @@ define(['jQuery', 'dom', 'loading', 'libraryMenu', 'globalize', 'listViewStyle'] $(document).on('pageinit', '#metadataImagesConfigurationPage', function() { $('.metadataImagesConfigurationForm').off('submit', onSubmit).on('submit', onSubmit); }).on('pageshow', '#metadataImagesConfigurationPage', function() { - libraryMenu.setTabs('metadata', 2, getTabs), loading.show(), loadPage(this); + libraryMenu.setTabs('metadata', 2, getTabs); + loading.show(); + loadPage(this); }); }); diff --git a/src/controllers/dashboard/notifications/notification.js b/src/controllers/dashboard/notifications/notification.js index 90d453cda..2b6f7a148 100644 --- a/src/controllers/dashboard/notifications/notification.js +++ b/src/controllers/dashboard/notifications/notification.js @@ -73,9 +73,6 @@ define(['jQuery', 'emby-checkbox', 'fnchecked'], function ($) { notificationOptions.Options.push(notificationConfig); } - types.filter(function (n) { - return n.Type == type; - })[0]; notificationConfig.Enabled = $('#chkEnabled', page).checked(); notificationConfig.SendToUserMode = $('#selectUsers', page).val(); notificationConfig.DisabledMonitorUsers = $('.chkMonitor', page).get().filter(function (c) { diff --git a/src/controllers/playback/videoosd.js b/src/controllers/playback/videoosd.js index e9923d779..ac42554b3 100644 --- a/src/controllers/playback/videoosd.js +++ b/src/controllers/playback/videoosd.js @@ -157,7 +157,7 @@ define(['playbackManager', 'dom', 'inputManager', 'datetime', 'itemHelper', 'med }); if (!displayName) { - displayItem.Type; + displayName = displayItem.Type; } titleElement.innerHTML = displayName; diff --git a/src/dlnaprofiles.html b/src/dlnaprofiles.html index 9f2a5e129..b47b2fd6b 100644 --- a/src/dlnaprofiles.html +++ b/src/dlnaprofiles.html @@ -11,7 +11,7 @@ - ${Help} + ${Help}

    ${CustomDlnaProfilesHelp}

    diff --git a/src/elements/emby-slider/emby-slider.js b/src/elements/emby-slider/emby-slider.js index e37455dfe..1b78fca0a 100644 --- a/src/elements/emby-slider/emby-slider.js +++ b/src/elements/emby-slider/emby-slider.js @@ -148,7 +148,7 @@ define(['browser', 'dom', 'layoutManager', 'keyboardnavigation', 'css!./emby-sli this.classList.add('mdl-slider'); this.classList.add('mdl-js-slider'); - if (browser.edge || browser.msie) { + if (browser.edge) { this.classList.add('slider-browser-edge'); } if (!layoutManager.mobile) { diff --git a/src/scripts/browser.js b/src/scripts/browser.js index b1912862b..a377f08fd 100644 --- a/src/scripts/browser.js +++ b/src/scripts/browser.js @@ -14,10 +14,6 @@ define([], function () { return true; } - if (userAgent.indexOf('nintendo') !== -1) { - return true; - } - if (userAgent.indexOf('viera') !== -1) { return true; } @@ -93,7 +89,7 @@ define([], function () { var _supportsCssAnimation; var _supportsCssAnimationWithPrefix; function supportsCssAnimation(allowPrefix) { - + // TODO: Assess if this is still needed, as all of our targets should natively support CSS animations. if (allowPrefix) { if (_supportsCssAnimationWithPrefix === true || _supportsCssAnimationWithPrefix === false) { return _supportsCssAnimationWithPrefix; @@ -145,7 +141,6 @@ define([], function () { /(chrome)[ \/]([\w.]+)/.exec(ua) || /(safari)[ \/]([\w.]+)/.exec(ua) || /(firefox)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || []; @@ -161,14 +156,6 @@ define([], function () { if (browser === 'edge') { platform_match = ['']; - } else { - if (ua.indexOf('windows phone') !== -1 || ua.indexOf('iemobile') !== -1) { - - // http://www.neowin.net/news/ie11-fakes-user-agent-to-fool-gmail-in-windows-phone-81-gdr1-update - browser = 'msie'; - } else if (ua.indexOf('like gecko') !== -1 && ua.indexOf('webkit') === -1 && ua.indexOf('opera') === -1 && ua.indexOf('chrome') === -1 && ua.indexOf('safari') === -1) { - browser = 'msie'; - } } if (browser === 'opr') { @@ -211,7 +198,7 @@ define([], function () { browser[matched.platform] = true; } - if (!browser.chrome && !browser.msie && !browser.edge && !browser.opera && userAgent.toLowerCase().indexOf('webkit') !== -1) { + if (!browser.chrome && !browser.edge && !browser.opera && userAgent.toLowerCase().indexOf('webkit') !== -1) { browser.safari = true; } @@ -224,7 +211,10 @@ define([], function () { browser.mobile = true; } - browser.xboxOne = userAgent.toLowerCase().indexOf('xbox') !== -1; + if (userAgent.toLowerCase().indexOf('xbox') !== -1) { + browser.xboxOne = true; + browser.tv = true; + } browser.animate = typeof document !== 'undefined' && document.documentElement.animate != null; browser.tizen = userAgent.toLowerCase().indexOf('tizen') !== -1 || self.tizen != null; browser.web0s = userAgent.toLowerCase().indexOf('Web0S'.toLowerCase()) !== -1; diff --git a/src/scripts/browserdeviceprofile.js b/src/scripts/browserdeviceprofile.js index 2668f2091..a550b6919 100644 --- a/src/scripts/browserdeviceprofile.js +++ b/src/scripts/browserdeviceprofile.js @@ -188,20 +188,6 @@ define(['browser'], function (browser) { return browser.tizen || browser.orsay || browser.web0s || browser.edgeUwp; } - function getFlvMseDirectPlayProfile() { - var videoAudioCodecs = ['aac']; - if (!browser.edge && !browser.msie) { - videoAudioCodecs.push('mp3'); - } - - return { - Container: 'flv', - Type: 'Video', - VideoCodec: 'h264', - AudioCodec: videoAudioCodecs.join(',') - }; - } - function getDirectPlayProfileForVideoContainer(container, videoAudioCodecs, videoTestElement, options) { var supported = false; var profileContainer = container; @@ -230,9 +216,6 @@ define(['browser'], function (browser) { break; case 'flv': supported = browser.tizen || browser.orsay; - //if (!supported && window.MediaSource != null && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"')) { - // return getFlvMseDirectPlayProfile(); - //} break; case '3gp': case 'mts': diff --git a/src/scripts/editorsidebar.js b/src/scripts/editorsidebar.js index ee7788407..816838956 100644 --- a/src/scripts/editorsidebar.js +++ b/src/scripts/editorsidebar.js @@ -209,7 +209,7 @@ define(['datetime', 'jQuery', 'globalize', 'material-icons'], function (datetime function onNodeOpen(event, data) { var page = $(this).parents('.page')[0]; var node = data.node; - if (node.children && node.children) { + if (node.children) { loadNodesToLoad(page, node); } if (node.li_attr && node.id != '#' && !node.li_attr.loadedFromServer) { @@ -221,7 +221,7 @@ define(['datetime', 'jQuery', 'globalize', 'material-icons'], function (datetime function onNodeLoad(event, data) { var page = $(this).parents('.page')[0]; var node = data.node; - if (node.children && node.children) { + if (node.children) { loadNodesToLoad(page, node); } if (node.li_attr && node.id != '#' && !node.li_attr.loadedFromServer) { diff --git a/src/scripts/librarymenu.js b/src/scripts/librarymenu.js index 81a381bff..669284ec4 100644 --- a/src/scripts/librarymenu.js +++ b/src/scripts/librarymenu.js @@ -567,27 +567,25 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' if (libraryMenuOptions) { getUserViews(apiClient, userId).then(function (result) { var items = result; - var html = ''; - html += '

    '; - html += globalize.translate('HeaderMedia'); - html += '

    '; + var html = `

    ${globalize.translate('HeaderMedia')}

    `; html += items.map(function (i) { var icon = i.icon || imageHelper.getLibraryIcon(i.CollectionType); var itemId = i.Id; - if (i.onclick) { - i.onclick; - } + const linkHtml = ` + + ${i.Name} + `; - return '' + i.Name + ''; + return linkHtml; }).join(''); libraryMenuOptions.innerHTML = html; var elem = libraryMenuOptions; var sidebarLinks = elem.querySelectorAll('.navMenuOption'); - for (var i = 0, length = sidebarLinks.length; i < length; i++) { - sidebarLinks[i].removeEventListener('click', onSidebarLinkClick); - sidebarLinks[i].addEventListener('click', onSidebarLinkClick); + for (const sidebarLink of sidebarLinks) { + sidebarLink.removeEventListener('click', onSidebarLinkClick); + sidebarLink.addEventListener('click', onSidebarLinkClick); } }); } @@ -900,6 +898,7 @@ define(['dom', 'layoutManager', 'inputManager', 'connectionManager', 'events', ' updateMenuForPageType(isDashboardPage, isLibraryPage); + // TODO: Seems to do nothing? Check if needed (also in other views). if (!e.detail.isRestored) { window.scrollTo(0, 0); } diff --git a/src/scripts/playlists.js b/src/scripts/playlists.js index 52e7ccb3b..974d00f8e 100644 --- a/src/scripts/playlists.js +++ b/src/scripts/playlists.js @@ -69,10 +69,11 @@ define(['loading', 'listView', 'cardBuilder', 'libraryMenu', 'libraryBrowser', ' showLoadingMessage(); var query = getQuery(view); var promise1 = ApiClient.getItems(Dashboard.getCurrentUserId(), query); + // TODO: promise2 is unused, check if necessary. var promise2 = Dashboard.getCurrentUser(); Promise.all([promise1, promise2]).then(function (responses) { var result = responses[0]; - responses[1]; + // TODO: Is the scroll necessary? window.scrollTo(0, 0); var html = ''; var viewStyle = getPageData(view).view; diff --git a/src/scripts/site.js b/src/scripts/site.js index c00169d22..939d362da 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -368,8 +368,7 @@ var AppInfo = {}; } } - function initRequireWithBrowser(browser) { - var bowerPath = getBowerPath(); + function initRequireWithBrowser() { var componentsPath = getComponentsPath(); var scriptsPath = getScriptsPath(); @@ -378,13 +377,7 @@ var AppInfo = {}; define('lazyLoader', [componentsPath + '/lazyloader/lazyloader-intersectionobserver'], returnFirstDependency); define('shell', [componentsPath + '/shell'], returnFirstDependency); - if ('registerElement' in document) { - define('registerElement', []); - } else if (browser.msie) { - define('registerElement', ['webcomponents'], returnFirstDependency); - } else { - define('registerElement', ['document-register-element'], returnFirstDependency); - } + define('registerElement', ['document-register-element'], returnFirstDependency); define('alert', [componentsPath + '/alert'], returnFirstDependency); @@ -609,8 +602,8 @@ var AppInfo = {}; /* eslint-enable compat/compat */ } - function onWebComponentsReady(browser) { - initRequireWithBrowser(browser); + function onWebComponentsReady() { + initRequireWithBrowser(); if (self.appMode === 'cordova' || self.appMode === 'android' || self.appMode === 'standalone') { AppInfo.isNativeApp = true; @@ -1127,7 +1120,7 @@ var AppInfo = {}; }); })(); - return require(['browser'], onWebComponentsReady); + return onWebComponentsReady(); }(); pageClassOn('viewshow', 'standalonePage', function () { From d4fbbea9915125142b7c41c999cbf9ce9476b487 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Thu, 14 May 2020 23:34:23 +0200 Subject: [PATCH 0055/2481] Remove code smells --- src/controllers/dashboard/notifications/notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/dashboard/notifications/notification.js b/src/controllers/dashboard/notifications/notification.js index 2b6f7a148..8654f3e99 100644 --- a/src/controllers/dashboard/notifications/notification.js +++ b/src/controllers/dashboard/notifications/notification.js @@ -58,10 +58,10 @@ define(['jQuery', 'emby-checkbox', 'fnchecked'], function ($) { function save(page) { var type = getParameterByName('type'); var promise1 = ApiClient.getNamedConfiguration(notificationsConfigurationKey); + // TODO: Check if this promise is really needed, as it's unused. var promise2 = ApiClient.getJSON(ApiClient.getUrl('Notifications/Types')); Promise.all([promise1, promise2]).then(function (responses) { var notificationOptions = responses[0]; - var types = responses[1]; var notificationConfig = notificationOptions.Options.filter(function (n) { return n.Type == type; })[0]; From ac060e1270a33c19e55ec8ae7c6ae2271f007b8f Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Thu, 14 May 2020 23:55:23 +0200 Subject: [PATCH 0056/2481] Fix some code smells --- .../accessschedule/accessschedule.js | 6 ++-- src/components/actionsheet/actionsheet.js | 33 ++++--------------- src/components/activitylog.js | 8 +++-- src/components/appRouter.js | 21 +++++------- 4 files changed, 24 insertions(+), 44 deletions(-) diff --git a/src/components/accessschedule/accessschedule.js b/src/components/accessschedule/accessschedule.js index 870231cf0..caf3e8204 100644 --- a/src/components/accessschedule/accessschedule.js +++ b/src/components/accessschedule/accessschedule.js @@ -72,12 +72,12 @@ define(['dialogHelper', 'datetime', 'globalize', 'emby-select', 'paper-icon-butt reject(); } }); - dlg.querySelector('.btnCancel').addEventListener('click', function (e) { + dlg.querySelector('.btnCancel').addEventListener('click', function () { dialogHelper.close(dlg); }); - dlg.querySelector('form').addEventListener('submit', function (e) { + dlg.querySelector('form').addEventListener('submit', function (event) { submitSchedule(dlg, options); - e.preventDefault(); + event.preventDefault(); return false; }); }; diff --git a/src/components/actionsheet/actionsheet.js b/src/components/actionsheet/actionsheet.js index e08fbf4a2..ab8998f1b 100644 --- a/src/components/actionsheet/actionsheet.js +++ b/src/components/actionsheet/actionsheet.js @@ -11,20 +11,10 @@ define(['dialogHelper', 'layoutManager', 'globalize', 'browser', 'dom', 'emby-bu } var box; - var elem; + for (let [index, elem] of elems) { + box = elem.getBoundingClientRect(); - for (var i = 0, length = elems.length; i < length; i++) { - - elem = elems[i]; - // Support: BlackBerry 5, iOS 3 (original iPhone) - // If we don't have gBCR, just use 0,0 rather than error - if (elem.getBoundingClientRect) { - box = elem.getBoundingClientRect(); - } else { - box = { top: 0, left: 0 }; - } - - results[i] = { + results[index] = { top: box.top, left: box.left, width: box.width, @@ -96,13 +86,11 @@ define(['dialogHelper', 'layoutManager', 'globalize', 'browser', 'dom', 'emby-bu scrollY: false }; - var backButton = false; var isFullscreen; if (layoutManager.tv) { dialogOptions.size = 'fullscreen'; isFullscreen = true; - backButton = true; dialogOptions.autoFocus = true; } else { @@ -139,16 +127,10 @@ define(['dialogHelper', 'layoutManager', 'globalize', 'browser', 'dom', 'emby-bu style += 'min-width:' + minWidth + 'px;'; } - var i; - var length; - var option; var renderIcon = false; var icons = []; var itemIcon; - for (i = 0, length = options.items.length; i < length; i++) { - - option = options.items[i]; - + for (let option of options.items) { itemIcon = option.icon || (option.selected ? 'check' : null); if (itemIcon) { @@ -206,10 +188,7 @@ define(['dialogHelper', 'layoutManager', 'globalize', 'browser', 'dom', 'emby-bu menuItemClass += ' actionsheet-xlargeFont'; } - for (i = 0, length = options.items.length; i < length; i++) { - - option = options.items[i]; - + for (let [index, option] of options) { if (option.divider) { html += '
    '; @@ -222,7 +201,7 @@ define(['dialogHelper', 'layoutManager', 'globalize', 'browser', 'dom', 'emby-bu var optionId = option.id == null || option.id === '' ? option.value : option.id; html += ''; - itemIcon = icons[i]; + itemIcon = icons[index]; if (itemIcon) { diff --git a/src/components/activitylog.js b/src/components/activitylog.js index a7b3f48bc..bbb099506 100644 --- a/src/components/activitylog.js +++ b/src/components/activitylog.js @@ -34,10 +34,14 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', html += '
    '; if (entry.Overview) { - html += ''; + html += ``; } - return html += '
    '; + html += '
    '; + + return html; } function renderList(elem, apiClient, result, startIndex, limit) { diff --git a/src/components/appRouter.js b/src/components/appRouter.js index 2e11ef88d..77b64f8e8 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -26,11 +26,11 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM connectionManager.connect({ enableAutoLogin: appSettings.enableAutoLogin() }).then(function (result) { - handleConnectionResult(result, loading); + handleConnectionResult(result); }); } - function handleConnectionResult(result, loading) { + function handleConnectionResult(result) { switch (result.State) { case 'SignedIn': loading.hide(); @@ -246,13 +246,11 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } if (setQuality) { - - var quality = 100; - + var quality; var type = options.type || 'Primary'; if (browser.tv || browser.slow) { - + // TODO: wtf if (browser.chrome) { // webp support quality = type === 'Primary' ? 40 : 50; @@ -384,7 +382,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM if (firstResult.State !== 'SignedIn' && !route.anonymous) { - handleConnectionResult(firstResult, loading); + handleConnectionResult(firstResult); return; } } @@ -463,7 +461,6 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM return Promise.resolve(); } - var isHandlingBackToDefault; var isDummyBackToHome; function loadContent(ctx, route, html, request) { @@ -589,8 +586,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM path = '/' + path; } - var baseRoute = baseUrl(); - path = path.replace(baseRoute, ''); + path = path.replace(baseUrl(), ''); if (currentRouteInfo && currentRouteInfo.path === path) { // can't use this with home right now due to the back menu @@ -621,10 +617,11 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } function showItem(item, serverId, options) { + // TODO: Refactor this so it only gets items, not strings. if (typeof (item) === 'string') { var apiClient = serverId ? connectionManager.getApiClient(serverId) : connectionManager.currentApiClient(); - apiClient.getItem(apiClient.getCurrentUserId(), item).then(function (item) { - appRouter.showItem(item, options); + apiClient.getItem(apiClient.getCurrentUserId(), item).then(function (itemObject) { + appRouter.showItem(itemObject, options); }); } else { if (arguments.length === 2) { From 594580ec2d50593f599dea02db4d126ffd75dfb3 Mon Sep 17 00:00:00 2001 From: Guilherme Danno Date: Sat, 16 May 2020 00:32:32 -0300 Subject: [PATCH 0057/2481] move files to scss creating this commit so git don't lose track of the changes --- .../css/{clearbutton.css => clearbutton.scss} | 0 .../css/{detailtable.css => detailtable.scss} | 0 .../css/{flexstyles.css => flexstyles.scss} | 0 src/assets/css/{fonts.css => fonts.scss} | 0 .../css/{fonts.sized.css => fonts.sized.scss} | 0 src/assets/css/{ios.css => ios.scss} | 0 src/assets/css/{livetv.css => livetv.scss} | 0 src/assets/css/{site.css => site.scss} | 63 +++++++++---------- 8 files changed, 29 insertions(+), 34 deletions(-) rename src/assets/css/{clearbutton.css => clearbutton.scss} (100%) rename src/assets/css/{detailtable.css => detailtable.scss} (100%) rename src/assets/css/{flexstyles.css => flexstyles.scss} (100%) rename src/assets/css/{fonts.css => fonts.scss} (100%) rename src/assets/css/{fonts.sized.css => fonts.sized.scss} (100%) rename src/assets/css/{ios.css => ios.scss} (100%) rename src/assets/css/{livetv.css => livetv.scss} (100%) rename src/assets/css/{site.css => site.scss} (76%) diff --git a/src/assets/css/clearbutton.css b/src/assets/css/clearbutton.scss similarity index 100% rename from src/assets/css/clearbutton.css rename to src/assets/css/clearbutton.scss diff --git a/src/assets/css/detailtable.css b/src/assets/css/detailtable.scss similarity index 100% rename from src/assets/css/detailtable.css rename to src/assets/css/detailtable.scss diff --git a/src/assets/css/flexstyles.css b/src/assets/css/flexstyles.scss similarity index 100% rename from src/assets/css/flexstyles.css rename to src/assets/css/flexstyles.scss diff --git a/src/assets/css/fonts.css b/src/assets/css/fonts.scss similarity index 100% rename from src/assets/css/fonts.css rename to src/assets/css/fonts.scss diff --git a/src/assets/css/fonts.sized.css b/src/assets/css/fonts.sized.scss similarity index 100% rename from src/assets/css/fonts.sized.css rename to src/assets/css/fonts.sized.scss diff --git a/src/assets/css/ios.css b/src/assets/css/ios.scss similarity index 100% rename from src/assets/css/ios.css rename to src/assets/css/ios.scss diff --git a/src/assets/css/livetv.css b/src/assets/css/livetv.scss similarity index 100% rename from src/assets/css/livetv.css rename to src/assets/css/livetv.scss diff --git a/src/assets/css/site.css b/src/assets/css/site.scss similarity index 76% rename from src/assets/css/site.css rename to src/assets/css/site.scss index 627145abc..db179afb2 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.scss @@ -1,19 +1,19 @@ -body, -html { +@mixin fullpage { margin: 0; padding: 0; height: 100%; } -.clipForScreenReader { - clip: rect(1px, 1px, 1px, 1px); - clip-path: inset(50%); - height: 1px; - width: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; +html { + @include fullpage; + line-height: 1.35; +} + +body { + @include fullpage; + overflow-x: hidden; + background-color: transparent !important; + -webkit-font-smoothing: antialiased; } .material-icons { @@ -30,10 +30,6 @@ html { contain: strict; } -html { - line-height: 1.35; -} - .layout-mobile, .layout-tv { -webkit-touch-callout: none; @@ -44,12 +40,6 @@ html { user-select: none; } -body { - overflow-x: hidden; - background-color: transparent !important; - -webkit-font-smoothing: antialiased; -} - .mainAnimatedPage { contain: style size !important; } @@ -62,7 +52,7 @@ body { overflow-y: hidden !important; } -div[data-role=page] { +div[data-role="page"] { outline: 0; } @@ -75,10 +65,10 @@ div[data-role=page] { padding-left: 0.15em; font-weight: 400; white-space: normal !important; -} -.fieldDescription + .fieldDescription { - margin-top: 0.3em; + + .fieldDescription { + margin-top: 0.3em; + } } .content-primary, @@ -89,9 +79,14 @@ div[data-role=page] { padding-bottom: 5em !important; } -@media all and (min-width: 50em) { - .readOnlyContent, - form { +.readOnlyContent { + @media all and (min-width: 50em) { + max-width: 54em; + } +} + +form { + @media all and (min-width: 50em) { max-width: 54em; } } @@ -111,12 +106,12 @@ div[data-role=page] { .headroom { will-change: transform; transition: transform 200ms linear; -} -.headroom--pinned { - transform: translateY(0%); -} + &--pinned { + transform: translateY(0%); + } -.headroom--unpinned { - transform: translateY(-100%); + &--unpinned { + transform: translateY(-100%); + } } From 575dcd8334c9b635bb375eaec1b6d80b941ab380 Mon Sep 17 00:00:00 2001 From: Guilherme Danno Date: Sat, 16 May 2020 00:54:46 -0300 Subject: [PATCH 0058/2481] refactor: use scss features --- src/assets/css/fonts.scss | 23 ++++++++--------- src/assets/css/fonts.sized.scss | 44 ++++++++++++++++----------------- src/assets/css/livetv.scss | 4 +-- src/assets/css/site.scss | 11 +++++++++ 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/assets/css/fonts.scss b/src/assets/css/fonts.scss index cb0da0f80..abffd3a0d 100644 --- a/src/assets/css/fonts.scss +++ b/src/assets/css/fonts.scss @@ -1,5 +1,11 @@ -html { +@mixin font($weight: null, $size: null) { font-family: "Noto Sans", sans-serif; + font-weight: $weight; + font-size: $size; +} + +html { + @include font; font-size: 93%; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; @@ -7,25 +13,16 @@ html { text-rendering: optimizeLegibility; } -h1, -h2, -h3 { - font-family: "Noto Sans", sans-serif; -} - h1 { - font-weight: 400; - font-size: 1.8em; + @include font(400, 1.8em); } h2 { - font-weight: 400; - font-size: 1.5em; + @include font(400, 1.5em); } h3 { - font-weight: 400; - font-size: 1.17em; + @include font(400, 1.17em); } .layout-tv { diff --git a/src/assets/css/fonts.sized.scss b/src/assets/css/fonts.sized.scss index f60a94f23..1cec58a4a 100644 --- a/src/assets/css/fonts.sized.scss +++ b/src/assets/css/fonts.sized.scss @@ -1,31 +1,31 @@ -h1 { +@mixin header-font($size: null) { font-weight: 400; - font-size: 1.8em; + font-size: $size; } -.layout-desktop h1 { - font-size: 2em; -} - -h2 { - font-weight: 400; - font-size: 1.5em; -} - -h3 { - font-weight: 400; - font-size: 1.17em; -} - -@media all and (min-height: 720px) { - html { +html { + @media all and (min-height: 720px) { font-size: 20px; } -} -/* This is supposed to be 1080p, but had to reduce the min height to account for possible browser chrome */ -@media all and (min-height: 1000px) { - html { + /* This is supposed to be 1080p, but had to reduce the min height to account for possible browser chrome */ + @media all and (min-height: 1000px) { font-size: 27px; } } + +h1 { + @include header-font(1.8em); + + .layout-desktop & { + font-size: 2em; + } +} + +h2 { + @include header-font(1.8em); +} + +h3 { + @include header-font(1.17em); +} diff --git a/src/assets/css/livetv.scss b/src/assets/css/livetv.scss index 695adff8c..032bcddf4 100644 --- a/src/assets/css/livetv.scss +++ b/src/assets/css/livetv.scss @@ -2,8 +2,8 @@ padding-bottom: 15em; } -@media all and (min-width: 62.5em) { - #guideTab { +#guideTab { + @media all and (min-width: 62.5em) { padding-left: 0.5em; } } diff --git a/src/assets/css/site.scss b/src/assets/css/site.scss index db179afb2..d900a8bb4 100644 --- a/src/assets/css/site.scss +++ b/src/assets/css/site.scss @@ -16,6 +16,17 @@ body { -webkit-font-smoothing: antialiased; } +.clipForScreenReader { + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} + .material-icons { /* Fix font ligatures on older WebOS versions */ -webkit-font-feature-settings: "liga"; From cb6d8c7394e8ca4bf64b48a3ae179eb569a511e6 Mon Sep 17 00:00:00 2001 From: ferferga Date: Sat, 16 May 2020 15:12:44 +0200 Subject: [PATCH 0059/2481] Remove imageFetcher --- src/components/images/imageFetcher.js | 38 --------------------------- 1 file changed, 38 deletions(-) delete mode 100644 src/components/images/imageFetcher.js diff --git a/src/components/images/imageFetcher.js b/src/components/images/imageFetcher.js deleted file mode 100644 index c6882ddb7..000000000 --- a/src/components/images/imageFetcher.js +++ /dev/null @@ -1,38 +0,0 @@ -define(['dom'], function (dom) { - 'use strict'; - - function loadImage(elem, url) { - - if (!elem) { - return Promise.reject('elem cannot be null'); - } - - if (elem.tagName !== 'IMG') { - - elem.style.backgroundImage = "url('" + url + "')"; - return Promise.resolve(); - - //return loadImageIntoImg(document.createElement('img'), url).then(function () { - // elem.style.backgroundImage = "url('" + url + "')"; - // return Promise.resolve(); - //}); - - } - return loadImageIntoImg(elem, url); - } - - function loadImageIntoImg(elem, url) { - return new Promise(function (resolve, reject) { - - dom.addEventListener(elem, 'load', resolve, { - once: true - }); - elem.setAttribute('src', url); - }); - } - - return { - loadImage: loadImage - }; - -}); From 5dd6f108518622703422443b03ebc988a566bb00 Mon Sep 17 00:00:00 2001 From: ferferga Date: Sat, 16 May 2020 15:21:07 +0200 Subject: [PATCH 0060/2481] Revert 9a47428 changes --- src/addserver.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addserver.html b/src/addserver.html index 02850fffb..fc0145e25 100644 --- a/src/addserver.html +++ b/src/addserver.html @@ -3,7 +3,7 @@

    ${HeaderConnectToServer}

    - +
    ${LabelServerHostHelp}

    From 0f7a79caabe461da578f52fcb8aee0934ef6af6a Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 00:36:10 +0900 Subject: [PATCH 0061/2481] implement select menu for chromecast version --- src/components/chromecast/chromecastplayer.js | 8 +++++++- .../playbacksettings/playbacksettings.js | 4 ++++ .../playbacksettings.template.html | 17 ++++++++++++++++- src/scripts/settings/userSettings.js | 8 ++++++++ src/strings/en-us.json | 3 +++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/components/chromecast/chromecastplayer.js b/src/components/chromecast/chromecastplayer.js index 52fa4f6bc..5a9945539 100644 --- a/src/components/chromecast/chromecastplayer.js +++ b/src/components/chromecast/chromecastplayer.js @@ -54,7 +54,13 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' // production version registered with google // replace this value if you want to test changes on another instance - var applicationID = 'F007D354'; + var applicationStable = 'F007D354'; + var applicationNightly = '6F511C87'; + + var applicationID = applicationStable; + if (userSettings.chromecastVersion === 'nightly') { + applicationID = applicationNightly; + } var messageNamespace = 'urn:x-cast:com.connectsdk'; diff --git a/src/components/playbacksettings/playbacksettings.js b/src/components/playbacksettings/playbacksettings.js index 259386397..bfee8bcb3 100644 --- a/src/components/playbacksettings/playbacksettings.js +++ b/src/components/playbacksettings/playbacksettings.js @@ -204,6 +204,9 @@ define(['require', 'browser', 'appSettings', 'apphost', 'focusManager', 'quality fillChromecastQuality(context.querySelector('.selectChromecastVideoQuality')); + var selectChromecastVersion = context.querySelector('.selectChromecastVersion'); + selectChromecastVersion.value = userSettings.chromecastVersion(); + var selectSkipForwardLength = context.querySelector('.selectSkipForwardLength'); fillSkipLengths(selectSkipForwardLength); selectSkipForwardLength.value = userSettings.skipForwardLength(); @@ -234,6 +237,7 @@ define(['require', 'browser', 'appSettings', 'apphost', 'focusManager', 'quality userSettingsInstance.enableCinemaMode(context.querySelector('.chkEnableCinemaMode').checked); userSettingsInstance.enableNextVideoInfoOverlay(context.querySelector('.chkEnableNextVideoOverlay').checked); + userSettingsInstance.chromecastVersion(context.querySelector('.selectChromecastVersion').value); userSettingsInstance.skipForwardLength(context.querySelector('.selectSkipForwardLength').value); userSettingsInstance.skipBackLength(context.querySelector('.selectSkipBackLength').value); diff --git a/src/components/playbacksettings/playbacksettings.template.html b/src/components/playbacksettings/playbacksettings.template.html index 0c8b726a4..03af5d6fa 100644 --- a/src/components/playbacksettings/playbacksettings.template.html +++ b/src/components/playbacksettings/playbacksettings.template.html @@ -1,12 +1,13 @@ -

    ${HeaderAudioSettings}

    +
    +
    +
    + +
    +
    diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index 072e3f306..f92262152 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -153,6 +153,14 @@ import events from 'events'; return this.get('datetimelocale', false); } + export function chromecastVersion(val) { + if (val !== undefined) { + return this.set('chromecastVersion', val.toString()); + } + + return this.get('chromecastVersion') || 'stable'; + } + export function skipBackLength(val) { if (val !== undefined) { return this.set('skipBackLength', val.toString()); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 2014f2d61..1b291e1f9 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -748,6 +748,9 @@ "LabelMusicStreamingTranscodingBitrate": "Music transcoding bitrate:", "LabelMusicStreamingTranscodingBitrateHelp": "Specify a max bitrate when streaming music.", "LabelName": "Name:", + "LabelChromecastVersion": "Chromecast Version", + "LabelStable": "Stable", + "LabelNightly": "Nightly", "LabelNewName": "New name:", "LabelNewPassword": "New password:", "LabelNewPasswordConfirm": "New password confirm:", From 200d8dd4160be52025cb7f1d067370ed7520199e Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 00:52:16 +0900 Subject: [PATCH 0062/2481] fix casing for a few files --- package.json | 6 ++-- .../{sessionplayer.js => sessionPlayer.js} | 0 .../{autobackdrops.js => autoBackdrops.js} | 0 ...viceprofile.js => browserDeviceProfile.js} | 0 .../deleteHelper.js} | 0 .../fileDownloader.js} | 0 ...ardnavigation.js => keyboardNavigation.js} | 0 .../{librarybrowser.js => libraryBrowser.js} | 0 .../{librarymenu.js => libraryMenu.js} | 0 .../multiDownload.js} | 0 .../scrollHelper.js} | 0 .../serverNotifications.js | 0 src/{components => scripts}/shell.js | 0 src/scripts/site.js | 29 +++++++++---------- .../{themeloader.js => themeLoader.js} | 0 .../touchhelper.js => scripts/touchHelper.js} | 0 16 files changed, 17 insertions(+), 18 deletions(-) rename src/components/{sessionplayer.js => sessionPlayer.js} (100%) rename src/scripts/{autobackdrops.js => autoBackdrops.js} (100%) rename src/scripts/{browserdeviceprofile.js => browserDeviceProfile.js} (100%) rename src/{components/deletehelper.js => scripts/deleteHelper.js} (100%) rename src/{components/filedownloader.js => scripts/fileDownloader.js} (100%) rename src/scripts/{keyboardnavigation.js => keyboardNavigation.js} (100%) rename src/scripts/{librarybrowser.js => libraryBrowser.js} (100%) rename src/scripts/{librarymenu.js => libraryMenu.js} (100%) rename src/{components/multidownload.js => scripts/multiDownload.js} (100%) rename src/{components/scrollhelper.js => scripts/scrollHelper.js} (100%) rename src/{components => scripts}/serverNotifications.js (100%) rename src/{components => scripts}/shell.js (100%) rename src/scripts/{themeloader.js => themeLoader.js} (100%) rename src/{components/touchhelper.js => scripts/touchHelper.js} (100%) diff --git a/package.json b/package.json index 1e7ebfd0c..a65b93cad 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "test": [ "src/components/autoFocuser.js", "src/components/cardbuilder/cardBuilder.js", - "src/components/filedownloader.js", + "src/scripts/fileDownloader.js", "src/components/images/imageLoader.js", "src/components/lazyloader/lazyloader-intersectionobserver.js", "src/components/playback/mediasession.js", @@ -102,11 +102,11 @@ "src/scripts/filesystem.js", "src/scripts/imagehelper.js", "src/scripts/inputManager.js", - "src/components/deletehelper.js", + "src/scripts/deleteHelper.js", "src/components/actionsheet/actionsheet.js", "src/components/playmenu.js", "src/components/indicators/indicators.js", - "src/scripts/keyboardnavigation.js", + "src/scripts/keyboardNavigation.js", "src/scripts/settings/appSettings.js", "src/scripts/settings/userSettings.js", "src/scripts/settings/webSettings.js" diff --git a/src/components/sessionplayer.js b/src/components/sessionPlayer.js similarity index 100% rename from src/components/sessionplayer.js rename to src/components/sessionPlayer.js diff --git a/src/scripts/autobackdrops.js b/src/scripts/autoBackdrops.js similarity index 100% rename from src/scripts/autobackdrops.js rename to src/scripts/autoBackdrops.js diff --git a/src/scripts/browserdeviceprofile.js b/src/scripts/browserDeviceProfile.js similarity index 100% rename from src/scripts/browserdeviceprofile.js rename to src/scripts/browserDeviceProfile.js diff --git a/src/components/deletehelper.js b/src/scripts/deleteHelper.js similarity index 100% rename from src/components/deletehelper.js rename to src/scripts/deleteHelper.js diff --git a/src/components/filedownloader.js b/src/scripts/fileDownloader.js similarity index 100% rename from src/components/filedownloader.js rename to src/scripts/fileDownloader.js diff --git a/src/scripts/keyboardnavigation.js b/src/scripts/keyboardNavigation.js similarity index 100% rename from src/scripts/keyboardnavigation.js rename to src/scripts/keyboardNavigation.js diff --git a/src/scripts/librarybrowser.js b/src/scripts/libraryBrowser.js similarity index 100% rename from src/scripts/librarybrowser.js rename to src/scripts/libraryBrowser.js diff --git a/src/scripts/librarymenu.js b/src/scripts/libraryMenu.js similarity index 100% rename from src/scripts/librarymenu.js rename to src/scripts/libraryMenu.js diff --git a/src/components/multidownload.js b/src/scripts/multiDownload.js similarity index 100% rename from src/components/multidownload.js rename to src/scripts/multiDownload.js diff --git a/src/components/scrollhelper.js b/src/scripts/scrollHelper.js similarity index 100% rename from src/components/scrollhelper.js rename to src/scripts/scrollHelper.js diff --git a/src/components/serverNotifications.js b/src/scripts/serverNotifications.js similarity index 100% rename from src/components/serverNotifications.js rename to src/scripts/serverNotifications.js diff --git a/src/components/shell.js b/src/scripts/shell.js similarity index 100% rename from src/components/shell.js rename to src/scripts/shell.js diff --git a/src/scripts/site.js b/src/scripts/site.js index c00169d22..4fd9e91f8 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -376,7 +376,7 @@ var AppInfo = {}; define('filesystem', [scriptsPath + '/filesystem'], returnFirstDependency); define('lazyLoader', [componentsPath + '/lazyloader/lazyloader-intersectionobserver'], returnFirstDependency); - define('shell', [componentsPath + '/shell'], returnFirstDependency); + define('shell', [scriptsPath + '/shell'], returnFirstDependency); if ('registerElement' in document) { define('registerElement', []); @@ -397,8 +397,8 @@ var AppInfo = {}; define('prompt', [componentsPath + '/prompt/prompt'], returnFirstDependency); define('loading', [componentsPath + '/loading/loading'], returnFirstDependency); - define('multi-download', [componentsPath + '/multidownload'], returnFirstDependency); - define('fileDownloader', [componentsPath + '/filedownloader'], returnFirstDependency); + define('multi-download', [scriptsPath + '/multiDownload'], returnFirstDependency); + define('fileDownloader', [scriptsPath + '/fileDownloader'], returnFirstDependency); define('castSenderApiLoader', [componentsPath + '/castSenderApi'], returnFirstDependency); } @@ -490,7 +490,7 @@ var AppInfo = {}; ]; if (appHost.supports('remotecontrol')) { - list.push('components/sessionplayer'); + list.push('components/sessionPlayer'); if (browser.chrome || browser.opera) { list.push('components/chromecast/chromecastplayer'); @@ -532,13 +532,13 @@ var AppInfo = {}; window.Emby.Page = appRouter; - require(['emby-button', 'scripts/themeloader', 'libraryMenu', 'scripts/routes'], function () { + require(['emby-button', 'scripts/themeLoader', 'libraryMenu', 'scripts/routes'], function () { Emby.Page.start({ click: false, hashbang: true }); - require(['components/thememediaplayer', 'scripts/autobackdrops']); + require(['components/thememediaplayer', 'scripts/autoBackdrops']); if (!browser.tv && !browser.xboxOne && !browser.ps4) { require(['components/nowplayingbar/nowplayingbar']); @@ -630,14 +630,14 @@ var AppInfo = {}; var scriptsPath = getScriptsPath(); var paths = { - browserdeviceprofile: 'scripts/browserdeviceprofile', + browserdeviceprofile: 'scripts/browserDeviceProfile', browser: 'scripts/browser', - libraryBrowser: 'scripts/librarybrowser', + libraryBrowser: 'scripts/libraryBrowser', inputManager: 'scripts/inputManager', datetime: 'scripts/datetime', globalize: 'scripts/globalize', dfnshelper: 'scripts/dfnshelper', - libraryMenu: 'scripts/librarymenu', + libraryMenu: 'scripts/libraryMenu', playlisteditor: componentsPath + '/playlisteditor/playlisteditor', medialibrarycreator: componentsPath + '/medialibrarycreator/medialibrarycreator', medialibraryeditor: componentsPath + '/medialibraryeditor/medialibraryeditor', @@ -826,7 +826,7 @@ var AppInfo = {}; define('cardBuilder', [componentsPath + '/cardbuilder/cardBuilder'], returnFirstDependency); define('peoplecardbuilder', [componentsPath + '/cardbuilder/peoplecardbuilder'], returnFirstDependency); define('chaptercardbuilder', [componentsPath + '/cardbuilder/chaptercardbuilder'], returnFirstDependency); - define('deleteHelper', [componentsPath + '/deletehelper'], returnFirstDependency); + define('deleteHelper', [componentsPath + '/deleteHelper'], returnFirstDependency); define('tvguide', [componentsPath + '/guide/guide'], returnFirstDependency); define('guide-settings-dialog', [componentsPath + '/guide/guide-settings'], returnFirstDependency); define('loadingDialog', [componentsPath + '/loadingdialog/loadingdialog'], returnFirstDependency); @@ -843,18 +843,17 @@ var AppInfo = {}; define('viewSettings', [componentsPath + '/viewsettings/viewsettings'], returnFirstDependency); define('filterMenu', [componentsPath + '/filtermenu/filtermenu'], returnFirstDependency); define('sortMenu', [componentsPath + '/sortmenu/sortmenu'], returnFirstDependency); - define('idb', [componentsPath + '/idb'], returnFirstDependency); define('sanitizefilename', [componentsPath + '/sanitizefilename'], returnFirstDependency); define('toast', [componentsPath + '/toast/toast'], returnFirstDependency); - define('scrollHelper', [componentsPath + '/scrollhelper'], returnFirstDependency); - define('touchHelper', [componentsPath + '/touchhelper'], returnFirstDependency); + define('scrollHelper', [componentsPath + '/scrollHelper'], returnFirstDependency); + define('touchHelper', [scriptsPath + '/touchHelper'], returnFirstDependency); define('imageUploader', [componentsPath + '/imageuploader/imageuploader'], returnFirstDependency); define('htmlMediaHelper', [componentsPath + '/htmlMediaHelper'], returnFirstDependency); define('viewContainer', [componentsPath + '/viewContainer'], returnFirstDependency); define('dialogHelper', [componentsPath + '/dialogHelper/dialogHelper'], returnFirstDependency); - define('serverNotifications', [componentsPath + '/serverNotifications'], returnFirstDependency); + define('serverNotifications', [scriptsPath + '/serverNotifications'], returnFirstDependency); define('skinManager', [componentsPath + '/skinManager'], returnFirstDependency); - define('keyboardnavigation', [scriptsPath + '/keyboardnavigation'], returnFirstDependency); + define('keyboardnavigation', [scriptsPath + '/keyboardNavigation'], returnFirstDependency); define('mouseManager', [scriptsPath + '/mouseManager'], returnFirstDependency); define('scrollManager', [componentsPath + '/scrollManager'], returnFirstDependency); define('autoFocuser', [componentsPath + '/autoFocuser'], returnFirstDependency); diff --git a/src/scripts/themeloader.js b/src/scripts/themeLoader.js similarity index 100% rename from src/scripts/themeloader.js rename to src/scripts/themeLoader.js diff --git a/src/components/touchhelper.js b/src/scripts/touchHelper.js similarity index 100% rename from src/components/touchhelper.js rename to src/scripts/touchHelper.js From 410a58ddae1e6a49ff241814b0012f2edeb4b193 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 01:00:25 +0900 Subject: [PATCH 0063/2481] organize more routes by category --- src/scripts/routes.js | 82 +++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/scripts/routes.js b/src/scripts/routes.js index a5ed0a00a..adb859d5c 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -174,6 +174,47 @@ define([ roles: 'admin', controller: 'dashboard/encodingsettings' }); + defineRoute({ + path: '/log.html', + roles: 'admin', + controller: 'dashboard/logs' + }); + defineRoute({ + path: '/metadataimages.html', + autoFocus: false, + roles: 'admin', + controller: 'dashboard/metadataimagespage' + }); + defineRoute({ + path: '/metadatanfo.html', + autoFocus: false, + roles: 'admin', + controller: 'dashboard/metadatanfo' + }); + defineRoute({ + path: '/notificationsetting.html', + autoFocus: false, + roles: 'admin', + controller: 'dashboard/notifications/notification' + }); + defineRoute({ + path: '/notificationsettings.html', + controller: 'dashboard/notifications/notifications', + autoFocus: false, + roles: 'admin' + }); + defineRoute({ + path: '/playbackconfiguration.html', + autoFocus: false, + roles: 'admin', + controller: 'dashboard/playbackconfiguration' + }); + defineRoute({ + path: '/availableplugins.html', + autoFocus: false, + roles: 'admin', + controller: 'dashboard/plugins/available' + }); defineRoute({ path: '/home.html', @@ -227,23 +268,6 @@ define([ roles: 'admin', controller: 'livetvtuner' }); - defineRoute({ - path: '/log.html', - roles: 'admin', - controller: 'dashboard/logs' - }); - defineRoute({ - path: '/metadataimages.html', - autoFocus: false, - roles: 'admin', - controller: 'dashboard/metadataimagespage' - }); - defineRoute({ - path: '/metadatanfo.html', - autoFocus: false, - roles: 'admin', - controller: 'dashboard/metadatanfo' - }); defineRoute({ path: '/movies.html', autoFocus: false, @@ -256,30 +280,6 @@ define([ autoFocus: false, transition: 'fade' }); - defineRoute({ - path: '/notificationsetting.html', - autoFocus: false, - roles: 'admin', - controller: 'dashboard/notifications/notification' - }); - defineRoute({ - path: '/notificationsettings.html', - controller: 'dashboard/notifications/notifications', - autoFocus: false, - roles: 'admin' - }); - defineRoute({ - path: '/playbackconfiguration.html', - autoFocus: false, - roles: 'admin', - controller: 'dashboard/playbackconfiguration' - }); - defineRoute({ - path: '/availableplugins.html', - autoFocus: false, - roles: 'admin', - controller: 'dashboard/plugins/available' - }); defineRoute({ path: '/installedplugins.html', autoFocus: false, From f06291e37b2441408ba0e5fca5c12fba1376733b Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 01:06:28 +0900 Subject: [PATCH 0064/2481] fix casing on a few more files and folders --- .../accessSchedule.js} | 2 +- .../accessSchedule.template.html} | 0 .../plugin.js | 0 .../{itemcontextmenu.js => itemContextMenu.js} | 0 src/components/{itemhelper.js => itemHelper.js} | 0 .../plugin.js | 0 .../style.css | 0 .../{qualityoptions.js => qualityOptions.js} | 0 .../{sanitizefilename.js => sanitizeFilename.js} | 0 .../{thememediaplayer.js => themeMediaPlayer.js} | 0 .../{tunerpicker.js => tunerPicker.js} | 0 src/controllers/userparentalcontrol.js | 2 +- src/scripts/site.js | 16 ++++++++-------- 13 files changed, 10 insertions(+), 10 deletions(-) rename src/components/{accessschedule/accessschedule.js => accessSchedule/accessSchedule.js} (97%) rename src/components/{accessschedule/accessschedule.template.html => accessSchedule/accessSchedule.template.html} (100%) rename src/components/{backdropscreensaver => backdropScreensaver}/plugin.js (100%) rename src/components/{itemcontextmenu.js => itemContextMenu.js} (100%) rename src/components/{itemhelper.js => itemHelper.js} (100%) rename src/components/{logoscreensaver => logoScreensaver}/plugin.js (100%) rename src/components/{logoscreensaver => logoScreensaver}/style.css (100%) rename src/components/{qualityoptions.js => qualityOptions.js} (100%) rename src/components/{sanitizefilename.js => sanitizeFilename.js} (100%) rename src/components/{thememediaplayer.js => themeMediaPlayer.js} (100%) rename src/components/{tunerpicker.js => tunerPicker.js} (100%) diff --git a/src/components/accessschedule/accessschedule.js b/src/components/accessSchedule/accessSchedule.js similarity index 97% rename from src/components/accessschedule/accessschedule.js rename to src/components/accessSchedule/accessSchedule.js index 870231cf0..2209f5761 100644 --- a/src/components/accessschedule/accessschedule.js +++ b/src/components/accessSchedule/accessSchedule.js @@ -50,7 +50,7 @@ define(['dialogHelper', 'datetime', 'globalize', 'emby-select', 'paper-icon-butt show: function (options) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); - xhr.open('GET', 'components/accessschedule/accessschedule.template.html', true); + xhr.open('GET', 'components/accessSchedule/accessSchedule.template.html', true); xhr.onload = function (e) { var template = this.response; diff --git a/src/components/accessschedule/accessschedule.template.html b/src/components/accessSchedule/accessSchedule.template.html similarity index 100% rename from src/components/accessschedule/accessschedule.template.html rename to src/components/accessSchedule/accessSchedule.template.html diff --git a/src/components/backdropscreensaver/plugin.js b/src/components/backdropScreensaver/plugin.js similarity index 100% rename from src/components/backdropscreensaver/plugin.js rename to src/components/backdropScreensaver/plugin.js diff --git a/src/components/itemcontextmenu.js b/src/components/itemContextMenu.js similarity index 100% rename from src/components/itemcontextmenu.js rename to src/components/itemContextMenu.js diff --git a/src/components/itemhelper.js b/src/components/itemHelper.js similarity index 100% rename from src/components/itemhelper.js rename to src/components/itemHelper.js diff --git a/src/components/logoscreensaver/plugin.js b/src/components/logoScreensaver/plugin.js similarity index 100% rename from src/components/logoscreensaver/plugin.js rename to src/components/logoScreensaver/plugin.js diff --git a/src/components/logoscreensaver/style.css b/src/components/logoScreensaver/style.css similarity index 100% rename from src/components/logoscreensaver/style.css rename to src/components/logoScreensaver/style.css diff --git a/src/components/qualityoptions.js b/src/components/qualityOptions.js similarity index 100% rename from src/components/qualityoptions.js rename to src/components/qualityOptions.js diff --git a/src/components/sanitizefilename.js b/src/components/sanitizeFilename.js similarity index 100% rename from src/components/sanitizefilename.js rename to src/components/sanitizeFilename.js diff --git a/src/components/thememediaplayer.js b/src/components/themeMediaPlayer.js similarity index 100% rename from src/components/thememediaplayer.js rename to src/components/themeMediaPlayer.js diff --git a/src/components/tunerpicker.js b/src/components/tunerPicker.js similarity index 100% rename from src/components/tunerpicker.js rename to src/components/tunerPicker.js diff --git a/src/controllers/userparentalcontrol.js b/src/controllers/userparentalcontrol.js index e14427123..e8255512d 100644 --- a/src/controllers/userparentalcontrol.js +++ b/src/controllers/userparentalcontrol.js @@ -191,7 +191,7 @@ define(['jQuery', 'datetime', 'loading', 'libraryMenu', 'globalize', 'listViewSt function showSchedulePopup(page, schedule, index) { schedule = schedule || {}; - require(['components/accessschedule/accessschedule'], function (accessschedule) { + require(['components/accessSchedule/accessSchedule'], function (accessschedule) { accessschedule.show({ schedule: schedule }).then(function (updatedSchedule) { diff --git a/src/scripts/site.js b/src/scripts/site.js index 4fd9e91f8..66ee590e9 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -485,8 +485,8 @@ var AppInfo = {}; 'components/htmlvideoplayer/plugin', 'components/photoplayer/plugin', 'components/youtubeplayer/plugin', - 'components/backdropscreensaver/plugin', - 'components/logoscreensaver/plugin' + 'components/backdropScreensaver/plugin', + 'components/logoScreensaver/plugin' ]; if (appHost.supports('remotecontrol')) { @@ -538,7 +538,7 @@ var AppInfo = {}; hashbang: true }); - require(['components/thememediaplayer', 'scripts/autoBackdrops']); + require(['components/themeMediaPlayer', 'scripts/autoBackdrops']); if (!browser.tv && !browser.xboxOne && !browser.ps4) { require(['components/nowplayingbar/nowplayingbar']); @@ -644,9 +644,9 @@ var AppInfo = {}; imageoptionseditor: componentsPath + '/imageoptionseditor/imageoptionseditor', apphost: componentsPath + '/apphost', visibleinviewport: bowerPath + '/visibleinviewport', - qualityoptions: componentsPath + '/qualityoptions', + qualityoptions: componentsPath + '/qualityOptions', focusManager: componentsPath + '/focusManager', - itemHelper: componentsPath + '/itemhelper', + itemHelper: componentsPath + '/itemHelper', itemShortcuts: componentsPath + '/shortcuts', playQueueManager: componentsPath + '/playback/playqueuemanager', nowPlayingHelper: componentsPath + '/playback/nowplayinghelper', @@ -774,7 +774,7 @@ var AppInfo = {}; define('chromecastHelper', [componentsPath + '/chromecast/chromecasthelpers'], returnFirstDependency); define('mediaSession', [componentsPath + '/playback/mediasession'], returnFirstDependency); define('actionsheet', [componentsPath + '/actionsheet/actionsheet'], returnFirstDependency); - define('tunerPicker', [componentsPath + '/tunerpicker'], returnFirstDependency); + define('tunerPicker', [componentsPath + '/tunerPicker'], returnFirstDependency); define('mainTabsManager', [componentsPath + '/maintabsmanager'], returnFirstDependency); define('imageLoader', [componentsPath + '/images/imageLoader'], returnFirstDependency); define('directorybrowser', [componentsPath + '/directorybrowser/directorybrowser'], returnFirstDependency); @@ -803,7 +803,7 @@ var AppInfo = {}; define('itemIdentifier', [componentsPath + '/itemidentifier/itemidentifier'], returnFirstDependency); define('itemMediaInfo', [componentsPath + '/itemMediaInfo/itemMediaInfo'], returnFirstDependency); define('mediaInfo', [componentsPath + '/mediainfo/mediainfo'], returnFirstDependency); - define('itemContextMenu', [componentsPath + '/itemcontextmenu'], returnFirstDependency); + define('itemContextMenu', [componentsPath + '/itemContextMenu'], returnFirstDependency); define('imageEditor', [componentsPath + '/imageeditor/imageeditor'], returnFirstDependency); define('imageDownloader', [componentsPath + '/imagedownloader/imagedownloader'], returnFirstDependency); define('dom', [scriptsPath + '/dom'], returnFirstDependency); @@ -843,7 +843,7 @@ var AppInfo = {}; define('viewSettings', [componentsPath + '/viewsettings/viewsettings'], returnFirstDependency); define('filterMenu', [componentsPath + '/filtermenu/filtermenu'], returnFirstDependency); define('sortMenu', [componentsPath + '/sortmenu/sortmenu'], returnFirstDependency); - define('sanitizefilename', [componentsPath + '/sanitizefilename'], returnFirstDependency); + define('sanitizefilename', [componentsPath + '/sanitizeFilename'], returnFirstDependency); define('toast', [componentsPath + '/toast/toast'], returnFirstDependency); define('scrollHelper', [componentsPath + '/scrollHelper'], returnFirstDependency); define('touchHelper', [scriptsPath + '/touchHelper'], returnFirstDependency); From 6a596bd4973f7d0ebe8a1c2ae8d1081dca0776a6 Mon Sep 17 00:00:00 2001 From: ferferga Date: Sat, 16 May 2020 18:07:32 +0200 Subject: [PATCH 0065/2481] Fix merge conflicts --- src/components/apphost.js | 1 - src/components/cardbuilder/cardBuilder.js | 6 +++--- src/components/guide/tvguide.template.html | 2 +- src/components/listview/listview.js | 2 +- src/components/metadataeditor/metadataeditor.template.html | 2 +- src/components/multiselect/multiselect.js | 2 +- src/controllers/livetvstatus.js | 4 ++-- src/controllers/userprofilespage.js | 2 +- src/itemdetails.html | 2 +- src/list.html | 4 ++-- 10 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/components/apphost.js b/src/components/apphost.js index 75e8ba17f..7b044d5d3 100644 --- a/src/components/apphost.js +++ b/src/components/apphost.js @@ -359,7 +359,6 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g return -1 !== supportedFeatures.indexOf(command.toLowerCase()); }, preferVisualCards: browser.android || browser.chrome, - moreIcon: browser.android ? 'more_vert' : 'more_horiz', getSyncProfile: getSyncProfile, getDefaultLayout: function () { if (window.NativeShell) { diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 43ca28f01..d4d4d7f73 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -869,7 +869,7 @@ import 'programStyles'; if (isOuterFooter && options.cardLayout && layoutManager.mobile) { if (options.cardFooterAside !== 'none') { - html += ''; + html += ''; } } @@ -1426,7 +1426,7 @@ import 'programStyles'; } if (options.overlayMoreButton) { - overlayButtons += ''; + overlayButtons += ''; } } @@ -1580,7 +1580,7 @@ import 'programStyles'; html += ''; } - html += ''; + html += ''; html += '
    '; html += '
    '; diff --git a/src/components/guide/tvguide.template.html b/src/components/guide/tvguide.template.html index 367aac6c1..730de5590 100644 --- a/src/components/guide/tvguide.template.html +++ b/src/components/guide/tvguide.template.html @@ -10,7 +10,7 @@
    diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js index 0743b3528..587355b35 100644 --- a/src/components/listview/listview.js +++ b/src/components/listview/listview.js @@ -426,7 +426,7 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan html += '
    '; - const moreIcon = 'more_horiz'; + const moreIcon = 'more_vert'; html += getTextLinesHtml(textlines, isLargeStyle); diff --git a/src/components/metadataeditor/metadataeditor.template.html b/src/components/metadataeditor/metadataeditor.template.html index 39894d4f6..784582428 100644 --- a/src/components/metadataeditor/metadataeditor.template.html +++ b/src/components/metadataeditor/metadataeditor.template.html @@ -9,7 +9,7 @@ ${Save}
    diff --git a/src/components/multiselect/multiselect.js b/src/components/multiselect/multiselect.js index 70a0924bc..6a22e5509 100644 --- a/src/components/multiselect/multiselect.js +++ b/src/components/multiselect/multiselect.js @@ -129,7 +129,7 @@ define(['browser', 'appStorage', 'apphost', 'loading', 'connectionManager', 'glo html += ''; html += '

    '; - const moreIcon = 'more_horiz'; + const moreIcon = 'more_vert'; html += ''; selectionCommandsPanel.innerHTML = html; diff --git a/src/controllers/livetvstatus.js b/src/controllers/livetvstatus.js index 053e5d3d4..737d49496 100644 --- a/src/controllers/livetvstatus.js +++ b/src/controllers/livetvstatus.js @@ -30,7 +30,7 @@ define(['jQuery', 'globalize', 'scripts/taskbutton', 'dom', 'libraryMenu', 'layo html += '
    '; html += '
    '; html += '
    '; - html += ''; + html += ''; html += '
    ' + (device.FriendlyName || getTunerName(device.Type)) + '
    '; html += '
    '; html += device.Url || ' '; @@ -112,7 +112,7 @@ define(['jQuery', 'globalize', 'scripts/taskbutton', 'dom', 'libraryMenu', 'layo html += '
    '; html += ''; html += '
    '; - html += ''; + html += ''; html += '
    '; } diff --git a/src/controllers/userprofilespage.js b/src/controllers/userprofilespage.js index 6afda79c7..c691c665f 100644 --- a/src/controllers/userprofilespage.js +++ b/src/controllers/userprofilespage.js @@ -189,7 +189,7 @@ define(['loading', 'dom', 'globalize', 'date-fns', 'dfnshelper', 'paper-icon-but html += '
    '; html += '
    '; html += '
    '; - html += ''; + html += ''; html += '
    '; html += '
    '; html += user.UserName; diff --git a/src/itemdetails.html b/src/itemdetails.html index 37f1432e2..4d5cab026 100644 --- a/src/itemdetails.html +++ b/src/itemdetails.html @@ -101,7 +101,7 @@ diff --git a/src/list.html b/src/list.html index efac2d891..464cd1553 100644 --- a/src/list.html +++ b/src/list.html @@ -42,10 +42,10 @@
    From 3577bcc52b190c6d6f183ad4ad13a456854f9c12 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 16 May 2020 18:58:27 +0200 Subject: [PATCH 0066/2481] Fix alignment of dialog button on Chrome --- src/components/formdialog.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/formdialog.css b/src/components/formdialog.css index 2b2df601c..94695f486 100644 --- a/src/components/formdialog.css +++ b/src/components/formdialog.css @@ -55,7 +55,7 @@ /* Without this emby-checkbox is able to appear on top */ z-index: 1; align-items: flex-end; - justify-content: end; + justify-content: flex-end; flex-wrap: wrap; } From d2f11981c62a1cff8480fc8405899e45a5875ee5 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 02:48:21 +0900 Subject: [PATCH 0067/2481] update casing for some components --- .../plugin.js | 0 .../plugin.js | 0 .../style.css | 0 .../imageUploader.js} | 2 +- .../imageUploader.template.html} | 0 .../style.css | 0 .../loadingDialog.js} | 0 .../mediaLibraryCreator.js} | 2 +- .../mediaLibraryCreator.template.html} | 0 .../mediaLibraryEditor.js} | 2 +- .../mediaLibraryEditor.template.html} | 0 .../multiSelect.css} | 0 .../multiSelect.js} | 2 +- .../nowPlayingBar.css} | 0 .../nowPlayingBar.js} | 2 +- .../{photoplayer => photoPlayer}/plugin.js | 0 .../playbackSettings.js} | 2 +- .../playbackSettings.template.html} | 0 src/scripts/site.js | 20 +++++++++---------- 19 files changed, 16 insertions(+), 16 deletions(-) rename src/components/{htmlaudioplayer => htmlAudioPlayer}/plugin.js (100%) rename src/components/{htmlvideoplayer => htmlVideoPlayer}/plugin.js (100%) rename src/components/{htmlvideoplayer => htmlVideoPlayer}/style.css (100%) rename src/components/{imageuploader/imageuploader.js => imageUploader/imageUploader.js} (98%) rename src/components/{imageuploader/imageuploader.template.html => imageUploader/imageUploader.template.html} (100%) rename src/components/{imageuploader => imageUploader}/style.css (100%) rename src/components/{loadingdialog/loadingdialog.js => loadingDialog/loadingDialog.js} (100%) rename src/components/{medialibrarycreator/medialibrarycreator.js => mediaLibraryCreator/mediaLibraryCreator.js} (98%) rename src/components/{medialibrarycreator/medialibrarycreator.template.html => mediaLibraryCreator/mediaLibraryCreator.template.html} (100%) rename src/components/{medialibraryeditor/medialibraryeditor.js => mediaLibraryEditor/mediaLibraryEditor.js} (98%) rename src/components/{medialibraryeditor/medialibraryeditor.template.html => mediaLibraryEditor/mediaLibraryEditor.template.html} (100%) rename src/components/{multiselect/multiselect.css => multiSelect/multiSelect.css} (100%) rename src/components/{multiselect/multiselect.js => multiSelect/multiSelect.js} (99%) rename src/components/{nowplayingbar/nowplayingbar.css => nowPlayingBar/nowPlayingBar.css} (100%) rename src/components/{nowplayingbar/nowplayingbar.js => nowPlayingBar/nowPlayingBar.js} (99%) rename src/components/{photoplayer => photoPlayer}/plugin.js (100%) rename src/components/{playbacksettings/playbacksettings.js => playbackSettings/playbackSettings.js} (99%) rename src/components/{playbacksettings/playbacksettings.template.html => playbackSettings/playbackSettings.template.html} (100%) diff --git a/src/components/htmlaudioplayer/plugin.js b/src/components/htmlAudioPlayer/plugin.js similarity index 100% rename from src/components/htmlaudioplayer/plugin.js rename to src/components/htmlAudioPlayer/plugin.js diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlVideoPlayer/plugin.js similarity index 100% rename from src/components/htmlvideoplayer/plugin.js rename to src/components/htmlVideoPlayer/plugin.js diff --git a/src/components/htmlvideoplayer/style.css b/src/components/htmlVideoPlayer/style.css similarity index 100% rename from src/components/htmlvideoplayer/style.css rename to src/components/htmlVideoPlayer/style.css diff --git a/src/components/imageuploader/imageuploader.js b/src/components/imageUploader/imageUploader.js similarity index 98% rename from src/components/imageuploader/imageuploader.js rename to src/components/imageUploader/imageUploader.js index 87ecc1bc3..5d1c0275c 100644 --- a/src/components/imageuploader/imageuploader.js +++ b/src/components/imageUploader/imageUploader.js @@ -125,7 +125,7 @@ define(['dialogHelper', 'connectionManager', 'dom', 'loading', 'scrollHelper', ' options = options || {}; - require(['text!./imageuploader.template.html'], function (template) { + require(['text!./imageUploader.template.html'], function (template) { currentItemId = options.itemId; currentServerId = options.serverId; diff --git a/src/components/imageuploader/imageuploader.template.html b/src/components/imageUploader/imageUploader.template.html similarity index 100% rename from src/components/imageuploader/imageuploader.template.html rename to src/components/imageUploader/imageUploader.template.html diff --git a/src/components/imageuploader/style.css b/src/components/imageUploader/style.css similarity index 100% rename from src/components/imageuploader/style.css rename to src/components/imageUploader/style.css diff --git a/src/components/loadingdialog/loadingdialog.js b/src/components/loadingDialog/loadingDialog.js similarity index 100% rename from src/components/loadingdialog/loadingdialog.js rename to src/components/loadingDialog/loadingDialog.js diff --git a/src/components/medialibrarycreator/medialibrarycreator.js b/src/components/mediaLibraryCreator/mediaLibraryCreator.js similarity index 98% rename from src/components/medialibrarycreator/medialibrarycreator.js rename to src/components/mediaLibraryCreator/mediaLibraryCreator.js index 450f5a64e..b941f2fcb 100644 --- a/src/components/medialibrarycreator/medialibrarycreator.js +++ b/src/components/mediaLibraryCreator/mediaLibraryCreator.js @@ -182,7 +182,7 @@ define(['loading', 'dialogHelper', 'dom', 'jQuery', 'components/libraryoptionsed currentResolve = resolve; hasChanges = false; var xhr = new XMLHttpRequest(); - xhr.open('GET', 'components/medialibrarycreator/medialibrarycreator.template.html', true); + xhr.open('GET', 'components/mediaLibraryCreator/mediaLibraryCreator.template.html', true); xhr.onload = function (e) { var template = this.response; diff --git a/src/components/medialibrarycreator/medialibrarycreator.template.html b/src/components/mediaLibraryCreator/mediaLibraryCreator.template.html similarity index 100% rename from src/components/medialibrarycreator/medialibrarycreator.template.html rename to src/components/mediaLibraryCreator/mediaLibraryCreator.template.html diff --git a/src/components/medialibraryeditor/medialibraryeditor.js b/src/components/mediaLibraryEditor/mediaLibraryEditor.js similarity index 98% rename from src/components/medialibraryeditor/medialibraryeditor.js rename to src/components/mediaLibraryEditor/mediaLibraryEditor.js index 314150962..c4da19ccd 100644 --- a/src/components/medialibraryeditor/medialibraryeditor.js +++ b/src/components/mediaLibraryEditor/mediaLibraryEditor.js @@ -199,7 +199,7 @@ define(['jQuery', 'loading', 'dialogHelper', 'dom', 'components/libraryoptionsed currentDeferred = deferred; hasChanges = false; var xhr = new XMLHttpRequest(); - xhr.open('GET', 'components/medialibraryeditor/medialibraryeditor.template.html', true); + xhr.open('GET', 'components/mediaLibraryEditor/mediaLibraryEditor.template.html', true); xhr.onload = function (e) { var template = this.response; diff --git a/src/components/medialibraryeditor/medialibraryeditor.template.html b/src/components/mediaLibraryEditor/mediaLibraryEditor.template.html similarity index 100% rename from src/components/medialibraryeditor/medialibraryeditor.template.html rename to src/components/mediaLibraryEditor/mediaLibraryEditor.template.html diff --git a/src/components/multiselect/multiselect.css b/src/components/multiSelect/multiSelect.css similarity index 100% rename from src/components/multiselect/multiselect.css rename to src/components/multiSelect/multiSelect.css diff --git a/src/components/multiselect/multiselect.js b/src/components/multiSelect/multiSelect.js similarity index 99% rename from src/components/multiselect/multiselect.js rename to src/components/multiSelect/multiSelect.js index 70a0924bc..cc0fc9cee 100644 --- a/src/components/multiselect/multiselect.js +++ b/src/components/multiSelect/multiSelect.js @@ -1,4 +1,4 @@ -define(['browser', 'appStorage', 'apphost', 'loading', 'connectionManager', 'globalize', 'appRouter', 'dom', 'css!./multiselect'], function (browser, appStorage, appHost, loading, connectionManager, globalize, appRouter, dom) { +define(['browser', 'appStorage', 'apphost', 'loading', 'connectionManager', 'globalize', 'appRouter', 'dom', 'css!./multiSelect'], function (browser, appStorage, appHost, loading, connectionManager, globalize, appRouter, dom) { 'use strict'; var selectedItems = []; diff --git a/src/components/nowplayingbar/nowplayingbar.css b/src/components/nowPlayingBar/nowPlayingBar.css similarity index 100% rename from src/components/nowplayingbar/nowplayingbar.css rename to src/components/nowPlayingBar/nowPlayingBar.css diff --git a/src/components/nowplayingbar/nowplayingbar.js b/src/components/nowPlayingBar/nowPlayingBar.js similarity index 99% rename from src/components/nowplayingbar/nowplayingbar.js rename to src/components/nowPlayingBar/nowPlayingBar.js index 546029a98..bc9c3c1a8 100644 --- a/src/components/nowplayingbar/nowplayingbar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -244,7 +244,7 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', return new Promise(function (resolve, reject) { - require(['appFooter-shared', 'itemShortcuts', 'css!./nowplayingbar.css', 'emby-slider'], function (appfooter, itemShortcuts) { + require(['appFooter-shared', 'itemShortcuts', 'css!./nowPlayingBar.css', 'emby-slider'], function (appfooter, itemShortcuts) { var parentContainer = appfooter.element; nowPlayingBarElement = parentContainer.querySelector('.nowPlayingBar'); diff --git a/src/components/photoplayer/plugin.js b/src/components/photoPlayer/plugin.js similarity index 100% rename from src/components/photoplayer/plugin.js rename to src/components/photoPlayer/plugin.js diff --git a/src/components/playbacksettings/playbacksettings.js b/src/components/playbackSettings/playbackSettings.js similarity index 99% rename from src/components/playbacksettings/playbacksettings.js rename to src/components/playbackSettings/playbackSettings.js index 259386397..2eb14a566 100644 --- a/src/components/playbacksettings/playbacksettings.js +++ b/src/components/playbackSettings/playbackSettings.js @@ -285,7 +285,7 @@ define(['require', 'browser', 'appSettings', 'apphost', 'focusManager', 'quality function embed(options, self) { - require(['text!./playbacksettings.template.html'], function (template) { + require(['text!./playbackSettings.template.html'], function (template) { options.element.innerHTML = globalize.translateDocument(template, 'core'); diff --git a/src/components/playbacksettings/playbacksettings.template.html b/src/components/playbackSettings/playbackSettings.template.html similarity index 100% rename from src/components/playbacksettings/playbacksettings.template.html rename to src/components/playbackSettings/playbackSettings.template.html diff --git a/src/scripts/site.js b/src/scripts/site.js index 66ee590e9..19ae9bb3d 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -481,9 +481,9 @@ var AppInfo = {}; var list = [ 'components/playback/playaccessvalidation', 'components/playback/experimentalwarnings', - 'components/htmlaudioplayer/plugin', - 'components/htmlvideoplayer/plugin', - 'components/photoplayer/plugin', + 'components/htmlAudioPlayer/plugin', + 'components/htmlVideoPlayer/plugin', + 'components/photoPlayer/plugin', 'components/youtubeplayer/plugin', 'components/backdropScreensaver/plugin', 'components/logoScreensaver/plugin' @@ -541,7 +541,7 @@ var AppInfo = {}; require(['components/themeMediaPlayer', 'scripts/autoBackdrops']); if (!browser.tv && !browser.xboxOne && !browser.ps4) { - require(['components/nowplayingbar/nowplayingbar']); + require(['components/nowPlayingBar/nowPlayingBar']); } if (appHost.supports('remotecontrol')) { @@ -639,8 +639,8 @@ var AppInfo = {}; dfnshelper: 'scripts/dfnshelper', libraryMenu: 'scripts/libraryMenu', playlisteditor: componentsPath + '/playlisteditor/playlisteditor', - medialibrarycreator: componentsPath + '/medialibrarycreator/medialibrarycreator', - medialibraryeditor: componentsPath + '/medialibraryeditor/medialibraryeditor', + medialibrarycreator: componentsPath + '/mediaLibraryCreator/mediaLibraryCreator', + medialibraryeditor: componentsPath + '/mediaLibraryEditor/mediaLibraryEditor', imageoptionseditor: componentsPath + '/imageoptionseditor/imageoptionseditor', apphost: componentsPath + '/apphost', visibleinviewport: bowerPath + '/visibleinviewport', @@ -785,7 +785,7 @@ var AppInfo = {}; define('playMethodHelper', [componentsPath + '/playback/playmethodhelper'], returnFirstDependency); define('brightnessOsd', [componentsPath + '/playback/brightnessosd'], returnFirstDependency); define('alphaNumericShortcuts', [scriptsPath + '/alphanumericshortcuts'], returnFirstDependency); - define('multiSelect', [componentsPath + '/multiselect/multiselect'], returnFirstDependency); + define('multiSelect', [componentsPath + '/multiSelect/multiSelect'], returnFirstDependency); define('alphaPicker', [componentsPath + '/alphapicker/alphapicker'], returnFirstDependency); define('tabbedView', [componentsPath + '/tabbedview/tabbedview'], returnFirstDependency); define('itemsTab', [componentsPath + '/tabbedview/itemstab'], returnFirstDependency); @@ -814,7 +814,7 @@ var AppInfo = {}; define('subtitleAppearanceHelper', [componentsPath + '/subtitlesettings/subtitleappearancehelper'], returnFirstDependency); define('subtitleSettings', [componentsPath + '/subtitlesettings/subtitlesettings'], returnFirstDependency); define('displaySettings', [componentsPath + '/displaysettings/displaysettings'], returnFirstDependency); - define('playbackSettings', [componentsPath + '/playbacksettings/playbacksettings'], returnFirstDependency); + define('playbackSettings', [componentsPath + '/playbackSettings/playbackSettings'], returnFirstDependency); define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency); define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager); define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager); @@ -829,7 +829,7 @@ var AppInfo = {}; define('deleteHelper', [componentsPath + '/deleteHelper'], returnFirstDependency); define('tvguide', [componentsPath + '/guide/guide'], returnFirstDependency); define('guide-settings-dialog', [componentsPath + '/guide/guide-settings'], returnFirstDependency); - define('loadingDialog', [componentsPath + '/loadingdialog/loadingdialog'], returnFirstDependency); + define('loadingDialog', [componentsPath + '/loadingDialog/loadingDialog'], returnFirstDependency); define('viewManager', [componentsPath + '/viewManager/viewManager'], function (viewManager) { window.ViewManager = viewManager; viewManager.dispatchPageEvents(true); @@ -847,7 +847,7 @@ var AppInfo = {}; define('toast', [componentsPath + '/toast/toast'], returnFirstDependency); define('scrollHelper', [componentsPath + '/scrollHelper'], returnFirstDependency); define('touchHelper', [scriptsPath + '/touchHelper'], returnFirstDependency); - define('imageUploader', [componentsPath + '/imageuploader/imageuploader'], returnFirstDependency); + define('imageUploader', [componentsPath + '/imageUploader/imageUploader'], returnFirstDependency); define('htmlMediaHelper', [componentsPath + '/htmlMediaHelper'], returnFirstDependency); define('viewContainer', [componentsPath + '/viewContainer'], returnFirstDependency); define('dialogHelper', [componentsPath + '/dialogHelper/dialogHelper'], returnFirstDependency); From 8047543d1a13dfdb7a9e70e80a40bd87ad62f90d Mon Sep 17 00:00:00 2001 From: Samuel Gagnon Date: Sat, 16 May 2020 17:12:57 +0000 Subject: [PATCH 0068/2481] Translated using Weblate (French (Canada)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/ --- src/strings/fr-ca.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/strings/fr-ca.json b/src/strings/fr-ca.json index 1f74b7bd5..5880dbb77 100644 --- a/src/strings/fr-ca.json +++ b/src/strings/fr-ca.json @@ -193,5 +193,10 @@ "AllowFfmpegThrottling": "Restreindre la vitesse de transcodage", "AlbumArtist": "Artiste de l'Album", "Album": "Album", - "AuthProviderHelp": "Sélectionner un fournisseur d'authentification pour authentifier le mot de passe de cet utilisateur." + "AuthProviderHelp": "Sélectionner un fournisseur d'authentification pour authentifier le mot de passe de cet utilisateur.", + "HeaderFavoriteSongs": "Chansons favorites", + "HeaderFavoriteShows": "Séries favorites", + "HeaderFavoriteEpisodes": "Épisodes favoris", + "HeaderFavoriteArtists": "Artistes favoris", + "HeaderFavoriteAlbums": "Albums favoris" } From e6526de5dd6e4607c611e603e9f087eb3251f11d Mon Sep 17 00:00:00 2001 From: ferferga Date: Sat, 16 May 2020 23:49:12 +0200 Subject: [PATCH 0069/2481] Fix scrollbars on dialog windows --- src/components/channelmapper/channelmapper.js | 2 +- src/components/imagedownloader/imagedownloader.template.html | 2 +- src/components/imageeditor/imageeditor.template.html | 2 +- src/components/imageuploader/imageuploader.template.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/channelmapper/channelmapper.js b/src/components/channelmapper/channelmapper.js index 83ae4d09c..f2ad88e71 100644 --- a/src/components/channelmapper/channelmapper.js +++ b/src/components/channelmapper/channelmapper.js @@ -79,7 +79,7 @@ define(['dom', 'dialogHelper', 'loading', 'connectionManager', 'globalize', 'act function getEditorHtml() { var html = ''; - html += '
    '; + html += '
    '; html += '
    '; html += ''; html += '

    ' + globalize.translate('HeaderChannels') + '

    '; diff --git a/src/components/imagedownloader/imagedownloader.template.html b/src/components/imagedownloader/imagedownloader.template.html index 6ca2a2418..b88ea7392 100644 --- a/src/components/imagedownloader/imagedownloader.template.html +++ b/src/components/imagedownloader/imagedownloader.template.html @@ -5,7 +5,7 @@
    -
    +
    diff --git a/src/components/imageeditor/imageeditor.template.html b/src/components/imageeditor/imageeditor.template.html index 1bf34f1aa..ce24e724b 100644 --- a/src/components/imageeditor/imageeditor.template.html +++ b/src/components/imageeditor/imageeditor.template.html @@ -5,7 +5,7 @@
    -
    +
    diff --git a/src/components/imageuploader/imageuploader.template.html b/src/components/imageuploader/imageuploader.template.html index 75f572375..9c6a139c0 100644 --- a/src/components/imageuploader/imageuploader.template.html +++ b/src/components/imageuploader/imageuploader.template.html @@ -5,7 +5,7 @@
    -
    +
    From 029c7382dbfdb8af03a04dd95671b505b1f4de36 Mon Sep 17 00:00:00 2001 From: ferferga Date: Sun, 17 May 2020 00:11:37 +0200 Subject: [PATCH 0070/2481] Use classes instead --- src/assets/css/site.css | 4 ++++ src/components/htmlvideoplayer/plugin.js | 2 +- src/components/skinManager.js | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/assets/css/site.css b/src/assets/css/site.css index 627145abc..da58848cd 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.css @@ -120,3 +120,7 @@ div[data-role=page] { .headroom--unpinned { transform: translateY(-100%); } + +.force-scroll { + overflow-y: scroll; +} diff --git a/src/components/htmlvideoplayer/plugin.js b/src/components/htmlvideoplayer/plugin.js index 987117d17..e8521d035 100644 --- a/src/components/htmlvideoplayer/plugin.js +++ b/src/components/htmlvideoplayer/plugin.js @@ -110,7 +110,7 @@ define(['browser', 'require', 'events', 'apphost', 'loading', 'dom', 'playbackMa let animatedPage = document.querySelector('.page:not(.hide)'); animatedPage.classList.add('hide'); // At this point, we must hide the scrollbar placeholder, so it's not being displayed while the item is being loaded - document.getElementsByTagName('body')[0].style = null; + document.body.classList.remove('force-scroll'); } function zoomIn(elem) { diff --git a/src/components/skinManager.js b/src/components/skinManager.js index fe64df1f2..5e5bac573 100644 --- a/src/components/skinManager.js +++ b/src/components/skinManager.js @@ -138,7 +138,7 @@ define(['apphost', 'userSettings', 'browser', 'events', 'pluginManager', 'backdr function onViewBeforeShow(e) { if (e.detail && e.detail.type === 'video-osd') { // This removes the space that the scrollbar takes while playing a video - document.getElementsByTagName('body')[0].style = null; + document.body.classList.remove('force-scroll'); return; } @@ -159,7 +159,7 @@ define(['apphost', 'userSettings', 'browser', 'events', 'pluginManager', 'backdr } // This keeps the scrollbar always present in all pages, so we avoid clipping while switching between pages // that need the scrollbar and pages that don't. - document.getElementsByTagName('body')[0].style = 'overflow-y: scroll'; + document.body.classList.add('force-scroll'); } document.addEventListener('viewshow', onViewBeforeShow); From 7aa34f1be22bb87e5b8519cf6ea208087c5069d6 Mon Sep 17 00:00:00 2001 From: ferferga Date: Sun, 17 May 2020 00:33:02 +0200 Subject: [PATCH 0071/2481] Switch scrollbars on slideshow.js --- src/assets/css/site.css | 4 ++++ src/components/slideshow/slideshow.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/assets/css/site.css b/src/assets/css/site.css index da58848cd..d489f77f0 100644 --- a/src/assets/css/site.css +++ b/src/assets/css/site.css @@ -124,3 +124,7 @@ div[data-role=page] { .force-scroll { overflow-y: scroll; } + +.hide-scroll { + overflow-y: hidden; +} diff --git a/src/components/slideshow/slideshow.js b/src/components/slideshow/slideshow.js index 4716135ce..ca394ef2a 100644 --- a/src/components/slideshow/slideshow.js +++ b/src/components/slideshow/slideshow.js @@ -603,6 +603,9 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f */ self.show = function () { createElements(options); + // Hides page scrollbar + document.body.classList.remove('force-scroll'); + document.body.classList.add('hide-scroll'); }; /** @@ -611,6 +614,9 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'f self.hide = function () { if (dialog) { dialogHelper.close(dialog); + // Shows page scrollbar + document.body.classList.remove('hide-scroll'); + document.body.classList.add('force-scroll'); } }; }; From 1aaaffde4c78bb7678cdc2b70f99577f2bb04e3a Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 17:04:36 +0900 Subject: [PATCH 0072/2481] rename more components and update references --- package.json | 4 ++-- .../actionSheet.css} | 0 .../actionSheet.js} | 2 +- .../alphaPicker.js} | 0 .../{alphapicker => alphaPicker}/style.css | 0 .../appfooter.css => appFooter/appFooter.css} | 0 .../appfooter.js => appFooter/appFooter.js} | 2 +- .../channelMapper.js} | 0 .../collectionEditor.js} | 0 .../displaySettings.js} | 2 +- .../displaySettings.template.html} | 0 .../homeScreenSettings.js} | 2 +- .../homeScreenSettings.template.html} | 0 .../imageDownloader.js} | 2 +- .../imageDownloader.template.html} | 0 .../imageOptionsEditor.js} | 2 +- .../imageOptionsEditor.template.html} | 0 .../lazyLoaderIntersectionObserver.js} | 0 .../metadataEditor.js} | 4 ++-- .../metadataEditor.template.html} | 0 .../personEditor.js} | 2 +- .../personEditor.template.html} | 0 src/controllers/livetvstatus.js | 2 +- src/scripts/site.js | 22 +++++++++---------- 24 files changed, 23 insertions(+), 23 deletions(-) rename src/components/{actionsheet/actionsheet.css => actionSheet/actionSheet.css} (100%) rename src/components/{actionsheet/actionsheet.js => actionSheet/actionSheet.js} (99%) rename src/components/{alphapicker/alphapicker.js => alphaPicker/alphaPicker.js} (100%) rename src/components/{alphapicker => alphaPicker}/style.css (100%) rename src/components/{appfooter/appfooter.css => appFooter/appFooter.css} (100%) rename src/components/{appfooter/appfooter.js => appFooter/appFooter.js} (93%) rename src/components/{channelmapper/channelmapper.js => channelMapper/channelMapper.js} (100%) rename src/components/{collectioneditor/collectioneditor.js => collectionEditor/collectionEditor.js} (100%) rename src/components/{displaysettings/displaysettings.js => displaySettings/displaySettings.js} (99%) rename src/components/{displaysettings/displaysettings.template.html => displaySettings/displaySettings.template.html} (100%) rename src/components/{homescreensettings/homescreensettings.js => homeScreenSettings/homeScreenSettings.js} (99%) rename src/components/{homescreensettings/homescreensettings.template.html => homeScreenSettings/homeScreenSettings.template.html} (100%) rename src/components/{imagedownloader/imagedownloader.js => imageDownloader/imageDownloader.js} (99%) rename src/components/{imagedownloader/imagedownloader.template.html => imageDownloader/imageDownloader.template.html} (100%) rename src/components/{imageoptionseditor/imageoptionseditor.js => imageOptionsEditor/imageOptionsEditor.js} (97%) rename src/components/{imageoptionseditor/imageoptionseditor.template.html => imageOptionsEditor/imageOptionsEditor.template.html} (100%) rename src/components/{lazyloader/lazyloader-intersectionobserver.js => lazyLoader/lazyLoaderIntersectionObserver.js} (100%) rename src/components/{metadataeditor/metadataeditor.js => metadataEditor/metadataEditor.js} (99%) rename src/components/{metadataeditor/metadataeditor.template.html => metadataEditor/metadataEditor.template.html} (100%) rename src/components/{metadataeditor/personeditor.js => metadataEditor/personEditor.js} (98%) rename src/components/{metadataeditor/personeditor.template.html => metadataEditor/personEditor.template.html} (100%) diff --git a/package.json b/package.json index a65b93cad..a88e5342c 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "src/components/cardbuilder/cardBuilder.js", "src/scripts/fileDownloader.js", "src/components/images/imageLoader.js", - "src/components/lazyloader/lazyloader-intersectionobserver.js", + "src/components/lazyLoader/lazyLoaderIntersectionObserver.js", "src/components/playback/mediasession.js", "src/components/sanatizefilename.js", "src/components/scrollManager.js", @@ -103,7 +103,7 @@ "src/scripts/imagehelper.js", "src/scripts/inputManager.js", "src/scripts/deleteHelper.js", - "src/components/actionsheet/actionsheet.js", + "src/components/actionSheet/actionSheet.js", "src/components/playmenu.js", "src/components/indicators/indicators.js", "src/scripts/keyboardNavigation.js", diff --git a/src/components/actionsheet/actionsheet.css b/src/components/actionSheet/actionSheet.css similarity index 100% rename from src/components/actionsheet/actionsheet.css rename to src/components/actionSheet/actionSheet.css diff --git a/src/components/actionsheet/actionsheet.js b/src/components/actionSheet/actionSheet.js similarity index 99% rename from src/components/actionsheet/actionsheet.js rename to src/components/actionSheet/actionSheet.js index 18239f8de..65b198254 100644 --- a/src/components/actionsheet/actionsheet.js +++ b/src/components/actionSheet/actionSheet.js @@ -3,7 +3,7 @@ import layoutManager from 'layoutManager'; import globalize from 'globalize'; import dom from 'dom'; import 'emby-button'; -import 'css!./actionsheet'; +import 'css!./actionSheet'; import 'material-icons'; import 'scrollStyles'; import 'listViewStyle'; diff --git a/src/components/alphapicker/alphapicker.js b/src/components/alphaPicker/alphaPicker.js similarity index 100% rename from src/components/alphapicker/alphapicker.js rename to src/components/alphaPicker/alphaPicker.js diff --git a/src/components/alphapicker/style.css b/src/components/alphaPicker/style.css similarity index 100% rename from src/components/alphapicker/style.css rename to src/components/alphaPicker/style.css diff --git a/src/components/appfooter/appfooter.css b/src/components/appFooter/appFooter.css similarity index 100% rename from src/components/appfooter/appfooter.css rename to src/components/appFooter/appFooter.css diff --git a/src/components/appfooter/appfooter.js b/src/components/appFooter/appFooter.js similarity index 93% rename from src/components/appfooter/appfooter.js rename to src/components/appFooter/appFooter.js index 07d7701ff..033a0b008 100644 --- a/src/components/appfooter/appfooter.js +++ b/src/components/appFooter/appFooter.js @@ -1,4 +1,4 @@ -define(['browser', 'css!./appfooter'], function (browser) { +define(['browser', 'css!./appFooter'], function (browser) { 'use strict'; function render(options) { diff --git a/src/components/channelmapper/channelmapper.js b/src/components/channelMapper/channelMapper.js similarity index 100% rename from src/components/channelmapper/channelmapper.js rename to src/components/channelMapper/channelMapper.js diff --git a/src/components/collectioneditor/collectioneditor.js b/src/components/collectionEditor/collectionEditor.js similarity index 100% rename from src/components/collectioneditor/collectioneditor.js rename to src/components/collectionEditor/collectionEditor.js diff --git a/src/components/displaysettings/displaysettings.js b/src/components/displaySettings/displaySettings.js similarity index 99% rename from src/components/displaysettings/displaysettings.js rename to src/components/displaySettings/displaySettings.js index 2b7b4bb60..b6938bc95 100644 --- a/src/components/displaysettings/displaysettings.js +++ b/src/components/displaySettings/displaySettings.js @@ -269,7 +269,7 @@ define(['require', 'browser', 'layoutManager', 'appSettings', 'pluginManager', ' } function embed(options, self) { - require(['text!./displaysettings.template.html'], function (template) { + require(['text!./displaySettings.template.html'], function (template) { options.element.innerHTML = globalize.translateDocument(template, 'core'); options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self)); if (options.enableSaveButton) { diff --git a/src/components/displaysettings/displaysettings.template.html b/src/components/displaySettings/displaySettings.template.html similarity index 100% rename from src/components/displaysettings/displaysettings.template.html rename to src/components/displaySettings/displaySettings.template.html diff --git a/src/components/homescreensettings/homescreensettings.js b/src/components/homeScreenSettings/homeScreenSettings.js similarity index 99% rename from src/components/homescreensettings/homescreensettings.js rename to src/components/homeScreenSettings/homeScreenSettings.js index 15812647c..b07e6de3c 100644 --- a/src/components/homescreensettings/homescreensettings.js +++ b/src/components/homeScreenSettings/homeScreenSettings.js @@ -470,7 +470,7 @@ define(['require', 'apphost', 'layoutManager', 'focusManager', 'globalize', 'loa function embed(options, self) { - require(['text!./homescreensettings.template.html'], function (template) { + require(['text!./homeScreenSettings.template.html'], function (template) { for (var i = 1; i <= numConfigurableSections; i++) { template = template.replace('{section' + i + 'label}', globalize.translate('LabelHomeScreenSectionValue', i)); diff --git a/src/components/homescreensettings/homescreensettings.template.html b/src/components/homeScreenSettings/homeScreenSettings.template.html similarity index 100% rename from src/components/homescreensettings/homescreensettings.template.html rename to src/components/homeScreenSettings/homeScreenSettings.template.html diff --git a/src/components/imagedownloader/imagedownloader.js b/src/components/imageDownloader/imageDownloader.js similarity index 99% rename from src/components/imagedownloader/imagedownloader.js rename to src/components/imageDownloader/imageDownloader.js index 3e0d00ac2..b30f1bf31 100644 --- a/src/components/imagedownloader/imagedownloader.js +++ b/src/components/imageDownloader/imageDownloader.js @@ -320,7 +320,7 @@ define(['dom', 'loading', 'apphost', 'dialogHelper', 'connectionManager', 'image function showEditor(itemId, serverId, itemType) { loading.show(); - require(['text!./imagedownloader.template.html'], function (template) { + require(['text!./imageDownloader.template.html'], function (template) { var apiClient = connectionManager.getApiClient(serverId); diff --git a/src/components/imagedownloader/imagedownloader.template.html b/src/components/imageDownloader/imageDownloader.template.html similarity index 100% rename from src/components/imagedownloader/imagedownloader.template.html rename to src/components/imageDownloader/imageDownloader.template.html diff --git a/src/components/imageoptionseditor/imageoptionseditor.js b/src/components/imageOptionsEditor/imageOptionsEditor.js similarity index 97% rename from src/components/imageoptionseditor/imageoptionseditor.js rename to src/components/imageOptionsEditor/imageOptionsEditor.js index 73a6d119d..cd8345fbb 100644 --- a/src/components/imageoptionseditor/imageoptionseditor.js +++ b/src/components/imageOptionsEditor/imageOptionsEditor.js @@ -82,7 +82,7 @@ define(['globalize', 'dom', 'dialogHelper', 'emby-checkbox', 'emby-select', 'emb this.show = function (itemType, options, availableOptions) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); - xhr.open('GET', 'components/imageoptionseditor/imageoptionseditor.template.html', true); + xhr.open('GET', 'components/imageOptionsEditor/imageOptionsEditor.template.html', true); xhr.onload = function (e) { var template = this.response; diff --git a/src/components/imageoptionseditor/imageoptionseditor.template.html b/src/components/imageOptionsEditor/imageOptionsEditor.template.html similarity index 100% rename from src/components/imageoptionseditor/imageoptionseditor.template.html rename to src/components/imageOptionsEditor/imageOptionsEditor.template.html diff --git a/src/components/lazyloader/lazyloader-intersectionobserver.js b/src/components/lazyLoader/lazyLoaderIntersectionObserver.js similarity index 100% rename from src/components/lazyloader/lazyloader-intersectionobserver.js rename to src/components/lazyLoader/lazyLoaderIntersectionObserver.js diff --git a/src/components/metadataeditor/metadataeditor.js b/src/components/metadataEditor/metadataEditor.js similarity index 99% rename from src/components/metadataeditor/metadataeditor.js rename to src/components/metadataEditor/metadataEditor.js index e1c1c8001..899bab778 100644 --- a/src/components/metadataeditor/metadataeditor.js +++ b/src/components/metadataEditor/metadataEditor.js @@ -1067,7 +1067,7 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi function show(itemId, serverId, resolve, reject) { loading.show(); - require(['text!./metadataeditor.template.html'], function (template) { + require(['text!./metadataEditor.template.html'], function (template) { var dialogOptions = { removeOnClose: true, @@ -1124,7 +1124,7 @@ define(['itemHelper', 'dom', 'layoutManager', 'dialogHelper', 'datetime', 'loadi loading.show(); - require(['text!./metadataeditor.template.html'], function (template) { + require(['text!./metadataEditor.template.html'], function (template) { elem.innerHTML = globalize.translateDocument(template, 'core'); diff --git a/src/components/metadataeditor/metadataeditor.template.html b/src/components/metadataEditor/metadataEditor.template.html similarity index 100% rename from src/components/metadataeditor/metadataeditor.template.html rename to src/components/metadataEditor/metadataEditor.template.html diff --git a/src/components/metadataeditor/personeditor.js b/src/components/metadataEditor/personEditor.js similarity index 98% rename from src/components/metadataeditor/personeditor.js rename to src/components/metadataEditor/personEditor.js index 9fb6fdec6..e71a7441c 100644 --- a/src/components/metadataeditor/personeditor.js +++ b/src/components/metadataEditor/personEditor.js @@ -11,7 +11,7 @@ define(['dialogHelper', 'layoutManager', 'globalize', 'require', 'paper-icon-but function show(person) { return new Promise(function (resolve, reject) { - require(['text!./personeditor.template.html'], function (template) { + require(['text!./personEditor.template.html'], function (template) { var dialogOptions = { removeOnClose: true, diff --git a/src/components/metadataeditor/personeditor.template.html b/src/components/metadataEditor/personEditor.template.html similarity index 100% rename from src/components/metadataeditor/personeditor.template.html rename to src/components/metadataEditor/personEditor.template.html diff --git a/src/controllers/livetvstatus.js b/src/controllers/livetvstatus.js index 737d49496..82d0b697d 100644 --- a/src/controllers/livetvstatus.js +++ b/src/controllers/livetvstatus.js @@ -155,7 +155,7 @@ define(['jQuery', 'globalize', 'scripts/taskbutton', 'dom', 'libraryMenu', 'layo } function mapChannels(page, providerId) { - require(['components/channelmapper/channelmapper'], function (channelmapper) { + require(['components/channelMapper/channelMapper'], function (channelmapper) { new channelmapper({ serverId: ApiClient.serverInfo().Id, providerId: providerId diff --git a/src/scripts/site.js b/src/scripts/site.js index 19ae9bb3d..ddb152eb4 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -375,7 +375,7 @@ var AppInfo = {}; define('filesystem', [scriptsPath + '/filesystem'], returnFirstDependency); - define('lazyLoader', [componentsPath + '/lazyloader/lazyloader-intersectionobserver'], returnFirstDependency); + define('lazyLoader', [componentsPath + '/lazyLoader/lazyLoaderIntersectionObserver'], returnFirstDependency); define('shell', [scriptsPath + '/shell'], returnFirstDependency); if ('registerElement' in document) { @@ -641,7 +641,7 @@ var AppInfo = {}; playlisteditor: componentsPath + '/playlisteditor/playlisteditor', medialibrarycreator: componentsPath + '/mediaLibraryCreator/mediaLibraryCreator', medialibraryeditor: componentsPath + '/mediaLibraryEditor/mediaLibraryEditor', - imageoptionseditor: componentsPath + '/imageoptionseditor/imageoptionseditor', + imageoptionseditor: componentsPath + '/imageOptionsEditor/imageOptionsEditor', apphost: componentsPath + '/apphost', visibleinviewport: bowerPath + '/visibleinviewport', qualityoptions: componentsPath + '/qualityOptions', @@ -738,7 +738,7 @@ var AppInfo = {}; // there are several objects that need to be instantiated // TODO find a better way to do this - define('appFooter', [componentsPath + '/appfooter/appfooter'], returnFirstDependency); + define('appFooter', [componentsPath + '/appFooter/appFooter'], returnFirstDependency); define('appFooter-shared', ['appFooter'], createSharedAppFooter); // TODO remove these libraries @@ -773,23 +773,23 @@ var AppInfo = {}; define('chromecastHelper', [componentsPath + '/chromecast/chromecasthelpers'], returnFirstDependency); define('mediaSession', [componentsPath + '/playback/mediasession'], returnFirstDependency); - define('actionsheet', [componentsPath + '/actionsheet/actionsheet'], returnFirstDependency); + define('actionsheet', [componentsPath + '/actionSheet/actionSheet'], returnFirstDependency); define('tunerPicker', [componentsPath + '/tunerPicker'], returnFirstDependency); define('mainTabsManager', [componentsPath + '/maintabsmanager'], returnFirstDependency); define('imageLoader', [componentsPath + '/images/imageLoader'], returnFirstDependency); define('directorybrowser', [componentsPath + '/directorybrowser/directorybrowser'], returnFirstDependency); - define('metadataEditor', [componentsPath + '/metadataeditor/metadataeditor'], returnFirstDependency); - define('personEditor', [componentsPath + '/metadataeditor/personeditor'], returnFirstDependency); + define('metadataEditor', [componentsPath + '/metadataEditor/metadataEditor'], returnFirstDependency); + define('personEditor', [componentsPath + '/metadataEditor/personEditor'], returnFirstDependency); define('playerSelectionMenu', [componentsPath + '/playback/playerSelectionMenu'], returnFirstDependency); define('playerSettingsMenu', [componentsPath + '/playback/playersettingsmenu'], returnFirstDependency); define('playMethodHelper', [componentsPath + '/playback/playmethodhelper'], returnFirstDependency); define('brightnessOsd', [componentsPath + '/playback/brightnessosd'], returnFirstDependency); define('alphaNumericShortcuts', [scriptsPath + '/alphanumericshortcuts'], returnFirstDependency); define('multiSelect', [componentsPath + '/multiSelect/multiSelect'], returnFirstDependency); - define('alphaPicker', [componentsPath + '/alphapicker/alphapicker'], returnFirstDependency); + define('alphaPicker', [componentsPath + '/alphaPicker/alphaPicker'], returnFirstDependency); define('tabbedView', [componentsPath + '/tabbedview/tabbedview'], returnFirstDependency); define('itemsTab', [componentsPath + '/tabbedview/itemstab'], returnFirstDependency); - define('collectionEditor', [componentsPath + '/collectioneditor/collectioneditor'], returnFirstDependency); + define('collectionEditor', [componentsPath + '/collectionEditor/collectionEditor'], returnFirstDependency); define('serverRestartDialog', [componentsPath + '/serverRestartDialog'], returnFirstDependency); define('playlistEditor', [componentsPath + '/playlisteditor/playlisteditor'], returnFirstDependency); define('recordingCreator', [componentsPath + '/recordingcreator/recordingcreator'], returnFirstDependency); @@ -805,7 +805,7 @@ var AppInfo = {}; define('mediaInfo', [componentsPath + '/mediainfo/mediainfo'], returnFirstDependency); define('itemContextMenu', [componentsPath + '/itemContextMenu'], returnFirstDependency); define('imageEditor', [componentsPath + '/imageeditor/imageeditor'], returnFirstDependency); - define('imageDownloader', [componentsPath + '/imagedownloader/imagedownloader'], returnFirstDependency); + define('imageDownloader', [componentsPath + '/imageDownloader/imageDownloader'], returnFirstDependency); define('dom', [scriptsPath + '/dom'], returnFirstDependency); define('playerStats', [componentsPath + '/playerstats/playerstats'], returnFirstDependency); define('searchFields', [componentsPath + '/search/searchfields'], returnFirstDependency); @@ -813,9 +813,9 @@ var AppInfo = {}; define('upNextDialog', [componentsPath + '/upnextdialog/upnextdialog'], returnFirstDependency); define('subtitleAppearanceHelper', [componentsPath + '/subtitlesettings/subtitleappearancehelper'], returnFirstDependency); define('subtitleSettings', [componentsPath + '/subtitlesettings/subtitlesettings'], returnFirstDependency); - define('displaySettings', [componentsPath + '/displaysettings/displaysettings'], returnFirstDependency); + define('displaySettings', [componentsPath + '/displaySettings/displaySettings'], returnFirstDependency); define('playbackSettings', [componentsPath + '/playbackSettings/playbackSettings'], returnFirstDependency); - define('homescreenSettings', [componentsPath + '/homescreensettings/homescreensettings'], returnFirstDependency); + define('homescreenSettings', [componentsPath + '/homeScreenSettings/homeScreenSettings'], returnFirstDependency); define('playbackManager', [componentsPath + '/playback/playbackmanager'], getPlaybackManager); define('layoutManager', [componentsPath + '/layoutManager', 'apphost'], getLayoutManager); define('homeSections', [componentsPath + '/homesections/homesections'], returnFirstDependency); From b448142f64b543de082f7282c721b6364b73a9f1 Mon Sep 17 00:00:00 2001 From: Franco Castillo Date: Sat, 16 May 2020 18:08:04 +0000 Subject: [PATCH 0073/2481] Translated using Weblate (Spanish (Argentina)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_AR/ --- src/strings/es-ar.json | 51 ++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/src/strings/es-ar.json b/src/strings/es-ar.json index 2ea87c424..84f206885 100644 --- a/src/strings/es-ar.json +++ b/src/strings/es-ar.json @@ -39,7 +39,7 @@ "Genres": "Géneros", "HeaderAlbumArtists": "Artistas de álbum", "HeaderContinueWatching": "Seguir viendo", - "HeaderNextUp": "A Continuación", + "HeaderNextUp": "Siguiente", "Movies": "Películas", "Photos": "Fotos", "Playlists": "Listas de reproducción", @@ -47,19 +47,19 @@ "Sync": "Sincronizar", "ValueSpecialEpisodeName": "Especial - {0}", "Absolute": "Absoluto", - "AccessRestrictedTryAgainLater": "El acceso está actualmente restringido. Por favor intente nuevamente mas tarde.", + "AccessRestrictedTryAgainLater": "El acceso está actualmente restringido. Por favor intente nuevamente más tarde.", "Actor": "Actor", "Add": "Agregar", - "AddItemToCollectionHelp": "Agregue items a las colecciones buscándolas y usando el click derecho o tocando el menu para añadirlas a una colección.", + "AddItemToCollectionHelp": "Agregue elementos a las colecciones buscándolos y usando el click derecho o tocando el menú para añadirlos a una colección.", "AddToCollection": "Añadir a la colección", - "AddToPlayQueue": "Añadir a la lista de reproducción", + "AddToPlayQueue": "Añadir a la cola de reproducción", "AddToPlaylist": "Añadir a la lista de reproducción", "AddedOnValue": "Añadidos {0}", - "AdditionalNotificationServices": "Navegue el catálogo de plugins para instalar servicios de notificación adicionales.", + "AdditionalNotificationServices": "Navegue el catálogo de complementos para instalar servicios de notificación adicionales.", "AirDate": "Fecha de emisión", "Aired": "Emitido", "Alerts": "Alertas", - "All": "Todo", + "All": "Todos", "AllChannels": "Todos los canales", "AllComplexFormats": "Todos los formatos complejos (ASS, SSA, VOBSUB, PGS, SUB, IDX, ...)", "AllEpisodes": "Todos los capítulos", @@ -69,9 +69,9 @@ "AllowMediaConversionHelp": "Permitir o denegar acceso a la opción de convertir medios.", "AllowOnTheFlySubtitleExtraction": "Permitir extracción de subtítulos al vuelo", "AllowOnTheFlySubtitleExtractionHelp": "Los subtítulos incrustados pueden extraerse de los videos y entregarse a los reproductores en texto plano para ayudar a evitar la transcodificación de video. En algunos sistemas, esto puede tardar mucho tiempo y provocar que la reproducción de video se detenga durante el proceso de extracción. Deshabilite esta opción para que los subtítulos incrustados se graben con transcodificación de video cuando no estén soportados de forma nativa por el dispositivo cliente.", - "AllowRemoteAccess": "Permitir conexiones remotas a este Servidor Jellyfin.", + "AllowRemoteAccess": "Permitir conexiones remotas a este servidor Jellyfin.", "AllowRemoteAccessHelp": "Si no está tildado, todas las conexiones remotas serán bloqueadas.", - "AlwaysPlaySubtitles": "Siempre mostrar subtítulos", + "AlwaysPlaySubtitles": "Reproducir siempre", "AnyLanguage": "Cualquier idioma", "Anytime": "Cualquier fecha", "Ascending": "Ascendente", @@ -81,8 +81,8 @@ "AutoBasedOnLanguageSetting": "Auto (basado en configuración de idioma)", "AutomaticallyConvertNewContent": "Convertir contenido nuevo automáticamente", "Backdrop": "Fondo", - "AllowHWTranscodingHelp": "Permite que el sintonizador transcodifique las transmisiones sobre la marcha. Esto puede ayudar a reducir la transcodificación requerida por el servidor.", - "AllowedRemoteAddressesHelp": "Lista separada por comas de direcciones IP o IP/máscara-de-red para redes a las que se les permitirá conectarse de forma remota. Si se deja vacía, todas las direcciones remotas serán permitidas.", + "AllowHWTranscodingHelp": "Permita que el sintonizador transcodifique transmisiones sobre la marcha. Esto puede ayudar a reducir la transcodificación requerida por el servidor.", + "AllowedRemoteAddressesHelp": "Lista separada por comas de direcciones IP o IP/máscara de red para redes a las que se les permitirá conectarse de forma remota. Si se deja vacía, todas las direcciones remotas serán permitidas.", "AlwaysPlaySubtitlesHelp": "Los subtítulos que concuerden con la preferencia de idioma se cargarán independientemente del idioma del audio.", "AndroidUnlockRestoreHelp": "Para recuperar tu compra anterior, por favor asegurate que iniciaste sesión en el dispositivo con la misma cuenta de Google (o Amazon) que hizo la compra originalmente. Asegurate de que la tienda de aplicaciones esté habilitada y no posea control parental alguno, y que tiene una conexión a Internet activa. Solo tendrás que hacer esto una sola vez para recuperar tu compra anterior.", "AroundTime": "Alrededor de {0}", @@ -409,7 +409,7 @@ "HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteAlbums": "Álbumes favoritos", - "Shows": "Series", + "Shows": "Programas", "CopyStreamURLError": "Hubo un error copiando la URL.", "CopyStreamURLSuccess": "URL copiada con éxito.", "CopyStreamURL": "Copiar la URL de la transmisión", @@ -454,8 +454,8 @@ "ClientSettings": "Configuración de cliente", "BoxSet": "Colección", "Artist": "Artista", - "AlbumArtist": "Artista del Album", - "Album": "Album", + "AlbumArtist": "Artista del álbum", + "Album": "Álbum", "HeaderDateIssued": "Fecha de Emisión", "HeaderCustomDlnaProfiles": "Perfiles personalizados", "HeaderContinueListening": "Seguir escuchando", @@ -540,5 +540,28 @@ "HeaderFeatureAccess": "Acceso a características", "HeaderFavoritePlaylists": "Listas de reproducción favoritas", "ButtonTogglePlaylist": "Lista de reproducción", - "ButtonToggleContextMenu": "Más" + "ButtonToggleContextMenu": "Más", + "HeaderPlaybackError": "Error de reproducción", + "HeaderPlayback": "Reproducción de medios", + "HeaderPlayOn": "Reproducir en", + "HeaderPlayAll": "Reproducir todo", + "HeaderPinCodeReset": "Restablecer código PIN", + "HeaderPhotoAlbums": "Álbumes de fotos", + "HeaderPeople": "Personas", + "HeaderPendingInvitations": "Invitaciones pendientes", + "HeaderPaths": "Rutas", + "HeaderPasswordReset": "Restablecer contraseña", + "HeaderPassword": "Contraseña", + "HeaderParentalRatings": "Calificaciones parentales", + "HeaderOtherItems": "Otros elementos", + "HeaderOnNow": "Ahora", + "HeaderNewDevices": "Nuevos dispositivos", + "HeaderNewApiKey": "Nueva clave de API", + "HeaderNavigation": "Navegación", + "HeaderMyMediaSmall": "Mis medios (pequeño)", + "HeaderMyMedia": "Mis medios", + "HeaderMyDevice": "Mi dispositivo", + "HeaderMusicVideos": "Videos musicales", + "HeaderMusicQuality": "Calidad de música", + "HeaderMovies": "Películas" } From 56a521610743e89dcc9bcd833b53ba04d8d7c49b Mon Sep 17 00:00:00 2001 From: Mednis Date: Sat, 16 May 2020 19:45:29 +0000 Subject: [PATCH 0074/2481] Translated using Weblate (Latvian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/lv/ --- src/strings/lv.json | 104 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/src/strings/lv.json b/src/strings/lv.json index 8aa0fd525..6c1086333 100644 --- a/src/strings/lv.json +++ b/src/strings/lv.json @@ -16,7 +16,7 @@ "MinutesAfter": "minūtes pēc", "MetadataManager": "Metadatu Pārvaldnieks", "Metadata": "Metadati", - "MessageYouHaveVersionInstalled": "", + "MessageYouHaveVersionInstalled": "Jums pašlaik ir uzstādīta versija {0}.", "MessageUnableToConnectToServer": "Mēs pašlaik nevaram sazināties ar izvēlēto serveri. Pārliecinies ka tas strādā, un mēģini vēlreiz.", "MessageTheFollowingLocationWillBeRemovedFromLibrary": "Sekojošie multvides ceļi tiks noņemti no tavas bibliotēkas:", "MessageSettingsSaved": "Iestatījumi saglabāti.", @@ -1134,5 +1134,105 @@ "Artist": "Izpildītājs", "AllowOnTheFlySubtitleExtractionHelp": "Iegultie subtitri var tikt izvilkto no video un nogādāti klientiem kā parasts teksts, lai nevajadzētu veikt lieku video trans kodēšanu. Uz dažām sistēmām tas var aizņemt ilgu laiku un likt video atskaņošanai uzkārties izvilkšanas procesa laikā. Atspējo šo lai iegultos subtitrus iededzinātu video trans kodēšanas veidā, kad tos noklusēti neatbalsta klienta ierīce.", "AlbumArtist": "Albuma Izpildītājs", - "Album": "Albums" + "Album": "Albums", + "PleaseRestartServerName": "Lūdzu atsāknē savu Jellyfin Serveri - {0}.", + "PlayNextEpisodeAutomatically": "Atskaņot nākamo epizodi automātiski", + "PlayFromBeginning": "Atskaņot no sākuma", + "PlayCount": "Atskaņošanas reizes", + "PlaybackData": "Atskaņošanas Dati", + "Person": "Persona", + "PerfectMatch": "Ideāla saderība", + "PasswordResetHeader": "Atiestatīt Paroli", + "PasswordResetConfirmation": "Vai tu tiešām gribi atiestatīt paroli?", + "PasswordResetComplete": "Parole ir atiestatīta.", + "PasswordMatchError": "Parolei un paroles pārbaudei ir jāsakrīt.", + "ParentalRating": "Vecāku vērtējums", + "PackageInstallFailed": "{0} (versija {1}) instalācija neizdevās.", + "PackageInstallCompleted": "{0} (versija {1}) instalācija pabeigta.", + "PackageInstallCancelled": "{0} (versija {1}) instalācija atcelta.", + "Overview": "Pārskats", + "OtherArtist": "Cits izpildītājs", + "OriginalAirDateValue": "Oriģinālais tiešraides datums: {0}", + "OptionWeekly": "Iknedēļu", + "OptionWeekends": "Nedēļas nogalēs", + "OptionWeekdays": "Darba dienās", + "OptionWednesday": "Trešdiena", + "OptionWakeFromSleep": "Modināt no miega", + "OptionUnplayed": "Neatskaņots", + "OptionTuesday": "Otrdiena", + "OptionTrackName": "Celiņa Nosaukums", + "OptionThumb": "Sīktēls", + "OptionThursday": "Ceturtdiena", + "OptionSunday": "Svētdiena", + "OptionSaveMetadataAsHidden": "Saglabāt metadatus un attēlus kā paslēptas datnes", + "OptionSaturday": "Sestdiena", + "OptionRequirePerfectSubtitleMatch": "Lejupielādēt tikai subtitrus, kas perfekti sakrīt ar manām video datnēm", + "OptionRegex": "Regex", + "OptionProtocolHls": "HTTP Tiešraides Straumes", + "OptionProfilePhoto": "Attēls", + "OptionPremiereDate": "Pirmizrādes Datums", + "OptionPoster": "Plakāts", + "OptionPlayCount": "Atskaņošanas Skaits", + "OptionOnAppStartup": "Lietotnes sāknēšanā", + "OptionNameSort": "Nosaukums", + "OptionMissingEpisode": "Trūkstošās Epizodes", + "OptionMax": "Maksimums", + "OptionLoginAttemptsBeforeLockoutHelp": "Vērtība nulle nozīmē noklusējuma trīs mēģinājumu priekš lietotājiem un piecu priekš administratoriem izmantošanu. Uzstādot uz -1 atspējos funkciju.", + "OptionLoginAttemptsBeforeLockout": "Nosaka, cik daudz nepareizi piekļuves mēģinājumi var notikt pirms notiek bloķēšana.", + "OptionImdbRating": "IMDb Vērtējums", + "OptionIgnoreTranscodeByteRangeRequestsHelp": "Ja iespējots, šie pieprasījumi tiks veikti bet ignorēs baitu diapazona hederu.", + "OptionIgnoreTranscodeByteRangeRequests": "Ignorēt trans-kodēšanas baitu diapazona pieprasījumus", + "OptionHomeVideos": "Attēli", + "OptionHlsSegmentedSubtitles": "HLS segmentēti subtitri", + "OptionHideUserFromLoginHelp": "Noderīgs lai paslēptu privātus vai paslēptus administratora kontus. Šim lietotājam būs jāieiet manuāli, ievadot savu lietotājvārdu un paroli.", + "OptionHideUser": "Paslēpt šo lietotāju no piekļuves ekrāniem", + "OptionHasThemeVideo": "Tēmu Video", + "OptionExternallyDownloaded": "Ārējā lejupielāde", + "OptionEstimateContentLength": "Novērtēt satura ilgumu trans-kodēšanas laikā", + "OptionEquals": "Vienāds ar", + "OptionEnableM2tsModeHelp": "Iespējo m2ts režīmu iekodējot uz mpegts.", + "OptionEnableM2tsMode": "Iespējot M2ts režīmu", + "OptionEnableForAllTuners": "Iespējot visām tūnera ierīcēm", + "OptionEnableExternalContentInSuggestionsHelp": "Atļaut interneta treilerus un tiešraides TV programmām tikt iekļautām ieteiktajā saturā.", + "OptionEnableExternalContentInSuggestions": "Iespējot ārējo saturu iekš ieteikumiem", + "OptionEnableAccessToAllChannels": "Iespējot piekļuvi visiem kanāliem", + "OptionEnableAccessFromAllDevices": "Iespējot piekļuvi no visām ierīcēm", + "OptionEmbedSubtitles": "Ievietot iekš konteinera", + "OptionDownloadThumbImage": "Sīktēls", + "OptionDownloadPrimaryImage": "Galvenais", + "OptionDownloadImagesInAdvanceHelp": "Noklusējumā, lielākā daļa attēlu tiek lejupielādēti kad tie ir pieprasīti Jellyfin lietotnē. Iespējo šo opciju lai iepriekšēji lejupielādētu visus attēlus, kad jauns saturs tiek importēts. Tas var radīt ievērojami ilgākus bibliotēkas skenējumus.", + "OptionDownloadImagesInAdvance": "Lejupielādēt attēlus iepriekš", + "OptionDownloadBackImage": "Aizmugure", + "OptionDownloadArtImage": "Māksla", + "OptionDisableUserHelp": "Ja atspējots, serveris neatļaus nekādus savienojumus no šī lietotāja. Esošie savienojumi tiks pēkšņi apturēti.", + "OptionDisableUser": "Atspējot šo lietotāju", + "OptionDatePlayed": "Atskaņošanas Datums", + "OptionDateAddedImportTime": "Izmantot datumu, kad tika ieskanēts bibliotēkā", + "OptionDateAddedFileTime": "Izmantot datņu izveides datumu", + "OptionDateAdded": "Pievienošanas Datums", + "OptionCriticRating": "Kritiķu Vēŗtējums", + "OptionCommunityRating": "Kopienas Vērtējums", + "OptionCaptionInfoExSamsung": "CaptionInfoEx (Samsung)", + "OptionBlockLiveTvChannels": "Tiešraides TV Kanāli", + "OptionAllowVideoPlaybackTranscoding": "Atļaut video atskaņošanu, kas prasa trans-kodēšanu", + "OptionAllowVideoPlaybackRemuxing": "Atļaut video atskaņošanu, kas prasa pārveidošanu bez pārkodēšanas", + "OptionAllowUserToManageServer": "Atļaut šim lietotājam pārvaldīt serveri", + "OptionAllowSyncTranscoding": "Atļaut satura lejupielādi un sinhronizēšanu, kas prasa trans-kodēšanu", + "OptionAllowRemoteSharedDevicesHelp": "DLNA ierīces tiek uzskatītas kā koplietotas līdz lietotājs sāk tās vadīt.", + "OptionAllowRemoteSharedDevices": "Atļaut koplietotu ierīču attālinātu vadību", + "OptionAllowMediaPlayback": "Atļaut satura atskaņošanu", + "OptionAllowContentDownloading": "Atļaut satura lejupielādi un sinhronizāciju", + "OptionForceRemoteSourceTranscoding": "Piespiest trans-kodēšanu no attāliem satura avotiem (kā Tiešraides TV)", + "OptionAllowAudioPlaybackTranscoding": "Atļaut audio atskaņošanu, kas prasa trans-kodēšanu", + "OnlyForcedSubtitlesHelp": "Tikai subtitri, kas atzīmēti kā piespiedu tiks ielādēti.", + "OnlyForcedSubtitles": "Tikai Piespiedu", + "MoreUsersCanBeAddedLater": "Papildus lietotāji var tikt pievienoti vēlāk no vadības paneļa.", + "MessagePluginConfigurationRequiresLocalAccess": "Lai konfigurētu šo paplašinājumu lūdzu tieši ieej savā lokālajā serverī.", + "MessagePleaseEnsureInternetMetadata": "Lūdzu pārliecinies vai metadatu lejupielāde no interneta ir iespējota.", + "MessageUnauthorizedUser": "Jūs neesat autorizēti lai piekļūtu serverim šajā brīdī. Lūdzu sazinieties ar savu servera administratoru priekš papildus informācijas.", + "MessageInstallPluginFromApp": "Šis paplašinājums ir jāuzstāda no lietotnes, kurā jūs to vēlaties izmantot.", + "LabelEmbedAlbumArtDidl": "Ievietot albumu vākus iekš Didl", + "EnableFastImageFadeIn": "Ātra attēlu ieplūšana", + "EnableFastImageFadeInHelp": "Iespējot ātrāku ieplūšanas animāciju priekš ielādētiem attēliem", + "LabelSelectFolderGroups": "Automātiski grupēt saturu no sekojošām datnēm skatos kā Filmas, Mūzika un TV:" } From d7ba814d35f949dcb3c3d2bd79580c6fa6f46d41 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 22:41:23 +0900 Subject: [PATCH 0075/2481] update translation utilities --- scripts/scdup.py | 11 +++++++++++ scripts/scgen.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/scdup.py b/scripts/scdup.py index 468e31f14..9b9ddf646 100644 --- a/scripts/scdup.py +++ b/scripts/scdup.py @@ -15,6 +15,8 @@ print(langlst) input('press enter to continue') keysus = [] +missing = [] + with open(langdir + '/' + 'en-us.json') as en: langus = json.load(en) for key in langus: @@ -32,10 +34,19 @@ for lang in langlst: for key in langjson: if key in keysus: langjnew[key] = langjson[key] + elif key not in missing: + missing.append(key) f.seek(0) f.write(json.dumps(langjnew, indent=inde, sort_keys=False, ensure_ascii=False)) f.write('\n') f.truncate() f.close() +print(missing) +print('LENGTH: ' + str(len(missing))) +with open('missing.txt', 'w') as out: + for item in missing: + out.write(item + '\n') + out.close() + print('DONE') diff --git a/scripts/scgen.py b/scripts/scgen.py index 0d831426e..12af27320 100644 --- a/scripts/scgen.py +++ b/scripts/scgen.py @@ -34,7 +34,7 @@ for lang in langlst: print(dep) print('LENGTH: ' + str(len(dep))) -with open('scout.txt', 'w') as out: +with open('unused.txt', 'w') as out: for item in dep: out.write(item + '\n') out.close() From 6c8884583d6330c1c4e52d35342b7d80cb3292dd Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 23:03:54 +0900 Subject: [PATCH 0076/2481] remove some deprecated translations --- src/strings/cs.json | 14 -------------- src/strings/da.json | 26 -------------------------- src/strings/de.json | 2 -- src/strings/es-ar.json | 5 ----- src/strings/fi.json | 4 ---- src/strings/is-is.json | 8 -------- src/strings/kk.json | 5 ----- src/strings/nl.json | 2 -- src/strings/sl-si.json | 4 ---- src/strings/zh-cn.json | 1 - src/strings/zh-tw.json | 19 ------------------- 11 files changed, 90 deletions(-) diff --git a/src/strings/cs.json b/src/strings/cs.json index 5e52da3f5..50f355fb4 100644 --- a/src/strings/cs.json +++ b/src/strings/cs.json @@ -197,8 +197,6 @@ "ErrorMessageStartHourGreaterThanEnd": "Čas ukončení musí být větší než čas startu.", "ErrorMessageUsernameInUse": "Uživatelské jméno se již používá. Prosím, vyberte nový název a zkuste to znovu.", "ErrorPleaseSelectLineup": "Vyberte prosím sestavu a zkuste to znovu. Pokud nejsou k dispozici žádné sestavy, zkontrolujte, zda je vaše uživatelské jméno, heslo a poštovní směrovací číslo správné.", - "ErrorReachingJellyfinConnect": "Došlo k chybě při navázání spojení k serveru Jellyfin Connect. Ujistěte se, zda je funkční připojení k internetu a zkuste to znovu.", - "ErrorRemovingJellyfinConnectAccount": "Nastala chyba při odebrání účtu Jellyfin Connect. Zkontrolujte zda máte aktivní internetové připojení a zkuste znovu.", "ErrorSavingTvProvider": "Při ukládání poskytovatele TV došlo k chybě. Prosím, ujistěte se, že je přístupný a zkuste to znovu.", "ExitFullscreen": "Opustit celou obrazovku", "ExtraLarge": "Extra velký", @@ -436,7 +434,6 @@ "LabelAccessDay": "Den týdne:", "LabelAccessEnd": "Konec:", "LabelAccessStart": "Začátek:", - "LabelAddConnectSupporterHelp": "Chcete-li přidat uživatele, který není uveden v seznamu, budete muset nejprve propojit svůj účet Jellyfin Connect ze strany profilu uživatele.", "LabelAddedOnDate": "Přidáno {0}", "LabelAirDate": "Dny vysílání:", "LabelAirDays": "Vysíláno:", @@ -580,7 +577,6 @@ "LabelKodiMetadataSaveImagePathsHelp": "Toto nastavení je doporučeno, pokud používáte názvy souborů (obrázků), které nejsou v souladu s pokyny Kodi.", "LabelLanguage": "Jazyk:", "LabelLastResult": "Poslední výsledky:", - "LabelLimitIntrosToUnwatchedContent": "Přehrávat trailery pouze u nezhlédnutého obsahu", "LabelLineup": "Hlavní linie:", "LabelLocalHttpServerPortNumber": "Lokální HTTP port:", "LabelLocalHttpServerPortNumberHelp": "Číslo portu TCP, ke kterému by se měl připojit HTTP server Jellyfin.", @@ -1232,7 +1228,6 @@ "AllowOnTheFlySubtitleExtractionHelp": "Vložené titulky mohou být extrahovány z videa a dodávány do aplikací ve formě prostého textu, aby se zabránilo překódování videa. V některých systémech to může trvat dlouho a způsobit zasekávání přehrávání videa. Při vypnutí funkce budou během překódování obsažené titulky vypáleny do obrazu, pokud je klientské zařízení nativně nepodporuje.", "AllowRemoteAccess": "Povolit vzdálené připojení na tento Jellyfin server.", "AllowRemoteAccessHelp": "Pokud není zapnuto, všechna vzdálená připojení budou blokována.", - "AllowSeasonalThemesHelp": "Pokud je povoleno, sezónní motivy občas přepíšou nastavení vašeho motivu.", "AllowedRemoteAddressesHelp": "Seznam IP adres nebo síťových masek oddělených čárkou pro sítě, ze kterých se lze vzdáleně připojit. Pokud necháte prázdné, všechny adresy budou povoleny.", "AnamorphicVideoNotSupported": "Anamorfní video není podporováno", "AndroidUnlockRestoreHelp": "Chcete-li obnovit předchozí nákup, ujistěte se, že jste přihlášeni do zařízení se stejným účtem Google (nebo Amazon), na kterém byl nákup původně proveden. Ujistěte se, že je povolen přístup k úložišti aplikací a není omezen žádnou rodičovskou kontrolou a ujistěte se, že máte aktivní připojení k internetu. Toto budete muset provést pouze jednou, abyste tento předchozí nákup obnovili.", @@ -1241,8 +1236,6 @@ "AudioBitDepthNotSupported": "Bitová hloubka zvuku není podporována", "AudioSampleRateNotSupported": "Frekvence vzorkování zvuku není podporována", "AutoBasedOnLanguageSetting": "Automaticky (na základě jazykového nastavení)", - "AutomaticallyConvertNewContent": "Automaticky zkonvertovat nový obsah", - "AutomaticallyConvertNewContentHelp": "Nový obsah přidaný do této složky bude automaticky zkonvertován.", "Banner": "Banner", "BestFit": "Nejvhodnější", "Blacklist": "Černá listina", @@ -1317,7 +1310,6 @@ "HeaderFetcherSettings": "Nastavení načítání", "HeaderImageLogo": "Logo", "HeaderImageOptions": "Volby obrázku", - "HeaderInviteWithJellyfinConnect": "Pozvat s Jellyfin Connect", "HeaderKodiMetadataHelp": "Chcete-li povolit nebo zakázat Nfo metadata, upravte nastavení knihovny v sekci ukládání metadat.", "HeaderLiveTV": "Živá TV", "HeaderLiveTv": "Živá TV", @@ -1335,14 +1327,11 @@ "HeaderTV": "TV", "HeaderTypeImageFetchers": "{0} stahovačů obrázků", "HeaderUpcomingEpisodes": "Následující epizody", - "HeaderUpcomingNews": "Následující novinky", "HeaderVideo": "Video", "HeaderVideoType": "Formát videa", "Horizontal": "Vodorovně", - "HowWouldYouLikeToAddUser": "Jak chcete přidat uživatele?", "HttpsRequiresCert": "Chcete-li povolit zabezpečená připojení, budete muset zadat důvěryhodný certifikát SSL, například Let's Encrypt. Zadejte prosím certifikát nebo zakažte zabezpečená připojení.", "Invitations": "Pozvánky", - "InviteAnJellyfinConnectUser": "Přidejte uživatele odesláním e-mailové pozvánky.", "KeepDownload": "Zachovat stahování", "LabelAlbum": "Album:", "LabelAllowedRemoteAddresses": "Filtr vzdálené IP adresy:", @@ -1355,7 +1344,6 @@ "LabelCameraUploadPathHelp": "Vyberte vlastní umístění nahraných souborů. Toto přepíše jakékoli výchozí nastavení v sekci nahrávání souborů z fotoaparátu. Pokud použijete vlastní umístění, bude potřeba jej také přidat jako knihovnu v nastavení Jellyfin.", "LabelCertificatePassword": "Heslo certifikátu:", "LabelCertificatePasswordHelp": "Pokud certifikát vyžaduje heslo, zadejte jej prosím zde.", - "LabelConvertTo": "Konvertovat na:", "LabelCustomCertificatePath": "Vlastní umístění SSL certifikátu:", "LabelCustomCertificatePathHelp": "Umístění souboru PKCS #12, který obsahuje certifikát a soukromý klíč k povolení podpory TLS na vlastní doméně.", "LabelDateTimeLocale": "Místní nastavení data:", @@ -1385,7 +1373,6 @@ "LabelSortOrder": "Pořadí řazení:", "LabelSpecialSeasonsDisplayName": "Zobrazovaný název pro zvláštní sezónu:", "LabelSubtitleDownloaders": "Stahovače titulků:", - "LabelSyncNoTargetsHelp": "Vypadá to, že v současné době nemáte žádné aplikace, které podporují stahování offline.", "LabelTVHomeScreen": "Hlavní obrazovka TV režimu:", "LabelTag": "Tag:", "LabelTypeMetadataDownloaders": "{0} stahovače metadat:", @@ -1484,7 +1471,6 @@ "SmallCaps": "Malá písmena", "Smaller": "Menší", "Sort": "Třídit", - "StatsForNerds": "Statistiky pro šprty", "SubtitleAppearanceSettingsAlsoPassedToCastDevices": "Tato nastavení platí také pro jakékoli přehrávání na Chromecastu spuštěné tímto zařízením.", "SubtitleAppearanceSettingsDisclaimer": "Tato nastavení se nevztahuje na grafické titulky (PGS, DVD atd.) nebo ASS/SSA titulky, které mají vlastní vložené styly.", "SubtitleDownloadersHelp": "Povolte a zařaďte preferované stahovače titulků v pořadí podle priority.", diff --git a/src/strings/da.json b/src/strings/da.json index 5e7bbd2e1..fa61bdeb7 100644 --- a/src/strings/da.json +++ b/src/strings/da.json @@ -632,11 +632,6 @@ "LabelSubtitleFormatHelp": "F. eks: srt", "LabelSubtitlePlaybackMode": "Underteksttilstand:", "LabelSupportedMediaTypes": "Understøttede medieformater:", - "LabelSyncJobName": "Navn til synkroniserings job:", - "LabelSyncPath": "Synked indholdssti:", - "LabelSyncTempPath": "Sti for midlertidige filer:", - "LabelSyncTempPathHelp": "Specificér en brugerdefineret synkroniserings arbejds-mappe. Konverterede filer vil under synkroniseringsprocessen blive gemt her.", - "LabelSyncTo": "Synkroniser til:", "LabelTheme": "Tema:", "LabelTime": "Tid:", "LabelTimeLimitHours": "Tidsgrænse (timer):", @@ -1111,10 +1106,6 @@ "AudioSampleRateNotSupported": "Lydens samplerate ikke understøttet", "Auto": "Auto", "AutoBasedOnLanguageSetting": "Automatisk (baseret på sprogindstilling)", - "AutomaticallyConvertNewContent": "Konverter automatisk nyt indhold", - "AutomaticallyConvertNewContentHelp": "Nyt indhold tilføjet til denne mappe vil blive konverteret automatisk.", - "AutomaticallySyncNewContent": "Download nyt indhold automatisk", - "AutomaticallySyncNewContentHelp": "Nyt indhold tilføjet til denne mappe vil automatisk blive downloadet til enheden.", "Backdrop": "Baggrund", "Backdrops": "Baggrunde", "Banner": "Banner", @@ -1198,8 +1189,6 @@ "Episodes": "Afsnit", "ErrorAddingGuestAccount1": "Der skete en fejl ved tilføjelsen af Jellyfin Connect kontoen. Har din gæst lavet en Jellyfin konto? De kan regsistrere sig på {0}.", "ErrorAddingGuestAccount2": "Hvis du stadig har problemer, så send venligst en email til {0}, og inkludér din email adresse såvel som deres.", - "ErrorAddingJellyfinConnectAccount1": "Der skete en fejl ved tilføjelsen af Jellyfin Connect kontoen. Har du lavet en Jellyfin konto? Registrer dig på {0}.", - "ErrorAddingJellyfinConnectAccount2": "Hvis du stadig har et problem, så send venligst en email til {0} fra den email adresse tilknyttet Jellyfin kontoen.", "ErrorDeletingItem": "Der skete en fejl ved sletningen af mediet fra Jellyfin Server. Tjek venligst at Jellyfin Server har skrive adgang til mediemappen og prøv igen.", "ExtraLarge": "Ekstra Stor", "Extras": "Bonusmateriale", @@ -1236,7 +1225,6 @@ "HeaderNavigation": "Navigation", "HeaderNextEpisodePlayingInValue": "Næste afsnit afspilles om {0}", "HeaderNextVideoPlayingInValue": "Næste video afspilles om {0}", - "HeaderOffline": "Offline", "HeaderPhotoAlbums": "Foto Albummer", "HeaderPlayOn": "Afspil På", "HeaderProgram": "Program", @@ -1252,18 +1240,15 @@ "HeaderTags": "Mærker", "HeaderTopPlugins": "Bedste Plugins", "HeaderType": "Type", - "HeaderUpcomingNews": "Kommende Nyheder", "HeaderVideo": "Video", "HeaderVideoQuality": "Video Kvalitet", "HeaderVideoType": "Video Type", - "HeaderWaitingForWifi": "Venter på Wifi", "Hide": "Skjul", "HideWatchedContentFromLatestMedia": "Skjul set indhold fra seneste medier", "Home": "Hjem", "Horizontal": "Horisontalt", "ImdbRating": "IMDb bedømmelse", "InterlacedVideoNotSupported": "Interlaced video ikke understøttet", - "InviteAnJellyfinConnectUser": "Tilføj en bruger ved at sende en email invitation.", "KeepDownload": "Behold hentning", "Label3DFormat": "3D format:", "LabelAlbum": "Album:", @@ -1275,7 +1260,6 @@ "LabelCache": "Cache:", "LabelCertificatePassword": "Adgangskode til certifikat:", "LabelCertificatePasswordHelp": "Hvis dit certifikat kræver en adgangskode, skriv det venligst her.", - "LabelConvertTo": "Konvertér til:", "LabelDashboardTheme": "Server dashboard tema:", "LabelDateTimeLocale": "Dato og tid område:", "LabelDefaultScreen": "Standard skærm:", @@ -1314,7 +1298,6 @@ "LabelSoundEffects": "Lydeffekter:", "LabelStatus": "Status:", "LabelSubtitles": "Undertekster", - "LabelSyncNoTargetsHelp": "Det ser ud til at du ikke har nogen apps der understøtter offline hentning.", "LabelTVHomeScreen": "TV modus hjemmeskærm:", "LabelTag": "Mærke:", "LabelTagline": "Taglinje:", @@ -1365,7 +1348,6 @@ "MessageImageFileTypeAllowed": "Kun JPEG og PNG filer er understøttet.", "MessageImageTypeNotSelected": "Vælg venligst en type af billede i drop-down menuen.", "MessageNoDownloadsFound": "Ingen offline hentninger. Hent dine medier til offline brug ved at klikke Hent igennem app'en.", - "MessageNoSyncJobsFound": "Ingen hentninger fundet. Opret hent job ved at bruge Hent knapperne igennem app'en.", "MessagePlayAccessRestricted": "Afspilning af dette indhold er begrænset. Kontakt venligst server administratoren for mere information.", "Metadata": "Metadata", "Mobile": "Mobil", @@ -1426,7 +1408,6 @@ "PlaybackSettings": "Afspilningsindstillinger", "PlaybackSettingsIntro": "For at indstille standard afspilningsindstillingerne, stop video afspilning, herefter klik på dit bruger ikon i øverste højre sektion af denne app.", "Playlists": "Afspilningslister", - "PleaseSelectDeviceToSyncTo": "Vælg venligst en enhed at hente til.", "Previous": "Forrige", "Primary": "Primær", "PrivacyPolicy": "Privatlivs politik", @@ -1455,16 +1436,12 @@ "Sort": "Sortér", "SortByValue": "Sortér efter {0}", "Standard": "Standard", - "StatsForNerds": "Stats for nørder", "SubtitleAppearanceSettingsAlsoPassedToCastDevices": "Disse indstilinger bliver aktiveret på enhver Chromecast afspilning på denne enhed.", "SubtitleAppearanceSettingsDisclaimer": "Disse indstillinger bliver ikke aktiveret på grafiske undertekster (PGS, DVD, etc) eller ASS/SSA undertekster der har deres egen indbyggede stil.", "SubtitleCodecNotSupported": "Undertekst format ikke understøttet", "SubtitleSettings": "Undertekst indstillinger", "SubtitleSettingsIntro": "For at konfigurere standard undertekst udseende og sprog indstillinger, stop video afspilning, herefter klik på dit bruger ikon i øverste højre sektion af denne app.", "Suggestions": "Forslag", - "SyncUnwatchedVideosOnly": "Hent kun usete videoer", - "SyncUnwatchedVideosOnlyHelp": "Kun usete videoer vil blive hentet, og videoer vil bliver slettet fra enheden når de er set.", - "SyncingDots": "Synkroniserer...", "TV": "TV", "TabAlbums": "Albummer", "TabCodecs": "Codeks", @@ -1478,7 +1455,6 @@ "TabPlaylist": "Afspilningsliste", "TabServer": "Server", "TabStreaming": "Streamer", - "TabSync": "Sync", "TabTV": "TV", "Tags": "Mærker", "TagsValue": "Mærker: {0}", @@ -1489,7 +1465,6 @@ "TitleLiveTV": "Live TV", "TitleServer": "Server", "TitleSupport": "Support", - "TitleSync": "Sync", "Trailer": "Forfilm", "Trailers": "Forfilm", "Transcoding": "Omkodning", @@ -1519,7 +1494,6 @@ "ViewTypeTvShows": "TV", "Watched": "Set", "Whitelist": "Hvidliste", - "WifiRequiredToDownload": "En Wifi forbindelse er påkrævet for at forsætte hentning.", "Yes": "Ja", "HeaderFavoriteMovies": "Favoritfilm", "HeaderFavoriteShows": "Favoritserier", diff --git a/src/strings/de.json b/src/strings/de.json index 1f70b418f..cb6c86d16 100644 --- a/src/strings/de.json +++ b/src/strings/de.json @@ -29,8 +29,6 @@ "AllowOnTheFlySubtitleExtractionHelp": "Eingebettete Untertitel können aus Videos extrahiert und in Reintext an Clients gesendet werden, um eine Videotranskodierung zu vermeiden. Auf manchen Systemen kann dieser Vorgang eine lange Zeit in Anspruch nehmen und deswegen währenddessen die Videowiedergabe stoppen. Deaktiviere diese Option, um eingebettete Untertitel während des Videotranskodierens einbrennen zu lassen, wenn sie nicht nativ vom Client unterstützt werden.", "AllowRemoteAccess": "Erlaube externe Verbindungen zu diesem Jellyfin Server.", "AllowRemoteAccessHelp": "Wenn deaktiviert werden alle externen Verbindungen blockiert.", - "AllowSeasonalThemes": "Erlaube automatische Jahreszeitenmotive", - "AllowSeasonalThemesHelp": "Wenn aktiviert, werden Jahreszeitenmotive von Zeit zu Zeit deine Motiveinstellungen überschreiben.", "AllowedRemoteAddressesHelp": "Kommagetrennte Liste von IP Adressen oder IP/Netzmasken für Netzwerke, für die externe Verbindungen erlaubt sind. Wenn leer, sind alle Adressen erlaubt.", "AlwaysPlaySubtitles": "Immer anzeigen", "AlwaysPlaySubtitlesHelp": "Untertitel die den Spracheinstellungen entsprechen werden unabhängig von der Tonspursprache geladen.", diff --git a/src/strings/es-ar.json b/src/strings/es-ar.json index 84f206885..bfc1a574a 100644 --- a/src/strings/es-ar.json +++ b/src/strings/es-ar.json @@ -79,7 +79,6 @@ "Audio": "Audio", "Auto": "Auto", "AutoBasedOnLanguageSetting": "Auto (basado en configuración de idioma)", - "AutomaticallyConvertNewContent": "Convertir contenido nuevo automáticamente", "Backdrop": "Fondo", "AllowHWTranscodingHelp": "Permita que el sintonizador transcodifique transmisiones sobre la marcha. Esto puede ayudar a reducir la transcodificación requerida por el servidor.", "AllowedRemoteAddressesHelp": "Lista separada por comas de direcciones IP o IP/máscara de red para redes a las que se les permitirá conectarse de forma remota. Si se deja vacía, todas las direcciones remotas serán permitidas.", @@ -91,9 +90,6 @@ "AspectRatio": "Relación de aspecto", "AudioBitrateNotSupported": "Tasa de bits de audio no soportado", "AudioSampleRateNotSupported": "Frecuencia de muestreo de audio no soportada", - "AutomaticallyConvertNewContentHelp": "El contenido nuevo que se agregue a esta carpeta va a convertirse automáticamente.", - "AutomaticallySyncNewContent": "Descargar contenido nuevo automáticamente", - "AutomaticallySyncNewContentHelp": "El contenido nuevo agregado a esta carpeta va a ser descargado automáticamente al dispositivo.", "Backdrops": "Imágenes de fondo", "Banner": "Cartel", "BestFit": "Mejor ajuste", @@ -255,7 +251,6 @@ "ChannelNameOnly": "Sólo canal {0}", "ChannelNumber": "Número del canal", "CinemaModeConfigurationHelp": "El modo cine trae la experiencia del cine directamente a tu living, con la posibilidad de reproducir tráilers e introducciones personalizadas antes de la función principal.", - "CinemaModeConfigurationHelp2": "Las aplicaciones de Jellyfin tienen la opcion de habilitar o deshabilitar el modo cine. Las aplicaciones de TV activan el modo cine de forma predeterminada.", "CinemaModeFeatureDescription": "El modo cine te da la verdadera experiencia del cine con tráilers e introducciones personalizadas antes de la función principal.", "CloudSyncFeatureDescription": "Sincroniza tus medios a la nube para respaldos, archivados y conversiones más fáciles.", "ColorPrimaries": "Colores primarios", diff --git a/src/strings/fi.json b/src/strings/fi.json index 1c7147a50..a5e5bd095 100644 --- a/src/strings/fi.json +++ b/src/strings/fi.json @@ -94,8 +94,6 @@ "AllowOnTheFlySubtitleExtractionHelp": "Sisäiset tekstitykset voidaan lähettää päätelaitteille ilmitekstinä, jotta videota ei tarvitsisi uudelleenkoodata. Joissain järjestelmissä tämä voi viedä paljon aikaa ja aiheuttaa toiston pysähtymisen purun ajaksi. Poista tämä käytöstä polttaaksesi tekstiykset suoraan videoon, mikäli päätelaite ei tue tekstityksiä.", "AllowRemoteAccess": "Salli etäyhteydet tähän Jellyfin palvelimeen.", "AllowRemoteAccessHelp": "Jos merkki puuttuu, kaikki ulkopuoliset yhteydet estetään.", - "AllowSeasonalThemes": "Salli automaattiset vuodenaikateemat", - "AllowSeasonalThemesHelp": "Jos asetettu, vuodenaikateemat satunnaisesti yliajavat teema-asetuksesi.", "AllowedRemoteAddressesHelp": "Pilkuilla eroteltu lista IP-osoitteista tai IP/verkonpeite merkinnöistä verkoille, joille sallitaan etäyhteys palvelimeen. Tyhjäksi jätetty lista tarkoittaa, että kaikki osoitteet sallitaan.", "AlwaysPlaySubtitles": "Näytä aina tekstitykset", "AlwaysPlaySubtitlesHelp": "Oletuskieliasetusta vastaava tekstitys otetaan käyttöön ääniraidan kielestä huolimatta.", @@ -911,7 +909,6 @@ "HeaderRemoteControl": "Etäohjaus", "HeaderPleaseSignIn": "Ole hyvä ja kirjaudu sisään", "BoxSet": "Laatikkosarja", - "LabelAccessEnd": "", "LabelManufacturerUrl": "Valmistajan verkko-osoite", "LabelManufacturer": "Valmistaja:", "LabelLogs": "Lokit:", @@ -1228,7 +1225,6 @@ "LabelLoginDisclaimer": "Sisäänkirjautumisen vastuuvapauslauseke:", "LabelLibraryPageSize": "Kirjastosivun kohteiden määrä:", "LabelLibraryPageSizeHelp": "Asettaa kirjastosivulla näytettävien kohteiden määrän. Arvo 0 poistaa sivutuksen käytöstä.", - "LabelLineup": "", "Unrated": "Luokittelematon", "ExtractChapterImagesHelp": "Pikkukuvien luominen mahdollistaa sovellusten näyttää graafiikkaa valintavalikoissa. Prosessi voi olla hidas, prosessoria kuormittava ja saattaa vaatia useita gigatavuja tilaa. Se suoritetaan, kun videoita havaitaan, ja myös yöksi suunniteltuna tehtävänä. Aikataulu on konfiguroitavissa ajoitetuissa tehtävissä. Tätä tehtävää ei ole suositeltavaa suorittaa korkean kuormituksen aikana.", "OnWakeFromSleep": "Lepotilasta poistuttaessa", diff --git a/src/strings/is-is.json b/src/strings/is-is.json index 70fc89119..ee0498679 100644 --- a/src/strings/is-is.json +++ b/src/strings/is-is.json @@ -241,17 +241,9 @@ "ConfirmDeleteImage": "Eyða mynd?", "ButtonRename": "Endurnefna", "Sync": "Samstilla", - "Never": "", - "News": "", "ButtonRevoke": "Afturkalla", "ButtonRepeat": "Endurtaka", - "MusicArtist": "", - "MusicAlbum": "", - "No": "", "Monday": "Mánudagur", - "Name": "", - "Mute": "", - "MusicVideo": "", "ButtonRefresh": "Endurhlaða", "ButtonParentalControl": "Foreldraeftirlit", "ButtonOff": "Af", diff --git a/src/strings/kk.json b/src/strings/kk.json index a31d2b375..776caad50 100644 --- a/src/strings/kk.json +++ b/src/strings/kk.json @@ -834,7 +834,6 @@ "LabelTypeText": "Mátin", "LabelUnairedMissingEpisodesWithinSeasons": "Kórsetilmegen bólimderdi maýsym ishinde beıneleý", "LabelUnknownLanguage": "Belgisiz til", - "LabelUploadSpeedLimit": "Júktep salý qarqynynyń shegi, Mbıt/s:", "LabelUrl": "URL:", "LabelUseNotificationServices": "Kelesi qyzmetterdi paıdalaný:", "LabelUser": "Paıdalanýshy:", @@ -872,7 +871,6 @@ "LiveBroadcasts": "Tikeleı taratymdar", "LiveTV": "Efır", "LiveTvFeatureDescription": "Jellyfin Server ornatylǵan úılesimdi TD-túner qurylǵysy arqyly kezkelgen Jellyfin-qoldanbaǵa TD-efırdi tikeleı jiberý.", - "LiveTvRequiresUnlock": "", "LiveTvUpdateAvailable": "(Jańartý qoljetimdi)", "LoginDisclaimer": "Jellyfin jeke tasyǵyshhanańyzdy (mysaly, úılik beıneler men fotosýretterdi) basqarýǵa kómektesý úshin arnalǵan. Bizdiń paıdalaný sharttaryn qarańyz. Kezkelgen Jellyfin baǵdarlamalyq jasaqtamasyn paıdalnǵanda osy sharttardyń qabyldaýyn bildiredi.", "Logo": "Logotıp", @@ -1229,7 +1227,6 @@ "SearchForSubtitles": "Sýbtıtrlerdi izdeý", "SearchResults": "Izdeý nátıjeleri", "SecondaryAudioNotSupported": "Dybys jolshyǵyn aýystyrý úshin qoldaý kórsetilmeıdi", - "SelectCameraUploadServers": "Kameradan fotosýretterdi kelesi serverlerge júktep salý:", "SendMessage": "Habar jiberý", "Series": "Telehıkaıa", "SeriesCancelled": "Telehıkaıa boldyrylmady.", @@ -1346,8 +1343,6 @@ "TrackCount": "{0} jolshyq", "Trailers": "Treılerler", "Transcoding": "Qaıta kodtaýda", - "TryMultiSelect": "Úndesken bólekteýdi synap kórý", - "TryMultiSelectMessage": "Birneshe tasyǵysh derekter elementterin óńdeý úshin, kezkelgen posterdi jaı ǵana tintýir batyrmaǵa basyp turyp nuqyńyz jáne basqarýyn qalaǵan elementterdi bólekteńiz. Synap kórińiz!", "Tuesday": "seısenbi", "TvLibraryHelp": "{0}TD-kórsetimdi ataý nusqaýlyǵyn{1} qarap shyǵý.", "Uniform": "Biryńǵaı", diff --git a/src/strings/nl.json b/src/strings/nl.json index a3c3a84f1..0b67a184d 100644 --- a/src/strings/nl.json +++ b/src/strings/nl.json @@ -1282,8 +1282,6 @@ "HeaderHttpHeaders": "HTTP Headers", "HeaderImageLogo": "Logo", "HeaderMenu": "Menu", - "HeaderOffline": "Offline", - "HeaderOfflineDownloads": "Offline Media", "HeaderResetTuner": "Ontvanger resetten", "HeaderReviews": "Beoordelingen", "HeaderStatus": "Status", diff --git a/src/strings/sl-si.json b/src/strings/sl-si.json index a692edbd3..109004cce 100644 --- a/src/strings/sl-si.json +++ b/src/strings/sl-si.json @@ -127,10 +127,6 @@ "AudioSampleRateNotSupported": "Frekvenca vzorčenja zvoka ni podprta", "Auto": "Samodejno", "AutoBasedOnLanguageSetting": "Samodejno (na podlagi nastavitve jezika)", - "AutomaticallyConvertNewContent": "Samodejno pretvori novo vsebino", - "AutomaticallyConvertNewContentHelp": "Nova vsebina dodana v to mapo bo samodejno pretvorjena.", - "AutomaticallySyncNewContent": "Samodejno prenesi novo vsebino", - "AutomaticallySyncNewContentHelp": "Nova vsebina dodana v to mapo bo samodejno prenesena na to napravo.", "Backdrop": "Ozadje", "Backdrops": "Ozadja", "BestFit": "Najboljše prileganje", diff --git a/src/strings/zh-cn.json b/src/strings/zh-cn.json index 41eb27c9b..28a52b1d6 100644 --- a/src/strings/zh-cn.json +++ b/src/strings/zh-cn.json @@ -1407,7 +1407,6 @@ "Shows": "节目", "SkipEpisodesAlreadyInMyLibraryHelp": "将使用季和剧集编号对剧集进行比较。", "Smaller": "更小", - "StatsForNerds": "统计数据", "SubtitleSettings": "字幕设置", "SubtitleSettingsIntro": "要配置默认字幕外观和语言设置,请停止视频播放,然后单击应用程序右上角的用户图标。", "TagsValue": "标签:{0}", diff --git a/src/strings/zh-tw.json b/src/strings/zh-tw.json index 4c7645226..3204e0dcc 100644 --- a/src/strings/zh-tw.json +++ b/src/strings/zh-tw.json @@ -413,7 +413,6 @@ "ChannelNumber": "頻道號碼", "Channels": "頻道", "CinemaModeConfigurationHelp": "劇影院模式直接為您的客廳帶來劇場級體驗,同時還可以播放預告片和自定開場白。", - "CinemaModeConfigurationHelp2": "Jellyfin 應用程式將有一個用於啟動或關閉劇院模式的設定。 電視的應用程式預設開啟劇院模式。", "CinemaModeFeatureDescription": "劇院模式用預告片和自定開場白帶給您最真實的劇院體驗。", "CloudSyncFeatureDescription": "將您的媒體備份到雲端當作簡單的備份,收藏和轉檔。", "Collections": "合輯", @@ -501,9 +500,6 @@ "ErrorAddingListingsToSchedulesDirect": "在將電視節目時間表新增到您的 Schedules Direct 帳號時出現錯誤。每個 Schedules Direct 帳號只允許有限的時間表。您在繼續前可能需要登入 Schedules Direct 網站並刪除帳號中的其它列表。", "ErrorAddingGuestAccount1": "新增Jellyfin Connect時發生錯誤。你的賓客有建立Jellyfin帳號嗎?他們可以在 {0} 創建帳號。", "ErrorAddingGuestAccount2": "若你還是遇到問題,請發送email至 {0} 並附上你和他們的email帳號。", - "ErrorAddingJellyfinConnectAccount1": "新增Jellyfin Connect時發生錯誤。您有建立Jellyfin帳號嗎?您可以在 {0} 創建帳號。", - "ErrorAddingJellyfinConnectAccount2": "若你還是遇到問題,請用發生問題的email帳號發送email至 {0}。", - "ErrorAddingJellyfinConnectAccount3": "這個 Jellyfin 帳號已經被連接至一個本地帳號。一個 Jellyfin帳號 只能同時被連接到一個本機帳號。", "ErrorAddingMediaPathToVirtualFolder": "新增媒體路徑時發生錯誤,請確認路徑是否有效,且你的 Jellyfin 伺服器有對該位置的存取權。", "ErrorAddingTunerDevice": "新增調諧器設備時發生錯誤,請確認它是否可被存取後再試一次。", "ErrorAddingXmlTvFile": "存取 XMLTV 檔案時發生錯誤,請確認該檔案是否存在後再試一次。", @@ -514,8 +510,6 @@ "ErrorMessageStartHourGreaterThanEnd": "結束時間必須在開始時間後。", "ErrorMessageUsernameInUse": "用戶名已存在。請重新選個名稱再試。", "ErrorPleaseSelectLineup": "請選擇節目表,然後再試一次。如果沒有可用的節目表,請檢查您的使用者名稱、密碼和郵遞區號是否正確。", - "ErrorReachingJellyfinConnect": "連接 Jellyfin Connect 伺服器時發生錯誤。請確認你的網絡狀態是否穩定後再試一次。", - "ErrorRemovingJellyfinConnectAccount": "移除 Jellyfin Connect 帳號時發生錯誤。請確認你的網絡狀態是否穩定後再試一次。", "ErrorSavingTvProvider": "儲存電視供應商時發生錯誤,請確認是否可存取並重試。", "EveryNDays": "每 {0} 天", "ExitFullscreen": "結束全螢幕", @@ -535,7 +529,6 @@ "FolderTypeUnset": "混合內容", "Folders": "資料夾", "FormatValue": "格式:{0}", - "FreeAppsFeatureDescription": "享受免費的Jellyfin應用程式。", "Fullscreen": "全螢幕", "General": "一般", "Genre": "類型", @@ -615,7 +608,6 @@ "HeaderContainerProfileHelp": "影片容器的設定檔標明了設備播放特定媒體格式時的限制。如果在限制之內則媒體將被轉碼,否則媒體格式將被設定為直接播放。", "HeaderContinueListening": "繼續聆聽", "HeaderContinueWatching": "繼續觀賞", - "HeaderConvertYourRecordings": "為你的錄制轉檔", "HeaderCredits": "製作人員名單", "HeaderDashboardUserPassword": "用戶的密碼被管理在每個用戶的私人配置設置中。", "HeaderDate": "日期", @@ -679,14 +671,7 @@ "HeaderImagePrimary": "主要", "HeaderImages": "圖片", "HeaderInstall": "安裝", - "HeaderInvitationSent": "邀請已傳送", - "HeaderInvitations": "邀請", - "HeaderInviteUser": "邀請使用者", - "HeaderInviteUserHelp": "通過 Jellyfin Connect 與朋友分享你的媒體比以往任何時候都更為簡單。", - "HeaderInviteWithJellyfinConnect": "通過 Jellyfin Connect 邀請", "HeaderItems": "項目", - "HeaderJellyfinAccountAdded": "Jellyfin 帳戶已添加", - "HeaderJellyfinAccountRemoved": "已移除 Jellyfin 帳戶", "HeaderKeepRecording": "繼續錄製", "HeaderKeepSeries": "保存系列", "HeaderKodiMetadataHelp": "要啟用或禁用 NFO 元數據, 請在 Jellyfin “建立媒體庫”頁面中編輯該媒體庫, 然後找到“元數據儲存”部分。", @@ -734,10 +719,6 @@ "HeaderNextVideoPlayingInValue": "下一部影片在 {0} 後播放", "HeaderNotifications": "通知", "HeaderNumberOfPlayers": "播放器", - "HeaderOffline": "離線", - "HeaderOfflineDownloads": "離線媒體", - "HeaderOfflineDownloadsDescription": "下載媒體以離線使用。", - "HeaderOfflineSync": "離線同步", "HeaderOnNow": "現正播放", "HeaderOptions": "選項", "HeaderOtherDisplaySettings": "顯示設定", From 4beac3f7f413e68c78c446c22db2f666e1e33d70 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 17 May 2020 23:19:21 +0900 Subject: [PATCH 0077/2481] update more controller locations --- .../dlna/{dlnaprofile.js => profile.js} | 0 .../dlna/{dlnaprofiles.js => profiles.js} | 0 .../dlna/{dlnasettings.js => settings.js} | 0 .../{medialibrarypage.js => mediaLibrary.js} | 0 ...etadataimagespage.js => metadataImages.js} | 0 .../{playbackconfiguration.js => playback.js} | 0 .../{streamingsettings.js => streaming.js} | 0 .../{ => dashboard/users}/useredit.js | 0 .../users}/userlibraryaccess.js | 0 .../{ => dashboard/users}/usernew.js | 0 .../users}/userparentalcontrol.js | 0 .../{ => dashboard/users}/userpasswordpage.js | 0 .../{ => dashboard/users}/userprofilespage.js | 0 .../{itemdetailpage.js => itemDetails.js} | 0 src/controllers/user/profile.js | 2 +- src/scripts/routes.js | 30 +++++++++---------- 16 files changed, 16 insertions(+), 16 deletions(-) rename src/controllers/dashboard/dlna/{dlnaprofile.js => profile.js} (100%) rename src/controllers/dashboard/dlna/{dlnaprofiles.js => profiles.js} (100%) rename src/controllers/dashboard/dlna/{dlnasettings.js => settings.js} (100%) rename src/controllers/dashboard/{medialibrarypage.js => mediaLibrary.js} (100%) rename src/controllers/dashboard/{metadataimagespage.js => metadataImages.js} (100%) rename src/controllers/dashboard/{playbackconfiguration.js => playback.js} (100%) rename src/controllers/dashboard/{streamingsettings.js => streaming.js} (100%) rename src/controllers/{ => dashboard/users}/useredit.js (100%) rename src/controllers/{ => dashboard/users}/userlibraryaccess.js (100%) rename src/controllers/{ => dashboard/users}/usernew.js (100%) rename src/controllers/{ => dashboard/users}/userparentalcontrol.js (100%) rename src/controllers/{ => dashboard/users}/userpasswordpage.js (100%) rename src/controllers/{ => dashboard/users}/userprofilespage.js (100%) rename src/controllers/{itemdetailpage.js => itemDetails.js} (100%) diff --git a/src/controllers/dashboard/dlna/dlnaprofile.js b/src/controllers/dashboard/dlna/profile.js similarity index 100% rename from src/controllers/dashboard/dlna/dlnaprofile.js rename to src/controllers/dashboard/dlna/profile.js diff --git a/src/controllers/dashboard/dlna/dlnaprofiles.js b/src/controllers/dashboard/dlna/profiles.js similarity index 100% rename from src/controllers/dashboard/dlna/dlnaprofiles.js rename to src/controllers/dashboard/dlna/profiles.js diff --git a/src/controllers/dashboard/dlna/dlnasettings.js b/src/controllers/dashboard/dlna/settings.js similarity index 100% rename from src/controllers/dashboard/dlna/dlnasettings.js rename to src/controllers/dashboard/dlna/settings.js diff --git a/src/controllers/dashboard/medialibrarypage.js b/src/controllers/dashboard/mediaLibrary.js similarity index 100% rename from src/controllers/dashboard/medialibrarypage.js rename to src/controllers/dashboard/mediaLibrary.js diff --git a/src/controllers/dashboard/metadataimagespage.js b/src/controllers/dashboard/metadataImages.js similarity index 100% rename from src/controllers/dashboard/metadataimagespage.js rename to src/controllers/dashboard/metadataImages.js diff --git a/src/controllers/dashboard/playbackconfiguration.js b/src/controllers/dashboard/playback.js similarity index 100% rename from src/controllers/dashboard/playbackconfiguration.js rename to src/controllers/dashboard/playback.js diff --git a/src/controllers/dashboard/streamingsettings.js b/src/controllers/dashboard/streaming.js similarity index 100% rename from src/controllers/dashboard/streamingsettings.js rename to src/controllers/dashboard/streaming.js diff --git a/src/controllers/useredit.js b/src/controllers/dashboard/users/useredit.js similarity index 100% rename from src/controllers/useredit.js rename to src/controllers/dashboard/users/useredit.js diff --git a/src/controllers/userlibraryaccess.js b/src/controllers/dashboard/users/userlibraryaccess.js similarity index 100% rename from src/controllers/userlibraryaccess.js rename to src/controllers/dashboard/users/userlibraryaccess.js diff --git a/src/controllers/usernew.js b/src/controllers/dashboard/users/usernew.js similarity index 100% rename from src/controllers/usernew.js rename to src/controllers/dashboard/users/usernew.js diff --git a/src/controllers/userparentalcontrol.js b/src/controllers/dashboard/users/userparentalcontrol.js similarity index 100% rename from src/controllers/userparentalcontrol.js rename to src/controllers/dashboard/users/userparentalcontrol.js diff --git a/src/controllers/userpasswordpage.js b/src/controllers/dashboard/users/userpasswordpage.js similarity index 100% rename from src/controllers/userpasswordpage.js rename to src/controllers/dashboard/users/userpasswordpage.js diff --git a/src/controllers/userprofilespage.js b/src/controllers/dashboard/users/userprofilespage.js similarity index 100% rename from src/controllers/userprofilespage.js rename to src/controllers/dashboard/users/userprofilespage.js diff --git a/src/controllers/itemdetailpage.js b/src/controllers/itemDetails.js similarity index 100% rename from src/controllers/itemdetailpage.js rename to src/controllers/itemDetails.js diff --git a/src/controllers/user/profile.js b/src/controllers/user/profile.js index 0f4f2c7bc..fd7d1e32c 100644 --- a/src/controllers/user/profile.js +++ b/src/controllers/user/profile.js @@ -1,4 +1,4 @@ -define(['controllers/userpasswordpage', 'loading', 'libraryMenu', 'apphost', 'globalize', 'emby-button'], function (UserPasswordPage, loading, libraryMenu, appHost, globalize) { +define(['controllers/dashboard/users/userpasswordpage', 'loading', 'libraryMenu', 'apphost', 'globalize', 'emby-button'], function (UserPasswordPage, loading, libraryMenu, appHost, globalize) { 'use strict'; function reloadUser(page) { diff --git a/src/scripts/routes.js b/src/scripts/routes.js index adb859d5c..6f13711b6 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -131,13 +131,13 @@ define([ path: '/dlnaprofile.html', autoFocus: false, roles: 'admin', - controller: 'dashboard/dlna/dlnaprofile' + controller: 'dashboard/dlna/profile' }); defineRoute({ path: '/dlnaprofiles.html', autoFocus: false, roles: 'admin', - controller: 'dashboard/dlna/dlnaprofiles' + controller: 'dashboard/dlna/profiles' }); defineRoute({ path: '/addplugin.html', @@ -149,7 +149,7 @@ define([ path: '/library.html', autoFocus: false, roles: 'admin', - controller: 'dashboard/medialibrarypage' + controller: 'dashboard/mediaLibrary' }); defineRoute({ path: '/librarydisplay.html', @@ -161,7 +161,7 @@ define([ path: '/dlnasettings.html', autoFocus: false, roles: 'admin', - controller: 'dashboard/dlna/dlnasettings' + controller: 'dashboard/dlna/settings' }); defineRoute({ path: '/edititemmetadata.html', @@ -183,7 +183,7 @@ define([ path: '/metadataimages.html', autoFocus: false, roles: 'admin', - controller: 'dashboard/metadataimagespage' + controller: 'dashboard/metadataImages' }); defineRoute({ path: '/metadatanfo.html', @@ -207,7 +207,7 @@ define([ path: '/playbackconfiguration.html', autoFocus: false, roles: 'admin', - controller: 'dashboard/playbackconfiguration' + controller: 'dashboard/playback' }); defineRoute({ path: '/availableplugins.html', @@ -235,7 +235,7 @@ define([ }); defineRoute({ path: '/itemdetails.html', - controller: 'itemdetailpage', + controller: 'itemDetails', autoFocus: false, transition: 'fade' }); @@ -314,7 +314,7 @@ define([ path: '/streamingsettings.html', autoFocus: false, roles: 'admin', - controller: 'dashboard/streamingsettings' + controller: 'dashboard/streaming' }); defineRoute({ path: '/tv.html', @@ -327,36 +327,36 @@ define([ path: '/useredit.html', autoFocus: false, roles: 'admin', - controller: 'useredit' + controller: 'dashboard/users/useredit' }); defineRoute({ path: '/userlibraryaccess.html', autoFocus: false, roles: 'admin', - controller: 'userlibraryaccess' + controller: 'dashboard/users/userlibraryaccess' }); defineRoute({ path: '/usernew.html', autoFocus: false, roles: 'admin', - controller: 'usernew' + controller: 'dashboard/users/usernew' }); defineRoute({ path: '/userparentalcontrol.html', autoFocus: false, roles: 'admin', - controller: 'userparentalcontrol' + controller: 'dashboard/users/userparentalcontrol' }); defineRoute({ path: '/userpassword.html', autoFocus: false, - controller: 'userpasswordpage' + controller: 'dashboard/users/userpasswordpage' }); defineRoute({ path: '/userprofiles.html', autoFocus: false, roles: 'admin', - controller: 'userprofilespage' + controller: 'dashboard/users/userprofilespage' }); defineRoute({ @@ -375,7 +375,7 @@ define([ path: '/wizardlibrary.html', autoFocus: false, anonymous: true, - controller: 'dashboard/medialibrarypage' + controller: 'dashboard/mediaLibrary' }); defineRoute({ path: '/wizardsettings.html', From 146e592a6d4510b31eeb834f69572c32e83bf3ed Mon Sep 17 00:00:00 2001 From: Adavier <30839200+adavier@users.noreply.github.com> Date: Sun, 17 May 2020 15:17:14 +0100 Subject: [PATCH 0078/2481] qualityoptions.js now shows correct options for aspect ratios below 16:9 (1.77) (such as 4:3 (1.33)) qualityoptions.js assumes that all videos are wider than 16:9 (1.77). So a video with resolution 1440x1080 (4:3 1080p) only exposes options of 720p as the width is not considered for 1080p. This fixes that by checking if the aspect ratio is less than 1.77, and if so padding the width to 16:9. --- src/components/playback/playersettingsmenu.js | 4 ++++ src/components/qualityOptions.js | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/components/playback/playersettingsmenu.js b/src/components/playback/playersettingsmenu.js index b89631f63..dd67b667e 100644 --- a/src/components/playback/playersettingsmenu.js +++ b/src/components/playback/playersettingsmenu.js @@ -7,11 +7,13 @@ define(['connectionManager', 'actionsheet', 'datetime', 'playbackManager', 'glob return stream.Type === 'Video'; })[0]; var videoWidth = videoStream ? videoStream.Width : null; + var videoHeight = videoStream ? videoStream.Height : null; var options = qualityoptions.getVideoQualityOptions({ currentMaxBitrate: playbackManager.getMaxStreamingBitrate(player), isAutomaticBitrateEnabled: playbackManager.enableAutomaticBitrateDetection(player), videoWidth: videoWidth, + videoHeight: videoHeight, enableAuto: true }); @@ -91,11 +93,13 @@ define(['connectionManager', 'actionsheet', 'datetime', 'playbackManager', 'glob })[0]; var videoWidth = videoStream ? videoStream.Width : null; + var videoHeight = videoStream ? videoStream.Height : null; var options = qualityoptions.getVideoQualityOptions({ currentMaxBitrate: playbackManager.getMaxStreamingBitrate(player), isAutomaticBitrateEnabled: playbackManager.enableAutomaticBitrateDetection(player), videoWidth: videoWidth, + videoHeight: videoHeight, enableAuto: true }); diff --git a/src/components/qualityOptions.js b/src/components/qualityOptions.js index 5ad69cb79..03bb9dacf 100644 --- a/src/components/qualityOptions.js +++ b/src/components/qualityOptions.js @@ -5,6 +5,13 @@ define(['globalize'], function (globalize) { var maxStreamingBitrate = options.currentMaxBitrate; var videoWidth = options.videoWidth; + var videoHeight = options.videoHeight; + + //If the aspect ratio is less than 16/9 (1.77), set the height as if it were pillerboxed. + // 4:3 1440x1080 -> 1920x1080 + if (videoWidth/videoHeight < 16/9) { + videoWidth=videoHeight*(16/9); + } var maxAllowedWidth = videoWidth || 4096; //var maxAllowedHeight = videoHeight || 2304; From 5785d2dd3f40942d8ac99f538c07810c6635095c Mon Sep 17 00:00:00 2001 From: dkanada Date: Mon, 18 May 2020 00:03:59 +0900 Subject: [PATCH 0079/2481] remove a few more unused strings --- src/strings/cs.json | 7 ------- src/strings/da.json | 5 ----- src/strings/de.json | 1 - src/strings/es-ar.json | 9 --------- src/strings/es-mx.json | 1 - src/strings/es.json | 3 --- src/strings/fi.json | 2 -- src/strings/gsw.json | 2 -- src/strings/hu.json | 1 - src/strings/kk.json | 2 -- src/strings/sl-si.json | 2 -- src/strings/zh-tw.json | 5 ----- 12 files changed, 40 deletions(-) diff --git a/src/strings/cs.json b/src/strings/cs.json index 50f355fb4..6b270e4d2 100644 --- a/src/strings/cs.json +++ b/src/strings/cs.json @@ -543,8 +543,6 @@ "LabelFormat": "Formát:", "LabelFree": "Zdarma", "LabelFriendlyName": "Přívětivý název:", - "LabelFriendlyServerName": "Název serveru:", - "LabelFriendlyServerNameHelp": "Toto jméno bude použito jako identifikace serveru, ponecháte-li prázdné bude použit název počítače.", "LabelFromHelp": "Například: {0} (na serveru)", "LabelGroupMoviesIntoCollections": "Seskupit filmy do kolekcí", "LabelGroupMoviesIntoCollectionsHelp": "Při zobrazení seznamů filmu, budou filmy patřící do kolekce, zobrazeny jako jedna položka.", @@ -819,8 +817,6 @@ "MessagePasswordResetForUsers": "Obnovení hesla bylo provedeno následujícími uživateli. Nyní se mohou přihlásit pomocí kódů PIN, které byly použity k provedení resetu.", "MessagePlayAccessRestricted": "Přehrávání tohoto obsahu je aktuálně omezeno. Další informace získáte od správce serveru.", "MessagePleaseEnsureInternetMetadata": "Prosím zkontrolujte, zda máte povoleno stahování metadat z internetu.", - "MessagePleaseRestart": "Pro dokončení aktualizací, prosím, restartujte.", - "MessagePleaseRestartServerToFinishUpdating": "Restartujte, prosím, server pro aplikaci aktualizací.", "MessagePluginConfigurationRequiresLocalAccess": "Pro konfiguraci zásuvného modulu se přihlaste přímo na lokální server.", "MessagePluginInstallDisclaimer": "Zasuvné moduly vytvořené členy Jellyfin komunity jsou skvělý způsob, jak zvýšit svůj Jellyfin prožitek pomocí doplňkových funkcí :-) Před instalací, se prosím seznamte se všemi dopady, které mohou mít na Jellyfin Server, jako je například delší prohledávání knihovny, další zpracování na pozadí, a snížení stability systému.", "MessageReenableUser": "Viz níže pro znovuzapnutí", @@ -1215,7 +1211,6 @@ "Yes": "Ano", "Yesterday": "Včera", "Absolute": "Absolutní", - "AddUserByManually": "Přidat místního uživatele ručním zadáním informací.", "AirDate": "Datum vysílání", "Aired": "Vysíláno", "Alerts": "Upozornění", @@ -1230,7 +1225,6 @@ "AllowRemoteAccessHelp": "Pokud není zapnuto, všechna vzdálená připojení budou blokována.", "AllowedRemoteAddressesHelp": "Seznam IP adres nebo síťových masek oddělených čárkou pro sítě, ze kterých se lze vzdáleně připojit. Pokud necháte prázdné, všechny adresy budou povoleny.", "AnamorphicVideoNotSupported": "Anamorfní video není podporováno", - "AndroidUnlockRestoreHelp": "Chcete-li obnovit předchozí nákup, ujistěte se, že jste přihlášeni do zařízení se stejným účtem Google (nebo Amazon), na kterém byl nákup původně proveden. Ujistěte se, že je povolen přístup k úložišti aplikací a není omezen žádnou rodičovskou kontrolou a ujistěte se, že máte aktivní připojení k internetu. Toto budete muset provést pouze jednou, abyste tento předchozí nákup obnovili.", "AnyLanguage": "Jakýkoli jazyk", "Ascending": "Vzestupně", "AudioBitDepthNotSupported": "Bitová hloubka zvuku není podporována", @@ -1331,7 +1325,6 @@ "HeaderVideoType": "Formát videa", "Horizontal": "Vodorovně", "HttpsRequiresCert": "Chcete-li povolit zabezpečená připojení, budete muset zadat důvěryhodný certifikát SSL, například Let's Encrypt. Zadejte prosím certifikát nebo zakažte zabezpečená připojení.", - "Invitations": "Pozvánky", "KeepDownload": "Zachovat stahování", "LabelAlbum": "Album:", "LabelAllowedRemoteAddresses": "Filtr vzdálené IP adresy:", diff --git a/src/strings/da.json b/src/strings/da.json index fa61bdeb7..45c0ddb33 100644 --- a/src/strings/da.json +++ b/src/strings/da.json @@ -1094,7 +1094,6 @@ "HeaderLiveTV": "Live-TV", "Shows": "Serier", "Songs": "Sange", - "AndroidUnlockRestoreHelp": "For at gendanne dit tidligere køb skal du sørge for, at du er logget ind på enheden med den samme Google- eller Amazon-konto, som oprindeligt gjorde købet. Sørg for, at app store er aktiveret og ikke begrænset af forældrekontrol, og sørg for, at du har en aktiv internetforbindelse. Du skal kun gøre dette én gang for at gendanne dit tidligere køb.", "AnyLanguage": "Hvilken som helst sprog", "Art": "Kunst", "Ascending": "Stigende", @@ -1316,7 +1315,6 @@ "LabelXDlnaCap": "X-DLNA begrænsning:", "LabelXDlnaDoc": "X-DLNA dokumentation:", "LabelYear": "År:", - "LabelffmpegVersion": "FFmpeg version:", "Large": "Stor", "LearnHowYouCanContribute": "Lær hvordan du kan bidrage.", "LeaveBlankToNotSetAPassword": "Du kan lade dette felt være tomt hvis du ikke ønsker adgangskode.", @@ -1361,10 +1359,7 @@ "OnlyForcedSubtitles": "Kun tvungne undertekster", "OnlyForcedSubtitlesHelp": "Kun undertekster markeret som tvungne vil blive indlæst.", "OnlyImageFormats": "Kun billedformater (VOBSUB, PGS, SUB)", - "Option2Player": "2+", "Option3D": "3D", - "Option3Player": "3+", - "Option4Player": "4+", "OptionAlbum": "Album", "OptionArtist": "Kunstner", "OptionAuto": "Automatisk", diff --git a/src/strings/de.json b/src/strings/de.json index cb6c86d16..0bb58dc51 100644 --- a/src/strings/de.json +++ b/src/strings/de.json @@ -9,7 +9,6 @@ "AddToPlayQueue": "Zur Warteschlange hinzufügen", "AddToPlaylist": "Zur Wiedergabeliste hinzufügen", "AddUser": "Benutzer anlegen", - "AddUserByManually": "Lege einen lokalen User durch manuelle Eingabe der User-Informationen an.", "AddedOnValue": "{0} hinzugefügt", "AdditionalNotificationServices": "Durchsuche den Pluginkatalog, um weitere Benachrichtigungsdienste zu installieren.", "AirDate": "Erstausstrahlung", diff --git a/src/strings/es-ar.json b/src/strings/es-ar.json index bfc1a574a..6e5c33f31 100644 --- a/src/strings/es-ar.json +++ b/src/strings/es-ar.json @@ -83,7 +83,6 @@ "AllowHWTranscodingHelp": "Permita que el sintonizador transcodifique transmisiones sobre la marcha. Esto puede ayudar a reducir la transcodificación requerida por el servidor.", "AllowedRemoteAddressesHelp": "Lista separada por comas de direcciones IP o IP/máscara de red para redes a las que se les permitirá conectarse de forma remota. Si se deja vacía, todas las direcciones remotas serán permitidas.", "AlwaysPlaySubtitlesHelp": "Los subtítulos que concuerden con la preferencia de idioma se cargarán independientemente del idioma del audio.", - "AndroidUnlockRestoreHelp": "Para recuperar tu compra anterior, por favor asegurate que iniciaste sesión en el dispositivo con la misma cuenta de Google (o Amazon) que hizo la compra originalmente. Asegurate de que la tienda de aplicaciones esté habilitada y no posea control parental alguno, y que tiene una conexión a Internet activa. Solo tendrás que hacer esto una sola vez para recuperar tu compra anterior.", "AroundTime": "Alrededor de {0}", "Art": "Arte", "AsManyAsPossible": "Tantos como sea posible", @@ -124,7 +123,6 @@ "ButtonConfigurePassword": "Configurar contraseña", "ButtonConfigurePinCode": "Configurar clave PIN", "ButtonConnect": "Conectar", - "ButtonConvertMedia": "Convertir medios", "ButtonCreate": "Crear", "ButtonDelete": "Eliminar", "ButtonDeleteImage": "Eliminar imagen", @@ -165,7 +163,6 @@ "ButtonParentalControl": "Control parental", "ButtonPause": "Pausar", "ButtonPlay": "Reproducir", - "ButtonPlayOneMinute": "Reproducir un minuto", "ButtonPlayTrailer": "Tráiler", "ButtonPlaylist": "Lista de reproducción", "ButtonPreferences": "Preferencias", @@ -174,10 +171,8 @@ "ButtonPrivacyPolicy": "Política de privacidad", "ButtonProfile": "Perfil", "ButtonProfileHelp": "Definir imagen de perfil y contraseña", - "ButtonPurchase": "Comprar", "ButtonQuality": "Calidad", "ButtonRecord": "Grabar", - "ButtonReenable": "Reactivar", "ButtonRefresh": "Actualizar", "ButtonRefreshGuideData": "Actualizar datos de la guía", "ButtonReject": "Rechazar", @@ -193,7 +188,6 @@ "ButtonResetTuner": "Restablecer sintonizador", "ButtonRestart": "Reiniciar", "ButtonRestartNow": "Reiniciar ahora", - "ButtonRestorePreviousPurchase": "Recuperar compra", "ButtonResume": "Resumir", "ButtonRevoke": "Revocar", "ButtonSave": "Guardar", @@ -214,7 +208,6 @@ "ButtonShuffle": "Aleatorio", "ButtonShutdown": "Apagar", "ButtonSignIn": "Iniciar sesión", - "ButtonSignUp": "Registrarse", "ButtonSkip": "Saltear", "ButtonSort": "Ordenar", "ButtonStart": "Iniciar", @@ -226,8 +219,6 @@ "ButtonTrailer": "Tráiler", "ButtonTryAgain": "Intentar de nuevo", "ButtonUninstall": "Desinstalar", - "ButtonUnlockPrice": "Desbloquear {0}", - "ButtonUnlockWithPurchase": "Desbloquear con compra", "ButtonUnmute": "Desilenciar", "ButtonUp": "Arriba", "ButtonUpdateNow": "Actualizar ahora", diff --git a/src/strings/es-mx.json b/src/strings/es-mx.json index fcf564381..39ac28451 100644 --- a/src/strings/es-mx.json +++ b/src/strings/es-mx.json @@ -30,7 +30,6 @@ "AlwaysPlaySubtitles": "Siempre mostrar subtítulos", "AlwaysPlaySubtitlesHelp": "Los subtítulos que coincidan con el lenguaje preferido serán cargados independientemente del lenguaje del audio.", "AnamorphicVideoNotSupported": "Video anamorfico no soportado", - "AndroidUnlockRestoreHelp": "Para restaurar su compra previa, por favor asegúrese de que se encuentra registrado en el dispositivo con la misma cuenta de Google (o Amazon) con que hizo la compra. Asegúrese que la tienda de aplicaciones esta habilitada, no esta restringida por cualquier control parental y que tiene una conexión de internet activa. Esto se tiene que hacer solo una vez para restaurar su compra previa.", "AnyLanguage": "Cualquier idioma", "Anytime": "En cualquier momento", "AroundTime": "Alrededor de {0}", diff --git a/src/strings/es.json b/src/strings/es.json index 1ce442579..e03aa0508 100644 --- a/src/strings/es.json +++ b/src/strings/es.json @@ -1328,10 +1328,7 @@ "No": "No", "Normal": "Normal", "Off": "Apagado", - "Option2Player": "2+", "Option3D": "3D", - "Option3Player": "3+", - "Option4Player": "4+", "OptionActor": "Actor", "OptionAuto": "Automático", "OptionAutomatic": "Automático", diff --git a/src/strings/fi.json b/src/strings/fi.json index a5e5bd095..c3ca9d81d 100644 --- a/src/strings/fi.json +++ b/src/strings/fi.json @@ -70,7 +70,6 @@ "AddToCollection": "Lisää kokoelmaan", "AddToPlayQueue": "Lisää toistojonoon", "AddToPlaylist": "Lisää toistolistalle", - "AddUserByManually": "Lisää paikallinen käyttäjä lisäämällä käyttäjän tiedot manuaalisesti.", "AddedOnValue": "Lisätty {0}", "AdditionalNotificationServices": "Selaa lisäosakatalogia asentaaksesi lisää ilmoituspalveluita.", "AirDate": "Ensiesityspäivä", @@ -98,7 +97,6 @@ "AlwaysPlaySubtitles": "Näytä aina tekstitykset", "AlwaysPlaySubtitlesHelp": "Oletuskieliasetusta vastaava tekstitys otetaan käyttöön ääniraidan kielestä huolimatta.", "AnamorphicVideoNotSupported": "Anamorfinen video ei ole tuettu", - "AndroidUnlockRestoreHelp": "Palauttaaksesi aikaisemman ostoksesi, varmista, että olet kirjautuneena samalla Google (tai Amazon) tunnuksella, jolla teit alkuperäisen oston. Varmista, että sovelluskauppa on päällä eikä sitä ole rajoitettu vanhempien lukolla. Varmista myös, että sinulla on toimiva internet -yhteys. Sinun tarvitsee tehdä taämä vain kerran.", "AnyLanguage": "Mikä tahansa", "Anytime": "Milloin tahansa", "AroundTime": "Noin {0}", diff --git a/src/strings/gsw.json b/src/strings/gsw.json index 2c5210969..e39631385 100644 --- a/src/strings/gsw.json +++ b/src/strings/gsw.json @@ -148,11 +148,9 @@ "HeaderFavoriteSongs": "Lieblingslieder", "HeaderLiveTV": "Live-Fernseh", "HeaderRecordingGroups": "Ufnahmegruppe", - "LabelIpAddressValue": "IP-Adrässe: {0}", "LabelRunningTimeValue": "Loufziit: {0}", "MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde", "MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde", - "MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde", "Movies": "Film", "Photos": "Fotis", "Playlists": "Wedergabeliste", diff --git a/src/strings/hu.json b/src/strings/hu.json index 61d4dfbf0..c7e04b2ab 100644 --- a/src/strings/hu.json +++ b/src/strings/hu.json @@ -853,7 +853,6 @@ "LabelCustomCertificatePath": "Egyéni SSL tanúsítvány elérési útvonala:", "LabelCustomCss": "Egyedi CSS:", "LabelCustomCssHelp": "Egyedi CSS stílusok alkalmazása a webes felülethez.", - "LabelCustomizeOptionsPerMediaType": "A média típusának testreszabása:", "LabelDeathDate": "Halálának dátuma:", "LabelDefaultScreen": "Alapértelmezett képernyő:", "LabelDefaultUser": "Alapértelmezett felhasználó:", diff --git a/src/strings/kk.json b/src/strings/kk.json index 776caad50..f32fe9d9e 100644 --- a/src/strings/kk.json +++ b/src/strings/kk.json @@ -30,7 +30,6 @@ "AlwaysPlaySubtitles": "Árqashan oınatý", "AlwaysPlaySubtitlesHelp": "Til teńshelimine sáıkes kelgen sýbtıtrler dybys tiline qatyssyz júkteledi.", "AnamorphicVideoNotSupported": "Anamorftyq beıne úshin qoldaý kórsetilmeıdi", - "AndroidUnlockRestoreHelp": "Aldyńǵy satyp alýdy qalpyna keltirý úshin, bastapqyda satyp alý jasalǵan naq sol Google (nemese Amazon) tirkelgisimen qurylǵyǵa kirińiz. Qoldanba dúkeni qosylǵan jáne kezkelgen ata-ana shekteýsiz, jáne belsendi ınternet baılanysy bar ekenine kóz jetkizińiz. Aldyńǵy satyp alý qalpyna keltirý úshin muny tek qana bir ret isteý kerek.", "AnyLanguage": "Qaı-qaısy til", "Anytime": "Árkezde", "AroundTime": "{0} aınalasynda", @@ -948,7 +947,6 @@ "MessagePluginConfigurationRequiresLocalAccess": "Osy plagındi teńsheý úshin jergilikti serverińizge tikeleı kirińiz.", "MessagePluginInstallDisclaimer": "Jellyfin qaýymdastyǵy múshelerimen qurylǵan plagınder Jellyfin tájirıbeńizdi qosymsha múmkindiktermen jáne jeńildiktermen jaqsartý úshin jaqsy tásili bolyp tabylady. Ornatpas buryn, olar Jellyfin serverińizge tasyǵyshhanany uzaq skanerleý, qosymsha óńdik óńdetý jáne júıeniń turaqtylyǵyn tómendetý sıaqty áserler etýge múmkin bolýyna habardar bolyńyz.", "MessageReenableUser": "Qaıta qosý úshin tómende qarańyz", - "MessageServerConfigurationUpdated": "Server konfıgýrasıasy jańartyldy", "MessageSettingsSaved": "Parametrler saqtaldy.", "MessageTheFollowingLocationWillBeRemovedFromLibrary": "Tasyǵyshhanańyzdan kelesi tasyǵysh ornalasýlary alastalady:", "MessageUnableToConnectToServer": "Tańdalǵan serverge qosylýymyz dál qazir múmkin emes. Bul iske qosylǵanyna kóz jetkizińiz jáne áreketti keıin qaıtalańyz.", diff --git a/src/strings/sl-si.json b/src/strings/sl-si.json index 109004cce..dde5007a5 100644 --- a/src/strings/sl-si.json +++ b/src/strings/sl-si.json @@ -100,10 +100,8 @@ "HeaderLiveTV": "TV v živo", "HeaderNextUp": "Sledi", "HeaderRecordingGroups": "Zbirke posnetkov", - "LabelIpAddressValue": "IP naslov: {0}", "LabelRunningTimeValue": "Čas trajanja: {0}", "MessageApplicationUpdated": "Jellyfin Server je bil posodobljen", - "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene", "Movies": "Filmi", "AddItemToCollectionHelp": "Dodajte elemente v zbirke tako, da jih poiščete in jih z desnim klikom ali dotikom menija dodate v zbirko.", "AllowedRemoteAddressesHelp": "Z vejico ločen seznam IP naslovov ali IP/maska omrežij, ki jim je dovoljen oddaljeni dostop. Če pustite prazno, bodo dovoljeni vsi oddaljeni naslovi.", diff --git a/src/strings/zh-tw.json b/src/strings/zh-tw.json index 3204e0dcc..ae086d08c 100644 --- a/src/strings/zh-tw.json +++ b/src/strings/zh-tw.json @@ -805,14 +805,9 @@ "HeaderTryPlayback": "嘗試播放", "HeaderTunerDevices": "調諧器裝置", "BobAndWeaveWithHelp": "Bob and weave (高品質,轉檔慢)", - "ButtonPurchase": "購買", - "ButtonRestorePreviousPurchase": "恢復購買", - "ButtonUnlockWithPurchase": "購買已解鎖", - "LabelIpAddressValue": "IP 位置: {0}", "LabelRunningTimeValue": "運行時間: {0}", "MessageApplicationUpdated": "Jellyfin Server 已經更新", "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已經更新", - "MessageServerConfigurationUpdated": "伺服器設定已經更新", "Movies": "電影", "Photos": "相片", "Playlists": "播放清單", From f897f7484c914300942f2e4fdcca36b8389598cb Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sun, 17 May 2020 18:17:47 +0200 Subject: [PATCH 0080/2481] Force spacing around operators --- .eslintrc.js | 3 ++- src/components/indicators/indicators.js | 2 +- src/components/remotecontrol/remotecontrol.js | 2 +- src/components/scrollManager.js | 6 +++--- src/controllers/itemdetailpage.js | 4 ++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4a3fec944..8b9a7dea1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,7 +42,8 @@ module.exports = { 'one-var': ["error", "never"], 'quotes': ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": false }], 'semi': ["error"], - 'space-before-blocks': ["error"] + 'space-before-blocks': ["error"], + "space-infix-ops": "error" }, overrides: [ { diff --git a/src/components/indicators/indicators.js b/src/components/indicators/indicators.js index 7bc8ddd4e..604f480f1 100644 --- a/src/components/indicators/indicators.js +++ b/src/components/indicators/indicators.js @@ -148,7 +148,7 @@ export function getTypeIndicator(item) { }; const icon = iconT[item.Type]; - return icon ? '
    ' : ''; + return icon ? '
    ' : ''; } export function getMissingIndicator(item) { diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index a4f25a774..089915a83 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -140,7 +140,7 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL } if (item.SeriesName != null) { var seriesName = item.SeriesName; - if (item.SeriesId !=null) { + if (item.SeriesId != null) { context.querySelector('.nowPlayingSerie').innerHTML = '${seriesName}`; } else { context.querySelector('.nowPlayingSerie').innerHTML = seriesName; diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index f7bd41ff8..3b01cf1ad 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -92,7 +92,7 @@ import layoutManager from 'layoutManager'; * @return {number} Eased value in range [0, 1]. */ function ease(t) { - return t*(2 - t); // easeOutQuad === ease-out + return t * (2 - t); // easeOutQuad === ease-out } /** @@ -402,8 +402,8 @@ import layoutManager from 'layoutManager'; k = ease(k); - const x = ox + dx*k; - const y = oy + dy*k; + const x = ox + dx * k; + const y = oy + dy * k; builtinScroll(xScroller, x, yScroller, y, false); diff --git a/src/controllers/itemdetailpage.js b/src/controllers/itemdetailpage.js index 45342208e..d9eab0ac4 100644 --- a/src/controllers/itemdetailpage.js +++ b/src/controllers/itemdetailpage.js @@ -803,9 +803,9 @@ define(['loading', 'appRouter', 'layoutManager', 'connectionManager', 'userSetti } if (editable && url === undefined) { - html += ""; + html += ""; } else if (!editable && url === undefined) { - html += " +
    + +
    ${EnableBlurhashHelp}
    +
    +
    -
    - -
    ${OptionDownloadImagesInAdvanceHelp}
    -
    -
    -
    +
    ${LabelCustomCssHelp}
    From a032f6a35da5ad1430fbedaf246992e864d0e454 Mon Sep 17 00:00:00 2001 From: Influence365 Date: Sat, 30 May 2020 09:44:23 +0100 Subject: [PATCH 0285/2481] Update emby-textarea.js --- src/elements/emby-textarea/emby-textarea.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/elements/emby-textarea/emby-textarea.js b/src/elements/emby-textarea/emby-textarea.js index e0ce77aa5..8d5a27265 100644 --- a/src/elements/emby-textarea/emby-textarea.js +++ b/src/elements/emby-textarea/emby-textarea.js @@ -55,6 +55,7 @@ define(['layoutManager', 'browser', 'css!./emby-textarea', 'registerElement', 'e newHeight = textarea.scrollHeight/* - offset*/; hasGrown = true; } + $('.customCSS1').css("height",newHeight + 'px'); textarea.style.height = newHeight + 'px'; } From 6ce8c77d8ec3cc547589fe22685d68c50cab5853 Mon Sep 17 00:00:00 2001 From: Influence365 Date: Sat, 30 May 2020 09:51:49 +0100 Subject: [PATCH 0286/2481] Update emby-textarea.js --- src/elements/emby-textarea/emby-textarea.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/emby-textarea/emby-textarea.js b/src/elements/emby-textarea/emby-textarea.js index 8d5a27265..8d01ff950 100644 --- a/src/elements/emby-textarea/emby-textarea.js +++ b/src/elements/emby-textarea/emby-textarea.js @@ -55,7 +55,7 @@ define(['layoutManager', 'browser', 'css!./emby-textarea', 'registerElement', 'e newHeight = textarea.scrollHeight/* - offset*/; hasGrown = true; } - $('.customCSS1').css("height",newHeight + 'px'); + $('.customCSS').css("height",newHeight + 'px'); textarea.style.height = newHeight + 'px'; } From f0d337ee37c8542a7cc297af4232a7e4ccd9af7d Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 30 May 2020 19:51:03 +0900 Subject: [PATCH 0287/2481] fix default export for photo player --- src/components/photoPlayer/plugin.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/photoPlayer/plugin.js b/src/components/photoPlayer/plugin.js index f658e4c70..d8d4ee70e 100644 --- a/src/components/photoPlayer/plugin.js +++ b/src/components/photoPlayer/plugin.js @@ -1,6 +1,6 @@ import connectionManager from 'connectionManager'; -export class PhotoPlayer { +export default class PhotoPlayer { constructor() { this.name = 'Photo Player'; this.type = 'mediaplayer'; @@ -43,5 +43,3 @@ export class PhotoPlayer { return (mediaType || '').toLowerCase() === 'photo'; } } - -export default PhotoPlayer; From 0c302adbb5970a94d498599ad434b407a3cd4ecc Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 30 May 2020 13:00:51 +0200 Subject: [PATCH 0288/2481] Add format checking to EPUB player --- src/components/bookPlayer/plugin.js | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/components/bookPlayer/plugin.js b/src/components/bookPlayer/plugin.js index 66bcb4697..b655b038a 100644 --- a/src/components/bookPlayer/plugin.js +++ b/src/components/bookPlayer/plugin.js @@ -215,24 +215,7 @@ export class BookPlayer { Id: item.Id } }; - if (!item.Path.endsWith('.epub')) { - return new Promise((resolve, reject) => { - let errorDialog = dialogHelper.createDialog({ - size: 'small', - autoFocus: false, - removeOnClose: true - }); - errorDialog.innerHTML = '

    This book type is not supported yet

    '; - - this.stop(); - - dialogHelper.open(errorDialog); - loading.hide(); - - return resolve(); - }); - } let serverId = item.ServerId; let apiClient = connectionManager.getApiClient(serverId); @@ -283,6 +266,13 @@ export class BookPlayer { canPlayMediaType(mediaType) { return (mediaType || '').toLowerCase() === 'book'; } + + canPlayItem(item) { + if (item.Path && (item.Path.endsWith('epub'))) { + return true; + } + return false; + } } export default BookPlayer; From 9535d94ab770089d7122d8702102340b6d85e2cf Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 30 May 2020 13:15:49 +0200 Subject: [PATCH 0289/2481] Add Path field to List query Fixes the lack of play button on books --- src/components/playback/playbackmanager.js | 2 -- src/controllers/list.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 053088ef2..88d4e0d59 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1129,7 +1129,6 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla } self.canPlay = function (item) { - var itemType = item.Type; if (itemType === 'PhotoAlbum' || itemType === 'MusicGenre' || itemType === 'Season' || itemType === 'Series' || itemType === 'BoxSet' || itemType === 'MusicAlbum' || itemType === 'MusicArtist' || itemType === 'Playlist') { @@ -1143,7 +1142,6 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla } if (itemType === 'Program') { - if (!item.EndDate || !item.StartDate) { return false; } diff --git a/src/controllers/list.js b/src/controllers/list.js index edd446900..d05616ec9 100644 --- a/src/controllers/list.js +++ b/src/controllers/list.js @@ -308,7 +308,7 @@ define(['globalize', 'listView', 'layoutManager', 'userSettings', 'focusManager' return apiClient.getItems(apiClient.getCurrentUserId(), modifyQueryWithFilters(instance, { StartIndex: startIndex, Limit: limit, - Fields: 'PrimaryImageAspectRatio,SortName', + Fields: 'PrimaryImageAspectRatio,SortName,Path', ImageTypeLimit: 1, ParentId: item.Id, SortBy: sortBy From d8375efc6918017a5fa4d3bb6a009c5b40c3641d Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 30 May 2020 13:33:38 +0200 Subject: [PATCH 0290/2481] Add Path to home sections Fixes play button on home sections for books --- src/components/homesections/homesections.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 3cd30893b..535f1cd68 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -229,7 +229,7 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la var options = { Limit: limit, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo', + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,Path', ImageTypeLimit: 1, EnableImageTypes: 'Primary,Backdrop,Thumb', ParentId: parentId @@ -667,7 +667,7 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la var apiClient = connectionManager.getApiClient(serverId); return apiClient.getNextUpEpisodes({ Limit: enableScrollX() ? 24 : 15, - Fields: 'PrimaryImageAspectRatio,SeriesInfo,DateCreated,BasicSyncInfo', + Fields: 'PrimaryImageAspectRatio,SeriesInfo,DateCreated,BasicSyncInfo,Path', UserId: apiClient.getCurrentUserId(), ImageTypeLimit: 1, EnableImageTypes: 'Primary,Backdrop,Banner,Thumb', From 8a1ca94760db89e7f848a3e782b6505199fd2e75 Mon Sep 17 00:00:00 2001 From: Influence365 Date: Sat, 30 May 2020 13:26:22 +0100 Subject: [PATCH 0291/2481] Update src/dashboardgeneral.html Co-authored-by: Julien Machiels --- src/dashboardgeneral.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboardgeneral.html b/src/dashboardgeneral.html index 9ac0c0b0c..6c0f43eb3 100644 --- a/src/dashboardgeneral.html +++ b/src/dashboardgeneral.html @@ -62,7 +62,7 @@
    ${LabelLoginDisclaimerHelp}
    -
    +
    ${LabelCustomCssHelp}
    From e6b7c8a6915540c7ef57ba833655b60174f46c63 Mon Sep 17 00:00:00 2001 From: Influence365 Date: Sat, 30 May 2020 13:27:06 +0100 Subject: [PATCH 0292/2481] Update emby-textarea.js --- src/elements/emby-textarea/emby-textarea.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/emby-textarea/emby-textarea.js b/src/elements/emby-textarea/emby-textarea.js index 8d01ff950..58402843e 100644 --- a/src/elements/emby-textarea/emby-textarea.js +++ b/src/elements/emby-textarea/emby-textarea.js @@ -55,7 +55,7 @@ define(['layoutManager', 'browser', 'css!./emby-textarea', 'registerElement', 'e newHeight = textarea.scrollHeight/* - offset*/; hasGrown = true; } - $('.customCSS').css("height",newHeight + 'px'); + $('.customCssContainer').css("height",newHeight + 'px'); textarea.style.height = newHeight + 'px'; } From 7169d8494d6794fb4a821488807d9e960e7fc45c Mon Sep 17 00:00:00 2001 From: ferferga Date: Sat, 30 May 2020 16:44:33 +0200 Subject: [PATCH 0293/2481] Add a toggle for blurhash --- .../displaySettings/displaySettings.js | 2 ++ .../displaySettings.template.html | 11 ++++--- src/components/images/imageLoader.js | 32 ++++++++++++++++--- src/components/images/style.css | 14 ++++++++ src/scripts/settings/userSettings.js | 10 ++++++ src/strings/es.json | 5 --- 6 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/components/displaySettings/displaySettings.js b/src/components/displaySettings/displaySettings.js index 4e068960a..c4eb35f49 100644 --- a/src/components/displaySettings/displaySettings.js +++ b/src/components/displaySettings/displaySettings.js @@ -181,6 +181,7 @@ define(['require', 'browser', 'layoutManager', 'appSettings', 'pluginManager', ' context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs(); context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos(); context.querySelector('#chkFadein').checked = userSettings.enableFastFadein(); + context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash(); context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops(); context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner(); @@ -223,6 +224,7 @@ define(['require', 'browser', 'layoutManager', 'appSettings', 'pluginManager', ' userSettingsInstance.skin(context.querySelector('.selectSkin').value); userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked); + userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked); userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked); userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked); diff --git a/src/components/displaySettings/displaySettings.template.html b/src/components/displaySettings/displaySettings.template.html index 698d60af8..a0c5cba61 100644 --- a/src/components/displaySettings/displaySettings.template.html +++ b/src/components/displaySettings/displaySettings.template.html @@ -143,12 +143,12 @@
    -
    +
    ${LabelLibraryPageSizeHelp}
    -
    +
    -
    +
    ${EnableBlurhashHelp}
    -
    + +
    -
    - -
    ${EnableDecodingColorDepth10Help}
    -
    -
    From 212c093dab3561707beabba163bc4059161bce7e Mon Sep 17 00:00:00 2001 From: artiume Date: Fri, 29 May 2020 12:34:05 -0400 Subject: [PATCH 0391/2481] Update en-us.json --- src/strings/en-us.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 102d7d65b..cbcd248bf 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1550,8 +1550,6 @@ "OnApplicationStartup": "On application startup", "UnsupportedPlayback": "Jellyfin cannot decrypt content protected by DRM but all content will be attempted regardless, including protected titles. Some files may appear completely black due to encryption or other unsupported features, such as interactive titles.", "EnableBlurhash": "Enable blurred placeholders for images", - "EnableBlurhashHelp": "Images that are still being loaded will be displayed with a blurred placeholder", + "EnableBlurhashHelp": "Images that are still being loaded will be displayed with a blurred placeholder" - "EnableDecodingColorDepth10": "Enable 10-Bit hardware decoding", - "EnableDecodingColorDepth10Help" : "Enable 10-Bit hardware decoding on supported hardware. Only works for HEVC and VP9 formats. Turn this off if you experience playback issues." } From dbe4020ba5e06b91dc85a7a9b302ceb8f17315e1 Mon Sep 17 00:00:00 2001 From: artiume Date: Fri, 29 May 2020 14:06:02 -0400 Subject: [PATCH 0392/2481] Update encodingsettings.js --- src/controllers/dashboard/encodingsettings.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/controllers/dashboard/encodingsettings.js b/src/controllers/dashboard/encodingsettings.js index 65f4e4401..0f54f9d70 100644 --- a/src/controllers/dashboard/encodingsettings.js +++ b/src/controllers/dashboard/encodingsettings.js @@ -5,6 +5,8 @@ define(['jQuery', 'loading', 'globalize', 'dom', 'libraryMenu'], function ($, lo Array.prototype.forEach.call(page.querySelectorAll('.chkDecodeCodec'), function (c) { c.checked = -1 !== (config.HardwareDecodingCodecs || []).indexOf(c.getAttribute('data-codec')); }); + page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc; + page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9; page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding; $('#selectVideoDecoder', page).val(config.HardwareAccelerationType); $('#selectThreadCount', page).val(config.EncodingThreadCount); @@ -67,6 +69,8 @@ define(['jQuery', 'loading', 'globalize', 'dom', 'libraryMenu'], function ($, lo }), function (c) { return c.getAttribute('data-codec'); }); + config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked; + config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked; config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked; ApiClient.updateNamedConfiguration('encoding', config).then(function () { updateEncoder(form); From d424ed669bf5397e1969f1da40e7a9968d5eed10 Mon Sep 17 00:00:00 2001 From: artiume Date: Fri, 29 May 2020 14:08:44 -0400 Subject: [PATCH 0393/2481] Update encodingsettings.html --- src/encodingsettings.html | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/encodingsettings.html b/src/encodingsettings.html index f63dd93e8..f5b7b0e1c 100644 --- a/src/encodingsettings.html +++ b/src/encodingsettings.html @@ -43,10 +43,6 @@ HEVC - -
    +
    + + +
    ${EnableDecodingColorDepth10Help}
    +
    +
    -
    ${EnableDecodingColorDepth10Help}
    From f0af15707c449a9c5987147165714e9e3fc106de Mon Sep 17 00:00:00 2001 From: artiume Date: Fri, 29 May 2020 16:11:25 -0400 Subject: [PATCH 0396/2481] Update encodingsettings.html --- src/encodingsettings.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/encodingsettings.html b/src/encodingsettings.html index e3866a7e7..858375b14 100644 --- a/src/encodingsettings.html +++ b/src/encodingsettings.html @@ -66,11 +66,13 @@
    -
    +
    +
    +