Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kiosk mode #533

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions psst-gui/src/data/config.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
32 changes: 21 additions & 11 deletions psst-gui/src/main.rs
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ mod widget;

use druid::AppLauncher;
use env_logger::{Builder, Env};
use std::env;
use webapi::WebApi;

use crate::{
@@ -33,32 +34,41 @@ 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<String> = env::args().collect();
state.config.kiosk_mode = args.iter().any(|arg| arg == "-k" || arg == "--kiosk");

WebApi::new(
state.session.clone(),
Config::proxy().as_deref(),
Config::cache_dir(),
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)
4 changes: 2 additions & 2 deletions psst-gui/src/ui/menu.rs
Original file line number Diff line number Diff line change
@@ -5,8 +5,8 @@ use crate::{
data::{AppState, Nav},
};

pub fn main_menu(_window: Option<WindowId>, _data: &AppState, _env: &Env) -> Menu<AppState> {
if cfg!(target_os = "macos") {
pub fn main_menu(_window: Option<WindowId>, data: &AppState, _env: &Env) -> Menu<AppState> {
if cfg!(target_os = "macos") && !data.config.kiosk_mode {
Menu::empty().entry(mac_app_menu())
} else {
Menu::empty()
58 changes: 50 additions & 8 deletions psst-gui/src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -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;

@@ -43,12 +44,30 @@ pub mod user;
pub mod utils;

pub fn main_window(config: &Config) -> WindowDesc<AppState> {
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 {
@@ -72,6 +91,7 @@ pub fn preferences_window() -> WindowDesc<AppState> {
.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)
@@ -83,9 +103,23 @@ pub fn preferences_window() -> WindowDesc<AppState> {
pub fn account_setup_window() -> WindowDesc<AppState> {
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<AppState> {
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)
@@ -110,7 +144,15 @@ fn account_setup_widget() -> impl Widget<AppState> {
)
}

fn root_widget() -> impl Widget<AppState> {
fn kiosk_setup_widget() -> impl Widget<AppState> {
ThemeScope::new(
preferences::kiosk_setup_widget()
.background(theme::BACKGROUND_DARK)
.expand(),
)
}

fn root_widget(config: &Config) -> impl Widget<AppState> {
let playlists = Scroll::new(playlist::list_widget())
.vertical()
.expand_height();
@@ -120,8 +162,8 @@ fn root_widget() -> impl Widget<AppState> {
.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)
SO9010 marked this conversation as resolved.
Show resolved Hide resolved
} else {
Insets::ZERO
60 changes: 57 additions & 3 deletions psst-gui/src/ui/preferences.rs
Original file line number Diff line number Diff line change
@@ -47,6 +47,29 @@ pub fn account_setup_widget() -> impl Widget<AppState> {
.padding(theme::grid(4.0))
}

pub fn kiosk_setup_widget() -> impl Widget<AppState> {
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<AppState> {
const PROPAGATE_FLAGS: Selector = Selector::new("app.preferences.propagate-flags");

@@ -247,19 +270,27 @@ fn general_tab_widget() -> impl Widget<AppState> {
.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
}

#[derive(Copy, Clone)]
enum AccountTab {
FirstSetup,
InPreferences,
KioskSetup,
}

fn account_tab_widget(tab: AccountTab) -> impl Widget<AppState> {
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<AppState> {
);

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<W: Widget<AppState>> Controller<AppState, W> 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<AppState> {
}
},
));
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<AppState> {
.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)),
)
}
11 changes: 9 additions & 2 deletions psst-gui/src/ui/user.rs
Original file line number Diff line number Diff line change
@@ -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<AppState> {
.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<T: Data>(svg: &SvgIcon) -> impl Widget<T> {