// SPDX-FileCopyrightText: 2020 George Florea Bănuș // SPDX-FileCopyrightText: 2026 Tri Vo Khac // SPDX-License-Identifier: GPL-3.0-or-later import QtQuick import QtQuick.Controls import QtQuick.Dialogs import QtQuick.Layouts import org.kde.ki18n import org.kde.kirigami as Kirigami import org.kde.config as KConfig // Main window, laid out like Haruna: menu bar, toolbar, video with the playlist at // its edge, and the playback controls below. ApplicationWindow { id: appWindow // Set from C++ (main.cpp) when a file is passed on the command line. required property url initialUrl property int previousVisibility: Window.Windowed readonly property bool isFullScreen: visibility === Window.FullScreen // Keep the original subtitle track so glossary edits update the generated // subtitle even while that translated track is selected in mpv. property var lastTranslationTrack: null property url lastTranslationVideo: "" function openFile(url: url) { playlistModel.openFile(url, QtPotatoSettings.playlistLoadSiblings) } function toggleFullScreen() { if (isFullScreen) { exitFullScreen() } else { showFullScreen() } } function exitFullScreen() { if (previousVisibility === Window.Maximized) { showMaximized() } else { showNormal() } } function translatedTrackTitle(): string { return KI18n.i18nc("@item:inlistbox subtitle track, %1 is a language", "%1 (translated)", QtPotatoSettings.targetLanguage) } function translateCurrentSubtitle() { let track = video.subtitleTracks.find(track => track.id === video.subtitleId) // Translate the original again, not the translation on screen. if (track && video.isTranslatedSubtitle(track.id)) { track = lastTranslationTrack ? video.subtitleTracks.find(track => track.id === lastTranslationTrack.id) : null } // No subtitle shown, or a picture-based one: translate the best text subtitle. if (!track || track.bitmap) { const sourceId = video.translationSourceId() track = video.subtitleTracks.find(track => track.id === sourceId) } if (track) { lastTranslationTrack = track lastTranslationVideo = video.currentUrl translationService.translate(video.currentUrl, track, video.mediaTitle) } } visible: true title: video.mediaTitle || KI18n.i18nc("@title:window", "QtPotato") width: Kirigami.Units.gridUnit * 55 height: Kirigami.Units.gridUnit * 33 minimumWidth: Kirigami.Units.gridUnit * 23 minimumHeight: Kirigami.Units.gridUnit * 11 color: Kirigami.Theme.backgroundColor onVisibilityChanged: visibility => { // Restored when leaving full screen. if (visibility !== Window.FullScreen) { previousVisibility = visibility } } onClosing: QtPotatoSettings.save() Component.onCompleted: { if (initialUrl.toString().length > 0) { openFile(initialUrl) } } KConfig.WindowStateSaver { configGroupName: "MainWindow" } menuBar: MainMenuBar { actions: appActions mpv: video visible: !appWindow.isFullScreen && QtPotatoSettings.showMenuBar } header: Header { id: headerItem actions: appActions mpv: video translator: translationService shown: !appWindow.isFullScreen && QtPotatoSettings.showHeader menuBarShown: appWindow.menuBar.visible } Actions { id: appActions mpv: video playlist: playlistModel translator: translationService osd: osdItem fullscreen: appWindow.isFullScreen shortcutsEnabled: !fileDialog.visible && !subtitleDialog.visible && !addToPlaylistDialog.visible && !openUrlPopup.visible onOpenFileRequested: fileDialog.open() onFileOpenRequested: url => appWindow.openFile(url) onOpenUrlRequested: openUrlPopup.open() onAddSubtitleRequested: subtitleDialog.open() onSettingsRequested: page => settingsWindow.openPage(page) onFullscreenToggleRequested: appWindow.toggleFullScreen() onFullscreenExitRequested: appWindow.exitFullScreen() onPlaylistToggleRequested: playlistPanel.toggle() onContextMenuRequested: contextMenu.popup() onTranslateRequested: appWindow.translateCurrentSubtitle() onEditGlossaryRequested: { translationService.loadGlossary(video.currentUrl) glossaryDialog.open() } onMenuBarToggleRequested: { QtPotatoSettings.showMenuBar = !QtPotatoSettings.showMenuBar QtPotatoSettings.save() } onHeaderToggleRequested: { QtPotatoSettings.showHeader = !QtPotatoSettings.showHeader QtPotatoSettings.save() } } PlaylistModel { id: playlistModel onPlayRequested: url => { // A translation made earlier is loaded and turned on with the file. video.loadFile(url, translationService.savedTranslation(url), appWindow.translatedTrackTitle()) RecentFilesModel.add(url) } } Binding { target: RecentFilesModel property: "maxCount" value: QtPotatoSettings.maxRecentFiles } MprisItem { mpvItem: video playlist: playlistModel window: appWindow onRaiseRequested: { appWindow.show() appWindow.raise() appWindow.requestActivate() } onOpenUriRequested: uri => appWindow.openFile(uri) onNextRequested: appActions.playNext() onPreviousRequested: appActions.playPrevious() } TranslationService { id: translationService onTranslated: (subtitleUrl, fromCache) => { video.showTranslatedSubtitleFile(subtitleUrl, appWindow.translatedTrackTitle()) osdItem.message(fromCache ? KI18n.i18nc("@info:osd", "Loaded the saved translation") : KI18n.i18nc("@info:osd", "Translation ready")) } } MpvVideo { id: video onCurrentUrlChanged: { translationService.cancel() translationService.forgetTranslation() appWindow.lastTranslationTrack = null appWindow.lastTranslationVideo = "" glossaryDialog.close() } actions: appActions fullscreen: appWindow.isFullScreen readonly property bool playlistBeside: playlistPanel.visible && !QtPotatoSettings.playlistOverlayVideo anchors.top: parent.top anchors.left: playlistBeside && playlistPanel.edge === Qt.LeftEdge ? playlistPanel.right : parent.left anchors.right: playlistBeside && playlistPanel.edge === Qt.RightEdge ? playlistPanel.left : parent.right height: parent.height - (footerItem.isDocked ? footerItem.height : 0) onPointerMoved: (x, y) => { footerItem.pointerMoved(y) if (!QtPotatoSettings.playlistToggleWithMouse) { return } const atEdge = playlistPanel.edge === Qt.RightEdge ? x > width - Kirigami.Units.gridUnit * 3 : x < Kirigami.Units.gridUnit * 3 playlistPanel.state = atEdge ? "visible" : "hidden" } onPointerLeft: footerItem.pointerLeft() onFilesDropped: (urls, append) => { if (append) { playlistModel.append(urls) } else if (urls.length === 1) { appWindow.openFile(urls[0]) } else { playlistModel.replace(urls) } } onSubtitleDropped: url => video.addSubtitleFile(url) onEndOfFile: { switch (QtPotatoSettings.playbackBehavior) { case "RepeatItem": video.seek(0) video.paused = false break case "StopAfterItem": break default: appActions.playNext() } } Osd { id: osdItem maxWidth: video.width } } Playlist { id: playlistPanel playlist: playlistModel mpv: video mainWindowWidth: appWindow.width fullscreen: appWindow.isFullScreen height: video.height onAddFilesRequested: addToPlaylistDialog.open() } Footer { id: footerItem actions: appActions mpv: video fullscreen: appWindow.isFullScreen showMenuButton: !headerItem.visible && !appWindow.menuBar.visible width: parent.width anchors.bottom: parent.bottom } Kirigami.InlineMessage { width: Math.min(Kirigami.Units.gridUnit * 40, appWindow.width * 0.8) anchors.centerIn: parent type: Kirigami.MessageType.Error text: translationService.errorText visible: text.length > 0 showCloseButton: true onVisibleChanged: if (!visible) translationService.clearError() actions: Kirigami.Action { icon.name: "configure" text: KI18n.i18nc("@action:button", "Translation Settings") onTriggered: settingsWindow.openPage("translation") } } MpvContextMenu { id: contextMenu actions: appActions mpv: video } SettingsWindow { id: settingsWindow mpv: video translator: translationService } InputPopup { id: openUrlPopup title: KI18n.i18nc("@title:window", "Open URL") preferredWidth: Kirigami.Units.gridUnit * 30 lastText: QtPotatoSettings.lastUrl placeholderText: "https://example.org/video.mkv" buttonText: KI18n.i18nc("@action:button", "Open") onSubmitted: text => { QtPotatoSettings.lastUrl = text QtPotatoSettings.save() appWindow.openFile(text) } } FileDialog { id: fileDialog title: KI18n.i18nc("@title:window", "Select File") currentFolder: QtPotatoSettings.fileDialogLastLocation fileMode: FileDialog.OpenFile nameFilters: [ KI18n.i18nc("@item:inlistbox file filter", "Video files (*.mkv *.mp4 *.webm *.avi *.mov *.m4v *.ts *.flv *.wmv)"), KI18n.i18nc("@item:inlistbox file filter", "All files (*)") ] onAccepted: { appWindow.openFile(selectedFile) QtPotatoSettings.fileDialogLastLocation = currentFolder QtPotatoSettings.save() } } FileDialog { id: addToPlaylistDialog title: KI18n.i18nc("@title:window", "Add to Playlist") currentFolder: QtPotatoSettings.fileDialogLastLocation fileMode: FileDialog.OpenFiles onAccepted: playlistModel.append(selectedFiles) } FileDialog { id: subtitleDialog title: KI18n.i18nc("@title:window", "Select Subtitles File") currentFolder: { const url = video.currentUrl.toString() return url.substring(0, url.lastIndexOf("/")) } fileMode: FileDialog.OpenFile nameFilters: [ KI18n.i18nc("@item:inlistbox file filter", "Subtitle files (*.srt *.ass *.ssa *.vtt *.sub *.sup)"), KI18n.i18nc("@item:inlistbox file filter", "All files (*)") ] onAccepted: video.addSubtitleFile(selectedFile) } GlossaryDialog { id: glossaryDialog translator: translationService } }