feat(qbz-qt): phase 3 — Discover > Home with live Qobuz data and artwork
Pure mapping port of home.rs/foryou.rs (quality tiers, award ribbons, slim grids) into Slint-free rows; discover index + favorites + release-watch + top artists fetched concurrently (12 sections, ~170 cards live-verified). Artwork via the shared qbz-cache ImageCacheService (semaphore-16 downloads, file:// rows, native QML decode). HomeView.qml: tab pill, five rail kinds, shimmer skeleton, error+Retry, offline placeholder — exact msgids throughout. Sections travel as one JSON document (cxx-qt-lib cannot nest QVariant containers — documented). km
No files matched your search
@@ -6514,13 +6514,18 @@ dependencies = [
|
||||
"log",
|
||||
"open",
|
||||
"qbz-app",
|
||||
"qbz-cache",
|
||||
"qbz-core",
|
||||
"qbz-credentials",
|
||||
"qbz-i18n",
|
||||
"qbz-log",
|
||||
"qbz-models",
|
||||
"qbz-qobuz",
|
||||
"qbz-text-utils",
|
||||
"rand 0.10.1",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
@@ -17,6 +17,9 @@ cxx-qt-lib = { version = "0.7", features = ["qt_full"] }
|
||||
tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
urlencoding = "2"
|
||||
open = "5"
|
||||
dirs = "5"
|
||||
@@ -25,6 +28,8 @@ qbz-core = { path = "../qbz-core" }
|
||||
qbz-app = { path = "../qbz-app" }
|
||||
qbz-qobuz = { path = "../qbz-qobuz" }
|
||||
qbz-models = { path = "../qbz-models" }
|
||||
qbz-cache = { path = "../qbz-cache" }
|
||||
qbz-text-utils = { path = "../qbz-text-utils" }
|
||||
qbz-credentials = { path = "../qbz-credentials" }
|
||||
qbz-i18n = { path = "../qbz-i18n" }
|
||||
qbz-log = { path = "../qbz-log" }
|
||||
|
||||
@@ -17,7 +17,10 @@ fn collect_qrc_files(dir: &Path, out: &mut Vec<String>) {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut qrc_files: Vec<String> = vec!["qml/assets/qbz-logo.png".to_string()];
|
||||
let mut qrc_files: Vec<String> = vec![
|
||||
"qml/assets/qbz-logo.png".to_string(),
|
||||
"qml/assets/hi-res.svg".to_string(),
|
||||
];
|
||||
collect_qrc_files(Path::new("qml/assets/icons"), &mut qrc_files);
|
||||
collect_qrc_files(Path::new("qml/assets/fonts"), &mut qrc_files);
|
||||
// Non-QML resources land at qrc:/qt/qml/com/blitzfc/qbz/<path> — QML
|
||||
|
||||
@@ -1,31 +1,983 @@
|
||||
// Content view stub for the only mounted route ("home" = Discover > Home).
|
||||
// Phase 3 fills this with the real Discover view; for now it proves the
|
||||
// content frame, theme and i18n wiring.
|
||||
// Discover > Home — QML port of crates/qbz-ui/ui/discover/HomeView.slint
|
||||
// (+ Carousel / AlbumCard / SlimCarousel / SlimCard / ArtistCarousel /
|
||||
// PlaylistCarousel / HomeSkeleton / OfflinePlaceholder).
|
||||
//
|
||||
// Data: QbzBridge.homeSectionsJson (one JSON document — see bridge.rs),
|
||||
// published by src/home_qt.rs; artwork file:// paths resolve through the
|
||||
// qbz-cache image cache. Section kinds: "album" | "playlist" | "slim" |
|
||||
// "artists" | "recentPlaceholder".
|
||||
//
|
||||
// POC-NOTEs:
|
||||
// - The genre filter + section-configurator gear are INERT visual stubs
|
||||
// (out of scope).
|
||||
// - Editor's Picks / For You / Recommendations tabs render a placeholder
|
||||
// page with the exact header copy; only "home" is real.
|
||||
// - Card clicks / hover actions (play / favorite / more / pin) and
|
||||
// "View all" / context menus are inert — album/artist pages, playback
|
||||
// and per-user stores are later phases.
|
||||
// - The offline mount mirrors AppShell's ADR-010 seam: OfflineState.offline
|
||||
// -> the OfflinePlaceholder replica (exact msgids).
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import com.blitzfc.qbz
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
color: theme.surfaceMain
|
||||
|
||||
QbzTheme { id: theme }
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.margins: theme.spacingXl
|
||||
spacing: theme.spacingSm
|
||||
// Reparsed whenever Rust republishes the JSON document.
|
||||
readonly property var sections: JSON.parse(QbzBridge.homeSectionsJson)
|
||||
property string activeTab: "home"
|
||||
|
||||
// ============================ shared components =======================
|
||||
|
||||
// Circular page-control button (Carousel's NavButton).
|
||||
component NavBtn: Rectangle {
|
||||
property string name: ""
|
||||
property bool btnEnabled: true
|
||||
signal clicked()
|
||||
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
opacity: btnEnabled ? 1.0 : 0.4
|
||||
color: (nbArea.containsMouse && btnEnabled) ? theme.surfaceHover : theme.surfaceElevated
|
||||
QbzIcon {
|
||||
name: parent.name
|
||||
width: 15
|
||||
height: 15
|
||||
anchors.centerIn: parent
|
||||
tintName: parent.btnEnabled ? "primary" : "muted"
|
||||
}
|
||||
MouseArea {
|
||||
id: nbArea
|
||||
anchors.fill: parent
|
||||
enabled: parent.btnEnabled
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.btnEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: parent.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
// Section header: title + page chevrons (Carousel header metrics).
|
||||
component RailHeader: Item {
|
||||
property string title: ""
|
||||
property bool leftEnabled: false
|
||||
property bool rightEnabled: false
|
||||
signal pageLeft()
|
||||
signal pageRight()
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
height: 28
|
||||
Text {
|
||||
text: QbzBridge.tr("Discover") + " / " + QbzBridge.tr("Home")
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: parent.title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontSection
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
NavBtn {
|
||||
name: "chevron-left"
|
||||
btnEnabled: parent.parent.leftEnabled
|
||||
onClicked: parent.parent.pageLeft()
|
||||
}
|
||||
NavBtn {
|
||||
name: "chevron-right"
|
||||
btnEnabled: parent.parent.rightEnabled
|
||||
onClicked: parent.parent.pageRight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Album card (AlbumCard.slint): 200x246 — 200px art (radius 8) + 6px +
|
||||
// 40px title/artist row with the icon-only quality badge.
|
||||
component AlbumCard: Rectangle {
|
||||
property var card: ({})
|
||||
width: 200
|
||||
height: 246
|
||||
color: "transparent"
|
||||
|
||||
readonly property bool overlayOn: artArea.containsMouse
|
||||
|
||||
Column {
|
||||
spacing: 0
|
||||
// --- Artwork + hover overlay --------------------------------
|
||||
Rectangle {
|
||||
width: 200
|
||||
height: 200
|
||||
radius: theme.radiusSm
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: card.artPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
// Hover scrim.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#000000"
|
||||
opacity: overlayOn ? 0.6 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
}
|
||||
// Hover meta — genre + year, top-left.
|
||||
Column {
|
||||
x: 12
|
||||
y: 12
|
||||
spacing: 2
|
||||
opacity: overlayOn ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
Text {
|
||||
visible: card.genre !== ""
|
||||
text: card.genre
|
||||
height: 20
|
||||
color: "#ebffffff"
|
||||
font.pixelSize: 13
|
||||
font.weight: theme.weightBold
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
visible: card.year !== ""
|
||||
text: card.year
|
||||
height: 17
|
||||
color: "#ccffffff"
|
||||
font.pixelSize: 12
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
// Card-open + hover detector (declared before the action
|
||||
// buttons so those win the pointer).
|
||||
MouseArea {
|
||||
id: artArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// POC-NOTE: the album page is out of scope — inert.
|
||||
}
|
||||
// Hover action buttons — favorite / play / more (INERT;
|
||||
// POC-NOTE: playback + favorites land in later phases).
|
||||
Row {
|
||||
x: 0
|
||||
y: 120
|
||||
width: 200
|
||||
height: 44
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 12
|
||||
opacity: overlayOn ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
|
||||
Item { width: (parent.width - 44 - 2 * 36 - 2 * 12) / 2; height: 1 }
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: favArea.containsMouse ? "#3dffffff" : "#24ffffff"
|
||||
border.width: 1.5
|
||||
border.color: "#ccffffff"
|
||||
QbzIcon {
|
||||
name: "heart"
|
||||
width: 16
|
||||
height: 16
|
||||
anchors.centerIn: parent
|
||||
tintName: "primary"
|
||||
}
|
||||
MouseArea { id: favArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor }
|
||||
}
|
||||
Rectangle {
|
||||
width: 44
|
||||
height: 44
|
||||
radius: 22
|
||||
color: playOvArea.containsMouse ? "#d6ffffff" : "#ffffff"
|
||||
// Black glyph on the white disc (Slint tints #000)
|
||||
// — the "black" baked variant.
|
||||
QbzIcon {
|
||||
name: "play-fill"
|
||||
width: 18
|
||||
height: 18
|
||||
anchors.centerIn: parent
|
||||
tintName: "black"
|
||||
}
|
||||
MouseArea { id: playOvArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor }
|
||||
}
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: moreArea.containsMouse ? "#3dffffff" : "#24ffffff"
|
||||
border.width: 1.5
|
||||
border.color: "#ccffffff"
|
||||
QbzIcon {
|
||||
name: "ellipsis"
|
||||
width: 16
|
||||
height: 16
|
||||
anchors.centerIn: parent
|
||||
tintName: "primary"
|
||||
}
|
||||
MouseArea { id: moreArea; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor }
|
||||
}
|
||||
Item { width: (parent.width - 44 - 2 * 36 - 2 * 12) / 2; height: 1 }
|
||||
}
|
||||
// Award ribbon — content-width, capped at the card width.
|
||||
Rectangle {
|
||||
visible: card.ribbon !== ""
|
||||
x: 0
|
||||
y: parent.height - height - 8
|
||||
height: 20
|
||||
width: Math.min(ribbonRow.width, 200)
|
||||
color: card.ribbonKind === "press" ? "#d49511" : "#e0000000"
|
||||
topRightRadius: 3
|
||||
bottomRightRadius: 3
|
||||
clip: true
|
||||
Rectangle {
|
||||
width: card.ribbonKind === "press" ? 0 : 3
|
||||
height: parent.height
|
||||
color: card.ribbonKind === "qobuzissime" ? "#8b5cf6" : "#eab308"
|
||||
}
|
||||
Row {
|
||||
id: ribbonRow
|
||||
height: parent.height
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
width: ribbonText.implicitWidth + 20
|
||||
Text {
|
||||
id: ribbonText
|
||||
height: parent.height
|
||||
text: card.ribbon
|
||||
color: card.ribbonKind === "press" ? "#1f1407" : "#ffffff"
|
||||
font.pixelSize: 9
|
||||
font.weight: theme.weightSemibold
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item { width: 1; height: 6 }
|
||||
// --- Title / artist + quality badge --------------------------
|
||||
Row {
|
||||
width: 200
|
||||
height: 40
|
||||
spacing: theme.spacingSm
|
||||
Column {
|
||||
width: parent.width - (qualityBadge.visible ? qualityBadge.width + theme.spacingSm : 0)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Text {
|
||||
width: parent.width
|
||||
height: 20
|
||||
text: card.title
|
||||
color: titleArea.containsMouse ? theme.accent : theme.textPrimary
|
||||
font.pixelSize: theme.fontBody - 2
|
||||
font.weight: theme.weightMedium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
MouseArea {
|
||||
id: titleArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
height: 18
|
||||
text: card.artist
|
||||
color: card.artistId !== "" && artistArea.containsMouse
|
||||
? theme.textPrimary : theme.textMuted
|
||||
font.pixelSize: theme.fontLink - 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
MouseArea {
|
||||
id: artistArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: card.artistId !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
// Icon-only quality badge (QualityBadge.slint).
|
||||
Item {
|
||||
id: qualityBadge
|
||||
visible: card.qualityTier !== ""
|
||||
width: card.qualityTier === "hires" ? 42 : 30
|
||||
height: 30
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Image {
|
||||
visible: card.qualityTier === "hires"
|
||||
source: "assets/hi-res.svg"
|
||||
width: 42
|
||||
height: 28
|
||||
anchors.centerIn: parent
|
||||
sourceSize: Qt.size(84, 56)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
Rectangle {
|
||||
visible: card.qualityTier === "cd"
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 3
|
||||
color: theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
QbzIcon {
|
||||
name: "cd"
|
||||
width: 16
|
||||
height: 16
|
||||
anchors.centerIn: parent
|
||||
tintName: "muted"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal album rail (Carousel.slint): header + clipped ListView,
|
||||
// page chevrons (per-page step like the Slint paging).
|
||||
component AlbumRail: Column {
|
||||
property var sectionData: ({})
|
||||
width: parent ? parent.width : 0
|
||||
spacing: 12
|
||||
|
||||
readonly property int perPage: Math.max(1, Math.floor((rail.width + 32) / 232))
|
||||
readonly property int step: perPage * 232
|
||||
readonly property real maxScroll: Math.max(0, rail.contentWidth - rail.width)
|
||||
|
||||
RailHeader {
|
||||
title: sectionData.title
|
||||
leftEnabled: rail.contentX > 1
|
||||
rightEnabled: rail.contentX < maxScroll - 1
|
||||
onPageLeft: rail.contentX = Math.min(0, rail.contentX - step)
|
||||
onPageRight: rail.contentX = Math.min(maxScroll, rail.contentX + step)
|
||||
}
|
||||
ListView {
|
||||
id: rail
|
||||
width: parent.width
|
||||
height: 246
|
||||
orientation: ListView.Horizontal
|
||||
spacing: 32
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: sectionData.items
|
||||
delegate: AlbumCard { card: modelData }
|
||||
}
|
||||
}
|
||||
|
||||
// Slim ranked card (SlimCard.slint): 60px row — rank / 44px thumb /
|
||||
// title+subtitle.
|
||||
component SlimCard: Rectangle {
|
||||
property var card: ({})
|
||||
height: 60
|
||||
radius: theme.radiusSm
|
||||
color: slArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 12
|
||||
Text {
|
||||
visible: card.rank !== ""
|
||||
width: 20
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: card.rank
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontBody
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Rectangle {
|
||||
width: 44
|
||||
height: 44
|
||||
radius: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: card.artPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
Column {
|
||||
width: parent.width - (card.rank !== "" ? 20 : 0) - 44 - 2 * 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Text {
|
||||
width: parent.width
|
||||
text: card.title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontLink
|
||||
font.weight: theme.weightMedium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Text {
|
||||
width: parent.width
|
||||
text: card.artist
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: slArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// POC-NOTE: album page out of scope — inert.
|
||||
}
|
||||
}
|
||||
|
||||
// Popular slim grid (SlimCarousel.slint): 4x3 pages of 12, capped 24.
|
||||
component SlimGrid: Column {
|
||||
property var sectionData: ({})
|
||||
width: parent ? parent.width : 0
|
||||
spacing: 12
|
||||
|
||||
readonly property int perPage: 12
|
||||
readonly property int total: Math.min(sectionData.items.length, 2 * perPage)
|
||||
readonly property int pageCount: Math.max(1, Math.ceil(total / perPage))
|
||||
readonly property real maxScroll: (pageCount - 1) * grid.width
|
||||
|
||||
RailHeader {
|
||||
title: sectionData.title
|
||||
leftEnabled: grid.contentX > 1
|
||||
rightEnabled: grid.contentX < maxScroll - 1
|
||||
onPageLeft: grid.contentX = Math.max(0, grid.contentX - grid.width)
|
||||
onPageRight: grid.contentX = Math.min(maxScroll, grid.contentX + grid.width)
|
||||
}
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 3 * 60 + 2 * 8
|
||||
clip: true
|
||||
Flickable {
|
||||
id: grid
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
contentWidth: pageCount * width
|
||||
contentHeight: height
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
readonly property real cellWidth: (width - 3 * 8) / 4
|
||||
|
||||
Repeater {
|
||||
model: sectionData.items
|
||||
delegate: SlimCard {
|
||||
readonly property int slot: index % perPage
|
||||
readonly property int pageIdx: Math.floor(index / perPage)
|
||||
visible: index < total
|
||||
width: grid.cellWidth
|
||||
height: 60
|
||||
x: pageIdx * grid.width + (slot % 4) * (grid.cellWidth + 8)
|
||||
y: Math.floor(slot / 4) * (60 + 8)
|
||||
card: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Playlist card (PlaylistCard.slint): 200x246 — 200px cover + category
|
||||
// subtag + title.
|
||||
component PlaylistCard: Rectangle {
|
||||
property var card: ({})
|
||||
width: 200
|
||||
height: 246
|
||||
color: "transparent"
|
||||
|
||||
Column {
|
||||
spacing: 0
|
||||
Rectangle {
|
||||
width: 200
|
||||
height: 200
|
||||
radius: theme.radiusSm
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: card.artPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// POC-NOTE: playlist page out of scope — inert.
|
||||
}
|
||||
}
|
||||
Item { width: 1; height: 8 }
|
||||
Text {
|
||||
visible: card.category !== ""
|
||||
text: card.category
|
||||
color: theme.accent
|
||||
font.pixelSize: 10
|
||||
font.weight: theme.weightSemibold
|
||||
font.letterSpacing: 0.5
|
||||
elide: Text.ElideRight
|
||||
width: 200
|
||||
}
|
||||
Item { width: 1; height: 4 }
|
||||
Text {
|
||||
width: 200
|
||||
text: card.title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontBody - 2
|
||||
font.weight: theme.weightMedium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Artist card (ArtistCard.slint): 160x220 — 120px circular art + name.
|
||||
component ArtistCard: Rectangle {
|
||||
property var card: ({})
|
||||
width: 160
|
||||
height: 220
|
||||
radius: 12
|
||||
color: "transparent"
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 8
|
||||
Item { width: 1; height: 24 }
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 120
|
||||
radius: 60
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: theme.surfaceElevated
|
||||
clip: true
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: card.artPath
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// POC-NOTE: artist page out of scope — inert.
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: 128
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: card.title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 14
|
||||
font.weight: theme.weightMedium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recently-played placeholder (HomeView.slint RecentPlaceholder).
|
||||
component RecentPlaceholder: Column {
|
||||
property string title: ""
|
||||
property string hint: ""
|
||||
width: parent ? parent.width : 0
|
||||
spacing: 10
|
||||
Text {
|
||||
text: "QBZ Qt POC — phase 3 fills this view"
|
||||
text: title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: 18
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
Text {
|
||||
text: hint
|
||||
color: theme.textMuted
|
||||
font.pixelSize: theme.fontLegal
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
|
||||
// Shimmer block + skeleton rows (HomeSkeleton.slint).
|
||||
component Shimmer: Rectangle {
|
||||
property bool phase: false
|
||||
color: theme.surfaceElevated
|
||||
radius: theme.radiusSm
|
||||
opacity: phase ? 0.85 : 0.4
|
||||
Behavior on opacity { NumberAnimation { duration: 900; easing.type: Easing.InOutQuad } }
|
||||
}
|
||||
component SkeletonRow: Column {
|
||||
id: skelRow
|
||||
property bool phase: false
|
||||
width: parent ? parent.width : 0
|
||||
spacing: 12
|
||||
Shimmer { phase: skelRow.phase; width: 180; height: 22 }
|
||||
Row {
|
||||
spacing: 32
|
||||
Repeater {
|
||||
model: 5
|
||||
delegate: Column {
|
||||
spacing: 8
|
||||
Shimmer { phase: skelRow.phase; width: 200; height: 200 }
|
||||
Shimmer { phase: skelRow.phase; width: 140; height: 14 }
|
||||
Shimmer { phase: skelRow.phase; width: 90; height: 12 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ offline gate ============================
|
||||
// (OfflinePlaceholder.slint replica; mounted INSTEAD of the view.)
|
||||
Column {
|
||||
visible: QbzBridge.offline
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
QbzIcon {
|
||||
name: "cloud-off"
|
||||
width: 56
|
||||
height: 56
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
tintName: "muted"
|
||||
}
|
||||
Item { width: 1; height: 18 }
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: QbzBridge.tr("You're offline")
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontHeading
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
Item { width: 1; height: 8 }
|
||||
Text {
|
||||
width: 420
|
||||
text: QbzBridge.offlineMode === 2
|
||||
? QbzBridge.tr("Offline mode is enabled. Disable it in Settings to use Qobuz.")
|
||||
: QbzBridge.tr("No internet connection. Your local library and downloads keep working.")
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: theme.fontBody
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
// POC-NOTE: induced-offline "Open Settings" button omitted — no
|
||||
// Settings view exists in the POC yet.
|
||||
}
|
||||
|
||||
// ============================ the view ================================
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
visible: !QbzBridge.offline
|
||||
|
||||
// --- Toolbar (fixed 56px) ---------------------------------------
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 56
|
||||
|
||||
Row {
|
||||
x: 32
|
||||
y: 25 - height / 2
|
||||
height: tabPill.height
|
||||
spacing: 16
|
||||
|
||||
Rectangle {
|
||||
id: tabPill
|
||||
width: tabRow.width
|
||||
height: tabRow.height
|
||||
color: theme.surfaceElevated
|
||||
radius: 6
|
||||
Row {
|
||||
id: tabRow
|
||||
padding: 3
|
||||
spacing: 4
|
||||
Repeater {
|
||||
model: [
|
||||
{ "id": "home", "label": QbzBridge.tr("Home") },
|
||||
{ "id": "editorPicks", "label": QbzBridge.tr("Editor's Picks") },
|
||||
{ "id": "forYou", "label": QbzBridge.tr("For You") },
|
||||
{ "id": "recommendations", "label": QbzBridge.tr("Recommendations") },
|
||||
]
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
property bool active: root.activeTab === modelData.id
|
||||
width: tabText.implicitWidth + 28
|
||||
height: tabText.implicitHeight + 12
|
||||
radius: 4
|
||||
color: active ? theme.surfaceMain
|
||||
: tabArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
Text {
|
||||
id: tabText
|
||||
anchors.centerIn: parent
|
||||
text: modelData.label
|
||||
color: parent.active ? theme.textPrimary : theme.textMuted
|
||||
font.pixelSize: 13
|
||||
font.weight: theme.weightMedium
|
||||
}
|
||||
MouseArea {
|
||||
id: tabArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.activeTab = modelData.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Genre filter + configurator gear — INERT stubs (POC-NOTE).
|
||||
Row {
|
||||
x: parent.width - width - 32
|
||||
y: 25 - height / 2
|
||||
height: 32
|
||||
spacing: 6
|
||||
Rectangle {
|
||||
width: genreRow.width
|
||||
height: 32
|
||||
radius: 6
|
||||
color: genreArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
|
||||
Row {
|
||||
id: genreRow
|
||||
height: parent.height
|
||||
leftPadding: 12
|
||||
rightPadding: 14
|
||||
spacing: 7
|
||||
QbzIcon {
|
||||
name: "list-filter"
|
||||
width: 14
|
||||
height: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
tintName: "secondary"
|
||||
}
|
||||
Text {
|
||||
text: QbzBridge.tr("Filter by genre")
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 13
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: genreArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 4
|
||||
color: gearArea.containsMouse ? theme.surfaceHover : "transparent"
|
||||
QbzIcon {
|
||||
name: "home-gear"
|
||||
width: 20
|
||||
height: 20
|
||||
anchors.centerIn: parent
|
||||
tintName: gearArea.containsMouse ? "primary" : "muted"
|
||||
}
|
||||
MouseArea {
|
||||
id: gearArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: theme.borderSubtle
|
||||
}
|
||||
|
||||
// --- Scrollable sections -----------------------------------------
|
||||
Flickable {
|
||||
id: homeFlick
|
||||
width: parent.width
|
||||
height: parent.height - 57
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: homeContent.implicitHeight
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
Column {
|
||||
id: homeContent
|
||||
width: parent.width
|
||||
padding: 32
|
||||
spacing: 40
|
||||
|
||||
// ===== Home tab ==========================================
|
||||
Column {
|
||||
id: homeTab
|
||||
visible: root.activeTab === "home"
|
||||
width: parent.width - 64
|
||||
spacing: 40
|
||||
|
||||
// Loading skeleton (HomeSkeleton: two shimmer rows).
|
||||
Column {
|
||||
visible: QbzBridge.homeLoading && root.sections.length === 0
|
||||
width: parent.width
|
||||
spacing: 40
|
||||
property bool phase: false
|
||||
Timer {
|
||||
interval: 900
|
||||
running: parent.visible
|
||||
repeat: true
|
||||
onTriggered: parent.phase = !parent.phase
|
||||
}
|
||||
SkeletonRow { phase: parent.phase }
|
||||
SkeletonRow { phase: parent.phase }
|
||||
}
|
||||
|
||||
// Error state with retry (the Slint Home has no error
|
||||
// arm; the box mirrors the FavoritesView Retry button).
|
||||
Rectangle {
|
||||
visible: QbzBridge.homeError !== ""
|
||||
width: parent.width
|
||||
height: errorColumn.height + 28
|
||||
radius: theme.radiusSm
|
||||
color: theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
Column {
|
||||
id: errorColumn
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
Text {
|
||||
text: QbzBridge.homeError
|
||||
color: theme.textSecondary
|
||||
font.pixelSize: 13
|
||||
}
|
||||
Rectangle {
|
||||
width: retryText.implicitWidth + 28
|
||||
height: 32
|
||||
radius: 6
|
||||
color: retryArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
|
||||
border.width: 1
|
||||
border.color: theme.borderSubtle
|
||||
Text {
|
||||
id: retryText
|
||||
anchors.centerIn: parent
|
||||
text: QbzBridge.tr("Retry")
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontLegal
|
||||
}
|
||||
MouseArea {
|
||||
id: retryArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: QbzBridge.reloadHome()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Section rails.
|
||||
Repeater {
|
||||
model: root.sections
|
||||
delegate: Loader {
|
||||
required property var modelData
|
||||
width: homeTab.width
|
||||
sourceComponent: modelData.kind === "album" ? albumRailComp
|
||||
: modelData.kind === "playlist" ? playlistRailComp
|
||||
: modelData.kind === "slim" ? slimGridComp
|
||||
: modelData.kind === "artists" ? artistRailComp
|
||||
: recentComp
|
||||
property var sectionData: modelData
|
||||
|
||||
Component {
|
||||
id: albumRailComp
|
||||
AlbumRail { sectionData: parent.sectionData }
|
||||
}
|
||||
Component {
|
||||
id: playlistRailComp
|
||||
Column {
|
||||
property var sectionData: parent.sectionData
|
||||
width: parent ? parent.width : 0
|
||||
spacing: 12
|
||||
Text {
|
||||
text: sectionData.title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontSection
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
ListView {
|
||||
width: parent.width
|
||||
height: 246
|
||||
orientation: ListView.Horizontal
|
||||
spacing: 32
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: sectionData.items
|
||||
delegate: PlaylistCard { card: modelData }
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: slimGridComp
|
||||
SlimGrid { sectionData: parent.sectionData }
|
||||
}
|
||||
Component {
|
||||
id: artistRailComp
|
||||
Column {
|
||||
property var sectionData: parent.sectionData
|
||||
width: parent ? parent.width : 0
|
||||
spacing: 12
|
||||
Text {
|
||||
text: sectionData.title
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontSection
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
ListView {
|
||||
width: parent.width
|
||||
height: 220
|
||||
orientation: ListView.Horizontal
|
||||
spacing: 32
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: sectionData.items
|
||||
delegate: ArtistCard { card: modelData }
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: recentComp
|
||||
RecentPlaceholder {
|
||||
property var sectionData: parent.sectionData
|
||||
title: sectionData.title
|
||||
hint: sectionData.hint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Placeholder tabs (POC-NOTE) ========================
|
||||
Column {
|
||||
visible: root.activeTab !== "home"
|
||||
width: parent.width - 64
|
||||
spacing: 10
|
||||
Text {
|
||||
text: root.activeTab === "editorPicks" ? QbzBridge.tr("Editor's Picks")
|
||||
: root.activeTab === "forYou" ? QbzBridge.tr("For You")
|
||||
: QbzBridge.tr("Recommendations")
|
||||
color: theme.textPrimary
|
||||
font.pixelSize: theme.fontSection
|
||||
font.weight: theme.weightSemibold
|
||||
}
|
||||
Text {
|
||||
text: "QBZ Qt POC — this Discover tab lands in a later phase"
|
||||
color: theme.textMuted
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,8 @@ Image {
|
||||
id: root
|
||||
// Icon file name, e.g. "compass" (".svg" appended).
|
||||
property string name: ""
|
||||
// One of: "primary" | "secondary" | "muted" | "accent" | "warning".
|
||||
// One of: "primary" | "secondary" | "muted" | "accent" | "warning" |
|
||||
// "black" (for glyphs on white discs).
|
||||
property string tintName: "secondary"
|
||||
|
||||
source: "assets/icons/" + tintName + "/" + name + ".svg"
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,15 @@
|
||||
<?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:#4285f4;}
|
||||
</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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4285f4" 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>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 677 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 5h20" />
|
||||
<path d="M6 12h12" />
|
||||
<path d="M9 19h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" 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>
|
||||
|
After Width: | Height: | Size: 371 B |
@@ -0,0 +1,15 @@
|
||||
<?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:#cccccc;}
|
||||
</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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 212 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 214 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 213 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" 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>
|
||||
|
After Width: | Height: | Size: 364 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" 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>
|
||||
|
After Width: | Height: | Size: 327 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" 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>
|
||||
|
After Width: | Height: | Size: 288 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" 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>
|
||||
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" 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>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 677 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" 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>
|
||||
|
After Width: | Height: | Size: 270 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" 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>
|
||||
|
After Width: | Height: | Size: 392 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 5h20" />
|
||||
<path d="M6 12h12" />
|
||||
<path d="M9 19h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15V6" />
|
||||
<path d="M18.5 18a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" />
|
||||
<path d="M12 12H3" />
|
||||
<path d="M16 6H3" />
|
||||
<path d="M12 18H3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 340 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10 12h11" />
|
||||
<path d="M10 18h11" />
|
||||
<path d="M10 6h11" />
|
||||
<path d="M4 10h2" />
|
||||
<path d="M4 6h1v4" />
|
||||
<path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m16 17 5-5-5-5" />
|
||||
<path d="M21 12H9" />
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 5h16" />
|
||||
<path d="M4 12h16" />
|
||||
<path d="M4 19h16" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,17 @@
|
||||
<!-- @license lucide-static v1.17.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-mic-vocal"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m11 7.601-5.994 8.19a1 1 0 0 0 .1 1.298l.817.818a1 1 0 0 0 1.314.087L15.09 12" />
|
||||
<path d="M16.5 21.174C15.5 20.5 14.372 20 13 20c-2.058 0-3.928 2.356-6 2-2.072-.356-2.775-3.369-1.5-4.5" />
|
||||
<circle cx="16" cy="7" r="5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 520 B |
@@ -0,0 +1,19 @@
|
||||
<!-- @license lucide-static v1.17.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-monitor-speaker"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5.5 20H8" />
|
||||
<path d="M17 9h.01" />
|
||||
<rect width="10" height="16" x="12" y="4" rx="2" />
|
||||
<path d="M8 6H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h4" />
|
||||
<circle cx="17" cy="15" r="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 482 B |
@@ -0,0 +1,12 @@
|
||||
<!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 width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<g id="SVGRepo_iconCarrier"> <path d="M8.50989 2.00001H15.49C15.7225 1.99995 15.9007 1.99991 16.0565 2.01515C17.1643 2.12352 18.0711 2.78958 18.4556 3.68678H5.54428C5.92879 2.78958 6.83555 2.12352 7.94337 2.01515C8.09917 1.99991 8.27741 1.99995 8.50989 2.00001Z" fill="#000000"/> <path d="M6.31052 4.72312C4.91989 4.72312 3.77963 5.56287 3.3991 6.67691C3.39117 6.70013 3.38356 6.72348 3.37629 6.74693C3.77444 6.62636 4.18881 6.54759 4.60827 6.49382C5.68865 6.35531 7.05399 6.35538 8.64002 6.35547H15.5321C17.1181 6.35538 18.4835 6.35531 19.5639 6.49382C19.9833 6.54759 20.3977 6.62636 20.7958 6.74693C20.7886 6.72348 20.781 6.70013 20.773 6.67691C20.3925 5.56287 19.2522 4.72312 17.8616 4.72312H6.31052Z" fill="#000000"/> <path d="M11.25 17C11.25 16.5858 10.9142 16.25 10.5 16.25C10.0858 16.25 9.75 16.5858 9.75 17C9.75 17.4142 10.0858 17.75 10.5 17.75C10.9142 17.75 11.25 17.4142 11.25 17Z" fill="#000000"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M8.67239 7.54204H15.3276C18.7024 7.54204 20.3898 7.54204 21.3377 8.52887C22.2855 9.51569 22.0625 11.0404 21.6165 14.0896L21.1935 16.9811C20.8437 19.3724 20.6689 20.568 19.7717 21.284C18.8745 22 17.5512 22 14.9046 22H9.09534C6.4488 22 5.12553 22 4.22834 21.284C3.33115 20.568 3.15626 19.3724 2.80648 16.9811L2.38351 14.0896C1.93748 11.0403 1.71447 9.5157 2.66232 8.52887C3.61017 7.54204 5.29758 7.54204 8.67239 7.54204ZM12.75 10.5C12.75 10.0858 12.4142 9.75 12 9.75C11.5858 9.75 11.25 10.0858 11.25 10.5V14.878C11.0154 14.7951 10.763 14.75 10.5 14.75C9.25736 14.75 8.25 15.7574 8.25 17C8.25 18.2426 9.25736 19.25 10.5 19.25C11.7426 19.25 12.75 18.2426 12.75 17V13.3197C13.4202 13.8634 14.2617 14.25 15 14.25C15.4142 14.25 15.75 13.9142 15.75 13.5C15.75 13.0858 15.4142 12.75 15 12.75C14.6946 12.75 14.1145 12.5314 13.5835 12.0603C13.0654 11.6006 12.75 11.0386 12.75 10.5Z" fill="#000000"/> </g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 18V5l12-2v13" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<circle cx="18" cy="16" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="14" y="3" width="5" height="18" rx="1" />
|
||||
<rect x="5" y="3" width="5" height="18" rx="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#000000" stroke="none">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 149 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1024 1024"
|
||||
width="1024"
|
||||
height="1024">
|
||||
<g transform="translate(0,1024) scale(0.1,-0.1)" fill="#000000">
|
||||
<path d="M4760 10144 c-19 -2 -91 -9 -160 -14 -847 -72 -1725 -399 -2431 -906
|
||||
-1164 -835 -1890 -2083 -2056 -3536 -25 -220 -25 -916 0 -1136 154 -1342 780
|
||||
-2504 1802 -3342 742 -609 1640 -983 2637 -1097 220 -25 915 -25 1138 0 1649
|
||||
187 3040 1107 3839 2540 319 572 519 1205 598 1899 25 220 25 916 0 1136 -272
|
||||
2372 -2067 4171 -4427 4438 -136 15 -842 29 -940 18z m670 -825 c683 -62 1338
|
||||
-282 1910 -642 l85 -54 6 -79 c3 -46 16 -103 29 -136 22 -57 22 -59 5 -103
|
||||
-59 -155 -443 -516 -731 -688 -138 -83 -405 -168 -570 -182 -84 -8 -196 6
|
||||
-394 46 -257 53 -610 73 -870 50 -1126 -100 -2011 -942 -2187 -2081 -25 -163
|
||||
-25 -601 1 -785 108 -784 631 -1513 1331 -1855 223 -109 407 -170 655 -216
|
||||
143 -26 698 -32 859 -9 248 36 473 106 720 226 90 43 165 79 168 79 2 0 89
|
||||
-83 193 -184 284 -276 310 -290 530 -284 110 3 146 8 193 27 81 32 168 114
|
||||
209 199 l33 67 0 160 c0 223 -5 234 -235 471 -95 97 -176 187 -182 199 -9 19
|
||||
-1 37 49 116 261 411 377 802 390 1304 11 452 -61 826 -228 1183 -116 247
|
||||
-206 320 -408 329 -228 11 -348 -48 -425 -207 -30 -64 -31 -68 -31 -215 l0
|
||||
-150 61 -130 c136 -287 178 -458 178 -730 1 -177 -1 -198 -28 -305 -40 -160
|
||||
-79 -268 -142 -390 -83 -161 -73 -155 -147 -84 -364 351 -377 363 -440 394
|
||||
l-65 32 -173 -4 c-199 -3 -235 -15 -327 -108 -88 -87 -107 -149 -107 -340 0
|
||||
-223 -4 -216 251 -478 108 -112 195 -204 193 -206 -31 -27 -207 -88 -332 -115
|
||||
-223 -48 -367 -48 -595 -1 -578 120 -1041 544 -1237 1134 -318 959 385 1998
|
||||
1426 2106 127 13 267 -2 544 -57 l230 -46 275 -4 c218 -4 299 -2 390 11 469
|
||||
66 863 279 1305 706 397 384 395 383 529 315 57 -29 81 -35 131 -35 72 0 66 5
|
||||
155 -128 578 -864 822 -1900 680 -2878 -163 -1115 -762 -2124 -1663 -2802
|
||||
-878 -660 -1994 -961 -3032 -816 -1884 261 -3370 1736 -3645 3618 -80 550 -42
|
||||
1102 116 1656 477 1672 1877 2869 3609 3085 212 26 554 33 755 14z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m17 2 4 4-4 4" />
|
||||
<path d="M3 11v-1a4 4 0 0 1 4-4h14" />
|
||||
<path d="m7 22-4-4 4-4" />
|
||||
<path d="M21 13v1a4 4 0 0 1-4 4H3" />
|
||||
<path d="M11 10h1v4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m17 2 4 4-4 4" />
|
||||
<path d="M3 11v-1a4 4 0 0 1 4-4h14" />
|
||||
<path d="m7 22-4-4 4-4" />
|
||||
<path d="M21 13v1a4 4 0 0 1-4 4H3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 251 B |
@@ -0,0 +1,18 @@
|
||||
<!-- @license lucide-static v1.17.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-settings-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 17H5" />
|
||||
<path d="M19 7h-9" />
|
||||
<circle cx="17" cy="17" r="3" />
|
||||
<circle cx="7" cy="7" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 400 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m18 14 4 4-4 4" />
|
||||
<path d="m18 2 4 4-4 4" />
|
||||
<path d="M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22" />
|
||||
<path d="M2 6h1.972a4 4 0 0 1 3.6 2.2" />
|
||||
<path d="M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 420 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17.971 4.285A2 2 0 0 1 21 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z" />
|
||||
<path d="M3 20V4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 311 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 4v16" />
|
||||
<path d="M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 309 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="8" r="5" />
|
||||
<path d="M20 21a8 8 0 0 0-16 0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z" />
|
||||
<path d="M16 9a5 5 0 0 1 0 6" />
|
||||
<path d="M19.364 18.364a9 9 0 0 0 0-12.728" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 452 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 5 6 9H2v6h4l5 4z" />
|
||||
<line x1="22" x2="16" y1="9" y2="15" />
|
||||
<line x1="16" x2="22" y1="9" y2="15" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 304 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1,15 @@
|
||||
<?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:#888888;}
|
||||
</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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#888888" 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>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 677 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#888888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 5h20" />
|
||||
<path d="M6 12h12" />
|
||||
<path d="M9 19h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,15 @@
|
||||
<?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:#ffffff;}
|
||||
</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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 677 B |
@@ -0,0 +1,5 @@
|
||||
<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 5h20" />
|
||||
<path d="M6 12h12" />
|
||||
<path d="M9 19h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,15 @@
|
||||
<?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:#cccccc;}
|
||||
</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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#cccccc" 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>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 677 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#cccccc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 5h20" />
|
||||
<path d="M6 12h12" />
|
||||
<path d="M9 19h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,15 @@
|
||||
<?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:#fbbf24;}
|
||||
</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>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" 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>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,7 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 677 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 5h20" />
|
||||
<path d="M6 12h12" />
|
||||
<path d="M9 19h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,111 @@
|
||||
//! Artwork pipeline for the Home view — a minimal port of the fetch side
|
||||
//! of `crates/qbz/src/artwork.rs` onto `qbz_cache::ImageCacheService`.
|
||||
//!
|
||||
//! Flow: when home sections are built, each card whose `art_url` is
|
||||
//! already on disk gets `artPath = file://<cached path>` synchronously;
|
||||
//! the misses are downloaded on tokio tasks (semaphore cap 16, shared
|
||||
//! reqwest client, 10s timeout — the artwork.rs constants), stored via
|
||||
//! `ImageCacheService::store`, and the section list is then republished.
|
||||
//!
|
||||
//! POC-NOTE: on download completion the WHOLE section list is republished
|
||||
//! (a `homeSectionsJson` swap) — per-row QAbstractListModel updates are
|
||||
//! the documented follow-up. No RGBA decode in Rust: QML `Image` decodes
|
||||
//! `file://` asynchronously and natively. Cache eviction (`evict`,
|
||||
//! 200 MB cap in the Slint app) is not wired — POC scale.
|
||||
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use qbz_cache::ImageCacheService;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
use crate::home_qt::HomeSection;
|
||||
|
||||
/// Matches artwork.rs `MAX_CONCURRENT`.
|
||||
const MAX_CONCURRENT: usize = 16;
|
||||
/// Matches artwork.rs `HTTP_TIMEOUT` (a hung request on a captive portal
|
||||
/// must not pin a semaphore permit indefinitely).
|
||||
const HTTP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
/// Shared client (artwork.rs: "instead of reqwest::get building a fresh
|
||||
/// client + TLS state per image").
|
||||
static HTTP: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.build()
|
||||
.expect("reqwest client")
|
||||
});
|
||||
|
||||
/// The process-wide image cache (`~/.cache/qbz/images`, shared with the
|
||||
/// Slint/Tauri apps). `Connection` is not Sync, hence the Mutex; `get` /
|
||||
/// `store` are short blocking calls, fine under it.
|
||||
static CACHE: OnceLock<Mutex<ImageCacheService>> = OnceLock::new();
|
||||
|
||||
fn cache() -> Option<&'static Mutex<ImageCacheService>> {
|
||||
if CACHE.get().is_none() {
|
||||
match ImageCacheService::new() {
|
||||
Ok(service) => {
|
||||
let _ = CACHE.set(Mutex::new(service));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("[qbz-qt] image cache open failed: {e}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
CACHE.get()
|
||||
}
|
||||
|
||||
/// Fill `art_path` from the disk cache for every card that already has one.
|
||||
/// Returns the urls still missing (deduped, non-empty).
|
||||
pub fn attach_cached(sections: &mut [HomeSection]) -> Vec<String> {
|
||||
let mut missing: Vec<String> = Vec::new();
|
||||
for section in sections.iter_mut() {
|
||||
for card in section.items.iter_mut() {
|
||||
if card.art_url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let hit = cache().and_then(|c| c.lock().unwrap().get(&card.art_url));
|
||||
if let Some(path) = hit {
|
||||
card.art_path = format!("file://{}", path.display());
|
||||
} else if !missing.contains(&card.art_url) {
|
||||
missing.push(card.art_url.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
missing
|
||||
}
|
||||
|
||||
/// Download the missing urls concurrently (semaphore-capped), storing each
|
||||
/// into the image cache. Returns when all downloads settled (failures are
|
||||
/// logged and skipped — the cards keep their placeholder).
|
||||
pub async fn download_missing(urls: Vec<String>) {
|
||||
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
|
||||
let mut handles = Vec::with_capacity(urls.len());
|
||||
for url in urls {
|
||||
let sem = Arc::clone(&semaphore);
|
||||
handles.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.expect("semaphore open");
|
||||
let bytes = match HTTP.get(&url).send().await {
|
||||
Ok(resp) => match resp.bytes().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] artwork read failed ({url}): {e}");
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] artwork download failed ({url}): {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(cache) = cache() {
|
||||
if let Err(e) = cache.lock().unwrap().store(&url, &bytes) {
|
||||
log::warn!("[qbz-qt] artwork store failed ({url}): {e}");
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,16 @@ pub mod qbz_bridge {
|
||||
// POC: empty until phase 4 (QML shows the empty states).
|
||||
#[qproperty(QList_QVariant, queue_model)]
|
||||
|
||||
// --- Discover > Home ---------------------------------------------
|
||||
#[qproperty(bool, home_loading)]
|
||||
#[qproperty(QString, home_error)]
|
||||
// POC-NOTE: the brief's QVariantList-of-QVariantMap shape is not
|
||||
// expressible in cxx-qt-lib 0.7.3 (no QVariantValue impls for
|
||||
// QMap/QList — a QVariant can't hold a nested map/list), so the
|
||||
// sections travel as ONE JSON document; QML JSON.parse()s it into
|
||||
// the exact same {id, title, kind, items:[{...}]} object graph.
|
||||
#[qproperty(QString, home_sections_json)]
|
||||
|
||||
type QbzBridge = super::QbzBridgeRust;
|
||||
|
||||
/// Called once from Main.qml's Component.onCompleted: registers the
|
||||
@@ -163,6 +173,11 @@ pub mod qbz_bridge {
|
||||
fn toggle_shuffle(self: Pin<&mut QbzBridge>);
|
||||
#[qinvokable]
|
||||
fn cycle_repeat(self: Pin<&mut QbzBridge>);
|
||||
|
||||
/// Home view retry button / manual refresh: refetch the discover
|
||||
/// index + rails and republish `homeSectionsJson`.
|
||||
#[qinvokable]
|
||||
fn reload_home(self: Pin<&mut QbzBridge>);
|
||||
}
|
||||
|
||||
impl cxx_qt::Threading for QbzBridge {}
|
||||
@@ -214,6 +229,9 @@ pub struct QbzBridgeRust {
|
||||
np_quality_tier: QString,
|
||||
np_quality_label: QString,
|
||||
queue_model: QListQVariant,
|
||||
home_loading: bool,
|
||||
home_error: QString,
|
||||
home_sections_json: QString,
|
||||
}
|
||||
|
||||
impl Default for QbzBridgeRust {
|
||||
@@ -255,6 +273,9 @@ impl Default for QbzBridgeRust {
|
||||
np_repeat_mode: 0,
|
||||
np_quality_label: QString::default(),
|
||||
queue_model: QListQVariant::default(),
|
||||
home_loading: false,
|
||||
home_error: QString::default(),
|
||||
home_sections_json: QString::from("[]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,4 +370,8 @@ impl qbz_bridge::QbzBridge {
|
||||
pub fn cycle_repeat(self: Pin<&mut Self>) {
|
||||
crate::now_playing::cycle_repeat();
|
||||
}
|
||||
|
||||
pub fn reload_home(self: Pin<&mut Self>) {
|
||||
crate::reload_home();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
//! Discover > Home data layer — a Slint-free port of the PURE mapping
|
||||
//! logic of `crates/qbz/src/home.rs` (+ the personalized rails of
|
||||
//! `crates/qbz/src/foryou.rs` that need only a live session).
|
||||
//!
|
||||
//! Produces plain data rows; serialization to the bridge happens as one
|
||||
//! JSON document (`homeSectionsJson`) — see the POC-NOTE in
|
||||
//! `publish_sections` for why not QVariantList-of-QVariantMap.
|
||||
//!
|
||||
//! POC-NOTEs (skipped vs the Slint controller):
|
||||
//! - Genre filter: OUT OF SCOPE — `get_discover_index(None)` (the
|
||||
//! no-filter default).
|
||||
//! - Artist/album blacklist filtering (T8): the blacklist store is not
|
||||
//! opened in this POC (phase-1 skip), so no rows are dropped.
|
||||
//! - Recently-played rails (local play-history store): OUT OF SCOPE — the
|
||||
//! view renders the Slint empty-data placeholders instead.
|
||||
//! - Reco-scored taste ordering of favorite albums (reco store skipped):
|
||||
//! favorites render in plain favorite order.
|
||||
//! - Pinned / Most Played Albums / Qobuz Mixes rows: stores/static tiles
|
||||
//! out of scope.
|
||||
//! - Discover prefs (per-tab section visibility/reorder): OUT OF SCOPE —
|
||||
//! a fixed superset order renders (see `SECTION_ORDER`).
|
||||
//! - Editor's Picks / For You / Recommendations tabs: placeholder pages
|
||||
//! (the editor section set is not built).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use qbz_app::shell::AppRuntime;
|
||||
use qbz_core::FrontendAdapter;
|
||||
use qbz_models::{
|
||||
Album, AlbumAward, Artist, DiscoverAlbum, DiscoverAudioInfo, DiscoverContainer,
|
||||
DiscoverPlaylist,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
/// One card row in a section rail (superset of home.rs's CardData /
|
||||
/// SlimData / PlaylistCardData / foryou ArtistSlim — unused fields stay
|
||||
/// empty per section kind).
|
||||
#[derive(Clone, Default, Serialize)]
|
||||
pub struct HomeCard {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
#[serde(rename = "artistId")]
|
||||
pub artist_id: String,
|
||||
pub genre: String,
|
||||
pub year: String,
|
||||
#[serde(rename = "qualityTier")]
|
||||
pub quality_tier: String,
|
||||
#[serde(rename = "qualityLabel")]
|
||||
pub quality_label: String,
|
||||
#[serde(rename = "qualityDetail")]
|
||||
pub quality_detail: String,
|
||||
pub ribbon: String,
|
||||
#[serde(rename = "ribbonKind")]
|
||||
pub ribbon_kind: String,
|
||||
/// Slim rows ("Popular albums"): the 1-based rank ("" = none).
|
||||
pub rank: String,
|
||||
/// Playlist rows: the UPPERCASE first-tag category subtag.
|
||||
pub category: String,
|
||||
#[serde(rename = "artUrl")]
|
||||
pub art_url: String,
|
||||
/// `file://<cached path>` when already on disk ("" = needs download).
|
||||
#[serde(rename = "artPath")]
|
||||
pub art_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct HomeSection {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
/// "album" | "slim" | "playlist" | "artists" | "recentPlaceholder".
|
||||
pub kind: String,
|
||||
/// Placeholder hint for recentPlaceholder sections.
|
||||
pub hint: String,
|
||||
pub items: Vec<HomeCard>,
|
||||
}
|
||||
|
||||
/// Push one album-carousel section from its discover container (drops
|
||||
/// empty/missing containers, like home.rs `push_section`).
|
||||
fn push_albums(
|
||||
out: &mut Vec<HomeSection>,
|
||||
id: &str,
|
||||
title: String,
|
||||
container: Option<DiscoverContainer<DiscoverAlbum>>,
|
||||
) {
|
||||
let Some(container) = container else {
|
||||
return;
|
||||
};
|
||||
if container.data.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
out.push(HomeSection {
|
||||
id: id.to_string(),
|
||||
title,
|
||||
kind: "album".to_string(),
|
||||
hint: String::new(),
|
||||
items: container.data.items.into_iter().map(map_album).collect(),
|
||||
});
|
||||
}
|
||||
|
||||
/// The fixed render order (prefs out of scope — see module docs). Mirrors
|
||||
/// the default-enabled Home order for the shared sections, then the
|
||||
/// optional rails whose data is fetched anyway.
|
||||
fn build_sections(
|
||||
containers: qbz_models::DiscoverContainers,
|
||||
favorite_albums: Vec<HomeCard>,
|
||||
release_watch: Vec<HomeCard>,
|
||||
top_artists: Vec<HomeCard>,
|
||||
) -> Vec<HomeSection> {
|
||||
let mut out: Vec<HomeSection> = Vec::new();
|
||||
|
||||
push_albums(&mut out, "newReleases", qbz_i18n::t("New Releases"), containers.new_releases);
|
||||
push_albums(&mut out, "pressAwards", qbz_i18n::t("Press Accolades"), containers.press_awards);
|
||||
|
||||
// Qobuz Playlists row (single-cover cards, first-tag category subtag).
|
||||
let playlists: Vec<HomeCard> = containers
|
||||
.playlists
|
||||
.map(|c| c.data.items)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.take(40)
|
||||
.map(map_playlist)
|
||||
.collect();
|
||||
if !playlists.is_empty() {
|
||||
out.push(HomeSection {
|
||||
id: "qobuzPlaylists".to_string(),
|
||||
title: qbz_i18n::t("Qobuz Playlists"),
|
||||
kind: "playlist".to_string(),
|
||||
hint: String::new(),
|
||||
items: playlists,
|
||||
});
|
||||
}
|
||||
|
||||
// Recently-played rails are OUT OF SCOPE (local store) — the Slint
|
||||
// empty-data placeholders render instead (always present on Home).
|
||||
out.push(HomeSection {
|
||||
id: "recentlyPlayedAlbums".to_string(),
|
||||
title: qbz_i18n::t("Recently Played Albums"),
|
||||
kind: "recentPlaceholder".to_string(),
|
||||
hint: qbz_i18n::t("Albums you play will appear here."),
|
||||
items: Vec::new(),
|
||||
});
|
||||
out.push(HomeSection {
|
||||
id: "continueListening".to_string(),
|
||||
title: qbz_i18n::t("Recently Played Tracks"),
|
||||
kind: "recentPlaceholder".to_string(),
|
||||
hint: qbz_i18n::t("Tracks you play will appear here."),
|
||||
items: Vec::new(),
|
||||
});
|
||||
|
||||
push_albums(
|
||||
&mut out,
|
||||
"idealDiscography",
|
||||
qbz_i18n::t("Ideal Discography"),
|
||||
containers.ideal_discography,
|
||||
);
|
||||
|
||||
// Most Streamed on Home renders as the "Popular albums" slim grid,
|
||||
// capped at 24 (two carousel pages of 12), 1-based ranked.
|
||||
let popular: Vec<HomeCard> = containers
|
||||
.most_streamed
|
||||
.map(|container| container.data.items)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.take(24)
|
||||
.enumerate()
|
||||
.map(|(index, album)| map_slim(index, album))
|
||||
.collect();
|
||||
if !popular.is_empty() {
|
||||
out.push(HomeSection {
|
||||
id: "mostStreamed".to_string(),
|
||||
title: qbz_i18n::t("Popular albums"),
|
||||
kind: "slim".to_string(),
|
||||
hint: String::new(),
|
||||
items: popular,
|
||||
});
|
||||
}
|
||||
|
||||
push_albums(
|
||||
&mut out,
|
||||
"editorPicks",
|
||||
qbz_i18n::t("Albums of the Week"),
|
||||
containers.album_of_the_week,
|
||||
);
|
||||
push_albums(&mut out, "qobuzissimes", qbz_i18n::t("Qobuzissimes"), containers.qobuzissims);
|
||||
|
||||
// Personalized rails (self-hide while empty, 1:1 Slint).
|
||||
if !favorite_albums.is_empty() {
|
||||
out.push(HomeSection {
|
||||
id: "favoriteAlbums".to_string(),
|
||||
title: qbz_i18n::t("Library Albums"),
|
||||
kind: "album".to_string(),
|
||||
hint: String::new(),
|
||||
items: favorite_albums,
|
||||
});
|
||||
}
|
||||
if !release_watch.is_empty() {
|
||||
out.push(HomeSection {
|
||||
id: "releaseWatch".to_string(),
|
||||
title: qbz_i18n::t("Release Watch"),
|
||||
kind: "album".to_string(),
|
||||
hint: String::new(),
|
||||
items: release_watch,
|
||||
});
|
||||
}
|
||||
if !top_artists.is_empty() {
|
||||
out.push(HomeSection {
|
||||
id: "topArtists".to_string(),
|
||||
title: qbz_i18n::t("Your Top Artists"),
|
||||
kind: "artists".to_string(),
|
||||
hint: String::new(),
|
||||
items: top_artists,
|
||||
});
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Fetch the discover index (UNFILTERED — genre filter out of scope) and
|
||||
/// the personalized rails concurrently (mirrors home.rs's `join!`), then
|
||||
/// map everything into plain rows.
|
||||
pub async fn load_home<A>(runtime: &Arc<AppRuntime<A>>) -> Result<Vec<HomeSection>, String>
|
||||
where
|
||||
A: FrontendAdapter + Send + Sync + 'static,
|
||||
{
|
||||
let (response, favorite_albums, release_watch, top_artists) = tokio::join!(
|
||||
runtime.core().get_discover_index(None),
|
||||
favorite_album_cards(runtime),
|
||||
fetch_release_watch(runtime),
|
||||
top_artist_cards(runtime),
|
||||
);
|
||||
let response = response.map_err(|e| e.to_string())?;
|
||||
log::info!(
|
||||
"[qbz-qt] discover index fetched; building home sections (fav={}, rw={}, artists={})",
|
||||
favorite_albums.len(),
|
||||
release_watch.len(),
|
||||
top_artists.len(),
|
||||
);
|
||||
Ok(build_sections(
|
||||
response.containers,
|
||||
favorite_albums,
|
||||
release_watch,
|
||||
top_artists,
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure mapping — ported from home.rs (Discover* inputs).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub(crate) fn map_album(album: DiscoverAlbum) -> HomeCard {
|
||||
let artist = album
|
||||
.artists
|
||||
.first()
|
||||
.map(|a| a.name.clone())
|
||||
.unwrap_or_default();
|
||||
let artist_id = album
|
||||
.artists
|
||||
.first()
|
||||
.map(|a| a.id.to_string())
|
||||
.unwrap_or_default();
|
||||
let genre = album.genre.map(|g| g.name).unwrap_or_default();
|
||||
let year = qbz_text_utils::dates::release_label(
|
||||
album
|
||||
.dates
|
||||
.as_ref()
|
||||
.and_then(|d| d.original.as_ref().or(d.download.as_ref()).or(d.stream.as_ref()))
|
||||
.map(|s| s.as_str()),
|
||||
);
|
||||
let (ribbon, ribbon_kind) = pick_ribbon(album.awards.as_deref());
|
||||
let artwork_url = album
|
||||
.image
|
||||
.large
|
||||
.or(album.image.thumbnail)
|
||||
.or(album.image.small)
|
||||
.unwrap_or_default();
|
||||
HomeCard {
|
||||
id: album.id,
|
||||
title: album.title,
|
||||
artist,
|
||||
artist_id,
|
||||
genre,
|
||||
year,
|
||||
quality_tier: quality_tier(album.audio_info.as_ref()).to_string(),
|
||||
quality_label: quality_label(album.audio_info.as_ref()),
|
||||
quality_detail: quality_detail(album.audio_info.as_ref()),
|
||||
ribbon,
|
||||
ribbon_kind,
|
||||
art_url: artwork_url,
|
||||
..HomeCard::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a Discover playlist into a single-cover card (1:1 home.rs
|
||||
/// `map_playlist`): landscape `rectangle` preferred, first square cover
|
||||
/// fallback; first tag -> UPPERCASE category subtag.
|
||||
fn map_playlist(p: DiscoverPlaylist) -> HomeCard {
|
||||
let art_url = p
|
||||
.image
|
||||
.rectangle
|
||||
.or_else(|| p.image.covers.and_then(|c| c.into_iter().next()))
|
||||
.unwrap_or_default();
|
||||
let category = p
|
||||
.tags
|
||||
.as_ref()
|
||||
.and_then(|t| t.first())
|
||||
.map(|t| t.name.to_uppercase())
|
||||
.unwrap_or_default();
|
||||
HomeCard {
|
||||
id: p.id.to_string(),
|
||||
title: p.name,
|
||||
category,
|
||||
art_url,
|
||||
..HomeCard::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A compact ranked slim item (home.rs `map_slim`).
|
||||
fn map_slim(index: usize, album: DiscoverAlbum) -> HomeCard {
|
||||
let subtitle = album
|
||||
.artists
|
||||
.first()
|
||||
.map(|a| a.name.clone())
|
||||
.unwrap_or_default();
|
||||
let art_url = album
|
||||
.image
|
||||
.thumbnail
|
||||
.or(album.image.small)
|
||||
.or(album.image.large)
|
||||
.unwrap_or_default();
|
||||
HomeCard {
|
||||
id: album.id,
|
||||
title: album.title,
|
||||
artist: subtitle,
|
||||
rank: (index + 1).to_string(),
|
||||
art_url,
|
||||
..HomeCard::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the single award ribbon, mirroring `pickAlbumRibbon`: award id 151
|
||||
/// = Album of the Week, 88 = Qobuzissime, otherwise the last award becomes
|
||||
/// a generic "press" ribbon.
|
||||
pub(crate) fn pick_ribbon(awards: Option<&[AlbumAward]>) -> (String, String) {
|
||||
let Some(awards) = awards else {
|
||||
return (String::new(), String::new());
|
||||
};
|
||||
if awards.is_empty() {
|
||||
return (String::new(), String::new());
|
||||
}
|
||||
if let Some(a) = awards.iter().find(|a| a.id.as_deref() == Some("151")) {
|
||||
return (a.name.clone(), "albumOfTheWeek".to_string());
|
||||
}
|
||||
if let Some(a) = awards.iter().find(|a| a.id.as_deref() == Some("88")) {
|
||||
return (a.name.clone(), "qobuzissime".to_string());
|
||||
}
|
||||
let last = awards.last().expect("non-empty checked above");
|
||||
(last.name.clone(), "press".to_string())
|
||||
}
|
||||
|
||||
/// Quality tier for the icon-only badge: 24-bit and up is Hi-Res, anything
|
||||
/// else with audio info is CD-quality, no audio info hides the badge.
|
||||
pub(crate) fn quality_tier(audio: Option<&DiscoverAudioInfo>) -> &'static str {
|
||||
let Some(audio) = audio else {
|
||||
return "";
|
||||
};
|
||||
match audio.maximum_bit_depth {
|
||||
Some(depth) if depth >= 24 => "hires",
|
||||
_ => "cd",
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact-quality label for the badge hover tooltip
|
||||
/// (`{tier}: {depth}-bit / {rate} kHz`).
|
||||
pub(crate) fn quality_label(audio: Option<&DiscoverAudioInfo>) -> String {
|
||||
let Some(audio) = audio else {
|
||||
return String::new();
|
||||
};
|
||||
let hi_res = matches!(audio.maximum_bit_depth, Some(depth) if depth >= 24);
|
||||
let tier = if hi_res { "Hi-Res" } else { "CD" };
|
||||
let depth = audio
|
||||
.maximum_bit_depth
|
||||
.unwrap_or(if hi_res { 24 } else { 16 });
|
||||
let rate = audio
|
||||
.maximum_sampling_rate
|
||||
.unwrap_or(if hi_res { 96.0 } else { 44.1 });
|
||||
format!("{tier}: {depth}-bit / {} kHz", format_rate(rate))
|
||||
}
|
||||
|
||||
/// Bare exact-quality detail ("24-bit / 96 kHz", no tier prefix).
|
||||
pub(crate) fn quality_detail(audio: Option<&DiscoverAudioInfo>) -> String {
|
||||
let Some(audio) = audio else {
|
||||
return String::new();
|
||||
};
|
||||
let hi_res = matches!(audio.maximum_bit_depth, Some(depth) if depth >= 24);
|
||||
let depth = audio
|
||||
.maximum_bit_depth
|
||||
.unwrap_or(if hi_res { 24 } else { 16 });
|
||||
let rate = audio
|
||||
.maximum_sampling_rate
|
||||
.unwrap_or(if hi_res { 96.0 } else { 44.1 });
|
||||
format!("{depth}-bit / {} kHz", format_rate(rate))
|
||||
}
|
||||
|
||||
/// Format a kHz sample rate without a trailing `.0` (96.0 -> "96").
|
||||
pub(crate) fn format_rate(rate: f64) -> String {
|
||||
if (rate.fract()).abs() < f64::EPSILON {
|
||||
format!("{}", rate as i64)
|
||||
} else {
|
||||
format!("{rate}")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Personalized rails — ported from foryou.rs (live-session only; the reco /
|
||||
// blacklist stores are skipped, see module docs).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Flat-Album card mapping (foryou.rs `map_album`).
|
||||
fn map_flat_album(album: Album) -> HomeCard {
|
||||
let year = album
|
||||
.release_date_original
|
||||
.as_deref()
|
||||
.and_then(|s| s.get(..4).map(|y| y.to_string()))
|
||||
.unwrap_or_default();
|
||||
let quality_tier = match album.maximum_bit_depth {
|
||||
Some(d) if d >= 24 => "hires",
|
||||
Some(_) => "cd",
|
||||
None => "",
|
||||
}
|
||||
.to_string();
|
||||
let quality_label = match (album.maximum_bit_depth, album.maximum_sampling_rate) {
|
||||
(Some(bd), Some(sr)) => format!("{}-bit / {} kHz", bd, sr),
|
||||
_ => String::new(),
|
||||
};
|
||||
HomeCard {
|
||||
id: album.id,
|
||||
title: album.title,
|
||||
artist: album.artist.name,
|
||||
artist_id: album.artist.id.to_string(),
|
||||
year,
|
||||
quality_tier,
|
||||
quality_label,
|
||||
art_url: album.image.best().cloned().unwrap_or_default(),
|
||||
..HomeCard::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// "Library Albums" — favorite albums, capped at 18, in favorite order
|
||||
/// (reco taste-ordering skipped — cold reco store = no reorder upstream).
|
||||
async fn favorite_album_cards<A>(runtime: &Arc<AppRuntime<A>>) -> Vec<HomeCard>
|
||||
where
|
||||
A: FrontendAdapter + Send + Sync + 'static,
|
||||
{
|
||||
match runtime.core().get_favorites("albums", 100, 0).await {
|
||||
Ok(value) => qbz_models::lenient::parse_items_array::<Album>(&value, "albums", "home fav album")
|
||||
.into_iter()
|
||||
.take(18)
|
||||
.map(map_flat_album)
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] favorite albums fetch failed: {e}");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// "Release Watch" — `/release/watch` artists, capped 18 (blacklist filter
|
||||
/// skipped — the store is not open in this POC).
|
||||
async fn fetch_release_watch<A>(runtime: &Arc<AppRuntime<A>>) -> Vec<HomeCard>
|
||||
where
|
||||
A: FrontendAdapter + Send + Sync + 'static,
|
||||
{
|
||||
match runtime.core().get_release_watch("artists", 18, 0).await {
|
||||
Ok(page) => page.items.into_iter().map(map_flat_album).collect(),
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] release watch fetch failed: {e}");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// "Your Top Artists" — favorite artists, capped 18.
|
||||
async fn top_artist_cards<A>(runtime: &Arc<AppRuntime<A>>) -> Vec<HomeCard>
|
||||
where
|
||||
A: FrontendAdapter + Send + Sync + 'static,
|
||||
{
|
||||
match runtime.core().get_favorites("artists", 50, 0).await {
|
||||
Ok(value) => qbz_models::lenient::parse_items_array::<Artist>(&value, "artists", "home artist")
|
||||
.into_iter()
|
||||
.take(18)
|
||||
.map(|a| HomeCard {
|
||||
id: a.id.to_string(),
|
||||
title: a.name,
|
||||
art_url: a
|
||||
.image
|
||||
.and_then(|img| img.best().cloned())
|
||||
.unwrap_or_default(),
|
||||
..HomeCard::default()
|
||||
})
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] favorite artists fetch failed: {e}");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn audio(depth: Option<u32>, rate: Option<f64>) -> DiscoverAudioInfo {
|
||||
DiscoverAudioInfo {
|
||||
maximum_sampling_rate: rate,
|
||||
maximum_bit_depth: depth,
|
||||
maximum_channel_count: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn award(id: Option<&str>, name: &str) -> AlbumAward {
|
||||
AlbumAward {
|
||||
id: id.map(|s| s.to_string()),
|
||||
name: name.to_string(),
|
||||
awarded_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_tier_mapping() {
|
||||
assert_eq!(quality_tier(None), "");
|
||||
assert_eq!(quality_tier(Some(&audio(Some(24), Some(96.0)))), "hires");
|
||||
assert_eq!(quality_tier(Some(&audio(Some(16), Some(44.1)))), "cd");
|
||||
// Bit depth present but < 24 is CD even without a rate.
|
||||
assert_eq!(quality_tier(Some(&audio(Some(16), None))), "cd");
|
||||
// No depth at all still maps to CD (audio info exists).
|
||||
assert_eq!(quality_tier(Some(&audio(None, None))), "cd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_label_and_detail() {
|
||||
assert_eq!(quality_label(None), "");
|
||||
assert_eq!(quality_detail(None), "");
|
||||
assert_eq!(
|
||||
quality_label(Some(&audio(Some(24), Some(96.0)))),
|
||||
"Hi-Res: 24-bit / 96 kHz"
|
||||
);
|
||||
assert_eq!(
|
||||
quality_label(Some(&audio(Some(16), Some(44.1)))),
|
||||
"CD: 16-bit / 44.1 kHz"
|
||||
);
|
||||
assert_eq!(
|
||||
quality_detail(Some(&audio(Some(24), Some(192.0)))),
|
||||
"24-bit / 192 kHz"
|
||||
);
|
||||
// Missing fields fall back per tier (hi-res: 24/96, cd: 16/44.1).
|
||||
assert_eq!(quality_label(Some(&audio(Some(24), None))), "Hi-Res: 24-bit / 96 kHz");
|
||||
assert_eq!(quality_detail(Some(&audio(None, Some(44.1)))), "16-bit / 44.1 kHz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_rate_trailing_zero() {
|
||||
assert_eq!(format_rate(96.0), "96");
|
||||
assert_eq!(format_rate(44.1), "44.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_ribbon_precedence() {
|
||||
assert_eq!(pick_ribbon(None), (String::new(), String::new()));
|
||||
assert_eq!(pick_ribbon(Some(&[])), (String::new(), String::new()));
|
||||
// 151 wins over everything.
|
||||
assert_eq!(
|
||||
pick_ribbon(Some(&[award(Some("88"), "Qobuzissime"), award(Some("151"), "AOTW")])),
|
||||
("AOTW".to_string(), "albumOfTheWeek".to_string())
|
||||
);
|
||||
// 88 wins over a generic press award.
|
||||
assert_eq!(
|
||||
pick_ribbon(Some(&[award(Some("1"), "Press X"), award(Some("88"), "Qobuzissime")])),
|
||||
("Qobuzissime".to_string(), "qobuzissime".to_string())
|
||||
);
|
||||
// Otherwise the LAST award is the press ribbon.
|
||||
assert_eq!(
|
||||
pick_ribbon(Some(&[award(Some("1"), "First"), award(Some("2"), "Last")])),
|
||||
("Last".to_string(), "press".to_string())
|
||||
);
|
||||
// Awards without ids are skipped by the id lookups.
|
||||
assert_eq!(
|
||||
pick_ribbon(Some(&[award(None, "NoId")])),
|
||||
("NoId".to_string(), "press".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@
|
||||
//! async work; results hop back to Qt through the bridge's `CxxQtThread`.
|
||||
|
||||
mod auth_qt;
|
||||
mod artwork_qt;
|
||||
mod bridge;
|
||||
mod home_qt;
|
||||
mod nav_qt;
|
||||
mod now_playing;
|
||||
mod offline_fwd;
|
||||
@@ -43,6 +45,10 @@ static QT_THREAD: OnceLock<CxxQtThread<QbzBridge>> = OnceLock::new();
|
||||
/// (mirrors `login_task` in crates/qbz/src/main.rs).
|
||||
static LOGIN_TASK: Mutex<Option<JoinHandle<()>>> = Mutex::new(None);
|
||||
|
||||
/// Whether the Home view has been loaded this session (the auto-load fires
|
||||
/// once per shell entry; `reloadHome()` bypasses the flag).
|
||||
static HOME_LOADED: Mutex<bool> = Mutex::new(false);
|
||||
|
||||
pub(crate) fn register_qt_thread(thread: CxxQtThread<QbzBridge>) {
|
||||
if QT_THREAD.set(thread).is_err() {
|
||||
log::warn!("[qbz-qt] Qt thread handle already registered");
|
||||
@@ -108,6 +114,10 @@ fn enter_shell(session: auth_qt::SessionInfo) {
|
||||
// history and push the current now-playing model onto the bar.
|
||||
nav_qt::record("home");
|
||||
now_playing::publish_current();
|
||||
// Phase 3: fetch Discover > Home (online sessions only — the offline
|
||||
// engine gates Qobuz calls anyway, and the view shows the offline
|
||||
// placeholder instead).
|
||||
load_home_once();
|
||||
ui(move |mut b| {
|
||||
b.as_mut().set_session_user_name(QString::from(session.display_name.as_str()));
|
||||
b.as_mut().set_session_subscription(QString::from(session.subscription.as_str()));
|
||||
@@ -203,17 +213,93 @@ pub(crate) fn do_logout() {
|
||||
if let Err(e) = auth_qt::logout(&runtime).await {
|
||||
log::error!("[qbz-qt] logout failed: {e}");
|
||||
}
|
||||
// A later login must re-fetch Home (new user, fresh rails).
|
||||
*HOME_LOADED.lock().unwrap() = false;
|
||||
ui(|mut b| {
|
||||
b.as_mut().set_session_user_name(QString::from(""));
|
||||
b.as_mut().set_session_user_name(QString::from(""));
|
||||
b.as_mut().set_session_subscription(QString::from(""));
|
||||
b.as_mut().set_login_error(QString::from(""));
|
||||
b.as_mut().set_restore_error(QString::from(""));
|
||||
b.as_mut().set_login_phase(0);
|
||||
b.as_mut().set_home_sections_json(QString::from("[]"));
|
||||
b.as_mut().set_home_error(QString::from(""));
|
||||
b.as_mut().set_home_loading(false);
|
||||
b.as_mut().set_screen(QString::from("login"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================ Discover > Home ==============================
|
||||
|
||||
/// Fire the Home auto-load once per shell entry. Gated on the offline
|
||||
/// engine: offline sessions show the OfflinePlaceholder instead of
|
||||
/// fetching (the Qobuz gate would refuse the calls anyway).
|
||||
fn load_home_once() {
|
||||
if *HOME_LOADED.lock().unwrap() {
|
||||
return;
|
||||
}
|
||||
if offline_fwd::engine().status().is_offline() {
|
||||
log::info!("[qbz-qt] home load skipped (offline session)");
|
||||
return;
|
||||
}
|
||||
*HOME_LOADED.lock().unwrap() = true;
|
||||
reload_home();
|
||||
}
|
||||
|
||||
/// `reloadHome()` invokable / auto-load worker: fetch + publish + artwork.
|
||||
pub(crate) fn reload_home() {
|
||||
if offline_fwd::engine().status().is_offline() {
|
||||
return;
|
||||
}
|
||||
ui(|mut b| {
|
||||
b.as_mut().set_home_loading(true);
|
||||
b.as_mut().set_home_error(QString::from(""));
|
||||
});
|
||||
let runtime = app();
|
||||
spawn(async move {
|
||||
match home_qt::load_home(&runtime).await {
|
||||
Ok(mut sections) => {
|
||||
// Artwork: disk hits attach synchronously; misses download
|
||||
// in the background and trigger ONE republish (POC-NOTE in
|
||||
// artwork_qt.rs — per-row model updates are the follow-up).
|
||||
let missing = artwork_qt::attach_cached(&mut sections);
|
||||
let count: usize = sections.iter().map(|s| s.items.len()).sum();
|
||||
publish_home_sections(§ions);
|
||||
log::info!(
|
||||
"[qbz-qt] home published: {} sections, {} cards, {} artwork misses",
|
||||
sections.len(),
|
||||
count,
|
||||
missing.len(),
|
||||
);
|
||||
if !missing.is_empty() {
|
||||
spawn(async move {
|
||||
artwork_qt::download_missing(missing).await;
|
||||
let mut sections = sections;
|
||||
let _ = artwork_qt::attach_cached(&mut sections);
|
||||
publish_home_sections(§ions);
|
||||
log::info!("[qbz-qt] home republished after artwork downloads");
|
||||
});
|
||||
}
|
||||
ui(|mut b| b.as_mut().set_home_loading(false));
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[qbz-qt] home load failed: {e}");
|
||||
ui(move |mut b| {
|
||||
b.as_mut().set_home_error(QString::from(e.as_str()));
|
||||
b.as_mut().set_home_loading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn publish_home_sections(sections: &[home_qt::HomeSection]) {
|
||||
let json = serde_json::to_string(sections).unwrap_or_else(|_| "[]".to_string());
|
||||
ui(move |mut b| {
|
||||
b.as_mut().set_home_sections_json(QString::from(json.as_str()));
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
qbz_log::install("info");
|
||||
qbz_i18n::set_language(qbz_i18n::resolve_auto());
|
||||
|
||||