feat: rebuild the interface after Haruna
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
54ad4eecf3
commit
614a31db1b
45 files changed
+3818
-798
No files matched your search
+35
-3
@@ -57,10 +57,42 @@ ecm_add_qml_module(qtpotato
|
||||
)
|
||||
|
||||
ecm_target_qml_sources(qtpotato SOURCES
|
||||
qml/Actions.qml
|
||||
qml/AudioMenu.qml
|
||||
qml/AudioTracksMenu.qml
|
||||
qml/FileMenu.qml
|
||||
qml/Footer.qml
|
||||
qml/GeneralSettings.qml
|
||||
qml/HamburgerMenu.qml
|
||||
qml/Header.qml
|
||||
qml/HelpMenu.qml
|
||||
qml/InputPopup.qml
|
||||
qml/LabelWithTooltip.qml
|
||||
qml/Main.qml
|
||||
qml/PlayerPage.qml
|
||||
qml/SubtitlePopup.qml
|
||||
qml/SettingsPage.qml
|
||||
qml/MainMenuBar.qml
|
||||
qml/MpvContextMenu.qml
|
||||
qml/MpvVideo.qml
|
||||
qml/Osd.qml
|
||||
qml/PlaybackMenu.qml
|
||||
qml/PlaybackSettings.qml
|
||||
qml/Playlist.qml
|
||||
qml/PlaylistItem.qml
|
||||
qml/PlaylistSettings.qml
|
||||
qml/RecentFilesMenu.qml
|
||||
qml/ResizeHandler.qml
|
||||
qml/SeekBar.qml
|
||||
qml/SettingsBasePage.qml
|
||||
qml/SettingsHeader.qml
|
||||
qml/SettingsMenu.qml
|
||||
qml/SettingsWindow.qml
|
||||
qml/SubtitleTracksMenu.qml
|
||||
qml/SubtitlesMenu.qml
|
||||
qml/SubtitlesSettings.qml
|
||||
qml/ToolTipButton.qml
|
||||
qml/TranslationSettings.qml
|
||||
qml/VideoMenu.qml
|
||||
qml/ViewMenu.qml
|
||||
qml/VolumeSlider.qml
|
||||
)
|
||||
|
||||
target_include_directories(qtpotato PRIVATE models mpris mpv playlist translate)
|
||||
|
||||
@@ -42,6 +42,9 @@ int main(int argc, char *argv[])
|
||||
i18n("© 2026 Tri Vo Khac"));
|
||||
aboutData.addAuthor(i18n("Tri Vo Khac"), i18nc("@info:credit", "Author"), QString(), u"https://github.com/vokhactri"_s);
|
||||
aboutData.setDesktopFileName(u"dev.trivk.qtpotato"_s);
|
||||
aboutData.setProgramLogo(u"multimedia-video-player"_s);
|
||||
aboutData.setHomepage(u"https://github.com/vokhactri/QtPotato"_s);
|
||||
aboutData.setBugAddress("https://github.com/vokhactri/QtPotato/issues");
|
||||
aboutData.setOrganizationDomain("trivk.dev");
|
||||
KAboutData::setApplicationData(aboutData);
|
||||
QApplication::setWindowIcon(QIcon::fromTheme(u"multimedia-video-player"_s));
|
||||
|
||||
@@ -327,11 +327,6 @@ void MpvItem::showOnlySubtitle(int id)
|
||||
setSecondarySubtitleId(0);
|
||||
}
|
||||
|
||||
void MpvItem::showText(const QString &text)
|
||||
{
|
||||
commandAsync(QStringList{QStringLiteral("show-text"), text});
|
||||
}
|
||||
|
||||
bool MpvItem::loadUserConfig() const
|
||||
{
|
||||
return m_loadUserConfig;
|
||||
|
||||
@@ -102,8 +102,6 @@ public:
|
||||
// Makes \a url the only subtitle shown, adding it first (named \a title) unless it is
|
||||
// already loaded.
|
||||
Q_INVOKABLE void showTranslatedSubtitleFile(const QUrl &url, const QString &title);
|
||||
// Shows a short message on mpv's on-screen display.
|
||||
Q_INVOKABLE void showText(const QString &text);
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentUrlChanged();
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// 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
|
||||
|
||||
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()
|
||||
|
||||
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: "F5"
|
||||
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")
|
||||
shortcut: "Right"
|
||||
onTriggered: root.seekBy(QtPotatoSettings.seekSmallStep)
|
||||
}
|
||||
|
||||
property Action seekBackwardSmall: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Seek Small Step Backward")
|
||||
shortcut: "Left"
|
||||
onTriggered: root.seekBy(-QtPotatoSettings.seekSmallStep)
|
||||
}
|
||||
|
||||
property Action seekForwardMedium: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Seek Medium Step 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")
|
||||
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")
|
||||
shortcut: "Up"
|
||||
onTriggered: root.seekBy(QtPotatoSettings.seekBigStep)
|
||||
}
|
||||
|
||||
property Action seekBackwardBig: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Seek Big Step Backward")
|
||||
shortcut: "Down"
|
||||
onTriggered: root.seekBy(-QtPotatoSettings.seekBigStep)
|
||||
}
|
||||
|
||||
property Action seekNextSubtitle: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Seek To Next Subtitle")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
shortcut: "."
|
||||
onTriggered: root.mpv.frameStep(true)
|
||||
}
|
||||
|
||||
property Action frameStepBackward: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Step Frame Backward")
|
||||
shortcut: ","
|
||||
onTriggered: root.mpv.frameStep(false)
|
||||
}
|
||||
|
||||
property Action speedUp: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Increase Playback Speed")
|
||||
shortcut: "]"
|
||||
onTriggered: root.changeSpeed(0.1)
|
||||
}
|
||||
|
||||
property Action speedDown: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Decrease Playback Speed")
|
||||
shortcut: "["
|
||||
onTriggered: root.changeSpeed(-0.1)
|
||||
}
|
||||
|
||||
property Action speedReset: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Reset Playback Speed")
|
||||
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")
|
||||
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")
|
||||
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", "Toggle Subtitles")
|
||||
shortcut: "Ctrl+S"
|
||||
onTriggered: {
|
||||
const visible = root.mpv.getProperty("sub-visibility")
|
||||
root.mpv.setPropertyAsync("sub-visibility", !visible)
|
||||
root.mpv.setPropertyAsync("secondary-sub-visibility", !visible)
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
shortcut: "Z"
|
||||
onTriggered: root.changeSubtitleDelay(-0.1)
|
||||
}
|
||||
|
||||
property Action subtitleLater: Action {
|
||||
enabled: root.shortcutsEnabled && root.hasFile
|
||||
text: KI18n.i18nc("@action", "Show Subtitles Later")
|
||||
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")
|
||||
onTriggered: root.changeSubtitleDelay(-root.mpv.subtitleDelay)
|
||||
}
|
||||
|
||||
property Action subtitleBigger: Action {
|
||||
enabled: root.shortcutsEnabled
|
||||
text: KI18n.i18nc("@action", "Increase Subtitle Font Size")
|
||||
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")
|
||||
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 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", "Toggle Deinterlacing")
|
||||
shortcut: "D"
|
||||
onTriggered: {
|
||||
const enabled = root.mpv.getProperty("deinterlace") === "yes"
|
||||
root.mpv.setPropertyAsync("deinterlace", enabled ? "no" : "yes")
|
||||
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")
|
||||
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", "Settings")
|
||||
icon.name: "configure"
|
||||
shortcut: "Ctrl+Shift+,"
|
||||
onTriggered: root.settingsRequested("general")
|
||||
}
|
||||
|
||||
property Action about: Action {
|
||||
text: KI18n.i18nc("@action", "About QtPotato")
|
||||
icon.name: "help-about"
|
||||
shortcut: "F1"
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
required property MpvItem mpv
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "&Audio")
|
||||
|
||||
AudioTracksMenu {
|
||||
mpv: root.mpv
|
||||
title: KI18n.i18nc("@title:menu", "&Track")
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.mute }
|
||||
MenuItem { action: root.actions.volumeUp }
|
||||
MenuItem { action: root.actions.volumeDown }
|
||||
MenuItem { action: root.actions.audioCycleUp }
|
||||
MenuItem { action: root.actions.audioCycleDown }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
// Checkable list of audio tracks.
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property MpvItem mpv
|
||||
|
||||
function trackLabel(track: var): string {
|
||||
const lang = track.lang ? `[${track.lang}]` : ""
|
||||
const description = [track.title, lang, track.codec].filter(part => part && part.length > 0).join(" ")
|
||||
return KI18n.i18nc("@item:inmenu audio track number and description", "%1: %2",
|
||||
track.id, description || KI18n.i18nc("@item:inmenu", "Untitled"))
|
||||
}
|
||||
|
||||
enabled: mpv.audioTracks.length > 0
|
||||
|
||||
Instantiator {
|
||||
model: root.mpv.audioTracks
|
||||
delegate: MenuItem {
|
||||
required property var modelData
|
||||
text: root.trackLabel(modelData)
|
||||
checkable: true
|
||||
checked: modelData.id === root.mpv.audioId
|
||||
onTriggered: root.mpv.audioId = modelData.id
|
||||
}
|
||||
onObjectAdded: (index, object) => root.insertItem(index, object)
|
||||
onObjectRemoved: (index, object) => root.removeItem(object)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "&File")
|
||||
|
||||
MenuItem { action: root.actions.openFile }
|
||||
MenuItem { action: root.actions.openUrl }
|
||||
RecentFilesMenu { actions: root.actions }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.quit }
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// Playback controls below the video. In the "floating" style, and in full screen, they
|
||||
// sit over the video and show up when the pointer moves near the bottom.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
required property MpvVideo mpv
|
||||
property bool fullscreen: false
|
||||
// The window's header and menu bar are hidden, so the footer offers their menu.
|
||||
property bool showMenuButton: false
|
||||
|
||||
readonly property bool isFloating: QtPotatoSettings.footerStyle === "floating"
|
||||
// Takes space below the video (instead of covering it).
|
||||
readonly property bool isDocked: !isFloating && !fullscreen
|
||||
readonly property bool isSmallSize: width < 600
|
||||
readonly property int buttonSize: isSmallSize ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium
|
||||
readonly property bool busy: footer.hovered || hamburgerMenu.isOpen || seekBar.chaptersPopupOpen || seekBar.pressed
|
||||
property bool shown: true
|
||||
|
||||
implicitHeight: isFloating
|
||||
? footer.height + Kirigami.Units.largeSpacing * (isSmallSize ? 1 : 2)
|
||||
: footer.height
|
||||
|
||||
function formatTime(totalSeconds: real): string {
|
||||
return seekBar.formatTime(totalSeconds)
|
||||
}
|
||||
|
||||
// Called when the pointer moves over the video.
|
||||
function pointerMoved(y: real) {
|
||||
if (root.isDocked) {
|
||||
return
|
||||
}
|
||||
const nearBottom = y > root.mpv.height - footer.height * 2
|
||||
if (root.isFloating) {
|
||||
// Every movement reveals the floating footer for a moment; it stays while
|
||||
// the pointer is near it.
|
||||
root.shown = true
|
||||
if (nearBottom) {
|
||||
hideTimer.stop()
|
||||
} else {
|
||||
hideTimer.restart()
|
||||
}
|
||||
return
|
||||
}
|
||||
// Docked style in full screen: only near the bottom edge.
|
||||
root.shown = nearBottom || root.busy
|
||||
}
|
||||
|
||||
function pointerLeft() {
|
||||
if (!root.isDocked) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onIsDockedChanged: {
|
||||
hideTimer.stop()
|
||||
shown = isDocked || !mpv.fileLoaded
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.mpv
|
||||
function onFileLoadedChanged() {
|
||||
if (!root.mpv.fileLoaded) {
|
||||
root.shown = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 1000
|
||||
onTriggered: {
|
||||
if (root.isDocked || root.busy || !root.mpv.fileLoaded) {
|
||||
return
|
||||
}
|
||||
root.shown = false
|
||||
}
|
||||
}
|
||||
|
||||
ToolBar {
|
||||
id: footer
|
||||
|
||||
x: (root.width - width) / 2
|
||||
width: root.isFloating
|
||||
? Math.min(root.width - Kirigami.Units.largeSpacing * (root.isSmallSize ? 2 : 4), 900)
|
||||
: root.width
|
||||
state: root.shown ? "visible" : "hidden"
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
position: ToolBar.Footer
|
||||
hoverEnabled: true
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.alpha(Kirigami.Theme.backgroundColor, root.isDocked ? 1.0 : 0.8)
|
||||
radius: root.isFloating ? Kirigami.Units.cornerRadius : 0
|
||||
}
|
||||
|
||||
onHoveredChanged: {
|
||||
if (!hovered) {
|
||||
root.pointerLeft()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
spacing: root.isSmallSize ? 1 : Kirigami.Units.smallSpacing
|
||||
|
||||
HamburgerMenu {
|
||||
id: hamburgerMenu
|
||||
actions: root.actions
|
||||
mpv: root.mpv
|
||||
inFooter: true
|
||||
visible: root.showMenuButton
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: playPauseButton
|
||||
action: root.actions.playPause
|
||||
display: AbstractButton.IconOnly
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
ToolTip.text: root.mpv.paused ? KI18n.i18nc("@info:tooltip", "Start playback") : KI18n.i18nc("@info:tooltip", "Pause playback")
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
action: root.actions.playPreviousFile
|
||||
icon.name: footer.LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward"
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: AbstractButton.IconOnly
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
ToolTip.text: KI18n.i18nc("@info:tooltip", "Play previous file")
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
action: root.actions.playNextFile
|
||||
icon.name: footer.LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward"
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: AbstractButton.IconOnly
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
ToolTip.text: KI18n.i18nc("@info:tooltip", "Play next file")
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
SeekBar {
|
||||
id: seekBar
|
||||
mpv: root.mpv
|
||||
actions: root.actions
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
action: root.actions.mute
|
||||
icon.name: root.mpv.mute || root.mpv.volume === 0 ? "player-volume-muted" : "player-volume"
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: AbstractButton.IconOnly
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
ToolTip.text: root.actions.mute.text
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
VolumeSlider {
|
||||
mpv: root.mpv
|
||||
Layout.preferredWidth: root.isSmallSize ? 60 : 100
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
action: root.actions.togglePlaylist
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: AbstractButton.IconOnly
|
||||
focusPolicy: Qt.NoFocus
|
||||
visible: !QtPotatoSettings.playlistToggleWithMouse
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
LabelWithTooltip {
|
||||
text: root.mpv.mediaTitle
|
||||
font.pointSize: Kirigami.Theme.smallFont.pointSize
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
LabelWithTooltip {
|
||||
id: timeInfo
|
||||
text: "%1 / %2".arg(root.formatTime(root.mpv.position)).arg(root.formatTime(root.mpv.duration))
|
||||
font.pointSize: Kirigami.Theme.smallFont.pointSize
|
||||
font.features: { "tnum": 1 }
|
||||
toolTipText: KI18n.i18nc("@info:tooltip", "Remaining: %1", root.formatTime(root.mpv.duration - root.mpv.position))
|
||||
alwaysShowToolTip: true
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
Layout.minimumWidth: textMetrics.width
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
font: timeInfo.font
|
||||
text: "000:00:00 / 000:00:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "hidden"
|
||||
PropertyChanges {
|
||||
footer.y: root.height
|
||||
footer.visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "visible"
|
||||
PropertyChanges {
|
||||
footer.y: 0
|
||||
footer.visible: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: "hidden"
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
target: footer
|
||||
property: "y"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
PropertyAction {
|
||||
target: footer
|
||||
property: "visible"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "hidden"
|
||||
to: "visible"
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
target: footer
|
||||
property: "visible"
|
||||
value: true
|
||||
}
|
||||
NumberAnimation {
|
||||
target: footer
|
||||
property: "y"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
SettingsBasePage {
|
||||
id: root
|
||||
|
||||
required property MpvItem mpv
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
|
||||
SettingsHeader {
|
||||
text: KI18n.i18nc("@title", "Interface")
|
||||
topMargin: 0
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:listbox", "Footer style")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ComboBox {
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ text: KI18n.i18nc("@item:inlistbox footer style", "Default"), value: "default" },
|
||||
{ text: KI18n.i18nc("@item:inlistbox footer style", "Floating"), value: "floating" }
|
||||
]
|
||||
Component.onCompleted: currentIndex = indexOfValue(QtPotatoSettings.footerStyle)
|
||||
onActivated: {
|
||||
QtPotatoSettings.footerStyle = currentValue
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTipButton {
|
||||
toolTipText: KI18n.i18nc("@info:tooltip", "Default: the controls sit below the video.<br>Floating: the controls float over the video and hide when the mouse rests.")
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
CheckBox {
|
||||
text: KI18n.i18nc("@option:check", "Show menu bar")
|
||||
checked: QtPotatoSettings.showMenuBar
|
||||
onClicked: {
|
||||
QtPotatoSettings.showMenuBar = checked
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
CheckBox {
|
||||
text: KI18n.i18nc("@option:check", "Show toolbar")
|
||||
checked: QtPotatoSettings.showHeader
|
||||
onClicked: {
|
||||
QtPotatoSettings.showHeader = checked
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:spinbox", "Maximum recent files")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
SpinBox {
|
||||
from: 0
|
||||
to: 50
|
||||
value: QtPotatoSettings.maxRecentFiles
|
||||
onValueModified: {
|
||||
QtPotatoSettings.maxRecentFiles = value
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTipButton {
|
||||
toolTipText: KI18n.i18nc("@info:tooltip", "The number of recent files to list. Enter 0 (zero) to hide the list.")
|
||||
}
|
||||
}
|
||||
|
||||
SettingsHeader {
|
||||
text: KI18n.i18nc("@title", "mpv")
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
RowLayout {
|
||||
CheckBox {
|
||||
text: KI18n.i18nc("@option:check", "Use mpv.conf")
|
||||
checked: QtPotatoSettings.loadMpvConfig
|
||||
onClicked: {
|
||||
QtPotatoSettings.loadMpvConfig = checked
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTipButton {
|
||||
toolTipText: KI18n.i18nc("@info:tooltip", "Reads mpv.conf from %1, like the mpv player does. Changes apply the next time QtPotato starts. The subtitle size and outline from the Subtitles page take precedence.", root.mpv.userConfigDir)
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
Label {
|
||||
text: root.mpv.loadedUserConfigFiles.length > 0
|
||||
? KI18n.i18nc("@info", "Loaded: %1", root.mpv.loadedUserConfigFiles.join(", "))
|
||||
: (QtPotatoSettings.loadMpvConfig
|
||||
? KI18n.i18nc("@info", "No mpv.conf found")
|
||||
: KI18n.i18nc("@info", "mpv.conf is not used"))
|
||||
wrapMode: Text.WordWrap
|
||||
opacity: 0.7
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 1
|
||||
visible: root.mpv.skippedUserOptions.length > 0
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: root.mpv.skippedUserOptions.length > 0
|
||||
text: KI18n.i18nc("@info", "Ignored because they would break the built-in player: %1", root.mpv.skippedUserOptions.join(", "))
|
||||
wrapMode: Text.WordWrap
|
||||
opacity: 0.7
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// The application menu button, used when the menu bar is hidden.
|
||||
ToolButton {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
required property MpvItem mpv
|
||||
// In the footer the menu opens upwards and also offers what the header would.
|
||||
property bool inFooter: false
|
||||
readonly property alias isOpen: menu.visible
|
||||
|
||||
icon.name: "application-menu"
|
||||
text: KI18n.i18nc("@action:button", "Main Menu")
|
||||
display: AbstractButton.IconOnly
|
||||
focusPolicy: Qt.NoFocus
|
||||
down: menu.visible
|
||||
// The menu is modal, so a click outside it (also on this button) only closes it.
|
||||
onPressed: menu.open()
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered && !menu.visible
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
Menu {
|
||||
id: menu
|
||||
|
||||
y: root.inFooter ? -height - Kirigami.Units.largeSpacing : root.height + Kirigami.Units.largeSpacing
|
||||
modal: true
|
||||
dim: false
|
||||
|
||||
MenuItem {
|
||||
action: root.actions.openFile
|
||||
visible: root.inFooter
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
MenuItem {
|
||||
action: root.actions.openUrl
|
||||
visible: root.inFooter
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
RecentFilesMenu { actions: root.actions }
|
||||
SubtitleTracksMenu {
|
||||
mpv: root.mpv
|
||||
isPrimary: true
|
||||
title: KI18n.i18nc("@title:menu", "Primary Subtitle")
|
||||
}
|
||||
SubtitleTracksMenu {
|
||||
mpv: root.mpv
|
||||
isPrimary: false
|
||||
title: KI18n.i18nc("@title:menu", "Secondary Subtitle")
|
||||
}
|
||||
AudioTracksMenu {
|
||||
mpv: root.mpv
|
||||
title: KI18n.i18nc("@title:menu", "Audio Track")
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.translateSubtitle }
|
||||
MenuItem { action: root.actions.toggleFullscreen }
|
||||
MenuItem { action: root.actions.togglePlaylist }
|
||||
MenuItem { action: root.actions.screenshot }
|
||||
MenuSeparator {}
|
||||
MenuItem {
|
||||
action: root.actions.configure
|
||||
visible: root.inFooter
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
MenuItem { action: root.actions.toggleMenuBar }
|
||||
MenuItem { action: root.actions.about }
|
||||
MenuSeparator {}
|
||||
Menu {
|
||||
title: KI18n.i18nc("@title:menu", "&More")
|
||||
FileMenu { actions: root.actions }
|
||||
ViewMenu { actions: root.actions }
|
||||
PlaybackMenu { actions: root.actions }
|
||||
VideoMenu { actions: root.actions }
|
||||
SubtitlesMenu {
|
||||
actions: root.actions
|
||||
mpv: root.mpv
|
||||
}
|
||||
AudioMenu {
|
||||
actions: root.actions
|
||||
mpv: root.mpv
|
||||
}
|
||||
SettingsMenu { actions: root.actions }
|
||||
HelpMenu { actions: root.actions }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// Toolbar above the video: open buttons, track menus, translation progress, settings.
|
||||
ToolBar {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
required property MpvItem mpv
|
||||
required property TranslationService translator
|
||||
// False in full screen and when the user hides the toolbar.
|
||||
property bool shown: true
|
||||
property bool menuBarShown: true
|
||||
|
||||
readonly property bool isSmallSize: width < 600
|
||||
readonly property int buttonSize: isSmallSize ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium
|
||||
|
||||
position: ToolBar.Header
|
||||
state: shown ? "visible" : "hidden"
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
|
||||
HamburgerMenu {
|
||||
actions: root.actions
|
||||
mpv: root.mpv
|
||||
visible: !root.menuBarShown
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
action: root.actions.openFile
|
||||
focusPolicy: Qt.NoFocus
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
action: root.actions.openUrl
|
||||
focusPolicy: Qt.NoFocus
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
|
||||
}
|
||||
|
||||
ToolSeparator {}
|
||||
|
||||
ToolButton {
|
||||
id: subtitleButton
|
||||
text: KI18n.i18nc("@action:intoolbar", "Subtitles")
|
||||
icon.name: "add-subtitle"
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
focusPolicy: Qt.NoFocus
|
||||
enabled: root.mpv.fileLoaded
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
|
||||
down: subtitleMenu.visible
|
||||
onPressed: subtitleMenu.open()
|
||||
|
||||
SubtitleTracksMenu {
|
||||
id: subtitleMenu
|
||||
mpv: root.mpv
|
||||
isPrimary: true
|
||||
y: parent.height
|
||||
modal: true
|
||||
dim: false
|
||||
// The track list is empty for files without subtitles, but a file can
|
||||
// still be added.
|
||||
enabled: true
|
||||
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.translateSubtitle }
|
||||
MenuItem { action: root.actions.addSubtitle }
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
text: KI18n.i18nc("@action:intoolbar", "Audio")
|
||||
icon.name: "audio-volume-high"
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
focusPolicy: Qt.NoFocus
|
||||
enabled: root.mpv.audioTracks.length > 0
|
||||
opacity: enabled ? 1.0 : 0.6
|
||||
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
|
||||
down: audioMenu.visible
|
||||
onPressed: audioMenu.open()
|
||||
|
||||
AudioTracksMenu {
|
||||
id: audioMenu
|
||||
mpv: root.mpv
|
||||
y: parent.height
|
||||
modal: true
|
||||
dim: false
|
||||
}
|
||||
}
|
||||
|
||||
// Translation progress, with a way to stop it.
|
||||
RowLayout {
|
||||
visible: root.translator.busy
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "translate"
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
from: 0
|
||||
to: 1
|
||||
value: root.translator.progress
|
||||
indeterminate: root.translator.progress === 0
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 6
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.translator.statusText
|
||||
elide: Text.ElideRight
|
||||
visible: !root.isSmallSize
|
||||
Layout.maximumWidth: Kirigami.Units.gridUnit * 14
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
action: root.actions.cancelTranslation
|
||||
display: AbstractButton.IconOnly
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
action: root.actions.configure
|
||||
focusPolicy: Qt.NoFocus
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "hidden"
|
||||
PropertyChanges {
|
||||
root.height: 0
|
||||
root.opacity: 0
|
||||
root.visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "visible"
|
||||
PropertyChanges {
|
||||
root.height: root.implicitHeight
|
||||
root.opacity: 1
|
||||
root.visible: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: "hidden"
|
||||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "opacity"
|
||||
duration: Kirigami.Units.veryShortDuration
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "height"
|
||||
duration: Kirigami.Units.veryShortDuration
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "visible"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "hidden"
|
||||
to: "visible"
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "visible"
|
||||
value: true
|
||||
}
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "height"
|
||||
duration: Kirigami.Units.veryShortDuration
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "opacity"
|
||||
duration: Kirigami.Units.veryShortDuration
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "&Help")
|
||||
|
||||
MenuItem { action: root.actions.about }
|
||||
MenuItem { action: root.actions.reportBug }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// Single-line text popup, used for Open URL.
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property string lastText: ""
|
||||
property string placeholderText: ""
|
||||
property string buttonText: ""
|
||||
|
||||
signal submitted(text: string)
|
||||
|
||||
modal: true
|
||||
|
||||
onOpened: {
|
||||
textField.text = root.lastText
|
||||
textField.forceActiveFocus(Qt.MouseFocusReason)
|
||||
textField.selectAll()
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
TextField {
|
||||
id: textField
|
||||
placeholderText: root.placeholderText
|
||||
inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
onAccepted: submitButton.clicked()
|
||||
Keys.onEscapePressed: root.close()
|
||||
}
|
||||
|
||||
Button {
|
||||
id: submitButton
|
||||
text: root.buttonText
|
||||
enabled: textField.text.trim().length > 0
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
onClicked: {
|
||||
root.submitted(textField.text.trim())
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// A label that shows its full text in a tooltip when it is cut off.
|
||||
Label {
|
||||
id: root
|
||||
|
||||
property string toolTipText
|
||||
property bool alwaysShowToolTip: false
|
||||
|
||||
ToolTip {
|
||||
visible: hoverHandler.hovered && (root.alwaysShowToolTip || root.truncated)
|
||||
text: root.toolTipText || root.text
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
}
|
||||
+302
-31
@@ -1,44 +1,315 @@
|
||||
import QtQuick
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
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
|
||||
|
||||
width: Kirigami.Units.gridUnit * 50
|
||||
height: Kirigami.Units.gridUnit * 32
|
||||
minimumWidth: Kirigami.Units.gridUnit * 20
|
||||
minimumHeight: Kirigami.Units.gridUnit * 14
|
||||
property int previousVisibility: Window.Windowed
|
||||
readonly property bool isFullScreen: visibility === Window.FullScreen
|
||||
|
||||
title: player.mediaTitle.length > 0 ? player.mediaTitle : "QtPotato"
|
||||
|
||||
pageStack.initialPage: PlayerPage {
|
||||
id: player
|
||||
initialUrl: root.initialUrl
|
||||
fullScreen: root.visibility === Window.FullScreen
|
||||
onAboutRequested: root.pageStack.layers.push(aboutPage)
|
||||
onSettingsRequested: root.pageStack.layers.push(settingsPage)
|
||||
onFullScreenToggled: root.visibility = root.visibility === Window.FullScreen ? Window.Windowed : Window.FullScreen
|
||||
function openFile(url: url) {
|
||||
playlistModel.openFile(url, QtPotatoSettings.playlistLoadSiblings)
|
||||
}
|
||||
|
||||
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
|
||||
pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn
|
||||
|
||||
onClosing: QtPotatoSettings.save()
|
||||
|
||||
Component {
|
||||
id: settingsPage
|
||||
SettingsPage {
|
||||
translator: player.translator
|
||||
mpvItem: player.mpvItem
|
||||
function toggleFullScreen() {
|
||||
if (isFullScreen) {
|
||||
exitFullScreen()
|
||||
} else {
|
||||
showFullScreen()
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: aboutPage
|
||||
FormCard.AboutPage {}
|
||||
function exitFullScreen() {
|
||||
if (previousVisibility === Window.Maximized) {
|
||||
showMaximized()
|
||||
} else {
|
||||
showNormal()
|
||||
}
|
||||
}
|
||||
|
||||
function translateCurrentSubtitle() {
|
||||
const track = video.subtitleTracks.find(track => track.id === video.subtitleId)
|
||||
if (track) {
|
||||
translationService.translate(video.currentUrl, track, video.mediaTitle)
|
||||
}
|
||||
}
|
||||
|
||||
visible: true
|
||||
title: video.mediaTitle || KI18n.i18nc("@title:window", "QtPotato")
|
||||
width: 1000
|
||||
height: 600
|
||||
minimumWidth: 400
|
||||
minimumHeight: 200
|
||||
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()
|
||||
onMenuBarToggleRequested: {
|
||||
QtPotatoSettings.showMenuBar = !QtPotatoSettings.showMenuBar
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
onHeaderToggleRequested: {
|
||||
QtPotatoSettings.showHeader = !QtPotatoSettings.showHeader
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
PlaylistModel {
|
||||
id: playlistModel
|
||||
onPlayRequested: url => {
|
||||
video.loadFile(url)
|
||||
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,
|
||||
KI18n.i18nc("@item:inlistbox subtitle track, %1 is a language", "%1 (translated)", QtPotatoSettings.targetLanguage))
|
||||
osdItem.message(fromCache
|
||||
? KI18n.i18nc("@info:osd", "Loaded the saved translation")
|
||||
: KI18n.i18nc("@info:osd", "Translation ready"))
|
||||
}
|
||||
}
|
||||
|
||||
MpvVideo {
|
||||
id: video
|
||||
|
||||
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 - 50 : x < 50
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// A translation belongs to the file it was started for.
|
||||
onCurrentUrlChanged: translationService.cancel()
|
||||
|
||||
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
|
||||
x: Kirigami.Units.largeSpacing
|
||||
y: Kirigami.Units.largeSpacing
|
||||
width: Math.min(appWindow.width * 0.9, 600)
|
||||
lastText: QtPotatoSettings.lastUrl
|
||||
placeholderText: "https://"
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
MenuBar {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
required property MpvItem mpv
|
||||
|
||||
hoverEnabled: true
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Header
|
||||
|
||||
FileMenu { actions: root.actions }
|
||||
ViewMenu { actions: root.actions }
|
||||
PlaybackMenu { actions: root.actions }
|
||||
VideoMenu { actions: root.actions }
|
||||
SubtitlesMenu {
|
||||
actions: root.actions
|
||||
mpv: root.mpv
|
||||
}
|
||||
AudioMenu {
|
||||
actions: root.actions
|
||||
mpv: root.mpv
|
||||
}
|
||||
SettingsMenu { actions: root.actions }
|
||||
HelpMenu { actions: root.actions }
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
// Right click menu of the video.
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
required property MpvItem mpv
|
||||
|
||||
MenuItem { action: root.actions.playPause }
|
||||
MenuItem { action: root.actions.playNextFile }
|
||||
MenuItem { action: root.actions.playPreviousFile }
|
||||
Menu {
|
||||
title: KI18n.i18nc("@title:menu", "Playback Speed")
|
||||
MenuItem { action: root.actions.speedUp }
|
||||
MenuItem { action: root.actions.speedDown }
|
||||
MenuItem { action: root.actions.speedReset }
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.openFile }
|
||||
MenuItem { action: root.actions.openUrl }
|
||||
MenuSeparator {}
|
||||
SubtitleTracksMenu {
|
||||
mpv: root.mpv
|
||||
isPrimary: true
|
||||
title: KI18n.i18nc("@title:menu", "Primary Subtitle")
|
||||
}
|
||||
SubtitleTracksMenu {
|
||||
mpv: root.mpv
|
||||
isPrimary: false
|
||||
title: KI18n.i18nc("@title:menu", "Secondary Subtitle")
|
||||
}
|
||||
AudioTracksMenu {
|
||||
mpv: root.mpv
|
||||
title: KI18n.i18nc("@title:menu", "Audio Track")
|
||||
}
|
||||
MenuItem { action: root.actions.translateSubtitle }
|
||||
MenuSeparator {}
|
||||
VideoMenu { actions: root.actions }
|
||||
SubtitlesMenu {
|
||||
actions: root.actions
|
||||
mpv: root.mpv
|
||||
}
|
||||
AudioMenu {
|
||||
actions: root.actions
|
||||
mpv: root.mpv
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.toggleFullscreen }
|
||||
MenuItem { action: root.actions.togglePlaylist }
|
||||
MenuItem { action: root.actions.toggleMenuBar }
|
||||
MenuItem { action: root.actions.toggleHeader }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.configure }
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// The video, with Haruna's default mouse bindings and drag and drop.
|
||||
MpvItem {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
property bool fullscreen: false
|
||||
|
||||
signal pointerMoved(x: real, y: real)
|
||||
signal pointerLeft()
|
||||
// Files dropped on the top part are queued; on the bottom part they play now.
|
||||
signal filesDropped(urls: list<url>, append: bool)
|
||||
signal subtitleDropped(url: url)
|
||||
|
||||
function isSubtitleFile(url: url): bool {
|
||||
return /\.(srt|ass|ssa|vtt|sub|sup)$/i.test(url.toString())
|
||||
}
|
||||
|
||||
// Plain-text and translated subtitles; styled ASS subtitles keep their own fonts.
|
||||
function applySubtitleStyle() {
|
||||
setPropertyAsync("sub-font-size", QtPotatoSettings.subtitleFontSize)
|
||||
setPropertyAsync("sub-outline-size", QtPotatoSettings.subtitleOutlineSize)
|
||||
}
|
||||
|
||||
loadUserConfig: QtPotatoSettings.loadMpvConfig
|
||||
|
||||
// Set once the saved volume is applied; mpv reports its default volume before that.
|
||||
property bool volumeRestored: false
|
||||
|
||||
// QtPotato's own settings win over mpv.conf.
|
||||
onUserConfigApplied: {
|
||||
applySubtitleStyle()
|
||||
volume = QtPotatoSettings.volume
|
||||
volumeRestored = true
|
||||
}
|
||||
|
||||
onVolumeChanged: {
|
||||
if (volumeRestored) {
|
||||
QtPotatoSettings.volume = volume
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int subtitleFontSize: QtPotatoSettings.subtitleFontSize
|
||||
onSubtitleFontSizeChanged: applySubtitleStyle()
|
||||
readonly property int subtitleOutlineSize: QtPotatoSettings.subtitleOutlineSize
|
||||
onSubtitleOutlineSizeChanged: applySubtitleStyle()
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
visible: !root.fileLoaded
|
||||
z: -1
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Kirigami.Units.gridUnit * 4
|
||||
visible: !root.fileLoaded && !dropArea.containsDrag
|
||||
icon.name: "video-symbolic"
|
||||
text: KI18n.i18nc("@info:placeholder", "No video open")
|
||||
explanation: KI18n.i18nc("@info:placeholder", "Open a file or drop one here")
|
||||
helpfulAction: root.actions.openFile
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideCursorTimer
|
||||
running: root.fullscreen && mouseArea.containsMouse
|
||||
repeat: true
|
||||
interval: 2000
|
||||
onTriggered: mouseArea.hideCursor = true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
property bool hideCursor: false
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton | Qt.ForwardButton | Qt.BackButton
|
||||
hoverEnabled: true
|
||||
cursorShape: hideCursor && root.fullscreen ? Qt.BlankCursor : Qt.ArrowCursor
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
hideCursor = false
|
||||
hideCursorTimer.restart()
|
||||
root.pointerMoved(mouse.x, mouse.y)
|
||||
}
|
||||
onExited: root.pointerLeft()
|
||||
|
||||
onPressed: mouse => {
|
||||
switch (mouse.button) {
|
||||
case Qt.LeftButton:
|
||||
root.actions.playPause.trigger()
|
||||
break
|
||||
case Qt.MiddleButton:
|
||||
root.actions.mute.trigger()
|
||||
break
|
||||
case Qt.RightButton:
|
||||
root.actions.openContextMenu.trigger()
|
||||
break
|
||||
case Qt.ForwardButton:
|
||||
root.actions.seekForwardSmall.trigger()
|
||||
break
|
||||
case Qt.BackButton:
|
||||
root.actions.seekBackwardSmall.trigger()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Both presses of the double click toggled playback, so it is unchanged.
|
||||
root.actions.toggleFullscreen.trigger()
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
root.actions.configure.trigger()
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: wheel => {
|
||||
if (wheel.angleDelta.y > 0) {
|
||||
root.actions.volumeUp.trigger()
|
||||
} else if (wheel.angleDelta.y < 0) {
|
||||
root.actions.volumeDown.trigger()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: dropArea
|
||||
|
||||
readonly property bool onTopPart: containsDrag && drag.y <= height * 0.25
|
||||
property bool onlySubtitles: false
|
||||
|
||||
anchors.fill: parent
|
||||
keys: ["text/uri-list"]
|
||||
|
||||
onEntered: drag => {
|
||||
onlySubtitles = drag.urls.length > 0 && drag.urls.every(url => root.isSubtitleFile(url))
|
||||
}
|
||||
|
||||
onDropped: drop => {
|
||||
if (!drop.hasUrls) {
|
||||
return
|
||||
}
|
||||
if (onlySubtitles) {
|
||||
root.subtitleDropped(drop.urls[0])
|
||||
} else {
|
||||
root.filesDropped(drop.urls, onTopPart)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: dropArea.containsDrag && !dropArea.onlySubtitles ? 2 : 0
|
||||
delegate: Rectangle {
|
||||
id: dropZone
|
||||
|
||||
required property int index
|
||||
readonly property bool isTop: index === 0
|
||||
readonly property bool active: isTop === dropArea.onTopPart
|
||||
|
||||
x: Kirigami.Units.smallSpacing
|
||||
y: isTop ? Kirigami.Units.smallSpacing : dropArea.height * 0.25 + Kirigami.Units.smallSpacing
|
||||
width: dropArea.width - Kirigami.Units.smallSpacing * 2
|
||||
height: (isTop ? dropArea.height * 0.25 : dropArea.height * 0.75) - Kirigami.Units.smallSpacing * 2
|
||||
color: "transparent"
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border.color: active ? Kirigami.Theme.activeTextColor : "transparent"
|
||||
border.width: active ? 3 : 0
|
||||
|
||||
Label {
|
||||
anchors.fill: parent
|
||||
text: dropZone.isTop
|
||||
? KI18n.i18nc("@info:drag&drop append items", "Add to Playlist")
|
||||
: KI18n.i18nc("@info:drag&drop play now", "Play Now")
|
||||
elide: Text.ElideRight
|
||||
color: "white"
|
||||
style: Text.Outline
|
||||
styleColor: "black"
|
||||
opacity: dropZone.active ? 1 : 0.7
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * (dropZone.isTop ? 1.6 : 2.2)
|
||||
font.bold: true
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// Short status messages in the top left corner of the video.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property int maxWidth
|
||||
|
||||
function message(text: string) {
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
label.text = text
|
||||
label.visible = true
|
||||
timer.restart()
|
||||
}
|
||||
|
||||
Label {
|
||||
id: label
|
||||
|
||||
property int textWidth: Math.ceil(textMetrics.advanceWidth) + padding * 2
|
||||
|
||||
x: Kirigami.Units.largeSpacing
|
||||
y: Kirigami.Units.largeSpacing
|
||||
width: textWidth > root.maxWidth ? root.maxWidth : undefined
|
||||
visible: false
|
||||
background: Rectangle {
|
||||
color: Qt.alpha(Kirigami.Theme.backgroundColor, 0.8)
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.2
|
||||
maximumLineCount: 5
|
||||
wrapMode: Text.WrapAnywhere
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
font: label.font
|
||||
text: label.text
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
interval: 3000
|
||||
onTriggered: label.visible = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "&Playback")
|
||||
|
||||
MenuItem { action: root.actions.playPause }
|
||||
MenuItem { action: root.actions.stop }
|
||||
MenuItem { action: root.actions.playNextFile }
|
||||
MenuItem { action: root.actions.playPreviousFile }
|
||||
MenuItem { action: root.actions.restart }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.speedUp }
|
||||
MenuItem { action: root.actions.speedDown }
|
||||
MenuItem { action: root.actions.speedReset }
|
||||
MenuSeparator {}
|
||||
Menu {
|
||||
title: KI18n.i18nc("@title:menu", "Seek")
|
||||
MenuItem { action: root.actions.seekForwardSmall }
|
||||
MenuItem { action: root.actions.seekBackwardSmall }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.seekForwardMedium }
|
||||
MenuItem { action: root.actions.seekBackwardMedium }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.seekForwardBig }
|
||||
MenuItem { action: root.actions.seekBackwardBig }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.seekNextSubtitle }
|
||||
MenuItem { action: root.actions.seekPreviousSubtitle }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.seekNextChapter }
|
||||
MenuItem { action: root.actions.seekPreviousChapter }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.frameStepForward }
|
||||
MenuItem { action: root.actions.frameStepBackward }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
|
||||
SettingsBasePage {
|
||||
id: root
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
|
||||
SettingsHeader {
|
||||
text: KI18n.i18nc("@title", "Seeking")
|
||||
topMargin: 0
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:spinbox", "Small step")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
from: 1
|
||||
to: 600
|
||||
value: QtPotatoSettings.seekSmallStep
|
||||
textFromValue: (value, locale) => KI18n.i18ncp("@item:valuesuffix", "%1 second", "%1 seconds", value)
|
||||
valueFromText: (text, locale) => parseInt(text) || 1
|
||||
onValueModified: {
|
||||
QtPotatoSettings.seekSmallStep = value
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:spinbox", "Medium step")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
from: 1
|
||||
to: 600
|
||||
value: QtPotatoSettings.seekMediumStep
|
||||
textFromValue: (value, locale) => KI18n.i18ncp("@item:valuesuffix", "%1 second", "%1 seconds", value)
|
||||
valueFromText: (text, locale) => parseInt(text) || 1
|
||||
onValueModified: {
|
||||
QtPotatoSettings.seekMediumStep = value
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:spinbox", "Big step")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
from: 1
|
||||
to: 600
|
||||
value: QtPotatoSettings.seekBigStep
|
||||
textFromValue: (value, locale) => KI18n.i18ncp("@item:valuesuffix", "%1 second", "%1 seconds", value)
|
||||
valueFromText: (text, locale) => parseInt(text) || 1
|
||||
onValueModified: {
|
||||
QtPotatoSettings.seekBigStep = value
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
SettingsHeader {
|
||||
text: KI18n.i18nc("@title", "Audio")
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:spinbox", "Volume step")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
from: 1
|
||||
to: 100
|
||||
value: QtPotatoSettings.volumeStep
|
||||
onValueModified: {
|
||||
QtPotatoSettings.volumeStep = value
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.ki18n
|
||||
|
||||
Kirigami.Page {
|
||||
id: page
|
||||
|
||||
property url initialUrl
|
||||
// Set by the window. In full screen the bars float over the video and hide when the
|
||||
// pointer rests.
|
||||
property bool fullScreen: false
|
||||
readonly property alias mediaTitle: mpv.mediaTitle
|
||||
readonly property alias translator: translator
|
||||
readonly property MpvItem mpvItem: mpv
|
||||
|
||||
signal aboutRequested()
|
||||
signal settingsRequested()
|
||||
signal fullScreenToggled()
|
||||
|
||||
title: KI18n.i18nc("@title", "Player")
|
||||
padding: 0
|
||||
globalToolBarStyle: fullScreen ? Kirigami.ApplicationHeaderStyle.None : Kirigami.ApplicationHeaderStyle.ToolBar
|
||||
|
||||
// Kept true for a moment after the pointer moves.
|
||||
property bool pointerActive: false
|
||||
readonly property bool controlsVisible: !fullScreen || pointerActive || topBarHover.hovered || footerHover.hovered
|
||||
|| subtitlePopup.visible || seekBar.pressed || !mpv.fileLoaded || menuOpen
|
||||
// Set while the full screen "More" menu is open, so the bars stay.
|
||||
property bool menuOpen: false
|
||||
|
||||
onFullScreenChanged: page.revealControls()
|
||||
|
||||
function revealControls() {
|
||||
pointerActive = true
|
||||
idleTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: idleTimer
|
||||
interval: 2500
|
||||
onTriggered: page.pointerActive = false
|
||||
}
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
icon.name: "document-open"
|
||||
text: KI18n.i18nc("@action", "Open File…")
|
||||
shortcut: StandardKey.Open
|
||||
onTriggered: fileDialog.open()
|
||||
},
|
||||
Kirigami.Action {
|
||||
// F and Esc are handled by the Shortcuts below, which work while the bars are hidden.
|
||||
icon.name: page.fullScreen ? "view-restore" : "view-fullscreen"
|
||||
text: page.fullScreen ? KI18n.i18nc("@action", "Exit Full Screen") : KI18n.i18nc("@action", "Full Screen")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
onTriggered: page.fullScreenToggled()
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "configure"
|
||||
text: KI18n.i18nc("@action", "Settings")
|
||||
displayHint: Kirigami.DisplayHint.AlwaysHide
|
||||
shortcut: StandardKey.Preferences
|
||||
onTriggered: page.settingsRequested()
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "help-about"
|
||||
text: KI18n.i18nc("@action", "About QtPotato")
|
||||
displayHint: Kirigami.DisplayHint.AlwaysHide
|
||||
onTriggered: page.aboutRequested()
|
||||
}
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
page.applySubtitleStyle()
|
||||
if (page.initialUrl.toString().length > 0) {
|
||||
mpv.loadFile(page.initialUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Plain-text and translated subtitles; styled ASS subtitles keep their own fonts.
|
||||
function applySubtitleStyle() {
|
||||
mpv.setPropertyAsync("sub-font-size", QtPotatoSettings.subtitleFontSize)
|
||||
mpv.setPropertyAsync("sub-outline-size", QtPotatoSettings.subtitleOutlineSize)
|
||||
}
|
||||
|
||||
readonly property int subtitleFontSize: QtPotatoSettings.subtitleFontSize
|
||||
onSubtitleFontSizeChanged: applySubtitleStyle()
|
||||
readonly property int subtitleOutlineSize: QtPotatoSettings.subtitleOutlineSize
|
||||
onSubtitleOutlineSizeChanged: applySubtitleStyle()
|
||||
|
||||
function formatTime(totalSeconds: real): string {
|
||||
const s = Math.max(0, Math.floor(totalSeconds))
|
||||
const h = Math.floor(s / 3600)
|
||||
const m = Math.floor((s % 3600) / 60)
|
||||
const sec = String(s % 60).padStart(2, "0")
|
||||
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${sec}` : `${m}:${sec}`
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Space"
|
||||
onActivated: mpv.togglePause()
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "F"
|
||||
onActivated: page.fullScreenToggled()
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "Escape"
|
||||
enabled: page.fullScreen
|
||||
onActivated: page.fullScreenToggled()
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "Left"
|
||||
onActivated: mpv.seek(mpv.position - 5)
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "Right"
|
||||
onActivated: mpv.seek(mpv.position + 5)
|
||||
}
|
||||
// Same keys as mpv: z shows subtitles earlier, x later.
|
||||
Shortcut {
|
||||
sequence: "Z"
|
||||
enabled: mpv.fileLoaded
|
||||
onActivated: page.nudgeSubtitleDelay(-0.1)
|
||||
}
|
||||
Shortcut {
|
||||
sequence: "X"
|
||||
enabled: mpv.fileLoaded
|
||||
onActivated: page.nudgeSubtitleDelay(0.1)
|
||||
}
|
||||
|
||||
function nudgeSubtitleDelay(seconds: real) {
|
||||
const delay = mpv.subtitleDelay + seconds
|
||||
mpv.subtitleDelay = delay
|
||||
mpv.showText(KI18n.i18nc("@info:osd", "Subtitle delay: %1 ms", Math.round(delay * 1000)))
|
||||
}
|
||||
|
||||
TranslationService {
|
||||
id: translator
|
||||
onTranslated: (subtitleUrl, fromCache) => {
|
||||
mpv.showTranslatedSubtitleFile(subtitleUrl,
|
||||
KI18n.i18nc("@item:inlistbox subtitle track, %1 is a language", "%1 (translated)", QtPotatoSettings.targetLanguage))
|
||||
mpv.showText(fromCache
|
||||
? KI18n.i18nc("@info:osd", "Loaded the saved translation")
|
||||
: KI18n.i18nc("@info:osd", "Translation ready"))
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: mpv
|
||||
// A translation belongs to the file it was started for.
|
||||
function onCurrentUrlChanged() {
|
||||
translator.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
function isSubtitleFile(url: url): bool {
|
||||
return /\.(srt|ass|ssa|vtt|sub|sup)$/i.test(url.toString())
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
title: KI18n.i18nc("@title:window", "Open Video")
|
||||
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: mpv.loadFile(selectedFile)
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
MpvItem {
|
||||
id: mpv
|
||||
loadUserConfig: QtPotatoSettings.loadMpvConfig
|
||||
// QtPotato's own subtitle settings win over mpv.conf.
|
||||
onUserConfigApplied: page.applySubtitleStyle()
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
bottom: page.fullScreen ? parent.bottom : footer.top
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
visible: !mpv.fileLoaded
|
||||
z: -1
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Kirigami.Units.gridUnit * 4
|
||||
visible: !mpv.fileLoaded
|
||||
icon.name: "video-symbolic"
|
||||
text: KI18n.i18nc("@info:placeholder", "No video open")
|
||||
explanation: KI18n.i18nc("@info:placeholder", "Open a file or drop one here")
|
||||
helpfulAction: page.actions[0]
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: page.controlsVisible ? Qt.ArrowCursor : Qt.BlankCursor
|
||||
onPositionChanged: page.revealControls()
|
||||
onClicked: mpv.togglePause()
|
||||
onDoubleClicked: page.fullScreenToggled()
|
||||
}
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onDropped: drop => {
|
||||
if (!drop.hasUrls) {
|
||||
return
|
||||
}
|
||||
const url = drop.urls[0]
|
||||
if (mpv.fileLoaded && page.isSubtitleFile(url)) {
|
||||
mpv.addSubtitleFile(url)
|
||||
} else {
|
||||
mpv.loadFile(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stands in for the page header in full screen.
|
||||
QQC2.ToolBar {
|
||||
id: topBar
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
}
|
||||
position: QQC2.ToolBar.Header
|
||||
// No fade: over a paused video Qt Quick renders frames too rarely for a smooth one.
|
||||
visible: page.fullScreen && page.controlsVisible
|
||||
|
||||
HoverHandler {
|
||||
id: topBarHover
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.smallSpacing
|
||||
level: 1
|
||||
elide: Text.ElideRight
|
||||
text: mpv.mediaTitle
|
||||
}
|
||||
|
||||
// Plain buttons: Kirigami.ActionToolBar dropped the full screen action here.
|
||||
QQC2.ToolButton {
|
||||
action: page.actions[0]
|
||||
focusPolicy: Qt.NoFocus
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
action: page.actions[1]
|
||||
focusPolicy: Qt.NoFocus
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: moreButton
|
||||
focusPolicy: Qt.NoFocus
|
||||
icon.name: "overflow-menu"
|
||||
text: KI18n.i18nc("@action:button", "More")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
down: moreMenu.visible
|
||||
onPressed: moreMenu.popup(moreButton, 0, moreButton.height)
|
||||
|
||||
QQC2.Menu {
|
||||
id: moreMenu
|
||||
onVisibleChanged: page.menuOpen = visible
|
||||
QQC2.MenuItem {
|
||||
action: page.actions[2]
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
action: page.actions[3]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolBar {
|
||||
id: footer
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
position: QQC2.ToolBar.Footer
|
||||
visible: page.controlsVisible
|
||||
|
||||
HoverHandler {
|
||||
id: footerHover
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ToolButton {
|
||||
enabled: mpv.fileLoaded
|
||||
focusPolicy: Qt.NoFocus
|
||||
icon.name: mpv.paused || !mpv.fileLoaded ? "media-playback-start" : "media-playback-pause"
|
||||
text: mpv.paused || !mpv.fileLoaded ? KI18n.i18nc("@action:button", "Play") : KI18n.i18nc("@action:button", "Pause")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: mpv.togglePause()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: page.formatTime(seekBar.pressed ? seekBar.value : mpv.position)
|
||||
font.features: { "tnum": 1 }
|
||||
}
|
||||
|
||||
QQC2.Slider {
|
||||
id: seekBar
|
||||
Layout.fillWidth: true
|
||||
enabled: mpv.fileLoaded && mpv.duration > 0
|
||||
focusPolicy: Qt.NoFocus
|
||||
from: 0
|
||||
to: Math.max(mpv.duration, 1)
|
||||
onMoved: mpv.seek(value)
|
||||
|
||||
// Follow playback, but not while the user is dragging the handle.
|
||||
Binding on value {
|
||||
value: mpv.position
|
||||
when: !seekBar.pressed
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: page.formatTime(mpv.duration)
|
||||
font.features: { "tnum": 1 }
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: subtitleButton
|
||||
enabled: mpv.fileLoaded
|
||||
focusPolicy: Qt.NoFocus
|
||||
checkable: true
|
||||
checked: subtitlePopup.visible
|
||||
icon.name: "media-view-subtitles-symbolic"
|
||||
text: KI18n.i18nc("@action:button", "Subtitles")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: subtitlePopup.visible ? subtitlePopup.close() : subtitlePopup.open()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered && !subtitlePopup.visible
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
SubtitlePopup {
|
||||
id: subtitlePopup
|
||||
objectName: "subtitlePopup"
|
||||
mpv: mpv
|
||||
translator: translator
|
||||
onSettingsRequested: page.settingsRequested()
|
||||
x: subtitleButton.width - width
|
||||
y: -height - Kirigami.Units.smallSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// Resizable panel at the left or right window edge listing the playlist.
|
||||
Page {
|
||||
id: root
|
||||
|
||||
required property PlaylistModel playlist
|
||||
required property MpvItem mpv
|
||||
required property int mainWindowWidth
|
||||
property bool fullscreen: false
|
||||
|
||||
readonly property int edge: QtPotatoSettings.playlistPosition === "left" ? Qt.LeftEdge : Qt.RightEdge
|
||||
readonly property int buttonSize: Kirigami.Units.iconSizes.small
|
||||
property real customWidth: QtPotatoSettings.playlistWidth
|
||||
|
||||
signal addFilesRequested()
|
||||
|
||||
function limitWidth(width: real): real {
|
||||
return Math.min(Math.max(width, 260), mainWindowWidth - 50)
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
state = state === "visible" ? "hidden" : "visible"
|
||||
}
|
||||
|
||||
width: limitWidth(customWidth * (fullscreen ? 1.36 : 1))
|
||||
padding: 0
|
||||
state: "hidden"
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.alpha(Kirigami.Theme.backgroundColor, QtPotatoSettings.playlistOverlayVideo ? 0.6 : 1)
|
||||
}
|
||||
|
||||
header: ToolBar {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Header
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Kirigami.Heading {
|
||||
level: 4
|
||||
text: KI18n.i18ncp("@title playlist header", "Playlist (%1 file)", "Playlist (%1 files)", root.playlist.count)
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
text: KI18n.i18nc("@action:button", "Add…")
|
||||
icon.name: "list-add"
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
focusPolicy: Qt.NoFocus
|
||||
onClicked: root.addFilesRequested()
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
text: KI18n.i18nc("@action:button", "Scroll to Playing Item")
|
||||
icon.name: "go-jump"
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: AbstractButton.IconOnly
|
||||
focusPolicy: Qt.NoFocus
|
||||
enabled: root.playlist.playingIndex >= 0
|
||||
onClicked: playlistView.positionViewAtIndex(root.playlist.playingIndex, ListView.Center)
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
text: KI18n.i18nc("@action:button", "Clear Playlist")
|
||||
icon.name: "edit-clear-all"
|
||||
icon.width: root.buttonSize
|
||||
icon.height: root.buttonSize
|
||||
display: AbstractButton.IconOnly
|
||||
focusPolicy: Qt.NoFocus
|
||||
enabled: root.playlist.count > 0
|
||||
onClicked: root.playlist.clear()
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
|
||||
ListView {
|
||||
id: playlistView
|
||||
|
||||
model: root.playlist
|
||||
spacing: 1
|
||||
clip: true
|
||||
reuseItems: true
|
||||
currentIndex: root.playlist.playingIndex
|
||||
highlightFollowsCurrentItem: false
|
||||
|
||||
delegate: PlaylistItem {
|
||||
playlist: root.playlist
|
||||
mpv: root.mpv
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Kirigami.Units.gridUnit * 2
|
||||
visible: root.playlist.count === 0
|
||||
icon.name: "view-media-playlist"
|
||||
text: KI18n.i18nc("@info:placeholder", "Playlist is empty")
|
||||
explanation: KI18n.i18nc("@info:placeholder", "Drop files on the top of the video to add them")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Border with a handle to resize the panel.
|
||||
Rectangle {
|
||||
id: edgeBorder
|
||||
|
||||
x: root.edge === Qt.RightEdge ? 0 : root.width - width
|
||||
y: -root.implicitHeaderHeight
|
||||
width: 1
|
||||
height: root.height
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
ResizeHandler {
|
||||
id: resizeHandlerTall
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 8
|
||||
height: parent.height
|
||||
onMoved: delta => root.customWidth = root.limitWidth(root.customWidth + (root.edge === Qt.RightEdge ? -delta : delta))
|
||||
onReleased: {
|
||||
QtPotatoSettings.playlistWidth = root.customWidth
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 5
|
||||
height: 50
|
||||
color: resizeHandlerTall.hovered ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "hidden"
|
||||
PropertyChanges {
|
||||
root.x: root.edge === Qt.RightEdge ? root.mainWindowWidth : -root.width
|
||||
root.visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "visible"
|
||||
PropertyChanges {
|
||||
root.x: root.edge === Qt.RightEdge ? root.mainWindowWidth - root.width + 1 : 0
|
||||
root.visible: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: "hidden"
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "x"
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.InQuad
|
||||
}
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "visible"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "hidden"
|
||||
to: "visible"
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "visible"
|
||||
value: true
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "x"
|
||||
duration: Kirigami.Units.longDuration
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// One row of the playlist: row number, playing icon and file name.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property int index
|
||||
required property url url
|
||||
required property string name
|
||||
required property bool isPlaying
|
||||
required property PlaylistModel playlist
|
||||
required property MpvItem mpv
|
||||
property real alpha: QtPotatoSettings.playlistOverlayVideo ? 0.6 : 1
|
||||
|
||||
readonly property bool hovered: hoverHandler.hovered
|
||||
readonly property real fontSize: Kirigami.Theme.defaultFont.pointSize
|
||||
|
||||
function labelColor(): color {
|
||||
if (root.isPlaying && !root.hovered) {
|
||||
return Kirigami.Theme.highlightedTextColor
|
||||
}
|
||||
return Kirigami.Theme.textColor
|
||||
}
|
||||
|
||||
implicitWidth: ListView.view.width
|
||||
implicitHeight: fontSize * 3
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onDoubleTapped: root.playlist.play(root.index)
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: contextMenu.popup()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
color: {
|
||||
if (root.isPlaying && !root.hovered) {
|
||||
return Qt.alpha(Kirigami.Theme.highlightColor, root.alpha)
|
||||
}
|
||||
if (root.hovered) {
|
||||
return Qt.alpha(Kirigami.Theme.hoverColor, root.alpha)
|
||||
}
|
||||
return Qt.alpha(Kirigami.Theme.backgroundColor, root.alpha)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Label {
|
||||
text: root.index + 1
|
||||
color: root.labelColor()
|
||||
visible: QtPotatoSettings.playlistShowRowNumber
|
||||
font.pointSize: root.fontSize
|
||||
font.features: { "tnum": 1 }
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.preferredWidth: rowNumberMetrics.width
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: rowNumberMetrics
|
||||
font.pointSize: root.fontSize
|
||||
text: String(root.playlist.count).replace(/./g, "0")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Qt.alpha(Kirigami.Theme.textColor, 0.2)
|
||||
visible: QtPotatoSettings.playlistShowRowNumber
|
||||
Layout.preferredWidth: 1
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "media-playback-start"
|
||||
color: root.labelColor()
|
||||
visible: root.isPlaying
|
||||
Layout.leftMargin: QtPotatoSettings.playlistShowRowNumber ? 0 : Kirigami.Units.largeSpacing
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
}
|
||||
|
||||
LabelWithTooltip {
|
||||
text: root.name
|
||||
toolTipText: root.url.toString().startsWith("file://") ? decodeURIComponent(root.url.toString().substring(7)) : root.url
|
||||
color: root.labelColor()
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: root.fontSize
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: !QtPotatoSettings.playlistShowRowNumber && !root.isPlaying ? Kirigami.Units.largeSpacing : 0
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: contextMenu
|
||||
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Play")
|
||||
icon.name: "media-playback-start"
|
||||
onTriggered: root.playlist.play(root.index)
|
||||
}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Open Containing Folder")
|
||||
icon.name: "folder-open"
|
||||
enabled: root.url.toString().startsWith("file://")
|
||||
onTriggered: {
|
||||
const path = root.url.toString()
|
||||
Qt.openUrlExternally(path.substring(0, path.lastIndexOf("/")))
|
||||
}
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Move Up")
|
||||
icon.name: "go-up"
|
||||
enabled: root.index > 0
|
||||
onTriggered: root.playlist.move(root.index, root.index - 1)
|
||||
}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Move Down")
|
||||
icon.name: "go-down"
|
||||
enabled: root.index < root.playlist.count - 1
|
||||
onTriggered: root.playlist.move(root.index, root.index + 1)
|
||||
}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Remove from Playlist")
|
||||
icon.name: "list-remove"
|
||||
onTriggered: root.playlist.remove(root.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
|
||||
SettingsBasePage {
|
||||
id: root
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:listbox", "Position")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ text: KI18n.i18nc("@item:inlistbox playlist position", "Left"), value: "left" },
|
||||
{ text: KI18n.i18nc("@item:inlistbox playlist position", "Right"), value: "right" }
|
||||
]
|
||||
Component.onCompleted: currentIndex = indexOfValue(QtPotatoSettings.playlistPosition)
|
||||
onActivated: {
|
||||
QtPotatoSettings.playlistPosition = currentValue
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:listbox", "When a file ends")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ text: KI18n.i18nc("@item:inlistbox playback behavior", "Play the next file, stop after the last"), value: "StopAfterLast" },
|
||||
{ text: KI18n.i18nc("@item:inlistbox playback behavior", "Play the next file, repeat the playlist"), value: "RepeatPlaylist" },
|
||||
{ text: KI18n.i18nc("@item:inlistbox playback behavior", "Stop"), value: "StopAfterItem" },
|
||||
{ text: KI18n.i18nc("@item:inlistbox playback behavior", "Repeat the file"), value: "RepeatItem" }
|
||||
]
|
||||
Component.onCompleted: currentIndex = indexOfValue(QtPotatoSettings.playbackBehavior)
|
||||
onActivated: {
|
||||
QtPotatoSettings.playbackBehavior = currentValue
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
RowLayout {
|
||||
CheckBox {
|
||||
text: KI18n.i18nc("@option:check", "Add the other files of the folder")
|
||||
checked: QtPotatoSettings.playlistLoadSiblings
|
||||
onClicked: {
|
||||
QtPotatoSettings.playlistLoadSiblings = checked
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTipButton {
|
||||
toolTipText: KI18n.i18nc("@info:tooltip", "Opening a file fills the playlist with the videos and audio files of its folder, sorted by name, so the next episode plays when one ends.")
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
CheckBox {
|
||||
text: KI18n.i18nc("@option:check", "Overlay video")
|
||||
checked: QtPotatoSettings.playlistOverlayVideo
|
||||
onClicked: {
|
||||
QtPotatoSettings.playlistOverlayVideo = checked
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
RowLayout {
|
||||
CheckBox {
|
||||
text: KI18n.i18nc("@option:check", "Open with the mouse")
|
||||
checked: QtPotatoSettings.playlistToggleWithMouse
|
||||
onClicked: {
|
||||
QtPotatoSettings.playlistToggleWithMouse = checked
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTipButton {
|
||||
toolTipText: KI18n.i18nc("@info:tooltip", "The playlist opens when the mouse reaches its edge of the video, and closes when it leaves. When off, use the Playlist button in the footer or press P.")
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
CheckBox {
|
||||
text: KI18n.i18nc("@option:check", "Show row numbers")
|
||||
checked: QtPotatoSettings.playlistShowRowNumber
|
||||
onClicked: {
|
||||
QtPotatoSettings.playlistShowRowNumber = checked
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "Recent Files")
|
||||
enabled: RecentFilesModel.count > 0
|
||||
|
||||
Instantiator {
|
||||
model: RecentFilesModel
|
||||
delegate: MenuItem {
|
||||
required property url url
|
||||
required property string name
|
||||
required property string path
|
||||
text: name
|
||||
onTriggered: root.actions.fileOpenRequested(url)
|
||||
|
||||
ToolTip.text: path
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 1000
|
||||
}
|
||||
onObjectAdded: (index, object) => root.insertItem(index, object)
|
||||
onObjectRemoved: (index, object) => root.removeItem(object)
|
||||
}
|
||||
|
||||
MenuSeparator {}
|
||||
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Clear List")
|
||||
icon.name: "edit-clear-history"
|
||||
onTriggered: RecentFilesModel.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
|
||||
// Drag area that reports horizontal movement, for resizing side panels.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias hovered: mouseArea.containsMouse
|
||||
|
||||
signal moved(delta: real)
|
||||
signal released()
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
property real pressX: 0
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.SizeHorCursor
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
|
||||
onPressed: mouse => pressX = mouse.x
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed) {
|
||||
root.moved(mouse.x - pressX)
|
||||
}
|
||||
}
|
||||
onReleased: root.released()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.ki18n
|
||||
|
||||
// Flat progress bar of Haruna's footer: click or drag to seek, hover for the time,
|
||||
// chapter marks, right click for the chapter list, wheel to seek.
|
||||
Slider {
|
||||
id: root
|
||||
|
||||
required property MpvItem mpv
|
||||
required property Actions actions
|
||||
|
||||
readonly property alias chaptersPopupOpen: chaptersPopup.visible
|
||||
readonly property bool showChapterMarkers: root.mpv.chapters.length * 20 < width
|
||||
// Playing state before a drag started; playback pauses while dragging.
|
||||
property bool pausedBeforeDrag: false
|
||||
|
||||
function formatTime(totalSeconds: real): string {
|
||||
const s = Math.max(0, Math.floor(totalSeconds))
|
||||
const h = Math.floor(s / 3600)
|
||||
const m = String(Math.floor((s % 3600) / 60)).padStart(2, "0")
|
||||
const sec = String(s % 60).padStart(2, "0")
|
||||
return `${String(h).padStart(2, "0")}:${m}:${sec}`
|
||||
}
|
||||
|
||||
function chapterTitleAt(time: real): string {
|
||||
let title = ""
|
||||
for (const chapter of root.mpv.chapters) {
|
||||
if (chapter.time > time) {
|
||||
break
|
||||
}
|
||||
title = chapter.title
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
function timeLabel(time: real): string {
|
||||
const chapter = chapterTitleAt(time)
|
||||
return chapter ? `${formatTime(time)} - ${chapter}` : formatTime(time)
|
||||
}
|
||||
|
||||
from: 0
|
||||
to: Math.max(root.mpv.duration, 0)
|
||||
enabled: root.mpv.fileLoaded && root.mpv.duration > 0
|
||||
focusPolicy: Qt.NoFocus
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
handle: Item { visible: false }
|
||||
|
||||
background: Rectangle {
|
||||
id: progressBarBackground
|
||||
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
scale: root.mirrored ? -1 : 1
|
||||
|
||||
Rectangle {
|
||||
width: root.visualPosition * parent.width
|
||||
height: parent.height
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.showChapterMarkers ? root.mpv.chapters : []
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
x: modelData.time / root.mpv.duration * progressBarBackground.width
|
||||
width: 2
|
||||
height: progressBarBackground.height
|
||||
color: Kirigami.Theme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: timeToolTip
|
||||
|
||||
property real pointerX: 0
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, pointerX - width / 2))
|
||||
y: -height - Kirigami.Units.largeSpacing
|
||||
z: 10
|
||||
width: timeLabel.implicitWidth + Kirigami.Units.smallSpacing * 2
|
||||
height: timeLabel.implicitHeight + Kirigami.Units.smallSpacing * 2
|
||||
visible: (mouseArea.containsMouse || root.pressed) && root.mpv.duration > 0
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Qt.darker(Kirigami.Theme.backgroundColor, 1.2)
|
||||
|
||||
Label {
|
||||
id: timeLabel
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
const time = root.pressed
|
||||
? root.value
|
||||
: timeToolTip.pointerX / Math.max(1, progressBarBackground.width) * root.to
|
||||
return root.timeLabel(time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
// Left button is the slider's own.
|
||||
acceptedButtons: Qt.MiddleButton | Qt.RightButton
|
||||
onPositionChanged: timeToolTip.pointerX = mouseX
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
const time = mouseX / width * root.to
|
||||
const next = root.mpv.chapters.findIndex(chapter => chapter.time > time)
|
||||
if (next >= 0) {
|
||||
root.mpv.chapter = next
|
||||
}
|
||||
} else if (mouse.button === Qt.RightButton && root.mpv.chapters.length > 0) {
|
||||
chaptersPopup.x = Math.max(0, Math.min(root.width - chaptersPopup.width, mouseX - chaptersPopup.width / 2))
|
||||
chaptersPopup.open()
|
||||
}
|
||||
}
|
||||
onWheel: wheel => {
|
||||
if (wheel.angleDelta.y > 0) {
|
||||
root.actions.seekForwardMedium.trigger()
|
||||
} else if (wheel.angleDelta.y < 0) {
|
||||
root.actions.seekBackwardMedium.trigger()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMoved: root.mpv.seek(value)
|
||||
|
||||
onPressedChanged: {
|
||||
if (pressed) {
|
||||
pausedBeforeDrag = root.mpv.paused
|
||||
root.mpv.paused = true
|
||||
timeToolTip.pointerX = root.visualPosition * root.width
|
||||
} else {
|
||||
root.mpv.paused = pausedBeforeDrag
|
||||
}
|
||||
}
|
||||
|
||||
// Follow playback, but not while the user drags the bar.
|
||||
Binding on value {
|
||||
value: root.mpv.position
|
||||
when: !root.pressed
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
enabled: root.pressed
|
||||
function onValueChanged() {
|
||||
timeToolTip.pointerX = root.visualPosition * root.width
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: chaptersPopup
|
||||
|
||||
property int itemWidth: Kirigami.Units.gridUnit * 16
|
||||
|
||||
y: -height - root.height
|
||||
width: Math.min(itemWidth, Math.max(Kirigami.Units.gridUnit * 40, root.width * 0.8))
|
||||
height: Math.min(Kirigami.Units.gridUnit * 20, listView.contentHeight)
|
||||
modal: true
|
||||
padding: 0
|
||||
|
||||
onOpened: listView.positionViewAtIndex(Math.max(0, root.mpv.chapter), ListView.Beginning)
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
clip: true
|
||||
model: root.mpv.chapters
|
||||
delegate: RadioDelegate {
|
||||
required property int index
|
||||
required property var modelData
|
||||
width: ListView.view.width
|
||||
text: `${root.formatTime(modelData.time)} - ${modelData.title}`
|
||||
checked: index === root.mpv.chapter
|
||||
onClicked: {
|
||||
chaptersPopup.close()
|
||||
root.mpv.chapter = index
|
||||
}
|
||||
onImplicitWidthChanged: chaptersPopup.itemWidth = Math.max(chaptersPopup.itemWidth, implicitWidth + Kirigami.Units.gridUnit * 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// Page of the settings window. Changes are saved as they are made.
|
||||
Kirigami.ScrollablePage {
|
||||
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
|
||||
Component.onDestruction: QtPotatoSettings.save()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// Section title of a settings page, between two lines.
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property int topMargin: Kirigami.Units.gridUnit
|
||||
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: root.topMargin
|
||||
|
||||
Rectangle {
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit
|
||||
Layout.preferredHeight: 1
|
||||
}
|
||||
|
||||
Kirigami.Heading {
|
||||
text: root.text
|
||||
level: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
Layout.preferredHeight: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "&Settings")
|
||||
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "General")
|
||||
onTriggered: root.actions.settingsRequested("general")
|
||||
}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Playback")
|
||||
onTriggered: root.actions.settingsRequested("playback")
|
||||
}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Subtitles")
|
||||
onTriggered: root.actions.settingsRequested("subtitles")
|
||||
}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Translation")
|
||||
onTriggered: root.actions.settingsRequested("translation")
|
||||
}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "Playlist")
|
||||
onTriggered: root.actions.settingsRequested("playlist")
|
||||
}
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@action:inmenu", "About")
|
||||
onTriggered: root.actions.settingsRequested("about")
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.ki18n
|
||||
|
||||
FormCard.FormCardPage {
|
||||
id: page
|
||||
|
||||
required property TranslationService translator
|
||||
required property MpvItem mpvItem
|
||||
|
||||
title: KI18n.i18nc("@title", "Settings")
|
||||
|
||||
// Fields write to the settings object as they change; this writes it to disk.
|
||||
Component.onDestruction: QtPotatoSettings.save()
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
type: Kirigami.MessageType.Error
|
||||
text: page.translator.errorText || ApiKeyStore.error
|
||||
visible: text.length > 0
|
||||
showCloseButton: true
|
||||
onVisibleChanged: if (!visible) page.translator.clearError()
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: KI18n.i18nc("@title:group", "Player")
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
FormCard.FormSwitchDelegate {
|
||||
text: KI18n.i18nc("@option:check", "Use mpv.conf")
|
||||
description: KI18n.i18nc("@info", "Reads mpv.conf from %1, like the mpv player does. Changes apply the next time QtPotato starts. Subtitle text size and outline set below take precedence.", page.mpvItem.userConfigDir)
|
||||
checked: QtPotatoSettings.loadMpvConfig
|
||||
onToggled: QtPotatoSettings.loadMpvConfig = checked
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextDelegate {
|
||||
text: page.mpvItem.loadedUserConfigFiles.length > 0
|
||||
? KI18n.i18nc("@info", "Loaded: %1", page.mpvItem.loadedUserConfigFiles.join(", "))
|
||||
: (QtPotatoSettings.loadMpvConfig
|
||||
? KI18n.i18nc("@info", "No mpv.conf found")
|
||||
: KI18n.i18nc("@info", "mpv.conf is not used"))
|
||||
description: page.mpvItem.skippedUserOptions.length > 0
|
||||
? KI18n.i18nc("@info", "Ignored because they would break the built-in player: %1", page.mpvItem.skippedUserOptions.join(", "))
|
||||
: ""
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: KI18n.i18nc("@title:group", "Subtitles")
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
FormCard.FormSpinBoxDelegate {
|
||||
label: KI18n.i18nc("@label:spinbox", "Text size")
|
||||
description: KI18n.i18nc("@info", "Text size and outline apply to translated and plain-text subtitles. Styled subtitles (ASS) keep the look they were made with.")
|
||||
from: 20
|
||||
to: 150
|
||||
stepSize: 2
|
||||
value: QtPotatoSettings.subtitleFontSize
|
||||
onValueModified: QtPotatoSettings.subtitleFontSize = value
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormSpinBoxDelegate {
|
||||
label: KI18n.i18nc("@label:spinbox", "Outline size")
|
||||
from: 0
|
||||
to: 10
|
||||
value: QtPotatoSettings.subtitleOutlineSize
|
||||
onValueModified: QtPotatoSettings.subtitleOutlineSize = value
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormHeader {
|
||||
title: KI18n.i18nc("@title:group", "Subtitle Translation")
|
||||
}
|
||||
|
||||
FormCard.FormCard {
|
||||
FormCard.FormTextFieldDelegate {
|
||||
label: KI18n.i18nc("@label:textbox", "API base URL")
|
||||
placeholderText: "https://example.com/v1"
|
||||
text: QtPotatoSettings.apiBaseUrl
|
||||
inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase
|
||||
onTextEdited: QtPotatoSettings.apiBaseUrl = text.trim()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormPasswordFieldDelegate {
|
||||
id: apiKeyField
|
||||
label: KI18n.i18nc("@label:textbox", "API key")
|
||||
placeholderText: ApiKeyStore.hasKey
|
||||
? KI18n.i18nc("@info:placeholder", "Saved in the wallet. Type a new key to replace it.")
|
||||
: KI18n.i18nc("@info:placeholder", "Not set")
|
||||
onEditingFinished: {
|
||||
if (text.trim().length > 0) {
|
||||
ApiKeyStore.setKey(text)
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: ApiKeyStore.hasKey
|
||||
icon.name: "edit-delete-remove"
|
||||
text: KI18n.i18nc("@action:button", "Remove Saved API Key")
|
||||
onClicked: ApiKeyStore.setKey("")
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: modelCombo
|
||||
|
||||
// Swapping the list makes the combo box jump to its first entry; that must
|
||||
// not overwrite the model the user chose.
|
||||
property bool updatingList: false
|
||||
|
||||
function showModels() {
|
||||
const chosen = QtPotatoSettings.model
|
||||
updatingList = true
|
||||
model = page.translator.availableModels
|
||||
currentIndex = page.translator.availableModels.indexOf(chosen)
|
||||
editText = chosen
|
||||
updatingList = false
|
||||
}
|
||||
|
||||
text: KI18n.i18nc("@label:listbox", "Model")
|
||||
editable: true
|
||||
onEditTextChanged: if (!updatingList) QtPotatoSettings.model = editText.trim()
|
||||
Component.onCompleted: showModels()
|
||||
|
||||
Connections {
|
||||
target: page.translator
|
||||
function onAvailableModelsChanged() {
|
||||
modelCombo.showModels()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
icon.name: "view-refresh"
|
||||
text: page.translator.fetchingModels
|
||||
? KI18n.i18nc("@action:button", "Loading Models…")
|
||||
: KI18n.i18nc("@action:button", "Load Models from the API")
|
||||
description: page.translator.availableModels.length > 0
|
||||
? KI18n.i18ncp("@info", "%1 model available", "%1 models available", page.translator.availableModels.length)
|
||||
: ""
|
||||
enabled: !page.translator.fetchingModels
|
||||
onClicked: page.translator.fetchModels()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
label: KI18n.i18nc("@label:textbox", "Translate into")
|
||||
text: QtPotatoSettings.targetLanguage
|
||||
onTextEdited: QtPotatoSettings.targetLanguage = text.trim()
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormSpinBoxDelegate {
|
||||
label: KI18n.i18nc("@label:spinbox", "Lines per request")
|
||||
from: 5
|
||||
to: 200
|
||||
stepSize: 5
|
||||
value: QtPotatoSettings.batchSize
|
||||
onValueModified: QtPotatoSettings.batchSize = value
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormSectionText {
|
||||
text: KI18n.i18nc("@info", "Works with any OpenAI-compatible API. The whole subtitle track is translated in batches before it is shown, and the result is kept so the same track is never translated twice.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.coreaddons as KCoreAddons
|
||||
|
||||
// Separate settings window with the list of pages on the left, as in Haruna. Created
|
||||
// when first opened and destroyed when closed.
|
||||
Loader {
|
||||
id: root
|
||||
|
||||
required property MpvItem mpv
|
||||
required property TranslationService translator
|
||||
|
||||
property string activePage: "general"
|
||||
|
||||
function openPage(page: string) {
|
||||
root.activePage = page
|
||||
if (!root.active) {
|
||||
root.active = true
|
||||
return
|
||||
}
|
||||
const window = root.item as Kirigami.ApplicationWindow
|
||||
window.showPage(page)
|
||||
window.raise()
|
||||
window.requestActivate()
|
||||
}
|
||||
|
||||
active: false
|
||||
|
||||
sourceComponent: Kirigami.ApplicationWindow {
|
||||
id: settingsWindow
|
||||
|
||||
readonly property list<QtObject> pages: [
|
||||
QtObject {
|
||||
readonly property string name: "general"
|
||||
readonly property string text: KI18n.i18nc("@action", "General")
|
||||
readonly property string iconName: "configure"
|
||||
readonly property Component component: Component {
|
||||
GeneralSettings { mpv: root.mpv }
|
||||
}
|
||||
},
|
||||
QtObject {
|
||||
readonly property string name: "playback"
|
||||
readonly property string text: KI18n.i18nc("@action", "Playback")
|
||||
readonly property string iconName: "media-playback-start"
|
||||
readonly property Component component: Component {
|
||||
PlaybackSettings {}
|
||||
}
|
||||
},
|
||||
QtObject {
|
||||
readonly property string name: "subtitles"
|
||||
readonly property string text: KI18n.i18nc("@action", "Subtitles")
|
||||
readonly property string iconName: "add-subtitle"
|
||||
readonly property Component component: Component {
|
||||
SubtitlesSettings {}
|
||||
}
|
||||
},
|
||||
QtObject {
|
||||
readonly property string name: "translation"
|
||||
readonly property string text: KI18n.i18nc("@action", "Translation")
|
||||
readonly property string iconName: "translate"
|
||||
readonly property Component component: Component {
|
||||
TranslationSettings { translator: root.translator }
|
||||
}
|
||||
},
|
||||
QtObject {
|
||||
readonly property string name: "playlist"
|
||||
readonly property string text: KI18n.i18nc("@action", "Playlist")
|
||||
readonly property string iconName: "view-media-playlist"
|
||||
readonly property Component component: Component {
|
||||
PlaylistSettings {}
|
||||
}
|
||||
},
|
||||
QtObject {
|
||||
readonly property string name: "about"
|
||||
readonly property string text: KI18n.i18nc("@action", "About")
|
||||
readonly property string iconName: "help-about"
|
||||
readonly property Component component: Component {
|
||||
Kirigami.AboutPage {
|
||||
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
|
||||
aboutData: KCoreAddons.AboutData
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function showPage(name: string) {
|
||||
let index = 0
|
||||
for (let i = 0; i < pages.length; ++i) {
|
||||
if (pages[i].name === name) {
|
||||
index = i
|
||||
}
|
||||
}
|
||||
pageList.currentIndex = index
|
||||
pageStack.pop(pageStack.items[0])
|
||||
// Created here, with a parent in the scene; pages that the page stack
|
||||
// creates from a component start without one, which Qt warns about.
|
||||
currentPage?.destroy()
|
||||
currentPage = pages[index].component.createObject(pageStack)
|
||||
pageStack.push(currentPage)
|
||||
}
|
||||
|
||||
property Item currentPage: null
|
||||
|
||||
width: 800
|
||||
height: 600
|
||||
minimumWidth: 750
|
||||
minimumHeight: 400
|
||||
title: KI18n.i18nc("@title:window", "Settings")
|
||||
visible: true
|
||||
|
||||
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.None
|
||||
pageStack.initialPage: Kirigami.ScrollablePage {
|
||||
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
|
||||
padding: 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
ListView {
|
||||
id: pageList
|
||||
model: settingsWindow.pages
|
||||
delegate: ItemDelegate {
|
||||
required property int index
|
||||
required property QtObject modelData
|
||||
width: ListView.view.width
|
||||
text: modelData.text
|
||||
icon.name: modelData.iconName
|
||||
highlighted: index === ListView.view.currentIndex
|
||||
onClicked: settingsWindow.showPage(modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
pageStack.columnView.columnWidth = Kirigami.Units.gridUnit * 12
|
||||
showPage(root.activePage)
|
||||
}
|
||||
|
||||
onClosing: {
|
||||
QtPotatoSettings.save()
|
||||
root.active = false
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Esc"
|
||||
onActivated: settingsWindow.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.ki18n
|
||||
|
||||
QQC2.Popup {
|
||||
id: popup
|
||||
|
||||
required property MpvItem mpv
|
||||
required property TranslationService translator
|
||||
|
||||
signal settingsRequested()
|
||||
|
||||
readonly property var currentTrack: mpv.subtitleTracks.find(track => track.id === mpv.subtitleId) ?? null
|
||||
|
||||
// [{ id, text }] with "Off" first, shared by both track pickers.
|
||||
readonly property var trackModel: [{ id: 0, text: KI18n.i18nc("@item:inlistbox no subtitle", "Off") }]
|
||||
.concat(mpv.subtitleTracks.map(track => ({ id: track.id, text: trackLabel(track) })))
|
||||
|
||||
function trackLabel(track: var): string {
|
||||
const name = track.title || (track.external ? track.externalFilename.split("/").pop() : "")
|
||||
const lang = track.lang ? `[${track.lang}]` : ""
|
||||
const description = [name, lang].filter(part => part.length > 0).join(" ")
|
||||
return KI18n.i18nc("@item:inlistbox subtitle track number and description", "%1: %2",
|
||||
track.id, description || KI18n.i18nc("@item:inlistbox", "Untitled"))
|
||||
}
|
||||
|
||||
function indexOfTrack(id: int): int {
|
||||
return Math.max(0, trackModel.findIndex(entry => entry.id === id))
|
||||
}
|
||||
|
||||
padding: Kirigami.Units.largeSpacing
|
||||
|
||||
contentItem: Kirigami.FormLayout {
|
||||
QQC2.ComboBox {
|
||||
id: primaryCombo
|
||||
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Subtitle:")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 16
|
||||
model: popup.trackModel
|
||||
textRole: "text"
|
||||
valueRole: "id"
|
||||
onActivated: popup.mpv.subtitleId = currentValue
|
||||
|
||||
// Always reflect what mpv actually selected, also after the user picks an entry.
|
||||
Binding on currentIndex {
|
||||
value: popup.indexOfTrack(popup.mpv.subtitleId)
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ComboBox {
|
||||
id: secondaryCombo
|
||||
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Secondary subtitle:")
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 16
|
||||
model: popup.trackModel
|
||||
textRole: "text"
|
||||
valueRole: "id"
|
||||
onActivated: popup.mpv.secondarySubtitleId = currentValue
|
||||
|
||||
Binding on currentIndex {
|
||||
value: popup.indexOfTrack(popup.mpv.secondarySubtitleId)
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Delay:")
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.SpinBox {
|
||||
id: delaySpin
|
||||
from: -600000
|
||||
to: 600000
|
||||
stepSize: 100
|
||||
editable: true
|
||||
textFromValue: (value, locale) => KI18n.i18nc("@item:valuesuffix milliseconds", "%1 ms", value)
|
||||
valueFromText: (text, locale) => parseInt(text) || 0
|
||||
onValueModified: popup.mpv.subtitleDelay = value / 1000
|
||||
|
||||
Binding on value {
|
||||
value: Math.round(popup.mpv.subtitleDelay * 1000)
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
enabled: popup.mpv.subtitleDelay !== 0
|
||||
icon.name: "edit-undo"
|
||||
text: KI18n.i18nc("@action:button", "Reset delay")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: popup.mpv.subtitleDelay = 0
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
icon.name: "add-subtitle"
|
||||
text: KI18n.i18nc("@action:button", "Load Subtitle File…")
|
||||
onClicked: subtitleDialog.open()
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Kirigami.FormData.isSection: true
|
||||
Kirigami.FormData.label: KI18n.i18nc("@title:group", "Translation")
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
visible: !popup.translator.busy
|
||||
enabled: popup.currentTrack !== null
|
||||
icon.name: "translate"
|
||||
text: KI18n.i18nc("@action:button", "Translate Subtitle into %1", QtPotatoSettings.targetLanguage)
|
||||
onClicked: popup.translator.translate(popup.mpv.currentUrl, popup.currentTrack, popup.mpv.mediaTitle)
|
||||
|
||||
QQC2.ToolTip.text: KI18n.i18nc("@info:tooltip", "Translates the subtitle chosen above and shows the result in its place")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: popup.translator.busy
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 16
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ProgressBar {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 1
|
||||
value: popup.translator.progress
|
||||
indeterminate: popup.translator.progress === 0
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
icon.name: "dialog-cancel"
|
||||
text: KI18n.i18nc("@action:button", "Cancel translation")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: popup.translator.cancel()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
visible: popup.translator.busy
|
||||
text: popup.translator.statusText
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 22
|
||||
type: Kirigami.MessageType.Error
|
||||
text: popup.translator.errorText
|
||||
visible: text.length > 0
|
||||
showCloseButton: true
|
||||
onVisibleChanged: if (!visible) popup.translator.clearError()
|
||||
actions: Kirigami.Action {
|
||||
icon.name: "configure"
|
||||
text: KI18n.i18nc("@action:button", "Settings")
|
||||
onTriggered: {
|
||||
popup.close()
|
||||
popup.settingsRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: subtitleDialog
|
||||
title: KI18n.i18nc("@title:window", "Load Subtitle")
|
||||
currentFolder: {
|
||||
const url = popup.mpv.currentUrl.toString()
|
||||
return url.substring(0, url.lastIndexOf("/"))
|
||||
}
|
||||
nameFilters: [
|
||||
KI18n.i18nc("@item:inlistbox file filter", "Subtitle files (*.srt *.ass *.ssa *.vtt *.sub *.sup)"),
|
||||
KI18n.i18nc("@item:inlistbox file filter", "All files (*)")
|
||||
]
|
||||
onAccepted: popup.mpv.addSubtitleFile(selectedFile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
// Checkable list of subtitle tracks, for the primary or the secondary subtitle.
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property MpvItem mpv
|
||||
property bool isPrimary: true
|
||||
|
||||
readonly property int selectedId: isPrimary ? mpv.subtitleId : mpv.secondarySubtitleId
|
||||
readonly property int otherId: isPrimary ? mpv.secondarySubtitleId : mpv.subtitleId
|
||||
|
||||
function trackLabel(track: var): string {
|
||||
const name = track.title || (track.external ? track.externalFilename.split("/").pop() : "")
|
||||
const lang = track.lang ? `[${track.lang}]` : ""
|
||||
const description = [name, lang].filter(part => part.length > 0).join(" ")
|
||||
return KI18n.i18nc("@item:inmenu subtitle track number and description", "%1: %2",
|
||||
track.id, description || KI18n.i18nc("@item:inmenu", "Untitled"))
|
||||
}
|
||||
|
||||
function select(id: int) {
|
||||
if (root.isPrimary) {
|
||||
root.mpv.subtitleId = id
|
||||
} else {
|
||||
root.mpv.secondarySubtitleId = id
|
||||
}
|
||||
}
|
||||
|
||||
enabled: mpv.subtitleTracks.length > 0
|
||||
|
||||
MenuItem {
|
||||
text: KI18n.i18nc("@item:inmenu no subtitle", "None")
|
||||
checkable: true
|
||||
checked: root.selectedId === 0
|
||||
onTriggered: {
|
||||
root.select(0)
|
||||
// Clicking a checked item unchecks it, which drops the binding.
|
||||
checked = Qt.binding(() => root.selectedId === 0)
|
||||
}
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: root.mpv.subtitleTracks
|
||||
delegate: MenuItem {
|
||||
required property var modelData
|
||||
text: root.trackLabel(modelData)
|
||||
checkable: true
|
||||
checked: modelData.id === root.selectedId
|
||||
// mpv cannot show one track in both places.
|
||||
enabled: modelData.id !== root.otherId
|
||||
onTriggered: root.select(modelData.id)
|
||||
}
|
||||
onObjectAdded: (index, object) => root.insertItem(index + 1, object)
|
||||
onObjectRemoved: (index, object) => root.removeItem(object)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
required property MpvItem mpv
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "&Subtitles")
|
||||
|
||||
SubtitleTracksMenu {
|
||||
mpv: root.mpv
|
||||
isPrimary: true
|
||||
title: KI18n.i18nc("@title:menu", "Primary Subtitle")
|
||||
}
|
||||
SubtitleTracksMenu {
|
||||
mpv: root.mpv
|
||||
isPrimary: false
|
||||
title: KI18n.i18nc("@title:menu", "Secondary Subtitle")
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.translateSubtitle }
|
||||
MenuItem {
|
||||
action: root.actions.cancelTranslation
|
||||
visible: root.actions.cancelTranslation.enabled
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.addSubtitle }
|
||||
MenuItem { action: root.actions.subtitleBigger }
|
||||
MenuItem { action: root.actions.subtitleSmaller }
|
||||
MenuItem { action: root.actions.subtitleEarlier }
|
||||
MenuItem { action: root.actions.subtitleLater }
|
||||
MenuItem { action: root.actions.subtitleDelayReset }
|
||||
MenuItem { action: root.actions.subtitleToggle }
|
||||
MenuItem { action: root.actions.subtitleCycleUp }
|
||||
MenuItem { action: root.actions.subtitleCycleDown }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
|
||||
SettingsBasePage {
|
||||
id: root
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:spinbox", "Font size")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
SpinBox {
|
||||
from: 20
|
||||
to: 150
|
||||
stepSize: 2
|
||||
value: QtPotatoSettings.subtitleFontSize
|
||||
onValueModified: {
|
||||
QtPotatoSettings.subtitleFontSize = value
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTipButton {
|
||||
toolTipText: KI18n.i18nc("@info:tooltip", "Font size and outline apply to translated and plain-text subtitles. Styled subtitles (ASS) keep the look they were made with. The font itself comes from sub-font in mpv.conf.")
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:spinbox", "Outline size")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
from: 0
|
||||
to: 10
|
||||
value: QtPotatoSettings.subtitleOutlineSize
|
||||
onValueModified: {
|
||||
QtPotatoSettings.subtitleOutlineSize = value
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
// Info button next to a setting; click it (or hover) to read what the setting does.
|
||||
ToolButton {
|
||||
id: root
|
||||
|
||||
required property string toolTipText
|
||||
|
||||
icon.name: "documentinfo"
|
||||
focusPolicy: Qt.NoFocus
|
||||
checkable: true
|
||||
|
||||
ToolTip {
|
||||
y: root.height
|
||||
visible: root.checked || root.hovered
|
||||
delay: root.checked ? 0 : Kirigami.Units.toolTipDelay
|
||||
timeout: -1
|
||||
closePolicy: Popup.NoAutoClose
|
||||
contentItem: Label {
|
||||
text: root.toolTipText
|
||||
wrapMode: Text.WordWrap
|
||||
width: Math.min(implicitWidth, Kirigami.Units.gridUnit * 20)
|
||||
}
|
||||
width: Math.min(contentItem.implicitWidth, Kirigami.Units.gridUnit * 20) + leftPadding + rightPadding
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.ki18n
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
SettingsBasePage {
|
||||
id: root
|
||||
|
||||
required property TranslationService translator
|
||||
|
||||
ColumnLayout {
|
||||
Kirigami.InlineMessage {
|
||||
Layout.fillWidth: true
|
||||
type: Kirigami.MessageType.Error
|
||||
text: root.translator.errorText || ApiKeyStore.error
|
||||
visible: text.length > 0
|
||||
showCloseButton: true
|
||||
onVisibleChanged: if (!visible) root.translator.clearError()
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:textbox", "API base URL")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
TextField {
|
||||
placeholderText: "https://example.com/v1"
|
||||
text: QtPotatoSettings.apiBaseUrl
|
||||
inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: QtPotatoSettings.apiBaseUrl = text.trim()
|
||||
onEditingFinished: QtPotatoSettings.save()
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:textbox", "API key")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Kirigami.PasswordField {
|
||||
id: apiKeyField
|
||||
placeholderText: ApiKeyStore.hasKey
|
||||
? KI18n.i18nc("@info:placeholder", "Saved in the wallet. Type a new key to replace it.")
|
||||
: KI18n.i18nc("@info:placeholder", "Not set")
|
||||
Layout.fillWidth: true
|
||||
onEditingFinished: {
|
||||
if (text.trim().length > 0) {
|
||||
ApiKeyStore.setKey(text)
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: ApiKeyStore.hasKey
|
||||
icon.name: "edit-delete-remove"
|
||||
text: KI18n.i18nc("@action:button", "Remove")
|
||||
onClicked: ApiKeyStore.setKey("")
|
||||
|
||||
ToolTip.text: KI18n.i18nc("@info:tooltip", "Remove the saved API key from the wallet")
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:listbox", "Model")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ComboBox {
|
||||
id: modelCombo
|
||||
|
||||
// Swapping the list makes the combo box jump to its first entry; that
|
||||
// must not overwrite the model the user chose.
|
||||
property bool updatingList: false
|
||||
|
||||
function showModels() {
|
||||
const chosen = QtPotatoSettings.model
|
||||
updatingList = true
|
||||
model = root.translator.availableModels
|
||||
currentIndex = root.translator.availableModels.indexOf(chosen)
|
||||
editText = chosen
|
||||
updatingList = false
|
||||
}
|
||||
|
||||
editable: true
|
||||
Layout.fillWidth: true
|
||||
onEditTextChanged: if (!updatingList) QtPotatoSettings.model = editText.trim()
|
||||
onActivated: QtPotatoSettings.save()
|
||||
Component.onCompleted: showModels()
|
||||
|
||||
Connections {
|
||||
target: root.translator
|
||||
function onAvailableModelsChanged() {
|
||||
modelCombo.showModels()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
icon.name: "view-refresh"
|
||||
text: root.translator.fetchingModels
|
||||
? KI18n.i18nc("@action:button", "Loading…")
|
||||
: KI18n.i18nc("@action:button", "Load Models")
|
||||
enabled: !root.translator.fetchingModels
|
||||
onClicked: root.translator.fetchModels()
|
||||
|
||||
ToolTip.text: KI18n.i18nc("@info:tooltip", "Ask the API which models it offers")
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 1
|
||||
visible: modelCountLabel.visible
|
||||
}
|
||||
|
||||
Label {
|
||||
id: modelCountLabel
|
||||
visible: root.translator.availableModels.length > 0
|
||||
text: KI18n.i18ncp("@info", "%1 model available", "%1 models available", root.translator.availableModels.length)
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:textbox", "Translate into")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
TextField {
|
||||
text: QtPotatoSettings.targetLanguage
|
||||
Layout.fillWidth: true
|
||||
onTextEdited: QtPotatoSettings.targetLanguage = text.trim()
|
||||
onEditingFinished: QtPotatoSettings.save()
|
||||
}
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@label:spinbox", "Lines per request")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
|
||||
SpinBox {
|
||||
from: 5
|
||||
to: 200
|
||||
stepSize: 5
|
||||
value: QtPotatoSettings.batchSize
|
||||
onValueModified: {
|
||||
QtPotatoSettings.batchSize = value
|
||||
QtPotatoSettings.save()
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.preferredWidth: 1 }
|
||||
|
||||
Label {
|
||||
text: KI18n.i18nc("@info", "Works with any OpenAI-compatible API. The whole subtitle track is translated in batches before it is shown, and the result is kept so the same track is never translated twice.")
|
||||
wrapMode: Text.WordWrap
|
||||
opacity: 0.7
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "&Video")
|
||||
|
||||
MenuItem { action: root.actions.toggleDeinterlacing }
|
||||
MenuItem { action: root.actions.zoomIn }
|
||||
MenuItem { action: root.actions.zoomOut }
|
||||
MenuItem { action: root.actions.zoomReset }
|
||||
MenuItem { action: root.actions.screenshot }
|
||||
MenuItem { action: root.actions.rotateClockwise }
|
||||
MenuItem { action: root.actions.rotateCounterClockwise }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.ki18n
|
||||
|
||||
Menu {
|
||||
id: root
|
||||
|
||||
required property Actions actions
|
||||
|
||||
title: KI18n.i18nc("@title:menu", "&View")
|
||||
|
||||
MenuItem { action: root.actions.toggleFullscreen }
|
||||
MenuItem { action: root.actions.togglePlaylist }
|
||||
MenuSeparator {}
|
||||
MenuItem { action: root.actions.toggleMenuBar }
|
||||
MenuItem { action: root.actions.toggleHeader }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.ki18n
|
||||
|
||||
// Flat volume bar with the value written on it, as in Haruna.
|
||||
Slider {
|
||||
id: root
|
||||
|
||||
required property MpvItem mpv
|
||||
|
||||
from: 0
|
||||
to: root.mpv.volumeMax
|
||||
implicitWidth: 100
|
||||
implicitHeight: 25
|
||||
wheelEnabled: true
|
||||
stepSize: QtPotatoSettings.volumeStep
|
||||
focusPolicy: Qt.NoFocus
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
handle: Item { visible: false }
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
scale: root.mirrored ? -1 : 1
|
||||
|
||||
Rectangle {
|
||||
width: root.visualPosition * parent.width
|
||||
height: parent.height
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
}
|
||||
|
||||
onMoved: root.mpv.volume = Math.round(value)
|
||||
|
||||
// Follow mpv, but not while the user drags (dragging replaces a plain binding).
|
||||
Binding on value {
|
||||
value: root.mpv.volume
|
||||
when: !root.pressed
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: root
|
||||
text: root.mpv.mute ? KI18n.i18nc("@info volume muted", "Muted") : Math.round(root.value)
|
||||
color: "#fff"
|
||||
style: Text.Outline
|
||||
styleColor: "#111"
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,67 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0 http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
|
||||
<kcfgfile name="qtpotatorc"/>
|
||||
<group name="General">
|
||||
<entry name="ShowMenuBar" type="Bool">
|
||||
<label>Show the menu bar above the toolbar</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="ShowHeader" type="Bool">
|
||||
<label>Show the toolbar above the video</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="FooterStyle" type="String">
|
||||
<label>"default" docks the playback controls below the video, "floating" shows them over it while the pointer moves</label>
|
||||
<default>default</default>
|
||||
</entry>
|
||||
<entry name="MaxRecentFiles" type="Int">
|
||||
<label>Files listed under Recent Files; 0 hides the list</label>
|
||||
<default>10</default>
|
||||
<min>0</min>
|
||||
<max>50</max>
|
||||
</entry>
|
||||
<entry name="FileDialogLastLocation" type="Url">
|
||||
<label>Folder the Open File dialog starts in</label>
|
||||
</entry>
|
||||
<entry name="LastUrl" type="String">
|
||||
<label>Address last entered in Open URL</label>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Player">
|
||||
<entry name="LoadMpvConfig" type="Bool">
|
||||
<label>Load the user's mpv.conf when the player starts</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="Volume" type="Int">
|
||||
<label>Volume restored at startup</label>
|
||||
<default>100</default>
|
||||
<min>0</min>
|
||||
<max>1000</max>
|
||||
</entry>
|
||||
<entry name="VolumeStep" type="Int">
|
||||
<label>Volume change per step of the volume actions and the mouse wheel</label>
|
||||
<default>5</default>
|
||||
<min>1</min>
|
||||
<max>100</max>
|
||||
</entry>
|
||||
<entry name="SeekSmallStep" type="Int">
|
||||
<label>Seconds skipped by Left and Right</label>
|
||||
<default>5</default>
|
||||
<min>1</min>
|
||||
<max>600</max>
|
||||
</entry>
|
||||
<entry name="SeekMediumStep" type="Int">
|
||||
<label>Seconds skipped by Shift+Left and Shift+Right</label>
|
||||
<default>15</default>
|
||||
<min>1</min>
|
||||
<max>600</max>
|
||||
</entry>
|
||||
<entry name="SeekBigStep" type="Int">
|
||||
<label>Seconds skipped by Up and Down</label>
|
||||
<default>30</default>
|
||||
<min>1</min>
|
||||
<max>600</max>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Subtitles">
|
||||
<entry name="SubtitleFontSize" type="Int">
|
||||
@@ -23,6 +79,36 @@
|
||||
<max>10</max>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Playlist">
|
||||
<entry name="PlaylistPosition" type="String">
|
||||
<label>Window edge the playlist opens at: "right" or "left"</label>
|
||||
<default>right</default>
|
||||
</entry>
|
||||
<entry name="PlaylistWidth" type="Int">
|
||||
<label>Width of the playlist panel</label>
|
||||
<default>380</default>
|
||||
</entry>
|
||||
<entry name="PlaylistOverlayVideo" type="Bool">
|
||||
<label>Show the playlist over the video instead of beside it</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="PlaylistToggleWithMouse" type="Bool">
|
||||
<label>Open the playlist when the pointer reaches its window edge</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="PlaylistLoadSiblings" type="Bool">
|
||||
<label>Opening a file also adds the other videos in its folder to the playlist</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="PlaylistShowRowNumber" type="Bool">
|
||||
<label>Show row numbers in the playlist</label>
|
||||
<default>true</default>
|
||||
</entry>
|
||||
<entry name="PlaybackBehavior" type="String">
|
||||
<label>What happens when a file ends: "StopAfterLast", "RepeatPlaylist", "StopAfterItem" or "RepeatItem"</label>
|
||||
<default>StopAfterLast</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Translation">
|
||||
<entry name="ApiBaseUrl" type="String">
|
||||
<label>Base URL of the OpenAI-compatible API, including the version path (for example …/v1)</label>
|
||||
|
||||
Reference in new issue
Block a user