// 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 org.kde.ki18n // Every command of the player, shared by the menus, the toolbar buttons and the // keyboard shortcuts. Shortcuts follow Haruna's defaults. Item { id: root required property MpvItem mpv required property PlaylistModel playlist required property TranslationService translator required property Osd osd // Off while a dialog has the keyboard, so typing there does not seek or pause. property bool shortcutsEnabled: true property bool fullscreen: false readonly property bool hasFile: mpv.fileLoaded readonly property bool subtitlesVisible: mpv.subtitleVisible readonly property bool deinterlacingEnabled: mpv.deinterlace signal openFileRequested() // Open a known file, such as one from Recent Files. signal fileOpenRequested(url: url) signal openUrlRequested() signal addSubtitleRequested() signal settingsRequested(page: string) signal fullscreenToggleRequested() signal fullscreenExitRequested() signal playlistToggleRequested() signal menuBarToggleRequested() signal headerToggleRequested() signal contextMenuRequested() signal translateRequested() signal editGlossaryRequested() function seekBy(seconds: real) { mpv.seekBy(seconds) } function changeVolume(steps: int) { mpv.volume = mpv.volume + steps * QtPotatoSettings.volumeStep osd.message(KI18n.i18nc("@info:osd", "Volume: %1", mpv.volume)) } function changeSpeed(delta: real) { mpv.speed = delta === 0 ? 1.0 : mpv.speed + delta osd.message(KI18n.i18nc("@info:osd", "Playback speed: %1", Number(mpv.speed).toFixed(2))) } function changeSubtitleDelay(seconds: real) { mpv.subtitleDelay = mpv.subtitleDelay + seconds osd.message(KI18n.i18nc("@info:osd", "Subtitle delay: %1 ms", Math.round(mpv.subtitleDelay * 1000))) } function cycleTrack(tracks: var, currentId: int, step: int): int { // Track ids with 0 ("off") first, as mpv's cycle commands do. const ids = [0].concat(tracks.map(track => track.id)) const index = Math.max(0, ids.indexOf(currentId)) return ids[(index + step + ids.length) % ids.length] } // The next or previous file, following the playback behavior setting. function playNext(): bool { return playlist.playNext(QtPotatoSettings.playbackBehavior === "RepeatPlaylist") } function playPrevious(): bool { return playlist.playPrevious(QtPotatoSettings.playbackBehavior === "RepeatPlaylist") } property Action openFile: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Open File…") icon.name: "folder-videos" shortcut: StandardKey.Open onTriggered: root.openFileRequested() } property Action openUrl: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Open URL…") icon.name: "internet-services" shortcut: "Ctrl+Shift+O" onTriggered: root.openUrlRequested() } property Action quit: Action { text: KI18n.i18nc("@action", "Quit") icon.name: "application-exit" shortcut: StandardKey.Quit onTriggered: Qt.quit() } property Action playPause: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Play/Pause") icon.name: root.mpv.paused ? "media-playback-start" : "media-playback-pause" shortcut: "Space" onTriggered: root.mpv.togglePause() } property Action stop: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Stop") icon.name: "media-playback-stop" shortcut: "Media Stop" onTriggered: root.mpv.stop() } property Action restart: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Restart Playback") icon.name: "edit-reset" shortcut: "Ctrl+Shift+R" onTriggered: root.mpv.seek(0) } property Action playNextFile: Action { enabled: root.shortcutsEnabled && root.playlist.count > 1 text: KI18n.i18nc("@action", "Play Next") icon.name: "media-skip-forward" shortcut: "Shift+." onTriggered: root.playNext() } property Action playPreviousFile: Action { enabled: root.shortcutsEnabled && root.playlist.count > 1 text: KI18n.i18nc("@action", "Play Previous") icon.name: "media-skip-backward" shortcut: "Shift+," onTriggered: root.playPrevious() } property Action seekForwardSmall: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Seek Small Step Forward") icon.name: "go-next" shortcut: "Right" onTriggered: root.seekBy(QtPotatoSettings.seekSmallStep) } property Action seekBackwardSmall: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Seek Small Step Backward") icon.name: "go-previous" shortcut: "Left" onTriggered: root.seekBy(-QtPotatoSettings.seekSmallStep) } property Action seekForwardMedium: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Seek Medium Step Forward") icon.name: "media-seek-forward" shortcut: "Shift+Right" onTriggered: root.seekBy(QtPotatoSettings.seekMediumStep) } property Action seekBackwardMedium: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Seek Medium Step Backward") icon.name: "media-seek-backward" shortcut: "Shift+Left" onTriggered: root.seekBy(-QtPotatoSettings.seekMediumStep) } property Action seekForwardBig: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Seek Big Step Forward") icon.name: "arrow-right-double" shortcut: "Up" onTriggered: root.seekBy(QtPotatoSettings.seekBigStep) } property Action seekBackwardBig: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Seek Big Step Backward") icon.name: "arrow-left-double" shortcut: "Down" onTriggered: root.seekBy(-QtPotatoSettings.seekBigStep) } property Action seekNextSubtitle: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Seek To Next Subtitle") icon.name: "go-down" shortcut: "Ctrl+Right" onTriggered: root.mpv.commandAsync(["sub-seek", "1"]) } property Action seekPreviousSubtitle: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Seek To Previous Subtitle") icon.name: "go-up" shortcut: "Ctrl+Left" onTriggered: root.mpv.commandAsync(["sub-seek", "-1"]) } property Action seekNextChapter: Action { enabled: root.shortcutsEnabled && root.mpv.chapters.length > 0 text: KI18n.i18nc("@action", "Seek To Next Chapter") icon.name: "go-last" shortcut: "PgUp" onTriggered: root.mpv.commandAsync(["add", "chapter", "1"]) } property Action seekPreviousChapter: Action { enabled: root.shortcutsEnabled && root.mpv.chapters.length > 0 text: KI18n.i18nc("@action", "Seek To Previous Chapter") icon.name: "go-first" shortcut: "PgDown" onTriggered: root.mpv.commandAsync(["add", "chapter", "-1"]) } property Action frameStepForward: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Step Frame Forward") icon.name: "keyframe-next" shortcut: "." onTriggered: root.mpv.frameStep(true) } property Action frameStepBackward: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Step Frame Backward") icon.name: "keyframe-previous" shortcut: "," onTriggered: root.mpv.frameStep(false) } property Action speedUp: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Increase Playback Speed") icon.name: "media-seek-forward" shortcut: "]" onTriggered: root.changeSpeed(0.1) } property Action speedDown: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Decrease Playback Speed") icon.name: "media-seek-backward" shortcut: "[" onTriggered: root.changeSpeed(-0.1) } property Action speedReset: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Reset Playback Speed") icon.name: "speedometer" shortcut: "Backspace" onTriggered: root.changeSpeed(0) } property Action mute: Action { enabled: root.shortcutsEnabled text: root.mpv.mute ? KI18n.i18nc("@action", "Unmute") : KI18n.i18nc("@action", "Mute") icon.name: root.mpv.mute ? "player-volume" : "player-volume-muted" shortcut: "M" onTriggered: { root.mpv.mute = !root.mpv.mute root.osd.message(root.mpv.mute ? KI18n.i18nc("@info:osd", "Audio muted") : KI18n.i18nc("@info:osd", "Audio unmuted")) } } property Action volumeUp: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Volume Up") icon.name: "audio-volume-high" shortcut: "9" onTriggered: root.changeVolume(1) } property Action volumeDown: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Volume Down") icon.name: "audio-volume-low" shortcut: "0" onTriggered: root.changeVolume(-1) } property Action audioCycleUp: Action { enabled: root.shortcutsEnabled && root.mpv.audioTracks.length > 1 text: KI18n.i18nc("@action", "Cycle Up Audio Track") icon.name: "go-up" shortcut: "Shift+3" onTriggered: root.mpv.audioId = root.cycleTrack(root.mpv.audioTracks, root.mpv.audioId, 1) } property Action audioCycleDown: Action { enabled: root.shortcutsEnabled && root.mpv.audioTracks.length > 1 text: KI18n.i18nc("@action", "Cycle Down Audio Track") icon.name: "go-down" shortcut: "Shift+2" onTriggered: root.mpv.audioId = root.cycleTrack(root.mpv.audioTracks, root.mpv.audioId, -1) } property Action addSubtitle: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Add External Subtitle…") icon.name: "add-subtitle" shortcut: "Alt+S" onTriggered: root.addSubtitleRequested() } property Action subtitleToggle: Action { enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0 text: KI18n.i18nc("@action", "Show Subtitles") icon.name: "media-view-subtitles-symbolic" checkable: true checked: root.subtitlesVisible shortcut: "Ctrl+S" onTriggered: { const visible = root.mpv.getProperty("sub-visibility") root.mpv.setPropertyAsync("sub-visibility", !visible) root.mpv.setPropertyAsync("secondary-sub-visibility", !visible) checked = Qt.binding(() => root.subtitlesVisible) root.osd.message(visible ? KI18n.i18nc("@info:osd", "Subtitles hidden") : KI18n.i18nc("@info:osd", "Subtitles shown")) } } property Action subtitleCycleUp: Action { enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0 text: KI18n.i18nc("@action", "Cycle Up Subtitle") icon.name: "go-up" shortcut: "J" onTriggered: root.mpv.subtitleId = root.cycleTrack(root.mpv.subtitleTracks, root.mpv.subtitleId, 1) } property Action subtitleCycleDown: Action { enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0 text: KI18n.i18nc("@action", "Cycle Down Subtitle") icon.name: "go-down" shortcut: "Shift+J" onTriggered: root.mpv.subtitleId = root.cycleTrack(root.mpv.subtitleTracks, root.mpv.subtitleId, -1) } property Action subtitleEarlier: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Show Subtitles Earlier") icon.name: "go-previous" shortcut: "Z" onTriggered: root.changeSubtitleDelay(-0.1) } property Action subtitleLater: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Show Subtitles Later") icon.name: "go-next" shortcut: "Shift+Z" onTriggered: root.changeSubtitleDelay(0.1) } property Action subtitleDelayReset: Action { enabled: root.shortcutsEnabled && root.hasFile && root.mpv.subtitleDelay !== 0 text: KI18n.i18nc("@action", "Reset Subtitle Delay") icon.name: "edit-reset" onTriggered: root.changeSubtitleDelay(-root.mpv.subtitleDelay) } property Action subtitleBigger: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Increase Subtitle Font Size") icon.name: "format-font-size-more" shortcut: "Ctrl+Z" onTriggered: { QtPotatoSettings.subtitleFontSize = Math.min(150, QtPotatoSettings.subtitleFontSize + 2) QtPotatoSettings.save() root.osd.message(KI18n.i18nc("@info:osd", "Subtitle font size: %1", QtPotatoSettings.subtitleFontSize)) } } property Action subtitleSmaller: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Decrease Subtitle Font Size") icon.name: "format-font-size-less" shortcut: "Ctrl+X" onTriggered: { QtPotatoSettings.subtitleFontSize = Math.max(20, QtPotatoSettings.subtitleFontSize - 2) QtPotatoSettings.save() root.osd.message(KI18n.i18nc("@info:osd", "Subtitle font size: %1", QtPotatoSettings.subtitleFontSize)) } } property Action translateSubtitle: Action { enabled: root.shortcutsEnabled && root.hasFile && root.mpv.subtitleId > 0 && !root.translator.busy text: KI18n.i18nc("@action", "Translate Subtitle into %1", QtPotatoSettings.targetLanguage) icon.name: "translate" shortcut: "Ctrl+T" onTriggered: root.translateRequested() } property Action editGlossary: Action { enabled: root.shortcutsEnabled && root.hasFile && root.mpv.currentUrl.toString().startsWith("file:") text: KI18n.i18nc("@action", "Edit Translation Glossary…") icon.name: "document-edit" onTriggered: root.editGlossaryRequested() } property Action cancelTranslation: Action { enabled: root.translator.busy text: KI18n.i18nc("@action", "Cancel Translation") icon.name: "dialog-cancel" onTriggered: root.translator.cancel() } property Action screenshot: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Screenshot") icon.name: "image-x-generic" shortcut: "S" onTriggered: { root.mpv.screenshot() root.osd.message(KI18n.i18nc("@info:osd", "Screenshot saved")) } } property Action toggleDeinterlacing: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Deinterlace") icon.name: "view-filter" checkable: true checked: root.deinterlacingEnabled shortcut: "D" onTriggered: { const enabled = root.mpv.getProperty("deinterlace") === true root.mpv.setPropertyAsync("deinterlace", !enabled) checked = Qt.binding(() => root.deinterlacingEnabled) root.osd.message(enabled ? KI18n.i18nc("@info:osd", "Deinterlacing off") : KI18n.i18nc("@info:osd", "Deinterlacing on")) } } property Action zoomIn: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Zoom In") icon.name: "zoom-in" shortcut: StandardKey.ZoomIn onTriggered: root.mpv.commandAsync(["add", "video-zoom", "0.1"]) } property Action zoomOut: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Zoom Out") icon.name: "zoom-out" shortcut: StandardKey.ZoomOut onTriggered: root.mpv.commandAsync(["add", "video-zoom", "-0.1"]) } property Action zoomReset: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Reset Zoom") icon.name: "zoom-original" shortcut: "Ctrl+0" onTriggered: root.mpv.setPropertyAsync("video-zoom", 0) } property Action rotateClockwise: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Rotate Clockwise") icon.name: "object-rotate-right" shortcut: "Ctrl+R" onTriggered: root.mpv.setPropertyAsync("video-rotate", (root.mpv.getProperty("video-rotate") + 90) % 360) } property Action rotateCounterClockwise: Action { enabled: root.shortcutsEnabled && root.hasFile text: KI18n.i18nc("@action", "Rotate Counter Clockwise") icon.name: "object-rotate-left" shortcut: "Ctrl+E" onTriggered: root.mpv.setPropertyAsync("video-rotate", (root.mpv.getProperty("video-rotate") + 270) % 360) } property Action toggleFullscreen: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Toggle Fullscreen") icon.name: "view-fullscreen" shortcut: "F" onTriggered: root.fullscreenToggleRequested() } property Action exitFullscreen: Action { // Only while full screen, so Esc still reaches everything else. enabled: root.shortcutsEnabled && root.fullscreen text: KI18n.i18nc("@action", "Exit Fullscreen") shortcut: "Escape" onTriggered: root.fullscreenExitRequested() } property Action togglePlaylist: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Playlist") icon.name: "view-media-playlist" shortcut: "P" onTriggered: root.playlistToggleRequested() } property Action toggleMenuBar: Action { enabled: root.shortcutsEnabled text: QtPotatoSettings.showMenuBar ? KI18n.i18nc("@action", "Hide Menubar") : KI18n.i18nc("@action", "Show Menubar") icon.name: "show-menu" shortcut: "Ctrl+M" onTriggered: root.menuBarToggleRequested() } property Action toggleHeader: Action { enabled: root.shortcutsEnabled text: QtPotatoSettings.showHeader ? KI18n.i18nc("@action", "Hide Toolbar") : KI18n.i18nc("@action", "Show Toolbar") icon.name: "configure-toolbars" shortcut: "Ctrl+H" onTriggered: root.headerToggleRequested() } property Action openContextMenu: Action { enabled: root.shortcutsEnabled text: KI18n.i18nc("@action", "Open Context Menu") icon.name: "application-menu" shortcut: "Menu" onTriggered: root.contextMenuRequested() } property Action configure: Action { text: KI18n.i18nc("@action", "Configure QtPotato…") icon.name: "configure" shortcut: "Ctrl+Shift+," onTriggered: root.settingsRequested("general") } property Action about: Action { text: KI18n.i18nc("@action", "About QtPotato") icon.name: "help-about" onTriggered: root.settingsRequested("about") } property Action reportBug: Action { text: KI18n.i18nc("@action", "Report Bug") icon.name: "tools-report-bug" onTriggered: Qt.openUrlExternally("https://github.com/vokhactri/QtPotato/issues") } }