A use-site `changed has-focus` on a std-widgets LineEdit/TextEdit never fires — has-focus is an alias into the inner TextInput (slint#7747) — so typing a letter that doubles as a shortcut ran the action instead of inserting the character. Sweep every text-input surface (search, import, create/edit playlist & folder, tag editor, colour picker, settings fields, QConnect dev modal, kiosk shell) and drive UiFocusState.text-input-focused from a `guard-focused` probe property that the change tracker does observe. cc
337 lines
14 KiB
Plaintext
337 lines
14 KiB
Plaintext
// Settings > Integrations — source-agnostic scrobblers (Last.fm + ListenBrainz).
|
|
// Mirrors the Plex section structure in LocalLibrarySettings.slint: a master
|
|
// enable + collapse header, then per-service connect/disconnect rows that bind
|
|
// the `ScrobbleState` global for state and `ScrobbleActions` for actions. The
|
|
// fire path is source-agnostic (Qobuz, local, AND Plex) and lives in Rust.
|
|
// ADR-008: no pills — plain bordered buttons.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { ScrobbleState, ScrobbleActions, DiscordState, SettingsState , UiFocusState } from "../state.slint";
|
|
import { SettingRow } from "SettingRow.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { QbzToggle } from "../primitives/QbzToggle.slint";
|
|
import { LineEdit } from "std-widgets.slint";
|
|
|
|
component GroupHeader inherits Text {
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
letter-spacing: 1.5px;
|
|
font-weight: Typography.semibold;
|
|
}
|
|
|
|
// Matches AppearanceSettings/OfflineSettings' SecondaryButton — the 160px
|
|
// settings-row standard: fixed width, 34px, body/medium label, right edge
|
|
// of every row lands on the same vertical line.
|
|
component SecondaryButton inherits Rectangle {
|
|
in property <string> label;
|
|
in property <bool> danger: false;
|
|
in property <bool> enabled: true;
|
|
callback clicked();
|
|
|
|
// 160px floor keeps the settings right-edge grid; longer translations grow.
|
|
width: Math.max(label-txt.preferred-width + 32px, 160px);
|
|
height: 34px;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: root.danger ? #e0564f : Theme.border-subtle;
|
|
background: ta.has-hover && root.enabled ? Theme.surface-hover : Theme.surface-elevated;
|
|
opacity: root.enabled ? 1.0 : 0.4;
|
|
|
|
label-txt := Text {
|
|
width: 100%;
|
|
height: 100%;
|
|
text: root.label;
|
|
color: root.danger ? #e0564f : Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: root.enabled ? pointer : default;
|
|
clicked => {
|
|
if (root.enabled) {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export component IntegrationsSettings inherits VerticalLayout {
|
|
spacing: 4px;
|
|
|
|
// Bubbles a persisted bool toggle up to SettingsView -> Rust handle_bool.
|
|
callback settings-bool(string, bool);
|
|
|
|
init => {
|
|
ScrobbleActions.load();
|
|
}
|
|
|
|
// ===================================================================
|
|
// RECOMMENDATIONS — opt-out for the external Discover "Recommendations"
|
|
// tab. The description nudges connecting the sources that feed it.
|
|
// ===================================================================
|
|
GroupHeader { text: @tr("RECOMMENDATIONS"); }
|
|
SettingRow {
|
|
label: @tr("Show Recommendations in Discover");
|
|
description: @tr("A personalized Discover tab built from your listening. For the best results connect Last.fm and ListenBrainz below — MusicBrainz is used automatically to match releases.");
|
|
QbzToggle {
|
|
checked: SettingsState.show-recommendations;
|
|
toggled(v) => {
|
|
SettingsState.show-recommendations = v;
|
|
root.settings-bool("show-recommendations", v);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===================================================================
|
|
// METADATA — MusicBrainz opt-out (default on). Enriches artist pages
|
|
// (relationships, origin/scene) and playlist song suggestions. No telemetry.
|
|
// ===================================================================
|
|
Rectangle { height: 12px; }
|
|
GroupHeader { text: @tr("METADATA"); }
|
|
Rectangle { height: 4px; }
|
|
SettingRow {
|
|
label: @tr("MusicBrainz");
|
|
description: @tr("Enable artist relationships and enhanced metadata from MusicBrainz. No telemetry — it only matches releases to enrich artist pages and playlist song suggestions. Turn off to disable those sections.");
|
|
QbzToggle {
|
|
checked: SettingsState.musicbrainz-enabled;
|
|
toggled(v) => {
|
|
SettingsState.musicbrainz-enabled = v;
|
|
root.settings-bool("musicbrainz", v);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===================================================================
|
|
// SCROBBLERS — master header: title + master toggle + collapse chevron.
|
|
// Body renders only when `enabled && !ui-collapsed`.
|
|
// ===================================================================
|
|
HorizontalLayout {
|
|
spacing: 10px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
horizontal-stretch: 1;
|
|
spacing: 2px;
|
|
GroupHeader { text: @tr("SCROBBLERS"); vertical-alignment: center; }
|
|
Text {
|
|
text: @tr("Send your plays to Last.fm and ListenBrainz. Works for Qobuz, local, and Plex tracks.");
|
|
color: Theme.text-muted;
|
|
font-size: 12px;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
// Collapse chevron — moved to row level so it is vertically centered
|
|
// with the master toggle (it used to be inline with the title and sat
|
|
// too high), and enlarged (28px hit-box / 18px glyph).
|
|
if ScrobbleState.enabled: VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: Radius.sm;
|
|
background: chev-ta.has-hover ? Theme.surface-hover : transparent;
|
|
QbzIcon {
|
|
source: ScrobbleState.ui-collapsed
|
|
? @image-url("../assets/icons/chevron-right.svg")
|
|
: @image-url("../assets/icons/chevron-down.svg");
|
|
width: 18px;
|
|
height: 18px;
|
|
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
|
|
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
|
|
tint: Theme.text-secondary;
|
|
}
|
|
chev-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
ScrobbleState.ui-collapsed = !ScrobbleState.ui-collapsed;
|
|
ScrobbleActions.collapse-toggle(ScrobbleState.ui-collapsed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzToggle {
|
|
checked: ScrobbleState.enabled;
|
|
toggled(v) => {
|
|
ScrobbleState.enabled = v;
|
|
ScrobbleActions.enable-toggle(v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Body — only when enabled and not collapsed.
|
|
if ScrobbleState.enabled && !ScrobbleState.ui-collapsed: VerticalLayout {
|
|
spacing: 4px;
|
|
|
|
Rectangle { height: 12px; }
|
|
|
|
// ----------------------------------------------------------------
|
|
// LAST.FM
|
|
// ----------------------------------------------------------------
|
|
GroupHeader { text: @tr("LAST.FM"); }
|
|
Rectangle { height: 4px; }
|
|
|
|
SettingRow {
|
|
label: @tr("Scrobble to Last.fm");
|
|
description: ScrobbleState.lastfm-authed
|
|
? @tr("Signed in as {}.", ScrobbleState.lastfm-username)
|
|
: @tr("Connect your Last.fm account to enable scrobbling.");
|
|
QbzToggle {
|
|
checked: ScrobbleState.lastfm-enabled;
|
|
toggled(v) => {
|
|
ScrobbleState.lastfm-enabled = v;
|
|
ScrobbleActions.lastfm-enable-toggle(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Connect (when not authed) — two-step browser flow.
|
|
if !ScrobbleState.lastfm-authed: SettingRow {
|
|
label: @tr("Connect");
|
|
description: @tr("Authorize QBZ in your browser, then click Finish.");
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
SecondaryButton {
|
|
label: ScrobbleState.lastfm-busy ? @tr("Working...") : @tr("Connect Last.fm");
|
|
enabled: !ScrobbleState.lastfm-busy;
|
|
clicked => { ScrobbleActions.lastfm-connect(); }
|
|
}
|
|
if ScrobbleState.lastfm-auth-url != "": SecondaryButton {
|
|
label: @tr("Open authorize page");
|
|
clicked => { ScrobbleActions.lastfm-open-auth-url(); }
|
|
}
|
|
if ScrobbleState.lastfm-auth-url != "": SecondaryButton {
|
|
label: ScrobbleState.lastfm-busy ? @tr("Working...") : @tr("Finish");
|
|
enabled: !ScrobbleState.lastfm-busy;
|
|
clicked => { ScrobbleActions.lastfm-confirm(); }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Disconnect (when authed).
|
|
if ScrobbleState.lastfm-authed: SettingRow {
|
|
label: @tr("Disconnect Last.fm");
|
|
description: @tr("Sign out of Last.fm.");
|
|
SecondaryButton {
|
|
label: @tr("Disconnect");
|
|
danger: true;
|
|
clicked => { ScrobbleActions.lastfm-disconnect(); }
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 18px; }
|
|
|
|
// ----------------------------------------------------------------
|
|
// LISTENBRAINZ
|
|
// ----------------------------------------------------------------
|
|
GroupHeader { text: @tr("LISTENBRAINZ"); }
|
|
Rectangle { height: 4px; }
|
|
|
|
SettingRow {
|
|
label: @tr("Scrobble to ListenBrainz");
|
|
description: ScrobbleState.listenbrainz-authed
|
|
? @tr("Signed in as {}.", ScrobbleState.listenbrainz-username)
|
|
: @tr("Paste your ListenBrainz user token to enable scrobbling.");
|
|
QbzToggle {
|
|
checked: ScrobbleState.listenbrainz-enabled;
|
|
toggled(v) => {
|
|
ScrobbleState.listenbrainz-enabled = v;
|
|
ScrobbleActions.listenbrainz-enable-toggle(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Token field (when not authed).
|
|
if !ScrobbleState.listenbrainz-authed: SettingRow {
|
|
label: @tr("User token");
|
|
description: @tr("From listenbrainz.org/settings.");
|
|
HorizontalLayout {
|
|
width: 240px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
horizontal-stretch: 1;
|
|
LineEdit {
|
|
text: ScrobbleState.listenbrainz-token-input;
|
|
placeholder-text: @tr("ListenBrainz token");
|
|
// Hotkey-guard probe: a use-site `changed has-focus` on a std-widgets
|
|
// input never fires (has-focus is an alias into the inner TextInput),
|
|
// so observe it through a local property instead (#619).
|
|
property <bool> guard-focused: self.has-focus;
|
|
changed guard-focused => { UiFocusState.text-input-focused = self.guard-focused; }
|
|
input-type: password;
|
|
enabled: !ScrobbleState.listenbrainz-busy;
|
|
edited(s) => {
|
|
ScrobbleState.listenbrainz-token-input = s;
|
|
}
|
|
accepted(s) => {
|
|
ScrobbleActions.listenbrainz-set-token(s);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !ScrobbleState.listenbrainz-authed: SettingRow {
|
|
label: @tr("Save token");
|
|
description: @tr("Validates the token against ListenBrainz.");
|
|
SecondaryButton {
|
|
label: ScrobbleState.listenbrainz-busy ? @tr("Validating...") : @tr("Connect ListenBrainz");
|
|
enabled: !ScrobbleState.listenbrainz-busy;
|
|
clicked => {
|
|
ScrobbleActions.listenbrainz-set-token(ScrobbleState.listenbrainz-token-input);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Disconnect (when authed).
|
|
if ScrobbleState.listenbrainz-authed: SettingRow {
|
|
label: @tr("Disconnect ListenBrainz");
|
|
description: @tr("Sign out of ListenBrainz.");
|
|
SecondaryButton {
|
|
label: @tr("Disconnect");
|
|
danger: true;
|
|
clicked => { ScrobbleActions.listenbrainz-disconnect(); }
|
|
}
|
|
}
|
|
|
|
// Shared status line.
|
|
if ScrobbleState.status-text != "": Text {
|
|
text: ScrobbleState.status-text;
|
|
color: ScrobbleState.status-kind == 3
|
|
? #e0564f
|
|
: (ScrobbleState.status-kind == 2 ? #3fae6a : Theme.text-muted);
|
|
font-size: Typography.legal;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
|
|
// ===================================================================
|
|
// DISCORD — Rich Presence "now listening" (opt-in, independent of the
|
|
// scrobblers above). State + persistence live in DiscordState / ui_prefs;
|
|
// the fire + IPC live in the `discord_rpc` controller.
|
|
// ===================================================================
|
|
Rectangle { height: 12px; }
|
|
GroupHeader { text: @tr("DISCORD"); }
|
|
Rectangle { height: 4px; }
|
|
SettingRow {
|
|
label: @tr("Discord Rich Presence");
|
|
description: @tr("Show what you're listening to as your Discord status. Opt-in — Discord must be running.");
|
|
QbzToggle {
|
|
checked: DiscordState.enabled;
|
|
toggled(v) => {
|
|
DiscordState.enabled = v;
|
|
DiscordState.set-enabled(v);
|
|
}
|
|
}
|
|
}
|
|
SettingRow {
|
|
label: @tr("Flatpak socket access");
|
|
description: @tr("Flatpak install and the presence isn't showing? Grant access to Discord's IPC socket, then restart QBZ:\nflatpak override --user --filesystem=xdg-run/discord-ipc-0 com.blitzfc.qbz");
|
|
}
|
|
}
|