Settings redesign: hub-based navigation, plus hardening and bug fixes
Rework the settings app around hub pages (home, network, displays, input, apps, privacy, system) with a redesigned sidebar, shared nav state, and new hub view components. Alongside the redesign: validate webview inputs into root commands (users, firewall, snapshots, wifi), fix set_charge_threshold writing the percentage via tee stdin, prevent streaming installs from hanging on inherited stdin, add frontend type fixes, sync versions to 0.8.2, and clean up clippy/svelte-check warnings.
This commit is contained in:
parent
f7b114f778
commit
dce2031743
97 changed files with 2723 additions and 1142 deletions
|
|
@ -5,6 +5,10 @@
|
|||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-start-dragging",
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,5 +69,7 @@ pub fn get_appearance() -> Appearance {
|
|||
#[tauri::command]
|
||||
pub fn save_appearance(appearance: Appearance) -> Result<(), String> {
|
||||
let json = serde_json::to_string_pretty(&appearance).map_err(|e| e.to_string())?;
|
||||
config::atomic_write(&config_path(), &json).map_err(|e| e.to_string())
|
||||
config::atomic_write(&config_path(), &json).map_err(|e| e.to_string())?;
|
||||
super::util::hypr_reload();
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ pub fn valid_restore_target(path: &Path) -> bool {
|
|||
return false;
|
||||
}
|
||||
let normalized = normalize_abs(path);
|
||||
if normalized == PathBuf::from("/") {
|
||||
if normalized == *"/" {
|
||||
return false;
|
||||
}
|
||||
normalized != normalize_abs(&home_dir())
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
use serde::Serialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
use super::util;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct FirewallRule {
|
||||
number: String,
|
||||
|
|
|
|||
|
|
@ -51,10 +51,30 @@ fn hypr_path(name: &str) -> std::path::PathBuf {
|
|||
config::config_dir().join("hypr").join(name)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct LiveMonitor {
|
||||
name: String,
|
||||
mode: String,
|
||||
pub name: String,
|
||||
pub mode: String,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub refresh: f64,
|
||||
pub scale: f64,
|
||||
pub transform: u32,
|
||||
pub available_modes: Vec<String>,
|
||||
}
|
||||
|
||||
fn json_i32(v: &serde_json::Value, key: &str) -> Option<i32> {
|
||||
v.get(key)?.as_i64().map(|n| n as i32).or_else(|| v.get(key)?.as_f64().map(|n| n.round() as i32))
|
||||
}
|
||||
|
||||
fn json_u32(v: &serde_json::Value, key: &str) -> Option<u32> {
|
||||
v.get(key)?.as_u64().map(|n| n as u32).or_else(|| v.get(key)?.as_f64().map(|n| n.round() as u32))
|
||||
}
|
||||
|
||||
fn json_f64(v: &serde_json::Value, key: &str) -> Option<f64> {
|
||||
v.get(key)?.as_f64().or_else(|| v.get(key)?.as_u64().map(|n| n as f64))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -69,15 +89,103 @@ pub fn get_live_monitors() -> Vec<LiveMonitor> {
|
|||
monitors
|
||||
.iter()
|
||||
.filter_map(|m| {
|
||||
let name = m.get("name")?.as_str()?;
|
||||
let w = m.get("width")?.as_u64()?;
|
||||
let h = m.get("height")?.as_u64()?;
|
||||
let refresh = m.get("refreshRate")?.as_f64()?;
|
||||
Some(LiveMonitor { name: name.to_string(), mode: format!("{w}x{h} @ {refresh:.0}Hz") })
|
||||
let name = m.get("name")?.as_str()?.to_string();
|
||||
let width = json_u32(m, "width")?;
|
||||
let height = json_u32(m, "height")?;
|
||||
let refresh = json_f64(m, "refreshRate").unwrap_or(60.0);
|
||||
let x = json_i32(m, "x").unwrap_or(0);
|
||||
let y = json_i32(m, "y").unwrap_or(0);
|
||||
let scale = json_f64(m, "scale").unwrap_or(1.0).max(0.1);
|
||||
let transform = json_u32(m, "transform").unwrap_or(0);
|
||||
let available_modes = m
|
||||
.get("availableModes")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|x| x.as_str().map(str::to_string))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(LiveMonitor {
|
||||
name,
|
||||
mode: format!("{width}x{height} @ {refresh:.0}Hz"),
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
refresh,
|
||||
scale,
|
||||
transform,
|
||||
available_modes,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn write_monitor_rules(rules: &[MonitorRule]) -> Result<(), String> {
|
||||
let file = MonitorsFile { monitors: rules.to_vec() };
|
||||
let json = serde_json::to_string_pretty(&file).map_err(|e| e.to_string())?;
|
||||
config::atomic_write(&config_path(), &json).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn lua_ident(name: &str) -> Result<(), String> {
|
||||
if name.is_empty()
|
||||
|| !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
|
||||
{
|
||||
return Err(format!("bad monitor name '{name}'"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Live-apply a layout via BOS Hyprland `hl.monitor()`, then persist monitors.json.
|
||||
#[tauri::command]
|
||||
pub fn apply_monitor_layout(monitors: Vec<LiveMonitor>) -> Result<(), String> {
|
||||
if monitors.is_empty() {
|
||||
return Err("no monitors".into());
|
||||
}
|
||||
let mut stmts = Vec::new();
|
||||
let mut rules = Vec::new();
|
||||
for m in &monitors {
|
||||
lua_ident(&m.name)?;
|
||||
let refresh = if m.refresh > 1.0 { m.refresh.round() as u32 } else { 60 };
|
||||
let mode = format!("{}x{}@{refresh}", m.width.max(1), m.height.max(1));
|
||||
let position = format!("{}x{}", m.x, m.y);
|
||||
let scale = if (m.scale - 1.0).abs() < 0.001 {
|
||||
"1".to_string()
|
||||
} else {
|
||||
format!("{:.2}", m.scale)
|
||||
};
|
||||
let transform = m.transform.min(7);
|
||||
stmts.push(format!(
|
||||
"hl.monitor({{ output = \"{}\", mode = \"{mode}\", position = \"{position}\", scale = \"{scale}\", transform = {transform}, vrr = false }})",
|
||||
m.name
|
||||
));
|
||||
rules.push(MonitorRule {
|
||||
output: m.name.clone(),
|
||||
mode,
|
||||
position,
|
||||
scale,
|
||||
});
|
||||
}
|
||||
let lua = stmts.join("; ");
|
||||
let output = std::process::Command::new("hyprctl")
|
||||
.args(["eval", &lua])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if stdout != "ok" {
|
||||
let low = stdout.to_lowercase();
|
||||
if low.contains("unknown request") || low.contains("unknown command") || stdout.is_empty() {
|
||||
return Err("Live layout needs BOS Hyprland (hyprctl eval / hl.monitor).".into());
|
||||
}
|
||||
return Err(format!("hyprctl: {stdout}"));
|
||||
}
|
||||
write_monitor_rules(&rules)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_monitor_rules() -> Vec<MonitorRule> {
|
||||
std::fs::read_to_string(config_path())
|
||||
|
|
@ -90,9 +198,9 @@ pub fn get_monitor_rules() -> Vec<MonitorRule> {
|
|||
|
||||
#[tauri::command]
|
||||
pub fn save_monitor_rules(rules: Vec<MonitorRule>) -> Result<(), String> {
|
||||
let file = MonitorsFile { monitors: rules };
|
||||
let json = serde_json::to_string_pretty(&file).map_err(|e| e.to_string())?;
|
||||
config::atomic_write(&config_path(), &json).map_err(|e| e.to_string())
|
||||
write_monitor_rules(&rules)?;
|
||||
super::util::hypr_reload();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens `hyprland.lua` in `$EDITOR` (nano if unset) inside a terminal —
|
||||
|
|
|
|||
|
|
@ -254,7 +254,9 @@ pub fn get_keybinds() -> BindsPayload {
|
|||
|
||||
#[tauri::command]
|
||||
pub fn save_keybinds(file: BindsFile, kind: SchemaKind) -> Result<(), String> {
|
||||
save(&file, kind).map_err(|e| e.to_string())
|
||||
save(&file, kind).map_err(|e| e.to_string())?;
|
||||
super::util::hypr_reload();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ use serde::Serialize;
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::process::Command;
|
||||
|
||||
use super::util;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct WifiNetwork {
|
||||
ssid: String,
|
||||
|
|
@ -95,6 +97,9 @@ pub async fn scan_wifi() -> Vec<WifiNetwork> {
|
|||
|
||||
#[tauri::command]
|
||||
pub async fn connect_wifi(ssid: String, password: Option<String>) -> Result<(), String> {
|
||||
if !util::valid_nm_id(&ssid) {
|
||||
return Err(format!("invalid SSID '{ssid}'"));
|
||||
}
|
||||
let known = known_connection_names().await;
|
||||
let output = if let Some(password) = password {
|
||||
Command::new("nmcli").args(["dev", "wifi", "connect", &ssid, "password", &password]).output().await
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ pub fn read_nvidia_offer() -> Option<NvidiaOffer> {
|
|||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|p| !p.is_empty())
|
||||
.unwrap_or_else(|| default_packages());
|
||||
.unwrap_or_else(default_packages);
|
||||
return Some(NvidiaOffer {
|
||||
gpu,
|
||||
reason,
|
||||
|
|
|
|||
|
|
@ -154,7 +154,8 @@ pub async fn set_brightness(percent: i64) -> Result<(), String> {
|
|||
let Some(device) = brightness_device().await else {
|
||||
return Err("No controllable backlight found".into());
|
||||
};
|
||||
let pct = format!("{percent}%");
|
||||
// Clamp before handing the value to brightnessctl.
|
||||
let pct = format!("{}%", percent.clamp(0, 100));
|
||||
Command::new("brightnessctl")
|
||||
.args(["--device", &device, "set", &pct])
|
||||
.status()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use serde::Serialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
use super::util;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct SnapshotRow {
|
||||
number: String,
|
||||
|
|
@ -44,6 +46,9 @@ pub async fn get_snapshots() -> Result<Vec<SnapshotRow>, String> {
|
|||
|
||||
#[tauri::command]
|
||||
pub async fn delete_snapshot(number: String) -> Result<(), String> {
|
||||
if !util::valid_number_id(&number) {
|
||||
return Err("invalid snapshot number".into());
|
||||
}
|
||||
let output = Command::new("snapper").args(["delete", &number]).status().await.map_err(|e| e.to_string())?;
|
||||
if output.success() {
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ pub(crate) async fn run_hardcoded_env(
|
|||
) -> bool {
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.args(args)
|
||||
// Null stdin, never inherit: a child reading the app's inherited
|
||||
// stdin (pkexec falling back to a tty password prompt, pacman's
|
||||
// interactive confirm) would block forever and hang the install
|
||||
// command. These commands are all non-interactive (--noconfirm/-y);
|
||||
// authenticating goes through the polkit agent as a GUI dialog.
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ fn css_custom_properties(p: &bread_theme::Palette) -> String {
|
|||
("fg", p.foreground.as_str()),
|
||||
("surface", p.color0.as_str()),
|
||||
("overlay", p.color7.as_str()),
|
||||
("accent", p.color4.as_str()),
|
||||
("accent", p.color1.as_str()),
|
||||
("red", p.color1.as_str()),
|
||||
("green", p.color2.as_str()),
|
||||
("yellow", p.color3.as_str()),
|
||||
|
|
@ -48,7 +48,7 @@ fn css_custom_properties(p: &bread_theme::Palette) -> String {
|
|||
("teal", p.color6.as_str()),
|
||||
("on-bg", bread_theme::ink_on(&p.background)),
|
||||
("on-surface", bread_theme::ink_on(&p.color0)),
|
||||
("on-accent", bread_theme::ink_on(&p.color4)),
|
||||
("on-accent", bread_theme::ink_on(&p.color1)),
|
||||
("on-red", bread_theme::ink_on(&p.color1)),
|
||||
("on-overlay", bread_theme::ink_on(&p.color7)),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ fn may_delete_user(username: &str, current: &str) -> Result<(), String> {
|
|||
|
||||
#[tauri::command]
|
||||
pub async fn change_password(username: String, password: String) -> Result<(), String> {
|
||||
// chpasswd_input validates the username *and* the password (rejects the
|
||||
// newline / `:` that would let one entry smuggle another).
|
||||
let input = chpasswd_input(&username, &password)?;
|
||||
if util::run_with_stdin(&["pkexec", "chpasswd"], &input).await {
|
||||
Ok(())
|
||||
|
|
@ -121,9 +123,11 @@ pub async fn change_password(username: String, password: String) -> Result<(), S
|
|||
#[tauri::command]
|
||||
pub async fn remove_user(username: String) -> Result<(), String> {
|
||||
let current = std::env::var("USER").unwrap_or_default();
|
||||
// Validates the username and refuses `root` / the current user.
|
||||
may_delete_user(&username, ¤t)?;
|
||||
// `--` so a username can never be read as a userdel option.
|
||||
let output = Command::new("pkexec")
|
||||
.args(["userdel", "-r", &username])
|
||||
.args(["userdel", "-r", "--", &username])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
|
@ -137,7 +141,14 @@ pub async fn remove_user(username: String) -> Result<(), String> {
|
|||
#[tauri::command]
|
||||
pub async fn add_user(username: String, full_name: String, password: String) -> Result<(), String> {
|
||||
let username = username.trim();
|
||||
// Validates the username and the password (newline / `:` injection).
|
||||
let input = chpasswd_input(username, &password)?;
|
||||
// GECOS field is otherwise free text; strip control chars and the field
|
||||
// separators so `-c` can't smuggle extra passwd fields or arguments.
|
||||
let gecos: String = full_name
|
||||
.chars()
|
||||
.filter(|c| !matches!(c, '\n' | '\r' | '\0' | ',' | ':'))
|
||||
.collect();
|
||||
let mut useradd_args = vec![
|
||||
"pkexec".to_string(),
|
||||
"useradd".to_string(),
|
||||
|
|
@ -145,10 +156,12 @@ pub async fn add_user(username: String, full_name: String, password: String) ->
|
|||
"-s".to_string(),
|
||||
"/bin/bash".to_string(),
|
||||
];
|
||||
if !full_name.trim().is_empty() {
|
||||
if !gecos.trim().is_empty() {
|
||||
useradd_args.push("-c".to_string());
|
||||
useradd_args.push(full_name.trim().to_string());
|
||||
useradd_args.push(gecos.trim().to_string());
|
||||
}
|
||||
// `--` so the username can never be read as a useradd option.
|
||||
useradd_args.push("--".to_string());
|
||||
useradd_args.push(username.to_string());
|
||||
let args_ref: Vec<&str> = useradd_args.iter().map(String::as_str).collect();
|
||||
let output = Command::new(args_ref[0])
|
||||
|
|
|
|||
|
|
@ -226,6 +226,11 @@ pub fn valid_nm_id(name: &str) -> bool {
|
|||
&& !t.contains(';')
|
||||
}
|
||||
|
||||
/// Reload Hyprland so settings.json / binds.json / monitors.json take effect now.
|
||||
pub fn hypr_reload() {
|
||||
let _ = std::process::Command::new("hyprctl").arg("reload").status();
|
||||
}
|
||||
|
||||
pub fn valid_printer_name(name: &str) -> bool {
|
||||
let bytes = name.as_bytes();
|
||||
!bytes.is_empty()
|
||||
|
|
@ -236,6 +241,43 @@ pub fn valid_printer_name(name: &str) -> bool {
|
|||
.all(|b| b.is_ascii_alphanumeric() || matches!(*b, b'-' | b'_' | b'.'))
|
||||
}
|
||||
|
||||
/// Linux account usernames: lowercase letters, digits, `_`, `-`, `.`;
|
||||
/// must start with a lowercase letter (rejects a leading `-`, which would
|
||||
/// be a flag injection into useradd/userdel); capped at useradd's 32-char MAX.
|
||||
///
|
||||
/// We deliberately do *not* accept a leading `@`/domain or spaces: account
|
||||
/// creation here is a plain local user.
|
||||
pub fn valid_username(name: &str) -> bool {
|
||||
let bytes = name.as_bytes();
|
||||
!bytes.is_empty()
|
||||
&& bytes.len() <= 32
|
||||
&& bytes[0].is_ascii_lowercase()
|
||||
&& bytes
|
||||
.iter()
|
||||
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(*b, b'_' | b'-' | b'.'))
|
||||
}
|
||||
|
||||
/// Free-form text thrown at a CLI tool as a single argv element (firewall
|
||||
/// rule string, etc). Blocks the two genuinely dangerous shapes — a leading
|
||||
/// flag `-` and any control/whitespace injection — while still allowing
|
||||
/// spaces, slashes, dots, colons etc that ufw rules legitimately use.
|
||||
pub fn valid_cli_value(name: &str) -> bool {
|
||||
let t = name.trim();
|
||||
!t.is_empty()
|
||||
&& t.len() <= 256
|
||||
&& !t.starts_with('-')
|
||||
&& !t.contains('\n')
|
||||
&& !t.contains('\r')
|
||||
&& !t.contains('\0')
|
||||
&& !t.contains(';')
|
||||
&& t.bytes().all(|b| !(0..=31).contains(&b))
|
||||
}
|
||||
|
||||
/// ufw / snapper numeric id — digits only.
|
||||
pub fn valid_number_id(num: &str) -> bool {
|
||||
!num.is_empty() && num.len() <= 12 && num.bytes().all(|b| b.is_ascii_digit())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -279,4 +321,43 @@ mod tests {
|
|||
assert!(!valid_printer_name("foo bar"));
|
||||
assert!(!valid_printer_name("-d"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn username_accepts_normal_accounts() {
|
||||
assert!(valid_username("alice"));
|
||||
assert!(valid_username("bob_2"));
|
||||
assert!(valid_username("john.doe"));
|
||||
assert!(valid_username("a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn username_rejects_flags_and_injection() {
|
||||
assert!(!valid_username(""));
|
||||
assert!(!valid_username("-r")); // system-account flag into useradd
|
||||
assert!(!valid_username("--system"));
|
||||
assert!(!valid_username("a\nb"));
|
||||
assert!(!valid_username("foo bar"));
|
||||
assert!(!valid_username("UPPER")); // must start lowercase
|
||||
assert!(!valid_username(&"a".repeat(33)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_value_rejects_flags_and_controls() {
|
||||
assert!(valid_cli_value("80/tcp"));
|
||||
assert!(valid_cli_value("from 192.168.1.0/24 to any port 53"));
|
||||
assert!(!valid_cli_value("--all"));
|
||||
assert!(!valid_cli_value("-n"));
|
||||
assert!(!valid_cli_value("a\nb"));
|
||||
assert!(!valid_cli_value("a;rm"));
|
||||
assert!(!valid_cli_value(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_id_is_digits_only() {
|
||||
assert!(valid_number_id("42"));
|
||||
assert!(!valid_number_id(""));
|
||||
assert!(!valid_number_id("-1"));
|
||||
assert!(!valid_number_id("12a"));
|
||||
assert!(!valid_number_id("1 2"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ pub fn run() {
|
|||
commands::hyprland::get_live_monitors,
|
||||
commands::hyprland::get_monitor_rules,
|
||||
commands::hyprland::save_monitor_rules,
|
||||
commands::hyprland::apply_monitor_layout,
|
||||
commands::hyprland::open_hyprland_conf,
|
||||
commands::hyprland::open_keybinds_viewer,
|
||||
commands::keybinds::get_keybinds,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const INITIAL_SETTLE_DELAY: Duration = Duration::from_millis(2000);
|
|||
const VIEW_SETTLE_DELAY: Duration = Duration::from_millis(2000);
|
||||
|
||||
const KNOWN_VIEWS: &[&str] = &[
|
||||
"home",
|
||||
"network",
|
||||
"breadcrumbs",
|
||||
"bluetooth",
|
||||
|
|
@ -45,7 +46,9 @@ const KNOWN_VIEWS: &[&str] = &[
|
|||
"power",
|
||||
"datetime",
|
||||
"hyprland",
|
||||
"displays",
|
||||
"keybinds",
|
||||
"input",
|
||||
"autostart",
|
||||
"users",
|
||||
"appearance",
|
||||
|
|
@ -56,17 +59,20 @@ const KNOWN_VIEWS: &[&str] = &[
|
|||
"breadpad",
|
||||
"breadsearch",
|
||||
"bread",
|
||||
"desktop",
|
||||
"packages",
|
||||
"aur",
|
||||
"firmware",
|
||||
"snapshots",
|
||||
"updates",
|
||||
"system",
|
||||
"printing",
|
||||
"vpn",
|
||||
"nightlight",
|
||||
"ime",
|
||||
"accessibility",
|
||||
"defaults",
|
||||
"apps",
|
||||
"channel",
|
||||
"backup",
|
||||
"optional",
|
||||
|
|
@ -74,6 +80,7 @@ const KNOWN_VIEWS: &[&str] = &[
|
|||
"breadshot",
|
||||
"breadmon",
|
||||
"breadhelp",
|
||||
"privacy",
|
||||
"about",
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "BOS Settings",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.2",
|
||||
"identifier": "dev.breadway.bos-settings",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm --prefix frontend run dev",
|
||||
|
|
@ -13,10 +13,12 @@
|
|||
"windows": [
|
||||
{
|
||||
"title": "BOS Settings",
|
||||
"width": 960,
|
||||
"height": 640,
|
||||
"width": 1280,
|
||||
"height": 840,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"decorations": false,
|
||||
"backgroundColor": "#0c0c0c"
|
||||
"backgroundColor": "#12161c"
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue