diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index cbb897f4..5852fdc2 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -100,6 +100,7 @@ pub struct Config { pub sort_criteria: SortCriteria, pub paginated_limit: usize, pub seek_duration: usize, + pub kiosk_mode: bool, } impl Default for Config { @@ -118,6 +119,7 @@ impl Default for Config { sort_criteria: Default::default(), paginated_limit: 500, seek_duration: 10, + kiosk_mode: false, } } } diff --git a/psst-gui/src/main.rs b/psst-gui/src/main.rs index 8832a3ac..0e0bed1d 100644 --- a/psst-gui/src/main.rs +++ b/psst-gui/src/main.rs @@ -12,6 +12,7 @@ mod widget; use druid::AppLauncher; use env_logger::{Builder, Env}; +use std::env; use webapi::WebApi; use crate::{ @@ -33,7 +34,11 @@ fn main() { let config = Config::load().unwrap_or_default(); let paginated_limit = config.paginated_limit; - let state = AppState::default_with_config(config); + let mut state = AppState::default_with_config(config); + + let args: Vec = env::args().collect(); + state.config.kiosk_mode = args.iter().any(|arg| arg == "-k" || arg == "--kiosk"); + WebApi::new( state.session.clone(), Config::proxy().as_deref(), @@ -41,24 +46,29 @@ fn main() { paginated_limit, ) .install_as_global(); - - let delegate; - let launcher; - if state.config.has_credentials() { + let (delegate, launcher) = if state.config.has_credentials() { // Credentials are configured, open the main window. let window = ui::main_window(&state.config); - delegate = Delegate::with_main(window.id); - launcher = AppLauncher::with_window(window).configure_env(ui::theme::setup); + let delegate = Delegate::with_main(window.id); // Load user's local tracks for the WebApi. WebApi::global().load_local_tracks(state.config.username().unwrap()); + + (delegate, AppLauncher::with_window(window)) } else { - // No configured credentials, open the account setup. - let window = ui::account_setup_window(); - delegate = Delegate::with_preferences(window.id); - launcher = AppLauncher::with_window(window).configure_env(ui::theme::setup); + // No configured credentials, open the setup window. + let window = if state.config.kiosk_mode { + ui::kiosk_setup_window() + } else { + ui::account_setup_window() + }; + let delegate = Delegate::with_preferences(window.id); + + (delegate, AppLauncher::with_window(window)) }; + let launcher = launcher.configure_env(ui::theme::setup); + launcher .delegate(delegate) .launch(state) diff --git a/psst-gui/src/ui/home.rs b/psst-gui/src/ui/home.rs index 8ea7d916..8e56a043 100644 --- a/psst-gui/src/ui/home.rs +++ b/psst-gui/src/ui/home.rs @@ -37,17 +37,16 @@ pub fn home_widget() -> impl Widget { } fn simple_title_label(title: &str) -> impl Widget { - Flex::column() - .with_default_spacer() - .with_child(Label::new(title) - .with_text_size(theme::grid(2.5)) - .align_left() - .padding((theme::grid(1.5), 0.0)) + Flex::column().with_default_spacer().with_child( + Label::new(title) + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), 0.0)), ) } pub fn made_for_you() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget, || {Empty}) + Async::new(spinner_widget, loaded_results_widget, || Empty) .lens( Ctx::make( AppState::common_ctx, @@ -64,7 +63,7 @@ pub fn made_for_you() -> impl Widget { } pub fn recommended_stations() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget, || {Empty}) + Async::new(spinner_widget, loaded_results_widget, || Empty) .lens( Ctx::make( AppState::common_ctx, @@ -82,27 +81,24 @@ pub fn recommended_stations() -> impl Widget { fn uniquely_yours_results_widget() -> impl Widget> { Either::new( - |results: &WithCtx, _| { - results.data.playlists.is_empty() - }, + |results: &WithCtx, _| results.data.playlists.is_empty(), Empty, - Flex::column().with_default_spacer() - .with_child(Label::new("Uniquely yours") - .with_text_size(theme::grid(2.5)) - .align_left() - .padding((theme::grid(1.5), 0.0)) - ).with_child( - Scroll::new( - Flex::row() - .with_child(playlist_results_widget()) + Flex::column() + .with_default_spacer() + .with_child( + Label::new("Uniquely yours") + .with_text_size(theme::grid(2.5)) + .align_left() + .padding((theme::grid(1.5), 0.0)), ) - .align_left(), - ), + .with_child( + Scroll::new(Flex::row().with_child(playlist_results_widget())).align_left(), + ), ) } pub fn uniquely_yours() -> impl Widget { - Async::new(spinner_widget, uniquely_yours_results_widget, || {Empty}) + Async::new(spinner_widget, uniquely_yours_results_widget, || Empty) .lens( Ctx::make( AppState::common_ctx, @@ -119,7 +115,7 @@ pub fn uniquely_yours() -> impl Widget { } pub fn user_top_mixes() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget, || {Empty}) + Async::new(spinner_widget, loaded_results_widget, || Empty) .lens( Ctx::make( AppState::common_ctx, @@ -136,7 +132,7 @@ pub fn user_top_mixes() -> impl Widget { } pub fn best_of_artists() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget, || {Empty}) + Async::new(spinner_widget, loaded_results_widget, || Empty) .lens( Ctx::make( AppState::common_ctx, @@ -153,7 +149,7 @@ pub fn best_of_artists() -> impl Widget { } pub fn your_shows() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget, || {Empty}) + Async::new(spinner_widget, loaded_results_widget, || Empty) .lens( Ctx::make( AppState::common_ctx, @@ -170,7 +166,7 @@ pub fn your_shows() -> impl Widget { } pub fn jump_back_in() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget, || {Empty}) + Async::new(spinner_widget, loaded_results_widget, || Empty) .lens( Ctx::make( AppState::common_ctx, @@ -187,7 +183,7 @@ pub fn jump_back_in() -> impl Widget { } pub fn shows_that_you_might_like() -> impl Widget { - Async::new(spinner_widget, loaded_results_widget, || {Empty}) + Async::new(spinner_widget, loaded_results_widget, || Empty) .lens( Ctx::make( AppState::common_ctx, diff --git a/psst-gui/src/ui/menu.rs b/psst-gui/src/ui/menu.rs index 76bd16fe..ecaa4a13 100644 --- a/psst-gui/src/ui/menu.rs +++ b/psst-gui/src/ui/menu.rs @@ -5,8 +5,8 @@ use crate::{ data::{AppState, Nav}, }; -pub fn main_menu(_window: Option, _data: &AppState, _env: &Env) -> Menu { - if cfg!(target_os = "macos") { +pub fn main_menu(_window: Option, data: &AppState, _env: &Env) -> Menu { + if cfg!(target_os = "macos") && !data.config.kiosk_mode { Menu::empty().entry(mac_app_menu()) } else { Menu::empty() diff --git a/psst-gui/src/ui/mod.rs b/psst-gui/src/ui/mod.rs index 0f7b9b6b..e0be42c2 100644 --- a/psst-gui/src/ui/mod.rs +++ b/psst-gui/src/ui/mod.rs @@ -4,6 +4,7 @@ use druid::{ im::Vector, widget::{CrossAxisAlignment, Either, Flex, Label, List, Scroll, Slider, Split, ViewSwitcher}, Color, Env, Insets, Key, LensExt, Menu, MenuItem, Selector, Widget, WidgetExt, WindowDesc, + WindowState, }; use druid_shell::Cursor; @@ -42,12 +43,30 @@ pub mod user; pub mod utils; pub fn main_window(config: &Config) -> WindowDesc { - let win = WindowDesc::new(root_widget()) + let mut win = WindowDesc::new(root_widget(config)) .title(compute_main_window_title) - .with_min_size((theme::grid(65.0), theme::grid(50.0))) - .window_size(config.window_size) - .show_title(false) - .transparent_titlebar(true); + .show_title(false); + + if config.kiosk_mode { + win = win + .set_window_state(WindowState::Maximized) + .resizable(false) + .show_titlebar(false); + + // Set the window size to the primary monitor's work area and position it at (0, 0) + if let Some(monitor) = druid::Screen::get_monitors().first() { + let work_area = monitor.virtual_work_rect(); + win = win + .window_size(work_area.size()) + .set_position(druid::Point::new(0.0, 0.0)); + } + } else { + win = win + .window_size(config.window_size) + .with_min_size((theme::grid(65.0), theme::grid(50.0))) + .transparent_titlebar(true); + } + if cfg!(target_os = "macos") { win.menu(menu::main_menu) } else { @@ -71,6 +90,7 @@ pub fn preferences_window() -> WindowDesc { .window_size(win_size) .resizable(false) .show_title(false) + .set_always_on_top(true) .transparent_titlebar(true); if cfg!(target_os = "macos") { win.menu(menu::main_menu) @@ -82,9 +102,23 @@ pub fn preferences_window() -> WindowDesc { pub fn account_setup_window() -> WindowDesc { let win = WindowDesc::new(account_setup_widget()) .title("Login") + .resizable(false) + .show_title(false) .window_size((theme::grid(50.0), theme::grid(45.0))) + .transparent_titlebar(true); + if cfg!(target_os = "macos") { + win.menu(menu::main_menu) + } else { + win + } +} + +pub fn kiosk_setup_window() -> WindowDesc { + let win = WindowDesc::new(kiosk_setup_widget()) + .title("Setup") .resizable(false) .show_title(false) + .window_size((theme::grid(50.0), theme::grid(45.0))) .transparent_titlebar(true); if cfg!(target_os = "macos") { win.menu(menu::main_menu) @@ -109,7 +143,15 @@ fn account_setup_widget() -> impl Widget { ) } -fn root_widget() -> impl Widget { +fn kiosk_setup_widget() -> impl Widget { + ThemeScope::new( + preferences::kiosk_setup_widget() + .background(theme::BACKGROUND_DARK) + .expand(), + ) +} + +fn root_widget(config: &Config) -> impl Widget { let playlists = Scroll::new(playlist::list_widget()) .vertical() .expand_height(); @@ -119,8 +161,8 @@ fn root_widget() -> impl Widget { .with_child(sidebar_menu_widget()) .with_default_spacer() .with_flex_child(playlists, 1.0) - .padding(if cfg!(target_os = "macos") { - // Accommodate the window controls on Mac. + .padding(if cfg!(target_os = "macos") && !config.kiosk_mode { + // Accommodate the window controls on macOS Insets::new(0.0, 24.0, 0.0, 0.0) } else { Insets::ZERO diff --git a/psst-gui/src/ui/preferences.rs b/psst-gui/src/ui/preferences.rs index d9c5d403..f276f934 100644 --- a/psst-gui/src/ui/preferences.rs +++ b/psst-gui/src/ui/preferences.rs @@ -47,6 +47,29 @@ pub fn account_setup_widget() -> impl Widget { .padding(theme::grid(4.0)) } +pub fn kiosk_setup_widget() -> impl Widget { + Flex::column() + .must_fill_main_axis(true) + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_spacer(theme::grid(2.0)) + .with_child( + Label::new("Please insert your Spotify Premium credentials.") + .with_font(theme::UI_FONT_MEDIUM) + .with_line_break_mode(LineBreaking::WordWrap), + ) + .with_spacer(theme::grid(2.0)) + .with_child( + Label::new( + "Psst connects only to the official servers, and does not store your password.", + ) + .with_text_color(theme::PLACEHOLDER_COLOR) + .with_line_break_mode(LineBreaking::WordWrap), + ) + .with_spacer(theme::grid(6.0)) + .with_child(account_tab_widget(AccountTab::KioskSetup).expand_width()) + .padding(theme::grid(4.0)) +} + pub fn preferences_widget() -> impl Widget { const PROPAGATE_FLAGS: Selector = Selector::new("app.preferences.propagate-flags"); @@ -247,6 +270,12 @@ fn general_tab_widget() -> impl Widget { .lens(AppState::config.then(Config::paginated_limit)), ); + col = col.with_default_spacer().with_child( + Button::new("Done") + .align_right() + .on_click(|ctx, _, _| ctx.submit_command(commands::CLOSE_WINDOW)), + ); + col } @@ -254,12 +283,14 @@ fn general_tab_widget() -> impl Widget { enum AccountTab { FirstSetup, InPreferences, + KioskSetup, } fn account_tab_widget(tab: AccountTab) -> impl Widget { let mut col = Flex::column().cross_axis_alignment(match tab { AccountTab::FirstSetup => CrossAxisAlignment::Center, AccountTab::InPreferences => CrossAxisAlignment::Start, + AccountTab::KioskSetup => CrossAxisAlignment::Start, }); if matches!(tab, AccountTab::InPreferences) { @@ -293,9 +324,16 @@ fn account_tab_widget(tab: AccountTab) -> impl Widget { ); if matches!(tab, AccountTab::InPreferences) { - col = col.with_child(Button::new("Log Out").on_left_click(|ctx, _, _, _| { - ctx.submit_command(cmd::LOG_OUT); - })) + col = col + .with_child(Button::new("Log Out").on_left_click(|ctx, _, _, _| { + ctx.submit_command(cmd::LOG_OUT); + })) + .with_default_spacer() + .with_child( + Button::new("Done") + .align_right() + .on_click(|ctx, _, _| ctx.submit_command(commands::CLOSE_WINDOW)), + ) } col.controller(Authenticate::new(tab)) @@ -391,6 +429,11 @@ impl> Controller for Authenticate { AccountTab::InPreferences => { ctx.submit_command(cmd::SESSION_CONNECT); } + AccountTab::KioskSetup => { + ctx.submit_command(commands::CLOSE_WINDOW); + ctx.submit_command(commands::SHOW_PREFERENCES); + ctx.submit_command(cmd::SHOW_MAIN); + } } } data.preferences.auth.access_token.clear(); @@ -442,6 +485,11 @@ fn cache_tab_widget() -> impl Widget { } }, )); + col = col.with_default_spacer().with_child( + Button::new("Done") + .align_right() + .on_click(|ctx, _, _| ctx.submit_command(commands::CLOSE_WINDOW)), + ); col.controller(MeasureCacheSize::new()) .lens(AppState::preferences) @@ -534,4 +582,10 @@ fn about_tab_widget() -> impl Widget { .with_child(commit_hash) .with_child(build_time) .with_child(remote_url) + .with_default_spacer() + .with_child( + Button::new("Done") + .align_right() + .on_click(|ctx, _, _| ctx.submit_command(commands::CLOSE_WINDOW)), + ) } diff --git a/psst-gui/src/ui/user.rs b/psst-gui/src/ui/user.rs index 5ba2c88f..f0a4efa3 100644 --- a/psst-gui/src/ui/user.rs +++ b/psst-gui/src/ui/user.rs @@ -7,7 +7,10 @@ use druid::{ use crate::{ data::{AppState, Library, UserProfile}, webapi::WebApi, - widget::{icons, icons::SvgIcon, Async, Empty, MyWidgetExt}, + widget::{ + icons::{self, SvgIcon}, + Async, Empty, MyWidgetExt, + }, }; use super::theme; @@ -51,7 +54,11 @@ pub fn user_widget() -> impl Widget { .with_child(user_profile) .padding(theme::grid(1.0)), ) - .with_child(preferences_widget(&icons::PREFERENCES)) + .with_child(Either::new( + |data: &AppState, _| !data.config.kiosk_mode, + preferences_widget(&icons::PREFERENCES), + Empty, + )) } fn preferences_widget(svg: &SvgIcon) -> impl Widget {