One sentence under the subscription notice so the always-visible 'Start offline' link is self-explanatory: QBZ works as an offline player without a Qobuz account, with no access to the Qobuz library. One msgid x7 locales. cc
295 lines
14 KiB
Plaintext
295 lines
14 KiB
Plaintext
// QBZ login screen — first feature screen of the Slint POC.
|
|
//
|
|
// Visual parity target: the current Svelte `LoginView.svelte` on the QBZ
|
|
// dark theme — centered card, logo, wordmark, ToS checkbox, browser
|
|
// sign-in button, secondary links, and the legal disclaimer.
|
|
//
|
|
// No domain logic here: the buttons emit callbacks that the Rust layer
|
|
// turns into typed commands (see `src/commands.rs`).
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Spacing } from "../foundation/spacing.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { QbzCheckbox } from "../primitives/QbzCheckbox.slint";
|
|
import { QbzPrimaryButton } from "../primitives/QbzPrimaryButton.slint";
|
|
import { QbzTextLink } from "../primitives/QbzTextLink.slint";
|
|
import { OfflineState, LoginState } from "../state.slint";
|
|
|
|
export component LoginScreen inherits Rectangle {
|
|
in-out property <bool> tos-accepted: true;
|
|
|
|
callback sign-in-via-browser();
|
|
callback cancel-login();
|
|
callback start-offline();
|
|
callback open-tos();
|
|
|
|
background: Theme.surface-main;
|
|
|
|
VerticalLayout {
|
|
alignment: center;
|
|
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
|
|
Rectangle {
|
|
width: 720px;
|
|
background: Theme.surface-card;
|
|
border-radius: Radius.lg;
|
|
drop-shadow-blur: 32px;
|
|
drop-shadow-color: Theme.card-shadow;
|
|
drop-shadow-offset-y: 8px;
|
|
|
|
VerticalLayout {
|
|
padding: Spacing.card-padding;
|
|
spacing: 0px;
|
|
|
|
// --- Brand ------------------------------------------------
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
Image {
|
|
source: @image-url("../assets/qbz-logo.png");
|
|
width: 140px;
|
|
height: 140px;
|
|
image-fit: contain;
|
|
}
|
|
}
|
|
Rectangle { height: Spacing.sm; }
|
|
Text {
|
|
text: @tr("QBZ");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.wordmark;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 8px;
|
|
horizontal-alignment: center;
|
|
}
|
|
Rectangle { height: 2px; }
|
|
Text {
|
|
text: @tr("QOBUZ™ PLAYER");
|
|
color: Theme.text-muted;
|
|
font-size: Typography.subtitle;
|
|
letter-spacing: 4px;
|
|
horizontal-alignment: center;
|
|
}
|
|
|
|
Rectangle { height: Spacing.xl; }
|
|
|
|
// --- Terms of Service row --------------------------------
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
spacing: Spacing.sm;
|
|
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzCheckbox {
|
|
checked <=> root.tos-accepted;
|
|
}
|
|
}
|
|
Text {
|
|
text: @tr("I have read the");
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
vertical-alignment: center;
|
|
}
|
|
tos-area := TouchArea {
|
|
width: tos-text.preferred-width;
|
|
height: tos-text.preferred-height;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.open-tos();
|
|
}
|
|
tos-text := Text {
|
|
text: @tr("Qobuz™ Terms of Service");
|
|
color: Theme.accent;
|
|
font-size: Typography.body;
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: Spacing.lg; }
|
|
|
|
// --- Sign in ---------------------------------------------
|
|
// Opens the user's default web browser (no embedded
|
|
// webview) — the label + status below must say so.
|
|
QbzPrimaryButton {
|
|
label: @tr("Sign in with your browser");
|
|
enabled: root.tos-accepted && LoginState.phase == 0;
|
|
clicked => {
|
|
root.sign-in-via-browser();
|
|
}
|
|
}
|
|
|
|
Rectangle { height: Spacing.md; }
|
|
|
|
// --- Browser-flow status ---------------------------------
|
|
// The browser may open in the background without stealing
|
|
// focus, and the code exchange takes a few seconds — the
|
|
// screen must narrate instead of sitting inert.
|
|
if LoginState.phase == 1: VerticalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Continue in your web browser — QBZ is waiting for you to finish signing in.");
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
Text {
|
|
text: @tr("The browser may open in the background without taking focus — check your other windows.");
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
Rectangle { height: 4px; }
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
QbzTextLink {
|
|
label: @tr("Cancel");
|
|
clicked => {
|
|
root.cancel-login();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if LoginState.phase == 2: Text {
|
|
text: @tr("Signing you in…");
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
horizontal-alignment: center;
|
|
}
|
|
if LoginState.phase == 0 && LoginState.error != "": VerticalLayout {
|
|
spacing: 0px;
|
|
Rectangle {
|
|
background: Theme.surface-elevated;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
VerticalLayout {
|
|
padding: 12px;
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Sign-in failed");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
horizontal-alignment: center;
|
|
}
|
|
Text {
|
|
text: LoginState.error;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Rectangle { height: 10px; }
|
|
|
|
// --- Offline-boot callout (spec §4.1) --------------------
|
|
// No connectivity: point the user at the Start-offline link
|
|
// right below. Gated on has-previous-session too — the link
|
|
// it points at is hidden without prior user data.
|
|
if OfflineState.has-previous-session && OfflineState.connectivity == 2: VerticalLayout {
|
|
spacing: 2px;
|
|
Rectangle {
|
|
background: Theme.surface-elevated;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
VerticalLayout {
|
|
padding: 12px;
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("No internet connection — you can start in offline mode with your local library and downloads");
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
if OfflineState.captive-portal: Text {
|
|
text: @tr("A network sign-in page may be blocking the connection");
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
}
|
|
// Caret pointing at the Start-offline link below.
|
|
Text {
|
|
text: "▼";
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
}
|
|
}
|
|
|
|
// Init-error variant of the same box (session restore
|
|
// failed for a non-connectivity reason). Hidden while the
|
|
// connectivity box shows — never two boxes at once.
|
|
if OfflineState.login-error != "" && OfflineState.connectivity != 2: VerticalLayout {
|
|
spacing: 0px;
|
|
Rectangle {
|
|
background: Theme.surface-elevated;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
VerticalLayout {
|
|
padding: 12px;
|
|
spacing: 4px;
|
|
Text {
|
|
text: @tr("Could not restore your session");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
horizontal-alignment: center;
|
|
}
|
|
Text {
|
|
text: OfflineState.login-error;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
}
|
|
Rectangle { height: Spacing.sm; }
|
|
}
|
|
|
|
// Always visible (#553): without a previous session this
|
|
// opens the GUEST profile (user 0) — Local Library only,
|
|
// adopted by the account on the first real login.
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
QbzTextLink {
|
|
label: @tr("Start offline (no access to Qobuz™ services)");
|
|
clicked => {
|
|
root.start-offline();
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: Spacing.xl; }
|
|
|
|
// --- Legal disclaimer ------------------------------------
|
|
Text {
|
|
text: @tr("QBZ requires an active Qobuz™ subscription. Your credentials are sent directly to Qobuz™.")
|
|
+ "\n"
|
|
+ @tr("QBZ can be used as an offline player without a Qobuz™ account (no access to the Qobuz™ library).")
|
|
+ "\n"
|
|
+ @tr("This application uses the Qobuz API but is not certified by Qobuz.")
|
|
+ " "
|
|
+ @tr("Qobuz™ is a trademark of Qobuz. QBZ is an open-source application licensed under the MIT License and is not affiliated with, endorsed by, or certified by Qobuz.");
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|