Audit main UI, menus, settings, and glossary against KDE HIG #4

Merged
vokhactri merged 4 commits from docs/kde-hig-ui-audit into main 2026-11-07 20:08:28 +07:00
43 changed files with 783 additions and 384 deletions

No files matched your search

+32
View File
@@ -2,6 +2,8 @@
#include "glossarystore.h" #include "glossarystore.h"
#include <QDir>
#include <QFile>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTest> #include <QTest>
using namespace Qt::Literals::StringLiterals; using namespace Qt::Literals::StringLiterals;
@@ -24,6 +26,36 @@ private Q_SLOTS:
QCOMPARE(merged.entries.at(0).target, u"Lâm"_s); QCOMPARE(merged.entries.at(0).target, u"Lâm"_s);
QCOMPARE(merged.entries.at(1).source, u"Sect"_s); QCOMPARE(merged.entries.at(1).source, u"Sect"_s);
} }
void saveFailureReturnsError()
{
QTemporaryDir temp;
QVERIFY(temp.isValid());
QStandardPaths::setTestModeEnabled(true);
const QString appData = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
const QString glossaries = appData + u"/glossaries"_s;
QDir(glossaries).removeRecursively();
QVERIFY(QDir().mkpath(appData));
QFile blocker(glossaries);
QVERIFY(blocker.open(QIODevice::WriteOnly));
blocker.write("blocked");
blocker.close();
GlossaryStore::Glossary glossary{temp.path(), {{u"Original"_s, u"Translation"_s}}};
QString error;
QVERIFY(!GlossaryStore::save(glossary, &error));
QVERIFY(!error.isEmpty());
QFile::remove(glossaries);
}
void saveRejectsIncompleteEntry()
{
QString error;
QVERIFY(!GlossaryStore::save({QDir::tempPath(), {{u"Original"_s, {}}}}, &error));
QVERIFY(error.contains(u"both"_s));
}
}; };
QTEST_MAIN(GlossaryStoreTest) QTEST_MAIN(GlossaryStoreTest)
+18
View File
@@ -45,6 +45,8 @@ MpvItem::MpvItem(QQuickItem *parent)
observeProperty(QStringLiteral("volume"), MPV_FORMAT_INT64); observeProperty(QStringLiteral("volume"), MPV_FORMAT_INT64);
observeProperty(QStringLiteral("volume-max"), MPV_FORMAT_INT64); observeProperty(QStringLiteral("volume-max"), MPV_FORMAT_INT64);
observeProperty(QStringLiteral("mute"), MPV_FORMAT_FLAG); observeProperty(QStringLiteral("mute"), MPV_FORMAT_FLAG);
observeProperty(QStringLiteral("sub-visibility"), MPV_FORMAT_FLAG);
observeProperty(QStringLiteral("deinterlace"), MPV_FORMAT_FLAG);
observeProperty(QStringLiteral("speed"), MPV_FORMAT_DOUBLE); observeProperty(QStringLiteral("speed"), MPV_FORMAT_DOUBLE);
observeProperty(QStringLiteral("chapter-list"), MPV_FORMAT_NODE); observeProperty(QStringLiteral("chapter-list"), MPV_FORMAT_NODE);
observeProperty(QStringLiteral("chapter"), MPV_FORMAT_INT64); observeProperty(QStringLiteral("chapter"), MPV_FORMAT_INT64);
@@ -258,6 +260,16 @@ bool MpvItem::mute() const
return m_mute; return m_mute;
} }
bool MpvItem::subtitleVisible() const
{
return m_subtitleVisible;
}
bool MpvItem::deinterlace() const
{
return m_deinterlace;
}
void MpvItem::setMute(bool mute) void MpvItem::setMute(bool mute)
{ {
setPropertyAsync(QStringLiteral("mute"), mute); setPropertyAsync(QStringLiteral("mute"), mute);
@@ -414,6 +426,12 @@ void MpvItem::onPropertyChanged(const QString &property, const QVariant &value)
} else if (property == u"mute") { } else if (property == u"mute") {
m_mute = value.toBool(); m_mute = value.toBool();
Q_EMIT muteChanged(); Q_EMIT muteChanged();
} else if (property == u"sub-visibility") {
m_subtitleVisible = value.toBool();
Q_EMIT subtitleVisibleChanged();
} else if (property == u"deinterlace") {
m_deinterlace = value.toBool();
Q_EMIT deinterlaceChanged();
} else if (property == u"speed") { } else if (property == u"speed") {
m_speed = value.toDouble(); m_speed = value.toDouble();
Q_EMIT speedChanged(); Q_EMIT speedChanged();
+8
View File
@@ -36,6 +36,8 @@ class MpvItem : public MpvAbstractItem
Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY volumeChanged) Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY volumeChanged)
Q_PROPERTY(int volumeMax READ volumeMax NOTIFY volumeMaxChanged) Q_PROPERTY(int volumeMax READ volumeMax NOTIFY volumeMaxChanged)
Q_PROPERTY(bool mute READ mute WRITE setMute NOTIFY muteChanged) Q_PROPERTY(bool mute READ mute WRITE setMute NOTIFY muteChanged)
Q_PROPERTY(bool subtitleVisible READ subtitleVisible NOTIFY subtitleVisibleChanged)
Q_PROPERTY(bool deinterlace READ deinterlace NOTIFY deinterlaceChanged)
Q_PROPERTY(double speed READ speed WRITE setSpeed NOTIFY speedChanged) Q_PROPERTY(double speed READ speed WRITE setSpeed NOTIFY speedChanged)
// Chapters of the current file, each a map with title and time (in seconds). // Chapters of the current file, each a map with title and time (in seconds).
Q_PROPERTY(QVariantList chapters READ chapters NOTIFY chaptersChanged) Q_PROPERTY(QVariantList chapters READ chapters NOTIFY chaptersChanged)
@@ -76,6 +78,8 @@ public:
int volumeMax() const; int volumeMax() const;
bool mute() const; bool mute() const;
void setMute(bool mute); void setMute(bool mute);
bool subtitleVisible() const;
bool deinterlace() const;
double speed() const; double speed() const;
void setSpeed(double speed); void setSpeed(double speed);
QVariantList chapters() const; QVariantList chapters() const;
@@ -116,6 +120,8 @@ Q_SIGNALS:
void volumeChanged(); void volumeChanged();
void volumeMaxChanged(); void volumeMaxChanged();
void muteChanged(); void muteChanged();
void subtitleVisibleChanged();
void deinterlaceChanged();
void speedChanged(); void speedChanged();
void chaptersChanged(); void chaptersChanged();
void chapterChanged(); void chapterChanged();
@@ -149,6 +155,8 @@ private:
int m_volume = 100; int m_volume = 100;
int m_volumeMax = 130; int m_volumeMax = 130;
bool m_mute = false; bool m_mute = false;
bool m_subtitleVisible = true;
bool m_deinterlace = false;
double m_speed = 1.0; double m_speed = 1.0;
QVariantList m_chapters; QVariantList m_chapters;
int m_chapter = -1; int m_chapter = -1;
+42 -7
View File
@@ -22,6 +22,9 @@ Item {
readonly property bool hasFile: mpv.fileLoaded readonly property bool hasFile: mpv.fileLoaded
readonly property bool subtitlesVisible: mpv.subtitleVisible
readonly property bool deinterlacingEnabled: mpv.deinterlace
signal openFileRequested() signal openFileRequested()
// Open a known file, such as one from Recent Files. // Open a known file, such as one from Recent Files.
signal fileOpenRequested(url: url) signal fileOpenRequested(url: url)
@@ -115,7 +118,7 @@ Item {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Restart Playback") text: KI18n.i18nc("@action", "Restart Playback")
icon.name: "edit-reset" icon.name: "edit-reset"
shortcut: "F5" shortcut: "Ctrl+Shift+R"
onTriggered: root.mpv.seek(0) onTriggered: root.mpv.seek(0)
} }
@@ -138,6 +141,7 @@ Item {
property Action seekForwardSmall: Action { property Action seekForwardSmall: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Seek Small Step Forward") text: KI18n.i18nc("@action", "Seek Small Step Forward")
icon.name: "go-next"
shortcut: "Right" shortcut: "Right"
onTriggered: root.seekBy(QtPotatoSettings.seekSmallStep) onTriggered: root.seekBy(QtPotatoSettings.seekSmallStep)
} }
@@ -145,6 +149,7 @@ Item {
property Action seekBackwardSmall: Action { property Action seekBackwardSmall: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Seek Small Step Backward") text: KI18n.i18nc("@action", "Seek Small Step Backward")
icon.name: "go-previous"
shortcut: "Left" shortcut: "Left"
onTriggered: root.seekBy(-QtPotatoSettings.seekSmallStep) onTriggered: root.seekBy(-QtPotatoSettings.seekSmallStep)
} }
@@ -152,6 +157,7 @@ Item {
property Action seekForwardMedium: Action { property Action seekForwardMedium: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Seek Medium Step Forward") text: KI18n.i18nc("@action", "Seek Medium Step Forward")
icon.name: "media-seek-forward"
shortcut: "Shift+Right" shortcut: "Shift+Right"
onTriggered: root.seekBy(QtPotatoSettings.seekMediumStep) onTriggered: root.seekBy(QtPotatoSettings.seekMediumStep)
} }
@@ -159,6 +165,7 @@ Item {
property Action seekBackwardMedium: Action { property Action seekBackwardMedium: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Seek Medium Step Backward") text: KI18n.i18nc("@action", "Seek Medium Step Backward")
icon.name: "media-seek-backward"
shortcut: "Shift+Left" shortcut: "Shift+Left"
onTriggered: root.seekBy(-QtPotatoSettings.seekMediumStep) onTriggered: root.seekBy(-QtPotatoSettings.seekMediumStep)
} }
@@ -166,6 +173,7 @@ Item {
property Action seekForwardBig: Action { property Action seekForwardBig: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Seek Big Step Forward") text: KI18n.i18nc("@action", "Seek Big Step Forward")
icon.name: "arrow-right-double"
shortcut: "Up" shortcut: "Up"
onTriggered: root.seekBy(QtPotatoSettings.seekBigStep) onTriggered: root.seekBy(QtPotatoSettings.seekBigStep)
} }
@@ -173,6 +181,7 @@ Item {
property Action seekBackwardBig: Action { property Action seekBackwardBig: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Seek Big Step Backward") text: KI18n.i18nc("@action", "Seek Big Step Backward")
icon.name: "arrow-left-double"
shortcut: "Down" shortcut: "Down"
onTriggered: root.seekBy(-QtPotatoSettings.seekBigStep) onTriggered: root.seekBy(-QtPotatoSettings.seekBigStep)
} }
@@ -180,6 +189,7 @@ Item {
property Action seekNextSubtitle: Action { property Action seekNextSubtitle: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Seek To Next Subtitle") text: KI18n.i18nc("@action", "Seek To Next Subtitle")
icon.name: "go-down"
shortcut: "Ctrl+Right" shortcut: "Ctrl+Right"
onTriggered: root.mpv.commandAsync(["sub-seek", "1"]) onTriggered: root.mpv.commandAsync(["sub-seek", "1"])
} }
@@ -187,6 +197,7 @@ Item {
property Action seekPreviousSubtitle: Action { property Action seekPreviousSubtitle: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Seek To Previous Subtitle") text: KI18n.i18nc("@action", "Seek To Previous Subtitle")
icon.name: "go-up"
shortcut: "Ctrl+Left" shortcut: "Ctrl+Left"
onTriggered: root.mpv.commandAsync(["sub-seek", "-1"]) onTriggered: root.mpv.commandAsync(["sub-seek", "-1"])
} }
@@ -194,6 +205,7 @@ Item {
property Action seekNextChapter: Action { property Action seekNextChapter: Action {
enabled: root.shortcutsEnabled && root.mpv.chapters.length > 0 enabled: root.shortcutsEnabled && root.mpv.chapters.length > 0
text: KI18n.i18nc("@action", "Seek To Next Chapter") text: KI18n.i18nc("@action", "Seek To Next Chapter")
icon.name: "go-last"
shortcut: "PgUp" shortcut: "PgUp"
onTriggered: root.mpv.commandAsync(["add", "chapter", "1"]) onTriggered: root.mpv.commandAsync(["add", "chapter", "1"])
} }
@@ -201,6 +213,7 @@ Item {
property Action seekPreviousChapter: Action { property Action seekPreviousChapter: Action {
enabled: root.shortcutsEnabled && root.mpv.chapters.length > 0 enabled: root.shortcutsEnabled && root.mpv.chapters.length > 0
text: KI18n.i18nc("@action", "Seek To Previous Chapter") text: KI18n.i18nc("@action", "Seek To Previous Chapter")
icon.name: "go-first"
shortcut: "PgDown" shortcut: "PgDown"
onTriggered: root.mpv.commandAsync(["add", "chapter", "-1"]) onTriggered: root.mpv.commandAsync(["add", "chapter", "-1"])
} }
@@ -208,6 +221,7 @@ Item {
property Action frameStepForward: Action { property Action frameStepForward: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Step Frame Forward") text: KI18n.i18nc("@action", "Step Frame Forward")
icon.name: "keyframe-next"
shortcut: "." shortcut: "."
onTriggered: root.mpv.frameStep(true) onTriggered: root.mpv.frameStep(true)
} }
@@ -215,6 +229,7 @@ Item {
property Action frameStepBackward: Action { property Action frameStepBackward: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Step Frame Backward") text: KI18n.i18nc("@action", "Step Frame Backward")
icon.name: "keyframe-previous"
shortcut: "," shortcut: ","
onTriggered: root.mpv.frameStep(false) onTriggered: root.mpv.frameStep(false)
} }
@@ -222,6 +237,7 @@ Item {
property Action speedUp: Action { property Action speedUp: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Increase Playback Speed") text: KI18n.i18nc("@action", "Increase Playback Speed")
icon.name: "media-seek-forward"
shortcut: "]" shortcut: "]"
onTriggered: root.changeSpeed(0.1) onTriggered: root.changeSpeed(0.1)
} }
@@ -229,6 +245,7 @@ Item {
property Action speedDown: Action { property Action speedDown: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Decrease Playback Speed") text: KI18n.i18nc("@action", "Decrease Playback Speed")
icon.name: "media-seek-backward"
shortcut: "[" shortcut: "["
onTriggered: root.changeSpeed(-0.1) onTriggered: root.changeSpeed(-0.1)
} }
@@ -236,6 +253,7 @@ Item {
property Action speedReset: Action { property Action speedReset: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Reset Playback Speed") text: KI18n.i18nc("@action", "Reset Playback Speed")
icon.name: "speedometer"
shortcut: "Backspace" shortcut: "Backspace"
onTriggered: root.changeSpeed(0) onTriggered: root.changeSpeed(0)
} }
@@ -270,6 +288,7 @@ Item {
property Action audioCycleUp: Action { property Action audioCycleUp: Action {
enabled: root.shortcutsEnabled && root.mpv.audioTracks.length > 1 enabled: root.shortcutsEnabled && root.mpv.audioTracks.length > 1
text: KI18n.i18nc("@action", "Cycle Up Audio Track") text: KI18n.i18nc("@action", "Cycle Up Audio Track")
icon.name: "go-up"
shortcut: "Shift+3" shortcut: "Shift+3"
onTriggered: root.mpv.audioId = root.cycleTrack(root.mpv.audioTracks, root.mpv.audioId, 1) onTriggered: root.mpv.audioId = root.cycleTrack(root.mpv.audioTracks, root.mpv.audioId, 1)
} }
@@ -277,6 +296,7 @@ Item {
property Action audioCycleDown: Action { property Action audioCycleDown: Action {
enabled: root.shortcutsEnabled && root.mpv.audioTracks.length > 1 enabled: root.shortcutsEnabled && root.mpv.audioTracks.length > 1
text: KI18n.i18nc("@action", "Cycle Down Audio Track") text: KI18n.i18nc("@action", "Cycle Down Audio Track")
icon.name: "go-down"
shortcut: "Shift+2" shortcut: "Shift+2"
onTriggered: root.mpv.audioId = root.cycleTrack(root.mpv.audioTracks, root.mpv.audioId, -1) onTriggered: root.mpv.audioId = root.cycleTrack(root.mpv.audioTracks, root.mpv.audioId, -1)
} }
@@ -291,12 +311,16 @@ Item {
property Action subtitleToggle: Action { property Action subtitleToggle: Action {
enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0 enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0
text: KI18n.i18nc("@action", "Toggle Subtitles") text: KI18n.i18nc("@action", "Show Subtitles")
icon.name: "media-view-subtitles-symbolic"
checkable: true
checked: root.subtitlesVisible
shortcut: "Ctrl+S" shortcut: "Ctrl+S"
onTriggered: { onTriggered: {
const visible = root.mpv.getProperty("sub-visibility") const visible = root.mpv.getProperty("sub-visibility")
root.mpv.setPropertyAsync("sub-visibility", !visible) root.mpv.setPropertyAsync("sub-visibility", !visible)
root.mpv.setPropertyAsync("secondary-sub-visibility", !visible) root.mpv.setPropertyAsync("secondary-sub-visibility", !visible)
checked = Qt.binding(() => root.subtitlesVisible)
root.osd.message(visible ? KI18n.i18nc("@info:osd", "Subtitles hidden") : KI18n.i18nc("@info:osd", "Subtitles shown")) root.osd.message(visible ? KI18n.i18nc("@info:osd", "Subtitles hidden") : KI18n.i18nc("@info:osd", "Subtitles shown"))
} }
} }
@@ -304,6 +328,7 @@ Item {
property Action subtitleCycleUp: Action { property Action subtitleCycleUp: Action {
enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0 enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0
text: KI18n.i18nc("@action", "Cycle Up Subtitle") text: KI18n.i18nc("@action", "Cycle Up Subtitle")
icon.name: "go-up"
shortcut: "J" shortcut: "J"
onTriggered: root.mpv.subtitleId = root.cycleTrack(root.mpv.subtitleTracks, root.mpv.subtitleId, 1) onTriggered: root.mpv.subtitleId = root.cycleTrack(root.mpv.subtitleTracks, root.mpv.subtitleId, 1)
} }
@@ -311,6 +336,7 @@ Item {
property Action subtitleCycleDown: Action { property Action subtitleCycleDown: Action {
enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0 enabled: root.shortcutsEnabled && root.mpv.subtitleTracks.length > 0
text: KI18n.i18nc("@action", "Cycle Down Subtitle") text: KI18n.i18nc("@action", "Cycle Down Subtitle")
icon.name: "go-down"
shortcut: "Shift+J" shortcut: "Shift+J"
onTriggered: root.mpv.subtitleId = root.cycleTrack(root.mpv.subtitleTracks, root.mpv.subtitleId, -1) onTriggered: root.mpv.subtitleId = root.cycleTrack(root.mpv.subtitleTracks, root.mpv.subtitleId, -1)
} }
@@ -318,6 +344,7 @@ Item {
property Action subtitleEarlier: Action { property Action subtitleEarlier: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Show Subtitles Earlier") text: KI18n.i18nc("@action", "Show Subtitles Earlier")
icon.name: "go-previous"
shortcut: "Z" shortcut: "Z"
onTriggered: root.changeSubtitleDelay(-0.1) onTriggered: root.changeSubtitleDelay(-0.1)
} }
@@ -325,6 +352,7 @@ Item {
property Action subtitleLater: Action { property Action subtitleLater: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Show Subtitles Later") text: KI18n.i18nc("@action", "Show Subtitles Later")
icon.name: "go-next"
shortcut: "Shift+Z" shortcut: "Shift+Z"
onTriggered: root.changeSubtitleDelay(0.1) onTriggered: root.changeSubtitleDelay(0.1)
} }
@@ -332,12 +360,14 @@ Item {
property Action subtitleDelayReset: Action { property Action subtitleDelayReset: Action {
enabled: root.shortcutsEnabled && root.hasFile && root.mpv.subtitleDelay !== 0 enabled: root.shortcutsEnabled && root.hasFile && root.mpv.subtitleDelay !== 0
text: KI18n.i18nc("@action", "Reset Subtitle Delay") text: KI18n.i18nc("@action", "Reset Subtitle Delay")
icon.name: "edit-reset"
onTriggered: root.changeSubtitleDelay(-root.mpv.subtitleDelay) onTriggered: root.changeSubtitleDelay(-root.mpv.subtitleDelay)
} }
property Action subtitleBigger: Action { property Action subtitleBigger: Action {
enabled: root.shortcutsEnabled enabled: root.shortcutsEnabled
text: KI18n.i18nc("@action", "Increase Subtitle Font Size") text: KI18n.i18nc("@action", "Increase Subtitle Font Size")
icon.name: "format-font-size-more"
shortcut: "Ctrl+Z" shortcut: "Ctrl+Z"
onTriggered: { onTriggered: {
QtPotatoSettings.subtitleFontSize = Math.min(150, QtPotatoSettings.subtitleFontSize + 2) QtPotatoSettings.subtitleFontSize = Math.min(150, QtPotatoSettings.subtitleFontSize + 2)
@@ -349,6 +379,7 @@ Item {
property Action subtitleSmaller: Action { property Action subtitleSmaller: Action {
enabled: root.shortcutsEnabled enabled: root.shortcutsEnabled
text: KI18n.i18nc("@action", "Decrease Subtitle Font Size") text: KI18n.i18nc("@action", "Decrease Subtitle Font Size")
icon.name: "format-font-size-less"
shortcut: "Ctrl+X" shortcut: "Ctrl+X"
onTriggered: { onTriggered: {
QtPotatoSettings.subtitleFontSize = Math.max(20, QtPotatoSettings.subtitleFontSize - 2) QtPotatoSettings.subtitleFontSize = Math.max(20, QtPotatoSettings.subtitleFontSize - 2)
@@ -392,11 +423,15 @@ Item {
property Action toggleDeinterlacing: Action { property Action toggleDeinterlacing: Action {
enabled: root.shortcutsEnabled && root.hasFile enabled: root.shortcutsEnabled && root.hasFile
text: KI18n.i18nc("@action", "Toggle Deinterlacing") text: KI18n.i18nc("@action", "Deinterlace")
icon.name: "view-filter"
checkable: true
checked: root.deinterlacingEnabled
shortcut: "D" shortcut: "D"
onTriggered: { onTriggered: {
const enabled = root.mpv.getProperty("deinterlace") === "yes" const enabled = root.mpv.getProperty("deinterlace") === true
root.mpv.setPropertyAsync("deinterlace", enabled ? "no" : "yes") root.mpv.setPropertyAsync("deinterlace", !enabled)
checked = Qt.binding(() => root.deinterlacingEnabled)
root.osd.message(enabled ? KI18n.i18nc("@info:osd", "Deinterlacing off") : KI18n.i18nc("@info:osd", "Deinterlacing on")) root.osd.message(enabled ? KI18n.i18nc("@info:osd", "Deinterlacing off") : KI18n.i18nc("@info:osd", "Deinterlacing on"))
} }
} }
@@ -476,6 +511,7 @@ Item {
property Action toggleHeader: Action { property Action toggleHeader: Action {
enabled: root.shortcutsEnabled enabled: root.shortcutsEnabled
text: QtPotatoSettings.showHeader ? KI18n.i18nc("@action", "Hide Toolbar") : KI18n.i18nc("@action", "Show Toolbar") text: QtPotatoSettings.showHeader ? KI18n.i18nc("@action", "Hide Toolbar") : KI18n.i18nc("@action", "Show Toolbar")
icon.name: "configure-toolbars"
shortcut: "Ctrl+H" shortcut: "Ctrl+H"
onTriggered: root.headerToggleRequested() onTriggered: root.headerToggleRequested()
} }
@@ -489,7 +525,7 @@ Item {
} }
property Action configure: Action { property Action configure: Action {
text: KI18n.i18nc("@action", "Settings") text: KI18n.i18nc("@action", "Configure QtPotato…")
icon.name: "configure" icon.name: "configure"
shortcut: "Ctrl+Shift+," shortcut: "Ctrl+Shift+,"
onTriggered: root.settingsRequested("general") onTriggered: root.settingsRequested("general")
@@ -498,7 +534,6 @@ Item {
property Action about: Action { property Action about: Action {
text: KI18n.i18nc("@action", "About QtPotato") text: KI18n.i18nc("@action", "About QtPotato")
icon.name: "help-about" icon.name: "help-about"
shortcut: "F1"
onTriggered: root.settingsRequested("about") onTriggered: root.settingsRequested("about")
} }
+23 -1
View File
@@ -1,8 +1,30 @@
// SPDX-FileCopyrightText: 2026 Tri Vo Khac // SPDX-FileCopyrightText: 2026 Tri Vo Khac
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls import QtQuick.Controls
import org.kde.kirigami as Kirigami
Menu { Menu {
delegate: AppMenuItem {} id: root
readonly property bool hasCheckables: {
for (let i = 0; i < count; ++i) {
const item = itemAt(i)
if (item && item.checkable) {
return true
}
}
return false
}
// Keep long filenames and translated labels from obscuring parent menus.
readonly property real maximumWidth: Math.min(Kirigami.Units.gridUnit * 30,
(root.parent && root.parent.Window.window ? root.parent.Window.window.width : Kirigami.Units.gridUnit * 30))
width: Math.min(implicitWidth, maximumWidth)
delegate: AppMenuItem {
icon.name: subMenu ? subMenu.icon.name : (action ? action.icon.name : "")
icon.source: subMenu ? subMenu.icon.source : (action ? action.icon.source : "")
}
} }
+6 -2
View File
@@ -9,6 +9,8 @@ import org.kde.kirigami as Kirigami
MenuItem { MenuItem {
id: root id: root
property int textElide: Text.ElideRight
// org.kde.desktop sets these on its MenuItem, but other Plasma styles such // org.kde.desktop sets these on its MenuItem, but other Plasma styles such
// as org.kde.union do not, which would leave the label below empty. // as org.kde.union do not, which would leave the label below empty.
Kirigami.MnemonicData.enabled: enabled && visible Kirigami.MnemonicData.enabled: enabled && visible
@@ -33,7 +35,7 @@ MenuItem {
layoutDirection: root.mirrored ? Qt.RightToLeft : Qt.LeftToRight layoutDirection: root.mirrored ? Qt.RightToLeft : Qt.LeftToRight
Item { Item {
Layout.preferredWidth: (root.ListView.view && root.ListView.view.hasCheckables) || root.checkable Layout.preferredWidth: (root.menu && root.menu.hasCheckables) || root.checkable
? root.indicator.width : Kirigami.Units.smallSpacing ? root.indicator.width : Kirigami.Units.smallSpacing
} }
@@ -62,12 +64,14 @@ MenuItem {
textFormat: Text.StyledText textFormat: Text.StyledText
font: root.font font: root.font
color: root.pressed ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor color: root.pressed ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor
elide: Text.ElideRight elide: root.textElide
horizontalAlignment: root.mirrored ? Text.AlignRight : Text.AlignLeft horizontalAlignment: root.mirrored ? Text.AlignRight : Text.AlignLeft
} }
Label { Label {
id: shortcut id: shortcut
Layout.leftMargin: root.mirrored ? 0 : Kirigami.Units.largeSpacing - row.spacing
Layout.rightMargin: root.mirrored ? Kirigami.Units.largeSpacing - row.spacing : 0
Layout.alignment: Qt.AlignBaseline Layout.alignment: Qt.AlignBaseline
visible: !root.subMenu && root.action && root.action.shortcut !== undefined visible: !root.subMenu && root.action && root.action.shortcut !== undefined
text: visible ? itemShortcut.nativeText : "" text: visible ? itemShortcut.nativeText : ""
+1
View File
@@ -8,6 +8,7 @@ import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "audio-volume-high"
required property Actions actions required property Actions actions
required property MpvItem mpv required property MpvItem mpv
+9 -1
View File
@@ -11,6 +11,7 @@ import org.kde.ki18n
// Checkable list of audio tracks. // Checkable list of audio tracks.
AppMenu { AppMenu {
id: root id: root
icon.name: "audio-x-generic"
required property MpvItem mpv required property MpvItem mpv
@@ -23,14 +24,21 @@ AppMenu {
enabled: mpv.audioTracks.length > 0 enabled: mpv.audioTracks.length > 0
ButtonGroup { id: tracksGroup; exclusive: true }
Instantiator { Instantiator {
model: root.mpv.audioTracks model: root.mpv.audioTracks
delegate: AppMenuItem { delegate: AppMenuItem {
id: trackItem
required property var modelData required property var modelData
text: root.trackLabel(modelData) text: root.trackLabel(modelData)
checkable: true checkable: true
ButtonGroup.group: tracksGroup
checked: modelData.id === root.mpv.audioId checked: modelData.id === root.mpv.audioId
onTriggered: root.mpv.audioId = modelData.id onTriggered: {
root.mpv.audioId = modelData.id
checked = Qt.binding(() => trackItem.modelData.id === root.mpv.audioId)
}
} }
onObjectAdded: (index, object) => root.insertItem(index, object) onObjectAdded: (index, object) => root.insertItem(index, object)
onObjectRemoved: (index, object) => root.removeItem(object) onObjectRemoved: (index, object) => root.removeItem(object)
+15 -2
View File
@@ -8,14 +8,27 @@ import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "folder"
required property Actions actions required property Actions actions
// The hamburger must not instantiate application management commands.
property bool includeQuit: true
title: KI18n.i18nc("@title:menu", "&File") title: KI18n.i18nc("@title:menu", "&File")
AppMenuItem { action: root.actions.openFile } AppMenuItem { action: root.actions.openFile }
AppMenuItem { action: root.actions.openUrl } AppMenuItem { action: root.actions.openUrl }
RecentFilesMenu { actions: root.actions } RecentFilesMenu { actions: root.actions }
MenuSeparator {} Instantiator {
AppMenuItem { action: root.actions.quit } model: root.includeQuit ? 1 : 0
delegate: MenuSeparator {}
onObjectAdded: (index, object) => root.insertItem(3, object)
onObjectRemoved: (index, object) => root.removeItem(object)
}
Instantiator {
model: root.includeQuit ? 1 : 0
delegate: AppMenuItem { action: root.actions.quit }
onObjectAdded: (index, object) => root.insertItem(4, object)
onObjectRemoved: (index, object) => root.removeItem(object)
}
} }
+42 -13
View File
@@ -13,6 +13,13 @@ import org.kde.kirigami as Kirigami
Item { Item {
id: root id: root
// Space activates the focused control before the window's playback action.
Keys.onShortcutOverride: event => {
if (event.key === Qt.Key_Space && event.modifiers === Qt.NoModifier) {
event.accepted = true
}
}
required property Actions actions required property Actions actions
required property MpvVideo mpv required property MpvVideo mpv
property bool fullscreen: false property bool fullscreen: false
@@ -22,9 +29,17 @@ Item {
readonly property bool isFloating: QtPotatoSettings.footerStyle === "floating" readonly property bool isFloating: QtPotatoSettings.footerStyle === "floating"
// Takes space below the video (instead of covering it). // Takes space below the video (instead of covering it).
readonly property bool isDocked: !isFloating && !fullscreen readonly property bool isDocked: !isFloating && !fullscreen
readonly property bool isSmallSize: width < 600 readonly property bool isSmallSize: width < Kirigami.Units.gridUnit * 30
readonly property int buttonSize: isSmallSize ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium 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 readonly property bool busy: footer.hovered || hamburgerMenu.isOpen || seekBar.chaptersPopupOpen || seekBar.pressed || controlHasFocus
readonly property bool controlHasFocus: {
let item = root.Window.window?.activeFocusItem
while (item) {
if (item === footer) return true
item = item.parent
}
return false
}
property bool shown: true property bool shown: true
implicitHeight: isFloating implicitHeight: isFloating
@@ -92,7 +107,7 @@ Item {
x: (root.width - width) / 2 x: (root.width - width) / 2
width: root.isFloating width: root.isFloating
? Math.min(root.width - Kirigami.Units.largeSpacing * (root.isSmallSize ? 2 : 4), 900) ? Math.min(root.width - Kirigami.Units.largeSpacing * (root.isSmallSize ? 2 : 4), Kirigami.Units.gridUnit * 50)
: root.width : root.width
state: root.shown ? "visible" : "hidden" state: root.shown ? "visible" : "hidden"
padding: Kirigami.Units.smallSpacing padding: Kirigami.Units.smallSpacing
@@ -112,16 +127,17 @@ Item {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: Kirigami.Units.smallSpacing
RowLayout { RowLayout {
spacing: root.isSmallSize ? 1 : Kirigami.Units.smallSpacing spacing: Kirigami.Units.mediumSpacing
HamburgerMenu { HamburgerMenu {
id: hamburgerMenu id: hamburgerMenu
actions: root.actions actions: root.actions
mpv: root.mpv mpv: root.mpv
inFooter: true inFooter: true
focusPolicy: Qt.StrongFocus
visible: root.showMenuButton visible: root.showMenuButton
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
@@ -133,7 +149,7 @@ Item {
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
ToolTip.text: root.mpv.paused ? KI18n.i18nc("@info:tooltip", "Start playback") : KI18n.i18nc("@info:tooltip", "Pause playback") ToolTip.text: root.mpv.paused ? KI18n.i18nc("@info:tooltip", "Start playback") : KI18n.i18nc("@info:tooltip", "Pause playback")
ToolTip.visible: hovered ToolTip.visible: hovered
@@ -146,7 +162,7 @@ Item {
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
ToolTip.text: KI18n.i18nc("@info:tooltip", "Play previous file") ToolTip.text: KI18n.i18nc("@info:tooltip", "Play previous file")
ToolTip.visible: hovered ToolTip.visible: hovered
@@ -159,7 +175,7 @@ Item {
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
ToolTip.text: KI18n.i18nc("@info:tooltip", "Play next file") ToolTip.text: KI18n.i18nc("@info:tooltip", "Play next file")
ToolTip.visible: hovered ToolTip.visible: hovered
@@ -171,7 +187,6 @@ Item {
mpv: root.mpv mpv: root.mpv
actions: root.actions actions: root.actions
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 20
} }
ToolButton { ToolButton {
@@ -180,7 +195,7 @@ Item {
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
ToolTip.text: root.actions.mute.text ToolTip.text: root.actions.mute.text
ToolTip.visible: hovered ToolTip.visible: hovered
@@ -188,9 +203,21 @@ Item {
} }
VolumeSlider { VolumeSlider {
id: volumeSlider
mpv: root.mpv mpv: root.mpv
Layout.preferredWidth: root.isSmallSize ? 60 : 100 Layout.preferredWidth: Kirigami.Units.gridUnit * (root.isSmallSize ? 3 : 5)
Layout.preferredHeight: 20 }
Label {
text: root.mpv.mute ? KI18n.i18nc("@info volume muted", "Muted") : KI18n.i18nc("@info volume percentage", "%1%", Math.round(volumeSlider.value))
font: Kirigami.Theme.smallFont
Layout.minimumWidth: volumeTextMetrics.width
TextMetrics {
id: volumeTextMetrics
font: Kirigami.Theme.smallFont
text: KI18n.i18nc("@info volume percentage", "%1%", root.mpv.volumeMax)
}
} }
ToolButton { ToolButton {
@@ -198,7 +225,7 @@ Item {
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
visible: !QtPotatoSettings.playlistToggleWithMouse visible: !QtPotatoSettings.playlistToggleWithMouse
ToolTip.text: text ToolTip.text: text
@@ -208,6 +235,8 @@ Item {
} }
RowLayout { RowLayout {
spacing: Kirigami.Units.largeSpacing
LabelWithTooltip { LabelWithTooltip {
text: root.mpv.mediaTitle text: root.mpv.mediaTitle
font.pointSize: Kirigami.Theme.smallFont.pointSize font.pointSize: Kirigami.Theme.smallFont.pointSize
+20 -30
View File
@@ -13,23 +13,19 @@ SettingsBasePage {
required property MpvItem mpv required property MpvItem mpv
GridLayout { Kirigami.FormLayout {
columns: 2
SettingsHeader { SettingsHeader {
text: KI18n.i18nc("@title", "Interface") text: KI18n.i18nc("@title", "Interface")
topMargin: 0
Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
} }
Label {
text: KI18n.i18nc("@label:listbox", "Footer style")
Layout.alignment: Qt.AlignRight
}
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Footer style")
Kirigami.FormData.buddyFor: footerStyleCombo
ComboBox { ComboBox {
id: footerStyleCombo
textRole: "text" textRole: "text"
valueRole: "value" valueRole: "value"
model: [ model: [
@@ -48,9 +44,7 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Switch {
CheckBox {
text: KI18n.i18nc("@option:check", "Show menu bar") text: KI18n.i18nc("@option:check", "Show menu bar")
checked: QtPotatoSettings.showMenuBar checked: QtPotatoSettings.showMenuBar
onClicked: { onClicked: {
@@ -59,9 +53,7 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Switch {
CheckBox {
text: KI18n.i18nc("@option:check", "Show toolbar") text: KI18n.i18nc("@option:check", "Show toolbar")
checked: QtPotatoSettings.showHeader checked: QtPotatoSettings.showHeader
onClicked: { onClicked: {
@@ -70,13 +62,12 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Maximum recent files")
Layout.alignment: Qt.AlignRight
}
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Maximum recent files")
Kirigami.FormData.buddyFor: maxRecentSpin
SpinBox { SpinBox {
id: maxRecentSpin
from: 0 from: 0
to: 50 to: 50
value: QtPotatoSettings.maxRecentFiles value: QtPotatoSettings.maxRecentFiles
@@ -93,14 +84,12 @@ SettingsBasePage {
SettingsHeader { SettingsHeader {
text: KI18n.i18nc("@title", "mpv") text: KI18n.i18nc("@title", "mpv")
Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
} }
Item { Layout.preferredWidth: 1 }
RowLayout { RowLayout {
CheckBox { spacing: Kirigami.Units.smallSpacing
Switch {
text: KI18n.i18nc("@option:check", "Use mpv.conf") text: KI18n.i18nc("@option:check", "Use mpv.conf")
checked: QtPotatoSettings.loadMpvConfig checked: QtPotatoSettings.loadMpvConfig
onClicked: { onClicked: {
@@ -114,7 +103,13 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Label {
text: KI18n.i18nc("@info", "Restart QtPotato to apply changes to this setting or to mpv.conf.")
wrapMode: Text.WordWrap
font: Kirigami.Theme.smallFont
color: Kirigami.Theme.disabledTextColor
Layout.fillWidth: true
}
Label { Label {
text: root.mpv.loadedUserConfigFiles.length > 0 text: root.mpv.loadedUserConfigFiles.length > 0
@@ -127,11 +122,6 @@ SettingsBasePage {
Layout.fillWidth: true Layout.fillWidth: true
} }
Item {
Layout.preferredWidth: 1
visible: root.mpv.skippedUserOptions.length > 0
}
Label { Label {
visible: root.mpv.skippedUserOptions.length > 0 visible: root.mpv.skippedUserOptions.length > 0
text: KI18n.i18nc("@info", "Ignored because they would break the built-in player: %1", root.mpv.skippedUserOptions.join(", ")) text: KI18n.i18nc("@info", "Ignored because they would break the built-in player: %1", root.mpv.skippedUserOptions.join(", "))
+71 -16
View File
@@ -14,10 +14,20 @@ Kirigami.Dialog {
required property var translator required property var translator
readonly property real removeButtonWidth: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2 readonly property real removeButtonWidth: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2
readonly property bool hasIncompleteTerms: {
for (let i = 0; i < glossaryModel.count; ++i) {
const entry = glossaryModel.get(i)
if (!entry.source.trim() || !entry.target.trim()) {
return true
}
}
return false
}
property string saveError: ""
// One inset for the header, content and footer so every edge lines up. // One inset for the header, content and footer so every edge lines up.
readonly property real inset: Kirigami.Units.largeSpacing readonly property real inset: Kirigami.Units.largeSpacing
title: KI18n.i18nc("@title:window", "Translation Glossary") title: KI18n.i18nc("@title:window", "Edit Translation Glossary")
standardButtons: Kirigami.Dialog.NoButton standardButtons: Kirigami.Dialog.NoButton
width: Math.min(parent.width * 0.9, Kirigami.Units.gridUnit * 38) width: Math.min(parent.width * 0.9, Kirigami.Units.gridUnit * 38)
height: Math.min(parent.height - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 30) height: Math.min(parent.height - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 30)
@@ -53,25 +63,28 @@ Kirigami.Dialog {
}) })
} }
function saveGlossary() {
const entries = []
for (let i = 0; i < glossaryModel.count; ++i) {
const entry = glossaryModel.get(i)
entries.push({ source: entry.source, target: entry.target })
}
if (!translator.saveGlossary(entries)) {
saveError = translator.glossaryError || KI18n.i18nc("@info", "Could not save the translation glossary. Check that the storage location is writable.")
return
}
saveError = ""
root.accept()
}
onOpened: { onOpened: {
saveError = ""
glossaryModel.clear() glossaryModel.clear()
for (const entry of translator.glossaryEntries) { for (const entry of translator.glossaryEntries) {
glossaryModel.append(entry) glossaryModel.append(entry)
} }
glossaryList.positionViewAtBeginning() glossaryList.positionViewAtBeginning()
} }
onAccepted: {
const entries = []
for (let i = 0; i < glossaryModel.count; ++i) {
const entry = glossaryModel.get(i)
entries.push({
source: entry.source,
target: entry.target
})
}
translator.saveGlossary(entries)
}
ListModel { ListModel {
id: glossaryModel id: glossaryModel
} }
@@ -94,7 +107,8 @@ Kirigami.Dialog {
Button { Button {
text: KI18n.i18nc("@action:button", "Save") text: KI18n.i18nc("@action:button", "Save")
icon.name: "document-save" icon.name: "document-save"
onClicked: root.accept() enabled: !root.hasIncompleteTerms
onClicked: root.saveGlossary()
} }
Button { Button {
@@ -112,9 +126,42 @@ Kirigami.Dialog {
onTriggered: root.addTerm() onTriggered: root.addTerm()
} }
contentItem: ColumnLayout { contentItem: ScrollView {
id: contentScroll
clip: true
contentWidth: availableWidth
contentHeight: contentColumn.height
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
// Fill the dialog normally; only scroll as a whole when the window is too
// short for the list's minimum height.
ColumnLayout {
id: contentColumn
width: contentScroll.availableWidth
height: Math.max(contentScroll.availableHeight, implicitHeight)
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: root.saveError.length > 0
type: Kirigami.MessageType.Error
text: root.saveError
showCloseButton: true
actions: Kirigami.Action {
text: KI18n.i18nc("@action:button", "Retry")
icon.name: "view-refresh"
onTriggered: root.saveGlossary()
}
}
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: root.hasIncompleteTerms
type: Kirigami.MessageType.Warning
text: KI18n.i18nc("@info", "Complete both fields for each term, or remove incomplete rows, before saving.")
showCloseButton: false
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
@@ -169,6 +216,8 @@ Kirigami.Dialog {
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.minimumHeight: Kirigami.Units.gridUnit * 6
Layout.preferredHeight: Kirigami.Units.gridUnit * 6
ScrollView { ScrollView {
id: listScroll id: listScroll
@@ -240,6 +289,8 @@ Kirigami.Dialog {
Layout.preferredWidth: 1 Layout.preferredWidth: 1
Layout.minimumWidth: 0 Layout.minimumWidth: 0
text: termRow.source text: termRow.source
// Show the start of long terms, not the end.
Component.onCompleted: cursorPosition = 0
placeholderText: KI18n.i18nc("@info:placeholder glossary source term", "Original term") placeholderText: KI18n.i18nc("@info:placeholder glossary source term", "Original term")
Accessible.name: KI18n.i18nc("@label:textbox glossary source term", "Original term") Accessible.name: KI18n.i18nc("@label:textbox glossary source term", "Original term")
onTextEdited: glossaryModel.setProperty(termRow.index, "source", text) onTextEdited: glossaryModel.setProperty(termRow.index, "source", text)
@@ -250,12 +301,14 @@ Kirigami.Dialog {
Layout.preferredWidth: 1 Layout.preferredWidth: 1
Layout.minimumWidth: 0 Layout.minimumWidth: 0
text: termRow.target text: termRow.target
// Show the start of long terms, not the end.
Component.onCompleted: cursorPosition = 0
placeholderText: KI18n.i18nc("@info:placeholder glossary translated term", "Translation") placeholderText: KI18n.i18nc("@info:placeholder glossary translated term", "Translation")
Accessible.name: KI18n.i18nc("@label:textbox glossary translated term", "Translation") Accessible.name: KI18n.i18nc("@label:textbox glossary translated term", "Translation")
onTextEdited: glossaryModel.setProperty(termRow.index, "target", text) onTextEdited: glossaryModel.setProperty(termRow.index, "target", text)
} }
ToolButton { Button {
Layout.preferredWidth: root.removeButtonWidth Layout.preferredWidth: root.removeButtonWidth
Layout.minimumWidth: root.removeButtonWidth Layout.minimumWidth: root.removeButtonWidth
Layout.maximumWidth: root.removeButtonWidth Layout.maximumWidth: root.removeButtonWidth
@@ -264,6 +317,7 @@ Kirigami.Dialog {
icon.name: "edit-delete" icon.name: "edit-delete"
icon.color: Kirigami.Theme.textColor icon.color: Kirigami.Theme.textColor
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
Accessible.name: text
text: KI18n.i18nc("@action:button remove glossary term", "Remove") text: KI18n.i18nc("@action:button remove glossary term", "Remove")
ToolTip.text: text ToolTip.text: text
ToolTip.visible: hovered ToolTip.visible: hovered
@@ -284,4 +338,5 @@ Kirigami.Dialog {
} }
} }
} }
}
} }
+6 -17
View File
@@ -13,7 +13,7 @@ ToolButton {
required property Actions actions required property Actions actions
required property MpvItem mpv required property MpvItem mpv
// In the footer the menu opens upwards and also offers what the header would. // In the footer the same menu opens upwards.
property bool inFooter: false property bool inFooter: false
readonly property alias isOpen: menu.visible readonly property alias isOpen: menu.visible
@@ -36,16 +36,8 @@ ToolButton {
modal: true modal: true
dim: false dim: false
AppMenuItem { AppMenuItem { action: root.actions.openFile }
action: root.actions.openFile AppMenuItem { action: root.actions.openUrl }
visible: root.inFooter
height: visible ? implicitHeight : 0
}
AppMenuItem {
action: root.actions.openUrl
visible: root.inFooter
height: visible ? implicitHeight : 0
}
RecentFilesMenu { actions: root.actions } RecentFilesMenu { actions: root.actions }
SubtitleTracksMenu { SubtitleTracksMenu {
mpv: root.mpv mpv: root.mpv
@@ -67,17 +59,14 @@ ToolButton {
AppMenuItem { action: root.actions.togglePlaylist } AppMenuItem { action: root.actions.togglePlaylist }
AppMenuItem { action: root.actions.screenshot } AppMenuItem { action: root.actions.screenshot }
MenuSeparator {} MenuSeparator {}
AppMenuItem { AppMenuItem { action: root.actions.configure }
action: root.actions.configure
visible: root.inFooter
height: visible ? implicitHeight : 0
}
AppMenuItem { action: root.actions.toggleMenuBar } AppMenuItem { action: root.actions.toggleMenuBar }
AppMenuItem { action: root.actions.about } AppMenuItem { action: root.actions.about }
MenuSeparator {} MenuSeparator {}
AppMenu { AppMenu {
title: KI18n.i18nc("@title:menu", "&More") title: KI18n.i18nc("@title:menu", "&More")
FileMenu { actions: root.actions } icon.name: "overflow-menu"
FileMenu { actions: root.actions; includeQuit: false }
ViewMenu { actions: root.actions } ViewMenu { actions: root.actions }
PlaybackMenu { actions: root.actions } PlaybackMenu { actions: root.actions }
VideoMenu { actions: root.actions } VideoMenu { actions: root.actions }
+38 -7
View File
@@ -12,6 +12,13 @@ import org.kde.kirigami as Kirigami
ToolBar { ToolBar {
id: root id: root
// Space activates the focused control before the window's playback action.
Keys.onShortcutOverride: event => {
if (event.key === Qt.Key_Space && event.modifiers === Qt.NoModifier) {
event.accepted = true
}
}
required property Actions actions required property Actions actions
required property MpvItem mpv required property MpvItem mpv
required property TranslationService translator required property TranslationService translator
@@ -19,7 +26,7 @@ ToolBar {
property bool shown: true property bool shown: true
property bool menuBarShown: true property bool menuBarShown: true
readonly property bool isSmallSize: width < 600 readonly property bool isSmallSize: width < Kirigami.Units.gridUnit * 34
readonly property int buttonSize: isSmallSize ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium readonly property int buttonSize: isSmallSize ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium
position: ToolBar.Header position: ToolBar.Header
@@ -27,29 +34,39 @@ ToolBar {
RowLayout { RowLayout {
width: parent.width width: parent.width
spacing: Kirigami.Units.mediumSpacing
HamburgerMenu { HamburgerMenu {
actions: root.actions actions: root.actions
mpv: root.mpv mpv: root.mpv
visible: !root.menuBarShown visible: !root.menuBarShown
focusPolicy: Qt.StrongFocus
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
} }
ToolButton { ToolButton {
action: root.actions.openFile action: root.actions.openFile
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
} }
ToolButton { ToolButton {
action: root.actions.openUrl action: root.actions.openUrl
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
} }
ToolSeparator {} ToolSeparator {}
@@ -60,10 +77,15 @@ ToolBar {
icon.name: "add-subtitle" icon.name: "add-subtitle"
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
enabled: root.mpv.fileLoaded enabled: root.mpv.fileLoaded
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
Accessible.role: Accessible.ButtonMenu
down: subtitleMenu.visible down: subtitleMenu.visible
onPressed: subtitleMenu.open() onPressed: subtitleMenu.open()
@@ -89,10 +111,15 @@ ToolBar {
icon.name: "audio-volume-high" icon.name: "audio-volume-high"
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
enabled: root.mpv.audioTracks.length > 0 enabled: root.mpv.audioTracks.length > 0
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
Accessible.role: Accessible.ButtonMenu
down: audioMenu.visible down: audioMenu.visible
onPressed: audioMenu.open() onPressed: audioMenu.open()
@@ -135,7 +162,7 @@ ToolBar {
ToolButton { ToolButton {
action: root.actions.cancelTranslation action: root.actions.cancelTranslation
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
ToolTip.text: text ToolTip.text: text
ToolTip.visible: hovered ToolTip.visible: hovered
@@ -149,10 +176,14 @@ ToolBar {
ToolButton { ToolButton {
action: root.actions.configure action: root.actions.configure
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
} }
} }
+1
View File
@@ -8,6 +8,7 @@ import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "help-contents"
required property Actions actions required property Actions actions
+53 -16
View File
@@ -5,46 +5,83 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.ki18n
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
// Single-line text popup, used for Open URL. Kirigami.PromptDialog {
Popup {
id: root id: root
property string lastText: "" property string lastText: ""
property string placeholderText: "" property string placeholderText: ""
property string buttonText: "" property string buttonText: ""
property string errorText: ""
readonly property bool validUrl: isValidUrl(textField.text.trim())
signal submitted(text: string) signal submitted(text: string)
function isValidUrl(text: string): bool {
// Network URLs need both a scheme and a host. Reject whitespace, missing
// authorities, empty labels, and ports outside the TCP/UDP range.
const match = /^([a-z][a-z0-9+.-]*):\/\/([^/?#\s]+)(?:[/?#][^\s]*)?$/i.exec(text)
if (!match) return false
const authority = match[2].substring(match[2].lastIndexOf("@") + 1)
const hostPort = /^(\[[0-9a-f:.]+\]|[^:@\[\]]+)(?::([0-9]+))?$/i.exec(authority)
if (!hostPort || (hostPort[2] !== undefined && (+hostPort[2] < 1 || +hostPort[2] > 65535))) return false
const host = hostPort[1]
return host.startsWith("[") || host.split(".").every(label => /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label))
}
function submit() {
if (!validUrl) {
errorText = KI18n.i18nc("@info:usagetip", "Enter a URL with a scheme and a host, such as https://example.org/video.mkv.")
textField.forceActiveFocus()
return
}
submitted(textField.text.trim())
close()
}
title: KI18n.i18nc("@title:window", "Open URL")
modal: true modal: true
standardButtons: Kirigami.Dialog.Cancel
customFooterActions: Kirigami.Action {
text: root.buttonText
icon.name: "document-open"
enabled: root.validUrl
onTriggered: root.submit()
}
onOpened: { onOpened: {
errorText = ""
textField.text = root.lastText textField.text = root.lastText
textField.forceActiveFocus(Qt.MouseFocusReason) textField.forceActiveFocus(Qt.OtherFocusReason)
textField.selectAll() textField.selectAll()
} }
contentItem: RowLayout { ColumnLayout {
spacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Label {
text: KI18n.i18nc("@label:textbox", "URL:")
Accessible.ignored: true
}
TextField { TextField {
id: textField id: textField
placeholderText: root.placeholderText placeholderText: root.placeholderText
Accessible.name: KI18n.i18nc("@label:textbox", "URL")
inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing onAccepted: root.submit()
onAccepted: submitButton.clicked() onTextEdited: root.errorText = ""
Keys.onEscapePressed: root.close()
} }
Button { Kirigami.InlineMessage {
id: submitButton Layout.fillWidth: true
text: root.buttonText type: Kirigami.MessageType.Error
enabled: textField.text.trim().length > 0 text: root.errorText
Layout.rightMargin: Kirigami.Units.largeSpacing visible: text.length > 0
onClicked: {
root.submitted(textField.text.trim())
root.close()
}
} }
} }
} }
+8 -9
View File
@@ -50,10 +50,10 @@ ApplicationWindow {
visible: true visible: true
title: video.mediaTitle || KI18n.i18nc("@title:window", "QtPotato") title: video.mediaTitle || KI18n.i18nc("@title:window", "QtPotato")
width: 1000 width: Kirigami.Units.gridUnit * 55
height: 600 height: Kirigami.Units.gridUnit * 33
minimumWidth: 400 minimumWidth: Kirigami.Units.gridUnit * 23
minimumHeight: 200 minimumHeight: Kirigami.Units.gridUnit * 11
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
onVisibilityChanged: visibility => { onVisibilityChanged: visibility => {
@@ -180,7 +180,7 @@ ApplicationWindow {
if (!QtPotatoSettings.playlistToggleWithMouse) { if (!QtPotatoSettings.playlistToggleWithMouse) {
return return
} }
const atEdge = playlistPanel.edge === Qt.RightEdge ? x > width - 50 : x < 50 const atEdge = playlistPanel.edge === Qt.RightEdge ? x > width - Kirigami.Units.gridUnit * 3 : x < Kirigami.Units.gridUnit * 3
playlistPanel.state = atEdge ? "visible" : "hidden" playlistPanel.state = atEdge ? "visible" : "hidden"
} }
onPointerLeft: footerItem.pointerLeft() onPointerLeft: footerItem.pointerLeft()
@@ -266,11 +266,10 @@ ApplicationWindow {
InputPopup { InputPopup {
id: openUrlPopup id: openUrlPopup
x: Kirigami.Units.largeSpacing title: KI18n.i18nc("@title:window", "Open URL")
y: Kirigami.Units.largeSpacing preferredWidth: Kirigami.Units.gridUnit * 30
width: Math.min(appWindow.width * 0.9, 600)
lastText: QtPotatoSettings.lastUrl lastText: QtPotatoSettings.lastUrl
placeholderText: "https://" placeholderText: "https://example.org/video.mkv"
buttonText: KI18n.i18nc("@action:button", "Open") buttonText: KI18n.i18nc("@action:button", "Open")
onSubmitted: text => { onSubmitted: text => {
QtPotatoSettings.lastUrl = text QtPotatoSettings.lastUrl = text
+1
View File
@@ -18,6 +18,7 @@ AppMenu {
AppMenuItem { action: root.actions.playPreviousFile } AppMenuItem { action: root.actions.playPreviousFile }
AppMenu { AppMenu {
title: KI18n.i18nc("@title:menu", "Playback Speed") title: KI18n.i18nc("@title:menu", "Playback Speed")
icon.name: "speedometer"
AppMenuItem { action: root.actions.speedUp } AppMenuItem { action: root.actions.speedUp }
AppMenuItem { action: root.actions.speedDown } AppMenuItem { action: root.actions.speedDown }
AppMenuItem { action: root.actions.speedReset } AppMenuItem { action: root.actions.speedReset }
+5 -1
View File
@@ -30,6 +30,9 @@ MpvItem {
setPropertyAsync("sub-outline-size", QtPotatoSettings.subtitleOutlineSize) setPropertyAsync("sub-outline-size", QtPotatoSettings.subtitleOutlineSize)
} }
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
loadUserConfig: QtPotatoSettings.loadMpvConfig loadUserConfig: QtPotatoSettings.loadMpvConfig
// Set once the saved volume is applied; mpv reports its default volume before that. // Set once the saved volume is applied; mpv reports its default volume before that.
@@ -55,7 +58,7 @@ MpvItem {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "black" color: Kirigami.Theme.backgroundColor
visible: !root.fileLoaded visible: !root.fileLoaded
z: -1 z: -1
} }
@@ -95,6 +98,7 @@ MpvItem {
onExited: root.pointerLeft() onExited: root.pointerLeft()
onPressed: mouse => { onPressed: mouse => {
root.forceActiveFocus(Qt.MouseFocusReason)
switch (mouse.button) { switch (mouse.button) {
case Qt.LeftButton: case Qt.LeftButton:
root.actions.playPause.trigger() root.actions.playPause.trigger()
+2
View File
@@ -8,6 +8,7 @@ import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "media-playback-start"
required property Actions actions required property Actions actions
@@ -25,6 +26,7 @@ AppMenu {
MenuSeparator {} MenuSeparator {}
AppMenu { AppMenu {
title: KI18n.i18nc("@title:menu", "Seek") title: KI18n.i18nc("@title:menu", "Seek")
icon.name: "go-jump"
AppMenuItem { action: root.actions.seekForwardSmall } AppMenuItem { action: root.actions.seekForwardSmall }
AppMenuItem { action: root.actions.seekBackwardSmall } AppMenuItem { action: root.actions.seekBackwardSmall }
MenuSeparator {} MenuSeparator {}
+6 -25
View File
@@ -5,27 +5,21 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
SettingsBasePage { SettingsBasePage {
id: root id: root
GridLayout { Kirigami.FormLayout {
columns: 2
SettingsHeader { SettingsHeader {
text: KI18n.i18nc("@title", "Seeking") text: KI18n.i18nc("@title", "Seeking")
topMargin: 0
Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Small step")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Small step")
from: 1 from: 1
to: 600 to: 600
value: QtPotatoSettings.seekSmallStep value: QtPotatoSettings.seekSmallStep
@@ -37,12 +31,8 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Medium step")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Medium step")
from: 1 from: 1
to: 600 to: 600
value: QtPotatoSettings.seekMediumStep value: QtPotatoSettings.seekMediumStep
@@ -54,12 +44,8 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Big step")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Big step")
from: 1 from: 1
to: 600 to: 600
value: QtPotatoSettings.seekBigStep value: QtPotatoSettings.seekBigStep
@@ -73,16 +59,11 @@ SettingsBasePage {
SettingsHeader { SettingsHeader {
text: KI18n.i18nc("@title", "Audio") text: KI18n.i18nc("@title", "Audio")
Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Volume step")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Volume step")
from: 1 from: 1
to: 100 to: 100
value: QtPotatoSettings.volumeStep value: QtPotatoSettings.volumeStep
+8 -8
View File
@@ -20,13 +20,13 @@ Page {
property bool fullscreen: false property bool fullscreen: false
readonly property int edge: QtPotatoSettings.playlistPosition === "left" ? Qt.LeftEdge : Qt.RightEdge readonly property int edge: QtPotatoSettings.playlistPosition === "left" ? Qt.LeftEdge : Qt.RightEdge
readonly property int buttonSize: Kirigami.Units.iconSizes.small readonly property int buttonSize: Kirigami.Units.iconSizes.smallMedium
property real customWidth: QtPotatoSettings.playlistWidth property real customWidth: QtPotatoSettings.playlistWidth
signal addFilesRequested() signal addFilesRequested()
function limitWidth(width: real): real { function limitWidth(width: real): real {
return Math.min(Math.max(width, 260), mainWindowWidth - 50) return Math.min(Math.max(width, Kirigami.Units.gridUnit * 13), mainWindowWidth - Kirigami.Units.gridUnit * 2.5)
} }
function toggle() { function toggle() {
@@ -63,7 +63,7 @@ Page {
icon.name: "list-add" icon.name: "list-add"
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
onClicked: root.addFilesRequested() onClicked: root.addFilesRequested()
} }
@@ -73,7 +73,7 @@ Page {
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
enabled: root.playlist.playingIndex >= 0 enabled: root.playlist.playingIndex >= 0
onClicked: playlistView.positionViewAtIndex(root.playlist.playingIndex, ListView.Center) onClicked: playlistView.positionViewAtIndex(root.playlist.playingIndex, ListView.Center)
@@ -88,7 +88,7 @@ Page {
icon.width: root.buttonSize icon.width: root.buttonSize
icon.height: root.buttonSize icon.height: root.buttonSize
display: AbstractButton.IconOnly display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
enabled: root.playlist.count > 0 enabled: root.playlist.count > 0
onClicked: root.playlist.clear() onClicked: root.playlist.clear()
@@ -141,7 +141,7 @@ Page {
ResizeHandler { ResizeHandler {
id: resizeHandlerTall id: resizeHandlerTall
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: 8 width: Kirigami.Units.smallSpacing * 2
height: parent.height height: parent.height
onMoved: delta => root.customWidth = root.limitWidth(root.customWidth + (root.edge === Qt.RightEdge ? -delta : delta)) onMoved: delta => root.customWidth = root.limitWidth(root.customWidth + (root.edge === Qt.RightEdge ? -delta : delta))
onReleased: { onReleased: {
@@ -152,8 +152,8 @@ Page {
Rectangle { Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
width: 5 width: Kirigami.Units.smallSpacing
height: 50 height: Kirigami.Units.gridUnit * 2.5
color: resizeHandlerTall.hovered ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor color: resizeHandlerTall.hovered ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
border.width: 1 border.width: 1
+40 -2
View File
@@ -22,6 +22,25 @@ Item {
readonly property bool hovered: hoverHandler.hovered readonly property bool hovered: hoverHandler.hovered
readonly property real fontSize: Kirigami.Theme.defaultFont.pointSize readonly property real fontSize: Kirigami.Theme.defaultFont.pointSize
activeFocusOnTab: true
function activate() {
root.playlist.play(root.index)
}
function showContextMenu() {
contextMenu.popup()
}
Keys.onReturnPressed: root.activate()
Keys.onEnterPressed: root.activate()
Keys.onMenuPressed: root.showContextMenu()
Keys.onPressed: event => {
if (event.key === Qt.Key_F10 && (event.modifiers & Qt.ShiftModifier)) {
root.showContextMenu()
event.accepted = true
}
}
function labelColor(): color { function labelColor(): color {
if (root.isPlaying && !root.hovered) { if (root.isPlaying && !root.hovered) {
@@ -39,6 +58,7 @@ Item {
TapHandler { TapHandler {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
onTapped: root.forceActiveFocus(Qt.MouseFocusReason)
onDoubleTapped: root.playlist.play(root.index) onDoubleTapped: root.playlist.play(root.index)
} }
@@ -60,6 +80,9 @@ Item {
return Qt.alpha(Kirigami.Theme.backgroundColor, root.alpha) return Qt.alpha(Kirigami.Theme.backgroundColor, root.alpha)
} }
border.width: root.activeFocus ? 2 : 0
border.color: Kirigami.Theme.focusColor
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
@@ -95,8 +118,8 @@ Item {
color: root.labelColor() color: root.labelColor()
visible: root.isPlaying visible: root.isPlaying
Layout.leftMargin: QtPotatoSettings.playlistShowRowNumber ? 0 : Kirigami.Units.largeSpacing Layout.leftMargin: QtPotatoSettings.playlistShowRowNumber ? 0 : Kirigami.Units.largeSpacing
Layout.preferredWidth: Kirigami.Units.iconSizes.small Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.small Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
} }
LabelWithTooltip { LabelWithTooltip {
@@ -105,11 +128,26 @@ Item {
color: root.labelColor() color: root.labelColor()
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: root.fontSize font.pointSize: root.fontSize
font.bold: root.isPlaying
verticalAlignment: Qt.AlignVCenter verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: !QtPotatoSettings.playlistShowRowNumber && !root.isPlaying ? Kirigami.Units.largeSpacing : 0 Layout.leftMargin: !QtPotatoSettings.playlistShowRowNumber && !root.isPlaying ? Kirigami.Units.largeSpacing : 0
Layout.rightMargin: Kirigami.Units.largeSpacing Layout.rightMargin: Kirigami.Units.largeSpacing
} }
Button {
text: KI18n.i18nc("@action:button remove playlist item", "Remove")
icon.name: "list-remove"
display: AbstractButton.IconOnly
flat: true
Accessible.name: text
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2
Layout.rightMargin: Kirigami.Units.smallSpacing
onClicked: root.playlist.remove(root.index)
}
} }
} }
+15 -25
View File
@@ -5,20 +5,18 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
SettingsBasePage { SettingsBasePage {
id: root id: root
GridLayout { Kirigami.FormLayout {
columns: 2
Label {
text: KI18n.i18nc("@label:listbox", "Position")
Layout.alignment: Qt.AlignRight
}
ComboBox { ComboBox {
Layout.fillWidth: true
Layout.minimumWidth: 0
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Position")
textRole: "text" textRole: "text"
valueRole: "value" valueRole: "value"
model: [ model: [
@@ -32,12 +30,10 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:listbox", "When a file ends")
Layout.alignment: Qt.AlignRight
}
ComboBox { ComboBox {
Layout.fillWidth: true
Layout.minimumWidth: 0
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "When a file ends")
textRole: "text" textRole: "text"
valueRole: "value" valueRole: "value"
model: [ model: [
@@ -53,11 +49,10 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 }
RowLayout { RowLayout {
CheckBox { spacing: Kirigami.Units.smallSpacing
text: KI18n.i18nc("@option:check", "Add the other files of the folder") Switch {
text: KI18n.i18nc("@option:check", "Add other files from the folder")
checked: QtPotatoSettings.playlistLoadSiblings checked: QtPotatoSettings.playlistLoadSiblings
onClicked: { onClicked: {
QtPotatoSettings.playlistLoadSiblings = checked QtPotatoSettings.playlistLoadSiblings = checked
@@ -70,9 +65,7 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Switch {
CheckBox {
text: KI18n.i18nc("@option:check", "Overlay video") text: KI18n.i18nc("@option:check", "Overlay video")
checked: QtPotatoSettings.playlistOverlayVideo checked: QtPotatoSettings.playlistOverlayVideo
onClicked: { onClicked: {
@@ -81,10 +74,9 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 }
RowLayout { RowLayout {
CheckBox { spacing: Kirigami.Units.smallSpacing
Switch {
text: KI18n.i18nc("@option:check", "Open with the mouse") text: KI18n.i18nc("@option:check", "Open with the mouse")
checked: QtPotatoSettings.playlistToggleWithMouse checked: QtPotatoSettings.playlistToggleWithMouse
onClicked: { onClicked: {
@@ -98,9 +90,7 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Switch {
CheckBox {
text: KI18n.i18nc("@option:check", "Show row numbers") text: KI18n.i18nc("@option:check", "Show row numbers")
checked: QtPotatoSettings.playlistShowRowNumber checked: QtPotatoSettings.playlistShowRowNumber
onClicked: { onClicked: {
+4 -1
View File
@@ -6,10 +6,12 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "document-open-recent"
required property Actions actions required property Actions actions
@@ -23,11 +25,12 @@ AppMenu {
required property string name required property string name
required property string path required property string path
text: name text: name
textElide: Text.ElideMiddle
onTriggered: root.actions.fileOpenRequested(url) onTriggered: root.actions.fileOpenRequested(url)
ToolTip.text: path ToolTip.text: path
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.delay: 1000 ToolTip.delay: Kirigami.Units.toolTipDelay
} }
onObjectAdded: (index, object) => root.insertItem(index, object) onObjectAdded: (index, object) => root.insertItem(index, object)
onObjectRemoved: (index, object) => root.removeItem(object) onObjectRemoved: (index, object) => root.removeItem(object)
+32 -42
View File
@@ -10,7 +10,7 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
// Flat progress bar of Haruna's footer: click or drag to seek, hover for the time, // Playback position: click or drag to seek, hover for the time,
// chapter marks, right click for the chapter list, wheel to seek. // chapter marks, right click for the chapter list, wheel to seek.
Slider { Slider {
id: root id: root
@@ -19,7 +19,7 @@ Slider {
required property Actions actions required property Actions actions
readonly property alias chaptersPopupOpen: chaptersPopup.visible readonly property alias chaptersPopupOpen: chaptersPopup.visible
readonly property bool showChapterMarkers: root.mpv.chapters.length * 20 < width readonly property bool showChapterMarkers: root.mpv.chapters.length * Kirigami.Units.gridUnit < width
// Playing state before a drag started; playback pauses while dragging. // Playing state before a drag started; playback pauses while dragging.
property bool pausedBeforeDrag: false property bool pausedBeforeDrag: false
@@ -47,64 +47,55 @@ Slider {
return chapter ? `${formatTime(time)} - ${chapter}` : formatTime(time) return chapter ? `${formatTime(time)} - ${chapter}` : formatTime(time)
} }
Accessible.name: KI18n.i18nc("@label:slider", "Playback position")
Accessible.description: timeLabel(value)
stepSize: 1
from: 0 from: 0
to: Math.max(root.mpv.duration, 0) to: Math.max(root.mpv.duration, 0)
enabled: root.mpv.fileLoaded && root.mpv.duration > 0 enabled: root.mpv.fileLoaded && root.mpv.duration > 0
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
leftPadding: 0
rightPadding: 0
handle: Item { visible: false } // Preserve the Slider's own arrow-key behavior when playback shortcuts exist.
Keys.onShortcutOverride: event => {
background: Rectangle { if (event.modifiers === Qt.NoModifier && [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End].includes(event.key)) {
id: progressBarBackground event.accepted = true
}
color: Kirigami.Theme.alternateBackgroundColor }
scale: root.mirrored ? -1 : 1 Keys.onPressed: event => {
if (event.modifiers === Qt.NoModifier && (event.key === Qt.Key_Home || event.key === Qt.Key_End)) {
Rectangle { value = event.key === Qt.Key_Home ? from : to
width: root.visualPosition * parent.width moved()
height: parent.height event.accepted = true
color: Kirigami.Theme.highlightColor }
} }
Repeater { Repeater {
model: root.showChapterMarkers ? root.mpv.chapters : [] model: root.showChapterMarkers ? root.mpv.chapters : []
delegate: Rectangle { delegate: Rectangle {
required property var modelData required property var modelData
x: modelData.time / root.mpv.duration * progressBarBackground.width x: root.leftPadding + (root.mirrored ? 1 - modelData.time / root.to : modelData.time / root.to) * root.availableWidth
width: 2 anchors.verticalCenter: parent.verticalCenter
height: progressBarBackground.height width: Kirigami.Units.smallSpacing / 2
height: Kirigami.Units.smallSpacing
color: Kirigami.Theme.textColor color: Kirigami.Theme.textColor
} }
} }
Rectangle { ToolTip {
id: timeToolTip id: timeToolTip
parent: root
property real pointerX: 0 property real pointerX: 0
x: Math.max(0, Math.min(parent.width - width, pointerX - width / 2)) x: {
const windowX = root.mapToItem(root.Window.window.contentItem, 0, 0).x
return Math.max(Kirigami.Units.largeSpacing - windowX,
Math.min(root.Window.window.width - width - Kirigami.Units.largeSpacing - windowX, pointerX - width / 2))
}
y: -height - Kirigami.Units.largeSpacing y: -height - Kirigami.Units.largeSpacing
z: 10 width: Math.min(implicitWidth, root.Window.window.width - Kirigami.Units.largeSpacing * 2)
width: timeLabel.implicitWidth + Kirigami.Units.smallSpacing * 2
height: timeLabel.implicitHeight + Kirigami.Units.smallSpacing * 2
visible: (mouseArea.containsMouse || root.pressed) && root.mpv.duration > 0 visible: (mouseArea.containsMouse || root.pressed) && root.mpv.duration > 0
color: Kirigami.Theme.backgroundColor text: root.timeLabel(root.pressed ? root.value : pointerX / Math.max(1, root.width) * root.to)
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 { MouseArea {
@@ -134,7 +125,6 @@ Slider {
} }
} }
} }
}
onMoved: root.mpv.seek(value) onMoved: root.mpv.seek(value)
+1
View File
@@ -6,6 +6,7 @@ import org.kde.kirigami as Kirigami
// Page of the settings window. Changes are saved as they are made. // Page of the settings window. Changes are saved as they are made.
Kirigami.ScrollablePage { Kirigami.ScrollablePage {
padding: Kirigami.Units.largeSpacing
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
Component.onDestruction: QtPotatoSettings.save() Component.onDestruction: QtPotatoSettings.save()
} }
+4 -28
View File
@@ -2,36 +2,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
// Section title of a settings page, between two lines. // FormLayout supplies the standard section heading and group spacing.
ColumnLayout { Item {
id: root
property string text: "" property string text: ""
property int topMargin: Kirigami.Units.gridUnit
spacing: 0 Kirigami.FormData.label: text
Kirigami.FormData.isSection: true
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
}
}
} }
+11 -9
View File
@@ -8,33 +8,35 @@ import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "settings-configure"
required property Actions actions required property Actions actions
title: KI18n.i18nc("@title:menu", "&Settings") title: KI18n.i18nc("@title:menu", "&Settings")
AppMenuItem { AppMenuItem {
text: KI18n.i18nc("@action:inmenu", "General") text: KI18n.i18nc("@action:inmenu", "General…")
icon.name: "configure"
onTriggered: root.actions.settingsRequested("general") onTriggered: root.actions.settingsRequested("general")
} }
AppMenuItem { AppMenuItem {
text: KI18n.i18nc("@action:inmenu", "Playback") text: KI18n.i18nc("@action:inmenu", "Playback…")
icon.name: "preferences-media-playback-amarok"
onTriggered: root.actions.settingsRequested("playback") onTriggered: root.actions.settingsRequested("playback")
} }
AppMenuItem { AppMenuItem {
text: KI18n.i18nc("@action:inmenu", "Subtitles") text: KI18n.i18nc("@action:inmenu", "Subtitles…")
icon.name: "preferences-desktop-font"
onTriggered: root.actions.settingsRequested("subtitles") onTriggered: root.actions.settingsRequested("subtitles")
} }
AppMenuItem { AppMenuItem {
text: KI18n.i18nc("@action:inmenu", "Translation") text: KI18n.i18nc("@action:inmenu", "Translation…")
icon.name: "translate"
onTriggered: root.actions.settingsRequested("translation") onTriggered: root.actions.settingsRequested("translation")
} }
AppMenuItem { AppMenuItem {
text: KI18n.i18nc("@action:inmenu", "Playlist") text: KI18n.i18nc("@action:inmenu", "Playlist…")
icon.name: "view-media-playlist"
onTriggered: root.actions.settingsRequested("playlist") onTriggered: root.actions.settingsRequested("playlist")
} }
AppMenuItem {
text: KI18n.i18nc("@action:inmenu", "About")
onTriggered: root.actions.settingsRequested("about")
}
} }
+5 -4
View File
@@ -109,10 +109,10 @@ Loader {
property Item currentPage: null property Item currentPage: null
width: 800 width: Kirigami.Units.gridUnit * 45
height: 600 height: Kirigami.Units.gridUnit * 34
minimumWidth: 750 minimumWidth: Kirigami.Units.gridUnit * 41
minimumHeight: 400 minimumHeight: Kirigami.Units.gridUnit * 22
title: KI18n.i18nc("@title:window", "Settings") title: KI18n.i18nc("@title:window", "Settings")
visible: true visible: true
@@ -132,6 +132,7 @@ Loader {
text: modelData.text text: modelData.text
icon.name: modelData.iconName icon.name: modelData.iconName
highlighted: index === ListView.view.currentIndex highlighted: index === ListView.view.currentIndex
font.bold: highlighted
onClicked: settingsWindow.showPage(modelData.name) onClicked: settingsWindow.showPage(modelData.name)
} }
} }
+11 -2
View File
@@ -11,6 +11,7 @@ import org.kde.ki18n
// Checkable list of subtitle tracks, for the primary or the secondary subtitle. // Checkable list of subtitle tracks, for the primary or the secondary subtitle.
AppMenu { AppMenu {
id: root id: root
icon.name: isPrimary ? "text-x-generic" : "text-x-script"
required property MpvItem mpv required property MpvItem mpv
property bool isPrimary: true property bool isPrimary: true
@@ -36,13 +37,16 @@ AppMenu {
enabled: mpv.subtitleTracks.length > 0 enabled: mpv.subtitleTracks.length > 0
ButtonGroup { id: tracksGroup; exclusive: true }
AppMenuItem { AppMenuItem {
text: KI18n.i18nc("@item:inmenu no subtitle", "None") text: KI18n.i18nc("@item:inmenu no subtitle", "None")
checkable: true checkable: true
ButtonGroup.group: tracksGroup
checked: root.selectedId === 0 checked: root.selectedId === 0
onTriggered: { onTriggered: {
root.select(0) root.select(0)
// Clicking a checked item unchecks it, which drops the binding. // Keep mpv authoritative after MenuItem updates its checked state.
checked = Qt.binding(() => root.selectedId === 0) checked = Qt.binding(() => root.selectedId === 0)
} }
} }
@@ -50,13 +54,18 @@ AppMenu {
Instantiator { Instantiator {
model: root.mpv.subtitleTracks model: root.mpv.subtitleTracks
delegate: AppMenuItem { delegate: AppMenuItem {
id: trackItem
required property var modelData required property var modelData
text: root.trackLabel(modelData) text: root.trackLabel(modelData)
checkable: true checkable: true
ButtonGroup.group: tracksGroup
checked: modelData.id === root.selectedId checked: modelData.id === root.selectedId
// mpv cannot show one track in both places. // mpv cannot show one track in both places.
enabled: modelData.id !== root.otherId enabled: modelData.id !== root.otherId
onTriggered: root.select(modelData.id) onTriggered: {
root.select(modelData.id)
checked = Qt.binding(() => trackItem.modelData.id === root.selectedId)
}
} }
onObjectAdded: (index, object) => root.insertItem(index + 1, object) onObjectAdded: (index, object) => root.insertItem(index + 1, object)
onObjectRemoved: (index, object) => root.removeItem(object) onObjectRemoved: (index, object) => root.removeItem(object)
+6 -5
View File
@@ -8,12 +8,14 @@ import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "media-view-subtitles-symbolic"
required property Actions actions required property Actions actions
required property MpvItem mpv required property MpvItem mpv
title: KI18n.i18nc("@title:menu", "&Subtitles") title: KI18n.i18nc("@title:menu", "&Subtitles")
SubtitleTracksMenu { SubtitleTracksMenu {
mpv: root.mpv mpv: root.mpv
isPrimary: true isPrimary: true
@@ -27,18 +29,17 @@ AppMenu {
MenuSeparator {} MenuSeparator {}
AppMenuItem { action: root.actions.translateSubtitle } AppMenuItem { action: root.actions.translateSubtitle }
AppMenuItem { action: root.actions.editGlossary } AppMenuItem { action: root.actions.editGlossary }
AppMenuItem { AppMenuItem { action: root.actions.cancelTranslation }
action: root.actions.cancelTranslation
visible: root.actions.cancelTranslation.enabled
height: visible ? implicitHeight : 0
}
MenuSeparator {} MenuSeparator {}
AppMenuItem { action: root.actions.addSubtitle } AppMenuItem { action: root.actions.addSubtitle }
MenuSeparator {}
AppMenuItem { action: root.actions.subtitleBigger } AppMenuItem { action: root.actions.subtitleBigger }
AppMenuItem { action: root.actions.subtitleSmaller } AppMenuItem { action: root.actions.subtitleSmaller }
MenuSeparator {}
AppMenuItem { action: root.actions.subtitleEarlier } AppMenuItem { action: root.actions.subtitleEarlier }
AppMenuItem { action: root.actions.subtitleLater } AppMenuItem { action: root.actions.subtitleLater }
AppMenuItem { action: root.actions.subtitleDelayReset } AppMenuItem { action: root.actions.subtitleDelayReset }
MenuSeparator {}
AppMenuItem { action: root.actions.subtitleToggle } AppMenuItem { action: root.actions.subtitleToggle }
AppMenuItem { action: root.actions.subtitleCycleUp } AppMenuItem { action: root.actions.subtitleCycleUp }
AppMenuItem { action: root.actions.subtitleCycleDown } AppMenuItem { action: root.actions.subtitleCycleDown }
+7 -12
View File
@@ -5,21 +5,20 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
SettingsBasePage { SettingsBasePage {
id: root id: root
GridLayout { Kirigami.FormLayout {
columns: 2
Label {
text: KI18n.i18nc("@label:spinbox", "Font size")
Layout.alignment: Qt.AlignRight
}
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Font size")
Kirigami.FormData.buddyFor: fontSizeSpin
SpinBox { SpinBox {
id: fontSizeSpin
from: 20 from: 20
to: 150 to: 150
stepSize: 2 stepSize: 2
@@ -35,12 +34,8 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Outline size")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Outline size")
from: 0 from: 0
to: 10 to: 10
value: QtPotatoSettings.subtitleOutlineSize value: QtPotatoSettings.subtitleOutlineSize
+15 -6
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick import QtQuick
import org.kde.ki18n
import QtQuick.Controls import QtQuick.Controls
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -9,10 +10,21 @@ import org.kde.kirigami as Kirigami
ToolButton { ToolButton {
id: root id: root
// Space activates the focused control before the window's playback action.
Keys.onShortcutOverride: event => {
if (event.key === Qt.Key_Space && event.modifiers === Qt.NoModifier) {
event.accepted = true
}
}
required property string toolTipText required property string toolTipText
icon.name: "documentinfo" icon.name: "documentinfo"
focusPolicy: Qt.NoFocus text: KI18n.i18nc("@action:button", "Help")
display: AbstractButton.IconOnly
Accessible.name: text
Accessible.description: toolTipText
focusPolicy: Qt.StrongFocus
checkable: true checkable: true
ToolTip { ToolTip {
@@ -21,11 +33,8 @@ ToolButton {
delay: root.checked ? 0 : Kirigami.Units.toolTipDelay delay: root.checked ? 0 : Kirigami.Units.toolTipDelay
timeout: -1 timeout: -1
closePolicy: Popup.NoAutoClose closePolicy: Popup.NoAutoClose
contentItem: Label {
text: root.toolTipText text: root.toolTipText
wrapMode: Text.WordWrap x: (root.width - width) / 2
width: Math.min(implicitWidth, Kirigami.Units.gridUnit * 20) width: Math.min(implicitWidth, Kirigami.Units.gridUnit * 20, root.Window.window.width - Kirigami.Units.largeSpacing * 2)
}
width: Math.min(contentItem.implicitWidth, Kirigami.Units.gridUnit * 20) + leftPadding + rightPadding
} }
} }
+89 -59
View File
@@ -13,44 +13,73 @@ SettingsBasePage {
required property TranslationService translator required property TranslationService translator
ColumnLayout { ColumnLayout {
spacing: Kirigami.Units.largeSpacing
Label {
text: KI18n.i18nc("@info", "Use an OpenAI-compatible API to translate subtitles. Tracks are translated in batches and saved for reuse.")
wrapMode: Text.WordWrap
Layout.preferredWidth: form.implicitWidth
Layout.alignment: Qt.AlignHCenter
}
Kirigami.InlineMessage { Kirigami.InlineMessage {
Layout.fillWidth: true Layout.preferredWidth: form.implicitWidth
Layout.alignment: Qt.AlignHCenter
type: Kirigami.MessageType.Error type: Kirigami.MessageType.Error
text: root.translator.errorText || ApiKeyStore.error text: root.translator.errorText
visible: text.length > 0 visible: text.length > 0
showCloseButton: true showCloseButton: true
onVisibleChanged: if (!visible) root.translator.clearError() onVisibleChanged: if (!visible) root.translator.clearError()
} }
GridLayout { Kirigami.FormLayout {
columns: 2 id: form
Label {
text: KI18n.i18nc("@label:textbox", "API base URL")
Layout.alignment: Qt.AlignRight
}
TextField { TextField {
Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "API base URL")
id: apiUrlField
// Keep an invalid draft in the field; only valid URLs reach KConfig.
property bool edited: false
readonly property bool valid: /^https?:\/\/(?:\[[0-9a-f:.]+\]|[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.?)(?::[0-9]+)?(?:[/?#][^\s]*)?$/i.test(text.trim())
function saveUrl() {
if (valid) {
QtPotatoSettings.apiBaseUrl = text.trim()
QtPotatoSettings.save()
}
}
placeholderText: "https://example.com/v1" placeholderText: "https://example.com/v1"
text: QtPotatoSettings.apiBaseUrl text: QtPotatoSettings.apiBaseUrl
inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase
Layout.fillWidth: true Layout.fillWidth: true
onTextEdited: QtPotatoSettings.apiBaseUrl = text.trim() Layout.preferredWidth: Kirigami.Units.gridUnit * 19
onEditingFinished: QtPotatoSettings.save() onTextEdited: edited = true
onEditingFinished: saveUrl()
} }
Label { Kirigami.InlineMessage {
text: KI18n.i18nc("@label:textbox", "API key") Layout.fillWidth: true
Layout.alignment: Qt.AlignRight Layout.preferredWidth: Kirigami.Units.gridUnit * 19
type: Kirigami.MessageType.Error
text: KI18n.i18nc("@info", "Enter a valid API base URL with http:// or https:// and a host name.")
visible: !apiUrlField.valid && (apiUrlField.edited || apiUrlField.text.length > 0)
} }
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "API key")
Kirigami.FormData.buddyFor: apiKeyField
Kirigami.PasswordField { Kirigami.PasswordField {
id: apiKeyField id: apiKeyField
enabled: !ApiKeyStore.busy
placeholderText: ApiKeyStore.hasKey placeholderText: ApiKeyStore.hasKey
? KI18n.i18nc("@info:placeholder", "Saved in the wallet. Type a new key to replace it.") ? KI18n.i18nc("@info:placeholder", "Saved in the wallet. Type a new key to replace it.")
: KI18n.i18nc("@info:placeholder", "Not set") : KI18n.i18nc("@info:placeholder", "Not set")
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
onEditingFinished: { onEditingFinished: {
if (text.trim().length > 0) { if (text.trim().length > 0) {
ApiKeyStore.setKey(text) ApiKeyStore.setKey(text)
@@ -61,6 +90,7 @@ SettingsBasePage {
Button { Button {
visible: ApiKeyStore.hasKey visible: ApiKeyStore.hasKey
enabled: !ApiKeyStore.busy
icon.name: "edit-delete-remove" icon.name: "edit-delete-remove"
text: KI18n.i18nc("@action:button", "Remove") text: KI18n.i18nc("@action:button", "Remove")
onClicked: ApiKeyStore.setKey("") onClicked: ApiKeyStore.setKey("")
@@ -71,12 +101,26 @@ SettingsBasePage {
} }
} }
Label { Kirigami.InlineMessage {
text: KI18n.i18nc("@label:listbox", "Model") Layout.fillWidth: true
Layout.alignment: Qt.AlignRight Layout.preferredWidth: Kirigami.Units.gridUnit * 19
type: Kirigami.MessageType.Error
text: ApiKeyStore.error
visible: text.length > 0
actions: Kirigami.Action {
text: KI18n.i18nc("@action:button", "Retry")
icon.name: "view-refresh"
enabled: !ApiKeyStore.busy
onTriggered: ApiKeyStore.retry()
}
} }
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Model")
Kirigami.FormData.buddyFor: modelCombo
ComboBox { ComboBox {
id: modelCombo id: modelCombo
@@ -95,6 +139,7 @@ SettingsBasePage {
editable: true editable: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
onEditTextChanged: if (!updatingList) QtPotatoSettings.model = editText.trim() onEditTextChanged: if (!updatingList) QtPotatoSettings.model = editText.trim()
onActivated: QtPotatoSettings.save() onActivated: QtPotatoSettings.save()
Component.onCompleted: showModels() Component.onCompleted: showModels()
@@ -112,8 +157,11 @@ SettingsBasePage {
text: root.translator.fetchingModels text: root.translator.fetchingModels
? KI18n.i18nc("@action:button", "Loading…") ? KI18n.i18nc("@action:button", "Loading…")
: KI18n.i18nc("@action:button", "Load Models") : KI18n.i18nc("@action:button", "Load Models")
enabled: !root.translator.fetchingModels enabled: apiUrlField.valid && !root.translator.fetchingModels
onClicked: root.translator.fetchModels() onClicked: {
apiUrlField.saveUrl()
root.translator.fetchModels()
}
ToolTip.text: KI18n.i18nc("@info:tooltip", "Ask the API which models it offers") ToolTip.text: KI18n.i18nc("@info:tooltip", "Ask the API which models it offers")
ToolTip.visible: hovered ToolTip.visible: hovered
@@ -121,11 +169,6 @@ SettingsBasePage {
} }
} }
Item {
Layout.preferredWidth: 1
visible: modelCountLabel.visible
}
Label { Label {
id: modelCountLabel id: modelCountLabel
visible: root.translator.availableModels.length > 0 visible: root.translator.availableModels.length > 0
@@ -133,26 +176,20 @@ SettingsBasePage {
opacity: 0.7 opacity: 0.7
} }
Label {
text: KI18n.i18nc("@label:textbox", "Translate into")
Layout.alignment: Qt.AlignRight
}
TextField { TextField {
Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "Translate into")
text: QtPotatoSettings.targetLanguage text: QtPotatoSettings.targetLanguage
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
onTextEdited: QtPotatoSettings.targetLanguage = text.trim() onTextEdited: QtPotatoSettings.targetLanguage = text.trim()
onEditingFinished: QtPotatoSettings.save() onEditingFinished: QtPotatoSettings.save()
} }
Label {
text: KI18n.i18nc("@label:listbox", "Preset")
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
ComboBox { ComboBox {
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Preset")
id: presetCombo id: presetCombo
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
model: root.translator.translationPresets model: root.translator.translationPresets
textRole: "name" textRole: "name"
valueRole: "id" valueRole: "id"
@@ -163,46 +200,49 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 }
Label { Label {
text: presetCombo.currentIndex >= 0 ? presetCombo.model[presetCombo.currentIndex].description : "" text: presetCombo.currentIndex >= 0 ? presetCombo.model[presetCombo.currentIndex].description : ""
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
opacity: 0.7 opacity: 0.7
font: Kirigami.Theme.smallFont font: Kirigami.Theme.smallFont
Layout.fillWidth: true Layout.fillWidth: true
} Layout.preferredWidth: Kirigami.Units.gridUnit * 19
Label {
text: KI18n.i18nc("@label:textbox", "Additional instructions")
Layout.alignment: Qt.AlignRight | Qt.AlignBaseline
} }
TextArea { TextArea {
Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "Additional instructions")
text: QtPotatoSettings.customInstructions text: QtPotatoSettings.customInstructions
placeholderText: KI18n.i18nc("@info:placeholder", "e.g. Translate 'Young Master' as 'Thiếu gia'. Use 'ta/ngươi' between enemies.") placeholderText: KI18n.i18nc("@info:placeholder", "e.g. Translate 'Young Master' as 'Thiếu gia'. Use 'ta/ngươi' between enemies.")
wrapMode: TextArea.Wrap wrapMode: TextArea.Wrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignBaseline Layout.preferredWidth: Kirigami.Units.gridUnit * 19
Layout.preferredHeight: Kirigami.Units.gridUnit * 4 Kirigami.FormData.labelAlignment: Qt.AlignTop
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, Kirigami.Units.gridUnit * 4)
// TextArea has no textEdited before Qt 6.9. // TextArea has no textEdited before Qt 6.9.
onTextChanged: QtPotatoSettings.customInstructions = text onTextChanged: QtPotatoSettings.customInstructions = text
onEditingFinished: QtPotatoSettings.save() onEditingFinished: QtPotatoSettings.save()
} }
CheckBox { Switch {
text: KI18n.i18nc("@option:check", "Build a glossary automatically before translating") text: KI18n.i18nc("@option:check", "Build glossary automatically")
checked: QtPotatoSettings.autoBuildGlossary checked: QtPotatoSettings.autoBuildGlossary
Layout.columnSpan: 2 onClicked: {
onToggled: { QtPotatoSettings.autoBuildGlossary = checked; QtPotatoSettings.save() } QtPotatoSettings.autoBuildGlossary = checked
QtPotatoSettings.save()
}
} }
Label { Label {
text: KI18n.i18nc("@label:spinbox", "Lines per request") text: KI18n.i18nc("@info", "Collect names and terms before translating.")
Layout.alignment: Qt.AlignRight wrapMode: Text.WordWrap
font: Kirigami.Theme.smallFont
color: Kirigami.Theme.disabledTextColor
Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
} }
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Lines per request")
from: 5 from: 5
to: 200 to: 200
stepSize: 5 stepSize: 5
@@ -212,16 +252,6 @@ SettingsBasePage {
QtPotatoSettings.save() 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
}
} }
} }
} }
+5
View File
@@ -8,16 +8,21 @@ import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "video-display"
required property Actions actions required property Actions actions
title: KI18n.i18nc("@title:menu", "&Video") title: KI18n.i18nc("@title:menu", "&Video")
AppMenuItem { action: root.actions.toggleDeinterlacing } AppMenuItem { action: root.actions.toggleDeinterlacing }
MenuSeparator {}
AppMenuItem { action: root.actions.zoomIn } AppMenuItem { action: root.actions.zoomIn }
AppMenuItem { action: root.actions.zoomOut } AppMenuItem { action: root.actions.zoomOut }
AppMenuItem { action: root.actions.zoomReset } AppMenuItem { action: root.actions.zoomReset }
MenuSeparator {}
AppMenuItem { action: root.actions.screenshot } AppMenuItem { action: root.actions.screenshot }
MenuSeparator {}
AppMenuItem { action: root.actions.rotateClockwise } AppMenuItem { action: root.actions.rotateClockwise }
AppMenuItem { action: root.actions.rotateCounterClockwise } AppMenuItem { action: root.actions.rotateCounterClockwise }
} }
+1
View File
@@ -8,6 +8,7 @@ import org.kde.ki18n
AppMenu { AppMenu {
id: root id: root
icon.name: "view-list-details"
required property Actions actions required property Actions actions
+17 -25
View File
@@ -7,49 +7,41 @@ import QtQuick.Controls
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
// Flat volume bar with the value written on it, as in Haruna. // Use the desktop style's handle and hover/focus feedback.
Slider { Slider {
id: root id: root
required property MpvItem mpv required property MpvItem mpv
Accessible.name: KI18n.i18nc("@label:slider", "Volume")
Accessible.description: root.mpv.mute ? KI18n.i18nc("@info volume muted", "Muted") : KI18n.i18nc("@info volume percentage", "%1%", Math.round(value))
from: 0 from: 0
to: root.mpv.volumeMax to: root.mpv.volumeMax
implicitWidth: 100 implicitWidth: Kirigami.Units.gridUnit * 5
implicitHeight: 25
wheelEnabled: true wheelEnabled: true
stepSize: QtPotatoSettings.volumeStep stepSize: QtPotatoSettings.volumeStep
focusPolicy: Qt.NoFocus focusPolicy: Qt.StrongFocus
leftPadding: 0
rightPadding: 0
handle: Item { visible: false } // Let the Slider consume navigation keys before window playback shortcuts.
Keys.onShortcutOverride: event => {
background: Rectangle { if (event.modifiers === Qt.NoModifier && [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End].includes(event.key)) {
color: Kirigami.Theme.alternateBackgroundColor event.accepted = true
scale: root.mirrored ? -1 : 1 }
}
Rectangle { Keys.onPressed: event => {
width: root.visualPosition * parent.width if (event.modifiers === Qt.NoModifier && (event.key === Qt.Key_Home || event.key === Qt.Key_End)) {
height: parent.height value = event.key === Qt.Key_Home ? from : to
color: Kirigami.Theme.highlightColor moved()
event.accepted = true
} }
} }
onMoved: root.mpv.volume = Math.round(value) onMoved: root.mpv.volume = Math.round(value)
// Follow mpv, but not while the user drags (dragging replaces a plain binding). // Follow mpv, but not while the user drags.
Binding on value { Binding on value {
value: root.mpv.volume value: root.mpv.volume
when: !root.pressed when: !root.pressed
restoreMode: Binding.RestoreNone 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"
}
} }
+44 -3
View File
@@ -2,6 +2,8 @@
#include <QCoroSignal> #include <QCoroSignal>
#include <KLocalizedString>
#include <QPointer> #include <QPointer>
#include <qt6keychain/keychain.h> #include <qt6keychain/keychain.h>
@@ -48,9 +50,31 @@ QString ApiKeyStore::error() const
return m_error; return m_error;
} }
bool ApiKeyStore::busy() const
{
return m_busy;
}
void ApiKeyStore::setKey(const QString &key) void ApiKeyStore::setKey(const QString &key)
{ {
store(key.trimmed()); if (m_busy) {
return;
}
m_pendingKey = key.trimmed();
m_retryStore = true;
store(m_pendingKey);
}
void ApiKeyStore::retry()
{
if (m_busy) {
return;
}
if (m_retryStore) {
store(m_pendingKey);
} else {
refresh();
}
} }
QCoro::Task<ApiKeyStore::ReadResult> ApiKeyStore::readKey() QCoro::Task<ApiKeyStore::ReadResult> ApiKeyStore::readKey()
@@ -63,13 +87,14 @@ QCoro::Task<ApiKeyStore::ReadResult> ApiKeyStore::readKey()
co_return ReadResult{}; co_return ReadResult{};
} }
if (job->error() != QKeychain::NoError) { if (job->error() != QKeychain::NoError) {
co_return ReadResult{{}, job->errorString()}; co_return ReadResult{{}, i18nc("@info", "Could not read the API key from the system wallet.\nMake sure KWallet or Secret Service is running, then retry.")};
} }
co_return ReadResult{job->textData(), {}}; co_return ReadResult{job->textData(), {}};
} }
QCoro::Task<> ApiKeyStore::refresh() QCoro::Task<> ApiKeyStore::refresh()
{ {
setBusy(true);
QPointer<ApiKeyStore> self(this); QPointer<ApiKeyStore> self(this);
const ReadResult result = co_await readKey(); const ReadResult result = co_await readKey();
if (!self) { if (!self) {
@@ -77,10 +102,12 @@ QCoro::Task<> ApiKeyStore::refresh()
} }
setHasKey(!result.key.isEmpty()); setHasKey(!result.key.isEmpty());
setError(result.error); setError(result.error);
setBusy(false);
} }
QCoro::Task<> ApiKeyStore::store(QString key) QCoro::Task<> ApiKeyStore::store(QString key)
{ {
setBusy(true);
QPointer<ApiKeyStore> self(this); QPointer<ApiKeyStore> self(this);
std::unique_ptr<QKeychain::Job, JobDeleter> job; std::unique_ptr<QKeychain::Job, JobDeleter> job;
if (key.isEmpty()) { if (key.isEmpty()) {
@@ -97,11 +124,17 @@ QCoro::Task<> ApiKeyStore::store(QString key)
} }
if (job->error() != QKeychain::NoError && job->error() != QKeychain::EntryNotFound) { if (job->error() != QKeychain::NoError && job->error() != QKeychain::EntryNotFound) {
setError(job->errorString()); setError(key.isEmpty()
? i18nc("@info", "Could not remove the API key from the system wallet.\nMake sure KWallet or Secret Service is running, then retry.")
: i18nc("@info", "Could not store the API key in the system wallet.\nMake sure KWallet or Secret Service is running, then retry."));
setBusy(false);
co_return; co_return;
} }
setError({}); setError({});
setHasKey(!key.isEmpty()); setHasKey(!key.isEmpty());
m_retryStore = false;
m_pendingKey.clear();
setBusy(false);
} }
void ApiKeyStore::setHasKey(bool hasKey) void ApiKeyStore::setHasKey(bool hasKey)
@@ -119,3 +152,11 @@ void ApiKeyStore::setError(const QString &error)
Q_EMIT errorChanged(); Q_EMIT errorChanged();
} }
} }
void ApiKeyStore::setBusy(bool busy)
{
if (busy != m_busy) {
m_busy = busy;
Q_EMIT busyChanged();
}
}
+9
View File
@@ -15,6 +15,7 @@ class ApiKeyStore : public QObject
Q_PROPERTY(bool hasKey READ hasKey NOTIFY hasKeyChanged) Q_PROPERTY(bool hasKey READ hasKey NOTIFY hasKeyChanged)
Q_PROPERTY(QString error READ error NOTIFY errorChanged) Q_PROPERTY(QString error READ error NOTIFY errorChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public: public:
struct ReadResult { struct ReadResult {
@@ -26,9 +27,12 @@ public:
bool hasKey() const; bool hasKey() const;
QString error() const; QString error() const;
bool busy() const;
// Stores \a key, or removes the stored key when it is empty. // Stores \a key, or removes the stored key when it is empty.
Q_INVOKABLE void setKey(const QString &key); Q_INVOKABLE void setKey(const QString &key);
// Repeats the failed read, write, or removal without asking for the key again.
Q_INVOKABLE void retry();
// An empty key without error means none is stored. // An empty key without error means none is stored.
static QCoro::Task<ReadResult> readKey(); static QCoro::Task<ReadResult> readKey();
@@ -36,13 +40,18 @@ public:
Q_SIGNALS: Q_SIGNALS:
void hasKeyChanged(); void hasKeyChanged();
void errorChanged(); void errorChanged();
void busyChanged();
private: private:
QCoro::Task<> refresh(); QCoro::Task<> refresh();
QCoro::Task<> store(QString key); QCoro::Task<> store(QString key);
void setHasKey(bool hasKey); void setHasKey(bool hasKey);
void setError(const QString &error); void setError(const QString &error);
void setBusy(bool busy);
bool m_hasKey = false; bool m_hasKey = false;
QString m_error; QString m_error;
bool m_busy = false;
bool m_retryStore = false;
QString m_pendingKey;
}; };
+17 -1
View File
@@ -56,12 +56,28 @@ GlossaryStore::Glossary GlossaryStore::load(const QString &directory)
bool GlossaryStore::save(const Glossary &glossary, QString *error) bool GlossaryStore::save(const Glossary &glossary, QString *error)
{ {
const QString directory = QDir::cleanPath(glossary.directory); const QString directory = QDir::cleanPath(glossary.directory);
for (const Entry &entry : glossary.entries) {
if (entry.source.trimmed().isEmpty() || entry.target.trimmed().isEmpty()) {
if (error) {
*error = QStringLiteral("Each glossary term must have both an original and a translation.");
}
return false;
}
}
const QString path = pathForDirectory(directory); const QString path = pathForDirectory(directory);
QDir().mkpath(QFileInfo(path).absolutePath()); if (!QDir().mkpath(QFileInfo(path).absolutePath()) && !QFileInfo::exists(QFileInfo(path).absolutePath())) {
if (error) {
*error = QStringLiteral("Could not create the glossary storage directory.");
}
return false;
}
QSaveFile file(path); QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(QJsonDocument(glossaryJson({directory, glossary.entries})).toJson(QJsonDocument::Indented)) < 0 || !file.commit()) { if (!file.open(QIODevice::WriteOnly) || file.write(QJsonDocument(glossaryJson({directory, glossary.entries})).toJson(QJsonDocument::Indented)) < 0 || !file.commit()) {
if (error) { if (error) {
*error = file.errorString(); *error = file.errorString();
if (error->isEmpty()) {
*error = QStringLiteral("Could not write the glossary file.");
}
} }
return false; return false;
} }
+30 -4
View File
@@ -139,25 +139,51 @@ QString TranslationService::glossaryDirectory() const
return m_glossary.directory; return m_glossary.directory;
} }
QString TranslationService::glossaryError() const
{
return m_glossaryError;
}
void TranslationService::loadGlossary(const QUrl &videoUrl) void TranslationService::loadGlossary(const QUrl &videoUrl)
{ {
const QString directory = videoUrl.isLocalFile() ? QFileInfo(videoUrl.toLocalFile()).absolutePath() : QString(); const QString directory = videoUrl.isLocalFile() ? QFileInfo(videoUrl.toLocalFile()).absolutePath() : QString();
m_glossary = directory.isEmpty() ? GlossaryStore::Glossary{} : GlossaryStore::load(directory); m_glossary = directory.isEmpty() ? GlossaryStore::Glossary{} : GlossaryStore::load(directory);
if (!m_glossaryError.isEmpty()) {
m_glossaryError.clear();
Q_EMIT glossaryErrorChanged();
}
Q_EMIT glossaryChanged(); Q_EMIT glossaryChanged();
} }
void TranslationService::saveGlossary(const QVariantList &entries) bool TranslationService::saveGlossary(const QVariantList &entries)
{ {
QList<GlossaryStore::Entry> result; QList<GlossaryStore::Entry> result;
for (const QVariant &value : entries) { for (const QVariant &value : entries) {
const QVariantMap map = value.toMap(); const QVariantMap map = value.toMap();
result.append({map.value(u"source"_s).toString().trimmed(), map.value(u"target"_s).toString().trimmed()}); result.append({map.value(u"source"_s).toString().trimmed(), map.value(u"target"_s).toString().trimmed()});
} }
m_glossary.entries = result;
if (!m_glossary.directory.isEmpty()) { GlossaryStore::Glossary candidate = m_glossary;
GlossaryStore::save(m_glossary); candidate.entries = result;
if (!candidate.directory.isEmpty()) {
QString error;
if (!GlossaryStore::save(candidate, &error)) {
const QString message = error.isEmpty() ? i18n("Could not save the translation glossary.")
: i18n("Could not save the translation glossary: %1", error);
if (m_glossaryError != message) {
m_glossaryError = message;
Q_EMIT glossaryErrorChanged();
}
return false;
}
}
m_glossary = candidate;
if (!m_glossaryError.isEmpty()) {
m_glossaryError.clear();
Q_EMIT glossaryErrorChanged();
} }
Q_EMIT glossaryChanged(); Q_EMIT glossaryChanged();
return true;
} }
void TranslationService::clearGlossary() void TranslationService::clearGlossary()
+5 -1
View File
@@ -34,6 +34,7 @@ class TranslationService : public QObject
Q_PROPERTY(bool fetchingModels READ isFetchingModels NOTIFY fetchingModelsChanged) Q_PROPERTY(bool fetchingModels READ isFetchingModels NOTIFY fetchingModelsChanged)
Q_PROPERTY(QVariantList glossaryEntries READ glossaryEntries NOTIFY glossaryChanged) Q_PROPERTY(QVariantList glossaryEntries READ glossaryEntries NOTIFY glossaryChanged)
Q_PROPERTY(QString glossaryDirectory READ glossaryDirectory NOTIFY glossaryChanged) Q_PROPERTY(QString glossaryDirectory READ glossaryDirectory NOTIFY glossaryChanged)
Q_PROPERTY(QString glossaryError READ glossaryError NOTIFY glossaryErrorChanged)
public: public:
explicit TranslationService(QObject *parent = nullptr); explicit TranslationService(QObject *parent = nullptr);
@@ -48,6 +49,7 @@ public:
bool isFetchingModels() const; bool isFetchingModels() const;
QVariantList glossaryEntries() const; QVariantList glossaryEntries() const;
QString glossaryDirectory() const; QString glossaryDirectory() const;
QString glossaryError() const;
// \a track is an entry of MpvItem::subtitleTracks. // \a track is an entry of MpvItem::subtitleTracks.
Q_INVOKABLE void translate(const QUrl &videoUrl, const QVariantMap &track, const QString &mediaTitle); Q_INVOKABLE void translate(const QUrl &videoUrl, const QVariantMap &track, const QString &mediaTitle);
@@ -56,7 +58,7 @@ public:
Q_INVOKABLE void fetchModels(); Q_INVOKABLE void fetchModels();
Q_INVOKABLE void clearError(); Q_INVOKABLE void clearError();
Q_INVOKABLE void loadGlossary(const QUrl &videoUrl); Q_INVOKABLE void loadGlossary(const QUrl &videoUrl);
Q_INVOKABLE void saveGlossary(const QVariantList &entries); Q_INVOKABLE bool saveGlossary(const QVariantList &entries);
Q_INVOKABLE void clearGlossary(); Q_INVOKABLE void clearGlossary();
Q_SIGNALS: Q_SIGNALS:
@@ -67,6 +69,7 @@ Q_SIGNALS:
void availableModelsChanged(); void availableModelsChanged();
void fetchingModelsChanged(); void fetchingModelsChanged();
void glossaryChanged(); void glossaryChanged();
void glossaryErrorChanged();
void translated(const QUrl &subtitleUrl, bool fromCache); void translated(const QUrl &subtitleUrl, bool fromCache);
private: private:
@@ -98,6 +101,7 @@ private:
double m_progress = 0.0; double m_progress = 0.0;
QString m_statusText; QString m_statusText;
QString m_errorText; QString m_errorText;
QString m_glossaryError;
QStringList m_availableModels; QStringList m_availableModels;
bool m_fetchingModels = false; bool m_fetchingModels = false;
GlossaryStore::Glossary m_glossary; GlossaryStore::Glossary m_glossary;