mirror of
https://github.com/jellyfin/jellyfin-desktop.git
synced 2026-10-26 16:45:24 +00:00
On startup the WebEngineView first shows the bundled qrc find-webclient page and then navigates to the server web client. That cross-origin navigation moves the view to a new renderer process, and a user script registered on the view is not reliably delivered to it, so the first load of the web client comes up without window.NativeShell. jellyfin-web then falls back to its HTML video player: playback works, but the native mpv player, MPRIS, and client settings are silently unavailable until the page is reloaded. Registering the script on the profile instead makes injection apply to every load, including the first one. Verified on Linux (Qt 6.11.2, Wayland): before the change window.jmpInfo was undefined on every cold start until a reload; after it is present on the first load.
452 lines
11 KiB
QML
452 lines
11 KiB
QML
import QtQuick
|
|
import Konvergo 1.0
|
|
import QtWebEngine
|
|
import QtWebChannel
|
|
import QtQuick.Window
|
|
import QtQuick.Controls
|
|
import Qt.labs.platform as Labs
|
|
|
|
Window
|
|
{
|
|
id: mainWindow
|
|
title: "Jellyfin Desktop"
|
|
objectName: "mainWindow"
|
|
width: 1280
|
|
height: 720
|
|
minimumWidth: 213
|
|
minimumHeight: 120
|
|
visible: true
|
|
color: "#000000"
|
|
|
|
// Properties previously from KonvergoWindow
|
|
property bool webDesktopMode: true
|
|
property bool showDebugLayer: false
|
|
property string debugInfo: ""
|
|
property string videoInfo: ""
|
|
property string webUrl: ""
|
|
|
|
property bool showSystemTrayIcon: webDesktopMode && components.system.isWindows &&
|
|
components.settings.windowsTrayIcon
|
|
|
|
signal reloadWebClient()
|
|
|
|
Component.onCompleted: {
|
|
if (components && components.settings) {
|
|
webUrl = components.settings.getWebClientUrl(webDesktopMode)
|
|
}
|
|
}
|
|
|
|
onClosing: function(close) {
|
|
if (showSystemTrayIcon) {
|
|
// Minimize to tray on close.
|
|
close.accepted = false
|
|
mainWindow.hide()
|
|
}
|
|
}
|
|
|
|
function toggleFullscreen() {
|
|
visibility = (visibility === Window.FullScreen) ? Window.Windowed : Window.FullScreen
|
|
}
|
|
|
|
function toggleDebug() {
|
|
showDebugLayer = !showDebugLayer
|
|
}
|
|
|
|
function setFullScreen(enable) {
|
|
visibility = enable ? Window.FullScreen : Window.Windowed
|
|
}
|
|
|
|
function minimizeWindow() {
|
|
if (visibility !== Window.FullScreen)
|
|
visibility = Window.Minimized
|
|
}
|
|
|
|
function restoreWindow() {
|
|
mainWindow.show()
|
|
mainWindow.raise()
|
|
mainWindow.requestActivate()
|
|
}
|
|
|
|
function runWebAction(action)
|
|
{
|
|
if (mainWindow.webDesktopMode)
|
|
web.triggerWebAction(action)
|
|
}
|
|
|
|
Action
|
|
{
|
|
enabled: mainWindow.webDesktopMode
|
|
shortcut:
|
|
{
|
|
if (components.system.isMacos) return "Ctrl+Meta+F"
|
|
return "F11"
|
|
}
|
|
onTriggered: mainWindow.toggleFullscreen()
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: "Alt+Return"
|
|
enabled:
|
|
{
|
|
if (mainWindow.webDesktopMode && components.system.isWindows)
|
|
return true;
|
|
return false;
|
|
}
|
|
onTriggered: mainWindow.toggleFullscreen()
|
|
}
|
|
|
|
Action
|
|
{
|
|
enabled: mainWindow.webDesktopMode
|
|
shortcut: StandardKey.Close
|
|
onTriggered: mainWindow.close()
|
|
}
|
|
|
|
Action
|
|
{
|
|
enabled: mainWindow.webDesktopMode
|
|
shortcut: {
|
|
if (components.system.isMacos) return "Ctrl+M";
|
|
return "Meta+Down";
|
|
}
|
|
onTriggered: mainWindow.minimizeWindow()
|
|
}
|
|
|
|
Action
|
|
{
|
|
enabled: mainWindow.webDesktopMode
|
|
shortcut: components.system.isWindows ? "Ctrl+Q" : StandardKey.Quit
|
|
onTriggered: Qt.quit()
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: "Ctrl+Shift+D"
|
|
enabled: mainWindow.webDesktopMode
|
|
onTriggered: mainWindow.toggleDebug()
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: StandardKey.Copy
|
|
onTriggered: runWebAction(WebEngineView.Copy)
|
|
id: action_copy
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: StandardKey.Cut
|
|
onTriggered: runWebAction(WebEngineView.Cut)
|
|
id: action_cut
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: StandardKey.Paste
|
|
onTriggered: runWebAction(WebEngineView.Paste)
|
|
id: action_paste
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: StandardKey.SelectAll
|
|
onTriggered: runWebAction(WebEngineView.SelectAll)
|
|
id: action_selectall
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: StandardKey.Undo
|
|
onTriggered: runWebAction(WebEngineView.Undo)
|
|
id: action_undo
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: StandardKey.Redo
|
|
onTriggered: runWebAction(WebEngineView.Redo)
|
|
id: action_redo
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: StandardKey.Back
|
|
onTriggered: runWebAction(WebEngineView.Back)
|
|
id: action_back
|
|
}
|
|
|
|
Action
|
|
{
|
|
shortcut: StandardKey.Forward
|
|
onTriggered: runWebAction(WebEngineView.Forward)
|
|
id: action_forward
|
|
}
|
|
|
|
Action
|
|
{
|
|
enabled: mainWindow.webDesktopMode
|
|
shortcut: "Ctrl+0"
|
|
onTriggered: web.zoomFactor = 1.0
|
|
}
|
|
|
|
WebChannel
|
|
{
|
|
id: webChannelObject
|
|
}
|
|
|
|
Binding
|
|
{
|
|
target: web
|
|
property: "zoomFactor"
|
|
value: 1.0
|
|
when: !components.settings.allowBrowserZoom()
|
|
}
|
|
|
|
MpvVideoItem
|
|
{
|
|
id: video
|
|
objectName: "video"
|
|
enabled: true
|
|
|
|
width: mainWindow.contentItem.width
|
|
height: mainWindow.contentItem.height
|
|
anchors.left: mainWindow.contentItem.left
|
|
anchors.right: mainWindow.contentItem.right
|
|
anchors.top: mainWindow.contentItem.top
|
|
|
|
Component.onCompleted: {
|
|
console.log("MpvVideoItem size:", width, "x", height, "visible:", visible)
|
|
}
|
|
onWidthChanged: console.log("MpvVideoItem width changed:", width)
|
|
onHeightChanged: console.log("MpvVideoItem height changed:", height)
|
|
}
|
|
|
|
WebEngineView
|
|
{
|
|
id: web
|
|
objectName: "web"
|
|
width: mainWindow.width
|
|
height: mainWindow.height
|
|
z: 100
|
|
backgroundColor: "transparent"
|
|
|
|
// this is needed to prevent intermittent(?) black screens when unminizing
|
|
// or resumsing from suspend (linux/{x11/wayland}, possibly others).
|
|
layer.enabled: true
|
|
|
|
webChannel: webChannelObject
|
|
settings.errorPageEnabled: false
|
|
settings.localContentCanAccessRemoteUrls: true
|
|
settings.localContentCanAccessFileUrls: true
|
|
settings.allowRunningInsecureContent: true
|
|
settings.playbackRequiresUserGesture: false
|
|
profile.httpUserAgent: components.system.getUserAgent()
|
|
profile.httpCacheType: WebEngineProfile.DiskHttpCache
|
|
url: mainWindow.webUrl
|
|
focus: true
|
|
property string currentHoveredUrl: ""
|
|
onLinkHovered: function(hoveredUrl)
|
|
{
|
|
web.currentHoveredUrl = hoveredUrl;
|
|
}
|
|
profile.persistentCookiesPolicy: WebEngineProfile.AllowPersistentCookies
|
|
profile.offTheRecord: false
|
|
profile.storageName: "JellyfinDesktopStorage"
|
|
|
|
Component.onCompleted:
|
|
{
|
|
console.log("WebEngineView size:", width, "x", height, "backgroundColor:", backgroundColor)
|
|
forceActiveFocus()
|
|
mainWindow.reloadWebClient.connect(reload)
|
|
|
|
// Handle CSP workaround from C++
|
|
components.system.pageContentReady.connect(function(html, finalUrl, hadCSP) {
|
|
if (hadCSP) {
|
|
console.log("CSP workaround: navigating to", finalUrl);
|
|
web.url = finalUrl;
|
|
}
|
|
})
|
|
|
|
var nativeshell =
|
|
{
|
|
sourceCode: components.system.getNativeShellScript(),
|
|
injectionPoint: WebEngineScript.DocumentCreation,
|
|
worldId: WebEngineScript.MainWorld
|
|
}
|
|
|
|
web.profile.userScripts.collection = [ nativeshell ];
|
|
}
|
|
|
|
onLoadingChanged: function(loadingInfo)
|
|
{
|
|
// we use a timer here to switch to the webview since
|
|
// it take a few moments for the webview to render
|
|
// after it has loaded.
|
|
//
|
|
if (loadingInfo.status == WebEngineView.LoadStartedStatus)
|
|
{
|
|
console.log("WebEngineLoadRequest starting: " + loadingInfo.url);
|
|
}
|
|
else if (loadingInfo.status == WebEngineView.LoadSucceededStatus)
|
|
{
|
|
console.log("WebEngineLoadRequest success: " + loadingInfo.url);
|
|
}
|
|
else if (loadingInfo.status == WebEngineView.LoadFailedStatus)
|
|
{
|
|
console.log("WebEngineLoadRequest failure: " + loadingInfo.url + " error code: " + loadingInfo.errorCode);
|
|
errorLabel.visible = true
|
|
errorLabel.text = "Error loading client, this is bad and should not happen<br>" +
|
|
"You can try to <a href='reload'>reload</a> or head to our <a href='http://jellyfin.org'>support page</a><br><br>Actual Error: <pre>" +
|
|
loadingInfo.errorString + " [" + loadingInfo.errorCode + "]</pre><br><br>" +
|
|
"Provide the <a target='_blank' href='file://"+ components.system.logFilePath + "'>logfile</a> as well."
|
|
}
|
|
}
|
|
|
|
onNewWindowRequested:
|
|
{
|
|
if (request.userInitiated)
|
|
{
|
|
console.log("Opening external URL: " + web.currentHoveredUrl)
|
|
components.system.openExternalUrl(web.currentHoveredUrl)
|
|
}
|
|
}
|
|
|
|
onFullScreenRequested:
|
|
{
|
|
console.log("Request fullscreen: " + request.toggleOn)
|
|
mainWindow.setFullScreen(request.toggleOn)
|
|
request.accept()
|
|
}
|
|
|
|
onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID)
|
|
{
|
|
components.system.jsLog(level, sourceID + ":" + lineNumber + " " + message);
|
|
}
|
|
|
|
onCertificateError: function(error)
|
|
{
|
|
console.log(error.url + " :" + error.description + error.error)
|
|
if (components.settings.ignoreSSLErrors()) {
|
|
error.acceptCertificate()
|
|
}
|
|
}
|
|
}
|
|
|
|
Text
|
|
{
|
|
id: errorLabel
|
|
z: 5
|
|
anchors.centerIn: parent
|
|
color: "#999999"
|
|
linkColor: "#a85dc3"
|
|
text: "Generic error"
|
|
font.pixelSize: 32
|
|
font.bold: true
|
|
visible: false
|
|
verticalAlignment: Text.AlignVCenter
|
|
textFormat: Text.StyledText
|
|
onLinkActivated:
|
|
{
|
|
if (url == "reload")
|
|
{
|
|
errorLabel.visible = false
|
|
web.reload()
|
|
}
|
|
else
|
|
{
|
|
Qt.openUrlExternally(url)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Rectangle
|
|
{
|
|
id: debug
|
|
color: "black"
|
|
z: 10
|
|
anchors.centerIn: parent
|
|
width: parent.width
|
|
height: parent.height
|
|
opacity: 0.7
|
|
visible: mainWindow.showDebugLayer
|
|
|
|
Text
|
|
{
|
|
id: debugLabel
|
|
width: (parent.width - 50) / 2
|
|
height: parent.height - 25
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 64
|
|
anchors.top: parent.top
|
|
anchors.topMargin: 54
|
|
anchors.bottomMargin: 54
|
|
color: "white"
|
|
font.pixelSize: Math.round(height / 65)
|
|
wrapMode: Text.WrapAnywhere
|
|
|
|
function windowDebug()
|
|
{
|
|
var dbg = mainWindow.debugInfo + "Window and web\n";
|
|
dbg += " Window size: " + parent.width + "x" + parent.height + " - " + web.width + "x" + web.height + "\n";
|
|
dbg += " DevicePixel ratio: " + Screen.devicePixelRatio + "\n";
|
|
|
|
return dbg;
|
|
}
|
|
|
|
text: windowDebug()
|
|
}
|
|
|
|
Text
|
|
{
|
|
id: videoLabel
|
|
width: (parent.width - 50) / 2
|
|
height: parent.height - 25
|
|
anchors.right: parent.right
|
|
anchors.left: debugLabel.right
|
|
anchors.rightMargin: 64
|
|
anchors.top: parent.top
|
|
anchors.topMargin: 54
|
|
anchors.bottomMargin: 54
|
|
color: "white"
|
|
font.pixelSize: Math.round(height / 65)
|
|
wrapMode: Text.WrapAnywhere
|
|
|
|
text: mainWindow.videoInfo
|
|
}
|
|
}
|
|
|
|
property QtObject webChannel: web.webChannel
|
|
|
|
Labs.SystemTrayIcon {
|
|
visible: showSystemTrayIcon
|
|
icon.source: "qrc:/images/icon.png"
|
|
tooltip: "Jellyfin Desktop"
|
|
|
|
onActivated: function(reason) {
|
|
if (reason === Labs.SystemTrayIcon.Context) {
|
|
// Right click: open context menu
|
|
contextMenu.open()
|
|
components.window.setCursorVisibility(true)
|
|
} else {
|
|
// All other clicks: restore window
|
|
restoreWindow()
|
|
}
|
|
}
|
|
|
|
menu: Labs.Menu {
|
|
id: contextMenu
|
|
Labs.MenuItem {
|
|
text: qsTr("Restore")
|
|
onTriggered: restoreWindow()
|
|
}
|
|
Labs.MenuSeparator {}
|
|
Labs.MenuItem {
|
|
text: qsTr("Quit")
|
|
onTriggered: Qt.quit()
|
|
}
|
|
}
|
|
}
|
|
}
|