349 lines
11 KiB
QML
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
|
|
}
|
|
}
|