refactor(frontend): retire Slint application

cx
This commit is contained in:
vicrodh committed 2026-08-26 11:10:18 -06:00
1 parent fb6c38cbfd
commit 69ed612077
720 files changed
+93 -273809

No files matched your search

+32 -3625
View File
File diff suppressed because it is too large. Load diff
+7 -66
View File
@@ -31,19 +31,9 @@ members = [
"qconnect-protocol",
"qconnect-transport-ws",
"qconnect-app",
"qbz-ui",
# The HiFi Wizard's frontend-agnostic core (ADR-006), extracted for the Qt
# port. `qbz-dac-wizard` is the SLINT one and is deliberately NOT wired to
# this crate: the Slint tree is frozen (owner, 2026-08-11 — "ya no hacemos
# cambios para Slint"), and rewiring it would have been a change to a tree
# nobody compiles or tests any more. The two therefore hold the same logic
# twice; that is safe only BECAUSE the Slint side is frozen, and it is the
# reason a fix here never needs porting back.
# The HiFi Wizard's frontend-agnostic core (ADR-006), consumed by Qt.
"qbz-dac-wizard-core",
"qbz-dac-wizard",
"qbz-text-utils",
"qbz-slint-common",
"qbz",
"qbz-credentials",
"qbz-media-controls",
"qbz-mixtape",
@@ -57,8 +47,7 @@ members = [
"qbzd",
"qbz-qt",
# The source-agnostic seam (one trait + one registry over Qobuz / local /
# Plex). Purely additive: nothing in `qbz` or `qbz-mixtape` changes, and it
# deliberately does NOT depend on qbz-audio / qbz-player.
# Plex). It deliberately does NOT depend on qbz-audio / qbz-player.
"qbz-source",
# Media-server protocol crates: siblings of `qbz-plex`, same shape. HTTP +
# DTOs only — no cache schema, no settings store, no Qt (ADR-006).
@@ -69,9 +58,8 @@ members = [
"qbz-media-cache",
]
# Qt-frontend POC worktree: plain `cargo build`/`cargo check` in this workspace
# produces ONLY the Qt binary (plus its Slint-free dependency closure). The
# Slint crates remain members but are never built by default here.
# The desktop application is Qt/QML. Plain `cargo build`/`cargo check` produces
# that binary plus its frontend-agnostic dependency closure.
default-members = ["qbz-qt"]
[workspace.package]
@@ -126,57 +114,10 @@ strsim = "0.11"
[profile.release]
# Strip symbol tables from release binaries. Measured on 2026-07-02:
# 400 MB -> 218 MB (-45%). There are no .debug_* sections in a default
# release build — the weight is .strtab (151 MB) + .symtab (22 MB), inflated
# by Slint's generated generic symbol names. Trade-off: panic backtraces show
# addresses instead of names; the release workflow archives the UNSTRIPPED
# binary as an Actions artifact per tag for offline symbolication.
# 400 MB -> 218 MB (-45%). Trade-off: panic backtraces show addresses instead
# of names; release automation retains an unstripped artifact for offline
# symbolication.
# Codegen knobs (opt-level / codegen-units / jobs) intentionally stay in the
# workflows' env (CARGO_PROFILE_RELEASE_*) — they are CI-memory tuning, not
# a property of the package.
strip = "symbols"
[patch.crates-io]
# Slint 1.16's femtovg-wgpu renderer leaves the wgpu surface alpha mode at
# `Auto`, which resolves as an opaque compositor surface on KWin/Wayland and
# makes borderless transparent windows show a black rectangular backing. The
# patch keeps Slint pinned but chooses a supported non-opaque composite alpha
# mode when the surface advertises one, so the miniplayer card can have real
# transparent rounded corners. The non-opaque mode is PER-WINDOW opt-in
# (`wgpu::set_surface_prefers_transparent`, flipped from qbz's winit
# window-attributes hook): only the miniplayer pays the compositor-blend cost,
# the main window keeps an Opaque swapchain.
i-slint-renderer-femtovg = { path = "vendor/i-slint-renderer-femtovg-1.16.1" }
# On modern macOS, fontique's CoreText per-script fallback can fail to resolve
# a Han-script family (the suggested PingFang is not always in the scanned
# family map), so Japanese kanji rendered blank while kana worked. The patch
# pre-registers the known system CJK families (Hiragino/PingFang/Apple SD
# Gothic Neo/Arial Unicode MS, Japanese-biased default) in the fallback map at
# collection creation, which takes priority over the CoreText query. macOS-only
# (#[cfg]); Linux keeps its working fontconfig fallback.
i-slint-common = { path = "vendor/i-slint-common-1.16.1" }
# With `overflow: elide`, sharedparley culled EVERY line whose bottom edge
# exceeded max_height — including the FIRST one, eliding the text to nothing.
# CJK fallback fonts (Hiragino on macOS) report line metrics taller than
# snug fixed-height boxes (TrackRow's 20px title line at 14px), so every
# CJK track title rendered blank on macOS while Latin fit (#543; verified
# with a headless repro matrix on the Mac Mini). The patch always draws the
# first line — it still gets the ellipsis treatment as the last visible one.
i-slint-core = { path = "vendor/i-slint-core-1.16.1" }
# Slint compiles femtovg with the swash raster path. Swash fails to scale
# some variable fonts at a non-default variation instance (the CFF2-based
# CJK system fonts on modern macOS), and femtovg cached those glyphs as
# permanently missing — so CJK text at any non-regular weight rendered
# blank (#543; regular weight = empty coords = renders fine). The patch
# retries the glyph at the font's DEFAULT instance before giving up: worst
# case the glyph draws at regular weight instead of not at all. No effect
# on fonts/instances that already render.
femtovg = { path = "vendor/femtovg-0.23.2" }
# macOS Skia renderer: skia bug 310510989 makes new_from_data fail for TTC
# faces at index > 0, and for some system collections (AppleSDGothicNeo.ttc,
# where EVERY weight sits at index > 0) the upstream read/write-fonts rebuild
# workaround ALSO fails — every Korean glyph run at non-regular weights drew
# nothing. The patch adds a second-chance workaround: resolve the same face
# through CoreText by family name + OS/2 weight, guarded by cmap glyph-id
# spot checks so a wrong match is rejected instead of drawing wrong glyphs.
i-slint-renderer-skia = { path = "vendor/i-slint-renderer-skia-1.16.1" }
+1 -6
View File
@@ -8,12 +8,7 @@
//! for no structural reason at all. ADR-006.
//!
//! The crash-chain level is injected by the host rather than read from a
//! frontend-local static: both binaries keep their own probe file, and this
//! module only needs the NUMBER.
//!
//! DEBT: the Slint copy is still there, untouched on purpose — moving it forces
//! a rebuild of the Slint binary, which pulls `qbz-ui` and its ~30 GB compile
//! that this box cannot verify. Collapse the two the next time Slint is built.
//! frontend-local static; this module only needs the numeric level.
//! What it is: the glue for the `persist_session` /
//! `resume_playback_position` playback preferences.
+1 -1
View File
@@ -11,7 +11,7 @@ name = "qbz_dac_wizard_core"
path = "src/lib.rs"
# Deliberately NO frontend dependency (no slint, no qbz-ui, no cxx-qt). ADR-006:
# the core is frontend-agnostic so Slint, Qt and qbzd consume the SAME logic.
# The core is frontend-agnostic so Qt and future non-GUI consumers can share it.
# Everything here returns plain data; pushing it into a UI is the frontend's job.
[dependencies]
qbz-audio = { path = "../qbz-audio" }
+5 -19
View File
@@ -2,26 +2,12 @@
//!
//! # Why this crate exists
//!
//! The wizard shipped as `qbz-dac-wizard`, whose every public function takes a
//! `&AppWindow` and writes straight into Slint globals — so the Qt port could
//! not reach any of it. This crate is that logic, lifted out and returning
//! plain data: `qbz-qt` drives it through `dac_wizard_qt.rs`. ADR-006,
//! frontend-agnostic core.
//! The retired Slint wizard wrote directly into frontend globals. This crate is
//! the surviving logic, returning plain data: `qbz-qt` drives it through
//! `dac_wizard_qt.rs`. ADR-006, frontend-agnostic core.
//!
//! # Its relationship to `qbz-dac-wizard` — read before "deduplicating"
//!
//! `qbz-dac-wizard` still holds its own copy of this logic and is NOT wired to
//! this crate. That is deliberate: the Slint tree is FROZEN (owner,
//! 2026-08-11 — "ya no hacemos cambios para Slint"), so rewiring it would have
//! been an unverifiable change to a tree nobody compiles or tests any more,
//! bought for a deduplication that a frozen tree cannot benefit from.
//!
//! The usual objection to two copies is drift. It does not apply while the
//! other copy never changes — and if Slint is ever unfrozen, the merge is the
//! diff of one file. Fixes land HERE; nothing needs porting back.
//!
//! `qbzd` duplicates parts of this again in `tui/wizard_core.rs`. THAT one is
//! live code and is a genuine candidate to fold onto this crate.
//! `qbzd` duplicates parts of this in `tui/wizard_core.rs`. That one is live
//! code and remains a genuine candidate to fold onto this crate.
//!
//! # What is guaranteed
//!
-18
View File
@@ -1,18 +0,0 @@
[package]
name = "qbz-dac-wizard"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "HiFi/DAC setup wizard controller (Slint) — extracted feature crate."
[lib]
name = "qbz_dac_wizard"
path = "src/lib.rs"
[dependencies]
qbz-ui = { path = "../qbz-ui" }
qbz-audio = { path = "../qbz-audio" }
qbz-models = { path = "../qbz-models" }
qbz-i18n = { path = "../qbz-i18n" }
slint = { version = "=1.16.1", default-features = false, features = ["std", "compat-1-2"] }
-794
View File
@@ -1,794 +0,0 @@
//! HiFi Wizard (DAC setup) controller.
//!
//! Slice 6 (check step): runs the frontend-agnostic audio-stack probes
//! (`qbz_audio::health`) on open, maps them to per-distro copy-paste
//! remediations the check step renders, and recomputes them when the user
//! overrides the distro. Read-only — nothing here writes a system file or
//! opens a stream.
use std::sync::Mutex;
use qbz_audio::{AudioStackHealth, Distro, InitSystem};
use slint::{ComponentHandle, Model, ModelRc, VecModel};
use qbz_ui::{AppWindow, DacCandidateRow, DacConfigRow, DacWizardState, RemediationRow};
// Last probe result, so a distro override recomputes commands without
// re-shelling on every dropdown change.
static LAST_HEALTH: Mutex<Option<AudioStackHealth>> = Mutex::new(None);
/// Synchronous part of opening the wizard: reset, fill the distro dropdown
/// (auto-detected, always overridable), and show a "checking…" state until the
/// async probe lands.
pub fn open_immediate(window: &AppWindow) {
let st = window.global::<DacWizardState>();
st.set_open(true);
st.set_step(0);
st.set_welcome_confirmed(false);
let distro_opts: Vec<slint::SharedString> =
Distro::ALL.iter().map(|d| d.label().into()).collect();
st.set_distro_options(ModelRc::new(VecModel::from(distro_opts)));
st.set_distro_index(qbz_audio::detect_distro().index() as i32);
let init_opts: Vec<slint::SharedString> =
InitSystem::ALL.iter().map(|i| i.label().into()).collect();
st.set_init_options(ModelRc::new(VecModel::from(init_opts)));
st.set_init_index(qbz_audio::detect_init().index() as i32);
let sandbox = qbz_audio::detect_sandbox();
st.set_sandboxed(sandbox != qbz_audio::Sandbox::None);
st.set_sandbox_name(
match sandbox {
qbz_audio::Sandbox::Flatpak => "Flatpak",
qbz_audio::Sandbox::Snap => "Snap",
qbz_audio::Sandbox::None => "",
}
.into(),
);
st.set_health_ok(false);
st.set_health_summary(qbz_i18n::t("Checking your audio stack…").into());
st.set_remediations(ModelRc::new(VecModel::from(Vec::<RemediationRow>::new())));
}
/// Apply a completed health probe: cache it and re-render from the current
/// distro/init selections.
pub fn apply_health(window: &AppWindow, health: AudioStackHealth) {
*LAST_HEALTH.lock().unwrap() = Some(health);
recompute(window);
}
/// User overrode the distro (package manager) — recompute.
pub fn set_distro(window: &AppWindow, index: i32) {
window.global::<DacWizardState>().set_distro_index(index);
recompute(window);
}
/// User overrode the init system (service commands) — recompute.
pub fn set_init(window: &AppWindow, index: i32) {
window.global::<DacWizardState>().set_init_index(index);
recompute(window);
}
/// Rebuild the remediations from the cached probe + the current distro/init
/// dropdown selections (either of which the user can override).
fn recompute(window: &AppWindow) {
let st = window.global::<DacWizardState>();
let health = LAST_HEALTH
.lock()
.unwrap()
.unwrap_or_else(qbz_audio::audio_stack_health);
let distro = Distro::ALL
.get(st.get_distro_index().max(0) as usize)
.copied()
.unwrap_or(Distro::Other);
let init = InitSystem::ALL
.get(st.get_init_index().max(0) as usize)
.copied()
.unwrap_or(InitSystem::Unknown);
// In a sandbox the host probes are blind, so don't render a health verdict —
// show reference setup commands for the chosen distro/init (Tauri-style,
// which never probed either). The UI shows a sandbox info banner instead.
let rows = if st.get_sandboxed() {
st.set_health_ok(false);
st.set_health_summary("".into());
reference_commands(distro, init)
} else {
let r = remediations(health, distro, init);
st.set_health_ok(health.is_ready());
st.set_health_summary(if health.is_ready() {
qbz_i18n::t("Your audio stack is ready for bit-perfect playback.").into()
} else {
let n = r.len();
qbz_i18n::tf(
"{} item needs attention before bit-perfect playback will work.",
"{} items need attention before bit-perfect playback will work.",
n as i64,
&[&n.to_string()],
)
.into()
});
r
};
let model: Vec<RemediationRow> = rows
.into_iter()
.map(|(caption, command)| RemediationRow {
caption: caption.into(),
command: command.into(),
})
.collect();
st.set_remediations(ModelRc::new(VecModel::from(model)));
}
/// (caption, copy-paste command) per missing probe, for the given distro.
///
/// Service/restart commands are INIT-SYSTEM aware per distro (OpenRC on Gentoo,
/// runit on Void, systemd elsewhere), mirroring the Tauri DistroSelector
/// `restartCommands`. Installs and the restart are kept as separate blocks so
/// the multi-line Gentoo guidance never gets `&&`-joined.
fn remediations(h: AudioStackHealth, d: Distro, init: InitSystem) -> Vec<(String, String)> {
// NixOS is declarative: you don't `apt/pacman install` pieces — you enable
// the PipeWire module and rebuild. So collapse all the missing pieces into
// one config block instead of per-package commands.
if d == Distro::NixOS {
if h.is_ready() {
return Vec::new();
}
return vec![(
qbz_i18n::t("Enable PipeWire in your NixOS configuration"),
NIXOS_PIPEWIRE_BLOCK.to_string(),
)];
}
let mut out = Vec::new();
let mut needs_restart = false;
if !h.has_pw_dump {
out.push((
qbz_i18n::t("Install the PipeWire tools (pw-dump)"),
install(d, pkg_pw_tools(d)),
));
needs_restart = true;
}
if !h.cpal_sees_pipewire {
// THE Ubuntu no-list / no-playback bug: the ALSA->PipeWire bridge PCM.
out.push((
qbz_i18n::t("Install the ALSA bridge so playback can reach PipeWire"),
install(d, "pipewire-alsa"),
));
needs_restart = true;
}
if !h.has_pactl {
out.push((
qbz_i18n::t("Install the Pulse compatibility tools (optional fallback)"),
install(d, pkg_pulse(d)),
));
needs_restart = true;
}
if !h.any_devices {
out.push((
qbz_i18n::t("No sinks detected — reinstall the ALSA UCM profiles, then reboot"),
install_reinstall(d, "alsa-ucm-conf"),
));
}
// WirePlumber down, or we just installed something → (re)start the stack
// with the ACTUAL init system running on this machine (not guessed from the
// distro — Gentoo+systemd and Gentoo+OpenRC must differ).
if !h.wireplumber_active || needs_restart {
out.push((
qbz_i18n::t("(Re)start the PipeWire audio services"),
restart_cmd(init).to_string(),
));
}
out
}
/// Init-system-aware "(re)start the audio services" command. PipeWire is a
/// user-session service, so only systemd has a first-class `--user` restart;
/// the others either use their user-service supervisor or a re-login.
fn restart_cmd(init: InitSystem) -> &'static str {
match init {
InitSystem::Systemd => "systemctl --user restart pipewire pipewire-pulse wireplumber",
InitSystem::OpenRc => {
"# OpenRC: PipeWire runs in your user session, not as an OpenRC service.\n\
# Log out and back in to restart it."
}
InitSystem::Runit => {
"sv restart pipewire wireplumber # if set up as runit user services; otherwise log out and back in"
}
InitSystem::S6 => "# s6: restart via your supervision tree, or log out and back in",
InitSystem::Dinit => "dinitctl restart pipewire wireplumber # or log out and back in",
InitSystem::Unknown => "# Restart PipeWire via your init system, or log out and back in",
}
}
fn pkg_pw_tools(d: Distro) -> &'static str {
match d {
// Debian-family (incl. antiX) ship pw-* in pipewire-bin.
Distro::Debian | Distro::Antix => "pipewire-bin",
Distro::Fedora => "pipewire-utils",
// Arch (incl. Artix) / openSUSE / Gentoo / Void ship pw-* with pipewire.
_ => "pipewire",
}
}
fn pkg_pulse(d: Distro) -> &'static str {
match d {
Distro::Debian | Distro::Antix => "pipewire-pulse pulseaudio-utils",
Distro::Fedora => "pipewire-pulseaudio",
_ => "pipewire-pulse",
}
}
fn install(d: Distro, pkgs: &str) -> String {
match d {
// Package manager is a property of the distro family, NOT the init.
Distro::Debian | Distro::Antix => format!("sudo apt install {pkgs}"),
Distro::Fedora => format!("sudo dnf install {pkgs}"),
Distro::Arch | Distro::Artix => format!("sudo pacman -S {pkgs}"),
Distro::OpenSuse => format!("sudo zypper install {pkgs}"),
Distro::Gentoo => format!("sudo emerge {pkgs} # package name may differ on Gentoo"),
Distro::Void => format!("sudo xbps-install -S {pkgs}"),
// NixOS is special-cased in remediations(); this is an unreached fallback.
Distro::NixOS => format!("# NixOS: add to configuration.nix (see the PipeWire block) — {pkgs}"),
Distro::Other => format!("Install with your package manager: {pkgs}"),
}
}
fn install_reinstall(d: Distro, pkg: &str) -> String {
match d {
Distro::Debian | Distro::Antix => format!("sudo apt install --reinstall {pkg}"),
Distro::Fedora => format!("sudo dnf reinstall {pkg}"),
_ => install(d, pkg),
}
}
const NIXOS_PIPEWIRE_BLOCK: &str = "# /etc/nixos/configuration.nix:\n\
services.pipewire = {\n\
\u{20}\u{20}enable = true;\n\
\u{20}\u{20}alsa.enable = true;\n\
\u{20}\u{20}pulse.enable = true;\n\
\u{20}\u{20}wireplumber.enable = true;\n\
};\n\
# then apply:\n\
sudo nixos-rebuild switch";
/// Full reference setup commands for the chosen distro/init, shown when QBZ
/// can't probe the host (sandbox). Mirrors the Tauri DistroSelector, which
/// always showed per-distro install + restart commands (no probing).
fn reference_commands(d: Distro, init: InitSystem) -> Vec<(String, String)> {
if d == Distro::NixOS {
return vec![(
qbz_i18n::t("Enable PipeWire in your NixOS configuration"),
NIXOS_PIPEWIRE_BLOCK.to_string(),
)];
}
vec![
(
qbz_i18n::t("Install the PipeWire audio stack"),
install(d, full_stack_pkgs(d)),
),
(
qbz_i18n::t("(Re)start the PipeWire audio services"),
restart_cmd(init).to_string(),
),
]
}
/// The full recommended package set (incl. `pipewire-alsa`, the bit the old
/// Tauri list omitted — the cause of the Ubuntu empty-list bug).
fn full_stack_pkgs(d: Distro) -> &'static str {
match d {
Distro::Debian | Distro::Antix => {
"pipewire pipewire-pulse pipewire-alsa wireplumber alsa-utils"
}
Distro::Fedora => "pipewire pipewire-pulseaudio pipewire-alsa wireplumber alsa-utils",
Distro::Arch | Distro::Artix => {
"pipewire pipewire-pulse pipewire-alsa wireplumber alsa-utils"
}
Distro::OpenSuse => "pipewire pipewire-pulseaudio pipewire-alsa wireplumber alsa-utils",
Distro::Gentoo => "media-video/pipewire media-video/wireplumber media-sound/alsa-utils",
Distro::Void => "pipewire wireplumber alsa-utils",
Distro::NixOS => "",
Distro::Other => "pipewire pipewire-pulse wireplumber alsa-utils",
}
}
// ── Slice 7: select-dacs (auto-detect + manual escape hatch) ───────────────
/// Plain, `Send` candidate produced on the worker thread.
pub struct DacCandidateData {
id: String,
description: String,
bus: String,
is_default: bool,
looks_like_dac: bool,
rates_label: String,
}
/// Immediate UI feedback before the (blocking) enumeration runs.
pub fn begin_detect(window: &AppWindow) {
window.global::<DacWizardState>().set_detecting(true);
}
/// Heavy work (enumerate sinks via the pw-dump-robust path + probe rates for
/// the likely DACs). Runs on a blocking thread; returns plain data.
pub fn detect_blocking() -> Vec<DacCandidateData> {
let devices = qbz_audio::backend::BackendManager::create_backend(
qbz_audio::backend::AudioBackendType::PipeWire,
)
.and_then(|b| b.enumerate_devices())
.unwrap_or_default();
let mut out = Vec::new();
for d in devices {
let bus = d.device_bus.unwrap_or_default();
let looks_like_dac = d.is_hardware && (bus == "usb" || bus == "pci");
// Only probe rates for likely DACs (skip virtual/monitor sinks).
let rates_label = if looks_like_dac {
format_rates(&qbz_audio::query_dac_capabilities(&d.id).sample_rates)
} else {
String::new()
};
let description = if d.name.is_empty() { d.id.clone() } else { d.name };
out.push(DacCandidateData {
id: d.id,
description,
bus,
is_default: d.is_default,
looks_like_dac,
rates_label,
});
}
out
}
/// Apply enumerated candidates to the state. Pre-selects the likely DACs; if
/// nothing enumerated, flips `has-enumeration` off so the manual escape hatch
/// shows.
pub fn apply_candidates(window: &AppWindow, data: Vec<DacCandidateData>) {
let st = window.global::<DacWizardState>();
let rows: Vec<DacCandidateRow> = data
.iter()
.map(|d| DacCandidateRow {
id: d.id.clone().into(),
description: d.description.clone().into(),
bus: d.bus.clone().into(),
is_default: d.is_default,
looks_like_dac: d.looks_like_dac,
checked: d.looks_like_dac,
rates_label: d.rates_label.clone().into(),
})
.collect();
let any = rows.iter().any(|r| r.checked);
st.set_has_enumeration(!data.is_empty());
st.set_any_dac_selected(any);
st.set_candidates(ModelRc::new(VecModel::from(rows)));
st.set_detecting(false);
}
/// Flip one candidate's checkbox + recompute the Next gate.
pub fn toggle_dac(window: &AppWindow, index: i32) {
let st = window.global::<DacWizardState>();
let model = st.get_candidates();
if let Some(vm) = model
.as_any()
.downcast_ref::<VecModel<DacCandidateRow>>()
{
if let Some(mut row) = vm.row_data(index.max(0) as usize) {
row.checked = !row.checked;
vm.set_row_data(index.max(0) as usize, row);
}
}
let any = (0..model.row_count()).any(|i| model.row_data(i).map(|r| r.checked).unwrap_or(false));
st.set_any_dac_selected(any);
}
/// Validate a manually-pasted node.name (escape hatch). 1:1 with the Tauri
/// `validateNodeName` / `detectDacType`.
pub fn validate_manual(window: &AppWindow, text: &str) {
let st = window.global::<DacWizardState>();
st.set_manual_node_name(text.into());
st.set_manual_valid(validate_node_name(text));
st.set_manual_dac_type(detect_dac_type(text).into());
}
fn validate_node_name(name: &str) -> bool {
let t = name.trim();
!t.is_empty() && (t.contains("alsa_output") || t.contains("alsa_input"))
}
fn detect_dac_type(name: &str) -> &'static str {
let l = name.to_lowercase();
if l.contains("usb-") || l.contains(".usb") {
"usb"
} else if l.contains("pci-") || l.contains(".pci") {
"pci"
} else if l.contains("bluez") || l.contains("bluetooth") {
"bluetooth"
} else if l.contains("virtual") || l.contains("null") || l.contains("dummy") {
"virtual"
} else {
"unknown"
}
}
/// "44.1 / 96 / 192 kHz" from a rate list (kHz, .1 only when non-integer).
fn format_rates(rates: &[u32]) -> String {
if rates.is_empty() {
return String::new();
}
let parts: Vec<String> = rates
.iter()
.map(|&r| {
if r % 1000 == 0 {
format!("{}", r / 1000)
} else {
format!("{:.1}", r as f64 / 1000.0)
}
})
.collect();
format!("{} kHz", parts.join(" / "))
}
#[cfg(test)]
mod slice7_tests {
use super::*;
#[test]
fn validates_node_names_like_tauri() {
assert!(validate_node_name("alsa_output.usb-Cambridge-00.analog-stereo"));
assert!(validate_node_name("alsa_input.pci-0000_00.analog-stereo"));
assert!(!validate_node_name(""));
assert!(!validate_node_name(" "));
assert!(!validate_node_name("bluez_output.AA_BB"));
}
#[test]
fn detects_dac_type() {
assert_eq!(detect_dac_type("alsa_output.usb-Cambridge-00.analog-stereo"), "usb");
assert_eq!(detect_dac_type("alsa_output.pci-0000_00_1f.3.analog-stereo"), "pci");
assert_eq!(detect_dac_type("bluez_output.AA"), "bluetooth");
assert_eq!(detect_dac_type("alsa_output.virtual-dummy"), "virtual");
assert_eq!(detect_dac_type("something.else"), "unknown");
}
#[test]
fn formats_rates_khz() {
assert_eq!(format_rates(&[44100, 96000, 192000]), "44.1 / 96 / 192 kHz");
assert_eq!(format_rates(&[]), "");
}
}
// ── Slice 9: self-service playback test (N6 read-back) ─────────────────────
/// One curated test track (owner-provided). Resolved by id-hint first, then by
/// "artist title" search if the id 404s (a pulled license) — never raw-id-only.
pub struct TestSeed {
pub depth: u32,
pub rate: f64,
pub id_hint: u64,
pub artist: &'static str,
pub title: &'static str,
}
pub const TEST_SEEDS: [TestSeed; 4] = [
TestSeed { depth: 16, rate: 44100.0, id_hint: 19301386, artist: "George Harrison", title: "My Sweet Lord" },
TestSeed { depth: 24, rate: 44100.0, id_hint: 266725027, artist: "Billie Eilish", title: "LUNCH" },
TestSeed { depth: 24, rate: 96000.0, id_hint: 126886854, artist: "Iron Maiden", title: "Stratego" },
TestSeed { depth: 24, rate: 192000.0, id_hint: 52265, artist: "Toto", title: "Africa" },
];
/// True if a resolved track matches this seed's family (rate + bit depth — the
/// two 44.1 seeds only differ by depth).
pub fn track_matches_seed(track: &qbz_models::Track, seed: &TestSeed) -> bool {
let rate_ok = track
.maximum_sampling_rate
.map(|r| (r * 1000.0 - seed.rate).abs() < 1.0 || (r - seed.rate).abs() < 1.0)
.unwrap_or(false);
let depth_ok = track.maximum_bit_depth.map(|d| d == seed.depth).unwrap_or(false);
rate_ok && depth_ok
}
/// Resolved test tracks, kept so the user can jump straight to any of them
/// (re-set the queue at the chosen index via the working play path).
static TEST_TRACKS: Mutex<Vec<qbz_models::Track>> = Mutex::new(Vec::new());
pub fn stash_test_tracks(tracks: Vec<qbz_models::Track>) {
*TEST_TRACKS.lock().unwrap() = tracks;
}
pub fn test_tracks() -> Vec<qbz_models::Track> {
TEST_TRACKS.lock().unwrap().clone()
}
/// Start the test: show the "playing" state. The read-back probes whichever
/// DAC is actively playing (scan), so no node needs to be stashed.
pub fn begin_test(window: &AppWindow) {
let st = window.global::<DacWizardState>();
st.set_test_playing(true);
st.set_test_rate_matched(false);
st.set_test_requested_label(qbz_i18n::t("Starting…").into());
st.set_test_negotiated_label("".into());
}
pub fn end_test(window: &AppWindow) {
window.global::<DacWizardState>().set_test_playing(false);
}
/// Guardrail: "Use my current queue" with an empty queue — show a hint instead
/// of starting a read-back that would just sit on "Nothing playing".
pub fn queue_empty_notice(window: &AppWindow) {
let st = window.global::<DacWizardState>();
st.set_test_playing(false);
st.set_test_rate_matched(false);
st.set_test_negotiated_label("".into());
st.set_test_requested_label(
qbz_i18n::t("Your queue is empty — add some tracks first, or press Play test.").into(),
);
}
/// Apply one poll: the rate QBZ requested vs the DAC's real negotiated rate (N6).
pub fn apply_poll(
window: &AppWindow,
requested_rate: u32,
requested_bits: u32,
negotiated: Option<qbz_audio::NegotiatedRate>,
) {
let st = window.global::<DacWizardState>();
st.set_test_requested_label(if requested_rate > 0 {
qbz_i18n::t_args(
"QBZ requesting {} · {}-bit",
&[&khz(requested_rate), &requested_bits.to_string()],
)
.into()
} else {
qbz_i18n::t("Nothing playing").into()
});
match negotiated {
Some(n) => {
// The DAC's REAL hardware params (N6): rate + ALSA container format
// (e.g. S32_LE = 24-bit in a 32-bit frame) + channels. This is the
// bit-perfect proof — exactly what the hardware is clocked at.
st.set_test_negotiated_label(
qbz_i18n::t_args(
"DAC: {} · {} · {} ch",
&[&khz(n.sample_rate), &n.format, &n.channels.to_string()],
)
.into(),
);
// Truth signal: the DAC's real clock matches what QBZ asked for.
st.set_test_rate_matched(requested_rate > 0 && n.sample_rate == requested_rate);
}
None => {
st.set_test_negotiated_label(qbz_i18n::t("Waiting for the DAC to start playing…").into());
st.set_test_rate_matched(false);
}
}
}
fn khz(hz: u32) -> String {
if hz % 1000 == 0 {
format!("{} kHz", hz / 1000)
} else {
format!("{:.1} kHz", hz as f64 / 1000.0)
}
}
// ── Slice 10: review-and-apply (per-DAC config generation) ─────────────────
/// Plain, `Send` per-DAC generated config (built on a worker thread).
pub struct DacConfigData {
name: String,
node_name: String,
pipewire_conf: String,
pulse_conf: String,
wireplumber_conf: String,
}
/// (node_name, display_name) for every checked candidate, or a valid manual one.
pub fn checked_dacs(window: &AppWindow) -> Vec<(String, String)> {
let st = window.global::<DacWizardState>();
let model = st.get_candidates();
let mut out = Vec::new();
for i in 0..model.row_count() {
if let Some(r) = model.row_data(i) {
if r.checked {
out.push((r.id.to_string(), r.description.to_string()));
}
}
}
if out.is_empty() {
let manual = st.get_manual_node_name().to_string();
if !manual.trim().is_empty() && st.get_manual_valid() {
out.push((manual.clone(), manual));
}
}
out
}
/// Re-probe rates + build the three config snippets per DAC (blocking).
pub fn gen_configs_blocking(dacs: Vec<(String, String)>) -> Vec<DacConfigData> {
dacs.into_iter()
.map(|(node_name, name)| {
let rates = qbz_audio::query_dac_capabilities(&node_name).sample_rates;
let short = short_name(&name, &node_name);
DacConfigData {
pipewire_conf: pipewire_conf(&short, &rates),
pulse_conf: pulse_conf(&short),
wireplumber_conf: wireplumber_conf(&short, &node_name, &rates, &name),
name,
node_name,
}
})
.collect()
}
/// Push generated configs + backup/restart/paths to the state.
pub fn apply_configs(window: &AppWindow, data: Vec<DacConfigData>) {
let st = window.global::<DacWizardState>();
let single = data.len() == 1;
let rows: Vec<DacConfigRow> = data
.iter()
.map(|d| DacConfigRow {
name: d.name.clone().into(),
node_name: d.node_name.clone().into(),
pipewire_conf: d.pipewire_conf.clone().into(),
pulse_conf: d.pulse_conf.clone().into(),
wireplumber_conf: d.wireplumber_conf.clone().into(),
expanded: single, // one DAC → expanded; multiple → collapsed accordions
})
.collect();
let mut paths: Vec<slint::SharedString> = Vec::new();
for d in &data {
let short = short_name(&d.name, &d.node_name);
paths.push(format!("~/.config/pipewire/pipewire.conf.d/99-qbz-dac-{short}.conf").into());
paths.push(format!("~/.config/pipewire/client.conf.d/99-qbz-bitperfect-{short}.conf").into());
paths
.push(format!("~/.config/wireplumber/wireplumber.conf.d/99-qbz-dac-{short}.conf").into());
}
st.set_dac_configs(ModelRc::new(VecModel::from(rows)));
st.set_created_paths(ModelRc::new(VecModel::from(paths)));
st.set_backup_cmd(BACKUP_CMD.into());
let init = InitSystem::ALL
.get(st.get_init_index().max(0) as usize)
.copied()
.unwrap_or(InitSystem::Unknown);
st.set_restart_cmd(restart_cmd(init).into());
}
/// Collapse/expand one DAC's generated-config accordion.
pub fn toggle_config(window: &AppWindow, index: i32) {
let model = window.global::<DacWizardState>().get_dac_configs();
if let Some(vm) = model.as_any().downcast_ref::<VecModel<DacConfigRow>>() {
if let Some(mut row) = vm.row_data(index.max(0) as usize) {
row.expanded = !row.expanded;
vm.set_row_data(index.max(0) as usize, row);
}
}
}
const BACKUP_CMD: &str = "BACKUP=~/.config/qbz/backups/pipewire-$(date +%Y%m%d-%H%M%S)\nmkdir -p \"$BACKUP\"\ncp -a ~/.config/pipewire \"$BACKUP/\" 2>/dev/null || true\ncp -a ~/.config/wireplumber \"$BACKUP/\" 2>/dev/null || true\necho \"Backup created at: $BACKUP\"";
/// A short, filename-safe DAC name: slug of the description, else the node.name.
fn short_name(name: &str, node_name: &str) -> String {
let slug = slugify(name);
if !slug.is_empty() {
return slug;
}
let nslug = slugify(node_name);
if nslug.is_empty() {
"dac".to_string()
} else {
nslug
}
}
fn slugify(s: &str) -> String {
let mut out = String::new();
let mut prev_dash = false;
for ch in s.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
prev_dash = false;
} else if !prev_dash && !out.is_empty() {
out.push('-');
prev_dash = true;
}
}
out.trim_matches('-').to_string()
}
fn rates_list(rates: &[u32]) -> String {
if rates.is_empty() {
"44100 48000 88200 96000 176400 192000".to_string()
} else {
rates
.iter()
.map(|r| r.to_string())
.collect::<Vec<_>>()
.join(" ")
}
}
fn pipewire_conf(short: &str, rates: &[u32]) -> String {
let rates = rates_list(rates);
[
"mkdir -p ~/.config/pipewire/pipewire.conf.d".to_string(),
format!("cat > ~/.config/pipewire/pipewire.conf.d/99-qbz-dac-{short}.conf << 'EOF'"),
"# QBZ DAC Setup - Sample Rate Switching".to_string(),
"context.properties = {".to_string(),
format!(" default.clock.allowed-rates = [ {rates} ]"),
"}".to_string(),
"EOF".to_string(),
]
.join("\n")
}
fn pulse_conf(short: &str) -> String {
[
"mkdir -p ~/.config/pipewire/client.conf.d".to_string(),
format!("cat > ~/.config/pipewire/client.conf.d/99-qbz-bitperfect-{short}.conf << 'EOF'"),
"# QBZ DAC Setup - Per-App Bit-Perfect".to_string(),
"stream.rules = [".to_string(),
" {".to_string(),
" matches = [".to_string(),
" { application.process.binary = \"qbz\" }".to_string(),
" { application.name = \"PipeWire ALSA [qbz]\" }".to_string(),
" ]".to_string(),
" actions = { update-props = { resample.disable = true, channelmix.disable = true } }"
.to_string(),
" }".to_string(),
"]".to_string(),
"EOF".to_string(),
]
.join("\n")
}
fn wireplumber_conf(short: &str, node_name: &str, rates: &[u32], description: &str) -> String {
let rates = rates_list(rates);
[
"mkdir -p ~/.config/wireplumber/wireplumber.conf.d".to_string(),
format!("cat > ~/.config/wireplumber/wireplumber.conf.d/99-qbz-dac-{short}.conf << 'EOF'"),
format!("# QBZ DAC Setup - {description}"),
"monitor.alsa.rules = [".to_string(),
" {".to_string(),
" matches = [".to_string(),
format!(" {{ node.name = \"{node_name}\", media.class = \"Audio/Sink\" }}"),
" ]".to_string(),
" actions = {".to_string(),
" update-props = {".to_string(),
format!(" audio.allowed-rates = [ {rates} ]"),
" resample.disable = true".to_string(),
" channelmix.disable = true".to_string(),
" }".to_string(),
" }".to_string(),
" }".to_string(),
"]".to_string(),
"EOF".to_string(),
]
.join("\n")
}
#[cfg(test)]
mod slice10_tests {
use super::*;
#[test]
fn slugifies_descriptions() {
assert_eq!(slugify("DacMagic Plus Analog Stereo"), "dacmagic-plus-analog-stereo");
assert_eq!(slugify("Built-in Audio Analog Stereo"), "built-in-audio-analog-stereo");
assert_eq!(slugify(" weird__name!! "), "weird-name");
assert_eq!(slugify(""), "");
}
#[test]
fn wireplumber_conf_pins_node_and_rates() {
let c = wireplumber_conf("dacmagic", "alsa_output.usb-x.analog-stereo", &[44100, 192000], "DacMagic");
assert!(c.contains("node.name = \"alsa_output.usb-x.analog-stereo\""));
assert!(c.contains("audio.allowed-rates = [ 44100 192000 ]"));
assert!(c.contains("99-qbz-dac-dacmagic.conf"));
assert!(c.contains("resample.disable = true"));
}
}
+1 -1
View File
@@ -26,5 +26,5 @@ chrono = { workspace = true }
url = "2"
# Percent-encoding the Plex transcode `url=`/token params (plex_thumb_url),
# matching the Tauri frontend's buildPlexArtworkUrl. Same crate used across
# qbz-slint / qbz-media-controls / qbz-integrations.
# qbz-qt / qbz-media-controls / qbz-integrations.
urlencoding = "2"
@@ -1012,84 +1012,6 @@ pub async fn download_purchase_track(
Ok(file_path)
}
/// Download-ONLY variant of the single-track primitive: identical CDN fetch +
/// `.part`→rename pipeline as `download_purchase_track`, but it does **NOT**
/// write the `downloaded_purchases` registry. Returns the final on-disk path.
///
/// **This function's original rationale was WRONG, and it is kept only because
/// `qbz-slint` calls it.** New code must not use it. Verified against the
/// reference on 2026-08-16:
///
/// The comment here used to claim that Svelte's `executeAlbumDownload` marks a
/// track `'complete'` first and then writes the registry as a best-effort step
/// that swallows failure, and therefore that an album-path registry failure
/// leaves the track `'complete'` but unregistered. That misreads the reference.
/// `executeAlbumDownload` awaits `downloadTrack`, which invokes
/// `v2_purchases_download_track` — and THAT command writes the registry itself
/// and propagates the error with `?` (`legacy_compat.rs:3019-3021`). So a
/// registry failure throws out of `downloadTrack`, is caught by the loop's
/// `catch`, and the track is marked **`'failed'`**, exactly as on the
/// single-track path. The `markTrackDownloaded(...).catch(() => {})` that follows
/// (`purchaseDownloadStore.ts:147`) is a SECOND, backfill-only write whose sole
/// effect is filling the `album_id` column; its swallowed failure costs that
/// column and nothing else.
///
/// Consequence: this "write the file, register separately" split has no basis in
/// the reference, and the Slint controller's use of it is the one silent
/// divergence in the purchases port — it leaves files on disk with no registry
/// row while reporting success. The Qt controller uses
/// `download_purchase_track` with `Some(album_id)` instead, which registers
/// inside the same call and propagates.
///
/// Addendum B.2 (extension from RESPONSE, registry/qualityDir from REQUESTED),
/// B.3 (silent overwrite), and B.5 (restrictions ignored) hold identically to
/// `download_purchase_track`.
pub async fn download_purchase_track_file_only(
client: &QobuzClient,
track_id: u64,
format_id: u32,
destination: &str,
quality_dir: &str,
) -> Result<String, String> {
// UNSIGNED on the purchase path (contract §11-6): the vendor's own desktop
// client sends `/track/get` without a signature, and so did the reference.
// The entitlement proof is the signature on `getFileUrl` below, which is
// untouched.
let track = client
.get_track_for_purchase(track_id)
.await
.map_err(|e| format!("Failed to fetch track {}: {}", track_id, e))?;
let stream = client
.get_track_file_url_by_format(track_id, format_id)
.await
.map_err(|e| format!("Failed to get download URL for track {}: {}", track_id, e))?;
let data = QobuzClient::download_audio(&stream.url).await?;
let artist_name = track
.performer
.as_ref()
.map(|artist| artist.name.clone())
.unwrap_or_else(|| "Unknown Artist".to_string());
let album_title = track
.album
.as_ref()
.map(|album| album.title.clone())
.unwrap_or_else(|| "Singles".to_string());
write_track_file(
&data,
&artist_name,
&album_title,
quality_dir,
track.track_number,
&track.title,
stream.format_id,
&stream.mime_type,
destination,
)
}
#[cfg(test)]
mod tests {
use super::*;
+41 -70
View File
@@ -1,21 +1,13 @@
[package]
name = "qbz-qt"
description = "Qt/QML frontend POC for QBZ (cxx-qt bridge over the UI-agnostic backend)"
description = "Qt/QML frontend for QBZ (cxx-qt bridge over the UI-agnostic backend)"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
# The SHIPPED name is `qbz` (owner, 2026-08-04). The package stays
# `qbz-qt`, so `cargo build -p qbz-qt` is unchanged; only the artifact
# moves to target/<profile>/qbz.
#
# ⚠ `crates/qbz` (the Slint binary) declares the SAME bin name and
# shares this workspace's target dir, so both write
# target/<profile>/qbz and the last build wins. That is survivable only
# because the Slint binary is never built in this worktree (the qbz-ui
# 20-30 GB wall). If it ever is, whoever built last owns the file — and
# a smoke against "the wrong qbz" is silent.
# The shipped executable remains `qbz`; the package name stays `qbz-qt` so
# package-scoped Cargo commands remain unambiguous.
name = "qbz"
path = "src/main.rs"
@@ -41,12 +33,11 @@ urlencoding = "2"
open = "5"
dirs = "5"
# Share links (src/share_qt.rs, the port of crates/qbz/src/share.rs:32-70).
# Share links (src/share_qt.rs).
# Rust owns the clipboard on this side because the toast does: cxx-qt-lib 0.7
# ships no QClipboard binding, and QML cannot publish QbzShell.toastJson, so
# splitting the copy and its confirmation across the boundary would buy
# nothing. Same pin as the reference (crates/qbz/Cargo.toml:68), comment
# included:
# nothing.
#
# wayland-data-control: Wayland-native clipboard via the data-control
# protocols (GNOME/KDE/sway/niri/Hyprland), falling back to X11 when Wayland
@@ -57,9 +48,7 @@ arboard = { version = "3", features = ["wayland-data-control"] }
qbz-core = { path = "../qbz-core" }
qbz-app = { path = "../qbz-app" }
qbz-audio = { path = "../qbz-audio" }
# The HiFi Wizard's frontend-agnostic core. NOT `qbz-dac-wizard` — that one is
# the Slint adapter and pulls in qbz-ui + slint, the ~1.6 M-line generated
# module this whole track exists to stay out of.
# The HiFi Wizard's frontend-agnostic core; Qt owns the presentation adapter.
qbz-dac-wizard-core = { path = "../qbz-dac-wizard-core" }
# Vulkan enumeration for the Preferred-GPU row — real adapter NAMES, MODELS and
# types, in the exact order `QT_VK_PHYSICAL_DEVICE_INDEX` counts them (both are
@@ -67,10 +56,12 @@ qbz-dac-wizard-core = { path = "../qbz-dac-wizard-core" }
# does with `wgpu::Instance::enumerate_adapters`.
#
# `ash` is a thin FFI binding that dlopens libvulkan at RUNTIME: no Vulkan SDK
# at build time, and a box without libvulkan simply enumerates nothing. It was
# already in the workspace lock (wgpu pulls it), so it costs no new compile.
# at build time, and a box without libvulkan simply enumerates nothing.
ash = "0.38"
qbz-qobuz = { path = "../qbz-qobuz" }
# Cross-provider "Open Music Link" resolver. The crate is frontend-agnostic;
# src/link_resolver_qt.rs supplies the live Qobuz search + Qt navigation seams.
qbz-music-link = { path = "../qbz-music-link" }
qbz-models = { path = "../qbz-models" }
qbz-cache = { path = "../qbz-cache" }
qbz-offline-cache = { path = "../qbz-offline-cache" }
@@ -79,9 +70,8 @@ qbz-library = { path = "../qbz-library" }
# its background bootstrap here; no Qt type crosses into the catalog crate.
qbz-local-catalog = { path = "../qbz-local-catalog" }
# MyQBZ: Mixtapes & Collections backend (schema + migrations, repo, shuffle,
# enqueue). Headless and frontend-agnostic (ADR-006) — the same crate the
# Slint app uses (crates/qbz/Cargo.toml:96), so the two frontends share
# library.db's mixtape tables rather than each growing their own.
# enqueue). Headless and frontend-agnostic (ADR-006), with the persistent
# library.db schema owned outside the Qt presentation layer.
qbz-mixtape = { path = "../qbz-mixtape" }
qbz-source = { path = "../qbz-source" }
# The media-server protocol crates. `qbz-qt` needs them directly for the SYNC
@@ -96,10 +86,8 @@ qbz-cast = { path = "../qbz-cast" }
qbz-plex = { path = "../qbz-plex" }
qbz-lyrics = { path = "../qbz-lyrics" }
# Playlist Importer: the provider scrapers (Spotify / Apple Music / Tidal /
# Deezer), the Qobuz matcher and the playlist creation. Headless and
# frontend-agnostic (ADR-006) — the same crate the Slint app uses
# (crates/qbz/Cargo.toml), so the two frontends share one importer instead of
# each growing its own. src/playlist_import_qt.rs drives it.
# Deezer), the Qobuz matcher and playlist creation. The implementation stays
# frontend-agnostic (ADR-006); src/playlist_import_qt.rs drives it.
qbz-playlist-import = { path = "../qbz-playlist-import" }
qbz-text-utils = { path = "../qbz-text-utils" }
qbz-credentials = { path = "../qbz-credentials" }
@@ -113,11 +101,8 @@ qbz-external-reco = { path = "../qbz-external-reco" }
# now-playing widget and the hardware media keys see QBZ), SMTC/MediaRemote
# elsewhere. Driven by src/media_controls_qt.rs.
#
# Plain, NOT target-gated, and with no features — the same line both other
# consumers use (crates/qbz/Cargo.toml:80, crates/qbzd/Cargo.toml:22). The
# crate carries its own cfg arms internally (qbz-media-controls/src/lib.rs:36-53)
# and is already a workspace member (crates/Cargo.toml:32), so this adds no
# lock entry.
# Plain, NOT target-gated, and with no features. The crate carries its own cfg
# arms internally and is already a workspace member.
#
# Its Linux backend rides mpris-server -> zbus 4 with its own current-thread
# tokio runtime, which is a DIFFERENT major from the zbus 5 that ksni / ashpd /
@@ -125,21 +110,19 @@ qbz-external-reco = { path = "../qbz-external-reco" }
# process; the rule in the ksni comment below applies here too — never enable a
# `tokio` feature on any D-Bus dependency.
qbz-media-controls = { path = "../qbz-media-controls" }
# Qobuz Connect (2026-08-01 Qt-port contract): the shared session/renderer
# orchestration and the WS transport, consumed VERBATIM — backend untouchable,
# same edges as the Slint crate (crates/qbz/Cargo.toml:87-88). Driven by
# src/qconnect_transport_qt.rs + src/qconnect_engine_qt.rs (+ the B3/B4 facade
# and sink).
# Qobuz Connect: shared session/renderer orchestration and WS transport,
# consumed without a frontend fork. Driven by src/qconnect_transport_qt.rs,
# src/qconnect_engine_qt.rs and the facade/event sink.
qconnect-app = { path = "../qconnect-app" }
qconnect-transport-ws = { path = "../qconnect-transport-ws" }
# Type-level edge only, sanctioned by contract §1.1: the QConnect renderer
# engine trait names `qbz_player::PlaybackState`, and the engine's feeder
# reaches the protected player seams THROUGH `runtime.core().player()` — the
# same edge the Slint crate has (crates/qbz/Cargo.toml:85). No
# qbz-player/qbz-audio internals are touched (TRACK-RULES §7).
# shared engine trait. No qbz-player/qbz-audio internals are touched
# (TRACK-RULES §7).
qbz-player = { path = "../qbz-player" }
# QConnect: uuid v4 for controller action ids (the B3 facade / B4 sink — same
# as crates/qbz/Cargo.toml:90); futures-util for the engine's HTTP feeder
# QConnect: uuid v4 for controller action ids (the B3 facade / B4 sink);
# futures-util for the engine's HTTP feeder
# (StreamExt over the response byte stream).
uuid = { version = "1", features = ["v4"] }
futures-util = { workspace = true }
@@ -152,32 +135,25 @@ image = { version = "0.25", default-features = false, features = ["jpeg", "png"]
# cover upload (src/myqbz_cover_qt.rs) and the My-QBZ branding icon picker
# (src/myqbz_prefs_qt.rs).
#
# The feature set is NOT negotiable and is copied verbatim from the reference
# (crates/qbz/Cargo.toml:72): "async-std" is rfd's name for the
# async-io/ashpd backend. NEVER enable rfd's "tokio" feature — zbus 5's
# tokio feature unifies graph-wide and breaks the accesskit/atspi/ksni
# threads. `default-features = false` keeps the GTK3 backend out; the
# xdg-portal path is the Wayland-correct one and needs no extra system libs.
# "async-std" is rfd's name for the async-io/ashpd backend. NEVER enable
# rfd's "tokio" feature: it would switch the shared zbus 5 executor choice.
# `default-features = false` keeps the GTK3 backend out; the xdg-portal path
# is the Wayland-correct one and needs no extra system libs.
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"] }
# Linux system tray via StatusNotifierItem (src/tray_linux.rs). Linux only —
# owner ruling K2: `crates/qbz/src/tray/macos.rs` exists because Slint's winit
# backend already bundles `muda`; under Qt that premise is gone, so macOS would
# be a NEW implementation, not a port. Windows has no tray in either tree.
#
# The comment below is copied verbatim from the reference
# (crates/qbz/Cargo.toml:139-143) because the feature set is not negotiable:
# Linux system tray via StatusNotifierItem (src/tray_linux.rs). macOS uses the
# native implementation in src/tray_macos.rs; Windows currently has no tray.
#
# default-features = false: ksni's default is "tokio", which flips the shared
# zbus 5 executor to tokio graph-wide (breaks accesskit/atspi plain-thread
# connections — see the rfd comment above). We only use the blocking API on
# zbus 5 executor to tokio graph-wide. We only use the blocking API on
# worker threads; async-io self-drives the connection from any context.
#
# ksni 0.3.4 is already in the workspace lock (pulled by crates/qbz), so this
# adds no new lock entry. Feature unification is per-crate-graph, not
# per-target: ANY dependency that turns on zbus/tokio poisons the whole binary.
# Feature unification is per-crate-graph, not per-target: any dependency that
# turns on zbus/tokio changes the whole binary.
[target.'cfg(target_os = "linux")'.dependencies]
ksni = { version = "0.3", default-features = false, features = ["blocking", "async-io"] }
# Session-bus single-instance guard. Keep the same async-io executor as ksni;
# enabling zbus/tokio would poison the graph-wide executor choice.
zbus = { version = "5", default-features = false, features = ["async-io", "blocking-api"] }
[build-dependencies]
cxx-qt-build = "0.7"
@@ -193,11 +169,9 @@ cc = "1"
# closed to menu bar" setting (Spotify behaviour — close puts the app in the
# menu bar and drops the Dock icon; minimize still goes to the Dock).
#
# objc2 0.5 / objc2-app-kit 0.2 are pinned to the SAME generation the Slint
# crate uses (crates/qbz/Cargo.toml:166-172). That matters even though this
# crate has no winit: mixing objc2 generations in one binary registers
# duplicate objc classes, which is the failure the reference documents at
# length. Only the two items `set_mac_dock_hidden` names are enabled.
# objc2 0.5 / objc2-app-kit 0.2 stay on one internally consistent generation;
# mixing objc2 generations in one binary can register duplicate Objective-C
# classes. Only the items used by the native tray/Dock integration are enabled.
#
# ⚠ NOT COMPILED OR TESTED HERE. This tree builds on Linux; the macOS arm is
# behind cfg, so `cargo build` cannot see it. First real check is a macOS
@@ -205,15 +179,12 @@ cc = "1"
[target.'cfg(target_os = "macos")'.dependencies]
# DIRECT objc2, added 2026-08-05 for the menu-bar tray: `declare_class!`,
# `msg_send_id!`, `sel!`, `ClassType`, `DeclaredClass`, `mutability` and
# `rc::Retained` all live at the objc2 root. Pinned to 0.5, the SAME
# generation objc2-app-kit 0.2 targets — mixing generations in one binary
# registers duplicate objc classes, which is the failure the reference
# documents at length (qbz/Cargo.toml:150-156).
# `rc::Retained` all live at the objc2 root. Pinned to the generation targeted
# by objc2-app-kit 0.2.
objc2 = "0.5"
# `MainThreadMarker` — in THIS generation it is exported by objc2-foundation,
# not by the objc2 root (objc2-foundation-0.2.2/src/lib.rs:152, ungated). The
# reference imports it from the same place. There is no direct `objc2`
# dependency because nothing here names a type from it.
# native integration imports it from here.
objc2-foundation = { version = "0.2", default-features = false, features = [
"std",
# NSThread gates `MainThreadMarker::new()` itself
-11
View File
@@ -1,11 +0,0 @@
[package]
name = "qbz-slint-common"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Shared Slint-coupled frontend helpers for the qbz app (toast; artwork primitives later)."
[dependencies]
qbz-ui = { path = "../qbz-ui" }
slint = { version = "=1.16.1", default-features = false, features = ["std", "compat-1-2"] }
-3
View File
@@ -1,3 +0,0 @@
//! Shared Slint-coupled helpers extracted from the qbz binary. Depends on
//! qbz-ui for the generated types (AppWindow, the globals, structs).
pub mod toast;
-115
View File
@@ -1,115 +0,0 @@
//! In-app toast controller — the Rust side of the Slint port of Tauri's
//! `toastStore`. One toast at a time (a new one replaces the current),
//! auto-hidden after a per-kind delay. `buffering` is persistent.
//!
//! All functions touch Slint globals and the shared hide `Timer`, so they
//! must run on the event-loop thread. From background threads / async
//! tasks use [`show_weak`] / [`error_weak`], which hop onto the event loop
//! for you (the established `upgrade_in_event_loop` pattern).
//!
//! This is the shared toast API surface; not every helper has a call site
//! yet (error surfacing is being wired in incrementally), so the unused
//! ones are intentionally allowed rather than removed.
#![allow(dead_code)]
use std::time::Duration;
use slint::{ComponentHandle, Timer, TimerMode, Weak};
use qbz_ui::{AppWindow, ToastKind, ToastState};
thread_local! {
// Single reusable auto-hide timer. Reused (stop + start) so a fresh
// toast cancels the previous toast's pending hide instead of letting
// it fire and dismiss the newer one early — mirrors the
// `clearTimeout` in Tauri's toastStore.
static HIDE_TIMER: Timer = Timer::default();
}
/// Auto-hide delay per kind. `None` = persistent (buffering). Matches
/// Tauri's `defaultDurations`.
fn auto_hide(kind: ToastKind) -> Option<Duration> {
let ms = match kind {
ToastKind::Success => 3000,
ToastKind::Info => 3000,
ToastKind::Warning => 4000,
ToastKind::Error => 5000,
ToastKind::Buffering => return None,
};
Some(Duration::from_millis(ms))
}
/// Show a toast. Must be called on the event-loop thread.
pub fn show(window: &AppWindow, message: impl Into<String>, kind: ToastKind) {
let message: String = message.into();
let state = window.global::<ToastState>();
// Non-error toasts honor the global toggle; errors always show
// (parity with Tauri's `showToast`).
if !state.get_enabled() && kind != ToastKind::Error {
return;
}
state.set_message(message.as_str().into());
state.set_kind(kind);
state.set_persistent(kind == ToastKind::Buffering);
state.set_visible(true);
HIDE_TIMER.with(|t| t.stop());
if let Some(delay) = auto_hide(kind) {
let weak = window.as_weak();
HIDE_TIMER.with(|t| {
t.start(TimerMode::SingleShot, delay, move || {
if let Some(w) = weak.upgrade() {
w.global::<ToastState>().set_visible(false);
}
});
});
}
}
/// Hide the current toast immediately (also stops the pending auto-hide).
pub fn hide(window: &AppWindow) {
HIDE_TIMER.with(|t| t.stop());
window.global::<ToastState>().set_visible(false);
}
/// Show a toast from any thread (async / background error paths). Hops
/// onto the event loop, so the message must be owned/`Send`.
pub fn show_weak(weak: &Weak<AppWindow>, message: impl Into<String>, kind: ToastKind) {
let message: String = message.into();
let _ = weak.upgrade_in_event_loop(move |w| show(&w, message, kind));
}
// ---- Convenience wrappers -------------------------------------------------
pub fn error(window: &AppWindow, message: impl Into<String>) {
show(window, message, ToastKind::Error);
}
pub fn success(window: &AppWindow, message: impl Into<String>) {
show(window, message, ToastKind::Success);
}
pub fn info(window: &AppWindow, message: impl Into<String>) {
show(window, message, ToastKind::Info);
}
pub fn warning(window: &AppWindow, message: impl Into<String>) {
show(window, message, ToastKind::Warning);
}
/// Error toast from any thread.
pub fn error_weak(weak: &Weak<AppWindow>, message: impl Into<String>) {
show_weak(weak, message, ToastKind::Error);
}
/// Success toast from any thread.
pub fn success_weak(weak: &Weak<AppWindow>, message: impl Into<String>) {
show_weak(weak, message, ToastKind::Success);
}
/// Info toast from any thread.
pub fn info_weak(weak: &Weak<AppWindow>, message: impl Into<String>) {
show_weak(weak, message, ToastKind::Info);
}
+2 -2
View File
@@ -26,8 +26,8 @@ qbz-media-cache = { path = "../qbz-media-cache" }
# — every statement lives in `qbz-media-cache` or `qbz-library`.
rusqlite = { version = "0.31", features = ["bundled"] }
# `ItemResolver` — this crate IMPLEMENTS it (mixtape_adapter.rs). qbz-mixtape is
# NOT modified and NOT absorbed; the dependency is one-directional so the `qbz`
# (Slint) crate keeps compiling untouched.
# NOT modified and NOT absorbed; the dependency remains one-directional so the
# Qt application can consume the adapter without coupling qbz-mixtape to it.
qbz-mixtape = { path = "../qbz-mixtape" }
async-trait = { workspace = true }
-22
View File
@@ -1,22 +0,0 @@
[package]
name = "qbz-ui"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Slint UI compilation unit for the qbz app — owns the .slint tree + bundled translations and exports the generated Rust types (AppWindow, MiniPlayerWindow, all globals + structs)."
[lib]
name = "qbz_ui"
path = "src/lib.rs"
[dependencies]
slint = { version = "=1.16.1", default-features = false, features = [
"std",
"compat-1-2",
"renderer-software",
"accessibility",
] }
[build-dependencies]
slint-build = "1.16"
-22
View File
@@ -1,22 +0,0 @@
//! Compiles the Slint UI tree with bundled translations. `ui/app.slint` is the
//! single entry point. Translations are bundled (pure-Rust, no C dep) from
//! `<lang>/LC_MESSAGES/qbz-ui.po`; msgid = English source, no context.
//!
//! THE CATALOGUES MOVED OUT of this crate and are now BORROWED. They belong to
//! `qbz-i18n`, which embeds them with `include_str!` and is the crate every
//! frontend uses — it declared itself "frontend-agnostic" while reaching into
//! this directory for its data, so deleting this crate (planned before the Qt
//! release) would have broken the Qt app over a path nobody read as a
//! dependency. This borrow is the temporary half: it dies with the crate.
fn main() {
// SLINT_SCALE_FACTOR is a RUNTIME preference in this app (main.rs sets it
// from the persisted interface-size preset). If it leaks into the BUILD
// environment, the slint compiler const-propagates the factor and the
// runtime override becomes a permanent no-op — strip it unconditionally.
std::env::remove_var("SLINT_SCALE_FACTOR");
let config = slint_build::CompilerConfiguration::new()
.with_bundled_translations("../qbz-i18n/translations")
.with_default_translation_context(slint_build::DefaultTranslationContext::None);
slint_build::compile_with_config("ui/app.slint", config)
.expect("Slint UI failed to compile");
}
-8
View File
@@ -1,8 +0,0 @@
//! Slint UI compilation unit. The whole `.slint` tree (entry `ui/app.slint`)
//! is compiled by `build.rs` with bundled translations; `include_modules!`
//! brings the generated Rust types (AppWindow, MiniPlayerWindow, the globals,
//! the structs/enums) into this crate's root. They are `pub`, so the
//! `qbz-slint` binary (and future feature crates) use them via
//! `qbz_slint_ui::AppWindow` etc. — Slint generated TYPES cross crate
//! boundaries fine (only `.slint` file imports do not).
slint::include_modules!();
@@ -1,309 +0,0 @@
// Album Booklet modal — in-app PDF reader for the digital booklet that
// ships with some Qobuz albums. Opened from the AlbumPageView header
// (book-open) button via media-action("album", id, "booklet"); the Rust
// controller (crate qbz-booklet) downloads the PDF, renders the current
// page to a PNG, and feeds BookletState. Mirrors AlbumCreditsModal's
// scrim + centered card pattern (ADR-009/010). The page area is a
// Flickable sized to the rendered pixel dimensions: centered when the
// page is smaller than the viewport, scrolling when larger.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { BookletState, BookletActions } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
// One toolbar button — a small square with a centered icon, hover fill,
// disable support (dims + gates the click).
component ToolbarButton inherits Rectangle {
in property <image> icon;
in property <bool> enabled: true;
callback clicked();
width: 32px;
height: 32px;
border-radius: Radius.sm;
opacity: root.enabled ? 1.0 : 0.4;
background: (ta.has-hover && root.enabled) ? Theme.surface-hover : transparent;
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: ta.has-hover && root.enabled ? Theme.text-primary : Theme.text-secondary;
}
ta := TouchArea {
mouse-cursor: root.enabled ? pointer : default;
clicked => {
if (root.enabled) {
root.clicked();
}
}
}
}
export component AlbumBookletModal inherits Rectangle {
visible: BookletState.open;
if BookletState.open: Rectangle {
background: #000000bf;
// Scrim — a click outside the card dismisses.
TouchArea {
mouse-cursor: default;
clicked => {
BookletActions.close();
}
}
Rectangle {
width: Math.min(root.width - 40px, 980px);
height: root.height - 80px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
border-radius: Radius.md;
background: Theme.surface-card;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 32px;
drop-shadow-color: Theme.card-shadow;
clip: true;
// Swallow clicks so they don't reach the scrim.
TouchArea { }
fs := FocusScope {
Timer {
interval: 30ms;
running: true;
triggered => {
self.running = false;
fs.focus();
}
}
key-pressed(event) => {
if (event.text == Key.Escape) {
BookletActions.close();
return accept;
}
return reject;
}
VerticalLayout {
spacing: 0px;
// ---- Header: title + close ----------------------------
HorizontalLayout {
padding-left: 24px;
padding-right: 24px;
padding-top: 16px;
padding-bottom: 16px;
spacing: 16px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: BookletState.title;
color: Theme.text-primary;
font-size: 16px;
font-weight: Typography.semibold;
overflow: elide;
}
}
close-x := TouchArea {
width: 32px;
height: 32px;
mouse-cursor: pointer;
clicked => {
BookletActions.close();
}
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 18px;
height: 18px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: close-x.has-hover ? Theme.text-primary : Theme.text-muted;
}
}
}
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
// ---- Toolbar: paging, zoom, fit, rotate, download -----
HorizontalLayout {
padding-left: 24px;
padding-right: 24px;
padding-top: 10px;
padding-bottom: 10px;
spacing: 6px;
alignment: start;
VerticalLayout {
alignment: center;
ToolbarButton {
icon: @image-url("../assets/icons/chevron-left.svg");
enabled: BookletState.current-page > 1;
clicked => {
BookletActions.prev-page();
}
}
}
VerticalLayout {
alignment: center;
Text {
text: BookletState.current-page + " / " + BookletState.num-pages;
color: Theme.text-secondary;
font-size: 13px;
vertical-alignment: center;
horizontal-alignment: center;
min-width: 64px;
}
}
VerticalLayout {
alignment: center;
ToolbarButton {
icon: @image-url("../assets/icons/chevron-right.svg");
enabled: BookletState.current-page < BookletState.num-pages;
clicked => {
BookletActions.next-page();
}
}
}
// Spacer between paging and zoom controls.
Rectangle { width: 16px; }
VerticalLayout {
alignment: center;
ToolbarButton {
icon: @image-url("../assets/icons/minus.svg");
clicked => {
BookletActions.zoom-out();
}
}
}
VerticalLayout {
alignment: center;
ToolbarButton {
icon: @image-url("../assets/icons/plus.svg");
clicked => {
BookletActions.zoom-in();
}
}
}
VerticalLayout {
alignment: center;
ToolbarButton {
icon: @image-url("../assets/icons/maximize-2.svg");
clicked => {
BookletActions.fit-width();
}
}
}
VerticalLayout {
alignment: center;
ToolbarButton {
icon: @image-url("../assets/icons/rotate-ccw.svg");
clicked => {
BookletActions.rotate();
}
}
}
Rectangle { horizontal-stretch: 1; }
VerticalLayout {
alignment: center;
ToolbarButton {
icon: @image-url("../assets/icons/cloud-download.svg");
clicked => {
BookletActions.download();
}
}
}
}
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
// ---- Page area ----------------------------------------
// A neutral viewport holding a Flickable sized to the
// rendered page pixels. When the page is smaller than the
// viewport it centers; when larger, the Flickable scrolls.
Rectangle {
vertical-stretch: 1;
background: Theme.surface-main;
clip: true;
page-flick := Flickable {
viewport-width: Math.max(self.width, BookletState.page-pixel-width * 1px);
viewport-height: Math.max(self.height, BookletState.page-pixel-height * 1px);
Image {
source: BookletState.page-image;
width: BookletState.page-pixel-width * 1px;
height: BookletState.page-pixel-height * 1px;
x: Math.max(0px, (parent.width - self.width) / 2);
y: Math.max(0px, (parent.height - self.height) / 2);
}
}
// Loading overlay — spinner + label while the PDF
// downloads or a page renders.
if BookletState.loading: Rectangle {
background: Theme.surface-main;
TouchArea { }
VerticalLayout {
alignment: center;
spacing: 14px;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
Text {
text: @tr("Loading…");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
}
// Error overlay.
if BookletState.error: Rectangle {
background: Theme.surface-main;
TouchArea { }
VerticalLayout {
alignment: center;
spacing: 8px;
Text {
text: @tr("Failed to load booklet");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
if BookletState.error-text != "": Text {
text: BookletState.error-text;
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: center;
wrap: word-wrap;
}
}
}
}
}
}
}
}
}
@@ -1,476 +0,0 @@
// Album Info (Credits / Review) modal — 1:1 port of Tauri's
// AlbumCreditsModal.svelte. Opened from the AlbumPageView header (i) button via
// media-action("album", id, "info"). The Rust controller (crate::info_modals)
// fetches the album, parses per-track performers, and populates AlbumInfoState.
// Rectangle-scrim + centered fixed-height card, mounted in AppShell after the
// other modals (ADR-009/010). Left column = album info (fixed). Right column =
// Credits (expandable track list) / Review (editorial text), independent scroll.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { AlbumInfoState, AlbumInfoActions, AlbumCreditTrack } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
// One track row in the Credits tab: number / play-on-hover, title + artist,
// expand chevron; an expanded panel with per-performer credit lines + the
// copyright. Local `expanded` persists while the modal stays open.
component AlbumTrackRow inherits VerticalLayout {
in property <AlbumCreditTrack> track;
in property <bool> is-last;
callback play(string /* track-id */);
callback musician(string /* name */, string /* primary-role */);
property <bool> expanded: false;
// ---- Header row -------------------------------------------------------
Rectangle {
height: 46px;
// Declared FIRST so the per-cell play TouchArea sits on top (footgun:
// a later full-row TouchArea would eat the cell clicks).
rowha := TouchArea {
mouse-cursor: root.track.has-credits ? pointer : default;
clicked => {
if (root.track.has-credits) {
root.expanded = !root.expanded;
}
}
}
HorizontalLayout {
spacing: 12px;
// Whole-row hover dim, 1:1 with Tauri .track-header:hover.
opacity: (rowha.has-hover && root.track.has-credits) ? 0.8 : 1.0;
// Number / play-on-hover cell.
Rectangle {
width: 28px;
if !(rowha.has-hover || pcell.has-hover): Text {
text: root.track.number;
color: Theme.text-muted;
font-size: 14px;
horizontal-alignment: center;
vertical-alignment: center;
}
pcell := TouchArea {
mouse-cursor: pointer;
clicked => {
root.play(root.track.id);
}
if rowha.has-hover || pcell.has-hover: Rectangle {
width: 28px;
height: 28px;
border-radius: 14px;
background: Theme.accent;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
QbzIcon {
source: @image-url("../assets/icons/play-fill.svg");
width: 14px;
height: 14px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: Theme.accent-text;
}
}
}
}
// Title + artist.
VerticalLayout {
horizontal-stretch: 1;
alignment: center;
spacing: 2px;
Text {
text: root.track.title;
color: Theme.text-primary;
font-size: 14px;
font-weight: Typography.medium;
overflow: elide;
}
Text {
text: root.track.artist;
color: Theme.text-muted;
font-size: 12px;
overflow: elide;
}
}
// Expand chevron (only when there are credits).
if root.track.has-credits: VerticalLayout {
alignment: center;
QbzIcon {
source: root.expanded
? @image-url("../assets/icons/chevron-up.svg")
: @image-url("../assets/icons/chevron-down.svg");
width: 18px;
height: 18px;
tint: root.expanded ? Theme.text-primary : Theme.text-muted;
}
}
}
}
// ---- Expanded credits -------------------------------------------------
if root.expanded && root.track.has-credits: VerticalLayout {
padding-left: 40px;
padding-bottom: 12px;
spacing: 0px;
for perf in root.track.performers: HorizontalLayout {
padding-top: 4px;
padding-bottom: 4px;
alignment: start;
pta := TouchArea {
width: pname.preferred-width;
height: pname.preferred-height;
mouse-cursor: pointer;
clicked => {
root.musician(perf.name, perf.primary-role);
}
pname := Text {
text: perf.name;
color: pta.has-hover ? Theme.accent : Theme.text-primary;
font-size: 13px;
font-weight: Typography.medium;
}
}
Text {
text: perf.roles;
color: Theme.text-muted;
font-size: 13px;
// Fill the row's remaining width and wrap the role list instead
// of letting it run off the right edge of the modal.
horizontal-stretch: 1;
wrap: word-wrap;
}
}
if root.track.copyright != "": Rectangle { height: 8px; }
if root.track.copyright != "": Rectangle {
height: 1px;
background: Theme.border-subtle;
}
if root.track.copyright != "": Rectangle { height: 8px; }
if root.track.copyright != "": Text {
text: root.track.copyright;
color: Theme.text-muted;
font-size: 12px;
wrap: word-wrap;
}
}
// ---- Divider ----------------------------------------------------------
if !root.is-last: Rectangle {
height: 1px;
background: Theme.border-subtle;
}
}
export component AlbumCreditsModal inherits Rectangle {
visible: AlbumInfoState.open;
if AlbumInfoState.open: Rectangle {
background: #000000bf;
TouchArea {
mouse-cursor: default;
clicked => {
AlbumInfoActions.close();
}
}
Rectangle {
width: Math.min(root.width - 40px, 850px);
// Content-sized with a decent cap: as tall as the header + the
// taller of the two columns (left meta vs tabs + the full credit
// list), but never beyond the content area (40px margin each side).
// When the list exceeds the cap, the right column's Flickable
// scrolls. All inputs are width-driven (card width is fixed by
// root.width) so the height binding is acyclic. The +42px is the
// tab strip when a review exists.
height: Math.min(
root.height - 80px,
hdr.preferred-height
+ Math.max(
leftcol.preferred-height,
(AlbumInfoState.has-review ? 42px : 0px) + content.preferred-height)
+ 24px);
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
border-radius: Radius.md;
background: Theme.surface-card;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 32px;
drop-shadow-color: Theme.card-shadow;
TouchArea { }
fs := FocusScope {
Timer {
interval: 30ms;
running: true;
triggered => {
self.running = false;
fs.focus();
}
}
key-pressed(event) => {
if (event.text == Key.Escape) {
AlbumInfoActions.close();
return accept;
}
return reject;
}
VerticalLayout {
// ---- Header -------------------------------------------
hdr := HorizontalLayout {
padding-left: 24px;
padding-right: 24px;
padding-top: 16px;
padding-bottom: 16px;
spacing: 16px;
VerticalLayout {
horizontal-stretch: 1;
spacing: 4px;
Text {
text: AlbumInfoState.title;
color: Theme.text-primary;
font-size: 16px;
font-weight: Typography.semibold;
overflow: elide;
}
Text {
text: AlbumInfoState.artist;
color: Theme.text-muted;
font-size: Typography.legal;
overflow: elide;
}
}
close-x := TouchArea {
width: 32px;
height: 32px;
mouse-cursor: pointer;
clicked => {
AlbumInfoActions.close();
}
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 18px;
height: 18px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: close-x.has-hover ? Theme.text-primary : Theme.text-muted;
}
}
}
// ---- Body (two columns) — always rendered so the card
// height can read the real content height (loading/error
// are overlays below, since Slint can't reference elements
// nested in an `if` from the height binding).
HorizontalLayout {
vertical-stretch: 1;
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
spacing: 24px;
// LEFT — album info (fixed width). alignment:start so
// the meta stacks tightly under the cover instead of
// the layout distributing the tall body's slack between
// the rows.
leftcol := VerticalLayout {
width: 260px;
spacing: 0px;
alignment: start;
Rectangle {
width: 200px;
height: 200px;
border-radius: 8px;
clip: true;
Image {
width: 200px;
height: 200px;
source: AlbumInfoState.artwork;
image-fit: cover;
}
}
Rectangle { height: 16px; }
// Released by <label> on <date>.
if AlbumInfoState.label != "": VerticalLayout {
spacing: 0px;
Text {
text: @tr("Released by");
color: Theme.text-muted;
font-size: 13px;
}
lbl := TouchArea {
width: lbltext.preferred-width;
height: lbltext.preferred-height;
mouse-cursor: AlbumInfoState.label-id != "" ? pointer : default;
clicked => {
if (AlbumInfoState.label-id != "") {
AlbumInfoActions.open-label(AlbumInfoState.label-id);
}
}
lbltext := Text {
text: AlbumInfoState.label;
color: (lbl.has-hover && AlbumInfoState.label-id != "")
? Theme.accent : Theme.text-primary;
font-size: 13px;
font-weight: Typography.semibold;
}
}
if AlbumInfoState.release-date != "": Text {
text: @tr("on") + " " + AlbumInfoState.release-date;
color: Theme.text-muted;
font-size: 13px;
}
}
if AlbumInfoState.label != "": Rectangle { height: 8px; }
if AlbumInfoState.meta-line != "": Text {
text: AlbumInfoState.meta-line;
color: Theme.text-muted;
font-size: 13px;
wrap: word-wrap;
}
if AlbumInfoState.quality != "": Rectangle { height: 12px; }
if AlbumInfoState.quality != "": Text {
text: AlbumInfoState.quality;
color: Theme.text-secondary;
font-size: 13px;
}
}
// RIGHT — tabs + scrollable content.
VerticalLayout {
horizontal-stretch: 1;
spacing: 0px;
// Tab switcher (only when a review exists).
if AlbumInfoState.has-review: VerticalLayout {
spacing: 0px;
HorizontalLayout {
spacing: 16px;
alignment: start;
credits-tab := TouchArea {
width: ct.preferred-width;
height: ct.preferred-height;
mouse-cursor: pointer;
clicked => {
AlbumInfoActions.set-tab("credits");
}
ct := Text {
text: @tr("Credits");
color: AlbumInfoState.active-tab == "credits"
? Theme.accent : Theme.text-muted;
font-size: 13px;
font-weight: Typography.medium;
}
}
review-tab := TouchArea {
width: rt.preferred-width;
height: rt.preferred-height;
mouse-cursor: pointer;
clicked => {
AlbumInfoActions.set-tab("review");
}
rt := Text {
text: @tr("Review");
color: AlbumInfoState.active-tab == "review"
? Theme.accent : Theme.text-muted;
font-size: 13px;
font-weight: Typography.medium;
}
}
}
Rectangle { height: 12px; }
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
Rectangle { height: 12px; }
}
// Content (scrollable) + overlaid scrollbar.
Rectangle {
vertical-stretch: 1;
rflick := Flickable {
viewport-width: self.width;
viewport-height: content.preferred-height;
content := VerticalLayout {
width: parent.width;
spacing: 0px;
// Reserve the scrollbar gutter so rows
// (and the expand chevron at the row's
// right edge) never sit under the 14px
// ListScrollbar.
padding-right: 18px;
// Credits tab.
if AlbumInfoState.active-tab == "credits": VerticalLayout {
spacing: 0px;
for t[i] in AlbumInfoState.tracks: AlbumTrackRow {
track: t;
is-last: i == AlbumInfoState.tracks.length - 1;
play(id) => {
AlbumInfoActions.play-track(id);
}
musician(n, r) => {
AlbumInfoActions.open-musician(n, r);
}
}
}
// Review tab.
if AlbumInfoState.active-tab == "review": Text {
text: AlbumInfoState.review;
color: Theme.text-secondary;
font-size: 14px;
wrap: word-wrap;
}
}
}
ListScrollbar {
x: parent.width - self.width;
width: 14px;
height: 100%;
viewport-height: rflick.viewport-height;
visible-height: rflick.height;
viewport-y <=> rflick.viewport-y;
}
}
}
}
}
// ---- Loading / error overlay -------------------------------
// Covers the (empty) skeleton while fetching or on error.
if AlbumInfoState.loading || AlbumInfoState.error != "": Rectangle {
background: Theme.surface-card;
border-radius: Radius.md;
// Swallow clicks so the skeleton behind isn't reachable.
TouchArea { }
VerticalLayout {
alignment: center;
spacing: 8px;
Text {
text: AlbumInfoState.error != ""
? @tr("Failed to load album credits")
: @tr("Loading credits...");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
if AlbumInfoState.error != "": Text {
text: AlbumInfoState.error;
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: center;
wrap: word-wrap;
}
}
}
}
}
}
}
File diff suppressed because it is too large. Load diff
-617
View File
@@ -1,617 +0,0 @@
// Dedicated Local Library album detail — SEPARATE from the Qobuz AlbumView.
// Reuses the internal components (NavButtons, CircleAction, TrackRow,
// QualityBadgeFull) but exposes ONLY local actions (play all / shuffle / edit
// tags / add to playlist). When the album has multiple physical versions
// (distinct source folders), a version picker switches the shown copy — so two
// copies of the same album never merge into a duplicated track list.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { LocalAlbumState, LocalAlbumActions, LocalAlbumVersion, NowPlayingState, NavState, DragState, UiFocusState, ShellState } from "../state.slint";
import { NavButtons } from "../shell/NavButtons.slint";
import { CircleAction } from "../primitives/CircleAction.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { TrackRow } from "../primitives/TrackRow.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
import { QualityBadgeFull } from "../primitives/QualityBadgeFull.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
import { DiscHeaderMenu } from "../primitives/DiscHeaderMenu.slint";
import { ListView } from "std-widgets.slint";
// Source glyph for a version: hard-drive (local), Qobuz logo (offline cache),
// Plex logo. The logos keep their own colors; the local glyph is tinted.
component SourceIcon inherits Image {
in property <string> kind;
width: 14px;
height: 14px;
image-fit: contain;
source: (root.kind == "qobuz_download" || root.kind == "qobuz_purchase")
? @image-url("../assets/icons/qobuz-logo-filled.svg")
: (root.kind == "plex"
? @image-url("../assets/icons/plex-logo.svg")
: @image-url("../assets/icons/hard-drive.svg"));
// `qobuz_purchase` → GOLD-tinted Qobuz mark (§8.7 purchase badge).
colorize: root.kind == "qobuz_purchase" ? #eab308
: ((root.kind == "qobuz_download" || root.kind == "plex")
? transparent
: Theme.text-secondary);
}
// Version picker — selected display + dropdown, both with the source icon.
component VersionPicker inherits Rectangle {
in property <[LocalAlbumVersion]> versions;
in property <int> current;
callback select(int);
// Matches QbzSelect `sm` (height 30, radius 6, no outline, 10px left pad)
// so it reads like every other small select instead of towering over them.
height: 30px;
width: pick-row.preferred-width;
border-radius: 6px;
background: pick-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
pick-row := HorizontalLayout {
padding-left: 10px;
padding-right: 8px;
spacing: 7px;
VerticalLayout {
alignment: center;
SourceIcon { kind: root.versions[root.current].source; }
}
VerticalLayout {
alignment: center;
Text {
text: root.versions[root.current].label;
color: Theme.text-secondary;
font-size: Typography.legal;
vertical-alignment: center;
}
}
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/chevron-down.svg");
width: 12px;
height: 12px;
tint: Theme.text-muted;
}
}
}
pick-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
menu.show();
}
}
menu := PopupWindow {
x: 0;
y: parent.height + 4px;
width: 280px;
close-policy: close-on-click;
Rectangle {
background: Theme.surface-card;
border-radius: 8px;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 22px;
drop-shadow-color: #00000080;
VerticalLayout {
padding: 6px;
spacing: 1px;
for v[i] in root.versions: Rectangle {
height: 30px;
border-radius: 6px;
background: i == root.current
? Theme.surface-elevated
: (row-ta.has-hover ? Theme.surface-hover : transparent);
HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
spacing: 8px;
VerticalLayout {
alignment: center;
SourceIcon { kind: v.source; }
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: v.label;
color: Theme.text-primary;
font-size: Typography.legal;
vertical-alignment: center;
overflow: elide;
}
}
if i == root.current: VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/check.svg");
width: 12px;
height: 12px;
tint: Theme.accent;
}
}
}
row-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.select(i);
}
}
}
}
}
}
}
export component LocalAlbumView inherits Rectangle {
// Navigate to the artist (the NAME — local/Plex artists have no id; the
// source-aware open-artist routes a name to the LocalLibrary Artists tab).
callback open-artist(string);
background: ShellState.app-background-active ? transparent : Theme.surface-main;
// Expanded multi-artist header list (spec §B2) — view-local, resets when
// another album opens (watched via the mirror property below).
property <bool> artists-expanded: false;
property <string> watched-album-id: LocalAlbumState.id;
changed watched-album-id => {
root.artists-expanded = false;
}
// Homologated 1:1 with the Qobuz AlbumPageView: whole-page Flickable, the
// same header proportions + action row, the toolbar (quality badge + track
// search), the column header, and source-aware TrackRows. The only
// intentional differences vs AlbumPageView: no label/awards right sidebar,
// no Qobuz context menus, and the local-only version picker.
flick := Flickable {
interactive: !DragState.active;
viewport-width: self.width;
viewport-height: page.preferred-height;
// Scroll-position restore on back/forward (see nav::scope_for).
property <string> sr-armed: NavState.restore-scope;
function sr-restore() {
if (NavState.restore-scope == "local-album" && self.viewport-height > self.height) {
self.viewport-y = NavState.scroll-restore;
NavState.restore-scope = "";
}
}
init => { self.sr-restore(); }
changed sr-armed => { self.sr-restore(); }
changed viewport-height => { self.sr-restore(); }
changed viewport-y => { NavState.report-scroll(self.viewport-y); }
// Header band (neutral — local albums have no artwork-derived tint yet).
Rectangle {
x: 0;
y: 0;
width: parent.width;
height: 340px;
background: @linear-gradient(180deg,
#181820 0%,
#181820 16%,
transparent 100%);
}
page := VerticalLayout {
width: parent.width;
padding-left: 32px;
padding-right: 32px;
padding-top: 11px;
padding-bottom: 100px;
spacing: 0px;
alignment: start;
// --- History navigation -------------------------------------
HorizontalLayout {
alignment: start;
NavButtons { }
}
Rectangle { height: 22px; }
// --- Album header -------------------------------------------
HorizontalLayout {
spacing: 32px;
Rectangle {
width: 224px;
height: 224px;
border-radius: 12px;
background: Theme.surface-elevated;
drop-shadow-blur: 32px;
drop-shadow-color: #00000080;
drop-shadow-offset-y: 8px;
clip: true;
Image {
width: 100%;
height: 100%;
source: LocalAlbumState.cover;
image-fit: cover;
}
}
VerticalLayout {
alignment: start;
padding-top: 4px;
spacing: 0px;
horizontal-stretch: 1;
Text {
text: LocalAlbumState.title;
color: Theme.text-primary;
font-size: Typography.section;
font-weight: Typography.bold;
overflow: elide;
}
Rectangle { height: 4px; }
HorizontalLayout {
alignment: start;
spacing: 8px;
la-artist := TouchArea {
width: la-artist-text.preferred-width;
height: la-artist-text.preferred-height;
mouse-cursor: pointer;
clicked => {
root.open-artist(LocalAlbumState.artist);
}
la-artist-text := Text {
text: LocalAlbumState.artist;
color: la-artist.has-hover
? Theme.text-primary
: Theme.text-secondary;
font-size: Typography.heading;
font-weight: Typography.bold;
}
}
// Multi-artist album (compilations): a "+N more
// artists" chevron expands the full track-artist list
// below (spec §B2). Single-artist albums never see it.
if LocalAlbumState.all-artists.length > 1: VerticalLayout {
alignment: center;
more-ta := TouchArea {
width: more-text.preferred-width;
height: more-text.preferred-height;
mouse-cursor: pointer;
clicked => {
root.artists-expanded = !root.artists-expanded;
}
more-text := Text {
text: root.artists-expanded
? @tr("Show less")
: @tr("+{} more artists", LocalAlbumState.all-artists.length - 1);
color: more-ta.has-hover ? Theme.accent : Theme.text-muted;
font-size: Typography.body;
}
}
}
}
// Expanded track-artist list — each entry routes to the
// LocalLibrary Artists tab (name-based open-artist).
if root.artists-expanded && LocalAlbumState.all-artists.length > 1: VerticalLayout {
spacing: 0px;
padding-top: 6px;
for artist in LocalAlbumState.all-artists: TouchArea {
height: 24px;
mouse-cursor: pointer;
clicked => {
root.open-artist(artist);
}
Text {
text: artist;
color: parent.has-hover ? Theme.accent : Theme.text-secondary;
font-size: Typography.body;
vertical-alignment: center;
}
}
}
Rectangle { height: 10px; }
Text {
text: LocalAlbumState.info-line;
color: Theme.text-secondary;
font-size: Typography.body;
overflow: elide;
}
Rectangle { height: 20px; }
// Local action row — same chrome as AlbumPageView, only the
// actions that apply to a local/Plex album.
HorizontalLayout {
alignment: start;
spacing: 12px;
CircleAction {
icon: @image-url("../assets/icons/play-fill.svg");
primary: true;
// The neutral header band has largely faded out at
// the action row's height — on light themes these
// sit on a mostly-plain surface, so use the
// theme-aware variant (reads fine over the faded
// band on dark themes too).
on-surface: true;
loading: NowPlayingState.loading
&& NowPlayingState.album-id == LocalAlbumState.id;
clicked => {
LocalAlbumActions.play-all();
}
}
VerticalLayout {
alignment: center;
CircleAction {
icon: @image-url("../assets/icons/shuffle.svg");
on-surface: true;
clicked => {
LocalAlbumActions.shuffle();
}
}
}
VerticalLayout {
alignment: center;
CircleAction {
icon: @image-url("../assets/icons/pencil.svg");
on-surface: true;
clicked => {
LocalAlbumActions.edit-tags();
}
}
}
VerticalLayout {
alignment: center;
CircleAction {
icon: @image-url("../assets/icons/list-plus.svg");
on-surface: true;
clicked => {
LocalAlbumActions.add-to-playlist();
}
}
}
VerticalLayout {
alignment: center;
CircleAction {
icon: @image-url("../assets/icons/cassette-tape.svg");
on-surface: true;
clicked => {
LocalAlbumActions.add-to-mixtape();
}
}
}
}
// Version picker (only with multiple physical copies).
if LocalAlbumState.versions.length > 1: Rectangle { height: 16px; }
if LocalAlbumState.versions.length > 1: HorizontalLayout {
alignment: start;
spacing: 8px;
VerticalLayout {
alignment: center;
Text {
text: "\{LocalAlbumState.versions.length} " + @tr("versions") + ":";
color: Theme.text-muted;
font-size: Typography.legal;
}
}
VerticalLayout {
alignment: center;
VersionPicker {
versions: LocalAlbumState.versions;
current: LocalAlbumState.version-index;
select(i) => {
LocalAlbumActions.select-version(i);
}
}
}
}
}
}
Rectangle { height: 20px; }
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
Rectangle { height: 8px; }
// --- Track list (no sidebar) --------------------------------
VerticalLayout {
spacing: 0px;
alignment: start;
if LocalAlbumState.loading && LocalAlbumState.tracks.length == 0: Rectangle {
height: 280px;
VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
}
}
// Toolbar — album quality badge + track search.
HorizontalLayout {
height: 52px;
spacing: 16px;
VerticalLayout {
alignment: center;
QualityBadgeFull {
tier: LocalAlbumState.quality-tier;
detail: LocalAlbumState.quality-detail;
}
}
Rectangle { horizontal-stretch: 1; }
VerticalLayout {
alignment: center;
Rectangle {
width: 280px;
height: 34px;
border-radius: 6px;
border-width: 1px;
border-color: Theme.border-subtle;
background: Theme.surface-elevated;
HorizontalLayout {
padding-left: 10px;
padding-right: 10px;
spacing: 7px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 14px;
height: 14px;
tint: Theme.text-muted;
}
}
Rectangle {
horizontal-stretch: 1;
track-search := TextInput {
width: 100%;
height: 100%;
color: Theme.text-primary;
font-size: 13px;
vertical-alignment: center;
single-line: true;
changed has-focus => { UiFocusState.text-input-focused = self.has-focus; }
edited => {
LocalAlbumActions.search(self.text);
}
}
Text {
visible: track-search.text == "";
text: @tr("Search tracks...");
width: 100%;
height: 100%;
color: Theme.text-muted;
font-size: 13px;
vertical-alignment: center;
}
}
}
}
}
}
// Column header.
HorizontalLayout {
height: 40px;
padding-left: 12px;
padding-right: 12px;
spacing: 16px;
Text {
text: "#";
width: 32px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Text {
// Capital-case to match PlaylistView's column headers.
text: @tr("Title");
horizontal-stretch: 1;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
vertical-alignment: center;
}
Text {
// Capital-case to match PlaylistView's column headers.
text: @tr("Duration");
width: 80px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
horizontal-alignment: center;
vertical-alignment: center;
}
Text {
// Capital-case to match PlaylistView's column headers.
text: @tr("Quality");
width: 80px;
color: Theme.text-muted;
font-size: Typography.legal;
letter-spacing: 0.5px;
horizontal-alignment: center;
vertical-alignment: center;
}
// Source column (local/Plex/qobuz indicator on each row).
Rectangle { width: 28px; }
// Aligns with the per-row more-button slot.
Rectangle { width: 32px; }
}
// Disc divider before the first row of each disc on a
// multi-disc local album (disc-header-number is stamped in
// apply_album_version; 0 for single-disc → flat list, as
// before). Same pattern as the Qobuz AlbumPageView.
for track[index] in LocalAlbumState.tracks: VerticalLayout {
spacing: 0px;
if track.disc-header-number > 0: Rectangle {
height: 40px;
HorizontalLayout {
padding-left: 12px;
padding-right: 12px;
spacing: 16px;
Text {
text: @tr("Disc") + " \{track.disc-header-number}";
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: 600;
letter-spacing: 0.5px;
vertical-alignment: center;
horizontal-alignment: left;
horizontal-stretch: 1;
}
// Per-disc ⋯ menu — actions scoped to this disc's
// tracks only (aligns with the column header's 32px
// right-edge more-button slot).
VerticalLayout {
alignment: center;
DiscHeaderMenu {
disc: track.disc-header-number;
disc-action(d, a) => {
LocalAlbumActions.disc-action(d, a);
}
}
}
}
}
TrackRow {
track: track;
index: index;
show-artwork: false;
show-album: false;
show-favorite: false;
show-download: false;
// Show the per-row source glyph (local / qobuz-offline / Plex).
show-source: true;
// Library surface: local menu even for qobuz_download
// rows (spec §3.4).
force-local-menu: true;
media-action(kind, id, action) => {
// Local-only: ignore album/artist link clicks. "play"
// (row body / play cell) plays; the context-menu
// actions (play-next / queue / add-to-playlist) route
// to their own handler instead of collapsing to play.
if (kind != "album" && kind != "artist") {
if (action == "play") {
LocalAlbumActions.play-track(id);
} else {
LocalAlbumActions.track-menu-action(id, action);
}
}
}
}
}
}
}
}
// Shared scrollbar in the right gutter (homologated cross-view).
ListScrollbar {
x: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: flick.viewport-height;
visible-height: flick.height;
viewport-y <=> flick.viewport-y;
}
}
-364
View File
@@ -1,364 +0,0 @@
// Track Info modal — 1:1 port of Tauri's TrackInfoModal.svelte. Opened from
// the NPB (i) button / song-card title / TrackRow context menu via
// media-action("track", id, "track-info"). The Rust controller
// (crate::info_modals) fetches the track, parses + groups + localizes the
// credits, and populates TrackInfoState. Rectangle-scrim + centered card
// (no PopupWindow), mounted in AppShell after the other modals (ADR-009/010).
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { TrackInfoState, TrackInfoActions, InfoCreditRow } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
// One metadata cell: uppercase label (11px) + a value supplied as a child.
// Exported so the immersive split Track Info panel can reuse it 1:1.
export component MetaCell inherits VerticalLayout {
in property <string> label;
spacing: 6px;
Text {
text: root.label;
color: Theme.text-muted;
font-size: 11px;
font-weight: Typography.semibold;
letter-spacing: 0.5px;
}
@children
}
// One credit cell: role label (11px upper) + the performer names, each
// clickable -> musician. Clipped so a long name list can't bleed into the
// neighbouring column (the common 1-3 name case fits one line).
// Exported so the immersive split Track Info panel can reuse it 1:1.
export component CreditCell inherits VerticalLayout {
in property <InfoCreditRow> cell;
// Fixed cell width handed down by the modal so the two columns stay aligned
// and the value text wraps inside the column instead of dictating its width.
in property <length> col-w: 280px;
callback name-clicked(string /* name */, string /* role-raw */);
spacing: 6px;
Text {
text: root.cell.role;
color: Theme.text-muted;
font-size: 11px;
font-weight: Typography.semibold;
letter-spacing: 0.5px;
width: root.col-w;
wrap: word-wrap;
}
// One clickable name per line (Slint has no flow layout, so a wrapping row
// of links would clip): keeps click-to-musician AND never overflows. Each
// name wraps within the fixed column width if a single name is very long.
VerticalLayout {
spacing: 2px;
for name in root.cell.names: ta := TouchArea {
width: root.col-w;
height: nmt.preferred-height;
mouse-cursor: pointer;
clicked => {
root.name-clicked(name, root.cell.role-raw);
}
nmt := Text {
text: name;
color: ta.has-hover ? Theme.accent : Theme.text-primary;
font-size: 14px;
width: root.col-w;
wrap: word-wrap;
}
}
}
}
export component TrackInfoModal inherits Rectangle {
visible: TrackInfoState.open;
if TrackInfoState.open: Rectangle {
background: #000000bf;
// Scrim — click outside dismisses.
TouchArea {
mouse-cursor: default;
clicked => {
TrackInfoActions.close();
}
}
card := Rectangle {
width: Math.min(root.width - 40px, 728px);
// Fixed credits-column width: card minus the 24px L/R content padding
// (48) minus the 24px inter-column gutter, halved. Pinning each credit
// cell to this (instead of horizontal-stretch, whose split skews with
// each cell's content width) keeps the right column aligned on every row.
property <length> credits-col-w: (self.width - 48px - 24px) / 2;
height: Math.min(root.height * 0.8, card-body.preferred-height);
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
border-radius: Radius.md;
background: Theme.surface-card;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 32px;
drop-shadow-color: Theme.card-shadow;
// Swallow clicks so they don't reach the scrim.
TouchArea { }
fs := FocusScope {
// Focus a tick after mount (synchronous focus races layout).
Timer {
interval: 30ms;
running: true;
triggered => {
self.running = false;
fs.focus();
}
}
key-pressed(event) => {
if (event.text == Key.Escape) {
TrackInfoActions.close();
return accept;
}
return reject;
}
flick := Flickable {
viewport-width: self.width;
viewport-height: card-body.preferred-height;
card-body := VerticalLayout {
width: parent.width;
// ---- Loading --------------------------------
if TrackInfoState.loading && TrackInfoState.error == "": VerticalLayout {
alignment: center;
padding: 60px;
Text {
text: @tr("Loading track info...");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
// ---- Error ----------------------------------
if TrackInfoState.error != "": VerticalLayout {
alignment: center;
padding: 60px;
spacing: 8px;
Text {
text: @tr("Failed to load track info");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
Text {
text: TrackInfoState.error;
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: center;
wrap: word-wrap;
}
}
// ---- Loaded ---------------------------------
if !TrackInfoState.loading && TrackInfoState.error == "": VerticalLayout {
// Header
HorizontalLayout {
padding-left: 24px;
padding-right: 24px;
padding-top: 16px;
padding-bottom: 16px;
spacing: 16px;
VerticalLayout {
horizontal-stretch: 1;
spacing: 4px;
Text {
text: TrackInfoState.title;
color: Theme.text-primary;
font-size: 16px;
font-weight: Typography.semibold;
wrap: word-wrap;
}
if TrackInfoState.album != "": Text {
text: TrackInfoState.album;
color: Theme.text-muted;
font-size: Typography.legal;
}
if TrackInfoState.artist != "": HorizontalLayout {
alignment: start;
artist-link := TouchArea {
width: artist-text.preferred-width;
height: artist-text.preferred-height;
mouse-cursor: TrackInfoState.artist-id != "" ? pointer : default;
clicked => {
if (TrackInfoState.artist-id != "") {
TrackInfoActions.open-artist(TrackInfoState.artist-id);
}
}
artist-text := Text {
text: TrackInfoState.artist;
color: TrackInfoState.artist-id != ""
? Theme.accent : Theme.text-primary;
font-size: 16px;
font-weight: Typography.semibold;
}
}
}
}
// Close X
close-x := TouchArea {
width: 32px;
height: 32px;
mouse-cursor: pointer;
clicked => {
TrackInfoActions.close();
}
QbzIcon {
source: @image-url("../assets/icons/x.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: close-x.has-hover ? Theme.text-primary : Theme.text-muted;
}
}
}
// Content
VerticalLayout {
padding-left: 24px;
padding-right: 24px;
padding-bottom: 24px;
spacing: 0px;
// Metadata: fixed 3-column rows.
VerticalLayout {
spacing: 24px;
HorizontalLayout {
spacing: 24px;
MetaCell {
horizontal-stretch: 1;
label: @tr("Duration");
Text {
text: TrackInfoState.duration;
color: Theme.text-primary;
font-size: 14px;
}
}
MetaCell {
horizontal-stretch: 1;
label: @tr("Quality");
Text {
text: TrackInfoState.quality;
color: Theme.text-primary;
font-size: 14px;
}
}
if TrackInfoState.isrc != "": MetaCell {
horizontal-stretch: 1;
label: "ISRC";
Text {
text: TrackInfoState.isrc;
color: Theme.text-muted;
font-size: 14px;
}
}
if TrackInfoState.isrc == "": Rectangle { horizontal-stretch: 1; }
}
if TrackInfoState.label != "": HorizontalLayout {
spacing: 24px;
MetaCell {
horizontal-stretch: 1;
label: @tr("Label");
label-link := TouchArea {
width: label-text.preferred-width;
height: label-text.preferred-height;
mouse-cursor: TrackInfoState.label-id != "" ? pointer : default;
clicked => {
if (TrackInfoState.label-id != "") {
TrackInfoActions.open-label(TrackInfoState.label-id);
}
}
label-text := Text {
text: TrackInfoState.label;
color: (label-link.has-hover && TrackInfoState.label-id != "")
? Theme.accent : Theme.text-primary;
font-size: 14px;
}
}
}
Rectangle { horizontal-stretch: 1; }
Rectangle { horizontal-stretch: 1; }
}
}
// Credits (2 columns), only when present.
if TrackInfoState.credits.length > 0: Rectangle { height: 20px; }
if TrackInfoState.credits.length > 0: Rectangle {
height: 1px;
background: Theme.surface-elevated;
}
if TrackInfoState.credits.length > 0: Rectangle { height: 20px; }
// Two INDEPENDENT columns (left/right stacks) with
// natural vertical spacing — no height-coupled rows,
// so a column with more names just grows on its own.
if TrackInfoState.credits.length > 0: HorizontalLayout {
spacing: 24px;
alignment: start;
VerticalLayout {
width: card.credits-col-w;
spacing: 20px;
alignment: start;
for entry in TrackInfoState.credits-left: CreditCell {
col-w: card.credits-col-w;
cell: entry;
name-clicked(n, r) => {
TrackInfoActions.open-musician(n, r);
}
}
}
VerticalLayout {
width: card.credits-col-w;
spacing: 20px;
alignment: start;
for entry in TrackInfoState.credits-right: CreditCell {
col-w: card.credits-col-w;
cell: entry;
name-clicked(n, r) => {
TrackInfoActions.open-musician(n, r);
}
}
}
}
// Copyright.
if TrackInfoState.copyright != "": Rectangle { height: 20px; }
if TrackInfoState.copyright != "": Rectangle {
height: 1px;
background: Theme.surface-elevated;
}
if TrackInfoState.copyright != "": Rectangle { height: 20px; }
if TrackInfoState.copyright != "": Text {
text: TrackInfoState.copyright;
color: Theme.text-muted;
font-size: 12px;
wrap: word-wrap;
}
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: flick.viewport-height;
visible-height: flick.height;
viewport-y <=> flick.viewport-y;
}
}
}
}
}
-305
View File
@@ -1,305 +0,0 @@
// QBZ Slint MVP — UI entry point.
//
// Stays thin: imports the feature screens and exports the single
// top-level window. No raw visual constants or domain logic here
// (MVP ADR, sections 13 and 21).
// Bundles + registers the Inter 18pt faces (compile-time embed) so the
// app can render with Inter — see the FONT_TEST_INTER toggle in main.rs.
import "assets/fonts/Inter_18pt-Regular.ttf";
import "assets/fonts/Inter_18pt-Medium.ttf";
import "assets/fonts/Inter_18pt-SemiBold.ttf";
import "assets/fonts/Inter_18pt-Bold.ttf";
import { Palette } from "std-widgets.slint";
import { Theme, ThemeColors } from "foundation/semantic-colors.slint";
import { ThemeState } from "foundation/theme-state.slint";
import { SplashScreen } from "splash/SplashScreen.slint";
import { LoginScreen } from "login/LoginScreen.slint";
import { AppShell } from "shell/AppShell.slint";
import { KioskShell } from "shell/KioskShell.slint";
import { Toast } from "primitives/Toast.slint";
import { ShellState, MiniPlayerState, NowPlayingState, AppearanceState, UiScale, WindowControlActions } from "state.slint";
import { MiniShell } from "miniplayer/MiniShell.slint";
// Re-export the theming globals so the Rust layer can push palettes (Theme.c /
// Theme.is-system) and keep the dropdown index (ThemeState.mode) in sync.
export { Theme, ThemeColors } from "foundation/semantic-colors.slint";
export { ThemeState } from "foundation/theme-state.slint";
// Typography is exported for the Small-preset font compensation only: Rust
// seeds `Typography.boost = 1.1` when the interface-size preset is Small.
export { Typography } from "foundation/typography.slint";
// Re-export the state globals so the Rust layer can populate them.
export { HomeState, HomeActions, RecentAlbumsState, MostPlayedAlbumsState, MostPlayedAlbumsActions, DiscoverState, DiscoverActions, SectionDescriptor, ConfigRow, DiscoverBrowseState, DiscoverBrowseActions, PlaylistBrowseState, PlaylistBrowseActions, ForYouState, PinnedItem, PinnedState, PinnedActions, ExternalRecoState, ExternalRecoActions, MixState, GenreFilterState, GenreFilterActions, AlbumState, ArtistState, NavState, ShellState, SessionState, SettingsState, AlbumActions, ArtistActions, ArtworkActions, NowPlayingState, QueueState, LyricsState, LyricsLineItem, SearchState, SearchActions, NetworkSidebarState, NetworkSidebarActions, MusicianState, MusicianActions, LabelState, LabelActions, AwardState, AwardActions, AwardEntry, ArtistReleasesState, ArtistReleasesActions, LocationViewState, LocationViewActions, FavoritesState, FavoritesActions, LibraryFeedItem, LibraryAllState, LibraryAllActions, PlaylistPickerState, PlaylistPickerActions, DuplicateConfirmState, DuplicateConfirmActions, PlaylistState, PlaylistActions, SidebarState, SidebarActions, SidebarFolderPopupState, CreatePlaylistState, CreatePlaylistActions, EditPlaylistState, EditPlaylistActions, CreateFolderState, CreateFolderActions, SettingsExportState, SettingsExportActions, SandboxState, MyQbzCreateState, MyQbzCreateActions, DragState, DragActions, PlaylistManagerState, PlaylistManagerActions, OfflineManagerState, OfflineManagerActions, BlacklistState, BlacklistActions, BlacklistedArtistItem, MyQbzState, MyQbzActions, MixtapeCardItem, MyQbzAddState, MyQbzAddActions, MyQbzAddRow, MyQbzDetailState, MyQbzDetailActions, MixtapeDetailItem, MyQbzEditState, MyQbzEditActions, MyQbzMixState, MyQbzMixActions, DiscoBuilderState, DiscoBuilderActions, DiscoGroup, DiscoCandidate, LocalLibraryState, LocalLibraryActions, LibraryFoldersState, LibFolderEditState, LibraryManageActions, LibraryScanState, LibAlbumFilterState, LocalAlbumState, LocalAlbumActions, TagEditorState, TagEditorActions, FolderEditState, FolderEditActions, ToastState, TextUtil, QconnectDevState, QconnectDevice, CastState, CastDevice, CastActions, AppearanceState, MyQbzBrandingState, EphemeralPlayChoiceState, EphemeralPlayChoiceActions, PlexSettingsState, PlexAuthActions, PlexSectionItem, ScrobbleState, ScrobbleActions, DiscordState, OfflineState, LoginState, OfflineModeActions, OfflineFavoritesState, OfflineFavoritesActions, ImportLogEntry, PlaylistImportState, PlaylistImportActions, DacWizardState, DacWizardActions, DacCandidateRow, RemediationRow, DacConfigRow, InfoCreditRow, InfoCreditPair, AlbumCreditPerformer, AlbumCreditTrack, TrackInfoState, TrackInfoActions, AlbumInfoState, AlbumInfoActions, BookletState, BookletActions, SuggestionsState, SuggestionsActions, SuggestionCard, PlaylistSuggestionsState, PlaylistSuggestionsActions, PlaylistSuggestionRow, VisualizerState, ImmersiveState, ImmersiveSearchActions, ImmersiveActions, MiniPlayerState, WindowControlActions, PurchasesState, PurchasesActions, PurchaseAlbumItem, PurchaseTrackItem, PurchaseAlbumGroup, PurchaseTrackGroup, PurchaseFormatItem, PurchaseDetailState, PurchaseDetailActions, PurchaseDetailTrack, KeybindingRow, KeybindingCategoryGroup, KeybindingsState, KeybindingsActions, KeyboardShortcutsState, LinkResolverState, LinkResolverActions, UiFocusState, UiScale, SleepTimerState, SleepTimerActions, LogRow, LogViewerState, DiagRow, DiagnosticsState, ReportIssueState, ReportIssueActions, AboutState, AboutActions, AboutContributorRow, AboutContributorGroup, WhatsNewState, WhatsNewActions, WhatsNewBlock, WhatsNewTocEntry } from "state.slint";
// Which top-level screen is shown. The app starts on `splash` while it
// restores a saved session, then resolves to `shell` or `login`.
export enum AppScreen { splash, login, shell, kiosk }
export component AppWindow inherits Window {
// "Show track in window title" (Appearance settings): when enabled and a
// track is loaded, the OS window title becomes "{track} - {artist} | qbz";
// otherwise the plain app name. Reactive — updates on every track change.
title: AppearanceState.window-title-show && NowPlayingState.has-track
? NowPlayingState.title + " - " + NowPlayingState.artist + " | qbz"
: "QBZ";
preferred-width: 1180px;
preferred-height: 760px;
// Divide the interface-size preset back out so the PHYSICAL minimum stays
// constant across presets (a plain 940px logical minimum at the XL preset
// would be 1410 physical px — taller than a 1366x768 screen).
min-width: 940px / UiScale.factor;
min-height: 600px / UiScale.factor;
background: Theme.surface-main;
// Custom window chrome (owner decision 2026-07-03: the header IS the
// title bar — it is always present; custom mode only drops the system
// decorations while HeaderBar gains the window controls + drag).
// Linux-only: macOS keeps native decorations and gets the overlay
// attributes (transparent titlebar + fullsize content view) from the
// winit attributes hook instead. `no-frame` is the authoritative switch
// (the adapter re-applies it at every realization — same lesson as the
// miniplayer, see MiniPlayerWindow below); the persisted pref is seeded
// into `system-title-bar-active` BEFORE the first show; the settings
// toggle edits the pref (`use-system-title-bar`) and Rust decides per
// platform whether it reaches this applied state live (Linux) or only
// on the next launch (macOS — attributes fixed at surface creation).
no-frame: !AppearanceState.is-macos && !AppearanceState.system-title-bar-active;
// Edge-resize for the frameless mode (the winit backend only engages it
// on undecorated windows, so this is inert under system chrome).
resize-border-width: 6px;
// Resolved by the Rust layer from the desktop environment (e.g. the
// KDE Plasma general font). Empty falls back to the Slint default.
in property <string> system-font: "";
default-font-family: root.system-font;
// Publish the live window width so content-area views can make
// responsiveness decisions (see ShellState.content-constrained).
init => {
ShellState.window-width = self.width;
root.sync-native-palette();
}
changed width => {
ShellState.window-width = self.width;
}
// Keep the std-widgets `Palette` (native LineEdit/TextEdit/… colors) in
// step with the active QBZ theme. Without this, native inputs stayed on the
// OS palette: pick a light QBZ theme while the OS palette was dark and the
// input text rendered in the dark-scheme foreground over the light page —
// text the same tone as its background, effectively invisible (Settings
// input fields on light themes). `unknown` lets System keep following the OS.
property <bool> pal-is-dark: Theme.is-dark;
property <bool> pal-is-system: Theme.is-system;
function sync-native-palette() {
Palette.color-scheme = Theme.is-system
? ColorScheme.unknown
: (Theme.is-dark ? ColorScheme.dark : ColorScheme.light);
}
changed pal-is-dark => { root.sync-native-palette(); }
changed pal-is-system => { root.sync-native-palette(); }
in-out property <AppScreen> screen: AppScreen.splash;
// Forwarded to the Rust layer.
callback sign-in-via-browser();
callback cancel-login();
callback start-offline();
callback open-tos();
callback open-album(string);
callback open-artist(string);
callback open-purchase-album(string);
callback media-action(string, string, string);
callback header-menu-navigate(string);
callback discover-view-all(string, string);
callback settings-bool(string, bool);
callback settings-select(string, int);
callback settings-slider(string, int);
callback settings-string(string, string);
callback settings-reset();
callback settings-release-device();
callback logout();
callback close-app();
// D2 recovery banner: sign back in with the saved token.
callback recovery-login();
if root.screen == AppScreen.splash: SplashScreen { }
if root.screen == AppScreen.login: LoginScreen {
sign-in-via-browser => {
root.sign-in-via-browser();
}
cancel-login => {
root.cancel-login();
}
start-offline => {
root.start-offline();
}
open-tos => {
root.open-tos();
}
}
if root.screen == AppScreen.shell: AppShell {
// Fill the window. A conditionally-mounted child sizes to its PREFERRED
// size by default, so without this the shell sized to its content — fine
// while the right panels were always mounted (tall content), but once
// they became conditional, with both closed the shell collapsed to the
// top with dead space below. Bind to the real window size (`root` here is
// the AppWindow); `100%` did not pin it.
width: root.width;
height: root.height;
open-album(id) => {
root.open-album(id);
}
open-artist(id) => {
root.open-artist(id);
}
open-purchase-album(id) => {
root.open-purchase-album(id);
}
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
header-menu-navigate(route) => {
root.header-menu-navigate(route);
}
discover-view-all(e, t) => {
root.discover-view-all(e, t);
}
settings-bool(k, v) => {
root.settings-bool(k, v);
}
settings-select(k, i) => {
root.settings-select(k, i);
}
settings-slider(k, v) => {
root.settings-slider(k, v);
}
settings-string(k, s) => {
root.settings-string(k, s);
}
settings-reset => {
root.settings-reset();
}
settings-release-device => {
root.settings-release-device();
}
logout => {
root.logout();
}
close-app => {
root.close-app();
}
recovery-login => {
root.recovery-login();
}
}
// Kiosk touch shell — a sibling of AppShell (small-panel profile). Same
// conditional-mount geometry pin as AppShell (a conditional child sizes to
// preferred size, so width/height are bound explicitly). Forwards the same
// callbacks to the AppWindow root.
if root.screen == AppScreen.kiosk: KioskShell {
width: root.width;
height: root.height;
open-album(id) => {
root.open-album(id);
}
open-artist(id) => {
root.open-artist(id);
}
open-purchase-album(id) => {
root.open-purchase-album(id);
}
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
header-menu-navigate(route) => {
root.header-menu-navigate(route);
}
discover-view-all(e, t) => {
root.discover-view-all(e, t);
}
settings-bool(k, v) => {
root.settings-bool(k, v);
}
settings-select(k, i) => {
root.settings-select(k, i);
}
settings-slider(k, v) => {
root.settings-slider(k, v);
}
settings-string(k, s) => {
root.settings-string(k, s);
}
settings-reset => {
root.settings-reset();
}
settings-release-device => {
root.settings-release-device();
}
logout => {
root.logout();
}
close-app => {
root.close-app();
}
recovery-login => {
root.recovery-login();
}
}
// App-global in-app toast overlay. Declared last so it floats over
// every screen (splash / login / shell). Driven by ToastState via
// crate::toast.
Toast {
width: 100%;
height: 100%;
}
// Frameless hairline: with the system decorations gone (custom chrome on
// Linux) the compositor draws no border or shadow, so a 1px in-app frame
// keeps the window edge readable against same-tone backgrounds. Skipped
// while maximized / fullscreen (no visible edge to mark). Declared last
// so it paints at the very rim, over everything.
if root.no-frame && !WindowControlActions.is-maximized
&& !WindowControlActions.is-fullscreen: Rectangle {
width: 100%;
height: 100%;
background: transparent;
border-width: 1px;
border-color: Theme.border-subtle;
}
}
// Second top-level window — the miniplayer. Borderless + transparent (set at
// the winit level in crate::miniplayer); the 6px transparent gutter + the
// rounded card recreate Tauri's floating-card look. The first QBZ surface with
// no system titlebar.
export component MiniPlayerWindow inherits Window {
title: "QBZ Mini Player";
min-width: 340px;
min-height: 57px;
background: transparent;
// BORDERLESS — the real fix. Slint's winit adapter OVERRIDES winit
// decorations at every window realization from this property
// (`set_decorations(!no_frame() || fullscreen)`, winitwindowadapter.rs:1268),
// so the BackendSelector attributes hook alone was being undone. `no-frame`
// is the native, authoritative borderless switch. First QBZ surface with no
// system titlebar; AppWindow keeps `no-frame: false` (default).
no-frame: true;
in property <string> system-font: "";
default-font-family: root.system-font;
// 6px transparent gutter around the rounded card.
MiniShell {
x: 6px;
y: 6px;
width: parent.width - 12px;
height: parent.height - 12px;
}
}
File diff suppressed because it is too large. Load diff
@@ -1,257 +0,0 @@
// Dedicated artist discography page — the full, paginated grid for one
// release bucket, reached via "See discography" on the artist page.
// Structure homologated with AwardAlbumsView: a fixed header (NavButtons +
// artist kicker + bucket title on the LEFT, a sort select on the RIGHT) then
// an infinite-scroll Flickable driving ArtistReleasesActions.load-more().
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { ArtistReleasesState, ArtistReleasesActions, NavState, ShellState } from "../state.slint";
import { AlbumCollectionView } from "../discover/AlbumCollectionView.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
import { NavButtons } from "../shell/NavButtons.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
export component ArtistReleasesView inherits Rectangle {
callback open-album(string);
callback open-artist(string);
callback media-action(string, string, string);
property <length> album-w: 200px;
property <length> album-h: 266px;
property <length> gap: 24px;
property <int> sort-index:
ArtistReleasesState.sort-by == "newest" ? 1 :
ArtistReleasesState.sort-by == "oldest" ? 2 :
ArtistReleasesState.sort-by == "title-asc" ? 3 :
ArtistReleasesState.sort-by == "title-desc" ? 4 : 0;
background: ShellState.app-background-active ? transparent : Theme.surface-main;
VerticalLayout {
spacing: 0px;
// --- Fixed header ------------------------------------------------
Rectangle {
height: 56px;
background: Theme.surface-main;
// Left cluster — nav buttons then the artist kicker + bucket title.
HorizontalLayout {
x: 32px;
y: 25px - self.height / 2;
width: self.preferred-width;
height: self.preferred-height;
spacing: 16px;
VerticalLayout {
alignment: center;
NavButtons { }
}
VerticalLayout {
alignment: center;
spacing: 2px;
// Kicker — the artist name, server text (NEVER @tr).
Text {
text: ArtistReleasesState.name;
color: Theme.text-muted;
font-size: 11px;
font-weight: Typography.semibold;
letter-spacing: 0.6px;
vertical-alignment: center;
}
Text {
text: ArtistReleasesState.title;
color: Theme.text-primary;
font-size: Typography.section;
font-weight: Typography.bold;
vertical-alignment: center;
}
}
}
// Right cluster — the sort select, anchored right.
HorizontalLayout {
x: parent.width - self.preferred-width - 32px;
y: 25px - self.height / 2;
width: self.preferred-width;
height: self.preferred-height;
VerticalLayout {
alignment: center;
QbzSelect {
sm: true;
menu-width: 150px;
current-index: root.sort-index;
options: [
@tr("Default"),
@tr("Newest"),
@tr("Oldest"),
@tr("A–Z"),
@tr("Z–A"),
];
selected(idx) => {
ArtistReleasesActions.set-sort(
idx == 1 ? "newest"
: idx == 2 ? "oldest"
: idx == 3 ? "title-asc"
: idx == 4 ? "title-desc"
: "default");
}
}
}
}
}
// --- Scrolling grid ----------------------------------------------
flick := Flickable {
vertical-stretch: 1;
viewport-width: self.width;
viewport-height: page.preferred-height;
property <string> sr-armed: NavState.restore-scope;
function sr-restore() {
if (NavState.restore-scope == "artist-releases" && self.viewport-height > self.height) {
self.viewport-y = NavState.scroll-restore;
NavState.restore-scope = "";
}
}
init => { self.sr-restore(); }
changed sr-armed => { self.sr-restore(); }
changed viewport-height => { self.sr-restore(); }
changed viewport-y => {
NavState.report-scroll(self.viewport-y);
if (-self.viewport-y + self.height >= self.viewport-height - 600px
&& ArtistReleasesState.has-more
&& !ArtistReleasesState.load-more-loading
&& !ArtistReleasesState.loading) {
ArtistReleasesActions.load-more();
}
}
page := VerticalLayout {
width: parent.width;
padding-left: 32px;
padding-right: 32px;
padding-top: 8px;
padding-bottom: 100px;
spacing: 0px;
alignment: start;
if ArtistReleasesState.loading: HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
Text {
text: @tr("Loading…");
color: Theme.text-muted;
font-size: 14px;
horizontal-alignment: center;
}
}
}
if !ArtistReleasesState.loading && ArtistReleasesState.load-error: HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: center;
spacing: 12px;
Text {
text: @tr("Failed to load Library");
color: Theme.text-secondary;
font-size: 14px;
horizontal-alignment: center;
}
Rectangle {
width: retry-row.preferred-width;
height: 34px;
border-radius: 6px;
background: retry-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
retry-row := HorizontalLayout {
padding-left: 16px;
padding-right: 16px;
Text {
text: @tr("Retry");
color: Theme.text-primary;
font-size: 13px;
font-weight: Typography.medium;
vertical-alignment: center;
}
}
retry-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
ArtistReleasesActions.retry();
}
}
}
}
}
if !ArtistReleasesState.loading && !ArtistReleasesState.load-error
&& ArtistReleasesState.albums.length == 0: HorizontalLayout {
alignment: center;
Text {
text: @tr("No releases found.");
color: Theme.text-muted;
font-size: 14px;
}
}
if !ArtistReleasesState.loading && !ArtistReleasesState.load-error: AlbumCollectionView {
albums: ArtistReleasesState.albums;
view-mode: "grid";
is-grouped: false;
show-source: false;
card-width: root.album-w;
card-height: root.album-h;
card-gap: root.gap;
// Viewport windowing (Phase 1): only cards near the
// visible band mount. The offset is the page's
// padding-top (8px) — the loading / error branches are
// mutually exclusive with the grid, so nothing else
// sits above it; the grid's one-viewport margin absorbs
// any small error.
windowed: true;
visible-top: -flick.viewport-y;
visible-height: flick.height;
content-offset: 8px;
// Element windowing only; artwork stays per-page —
// pages arrive index-keyed with covers already on the
// model (id-keyed banding is a future migration).
open-album(id) => { root.open-album(id); }
open-artist(id) => { root.open-artist(id); }
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
}
if ArtistReleasesState.load-more-loading: HorizontalLayout {
alignment: center;
Rectangle { height: 24px; }
LoadingSpinner { size: 28px; }
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
y: 56px;
width: 14px;
height: parent.height - 56px;
viewport-height: flick.viewport-height;
visible-height: flick.height;
viewport-y <=> flick.viewport-y;
auto-hide: true;
hover-active: ShellState.pointer-in-window
&& ShellState.pointer-x >= flick.absolute-position.x
&& ShellState.pointer-x <= flick.absolute-position.x + flick.width
&& ShellState.pointer-y >= flick.absolute-position.y
&& ShellState.pointer-y <= flick.absolute-position.y + flick.height;
}
}
-142
View File
@@ -1,142 +0,0 @@
// Wrapping grid of AlbumCards for the artist page's release sections
// (Albums, EPs & Singles, Live, Compilations, …).
//
// The discography lays these as grids — never as horizontally paginated
// carousels — so a long discography wraps into rows and the whole shape
// stays scannable. Pattern mirrors `discover/GridSection`: absolute
// positioning inside a Rectangle whose height comes from the row count.
//
// Header carries a per-section sort + a "See discography" link to the
// dedicated page. No item COUNT is shown — the bucket preview is partial
// (has_more), so a count would mislead (e.g. "Live 7" when there are more).
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { ArtistReleaseSection } from "../state.slint";
import { AlbumCard } from "../discover/AlbumCard.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
export component ReleaseGrid inherits VerticalLayout {
in property <ArtistReleaseSection> section;
// Hide the title/sort/see-all header row — used by the collapsible
// "Other" section, which supplies its own header + collapse toggle.
in property <bool> hide-header: false;
callback album-clicked(string);
callback media-action(string, string, string);
callback set-section-sort(string /* release-type */, string /* sort */);
callback load-more-section(string /* release-type */);
callback open-releases(string /* release-type */);
property <length> card-width: 200px;
property <length> card-height: 246px;
property <length> gap: 24px;
property <int> count: root.section.albums.length;
// sort-by string -> QbzSelect index.
property <int> sort-index:
root.section.sort-by == "newest" ? 1 :
root.section.sort-by == "oldest" ? 2 :
root.section.sort-by == "title-asc" ? 3 :
root.section.sort-by == "title-desc" ? 4 : 0;
spacing: 12px;
// --- Header: title + see-discography link + sort ---------------------
if !root.hide-header: HorizontalLayout {
spacing: 12px;
Text {
text: root.section.title;
color: Theme.text-primary;
font-size: Typography.heading;
font-weight: Typography.semibold;
vertical-alignment: center;
horizontal-stretch: 1;
}
Rectangle {
width: see-all-text.preferred-width;
height: 28px;
see-all-text := Text {
text: @tr("See discography");
color: see-all-ta.has-hover ? Theme.text-primary : Theme.text-secondary;
font-size: 13px;
vertical-alignment: center;
}
see-all-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.open-releases(root.section.release-type);
}
}
}
QbzSelect {
sm: true;
menu-width: 150px;
current-index: root.sort-index;
options: [
@tr("Default"),
@tr("Newest"),
@tr("Oldest"),
@tr("A–Z"),
@tr("Z–A"),
];
selected(idx) => {
root.set-section-sort(
root.section.release-type,
idx == 1 ? "newest"
: idx == 2 ? "oldest"
: idx == 3 ? "title-asc"
: idx == 4 ? "title-desc"
: "default");
}
}
}
// --- Grid: absolute-positioned cards inside a sized Rectangle --------
grid := Rectangle {
// Columns recomputed per resize so the grid flows from 5 wide
// at large viewports down to 2 on narrow ones.
property <int> columns: Math.max(
1,
Math.floor((self.width + root.gap) / (root.card-width + root.gap)));
property <int> rows: Math.ceil(root.count / self.columns);
height: self.rows > 0
? self.rows * root.card-height + Math.max(0, self.rows - 1) * root.gap
: 0px;
for album[i] in root.section.albums: AlbumCard {
x: Math.mod(i, grid.columns) * (root.card-width + root.gap);
y: Math.floor(i / grid.columns) * (root.card-height + root.gap);
width: root.card-width;
height: root.card-height;
album: album;
clicked(id) => {
root.album-clicked(id);
}
media-action(kind, id, action) => {
root.media-action(kind, id, action);
}
}
}
// --- Load more (per-section, capped to 4 pages on the index) ---------
if root.section.has-more: HorizontalLayout {
alignment: center;
Rectangle {
width: load-more-text.preferred-width + 24px;
height: 28px;
load-more-text := Text {
text: @tr("Load more");
color: load-more-ta.has-hover ? Theme.text-primary : Theme.text-secondary;
font-size: 13px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
load-more-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.load-more-section(root.section.release-type);
}
}
}
}
}
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="128" width="128" version="1.1" id="_x32_" viewBox="0 0 512 512" xml:space="preserve">
<style type="text/css">
.st0{fill:#000000;}
</style>
<g>
<path class="st0" d="M508.797,215.893c-9.977-62.966-42.222-117.038-87.362-155.217C376.304,22.48,318.172,0,256.298,0 c-13.335,0-26.842,1.043-40.405,3.195c-62.966,9.985-117.038,42.23-155.217,87.37C22.48,135.689,0,193.829,0,255.703 c0,13.334,1.043,26.841,3.204,40.404c9.976,62.966,42.222,117.039,87.362,155.218C135.697,489.52,193.829,512,255.703,512 c13.334,0,26.841-1.043,40.404-3.204c62.966-9.976,117.039-42.222,155.218-87.361C489.52,376.304,512,318.172,512,256.298 C512,242.963,510.957,229.456,508.797,215.893z M425.274,399.387c-33.166,39.165-79.838,67.025-134.514,75.714 c-11.794,1.866-23.5,2.771-35.057,2.771c-53.56,0-103.924-19.456-143.09-52.598c-39.165-33.166-67.024-79.838-75.714-134.514 c-1.866-11.794-2.771-23.5-2.771-35.057c0-53.56,19.448-103.924,52.598-143.09c33.166-39.173,79.838-67.025,134.514-75.714 c11.794-1.866,23.499-2.771,35.057-2.771c53.56,0,103.924,19.448,143.089,52.598c39.165,33.166,67.025,79.838,75.714,134.514 c1.866,11.794,2.771,23.499,2.771,35.057C477.872,309.858,458.416,360.222,425.274,399.387z"/>
<path class="st0" d="M345.77,309.735l80.132,47.919c6.284-10.514,11.607-21.575,15.772-33.174l-87.508-32.286 C351.916,298.332,349.047,304.209,345.77,309.735z"/>
<path class="st0" d="M287.899,355.69l28.414,88.853c35.587-11.322,66.21-32.212,89.505-59.192l-70.628-60.953 C322.867,338.662,306.728,349.723,287.899,355.69z"/>
<path class="st0" d="M70.35,187.552l87.435,32.286c2.25-6.137,5.119-12.015,8.396-17.541l-80.067-47.854 C79.839,164.893,74.516,175.954,70.35,187.552z"/>
<path class="st0" d="M224.135,156.311l-28.407-88.853c-35.579,11.386-66.275,32.22-89.562,59.273l70.628,60.936 C189.102,173.354,205.314,162.285,224.135,156.311z"/>
<path class="st0" d="M250.804,223.254c-18.087,2.869-30.42,19.864-27.55,37.942c2.869,18.095,19.856,30.419,37.942,27.55 c18.079-2.87,30.419-19.848,27.55-37.942C285.877,232.725,268.891,220.384,250.804,223.254z"/>
<path class="st0" d="M339.062,256.086c0-4.328-0.342-8.714-1.044-13.106c-6.504-40.894-41.757-70.041-81.933-70.041 c-4.328,0-8.706,0.342-13.106,1.035c-40.893,6.513-70.034,41.765-70.041,81.933c0,4.328,0.342,8.706,1.035,13.107 c6.52,40.893,41.765,70.041,81.941,70.041c4.328,0,8.705-0.334,13.107-1.035C309.906,331.514,339.062,296.27,339.062,256.086z M265.801,317.796c-3.325,0.53-6.626,0.783-9.886,0.783c-30.199,0-56.836-21.968-61.711-52.778c-0.53-3.334-0.79-6.634-0.79-9.895 c0-30.199,21.975-56.836,52.785-61.702c3.325-0.53,6.627-0.79,9.887-0.79c30.207,0,56.844,21.975,61.71,52.777 c0.53,3.334,0.791,6.636,0.791,9.896C318.587,286.293,296.612,312.93,265.801,317.796z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
-293
View File
@@ -1,293 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48.4249878 48.4255981" style="enable-background:new 0 0 48.4249878 48.4255981;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;fill:#FFFFFF;}
.st2{display:inline;}
.st3{fill:#FFFFFF;}
.st4{clip-path:url(#SVGID_2_);fill:url(#SVGID_3_);}
.st5{clip-path:url(#SVGID_5_);fill:url(#SVGID_6_);}
.st6{clip-path:url(#SVGID_8_);fill:url(#SVGID_9_);}
.st7{fill:#6A4018;}
.st8{clip-path:url(#SVGID_11_);fill:url(#SVGID_12_);}
.st9{clip-path:url(#SVGID_14_);fill:url(#SVGID_15_);}
.st10{clip-path:url(#SVGID_17_);fill:url(#SVGID_18_);}
.st11{clip-path:url(#SVGID_20_);fill:url(#SVGID_21_);}
</style>
<g class="st0">
<polygon class="st1" points="48.4248047,60.5088501 0.0097961,60.5368652 0.0097961,48.4248657 48.4248047,48.4248657 "/>
<polygon class="st2" points="48.4248047,60.5088501 0.0097961,60.5368652 0.0097961,48.4248657 48.4248047,48.4248657 "/>
<g class="st2">
<polygon class="st3" points="7.6476746,51.9105225 7.6136475,51.9105225 6.7856445,57.621582 5.0166626,57.621582
3.9126587,50.6075439 5.3036499,50.6075439 5.9666443,56.0345459 5.9856567,56.0345459 6.732666,50.6075439 8.5496521,50.6075439
9.2836609,56.0345459 9.2916565,56.0345459 9.956665,50.6075439 11.3336182,50.6075439 10.2156677,57.621582 8.4226685,57.621582
"/>
<rect x="12.6079712" y="50.6080322" class="st3" width="1.2849731" height="7.0139771"/>
<path class="st3" d="M15.5543823,50.6078491h2.2130127c1.0039673,0,2.0079956,0.4530029,2.0079956,1.8979492
c0,0.848999-0.4020386,1.3780518-0.9780273,1.6280518v0.0239868c0.3590088,0.157959,0.4960327,0.4110107,0.6940308,1.2600098
c0.1859741,0.7559814,0.3330078,1.4419556,0.5,2.2039795h-1.3650513c-0.098999-0.5310059-0.2679443-1.401001-0.3839722-1.8369751
c-0.1599731-0.7560425-0.3480225-1.0090332-1.0029907-1.0090332h-0.3759766v2.8460083h-1.309021V50.6078491z
M16.8634033,53.6517944h0.6089478c0.5230103,0,1.0020142-0.2289429,1.0020142-0.9639893
c0-0.7769775-0.5219727-0.9379883-1.0529785-0.9379883h-0.5579834V53.6517944z"/>
<polygon class="st3" points="21.3668823,50.607605 24.7418823,50.607605 24.7418823,51.7496338 22.6618652,51.7496338
22.6618652,53.4726563 24.5788574,53.4726563 24.5788574,54.5906372 22.6618652,54.6176147 22.6618652,56.4736328
24.8438721,56.4736328 24.8438721,57.6216431 21.3668823,57.6216431 "/>
<polygon class="st3" points="26.3867188,50.607605 27.6746826,50.607605 27.6746826,56.4736328 29.8087158,56.4736328
29.8087158,57.6216431 26.3867188,57.6216431 "/>
<polygon class="st3" points="31.2047119,50.607605 34.5976563,50.607605 34.5976563,51.7496338 32.5136719,51.7496338
32.5136719,53.4726563 34.4146729,53.4726563 34.4146729,54.5906372 32.5136719,54.5906372 32.5136719,56.4736328
34.6676636,56.4736328 34.6676636,57.6216431 31.2047119,57.6216431 "/>
<path class="st3" d="M39.250061,51.8395996c-0.3650513-0.1329956-0.6610107-0.2039795-1.0570068-0.2039795
c-0.4570313,0-0.9250488,0.2039795-0.9250488,0.7780151c0,0.4169922,0.2280273,0.6419678,0.9380493,1.0780029
c0.9759521,0.5739746,1.6300049,1.0119629,1.6300049,2.2009888c0,1.3980103-1.052002,2.0440063-2.257019,2.0440063
c-0.507019,0-1.1240234-0.0960083-1.6170044-0.2750244l0.2630005-1.1479492
c0.4680176,0.1849976,0.9039917,0.2709961,1.3289795,0.2709961c0.5910034,0,0.9910278-0.2709961,0.9910278-0.8210449
c0-0.506958-0.3569946-0.8029785-1.0579834-1.2160034c-0.8950195-0.4819946-1.4980469-1.0310059-1.4980469-2.0629883
c0-1.4909668,1.0830078-1.9940186,2.1360474-1.9940186c0.5219727,0,0.9919434,0.0680542,1.3839722,0.2030029L39.250061,51.8395996
z"/>
<path class="st3" d="M44.0172119,51.8395996c-0.3500366-0.1329956-0.6590576-0.2039795-1.0510254-0.2039795
c-0.4570313,0-0.9260254,0.2039795-0.9260254,0.7559814c0,0.4390259,0.2290039,0.6640015,0.9520264,1.1000366
c0.9649658,0.5739746,1.5889893,1.0119629,1.5889893,2.2009888c0,1.3980103-1.039978,2.0440063-2.2529907,2.0440063
c-0.4910278,0-1.1019897-0.0960083-1.5839844-0.2750244l0.2749634-1.1479492
c0.4390259,0.1849976,0.8920288,0.2709961,1.309021,0.2709961c0.5709839,0,0.9819946-0.2709961,0.9819946-0.8210449
c0-0.506958-0.3439941-0.8029785-1.065979-1.2160034c-0.901001-0.4819946-1.5-1.0310059-1.5-2.0629883
c0-1.4909668,1.0929565-1.9940186,2.1389771-1.9940186c0.5219727,0,1.0159912,0.0680542,1.3930054,0.2030029
L44.0172119,51.8395996z"/>
</g>
</g>
<g>
<path d="M41.7749634,14.1011963c-1.2600098-0.5539551-1.6170044-0.8739624-1.6170044-1.5819702
c0-0.9199829,0.7649536-1.2579956,1.5019531-1.2579956c0.6420288,0,1.4120483,0.177002,2.06604,0.4309692l0.401001-1.6709595
c-0.6900024-0.3010254-1.5610352-0.4840088-2.5370483-0.4840088c-1.6729736,0-3.4299927,0.9379883-3.4299927,3.0750122
c0,1.5319824,0.94104,2.3599854,2.5840454,3.0949707c1.2630005,0.592041,1.7909546,1.007019,1.7909546,1.809021
c0,0.8930054-0.6679688,1.2359619-1.5539551,1.2359619c-0.7740479,0-1.6670532-0.1799927-2.5310059-0.5459595l-0.3920288,1.7160034
c0.8710327,0.3460083,1.9949951,0.5509644,2.9230347,0.5509644c1.6900024,0,3.5479736-0.8009644,3.5479736-3.0709839
C44.5289307,15.6342163,43.4369507,14.8822021,41.7749634,14.1011963"/>
<path d="M33.770813,9.5367432c-2.6090088,0-3.9619751,2.4330444-3.9619751,5.5050049
c0,3.1640015,1.5310059,5.4320068,4.5939941,5.4320068c0.8619995,0,1.6459961-0.1589966,2.1539917-0.3659668l-0.242981-1.651001
c-0.5709839,0.2059937-1.1790161,0.2959595-1.809021,0.2959595c-1.6209717,0-2.6350098-1.0749512-2.7449951-3.09198h5.572998
v-0.8709717C37.3328247,11.6497803,36.3718262,9.5367432,33.770813,9.5367432 M35.4588623,14.0057373h-3.6990356
c0.1359863-1.5789795,0.7160034-2.8409424,1.9920044-2.8409424c1.1779785,0,1.7070313,1.1709595,1.7070313,2.7529907V14.0057373z"
/>
<path d="M26.9191284,13.7351685v-0.026001c1.1640015-0.4550171,1.8520508-1.559021,1.8520508-3.1409912
c0-2.6340332-1.5890503-3.6229858-3.4370117-3.6229858l-3.848999,0.0229492v13.4620361h1.8460083v-5.7349854h1.1459961
c1.3859863,0,1.7039795,0.4609985,2.0369873,2.0199585c0.1950073,0.8699951,0.5529785,2.6799927,0.7420044,3.7150269h2.0219727
c-0.3410034-1.4700317-0.617981-2.7520142-0.9389648-4.197998C27.9751587,14.6031494,27.663147,14.0061646,26.9191284,13.7351685
M24.8101807,12.8851318h-1.4790039V8.736145h1.5379639c1.0050049,0,2.0250244,0.342041,2.0250244,2.0629883
C26.894165,12.335144,26.0111694,12.8851318,24.8101807,12.8851318"/>
<rect x="17.7680054" y="12.6350098" width="2.1149902" height="2.0369873"/>
<rect x="14.2819824" y="6.9680176" width="1.8839722" height="1.9249878"/>
<rect x="14.2730103" y="10.7529907" width="1.8770142" height="9.677002"/>
<polygon points="10.364502,12.6348267 6.1434937,12.6348267 6.1434937,6.9678345 4.2354736,6.9678345 4.2354736,20.4308472
6.1434937,20.4308472 6.1434937,14.4208374 10.364502,14.4208374 10.364502,20.4308472 12.2524414,20.4308472
12.2524414,6.9678345 10.364502,6.9678345 "/>
<path d="M24.2147217,29.095459c-8.0780029,0-15.7089539-1.992981-22.4049683-5.5v23.0170288l44.8029785-0.0250244V23.595459
C39.916748,27.102478,32.2977295,29.095459,24.2147217,29.095459 M10.17276,42.3274536l-0.7160034-2.2049561H6.3267517
l-0.7089844,2.2049561H3.8527527l2.9330139-9.0119629h2.269989l2.8990173,9.0119629H10.17276z M20.2657471,39.6624756
c0,1.8380127-1.3859863,2.8259888-3.2579956,2.8259888c-1.8430176,0-3.2410278-0.9879761-3.2410278-2.8259888v-6.3469849h1.6660156
V39.4375c0,1.1029663,0.6610107,1.5559692,1.565979,1.5559692c0.9050293,0,1.6060181-0.4810181,1.6060181-1.5559692v-6.1220093
h1.6610107V39.6624756z M25.8157349,42.3274536h-2.7529907v-9.0119629h2.7529907
c2.6600342,0,4.1010132,1.697998,4.1010132,4.4679565C29.916748,40.5614624,28.475769,42.3274536,25.8157349,42.3274536
M34.1607666,42.3274536h-1.6470337v-9.0119629h1.6470337V42.3274536z M40.5587769,42.4634399
c-2.434021,0-3.7790527-1.9229736-3.7790527-4.6309814s1.3270264-4.6799927,3.7790527-4.6799927
c2.506958,0,3.8199463,1.9719849,3.8199463,4.6799927S43.0377197,42.4634399,40.5587769,42.4634399"/>
<polygon points="6.7034912,38.6351318 9.055481,38.6351318 7.8884888,34.7351685 "/>
<path d="M25.585083,34.7817993h-0.8700562v6.0510254h0.8700562c1.8469849,0,2.6709595-1.1879883,2.6709595-3.0250244
C28.2560425,35.9988403,27.4320679,34.7817993,25.585083,34.7817993"/>
<path d="M40.5587769,34.6429443c-1.4580078,0-2.1360474,1.4020386-2.1360474,3.1890259
c0,1.7659912,0.6950073,3.1619873,2.1700439,3.1619873c1.4909668,0,2.1459961-1.3959961,2.1459961-3.1619873
C42.7387695,36.0449829,42.0497437,34.6429443,40.5587769,34.6429443"/>
</g>
<g>
<defs>
<polygon id="SVGID_1_" points="0,0 0.2749939,0.2750244 48.1409912,0.2750244 48.4249878,0 "/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-276.0588379" y1="452.5148621" x2="-275.0588074" y2="452.5148621" gradientTransform="matrix(48.4245529 -0.0025294 0.0025294 48.4245529 13366.8808594 -21913.3886719)">
<stop offset="0" style="stop-color:#DDC77C"/>
<stop offset="1" style="stop-color:#B4802C"/>
</linearGradient>
<rect x="-0.0000306" y="-0.0012512" class="st4" width="48.4249878" height="0.2774963"/>
</g>
<g>
<defs>
<path id="SVGID_4_" d="M24.2139893,28.4770508c-8.0770264,0-15.7080078-1.9720459-22.4039917-5.4820557v0.6000366
c6.6959839,3.507019,14.3269653,5.5,22.4039917,5.5c8.0830078,0,15.7030029-1.992981,22.3989868-5.5v-0.6000366
C39.9169922,26.5050049,32.2969971,28.4770508,24.2139893,28.4770508"/>
</defs>
<clipPath id="SVGID_5_">
<use xlink:href="#SVGID_4_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-276.4433289" y1="453.0949707" x2="-275.4433289" y2="453.0949707" gradientTransform="matrix(44.8038025 -0.0023356 0.0023356 44.8038025 12386.4638672 -20274.9785156)">
<stop offset="0" style="stop-color:#472613"/>
<stop offset="0.0408205" style="stop-color:#5C3A1A"/>
<stop offset="0.0949331" style="stop-color:#704F21"/>
<stop offset="0.1483985" style="stop-color:#7D5B25"/>
<stop offset="0.2" style="stop-color:#815F26"/>
<stop offset="0.2510605" style="stop-color:#8A6527"/>
<stop offset="0.3343309" style="stop-color:#A2752A"/>
<stop offset="0.438834" style="stop-color:#CA8F30"/>
<stop offset="0.5" style="stop-color:#E4A133"/>
<stop offset="0.5320022" style="stop-color:#E8A73C"/>
<stop offset="0.584035" style="stop-color:#F4B754"/>
<stop offset="0.6" style="stop-color:#F8BD5D"/>
<stop offset="0.6378715" style="stop-color:#EFB659"/>
<stop offset="0.6996277" style="stop-color:#D7A34E"/>
<stop offset="0.777076" style="stop-color:#AF843B"/>
<stop offset="0.85" style="stop-color:#846227"/>
<stop offset="0.8968558" style="stop-color:#815E26"/>
<stop offset="0.9453847" style="stop-color:#7A5123"/>
<stop offset="0.9945391" style="stop-color:#6D3D1E"/>
<stop offset="1" style="stop-color:#6B3A1D"/>
</linearGradient>
<rect x="1.8097992" y="22.993866" class="st5" width="44.803299" height="6.1023865"/>
</g>
<g>
<defs>
<polygon id="SVGID_7_" points="0,0 0.0090027,48.4250488 48.4249878,48.4250488 48.4249878,0 "/>
</defs>
<clipPath id="SVGID_8_">
<use xlink:href="#SVGID_7_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="-266.1282043" y1="453.7897644" x2="-265.1282043" y2="453.7897644" gradientTransform="matrix(17.786274 38.1375618 -38.1375618 17.786274 22052.6953125 2078.0583496)">
<stop offset="0" style="stop-color:#F9E1B4"/>
<stop offset="0.2" style="stop-color:#FAD37E"/>
<stop offset="0.6" style="stop-color:#F6A936"/>
<stop offset="0.7426466" style="stop-color:#DF9430"/>
<stop offset="0.8883532" style="stop-color:#CF852C"/>
<stop offset="1" style="stop-color:#C9802A"/>
</linearGradient>
<polygon class="st6" points="-18.5495605,8.6509399 8.6509094,66.9746094 66.9744873,39.7741089 39.7739868,-18.5494995 "/>
</g>
<g>
<path d="M41.7749634,14.1011963c-1.2600098-0.5539551-1.6170044-0.8739624-1.6170044-1.5819702
c0-0.9199829,0.7649536-1.2579956,1.5019531-1.2579956c0.6420288,0,1.4120483,0.177002,2.06604,0.4309692l0.401001-1.6709595
c-0.6900024-0.3010254-1.5610352-0.4840088-2.5370483-0.4840088c-1.6729736,0-3.4299927,0.9379883-3.4299927,3.0750122
c0,1.5319824,0.94104,2.3599854,2.5840454,3.0949707c1.2630005,0.5690308,1.7909546,1.007019,1.7909546,1.809021
c0,0.8930054-0.6679688,1.2359619-1.5539551,1.2359619c-0.7740479,0-1.6670532-0.1799927-2.5310059-0.5459595l-0.3920288,1.7160034
c0.8710327,0.3460083,1.9949951,0.5509644,2.9230347,0.5509644c1.6900024,0,3.5479736-0.8009644,3.5479736-3.0709839
C44.5289307,15.6342163,43.4369507,14.8822021,41.7749634,14.1011963"/>
<path d="M33.770813,9.5367432c-2.6090088,0-3.9619751,2.4330444-3.9619751,5.5050049
c0,3.1640015,1.5310059,5.4320068,4.5939941,5.4320068c0.8619995,0,1.6459961-0.1589966,2.1539917-0.3659668l-0.242981-1.651001
c-0.5709839,0.2059937-1.1790161,0.2959595-1.809021,0.2959595c-1.6209717,0-2.6350098-1.0749512-2.7449951-3.09198h5.572998
v-0.8709717C37.3328247,11.6497803,36.3718262,9.5367432,33.770813,9.5367432 M35.4588623,14.0057373h-3.6990356
c0.1359863-1.5789795,0.7160034-2.8409424,1.9920044-2.8409424c1.1779785,0,1.7070313,1.1709595,1.7070313,2.7529907V14.0057373z"
/>
<path d="M26.9251709,13.7351685v-0.026001c1.157959-0.4550171,1.8460083-1.559021,1.8460083-3.1409912
c0-2.6340332-1.5890503-3.6229858-3.4370117-3.6229858l-3.848999,0.0229492v13.4620361h1.8460083v-5.7349854h1.1459961
c1.3859863,0,1.7039795,0.4609985,2.0369873,2.0199585c0.1950073,0.8699951,0.5529785,2.6799927,0.7420044,3.7150269h2.0219727
c-0.3410034-1.4700317-0.617981-2.7520142-0.9389648-4.197998C27.9751587,14.6031494,27.663147,14.0061646,26.9251709,13.7351685
M24.8101807,12.8851318h-1.4790039V8.736145h1.5379639c1.0050049,0,2.0250244,0.342041,2.0250244,2.0629883
C26.894165,12.335144,26.0111694,12.8851318,24.8101807,12.8851318"/>
<rect x="17.7680054" y="12.6350098" width="2.1149902" height="2.0369873"/>
<rect x="14.2819824" y="6.9680176" width="1.8839722" height="1.9249878"/>
<rect x="14.2730103" y="10.7529907" width="1.8770142" height="9.677002"/>
<polygon points="10.364502,12.6348267 6.1434937,12.6348267 6.1434937,6.9678345 4.2354736,6.9678345 4.2354736,20.4308472
6.1434937,20.4308472 6.1434937,14.4208374 10.364502,14.4208374 10.364502,20.4308472 12.2524414,20.4308472
12.2524414,6.9678345 10.364502,6.9678345 "/>
<path d="M24.2147217,29.095459c-8.0780029,0-15.7089539-1.992981-22.4049683-5.5v23.0170288l44.8029785-0.0250244V23.595459
C39.916748,27.102478,32.2977295,29.095459,24.2147217,29.095459 M10.17276,42.3274536l-0.7160034-2.2049561H6.3267517
l-0.7089844,2.2049561H3.8527527l2.9330139-9.0119629h2.269989l2.9139709,9.0119629H10.17276z M20.2657471,39.6624756
c0,1.8380127-1.3859863,2.8259888-3.2579956,2.8259888c-1.8430176,0-3.2410278-0.9879761-3.2410278-2.8259888v-6.3469849h1.6660156
V39.4375c0,1.1029663,0.6610107,1.5559692,1.565979,1.5559692c0.9050293,0,1.6060181-0.4810181,1.6060181-1.5559692v-6.1220093
h1.6610107V39.6624756z M25.8157349,42.3274536h-2.7529907v-9.0119629h2.7529907
c2.6600342,0,4.1010132,1.697998,4.1010132,4.4679565C29.916748,40.5614624,28.475769,42.3274536,25.8157349,42.3274536
M34.1607666,42.3274536h-1.6470337v-9.0119629h1.6470337V42.3274536z M40.5587769,42.4634399
c-2.434021,0-3.7790527-1.9229736-3.7790527-4.6309814s1.3270264-4.6799927,3.7790527-4.6799927
c2.506958,0,3.8199463,1.9719849,3.8199463,4.6799927S43.0377197,42.4634399,40.5587769,42.4634399"/>
<polygon points="6.7034912,38.6351318 9.055481,38.6351318 7.8884888,34.7351685 "/>
<path d="M25.585083,34.7817993h-0.8700562v6.0510254h0.8700562c1.8469849,0,2.6709595-1.1879883,2.6709595-3.0250244
C28.2560425,35.9988403,27.4320679,34.7817993,25.585083,34.7817993"/>
<path d="M40.5587769,34.6429443c-1.4580078,0-2.1360474,1.4020386-2.1360474,3.1890259
c0,1.7659912,0.6950073,3.1619873,2.1700439,3.1619873c1.4909668,0,2.1459961-1.3959961,2.1459961-3.1619873
C42.7387695,36.0449829,42.0497437,34.6429443,40.5587769,34.6429443"/>
<polygon class="st7" points="0.2755737,48.12854 0.009552,48.4255981 48.4245605,48.4255981 48.147522,48.12854 "/>
</g>
<g>
<defs>
<polygon id="SVGID_10_" points="48.1409912,0.2750244 48.1439819,23.2990112 48.1480103,48.1280518 48.4249878,48.4250488
48.4249878,0 "/>
</defs>
<clipPath id="SVGID_11_">
<use xlink:href="#SVGID_10_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="-278.4791565" y1="450.0944519" x2="-277.4791565" y2="450.0944519" gradientTransform="matrix(-0.0025294 -48.4249802 -48.4249802 0.0025294 21843.3945313 -13438.0615234)">
<stop offset="0" style="stop-color:#815F26"/>
<stop offset="0.5941817" style="stop-color:#836128"/>
<stop offset="0.8079586" style="stop-color:#89682E"/>
<stop offset="0.9600694" style="stop-color:#947338"/>
<stop offset="1" style="stop-color:#98783C"/>
</linearGradient>
<rect x="48.1397095" y="-0.0000306" class="st8" width="0.2865295" height="48.4250793"/>
</g>
<g>
<defs>
<polygon id="SVGID_13_" points="0.0090027,48.4250488 0.2749939,48.1280518 0.2749939,0.2750244 0,0 "/>
</defs>
<clipPath id="SVGID_14_">
<use xlink:href="#SVGID_13_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="-264.122467" y1="450.0944519" x2="-263.122467" y2="450.0944519" gradientTransform="matrix(0.0025295 48.4249763 -48.4249763 0.0025295 21796.6171875 12788.9853516)">
<stop offset="0" style="stop-color:#CFAD75"/>
<stop offset="1" style="stop-color:#816328"/>
</linearGradient>
<rect x="-0.001297" y="0" class="st9" width="0.2775269" height="48.4250488"/>
</g>
<g>
<defs>
<polygon id="SVGID_16_" points="0,0 0.2749939,0.2750244 48.1409912,0.2750244 48.4249878,0 "/>
</defs>
<clipPath id="SVGID_17_">
<use xlink:href="#SVGID_16_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="-276.0588379" y1="452.5148621" x2="-275.0588074" y2="452.5148621" gradientTransform="matrix(48.4245529 -0.0025294 0.0025294 48.4245529 13366.8808594 -21913.3886719)">
<stop offset="0" style="stop-color:#DDC77C"/>
<stop offset="1" style="stop-color:#B4802C"/>
</linearGradient>
<rect x="-0.0000306" y="-0.0012512" class="st10" width="48.4249878" height="0.2774963"/>
</g>
<g>
<defs>
<path id="SVGID_19_" d="M24.2139893,28.4770508c-8.0770264,0-15.7080078-1.9720459-22.4039917-5.4820557v0.6000366
c6.6959839,3.507019,14.3269653,5.5,22.4039917,5.5c8.0830078,0,15.7030029-1.992981,22.3989868-5.5v-0.6000366
C39.9169922,26.5050049,32.2969971,28.4770508,24.2139893,28.4770508"/>
</defs>
<clipPath id="SVGID_20_">
<use xlink:href="#SVGID_19_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="-276.4433289" y1="453.0949707" x2="-275.4433289" y2="453.0949707" gradientTransform="matrix(44.8038025 -0.0023356 0.0023356 44.8038025 12386.4638672 -20274.9785156)">
<stop offset="0" style="stop-color:#472613"/>
<stop offset="0.0408205" style="stop-color:#5C3A1A"/>
<stop offset="0.0949331" style="stop-color:#704F21"/>
<stop offset="0.1483985" style="stop-color:#7D5B25"/>
<stop offset="0.2" style="stop-color:#815F26"/>
<stop offset="0.2510605" style="stop-color:#8A6527"/>
<stop offset="0.3343309" style="stop-color:#A2752A"/>
<stop offset="0.438834" style="stop-color:#CA8F30"/>
<stop offset="0.5" style="stop-color:#E4A133"/>
<stop offset="0.5320022" style="stop-color:#E8A73C"/>
<stop offset="0.584035" style="stop-color:#F4B754"/>
<stop offset="0.6" style="stop-color:#F8BD5D"/>
<stop offset="0.6378715" style="stop-color:#EFB659"/>
<stop offset="0.6996277" style="stop-color:#D7A34E"/>
<stop offset="0.777076" style="stop-color:#AF843B"/>
<stop offset="0.85" style="stop-color:#846227"/>
<stop offset="0.8968558" style="stop-color:#815E26"/>
<stop offset="0.9453847" style="stop-color:#7A5123"/>
<stop offset="0.9945391" style="stop-color:#6D3D1E"/>
<stop offset="1" style="stop-color:#6B3A1D"/>
</linearGradient>
<rect x="1.8097992" y="22.993866" class="st11" width="44.803299" height="6.1023865"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

@@ -1,7 +0,0 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" stroke="#000000" stroke-width="0.4800000000000001">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.048"/>
<g id="SVGRepo_iconCarrier"> <path d="M18,7 L20.5,7 C20.7761424,7 21,7.22385763 21,7.5 C21,7.77614237 20.7761424,8 20.5,8 L18,8 L18,10.5 C18,10.7761424 17.7761424,11 17.5,11 C17.2238576,11 17,10.7761424 17,10.5 L17,8 L14.5,8 C14.2238576,8 14,7.77614237 14,7.5 C14,7.22385763 14.2238576,7 14.5,7 L17,7 L17,4.5 C17,4.22385763 17.2238576,4 17.5,4 C17.7761424,4 18,4.22385763 18,4.5 L18,7 Z M11.5,7 C11.7761424,7 12,7.22385763 12,7.5 C12,7.77614237 11.7761424,8 11.5,8 L3.5,8 C3.22385763,8 3,7.77614237 3,7.5 C3,7.22385763 3.22385763,7 3.5,7 L11.5,7 Z M14.5,12 C14.7761424,12 15,12.2238576 15,12.5 C15,12.7761424 14.7761424,13 14.5,13 L3.5,13 C3.22385763,13 3,12.7761424 3,12.5 C3,12.2238576 3.22385763,12 3.5,12 L14.5,12 Z M20.5,17 C20.7761424,17 21,17.2238576 21,17.5 C21,17.7761424 20.7761424,18 20.5,18 L3.5,18 C3.22385763,18 3,17.7761424 3,17.5 C3,17.2238576 3.22385763,17 3.5,17 L20.5,17 Z"/> </g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="9" height="6" x="6" y="14" rx="2" />
<rect width="16" height="6" x="6" y="4" rx="2" />
<path d="M2 2v20" />
</svg>

Before

Width:  |  Height:  |  Size: 311 B

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Apple Music" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%" fill="url(#g)"/><linearGradient id="g" x1=".5" y1=".99" x2=".5" y2=".02"><stop offset="0" stop-color="#FA233B"/><stop offset="1" stop-color="#FB5C74"/></linearGradient><path fill="#ffffff" d="M199 359V199q0-9 10-11l138-28q11-2 12 10v122q0 15-45 20c-57 9-48 105 30 79 30-11 35-40 35-69V88s0-20-17-15l-170 35s-13 2-13 18v203q0 15-45 20c-57 9-48 105 30 79 30-11 35-40 35-69"/></svg>

Before

Width:  |  Height:  |  Size: 636 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>

Before

Width:  |  Height:  |  Size: 238 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m21 16-4 4-4-4" />
<path d="M17 20V4" />
<path d="m3 8 4-4 4 4" />
<path d="M7 4v16" />
</svg>

Before

Width:  |  Height:  |  Size: 289 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 10v3" />
<path d="M6 6v11" />
<path d="M10 3v18" />
<path d="M14 8v7" />
<path d="M18 5v13" />
<path d="M22 10v3" />
</svg>

Before

Width:  |  Height:  |  Size: 325 B

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
<line x1="12" x2="12" y1="19" y2="22" />
</svg>

Before

Width:  |  Height:  |  Size: 337 B

@@ -1,2 +0,0 @@
<?xml version="1.0" ?>
<svg fill="#ffffff" width="24" height="24" viewBox="0 0 200 200" data-name="Layer 1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"><title/><path d="M177.5,67.4a9.75,9.75,0,0,0-14-1.5c-10.5,8.5-20,14.5-29.5,17.5-9.5,3.5-20.5,5-34,5s-24.5-1.5-34-5-19-9-29.5-17.5c-4.5-3.5-10.5-3-14,1.5s-3,10.5,1.5,14A132.06,132.06,0,0,0,45,95.9l-8.5,14.5c-2.5,5-1,11,3.5,13.5,5,2.5,11,1,13.5-3.5L63,103.9a112.84,112.84,0,0,0,27,4.5v18a10,10,0,0,0,20,0v-18a106.6,106.6,0,0,0,29-5.5l10,17.5a9.86,9.86,0,0,0,17-10l-9-15.5a111.22,111.22,0,0,0,19-13.5C180.5,77.9,181,71.9,177.5,67.4Z"/></svg>

Before

Width:  |  Height:  |  Size: 595 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 7v14" />
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
</svg>

Before

Width:  |  Height:  |  Size: 354 B

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg fill="#d9dde2" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><path d="M1.742 11.982c0-5.668 4.61-10.278 10.276-10.278 1.824 0 3.537.48 5.025 1.317l.814-1.488A11.914 11.914 0 0 0 12.19.003h-.195C5.41.013.072 5.31 0 11.885V12a11.983 11.983 0 0 0 3.775 8.72l1.185-1.28a10.249 10.249 0 0 1-3.218-7.459zm18.62-8.577-1.154 1.24a10.25 10.25 0 0 1 3.088 7.337c0 5.666-4.61 10.276-10.276 10.276-1.783 0-3.46-.456-4.922-1.258l-.854 1.522A11.946 11.946 0 0 0 12 23.998c6.626 0 12.001-5.373 12.001-12a11.977 11.977 0 0 0-3.638-8.593zM1.963 11.982a10.03 10.03 0 0 0 3.146 7.295l1.18-1.276a8.295 8.295 0 0 1-2.586-6.019c0-4.586 3.73-8.315 8.315-8.315 1.483 0 2.875.391 4.082 1.075l.835-1.526a9.973 9.973 0 0 0-4.917-1.289C6.475 1.925 1.963 6.437 1.963 11.982zm18.37 0c0 4.586-3.73 8.315-8.315 8.315a8.273 8.273 0 0 1-3.962-1.005l-.852 1.516a10.006 10.006 0 0 0 4.814 1.229c5.543 0 10.055-4.512 10.055-10.055 0-2.808-1.157-5.347-3.017-7.173l-1.183 1.274a8.282 8.282 0 0 1 2.46 5.899zm-1.948 0a6.37 6.37 0 0 1-6.365 6.364 6.329 6.329 0 0 1-3.006-.756l-.848 1.507a8.039 8.039 0 0 0 3.854.977c4.464 0 8.095-3.63 8.095-8.094a8.07 8.07 0 0 0-2.39-5.738l-1.179 1.267a6.356 6.356 0 0 1 1.839 4.473zm-14.459 0c0 2.301.967 4.382 2.515 5.858l1.173-1.27a6.344 6.344 0 0 1-1.96-4.588 6.37 6.37 0 0 1 6.364-6.364 6.32 6.32 0 0 1 3.144.835l.83-1.517a8.055 8.055 0 0 0-3.974-1.048c-4.461 0-8.092 3.63-8.092 8.094zm12.53 0a4.438 4.438 0 0 1-4.438 4.437 4.42 4.42 0 0 1-2.061-.509l-.835 1.488a6.114 6.114 0 0 0 2.896.727 6.149 6.149 0 0 0 6.143-6.143 6.123 6.123 0 0 0-1.768-4.308l-1.162 1.25a4.43 4.43 0 0 1 1.224 3.058zm-10.581 0a6.12 6.12 0 0 0 1.888 4.425l1.157-1.25.014.014a4.419 4.419 0 0 1-1.355-3.187 4.436 4.436 0 0 1 4.437-4.437 4.4 4.4 0 0 1 2.217.598l.82-1.498a6.097 6.097 0 0 0-3.037-.806c-3.384-.005-6.141 2.753-6.141 6.141zm6.68 0a.538.538 0 0 1-1.074 0 .537.537 0 1 1 1.075 0zm-3.94 0a3.4 3.4 0 1 1 6.801 0 3.4 3.4 0 0 1-6.8 0zm.149 0a3.256 3.256 0 0 0 3.252 3.252 3.255 3.255 0 0 0 3.254-3.252 3.253 3.253 0 1 0-6.506 0z"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112.195 112.195" xml:space="preserve">
<g>
<path style="fill:#D51007;" d="M112.195,56.097c0,30.983-25.114,56.099-56.097,56.099C25.115,112.195,0,87.08,0,56.097C0,25.117,25.115,0,56.099,0C87.081,0,112.195,25.117,112.195,56.097z"/>
<path style="fill:#FFFFFF;" d="M80.487,52.141c-6.203-1.849-8.412-2.874-8.412-4.922c0-3.416,4.937-4.702,5.497-4.79c3.139-0.465,7.072,1.173,8.34,4.99l8.44-2.553c-2.65-8.385-10.487-12.099-18.099-10.975c-7.788,1.146-12.782,6.786-12.782,13.327c0,8.8,8.244,11.332,14.43,13.181c6.509,1.944,8.401,2.637,8.401,5.023c0,2.016-1.196,3.895-4.374,4.735c-6.32,1.664-14.586-0.672-16.887-4.523c-1.351-2.261-2.733-5.203-4.001-8.53c-3.766-9.866-8.926-23.371-23.764-23.371c-7.731,0-21.851,3.743-21.851,24.868c0,8.744,7.481,19.613,21.578,19.613c11.829,0,13.888-4.32,14.358-5.085l-3.861-7.555c-0.098,0.138-3.692,4.936-10.497,4.936c-11.107,0-12.735-11.776-12.735-11.909c0-10.967,4.687-16.238,13.008-16.238c8.094,0,11.382,7.04,15.48,17.759c1.397,3.683,2.922,6.966,4.65,9.865c3.657,6.116,11.55,8.091,19.473,8.091c2.418,0,5.2,0.137,7.351-0.432c7.093-1.878,10.905-6.178,10.905-12.224C95.137,56.218,86.852,54.044,80.487,52.141z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>MusicBrainz icon</title><defs><linearGradient id="mbHalf" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#ba478f"/><stop offset="0.5" stop-color="#ba478f"/><stop offset="0.5" stop-color="#eb743b"/><stop offset="1" stop-color="#eb743b"/></linearGradient></defs><path fill="url(#mbHalf)" d="M11.582 0L1.418 5.832v12.336L11.582 24V10.01L7.1 12.668v3.664c.01.111.01.225 0 .336-.103.435-.54.804-1 1.111-.802.537-1.752.509-2.166-.111-.413-.62-.141-1.631.666-2.168.384-.28.863-.399 1.334-.332V6.619c0-.154.134-.252.226-.308L11.582 3zm.836 0v6.162c.574.03 1.14.16 1.668.387a2.225 2.225 0 0 0 1.656-.717 1.02 1.02 0 1 1 1.832-.803l.004.006a1.022 1.022 0 0 1-1.295 1.197c-.34.403-.792.698-1.297.85.34.263.641.576.891.928a1.04 1.04 0 0 1 .777.125c.768.486.568 1.657-.318 1.857-.886.2-1.574-.77-1.09-1.539.02-.03.042-.06.065-.09a3.598 3.598 0 0 0-1.436-1.166 4.142 4.142 0 0 0-1.457-.369v4.01c.855.06 1.256.493 1.555.834.227.256.356.39.578.402.323.018.568.008.806 0a5.44 5.44 0 0 1 .895.022c.94-.017 1.272-.226 1.605-.446a2.533 2.533 0 0 1 1.131-.463 1.027 1.027 0 0 1 .12-.263 1.04 1.04 0 0 1 .105-.137c.023-.025.047-.044.07-.066a4.775 4.775 0 0 1 0-2.405l-.012-.01a1.02 1.02 0 1 1 .692.272h-.057a4.288 4.288 0 0 0 0 1.877h.063a1.02 1.02 0 1 1-.545 1.883l-.047-.033a1 1 0 0 1-.352-.442 1.885 1.885 0 0 0-.814.354 3.03 3.03 0 0 1-.703.365c.757.555 1.772 1.6 2.199 2.299a1.03 1.03 0 0 1 .256-.033 1.02 1.02 0 1 1-.545 1.88l-.047-.03a1.017 1.017 0 0 1-.27-1.376.72.72 0 0 1 .051-.072c-.445-.775-2.026-2.28-2.46-2.387a4.037 4.037 0 0 0-1.31-.117c-.24.008-.513.018-.866 0-.515-.027-.783-.333-1.043-.629-.26-.296-.51-.56-1.055-.611V18.5a1.877 1.877 0 0 0 .426-.135.333.333 0 0 1 .058-.027c.56-.267 1.421-.91 2.096-2.447a1.02 1.02 0 0 1-.27-1.344 1.02 1.02 0 1 1 .915 1.54 6.273 6.273 0 0 1-1.432 2.136 1.785 1.785 0 0 1 .691.306.667.667 0 0 0 .37.168 3.31 3.31 0 0 0 .888-.222 1.02 1.02 0 0 1 1.787-.79v-.005a1.02 1.02 0 0 1-.773 1.683 1.022 1.022 0 0 1-.719-.287 3.935 3.935 0 0 1-1.168.287h-.05a1.313 1.313 0 0 1-.71-.275c-.262-.177-.51-.345-1.402-.12a2.098 2.098 0 0 1-.707.2V24l10.164-5.832V5.832zm4.154 4.904a.352.352 0 0 0-.197.639l.018.01c.163.1.378.053.484-.108v-.002a.352.352 0 0 0-.303-.539zm-4.99 1.928L7.082 9.5v2l4.5-2.668zm8.385.38a.352.352 0 0 0-.295.165v.002a.35.35 0 0 0 .096.473l.013.01a.357.357 0 0 0 .487-.108.352.352 0 0 0-.301-.541zM16.09 8.647a.352.352 0 0 0-.277.163.355.355 0 0 0 .296.54c.482 0 .463-.73-.02-.703zm3.877 2.477a.352.352 0 0 0-.295.164.35.35 0 0 0 .094.475l.015.01a.357.357 0 0 0 .485-.11.352.352 0 0 0-.3-.539zm-4.375 3.594a.352.352 0 0 0-.291.172.35.35 0 0 0-.04.265.352.352 0 1 0 .33-.437zm4.375.789a.352.352 0 0 0-.295.164v.002a.352.352 0 0 0 .094.473l.015.01a.357.357 0 0 0 .485-.108.352.352 0 0 0-.3-.54zm-2.803 2.488v.002a.347.347 0 0 0-.223.084.352.352 0 0 0 .23.62.347.347 0 0 0 .23-.085.348.348 0 0 0 .12-.24.353.353 0 0 0-.35-.38.347.347 0 0 0-.007 0Z"/></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

-13
View File
@@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20v-9" />
<path d="M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z" />
<path d="M14.12 3.88 16 2" />
<path d="M21 21a4 4 0 0 0-3.81-4" />
<path d="M21 5a4 4 0 0 1-3.55 3.97" />
<path d="M22 13h-4" />
<path d="M3 21a4 4 0 0 1 3.81-4" />
<path d="M3 5a4 4 0 0 0 3.55 3.97" />
<path d="M6 13H2" />
<path d="m8 2 1.88 1.88" />
<path d="M9 7.13V6a3 3 0 1 1 6 0v1.13" />
</svg>

Before

Width:  |  Height:  |  Size: 591 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="14" x="3" y="5" rx="2" />
<path d="M7 15h4" />
<path d="M15 15h2" />
<path d="M7 11h2" />
<path d="M13 11h4" />
</svg>

Before

Width:  |  Height:  |  Size: 331 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2" />
<circle cx="8" cy="10" r="2" />
<path d="M8 12h8" />
<circle cx="16" cy="10" r="2" />
<path d="m6 20 .7-2.9A1.4 1.4 0 0 1 8.1 16h7.8a1.4 1.4 0 0 1 1.4 1l.7 3" />
</svg>

Before

Width:  |  Height:  |  Size: 407 B

@@ -1,2 +0,0 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M25.338,6l-1.559,5.285c-0.126,0.425 -0.516,0.717 -0.96,0.717l-14.138,-0c-0.444,-0 -0.834,-0.292 -0.96,-0.717l-1.559,-5.285l-1.162,0c-0.796,0 -1.559,0.316 -2.121,0.879c-0.563,0.562 -0.879,1.325 -0.879,2.121c0,3.462 -0,10.533 0,13.995c-0,0.796 0.316,1.559 0.879,2.122c0.562,0.562 1.325,0.878 2.121,0.878l22,0c0.796,0 1.559,-0.316 2.121,-0.878c0.563,-0.563 0.879,-1.326 0.879,-2.122l-0,-13.995c0,-0.796 -0.316,-1.559 -0.879,-2.121c-0.562,-0.563 -1.325,-0.879 -2.121,-0.879l-1.662,-0Zm-6.756,14l-5.118,0.001c0.341,-0.588 0.536,-1.272 0.536,-2c0,-2.207 -1.792,-3.999 -3.999,-3.999c-2.207,-0 -3.999,1.792 -3.999,3.999c-0,2.185 1.757,3.964 3.934,3.998c0.022,0.002 0.043,0.003 0.065,0.003l12.044,-0.002c2.207,-0 3.999,-1.792 3.999,-3.999c0,-2.207 -1.792,-3.999 -3.999,-3.999c-2.207,-0 -3.999,1.792 -3.999,3.999c-0,0.728 0.195,1.411 0.536,1.999Zm-8.581,-3.998c1.103,-0 1.999,0.895 1.999,1.999c-0,1.103 -0.896,1.999 -1.999,1.999c-1.104,0 -1.999,-0.896 -1.999,-1.999c-0,-1.104 0.895,-1.999 1.999,-1.999Zm12.044,-0c1.104,-0 1.999,0.895 1.999,1.999c0,1.103 -0.895,1.999 -1.999,1.999c-1.103,0 -1.999,-0.896 -1.999,-1.999c-0,-1.104 0.896,-1.999 1.999,-1.999Zm1.207,-10.002l-15.004,-0l1.18,4.002c0,-0 12.644,-0 12.644,-0l1.18,-4.002Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-6
View File
@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6" />
<path d="M2 12a9 9 0 0 1 8 8" />
<path d="M2 16a5 5 0 0 1 4 4" />
<line x1="2" x2="2.01" y1="20" y2="20" />
</svg>

Before

Width:  |  Height:  |  Size: 371 B

-18
View File
@@ -1,18 +0,0 @@
<!-- @license lucide-static v1.17.0 - ISC -->
<svg
class="lucide lucide-cat"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 5c.67 0 1.35.09 2 .26 1.78-2 5.03-2.84 6.42-2.26 1.4.58-.42 7-.42 7 .57 1.07 1 2.24 1 3.44C21 17.9 16.97 21 12 21s-9-3-9-7.56c0-1.25.5-2.4 1-3.44 0 0-1.89-6.42-.5-7 1.39-.58 4.72.23 6.5 2.23A9.04 9.04 0 0 1 12 5Z" />
<path d="M8 14v.5" />
<path d="M16 14v.5" />
<path d="M11.25 16.25h1.5L12 17l-.75-.75Z" />
</svg>

Before

Width:  |  Height:  |  Size: 606 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" x2="18" y1="20" y2="10" />
<line x1="12" x2="12" y1="20" y2="4" />
<line x1="6" x2="6" y1="20" y2="14" />
</svg>

Before

Width:  |  Height:  |  Size: 310 B

-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>

Before

Width:  |  Height:  |  Size: 215 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6" />
</svg>

Before

Width:  |  Height:  |  Size: 212 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m15 18-6-6 6-6" />
</svg>

Before

Width:  |  Height:  |  Size: 214 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 18 6-6-6-6" />
</svg>

Before

Width:  |  Height:  |  Size: 213 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m18 15-6-6-6 6" />
</svg>

Before

Width:  |  Height:  |  Size: 214 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" />
</svg>

Before

Width:  |  Height:  |  Size: 308 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.801 10A10 10 0 1 1 17 3.335" />
<path d="m9 11 3 3L22 4" />
</svg>

Before

Width:  |  Height:  |  Size: 261 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="2.5" fill="#ffffff" stroke="none" />
</svg>

Before

Width:  |  Height:  |  Size: 286 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<rect x="9" y="9" width="6" height="6" rx="1" />
</svg>

Before

Width:  |  Height:  |  Size: 271 B

-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
</svg>

Before

Width:  |  Height:  |  Size: 220 B

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>

Before

Width:  |  Height:  |  Size: 261 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 13v8l-4-4" />
<path d="m12 21 4-4" />
<path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284" />
</svg>

Before

Width:  |  Height:  |  Size: 317 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m2 2 20 20" />
<path d="M5.782 5.782A7 7 0 0 0 9 19h8.5a4.5 4.5 0 0 0 1.307-.193" />
<path d="M21.532 16.5A4.5 4.5 0 0 0 17.5 10h-1.79A7.008 7.008 0 0 0 10 5.07" />
</svg>

Before

Width:  |  Height:  |  Size: 364 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 13v8" />
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" />
<path d="m8 17 4-4 4 4" />
</svg>

Before

Width:  |  Height:  |  Size: 309 B

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 60.02 60.02" xml:space="preserve">
<g>
<path d="M59.766,15.896l-0.363-0.386h-1.837l0.409-3.34c0.043-0.424-0.097-0.848-0.382-1.163c-0.286-0.315-0.694-0.497-1.12-0.497
h-2.437V7c0-0.821-0.669-1.49-1.491-1.49h-2.509V2c0-0.821-0.669-1.49-1.491-1.49H11.526c-0.821,0-1.49,0.669-1.49,1.49v3.51h-2.51
c-0.821,0-1.49,0.669-1.49,1.49v3.51H3.599c-0.426,0-0.834,0.182-1.12,0.497c-0.285,0.315-0.425,0.739-0.379,1.185l0.406,3.318
h-1.37c-0.321,0-0.623,0.141-0.83,0.386L0,16.306l5.959,41.856c0.138,0.781,0.813,1.348,1.605,1.348h44.942
c0.791,0,1.467-0.565,1.611-1.38l5.886-41.352C60.059,16.462,59.972,16.141,59.766,15.896z M12.036,2.51h36v3h-36V2.51z
M8.036,7.51h2h40h2v3h-44V7.51z M4.153,12.51h1.883h48h1.882l-0.367,3H4.521L4.153,12.51z M29.536,54.51
c-11.304,0-20.5-7.626-20.5-17s9.196-17,20.5-17s20.5,7.626,20.5,17S40.84,54.51,29.536,54.51z"/>
<path d="M30.536,25.449c0-0.553-0.447-1-1-1c-3.993,0-7.778,1.21-10.946,3.499c-0.447,0.323-0.548,0.948-0.225,1.396
c0.195,0.271,0.501,0.414,0.812,0.414c0.203,0,0.407-0.062,0.585-0.189c2.824-2.041,6.204-3.12,9.774-3.12
C30.089,26.449,30.536,26.002,30.536,25.449z"/>
<path d="M30.536,29.2c0-0.553-0.447-1-1-1c-3.105,0-6.049,0.941-8.512,2.723c-0.447,0.324-0.548,0.949-0.225,1.396
c0.196,0.271,0.501,0.414,0.812,0.414c0.203,0,0.408-0.062,0.585-0.189c2.12-1.533,4.658-2.344,7.34-2.344
C30.089,30.2,30.536,29.753,30.536,29.2z"/>
<path d="M30.536,32.95c0-0.553-0.447-1-1-1c-2.229,0-4.339,0.68-6.104,1.967c-0.446,0.325-0.545,0.95-0.219,1.396
c0.195,0.269,0.5,0.411,0.809,0.411c0.204,0,0.41-0.063,0.588-0.192c1.42-1.035,3.124-1.582,4.926-1.582
C30.089,33.95,30.536,33.503,30.536,32.95z"/>
<path d="M39.4,45.638c-2.824,2.042-6.204,3.121-9.774,3.121c-0.553,0-1,0.447-1,1s0.447,1,1,1c3.994,0,7.779-1.21,10.946-3.5
c0.447-0.324,0.548-0.949,0.225-1.396C40.474,45.414,39.847,45.314,39.4,45.638z"/>
<path d="M28.626,46.008c0,0.553,0.447,1,1,1c3.106,0,6.05-0.942,8.512-2.724c0.448-0.324,0.548-0.949,0.225-1.396
c-0.325-0.448-0.95-0.548-1.396-0.225c-2.119,1.534-4.657,2.345-7.34,2.345C29.073,45.008,28.626,45.455,28.626,46.008z"/>
<path d="M28.626,42.257c0,0.553,0.447,1,1,1c2.23,0,4.341-0.68,6.104-1.967c0.445-0.325,0.544-0.951,0.218-1.397
c-0.324-0.444-0.949-0.544-1.397-0.218c-1.418,1.035-3.121,1.582-4.925,1.582C29.073,41.257,28.626,41.704,28.626,42.257z"/>
<ellipse cx="29.932" cy="37.494" rx="2.327" ry="1.689"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" />
</svg>

Before

Width:  |  Height:  |  Size: 327 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>

Before

Width:  |  Height:  |  Size: 311 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z"/><path d="M5 21h14"/></svg>

Before

Width:  |  Height:  |  Size: 431 B

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Deezer" role="img"
fill="none" stroke-width="20"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%"
fill="#231f20"/><path stroke="#f1796e" d="M78 241h64m-64 26h64"/><path stroke="#ec2529" d="M78 293h64m-64 26h64m-64 26h64"/><path stroke="#f6dd05" d="M148 293h64m-64 26h64m-64 26h64"/><path stroke="#e78dcc" d="M218 241h64m-64 26h64m-64-52h64m-64-26h64"/><path stroke="#d81996" d="M218 293h64m-64 26h64m-64 26h64"/><path stroke="#d6e465" d="M288 241h64m-64 26h64"/><path stroke="#adda1a" d="M288 293h64m-64 26h64m-64 26h64"/><path stroke="#b0d8e6" d="M358 241h64m-64 26h64m-64-52h64m-64-26h64m-64-26h64"/><path stroke="#56bbe9" d="M358 293h64m-64 26h64m-64 26h64"/></svg>

Before

Width:  |  Height:  |  Size: 852 B

-6
View File
@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 12c0-1.7.7-3.2 1.8-4.2" />
<circle cx="12" cy="12" r="2" />
<path d="M18 12c0 1.7-.7 3.2-1.8 4.2" />
<circle cx="12" cy="12" r="10" />
</svg>

Before

Width:  |  Height:  |  Size: 339 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" />
<circle cx="12" cy="12" r="5" />
<path d="M12 12h.01" />
</svg>

Before

Width:  |  Height:  |  Size: 298 B

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="3" />
</svg>

Before

Width:  |  Height:  |  Size: 255 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" x2="12" y1="15" y2="3" />
</svg>

Before

Width:  |  Height:  |  Size: 324 B

@@ -1,13 +0,0 @@
<?xml version="1.0" ?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 512 512" id="svg2793" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:serif="http://www.serif.com/" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs2797"/>
<g id="_24-Connect" style="display:inline" transform="translate(-3072,-4096)">
<g id="g2211" transform="translate(3339.32,4267.43)">
<path d="m 0,169.14 c -44.818,0 -81.28,-36.462 -81.28,-81.28 0,-44.818 36.462,-81.28 81.28,-81.28 44.817,0 81.279,36.462 81.279,81.28 0,44.818 -36.462,81.28 -81.279,81.28 m -149.2,141.43 c -24.587,0 -44.59,-20.003 -44.59,-44.59 0,-24.587 20.003,-44.59 44.59,-44.59 24.587,0 44.59,20.003 44.59,44.59 0,24.587 -20.003,44.59 -44.59,44.59 m -83.91,-398.5 c 0,-29.5 24,-53.5 53.5,-53.5 29.494,0 53.49,24 53.49,53.5 0,29.494 -23.996,53.49 -53.49,53.49 -29.5,0 -53.5,-23.996 -53.5,-53.49 m 412.099,37 c 17.364,0 31.49,14.126 31.49,31.49 0,17.369 -14.126,31.5 -31.49,31.5 -17.369,0 -31.5,-14.131 -31.5,-31.5 0,-17.364 14.131,-31.49 31.5,-31.49 m -44.46,73.915 c 11.207,11.739 26.989,19.075 44.46,19.075 33.907,0 61.49,-27.589 61.49,-61.5 0,-33.906 -27.583,-61.49 -61.49,-61.49 -33.911,0 -61.5,27.584 -61.5,61.49 0,5.628 0.776,11.076 2.198,16.259 L 87.091,18.672 C 66.687,-6.959 35.231,-23.42 0,-23.42 c -26.243,0 -50.388,9.137 -69.436,24.388 l -44.024,-38.046 c 10.859,-14.093 17.34,-31.725 17.34,-50.852 0,-46.042 -37.453,-83.5 -83.49,-83.5 -46.042,0 -83.5,37.458 -83.5,83.5 0,46.036 37.458,83.49 83.5,83.49 16.012,0 30.98,-4.54 43.702,-12.388 l 45.751,39.538 c -13.281,18.326 -21.123,40.838 -21.123,65.15 0,27.971 10.377,53.562 27.478,73.133 l -30.6,39.04 c -10.397,-5.508 -22.236,-8.643 -34.798,-8.643 -41.129,0 -74.59,33.461 -74.59,74.59 0,41.129 33.461,74.59 74.59,74.59 41.129,0 74.59,-33.461 74.59,-74.59 0,-17.745 -6.24,-34.055 -16.625,-46.873 l 30.054,-38.343 c 17.567,11.607 38.596,18.376 61.181,18.376 61.359,0 111.279,-49.92 111.279,-111.28 0,-15.382 -3.138,-30.045 -8.806,-43.383 z" id="path2209" style="fill-rule:nonzero"/>
</g>
<g id="g2215" transform="translate(3521.08,4502.59)">
<path d="M 0,57.45 C -15.842,57.45 -28.73,44.562 -28.73,28.72 -28.73,12.884 -15.842,0 0,0 15.836,0 28.72,12.884 28.72,28.72 28.72,44.562 15.836,57.45 0,57.45 M 0,-30 c -32.384,0 -58.73,26.342 -58.73,58.72 0,32.384 26.346,58.73 58.73,58.73 32.378,0 58.72,-26.346 58.72,-58.73 C 58.72,-3.658 32.378,-30 0,-30" id="path2213" style="fill-rule:nonzero"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
<circle cx="5" cy="12" r="1" />
</svg>

Before

Width:  |  Height:  |  Size: 288 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h6v6" />
<path d="M10 14 21 3" />
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
</svg>

Before

Width:  |  Height:  |  Size: 308 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
<path d="m2 2 20 20" />
</svg>

Before

Width:  |  Height:  |  Size: 480 B

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
<circle cx="12" cy="12" r="3" />
</svg>

Before

Width:  |  Height:  |  Size: 336 B

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
<path d="m9 13 2 2 4-4" />
</svg>

Before

Width:  |  Height:  |  Size: 347 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.93a2 2 0 0 1 1.66.9l.82 1.2a2 2 0 0 0 1.66.9H18a2 2 0 0 1 2 2v2" />
</svg>

Before

Width:  |  Height:  |  Size: 378 B

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 10v6" />
<path d="M9 13h6" />
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
</svg>

Before

Width:  |  Height:  |  Size: 365 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 14 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z" />
<path d="M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.92-.6H12a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z" />
<path d="M3 5a2 2 0 0 0 2 2h3" />
<path d="M3 3v13a2 2 0 0 0 2 2h3" />
</svg>

Before

Width:  |  Height:  |  Size: 525 B

-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
</svg>

Before

Width:  |  Height:  |  Size: 318 B

-162
View File
@@ -1,162 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" viewBox="0.00 0.00 100.00 100.00" width="100.00" height="100.00">
<g>
<path fill="#000000" d="
M 44.00 5.56
Q 44.18 5.56 44.24 5.52
C 48.35 3.31 54.84 4.30 59.34 6.39
Q 73.67 13.03 75.28 28.51
Q 77.00 45.04 81.97 93.69
A 1.92 1.92 0.0 0 1 80.24 95.80
L 80.13 95.81
A 1.95 1.95 0.0 0 1 78.01 94.07
Q 74.00 55.61 71.84 33.50
Q 70.90 23.96 68.99 20.35
C 64.91 12.62 56.73 7.78 47.81 8.34
A 2.80 2.79 -61.0 0 0 46.66 8.67
C 45.87 9.10 44.88 9.96 44.00 9.96
C 43.12 9.96 42.13 9.10 41.34 8.67
A 2.80 2.79 61.0 0 0 40.19 8.34
C 31.27 7.78 23.09 12.62 19.01 20.35
Q 17.10 23.96 16.16 33.50
Q 14.00 55.61 9.98 94.07
A 1.95 1.95 0.0 0 1 7.86 95.81
L 7.75 95.80
A 1.92 1.92 0.0 0 1 6.02 93.69
Q 11.00 45.04 12.72 28.51
Q 14.33 13.03 28.66 6.39
C 33.16 4.30 39.65 3.31 43.76 5.52
Q 43.82 5.56 44.00 5.56
Z"
/>
<path fill="#000000" d="
M 88.00 4.33
Q 88.20 4.33 88.27 4.44
Q 97.14 18.60 97.64 19.43
A 1.75 1.67 31.1 0 1 97.89 20.35
Q 97.86 27.45 95.17 40.45
Q 93.80 47.13 90.33 51.14
A 1.37 1.34 65.9 0 0 90.00 52.03
L 90.00 95.35
A 0.65 0.65 0.0 0 1 89.35 96.00
Q 89.05 96.00 88.00 96.00
Q 86.95 96.00 86.65 96.00
A 0.65 0.65 0.0 0 1 86.00 95.35
L 86.00 52.03
A 1.37 1.34 -65.9 0 0 85.67 51.14
Q 82.19 47.13 80.82 40.45
Q 78.13 27.45 78.10 20.35
A 1.75 1.67 -31.1 0 1 78.35 19.43
Q 78.85 18.60 87.72 4.44
Q 87.79 4.33 88.00 4.33
Z
M 87.99 16.37
Q 88.16 16.37 88.95 13.91
A 1.31 1.31 0.0 0 0 88.90 12.99
Q 88.43 11.92 88.00 11.92
Q 87.58 11.92 87.10 12.98
A 1.31 1.31 0.0 0 0 87.05 13.91
Q 87.82 16.37 87.99 16.37
Z
M 83.74 18.25
L 82.28 20.78
A 1.66 1.66 0.0 0 0 82.07 21.79
L 83.34 33.00
A 0.20 0.20 0.0 0 0 83.71 33.07
Q 87.68 25.64 84.59 18.31
A 0.48 0.47 48.8 0 0 83.74 18.25
Z
M 92.26 18.20
A 0.48 0.48 0.0 0 0 91.40 18.26
Q 88.31 25.62 92.30 33.08
A 0.20 0.20 0.0 0 0 92.67 33.01
L 93.94 21.75
A 1.67 1.66 -56.9 0 0 93.73 20.74
L 92.26 18.20
Z
M 88.00 47.62
Q 88.34 47.62 89.14 46.13
Q 89.90 44.71 91.10 40.33
A 1.08 1.06 -51.2 0 0 91.03 39.62
L 88.14 33.79
A 0.09 0.08 79.5 0 0 88.07 33.73
Q 88.03 33.73 88.00 33.73
Q 87.97 33.73 87.94 33.73
A 0.09 0.08 -79.5 0 0 87.87 33.79
L 84.97 39.62
A 1.08 1.06 51.3 0 0 84.90 40.33
Q 86.09 44.71 86.85 46.13
Q 87.65 47.62 88.00 47.62
Z"
/>
<path fill="#000000" d="
M 44.00 24.65
Q 44.18 24.65 44.65 24.16
C 52.89 15.80 65.71 21.08 65.96 32.76
Q 66.04 36.51 65.99 92.81
A 2.01 2.01 0.0 0 1 64.05 94.82
L 63.97 94.82
A 1.91 1.90 -0.9 0 1 62.00 92.92
L 62.00 65.35
A 1.24 1.20 -67.8 0 0 61.66 64.50
Q 57.89 60.53 53.58 58.45
A 0.75 0.75 0.0 0 0 52.70 58.61
C 50.31 61.10 47.59 61.96 44.00 61.96
C 40.41 61.96 37.69 61.10 35.30 58.61
A 0.75 0.75 0.0 0 0 34.42 58.45
Q 30.11 60.53 26.34 64.50
A 1.24 1.20 67.8 0 0 26.00 65.35
L 26.00 92.92
A 1.91 1.90 0.9 0 1 24.03 94.82
L 23.95 94.82
A 2.01 2.01 0.0 0 1 22.01 92.81
Q 21.96 36.51 22.04 32.76
C 22.28 21.08 35.10 15.80 43.34 24.16
Q 43.82 24.65 44.00 24.65
Z
M 44.00 57.96
C 46.12 57.96 49.49 57.46 50.07 55.04
Q 50.40 53.66 51.73 53.46
A 1.97 1.91 -38.5 0 1 52.66 53.55
Q 57.64 55.37 61.44 58.95
A 0.32 0.32 0.0 0 0 61.98 58.71
Q 62.04 40.89 61.97 32.78
C 61.91 25.07 54.00 21.48 47.99 26.16
C 46.66 27.20 45.76 29.97 43.99 29.97
C 42.23 29.97 41.33 27.20 39.99 26.16
C 33.98 21.49 26.07 25.08 26.02 32.79
Q 25.95 40.90 26.02 58.72
A 0.32 0.32 0.0 0 0 26.56 58.96
Q 30.36 55.38 35.34 53.56
A 1.97 1.91 38.5 0 1 36.27 53.46
Q 37.60 53.66 37.93 55.04
C 38.51 57.46 41.89 57.96 44.00 57.96
Z"
/>
<rect fill="#000000" x="37.07" y="68.01" width="13.86" height="3.98" rx="1.92"/>
</g>
<g>
<path fill="#000000" d="
M 35.01 33.16
C 36.76 33.18 40.51 34.06 39.85 36.58
A 1.92 1.90 21.6 0 1 37.08 37.76
Q 35.98 37.17 34.97 37.16
Q 33.95 37.14 32.84 37.72
A 1.92 1.90 -20.3 0 1 30.09 36.48
C 29.49 33.94 33.26 33.14 35.01 33.16
Z"
/>
<path fill="#000000" d="
M 53.03 37.15
Q 52.02 37.17 50.91 37.76
A 1.92 1.90 -21.6 0 1 48.15 36.58
C 47.49 34.06 51.24 33.17 52.99 33.15
C 54.74 33.13 58.52 33.94 57.91 36.48
A 1.92 1.90 20.4 0 1 55.16 37.72
Q 54.05 37.14 53.03 37.15
Z"
/>
<circle fill="#000000" cx="34.75" cy="42.20" r="2.27"/>
<circle fill="#000000" cx="54.00" cy="42.00" r="1.96"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>

Before

Width:  |  Height:  |  Size: 307 B

@@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="9" cy="12" r="1" />
<circle cx="9" cy="5" r="1" />
<circle cx="9" cy="19" r="1" />
<circle cx="15" cy="12" r="1" />
<circle cx="15" cy="5" r="1" />
<circle cx="15" cy="19" r="1" />
</svg>

Before

Width:  |  Height:  |  Size: 389 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 16h.01" />
<path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
<path d="M21.946 12.013H2.054" />
<path d="M6 16h.01" />
</svg>

Before

Width:  |  Height:  |  Size: 435 B

@@ -1,14 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12.409 5.824c-.702.792-1.15 1.496-1.415 2.166l2.153 2.156a.5.5 0 0 1 0 .707l-2.293 2.293a.5.5 0 0 0 0 .707L12 15" />
<path d="M13.508 20.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.677.6.6 0 0 0 .818.001A5.5 5.5 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5z" />
</svg>

Before

Width:  |  Height:  |  Size: 496 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5" />
</svg>

Before

Width:  |  Height:  |  Size: 352 B

-3
View File
@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5" />
</svg>

Before

Width:  |  Height:  |  Size: 349 B

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zM6 19h12V9.157l-6-5.454-6 5.454V19zm2.591-5.191a3.508 3.508 0 0 1 0-1.622l-.991-.572 1-1.732.991.573a3.495 3.495 0 0 1 1.404-.812V8.5h2v1.144c.532.159 1.01.44 1.404.812l.991-.573 1 1.731-.991.573a3.508 3.508 0 0 1 0 1.622l.991.572-1 1.731-.991-.572a3.495 3.495 0 0 1-1.404.811v1.145h-2V16.35a3.495 3.495 0 0 1-1.404-.811l-.991.572-1-1.73.991-.573zm3.404.688a1.5 1.5 0 1 0 0-2.998 1.5 1.5 0 0 0 0 2.998z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 677 B

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 5h6" />
<path d="M19 2v6" />
<path d="M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
</svg>

Before

Width:  |  Height:  |  Size: 397 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>

Before

Width:  |  Height:  |  Size: 326 B

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v12" />
<path d="m8 11 4 4 4-4" />
<path d="M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4" />
</svg>

Before

Width:  |  Height:  |  Size: 326 B

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18.178 8c5.096 0 5.096 8 0 8-5.095 0-7.133-8-12.739-8-4.781 0-4.781 8 0 8 5.606 0 7.644-8 12.739-8z" />
</svg>

Before

Width:  |  Height:  |  Size: 300 B

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>

Before

Width:  |  Height:  |  Size: 270 B

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 8h.01" />
<path d="M12 12h.01" />
<path d="M14 8h.01" />
<path d="M16 12h.01" />
<path d="M18 8h.01" />
<path d="M6 8h.01" />
<path d="M7 16h10" />
<path d="M8 12h.01" />
<rect width="20" height="16" x="2" y="4" rx="2" />
</svg>

Before

Width:  |  Height:  |  Size: 437 B

-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.84z" />
<path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12" />
<path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17" />
</svg>

Before

Width:  |  Height:  |  Size: 486 B

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="7" height="7" x="3" y="3" rx="1" />
<rect width="7" height="7" x="14" y="3" rx="1" />
<rect width="7" height="7" x="14" y="14" rx="1" />
<rect width="7" height="7" x="3" y="14" rx="1" />
</svg>

Before

Width:  |  Height:  |  Size: 392 B

Loaded 100 of 720 files, more files were not shown because too many files have changed in this diff. Show more