Files
QtPotato/src/qml/Main.qml
T

349 lines
11 KiB
QML

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