Add the library, collections, and trash views; theme from style.yaml
The UI gains the views the catalog work was building toward: a windowed
library grid with ratings and flags, the collection tree with drag-to-add,
and trash with restore. derived_sync pushes thumbnail shards and the catalog
snapshot to the server's derived folder.
Tokens now have one source of truth. build.rs reads style.yaml and generates
theme.slint into OUT_DIR, which answers every existing
`import { Theme } from "theme.slint"` unchanged, because Slint resolves
imports against the importing file's directory first and the include paths
after. Generating into OUT_DIR rather than beside the hand-written Slint is
the point: a generated file sitting in ui/ looks exactly like the files
around it that are meant to be edited, and an edit to it would survive until
the next touch of style.yaml — a bug that hides for weeks. build.rs fails
loudly if a stale ui/theme.slint exists, which would otherwise shadow the
generated one silently and make every palette change vanish with no error.
The palette moves to near-neutral dark with achromatic signalling, so the
accent means "modified" or "active" rather than "heading". Shared components
land in widgets.slint: a token that binds several values into one concept is
a component, not a row in a YAML file.
Adds an optional live-style feature that makes the tokens in-out so they can
be written at startup — a feature rather than the default because it stops
the properties being constant-folded.
serde_norway is the YAML crate: serde_yaml and serde_yml are both deprecated,
and its mappings preserve insertion order, which is what lets the generated
Slint keep the token ordering the author chose.
Assisted-by: LLM
This commit is contained in:
@@ -29,6 +29,23 @@ pub enum LaunchState {
|
||||
},
|
||||
}
|
||||
|
||||
/// What the app opens on.
|
||||
///
|
||||
/// Three outcomes, and the middle one is easy to lose: a configured library
|
||||
/// means the launch screen is skipped, and skipping it must not also skip
|
||||
/// opening the library — otherwise the app lands on an empty view with no
|
||||
/// route back to the grid, because the "Open library" button is on the screen
|
||||
/// that was never shown.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Startup {
|
||||
/// Files were named on the command line; show those.
|
||||
ShowLocalFiles,
|
||||
/// An account and a folder are configured; scan and show the grid.
|
||||
OpenLibrary,
|
||||
/// Nothing configured; ask the user to sign in.
|
||||
ShowLaunchScreen,
|
||||
}
|
||||
|
||||
/// TRACES: FR-NC-1 | FR-NC-4 | M-1 | M-3 | M-4
|
||||
/// Everything the launch screen renders from.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -197,6 +214,23 @@ impl LaunchModel {
|
||||
self.is_signed_in() && !self.library_root().is_empty()
|
||||
}
|
||||
|
||||
/// What the app should do on startup.
|
||||
///
|
||||
/// Pure so it can be tested without a secret store or a display server —
|
||||
/// and it needs testing, because the interesting case is the one with no
|
||||
/// visible symptom until you are staring at an empty window.
|
||||
pub fn startup_action(&self, have_local_paths: bool) -> Startup {
|
||||
if have_local_paths {
|
||||
// Files named on the command line win: the user asked for those
|
||||
// specifically, not for their library.
|
||||
Startup::ShowLocalFiles
|
||||
} else if self.can_open_library() {
|
||||
Startup::OpenLibrary
|
||||
} else {
|
||||
Startup::ShowLaunchScreen
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_filter(&self) -> FormatFilter {
|
||||
FormatFilter::from_formats(self.formats.iter().filter(|(_, on)| *on).map(|(f, _)| *f))
|
||||
}
|
||||
@@ -393,6 +427,38 @@ mod tests {
|
||||
assert!(m.can_open_library());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_configured_library_opens_rather_than_showing_nothing() {
|
||||
// The regression this guards: skipping the launch screen because a
|
||||
// library is configured used to mean the library was never opened,
|
||||
// since `on_open_library` only fires from a button nobody saw.
|
||||
let mut m = LaunchModel::default();
|
||||
m.signed_in(session_with_root("PhotosRaw"));
|
||||
assert_eq!(m.startup_action(false), Startup::OpenLibrary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_configuration_shows_the_launch_screen() {
|
||||
let m = LaunchModel::default();
|
||||
assert_eq!(m.startup_action(false), Startup::ShowLaunchScreen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_signed_in_account_without_a_folder_still_needs_the_screen() {
|
||||
// Opening without a chosen folder would scan the whole account.
|
||||
let mut m = LaunchModel::default();
|
||||
m.signed_in(session_with_root(""));
|
||||
assert_eq!(m.startup_action(false), Startup::ShowLaunchScreen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_line_files_win_over_a_configured_library() {
|
||||
// The user asked for those files specifically.
|
||||
let mut m = LaunchModel::default();
|
||||
m.signed_in(session_with_root("PhotosRaw"));
|
||||
assert_eq!(m.startup_action(true), Startup::ShowLocalFiles);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failure_never_strands_the_screen_in_busy() {
|
||||
let mut m = LaunchModel::default();
|
||||
|
||||
Reference in New Issue
Block a user