Files
qbz/crates/qbz-ui/ui/settings/LocalLibrarySettings.slint
T
vicrodh c72e71d410 feat(library): album-identity mode — header dropdown, Settings row, persistence
Spec 2026-07-19-local-album-grouping-mode (A2-A5 + i18n). The Albums tab
gains a third header dropdown ('Albums by folder' / 'Albums by metadata')
and Settings > Local Library a matching 'Album grouping' row — both write
the same pref: LocalLibraryState.albums-id-mode, persisted in
locallibrary_ui.json (default 'folder': compilations and box sets stay
whole; the metadata split stays opt-in for #411's use case).

The group key changes with the mode, so switching reloads the Albums set
(reload_albums) and invalidates the Artists tab (its album cache groups
the same way — a folder-mode compilation cross-lists under every artist
in all_artists, the owner's 'appears in all artists' ask). Threaded
through spawn_albums_load, seed_counts, the Plex quality-hydration
aggregate lookup, and the artists feed; myqbz_builder pins Metadata
(artist-scoped candidates, not the Albums view).

5 msgids x 8 locales (msgfmt clean); slintcheck green.

km
2026-07-21 14:11:04 -06:00

830 lines
33 KiB
Plaintext

// Settings > Local Library — the folder-management surface relocated here
// from the Tauri browse view's inline gear panel (per the existing decision
// in local_library.rs: management lives in Settings, the browse-view gear
// routes here). Folder list (add/remove/edit/enable) + filter + maintenance
// (cleanup missing) + two-step danger-zone clear. The scan engine + progress
// bar land with Slice B (LibraryScanState). ADR-008: no pills — plain
// bordered icon buttons + square badges.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { LibraryFoldersState, LibraryManageActions, LibraryScanState, PlexSettingsState, PlexAuthActions , UiFocusState, LocalLibraryState, LocalLibraryActions } from "../state.slint";
import { SettingRow } from "SettingRow.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { QbzCheckbox } from "../primitives/QbzCheckbox.slint";
import { QbzToggle } from "../primitives/QbzToggle.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
import { ExpandableSearch } from "../primitives/ExpandableSearch.slint";
import { ScrollView, 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 OfflineSettings/AudioSettings' SecondaryButton.
component SecondaryButton inherits Rectangle {
in property <string> label;
in property <bool> danger: false;
in property <bool> enabled: true;
callback clicked();
// 170px floor keeps the settings right-edge grid; longer translations grow.
width: Math.max(label-txt.preferred-width + 32px, 170px);
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.5;
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(); }
}
}
}
// Plain bordered icon button for the folder toolbar (ADR-008: NOT a pill).
component IconBtn inherits Rectangle {
in property <image> icon;
in property <bool> enabled: true;
callback clicked();
width: 34px;
height: 34px;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: ta.has-hover && root.enabled ? Theme.surface-hover : Theme.surface-elevated;
opacity: root.enabled ? 1.0 : 0.4;
QbzIcon {
source: root.icon;
width: 16px;
height: 16px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: Theme.text-secondary;
}
ta := TouchArea {
mouse-cursor: root.enabled ? pointer : default;
clicked => {
if (root.enabled) { root.clicked(); }
}
}
}
// Shared column geometry for the folder table (header + rows MUST match).
// checkbox 20 · folder (stretch) · last-scan 120 · status 84 · actions 76.
component FolderTableHeader inherits Rectangle {
height: 26px;
HorizontalLayout {
padding-left: 10px;
padding-right: 12px;
spacing: 12px;
Rectangle { width: 20px; }
Text {
text: @tr("FOLDER");
horizontal-stretch: 1;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Text {
text: @tr("LAST SCAN");
width: 120px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Text {
text: @tr("STATUS");
width: 84px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Rectangle { width: 76px; }
}
}
// One folder row — a compact table row aligned to FolderTableHeader's columns.
component FolderRow inherits Rectangle {
in property <string> display-name;
in property <string> last-scan-label;
in property <bool> enabled-folder;
in property <bool> is-network;
in property <bool> accessible;
in property <bool> selected;
in property <bool> scanning;
callback toggle();
callback sync();
callback remove();
height: 40px;
border-radius: Radius.sm;
background: root.selected
? Theme.surface-elevated
: (ta.has-hover ? Theme.surface-hover : transparent);
// Declared FIRST so it sits BENEATH the per-row IconBtns: their own
// TouchAreas (on top) take their clicks (sync/remove), while clicks on the
// empty row area fall through to here to toggle selection. (Was last —
// i.e. on top — which swallowed the button clicks into a select-toggle.)
ta := TouchArea {
mouse-cursor: pointer;
clicked => { root.toggle(); }
}
HorizontalLayout {
padding-left: 10px;
padding-right: 12px;
spacing: 12px;
// Select.
VerticalLayout {
width: 20px;
alignment: center;
QbzCheckbox {
checked: root.selected;
toggled => { root.toggle(); }
}
}
// FOLDER: type glyph + name.
HorizontalLayout {
horizontal-stretch: 1;
spacing: 8px;
VerticalLayout {
alignment: center;
QbzIcon {
source: root.is-network
? @image-url("../assets/icons/network.svg")
: @image-url("../assets/icons/hard-drive.svg");
width: 16px;
height: 16px;
tint: root.is-network
? (root.accessible ? #3fae6a : #e0564f)
: Theme.text-secondary;
}
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: root.display-name;
color: root.enabled-folder ? Theme.text-primary : Theme.text-muted;
font-size: Typography.body;
overflow: elide;
}
}
}
// LAST SCAN.
VerticalLayout {
width: 120px;
alignment: center;
Text {
text: root.last-scan-label;
color: Theme.text-muted;
font-size: Typography.legal;
overflow: elide;
}
}
// STATUS.
VerticalLayout {
width: 84px;
alignment: center;
Text {
text: !root.enabled-folder
? @tr("Disabled")
: ((root.is-network && !root.accessible) ? @tr("Unavailable") : @tr("Active"));
color: (root.is-network && !root.accessible && root.enabled-folder)
? #e0564f
: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.medium;
overflow: elide;
}
}
// ACTIONS: per-folder sync + delete (recycled IconBtns).
HorizontalLayout {
width: 76px;
spacing: 4px;
alignment: end;
VerticalLayout {
alignment: center;
IconBtn {
icon: @image-url("../assets/icons/refresh-cw.svg");
enabled: root.enabled-folder && !root.scanning;
clicked => { root.sync(); }
}
}
VerticalLayout {
alignment: center;
IconBtn {
icon: @image-url("../assets/icons/trash-2.svg");
clicked => { root.remove(); }
}
}
}
}
}
export component LocalLibrarySettings inherits VerticalLayout {
spacing: 4px;
// Lazy-load the folder list each time the panel is shown (the `if
// section==4` mount in SettingsView creates this fresh on entry). The
// Plex panel below self-loads here too (seed from store + warm cache).
init => {
LibraryManageActions.load();
PlexAuthActions.load();
}
// --- Library folders -------------------------------------------------
HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
GroupHeader { text: @tr("LIBRARY FOLDERS"); }
}
// Toolbar: scan-all / add / edit (one selected) / remove (any selected).
IconBtn {
icon: @image-url("../assets/icons/refresh-cw.svg");
enabled: !LibraryScanState.scanning;
clicked => { LibraryManageActions.scan-all(); }
}
IconBtn {
icon: @image-url("../assets/icons/folder-plus.svg");
clicked => { LibraryManageActions.add-folder(); }
}
IconBtn {
icon: @image-url("../assets/icons/pencil.svg");
enabled: LibraryFoldersState.selected-count == 1;
clicked => { LibraryManageActions.edit-folder(0); }
}
IconBtn {
icon: @image-url("../assets/icons/trash-2.svg");
enabled: LibraryFoldersState.selected-count > 0;
clicked => { LibraryManageActions.remove-folders(); }
}
}
Rectangle { height: 6px; }
// Filter.
HorizontalLayout {
ExpandableSearch {
placeholder: @tr("Filter folders...");
sm: true;
query <=> LibraryFoldersState.filter;
edited(q) => { LibraryManageActions.set-filter(q); }
}
Rectangle { horizontal-stretch: 1; }
}
Rectangle { height: 8px; }
// Folder list.
if LibraryFoldersState.folders.length == 0 && !LibraryFoldersState.loading: Text {
text: LibraryFoldersState.filter == ""
? @tr("No folders yet. Add a folder to build your local library.")
: @tr("No folders match your filter.");
color: Theme.text-muted;
font-size: Typography.body;
}
// Compact table: sticky header + a scrolling body (internal scroll, capped
// height) aligned to the same columns. Rows recycle the sync/delete buttons.
if LibraryFoldersState.folders.length > 0: VerticalLayout {
spacing: 0px;
FolderTableHeader { }
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
Rectangle {
height: Math.min(LibraryFoldersState.folders.length * 41px, 360px);
ScrollView {
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
spacing: 1px;
for f in LibraryFoldersState.folders: FolderRow {
display-name: f.display-name;
last-scan-label: f.last-scan-label;
enabled-folder: f.enabled;
is-network: f.is-network;
accessible: f.accessible;
selected: f.selected;
scanning: LibraryScanState.scanning;
toggle => { LibraryManageActions.toggle-folder-select(f.id); }
sync => { LibraryManageActions.scan-folder(f.id); }
remove => { LibraryManageActions.remove-folder(f.id); }
}
}
}
}
}
// --- Scan progress (determinate; only while scanning) ----------------
if LibraryScanState.scanning: Rectangle {
height: scan-box.preferred-height + 24px;
VerticalLayout {
scan-box := VerticalLayout {
padding-top: 14px;
spacing: 8px;
HorizontalLayout {
Text {
text: @tr("Scanning") + ": \{LibraryScanState.processed-files} / \{LibraryScanState.total-files}";
color: Theme.text-secondary;
font-size: Typography.body;
font-weight: Typography.medium;
horizontal-stretch: 1;
vertical-alignment: center;
}
// Stop scan.
Rectangle {
// Size to content. The inner HorizontalLayout fills this
// Rectangle, so the old `x: (parent-self)/2` centering
// collapsed to 0 and the glyph touched the border — use
// explicit padding on the row instead (same fix as the
// album VersionPicker).
width: stop-row.preferred-width;
height: 30px;
border-radius: Radius.sm;
border-width: 1px;
border-color: #e0564f;
background: stop-ta.has-hover ? #e0564f22 : transparent;
stop-row := HorizontalLayout {
padding-left: 12px;
padding-right: 12px;
spacing: 6px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 13px;
height: 13px;
tint: #e0564f;
}
}
VerticalLayout {
alignment: center;
Text {
text: @tr("Stop");
color: #e0564f;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
}
}
stop-ta := TouchArea {
mouse-cursor: pointer;
clicked => { LibraryManageActions.stop-scan(); }
}
}
}
// Determinate bar (2 rectangles; ADR-008-clean).
Rectangle {
height: 6px;
border-radius: 3px;
background: Theme.surface-elevated;
clip: true;
Rectangle {
x: 0;
width: parent.width * LibraryScanState.progress;
height: parent.height;
border-radius: 3px;
background: Theme.accent;
}
}
if LibraryScanState.current-file != "": Text {
text: LibraryScanState.current-file;
color: Theme.text-muted;
font-size: Typography.legal;
overflow: elide;
}
}
}
}
Rectangle { height: 22px; }
// --- Albums view -------------------------------------------------------
// Same pref as the Albums-tab header dropdown — this is the discoverable
// home, the header is the quick switch (both write albums-id-mode).
GroupHeader { text: @tr("ALBUMS VIEW"); }
SettingRow {
label: @tr("Album grouping");
description: @tr("Folders: one card per album folder — best for compilations and box sets. Metadata: split by album + artist tags.");
VerticalLayout {
alignment: center;
QbzSelect {
menu-width: 190px;
sm: true;
options: [
@tr("Albums by folder"),
@tr("Albums by metadata"),
];
current-index: LocalLibraryState.albums-id-mode == "metadata" ? 1 : 0;
selected(i) => {
LocalLibraryActions.albums-set-id-mode(i == 1 ? "metadata" : "folder");
}
}
}
}
Rectangle { height: 22px; }
// --- Maintenance -----------------------------------------------------
GroupHeader { text: @tr("MAINTENANCE"); }
SettingRow {
label: @tr("Cleanup missing files");
description: @tr("Remove tracks whose files no longer exist on disk.");
VerticalLayout {
alignment: center;
spacing: 4px;
SecondaryButton {
label: LibraryFoldersState.cleaning-missing ? @tr("Cleaning up...") : @tr("Cleanup");
enabled: !LibraryFoldersState.cleaning-missing;
clicked => { LibraryManageActions.cleanup-missing(); }
}
if LibraryFoldersState.cleanup-status != "": Text {
text: LibraryFoldersState.cleanup-status;
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: right;
}
}
}
Rectangle { height: 22px; }
// --- Danger zone -----------------------------------------------------
GroupHeader { text: @tr("DANGER ZONE"); }
SettingRow {
label: @tr("Clear library database");
description: @tr("Remove all indexed tracks. Your audio files are not deleted.");
SecondaryButton {
label: LibraryFoldersState.clearing-library ? @tr("Clearing...") : @tr("Clear all");
danger: true;
enabled: !LibraryFoldersState.clearing-library;
clicked => { LibraryManageActions.clear-library(); }
}
}
Rectangle { height: 28px; }
// ===================================================================
// PLEX — LAN-only Plex Media Server connection (Settings > Plex).
// Body renders only when `enabled && !ui-collapsed`. ADR-008: no pills.
// ===================================================================
// Header row: title + master toggle + collapse chevron.
HorizontalLayout {
spacing: 10px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
spacing: 2px;
GroupHeader { text: @tr("PLEX"); vertical-alignment: center; }
Text {
text: @tr("Connect a Plex Media Server on your local network.");
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 PlexSettingsState.enabled: VerticalLayout {
alignment: center;
Rectangle {
width: 28px;
height: 28px;
border-radius: Radius.sm;
background: chev-ta.has-hover ? Theme.surface-hover : transparent;
QbzIcon {
source: PlexSettingsState.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 => {
PlexSettingsState.ui-collapsed = !PlexSettingsState.ui-collapsed;
PlexAuthActions.collapse-toggle(PlexSettingsState.ui-collapsed);
}
}
}
}
VerticalLayout {
alignment: center;
QbzToggle {
checked: PlexSettingsState.enabled;
toggled(v) => {
PlexSettingsState.enabled = v;
PlexAuthActions.enable-toggle(v);
}
}
}
}
// Body — only when enabled and not collapsed.
if PlexSettingsState.enabled && !PlexSettingsState.ui-collapsed: VerticalLayout {
spacing: 4px;
Rectangle { height: 10px; }
// --- Server address ------------------------------------------------
SettingRow {
label: @tr("Server address");
description: @tr("Only local network servers are supported.");
HorizontalLayout {
width: 240px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
LineEdit {
text: PlexSettingsState.server-url;
placeholder-text: @tr("http://127.0.0.1:32400");
// 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; }
edited(s) => {
PlexSettingsState.server-url = s;
}
accepted(s) => {
PlexAuthActions.set-server-url(s);
}
}
}
}
}
// LAN-only inline warning (read-only indicator).
if !PlexSettingsState.is-local-address && PlexSettingsState.server-url != "": Text {
text: @tr("Only local network servers are supported.");
color: #e0564f;
font-size: Typography.legal;
wrap: word-wrap;
}
Rectangle { height: 8px; }
// --- Authorize (PIN) ----------------------------------------------
// Hidden in manual-token mode.
if !PlexSettingsState.manual-token-mode: SettingRow {
label: @tr("Authorize");
description: @tr("Generate a code and sign in to Plex in your browser.");
VerticalLayout {
alignment: center;
SecondaryButton {
label: PlexSettingsState.pin-busy ? @tr("Working...") : @tr("Generate code");
enabled: PlexSettingsState.enabled
&& PlexSettingsState.is-local-address
&& !PlexSettingsState.pin-busy;
clicked => { PlexAuthActions.generate-code(); }
}
}
}
// --- Code block (only while a code exists) ------------------------
if PlexSettingsState.pin-code != "": SettingRow {
label: @tr("Link code");
description: @tr("Enter this code at the Plex sign-in page.");
HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
Rectangle {
width: code-text.preferred-width + 20px;
height: 34px;
border-radius: Radius.sm;
border-width: 1px;
border-color: Theme.border-subtle;
background: Theme.surface-elevated;
code-text := Text {
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
text: PlexSettingsState.pin-code;
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.semibold;
letter-spacing: 1.5px;
}
}
}
SecondaryButton {
label: @tr("Copy code");
clicked => { PlexAuthActions.copy-code(); }
}
SecondaryButton {
label: @tr("Open Plex sign-in");
clicked => { PlexAuthActions.open-auth-url(); }
}
}
}
Rectangle { height: 8px; }
// --- Manual token toggle + field ----------------------------------
SettingRow {
label: @tr("Enter token manually");
description: @tr("Use an existing X-Plex-Token instead of the PIN flow.");
VerticalLayout {
alignment: center;
QbzToggle {
checked: PlexSettingsState.manual-token-mode;
toggled(v) => {
PlexSettingsState.manual-token-mode = v;
PlexAuthActions.manual-token-toggle(v);
}
}
}
}
if PlexSettingsState.manual-token-mode: SettingRow {
label: @tr("Token");
HorizontalLayout {
width: 240px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
LineEdit {
text: PlexSettingsState.token-input;
placeholder-text: @tr("X-Plex-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;
edited(s) => {
PlexSettingsState.token-input = s;
}
accepted(s) => {
PlexAuthActions.set-token(s);
}
}
}
}
}
Rectangle { height: 14px; }
// --- Connection actions -------------------------------------------
SettingRow {
label: @tr("Check connection");
description: @tr("Verify the server is reachable.");
VerticalLayout {
alignment: center;
SecondaryButton {
label: @tr("Check connection");
enabled: PlexSettingsState.can-use && !PlexSettingsState.busy;
clicked => { PlexAuthActions.ping(); }
}
}
}
SettingRow {
label: @tr("Libraries");
description: @tr("Fetch your Plex music libraries.");
VerticalLayout {
alignment: center;
SecondaryButton {
label: @tr("Get libraries");
enabled: PlexSettingsState.can-use && !PlexSettingsState.busy;
clicked => { PlexAuthActions.load-sections(); }
}
}
}
// Status line (resolved (key,args) -> text; highlighted when ok).
if PlexSettingsState.status-text != "": Text {
text: PlexSettingsState.status-text;
color: PlexSettingsState.status-kind == 3
? #e0564f
: (PlexSettingsState.status-kind == 2 ? #3fae6a : Theme.text-muted);
font-size: Typography.legal;
wrap: word-wrap;
}
Rectangle { height: 6px; }
// --- Section multi-select grid ------------------------------------
if PlexSettingsState.sections.length > 0: VerticalLayout {
spacing: 2px;
GroupHeader { text: @tr("MUSIC LIBRARIES"); }
Rectangle { height: 4px; }
Rectangle {
height: Math.min(PlexSettingsState.sections.length * 44px, 264px);
ScrollView {
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
spacing: 2px;
for sec in PlexSettingsState.sections: Rectangle {
height: 42px;
border-radius: Radius.sm;
background: sec-ta.has-hover ? Theme.surface-hover : transparent;
HorizontalLayout {
padding-left: 10px;
padding-right: 12px;
spacing: 12px;
VerticalLayout {
alignment: center;
QbzCheckbox {
checked: sec.selected;
toggled => { PlexAuthActions.toggle-section(sec.key); }
}
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: sec.title;
color: Theme.text-primary;
font-size: Typography.body;
overflow: elide;
}
}
VerticalLayout {
alignment: center;
Text {
text: @tr("({} tracks)", sec.count);
color: Theme.text-muted;
font-size: Typography.legal;
}
}
}
sec-ta := TouchArea {
mouse-cursor: pointer;
clicked => { PlexAuthActions.toggle-section(sec.key); }
}
}
}
}
}
Rectangle { height: 14px; }
}
// --- Metadata write toggle ----------------------------------------
SettingRow {
label: @tr("Write metadata to Plex (experimental)");
description: @tr("Allow QBZ to write track metadata back to your Plex server.");
VerticalLayout {
alignment: center;
QbzToggle {
checked: PlexSettingsState.metadata-write-enabled;
toggled(v) => {
PlexSettingsState.metadata-write-enabled = v;
PlexAuthActions.metadata-write-toggle(v);
}
}
}
}
Rectangle { height: 18px; }
// --- Disconnect / Clear cache (danger) ----------------------------
GroupHeader { text: @tr("PLEX DANGER ZONE"); }
SettingRow {
label: @tr("Disconnect");
description: @tr("Sign out of Plex and clear the local cache.");
SecondaryButton {
label: @tr("Disconnect");
danger: true;
enabled: PlexSettingsState.token != "";
clicked => { PlexAuthActions.disconnect(); }
}
}
SettingRow {
label: @tr("Clear cache");
description: @tr("Remove cached Plex libraries and tracks. Your sign-in is kept.");
SecondaryButton {
label: @tr("Clear cache");
danger: true;
enabled: PlexSettingsState.token != "";
clicked => { PlexAuthActions.clear-cache(); }
}
}
}
}