Merge branch 'main' into sdfs
This commit is contained in:
commit
5a4dbf1300
8 changed files with 293 additions and 119 deletions
|
|
@ -363,7 +363,8 @@ Singleton {
|
||||||
smartScheme: services.smartScheme,
|
smartScheme: services.smartScheme,
|
||||||
defaultPlayer: services.defaultPlayer,
|
defaultPlayer: services.defaultPlayer,
|
||||||
playerAliases: services.playerAliases,
|
playerAliases: services.playerAliases,
|
||||||
showLyrics: services.showLyrics
|
showLyrics: services.showLyrics,
|
||||||
|
lyricsBackend: services.lyricsBackend
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,6 @@ JsonObject {
|
||||||
"to": "YT Music"
|
"to": "YT Music"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
property bool showLyrics: true
|
property bool showLyrics: false
|
||||||
|
property string lyricsBackend: "Auto"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,11 @@ ColumnLayout {
|
||||||
forceActiveFocus();
|
forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
event.accepted = false;
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
// Clear error when user starts typing
|
// Clear error when user starts typing
|
||||||
if (connectButton.hasError && event.text && event.text.length > 0) {
|
if (connectButton.hasError && event.text && event.text.length > 0) {
|
||||||
connectButton.hasError = false;
|
connectButton.hasError = false;
|
||||||
|
|
@ -298,6 +303,10 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
} else if (event.text && event.text.length > 0) {
|
} else if (event.text && event.text.length > 0) {
|
||||||
|
if (event.key === Qt.Key_Tab) {
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
passwordBuffer += event.text;
|
passwordBuffer += event.text;
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,11 @@ Item {
|
||||||
forceActiveFocus();
|
forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
event.accepted = false;
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
if (connectButton.hasError && event.text && event.text.length > 0) {
|
if (connectButton.hasError && event.text && event.text.length > 0) {
|
||||||
connectButton.hasError = false;
|
connectButton.hasError = false;
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +224,10 @@ Item {
|
||||||
}
|
}
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
} else if (event.text && event.text.length > 0) {
|
} else if (event.text && event.text.length > 0) {
|
||||||
|
if (event.key === Qt.Key_Tab) {
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
passwordBuffer += event.text;
|
passwordBuffer += event.text;
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ StyledRect {
|
||||||
anchors.margins: Appearance.padding.large
|
anchors.margins: Appearance.padding.large
|
||||||
spacing: Appearance.spacing.normal
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
// Header: icon, backend name, refresh, toggle
|
// Header: icon, backend selector, refresh, toggle
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Appearance.padding.small
|
spacing: Appearance.padding.small
|
||||||
|
|
@ -44,12 +44,49 @@ StyledRect {
|
||||||
font.pointSize: Appearance.spacing.large
|
font.pointSize: Appearance.spacing.large
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
Rectangle {
|
||||||
|
Layout.preferredHeight: 24
|
||||||
|
Layout.preferredWidth: 80
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15)
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: LyricsService.preferredBackend
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
const backends = ["Auto", "Local", "NetEase"];
|
||||||
|
const currentIndex = backends.indexOf(LyricsService.preferredBackend);
|
||||||
|
const nextIndex = (currentIndex + 1) % backends.length;
|
||||||
|
LyricsService.preferredBackend = backends[nextIndex];
|
||||||
|
LyricsService.loadLyrics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredHeight: 24
|
||||||
|
Layout.preferredWidth: 60
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
visible: LyricsService.preferredBackend === "Auto"
|
||||||
|
color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15)
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: LyricsService.backend
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: LyricsService.backend
|
|
||||||
font.pointSize: Appearance.font.size.normal
|
|
||||||
color: Colours.palette.m3secondary
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
IconButton {
|
||||||
|
|
@ -66,117 +103,141 @@ StyledRect {
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: "Fetched Candidates:"
|
text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:"
|
||||||
color: Colours.palette.m3outline
|
color: Colours.palette.m3outline
|
||||||
font.pointSize: Appearance.font.size.small
|
font.pointSize: Appearance.font.size.small
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
|
visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local file info (shown in Local mode)
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 48
|
||||||
|
visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1)
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Appearance.padding.small
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: {
|
||||||
|
const path = LyricsService.loadedLocalFile;
|
||||||
|
const parts = path.split('/');
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3tertiary
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: {
|
||||||
|
const path = LyricsService.loadedLocalFile;
|
||||||
|
const parts = path.split('/');
|
||||||
|
if (parts.length > 2) {
|
||||||
|
return parts.slice(-3, -1).join('/');
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3outline
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Candidates list
|
// Candidates list
|
||||||
ListView {
|
Loader {
|
||||||
id: candidatesView
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
visible: LyricsService.candidatesModel.count > 0
|
active: LyricsService.preferredBackend !== "Local"
|
||||||
model: LyricsService.candidatesModel
|
|
||||||
clip: true
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
opacity: visible ? 1 : 0
|
sourceComponent: ListView {
|
||||||
// Behavior on opacity {
|
id: candidatesView
|
||||||
// NumberAnimation { duration: Appearance.anim.durations.normal }
|
|
||||||
// }
|
|
||||||
|
|
||||||
delegate: Item {
|
model: LyricsService.candidatesModel
|
||||||
id: delegateRoot
|
clip: true
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
visible: LyricsService.candidatesModel.count > 0
|
||||||
|
opacity: visible ? 1 : 0
|
||||||
|
|
||||||
required property real id
|
delegate: Item {
|
||||||
required property string title
|
id: delegateRoot
|
||||||
required property string artist
|
|
||||||
property bool hovered: false
|
|
||||||
property bool pressed: false
|
|
||||||
|
|
||||||
width: ListView.view.width * 0.98
|
required property real id
|
||||||
height: 70
|
required property string title
|
||||||
anchors.horizontalCenter: parent?.horizontalCenter
|
required property string artist
|
||||||
scale: hovered ? 1.02 : 1.0
|
|
||||||
|
|
||||||
Behavior on scale {
|
property bool hovered: false
|
||||||
NumberAnimation {
|
property bool pressed: false
|
||||||
duration: Appearance.anim.durations.small
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
width: ListView.view.width * 0.98
|
||||||
id: background
|
height: 70
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.horizontalCenter: parent?.horizontalCenter
|
||||||
radius: Appearance.rounding.small
|
scale: hovered ? 1.02 : 1.0
|
||||||
|
|
||||||
color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03)
|
Behavior on scale {
|
||||||
|
|
||||||
border.width: delegateRoot.hovered ? 1 : 0
|
|
||||||
border.color: Colours.palette.m3primary
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Appearance.anim.durations.small
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Appearance.anim.durations.small
|
duration: Appearance.anim.durations.small
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onEntered: delegateRoot.hovered = true
|
|
||||||
onExited: delegateRoot.hovered = false
|
|
||||||
onPressed: delegateRoot.pressed = true
|
|
||||||
onReleased: delegateRoot.pressed = false
|
|
||||||
onClicked: LyricsService.selectCandidate(delegateRoot.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Appearance.padding.normal
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
// Active indicator bar
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 4
|
id: background
|
||||||
height: parent.height * 0.6
|
|
||||||
radius: 2
|
anchors.fill: parent
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
radius: Appearance.rounding.small
|
||||||
color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent"
|
|
||||||
|
color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03)
|
||||||
|
|
||||||
|
border.width: delegateRoot.hovered ? 1 : 0
|
||||||
|
border.color: Colours.palette.m3primary
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
duration: Appearance.anim.durations.small
|
duration: Appearance.anim.durations.small
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Behavior on border.width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
MouseArea {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.fill: parent
|
||||||
width: parent.width - 30
|
hoverEnabled: true
|
||||||
spacing: 4
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
Text {
|
onEntered: delegateRoot.hovered = true
|
||||||
text: delegateRoot.title
|
onExited: delegateRoot.hovered = false
|
||||||
font.pointSize: Appearance.font.size.normal
|
onPressed: delegateRoot.pressed = true
|
||||||
font.bold: true
|
onReleased: delegateRoot.pressed = false
|
||||||
color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
onClicked: LyricsService.selectCandidate(delegateRoot.id)
|
||||||
width: parent.width
|
}
|
||||||
elide: Text.ElideRight
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Appearance.padding.normal
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
// Active indicator bar
|
||||||
|
Rectangle {
|
||||||
|
width: 4
|
||||||
|
height: parent.height * 0.6
|
||||||
|
radius: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent"
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
|
|
@ -185,11 +246,32 @@ StyledRect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Column {
|
||||||
text: delegateRoot.artist
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
font.pointSize: Appearance.font.size.small
|
width: parent.width - 30
|
||||||
color: Colours.palette.m3onSurfaceVariant
|
spacing: 4
|
||||||
elide: Text.ElideRight
|
|
||||||
|
Text {
|
||||||
|
text: delegateRoot.title
|
||||||
|
font.pointSize: Appearance.font.size.normal
|
||||||
|
font.bold: true
|
||||||
|
color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Appearance.anim.durations.small
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: delegateRoot.artist
|
||||||
|
font.pointSize: Appearance.font.size.small
|
||||||
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +280,7 @@ StyledRect {
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
visible: LyricsService.candidatesModel.count == 0
|
visible: LyricsService.candidatesModel.count == 0 && LyricsService.preferredBackend !== "Local"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual search
|
// Manual search
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import Caelestia
|
import Caelestia
|
||||||
import Caelestia.Services
|
import Caelestia.Services
|
||||||
|
|
@ -67,6 +68,15 @@ Singleton {
|
||||||
Pipewire.preferredDefaultAudioSource = newSource;
|
Pipewire.preferredDefaultAudioSource = newSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cycleNextAudioOutput(): void {
|
||||||
|
if (sinks.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const currentIndex = sinks.findIndex(s => s === sink);
|
||||||
|
const nextIndex = (currentIndex + 1) % sinks.length;
|
||||||
|
setAudioSink(sinks[nextIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
function setStreamVolume(stream: PwNode, newVolume: real): void {
|
function setStreamVolume(stream: PwNode, newVolume: real): void {
|
||||||
if (stream?.ready && stream?.audio) {
|
if (stream?.ready && stream?.audio) {
|
||||||
stream.audio.muted = false;
|
stream.audio.muted = false;
|
||||||
|
|
@ -162,4 +172,12 @@ Singleton {
|
||||||
BeatTracker {
|
BeatTracker {
|
||||||
id: beatTracker
|
id: beatTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function cycleOutput(): void {
|
||||||
|
root.cycleNextAudioOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "audio"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,22 +17,17 @@ Singleton {
|
||||||
property bool isManualSeeking: false
|
property bool isManualSeeking: false
|
||||||
property bool lyricsVisible: Config.services.showLyrics
|
property bool lyricsVisible: Config.services.showLyrics
|
||||||
property string backend: "Local"
|
property string backend: "Local"
|
||||||
|
property string preferredBackend: Config.services.lyricsBackend
|
||||||
property real currentSongId: 0
|
property real currentSongId: 0
|
||||||
|
property string loadedLocalFile: ""
|
||||||
property real offset
|
property real offset
|
||||||
|
property int currentRequestId: 0
|
||||||
|
property var lyricsMap: ({})
|
||||||
|
|
||||||
readonly property string lyricsDir: Paths.absolutePath(Config.paths.lyricsDir)
|
readonly property string lyricsDir: Paths.absolutePath(Config.paths.lyricsDir)
|
||||||
readonly property string lyricsMapFile: Paths.absolutePath(Config.paths.lyricsDir) + "/lyrics_map.json"
|
readonly property string lyricsMapFile: Paths.absolutePath(Config.paths.lyricsDir) + "/lyrics_map.json"
|
||||||
|
|
||||||
property int currentRequestId: 0
|
|
||||||
|
|
||||||
// The data source for the UI
|
|
||||||
readonly property alias model: lyricsModel
|
readonly property alias model: lyricsModel
|
||||||
readonly property alias candidatesModel: fetchedCandidatesModel
|
readonly property alias candidatesModel: fetchedCandidatesModel
|
||||||
|
|
||||||
property var lyricsMap: ({})
|
|
||||||
|
|
||||||
// shared headers for all NetEase requests
|
|
||||||
readonly property var _netEaseHeaders: ({
|
readonly property var _netEaseHeaders: ({
|
||||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0",
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0",
|
||||||
"Referer": "https://music.163.com/"
|
"Referer": "https://music.163.com/"
|
||||||
|
|
@ -90,7 +85,6 @@ Singleton {
|
||||||
lyricsModel.clear();
|
lyricsModel.clear();
|
||||||
currentIndex = -1;
|
currentIndex = -1;
|
||||||
root.currentSongId = 0;
|
root.currentSongId = 0;
|
||||||
root.backend = "Local";
|
|
||||||
|
|
||||||
root.currentRequestId++;
|
root.currentRequestId++;
|
||||||
let requestId = root.currentRequestId;
|
let requestId = root.currentRequestId;
|
||||||
|
|
@ -99,29 +93,47 @@ Singleton {
|
||||||
let saved = root.lyricsMap[key];
|
let saved = root.lyricsMap[key];
|
||||||
root.offset = saved?.offset ?? 0.0;
|
root.offset = saved?.offset ?? 0.0;
|
||||||
|
|
||||||
if (saved?.neteaseId && saved?.backend === "NetEase") {
|
if (root.preferredBackend === "NetEase") {
|
||||||
root.backend = "NetEase";
|
root.backend = "NetEase";
|
||||||
root.currentSongId = saved.neteaseId;
|
fetchNetEase(meta.title, meta.artist, requestId);
|
||||||
fetchNetEaseLyrics(saved.neteaseId, requestId);
|
|
||||||
fetchNetEaseCandidates(meta.title, meta.artist, requestId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saved?.backend === "NetEase") {
|
if (root.preferredBackend === "Local") {
|
||||||
fallbackTimer.restart();
|
root.backend = "Local";
|
||||||
|
let cleanDir = lyricsDir.replace(/\/$/, "");
|
||||||
|
let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`;
|
||||||
|
|
||||||
|
// Search for files matching "Artist - Title.lrc" pattern
|
||||||
|
const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || "");
|
||||||
|
const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || "");
|
||||||
|
const escapedTitle = titleStr.replace(/'/g, "'\\''");
|
||||||
|
const escapedArtist = artistStr.replace(/'/g, "'\\''");
|
||||||
|
findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`];
|
||||||
|
findLyricsInSubdirs.requestId = requestId;
|
||||||
|
findLyricsInSubdirs.running = true;
|
||||||
|
|
||||||
|
lrcFile.path = "";
|
||||||
|
lrcFile.path = flatPath;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto mode: try local first
|
||||||
|
root.backend = "Local";
|
||||||
let cleanDir = lyricsDir.replace(/\/$/, "");
|
let cleanDir = lyricsDir.replace(/\/$/, "");
|
||||||
let fullPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`;
|
let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`;
|
||||||
|
|
||||||
|
const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || "");
|
||||||
|
const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || "");
|
||||||
|
const escapedTitle = titleStr.replace(/'/g, "'\\''");
|
||||||
|
const escapedArtist = artistStr.replace(/'/g, "'\\''");
|
||||||
|
findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`];
|
||||||
|
findLyricsInSubdirs.requestId = requestId;
|
||||||
|
findLyricsInSubdirs.running = true;
|
||||||
|
|
||||||
lrcFile.path = "";
|
lrcFile.path = "";
|
||||||
lrcFile.path = fullPath;
|
lrcFile.path = flatPath;
|
||||||
fetchNetEaseCandidates(meta.title, meta.artist, requestId); //to populate the list regardless
|
fetchNetEaseCandidates(meta.title, meta.artist, requestId);
|
||||||
|
|
||||||
// if the file is missing, FileView will not fire onLoaded, so we arm the fallback timer here as a safety net. It is cancelled in onLoaded if the file loads successfully.
|
|
||||||
if (saved?.backend !== "Local")
|
|
||||||
fallbackTimer.restart();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateModel(parsedArray) {
|
function updateModel(parsedArray) {
|
||||||
|
|
@ -256,6 +268,13 @@ Singleton {
|
||||||
seekTimer.restart();
|
seekTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPreferredBackendChanged: {
|
||||||
|
if (Config.services.lyricsBackend !== preferredBackend) {
|
||||||
|
Config.services.lyricsBackend = preferredBackend;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ListModel {
|
ListModel {
|
||||||
id: lyricsModel
|
id: lyricsModel
|
||||||
}
|
}
|
||||||
|
|
@ -314,12 +333,15 @@ Singleton {
|
||||||
let parsed = Lrc.parseLrc(text());
|
let parsed = Lrc.parseLrc(text());
|
||||||
if (parsed.length > 0) {
|
if (parsed.length > 0) {
|
||||||
root.backend = "Local";
|
root.backend = "Local";
|
||||||
|
root.loadedLocalFile = path;
|
||||||
updateModel(parsed);
|
updateModel(parsed);
|
||||||
loading = false;
|
loading = false;
|
||||||
} else {
|
} else if (root.preferredBackend === "Local") {
|
||||||
|
// Local mode only - fail immediately
|
||||||
root.backend = "NetEase";
|
root.backend = "NetEase";
|
||||||
fallbackToOnline();
|
fallbackToOnline();
|
||||||
}
|
}
|
||||||
|
// In Auto mode, let the Process onExited handle fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -346,4 +368,35 @@ Singleton {
|
||||||
|
|
||||||
command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`]
|
command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: findLyricsInSubdirs
|
||||||
|
|
||||||
|
property int requestId: -1
|
||||||
|
property bool foundFile: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => {
|
||||||
|
if (findLyricsInSubdirs.requestId === root.currentRequestId) {
|
||||||
|
const foundPath = data.trim();
|
||||||
|
if (foundPath && foundPath.length > 0) {
|
||||||
|
findLyricsInSubdirs.foundFile = true;
|
||||||
|
fallbackTimer.stop();
|
||||||
|
root.loadedLocalFile = foundPath;
|
||||||
|
lrcFile.path = "";
|
||||||
|
lrcFile.path = foundPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: (exitCode, exitStatus) => { // qmllint disable signal-handler-parameters
|
||||||
|
if (requestId === root.currentRequestId && !foundFile && root.preferredBackend === "Auto") {
|
||||||
|
if (lyricsModel.count === 0) {
|
||||||
|
fallbackTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundFile = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ Singleton {
|
||||||
const forecastList = [];
|
const forecastList = [];
|
||||||
for (let i = 0; i < json.daily.time.length; i++)
|
for (let i = 0; i < json.daily.time.length; i++)
|
||||||
forecastList.push({
|
forecastList.push({
|
||||||
date: json.daily.time[i],
|
date: json.daily.time[i].replace(/-/g, "/"),
|
||||||
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
|
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
|
||||||
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
|
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
|
||||||
minTempC: Math.round(json.daily.temperature_2m_min[i]),
|
minTempC: Math.round(json.daily.temperature_2m_min[i]),
|
||||||
|
|
@ -141,7 +141,8 @@ Singleton {
|
||||||
const hourlyList = [];
|
const hourlyList = [];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
for (let i = 0; i < json.hourly.time.length; i++) {
|
for (let i = 0; i < json.hourly.time.length; i++) {
|
||||||
const time = new Date(json.hourly.time[i]);
|
const time = new Date(json.hourly.time[i].replace("T", " "));
|
||||||
|
|
||||||
if (time < now)
|
if (time < now)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue