use ratatui::layout::Rect;
use crossterm::event::
{
KeyCode,
KeyEvent,
KeyModifiers,
};
use why2_chat::
{
config,
misc,
network::codes::{ ServerSetting, SettingValue, UserProfile },
};
#[cfg(feature = "client_voice")]
use why2_chat::network::voice::client::options as voice_options;
#[cfg(feature = "client_screen")]
use why2_chat::network::screen::client::options as screen_options;
use super::
{
consts,
state::
{
App,
Fitted,
},
};
pub enum Mode {
Client, Server, Profile { own: bool }, }
pub enum Value
{
Toggle { on: bool, invert: bool },
Number(i64),
Text(String),
Avatar(Option<String>),
#[cfg(feature = "client_voice")]
Volume(u32),
#[cfg(feature = "client_voice")]
Device { id: String, input: bool }, }
pub enum Row
{
Header(String),
Item(Item),
Action(&'static str), }
pub struct Item
{
pub label: String,
pub key: String, pub value: Value,
pub hint: String, pub changed: bool, pub restart: bool, }
#[derive(Clone, Default)]
pub struct DeviceEntry
{
pub id: String,
pub label: String,
}
pub struct Picker {
pub title: &'static str,
pub entries: Vec<DeviceEntry>, pub selected: usize,
pub row: usize, pub offset: usize, }
#[derive(Default)]
pub struct Devices {
#[cfg(feature = "client_voice")]
pub input: Vec<DeviceEntry>,
#[cfg(feature = "client_voice")]
pub output: Vec<DeviceEntry>,
}
pub struct Settings {
pub open: bool,
pub rows: Vec<Row>,
pub selected: usize,
pub picker: Option<Picker>,
pub offset: usize,
pub page: usize,
pub mode: Mode,
pub subject: String,
pub avatar: Option<[u8; 32]>,
pub picture: Option<Box<Fitted>>,
pub picture_of: Option<[u8; 32]>,
pub picture_area: Rect,
pub edit: Option<String>, pub paths: Vec<String>, pub path: usize, pub saving: bool,
save: Option<Vec<ServerSetting>>, profile: Option<UserProfile>, avatar_save: Option<String>,
pub restart_note: Option<String>,
pub confirm: bool,
restart: bool,
#[cfg(feature = "client_voice")]
devices: Devices,
}
impl Default for Settings
{
fn default() -> Self { Self::new() }
}
impl Item
{
fn client(label: &str, key: &str, value: Value) -> Self {
Self { label: label.to_string(), key: key.to_string(), value, hint: String::new(), changed: false, restart: false }
}
}
impl Settings
{
pub fn new() -> Self
{
Self
{
open: false,
rows: Vec::new(),
selected: 0,
picker: None,
offset: 0,
page: 0,
mode: Mode::Client,
subject: String::new(),
avatar: None,
picture: None,
picture_of: None,
picture_area: Rect::ZERO,
edit: None,
paths: Vec::new(),
path: 0,
saving: false,
save: None,
profile: None,
avatar_save: None,
restart_note: None,
confirm: false,
restart: false,
#[cfg(feature = "client_voice")]
devices: Devices::default(),
}
}
pub fn open(&mut self, devices: Devices)
{
let mut rows: Vec<Row> = Vec::new();
#[cfg(feature = "client_voice")]
{
rows.push(Row::Header(String::from("Audio")));
rows.push(Row::Item(Item::client("Input device", "input_device",
Value::Device { id: config::read_config::<String>("input_device"), input: true })));
rows.push(Row::Item(Item::client("Output device", "output_device",
Value::Device { id: config::read_config::<String>("output_device"), input: false })));
rows.push(Row::Item(Item::client("Input volume", "input_volume",
Value::Volume(voice_options::clamp_volume(config::read_config::<u32>("input_volume"))))));
rows.push(Row::Item(Item::client("Output volume", "output_volume",
Value::Volume(voice_options::clamp_volume(config::read_config::<u32>("output_volume"))))));
#[cfg(feature = "client_screen")]
rows.push(Row::Item(Item::client("Screen share volume", "screen_volume",
Value::Volume(voice_options::clamp_volume(config::read_config::<u32>("screen_volume"))))));
rows.push(Row::Item(Item::client("Noise suppression", "noise_suppression",
toggle_value("noise_suppression", false))));
rows.push(Row::Item(Item::client("Automatic gain", "automatic_gain",
toggle_value("automatic_gain", false))));
}
rows.push(Row::Header(String::from("Interface")));
rows.push(Row::Item(Item::client("Message colors", "disable_colors", toggle_value("disable_colors", true))));
rows.push(Row::Item(Item::client("Background logo", "disable_logo", toggle_value("disable_logo", true))));
rows.push(Row::Item(Item::client("Show images automatically", "auto_show_images",
toggle_value("auto_show_images", false))));
rows.push(Row::Item(Item::client("Math rendering", "render_math", toggle_value("render_math", false))));
rows.push(Row::Item(Item::client("Show client IDs", "show_id", toggle_value("show_id", false))));
rows.push(Row::Item(Item::client("Show message IDs", "show_message_ids", toggle_value("show_message_ids", false))));
rows.push(Row::Header(String::from("Privacy")));
rows.push(Row::Item(Item::client("Share device", "share_device", toggle_value("share_device", false))));
rows.push(Row::Item(Item::client("Typing indicator", "typing_indicator",
toggle_value("typing_indicator", false))));
self.rows = rows;
self.picker = None;
self.open = true;
self.mode = Mode::Client;
self.edit = None;
self.saving = false;
self.selected = 0;
self.offset = 0;
#[cfg(feature = "client_voice")]
{
self.devices = devices;
}
#[cfg(not(feature = "client_voice"))]
let _ = devices;
self.step(1); }
pub fn open_server(&mut self, settings: Vec<ServerSetting>)
{
let mut rows: Vec<Row> = Vec::new();
let mut section = String::new();
for setting in settings
{
if setting.section != section
{
section = setting.section.clone();
if !section.is_empty() { rows.push(Row::Header(section.clone())); }
}
rows.push(Row::Item(Item
{
label: setting.key.replace('_', " "),
key: setting.key,
value: match setting.value
{
SettingValue::Toggle(on) => Value::Toggle { on, invert: false },
SettingValue::Number(number) => Value::Number(number),
SettingValue::Text(text) => Value::Text(text),
},
hint: setting.description,
changed: false,
restart: setting.restart, }));
}
rows.push(Row::Action(consts::SAVE_LABEL)); rows.push(Row::Action(consts::RESTART_LABEL));
self.rows = rows;
self.picker = None;
self.open = true;
self.mode = Mode::Server;
self.edit = None;
self.saving = false;
self.confirm = false;
self.selected = 0;
self.offset = 0;
self.step(1);
}
pub fn open_profile(&mut self, username: String, mut profile: UserProfile, own: bool)
{
let mut rows: Vec<Row> = match own
{
true => vec![Row::Item(Item
{
label: String::from(consts::AVATAR_LABEL),
key: String::from(consts::AVATAR_KEY),
value: Value::Avatar(None),
hint: String::new(),
changed: false,
restart: false,
})],
false => Vec::new(),
};
rows.extend(UserProfile::KEYS.iter().map(|key| Row::Item(Item
{
label: field_label(key),
key: (*key).to_string(),
value: Value::Text(profile.field_mut(key).map(std::mem::take).unwrap_or_default()),
hint: String::new(),
changed: false,
restart: false,
})));
if own { rows.push(Row::Action(consts::SAVE_LABEL)); }
self.avatar = profile.avatar;
if self.picture_of != self.avatar
{
self.picture = None;
self.picture_of = None;
}
self.rows = rows;
self.picker = None;
self.open = true;
self.mode = Mode::Profile { own };
self.subject = username;
self.edit = None;
self.saving = false;
self.confirm = false;
self.selected = 0;
self.offset = 0;
self.land(0, 1);
}
pub fn close(&mut self)
{
self.open = false;
self.picker = None;
self.avatar = None;
self.picture = None;
self.picture_of = None;
self.picture_area = Rect::ZERO;
self.edit = None;
self.mode = Mode::Client;
self.saving = false;
self.confirm = false;
self.rows = Vec::new();
self.offset = 0;
self.page = 0;
}
pub fn picture_rows(&self) -> u16
{
match self.picture.is_some()
{
true => consts::AVATAR_ROWS + 1,
false => 0,
}
}
pub fn editing_avatar(&self) -> bool
{
self.edit.is_some() && matches!(self.rows.get(self.selected), Some(Row::Item(item)) if item.key == consts::AVATAR_KEY)
}
fn refresh_paths(&mut self)
{
self.paths = match (self.editing_avatar(), self.edit.as_deref())
{
(true, Some(edit)) => super::palette::paths(edit, true),
_ => Vec::new(),
};
self.path = 0;
}
pub fn server(&self) -> bool { matches!(self.mode, Mode::Server) }
pub fn profile(&self) -> bool { matches!(self.mode, Mode::Profile { .. }) }
pub fn readonly(&self) -> bool { matches!(self.mode, Mode::Profile { own: false }) }
pub fn deferred(&self) -> bool { matches!(self.mode, Mode::Server | Mode::Profile { own: true }) }
pub fn link(&self) -> Option<&str>
{
if !self.profile() { return None; }
let Some(Row::Item(item)) = self.rows.get(self.selected) else { return None };
let Value::Text(text) = &item.value else { return None };
(item.key == "website" && misc::is_web_url(text)).then_some(text.as_str())
}
pub fn title(&self) -> String {
let name = match self.mode
{
Mode::Client => return String::from(" Settings "),
Mode::Server => String::from("Server settings"),
Mode::Profile { own: true } => String::from("Your profile"),
Mode::Profile { own: false } => format!("{}'s profile", self.subject),
};
if self.saving { return format!(" {name} · saving… "); }
if self.unsaved() { format!(" {name} · unsaved ") } else { format!(" {name} ") }
}
pub fn unsaved(&self) -> bool {
self.rows.iter().any(|row| matches!(row, Row::Item(item) if item.changed))
}
pub fn take_save(&mut self) -> Option<Vec<ServerSetting>> { self.save.take() }
pub fn take_profile_save(&mut self) -> Option<UserProfile> { self.profile.take() }
pub fn take_avatar_save(&mut self) -> Option<String> { self.avatar_save.take() }
pub fn take_restart(&mut self) -> bool { std::mem::take(&mut self.restart) }
pub fn stored(&mut self, settings: Vec<ServerSetting>)
{
let selected = self.selected;
let offset = self.offset;
self.open_server(settings);
self.selected = selected.min(self.rows.len().saturating_sub(1));
self.offset = offset;
if matches!(self.rows.get(self.selected), Some(Row::Header(_))) { self.step(1); }
}
pub fn stored_profile(&mut self, username: String, profile: UserProfile)
{
let selected = self.selected;
self.open_profile(username, profile, true);
self.selected = selected.min(self.rows.len().saturating_sub(1));
}
fn step(&mut self, delta: isize)
{
if self.rows.is_empty() { return; }
let mut index = self.selected as isize;
loop
{
index += delta;
if index < 0 || index as usize >= self.rows.len() { return; }
if !matches!(self.rows[index as usize], Row::Header(_))
{
self.selected = index as usize;
return;
}
}
}
fn page_move(&mut self, direction: isize)
{
if self.rows.is_empty() { return; }
let page = self.page.max(1) as isize;
self.offset = (self.offset as isize + direction * page).max(0) as usize;
let target = (self.selected as isize + direction * page).clamp(0, self.rows.len() as isize - 1);
self.land(target as usize, direction);
}
fn land(&mut self, index: usize, direction: isize)
{
if self.rows.is_empty() { return; }
self.selected = index.min(self.rows.len() - 1);
if !matches!(self.rows[self.selected], Row::Header(_)) { return; }
let before = self.selected;
self.step(direction);
if self.selected == before { self.step(-direction); }
}
fn first_row(&mut self)
{
self.offset = 0;
self.land(0, 1);
}
fn last_row(&mut self)
{
self.offset = self.rows.len();
self.land(self.rows.len().saturating_sub(1), -1);
}
#[cfg(feature = "client_voice")]
pub fn device_label(&self, id: &str, input: bool) -> String
{
if id.is_empty() { return String::from(consts::DEFAULT_DEVICE); }
let devices = if input { &self.devices.input } else { &self.devices.output };
devices.iter().find(|device| device.id == id).map(|device| device.label.clone()).unwrap_or_else(|| String::from(id))
}
#[cfg(feature = "client_voice")]
pub fn refresh_devices(&mut self)
{
for row in self.rows.iter_mut()
{
let Row::Item(item) = row else { continue };
if let Value::Device { input, .. } = item.value
{
item.value = Value::Device { id: config::read_config::<String>(&item.key), input };
}
}
}
}
fn field_label(key: &str) -> String
{
match key
{
"bio" => String::from("Description"),
other => other[..1].to_uppercase() + &other[1..],
}
}
fn toggle_value(key: &str, invert: bool) -> Value
{
let stored = config::read_config::<bool>(key);
Value::Toggle { on: if invert { !stored } else { stored }, invert }
}
pub fn handle_key(app: &mut App, key: KeyEvent)
{
if app.settings.picker.is_some()
{
handle_picker_key(app, key);
return;
}
if app.settings.edit.is_some()
{
handle_edit_key(app, key);
return;
}
if !matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) { app.settings.confirm = false; }
if app.settings.deferred() && key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s')
{
save(app);
return;
}
match key.code
{
KeyCode::Esc => app.settings.close(),
KeyCode::Up => app.settings.step(-1),
KeyCode::Down => app.settings.step(1),
KeyCode::PageUp => app.settings.page_move(-1),
KeyCode::PageDown => app.settings.page_move(1),
KeyCode::Home => app.settings.first_row(),
KeyCode::End => app.settings.last_row(),
KeyCode::Left => adjust(app, -1),
KeyCode::Right => adjust(app, 1),
KeyCode::Enter | KeyCode::Char(' ') => activate(app),
_ => {},
}
}
pub fn scroll(app: &mut App, delta: isize)
{
if app.settings.edit.is_some() { return; }
match app.settings.picker.as_mut()
{
Some(picker) =>
{
let next = picker.selected as isize + delta;
picker.selected = next.clamp(0, picker.entries.len() as isize - 1) as usize;
},
None => app.settings.step(delta),
}
}
fn handle_picker_key(app: &mut App, key: KeyEvent)
{
match key.code
{
KeyCode::Esc =>
{
app.settings.picker = None;
return;
},
KeyCode::Enter =>
{
let Some(picker) = app.settings.picker.take() else { return };
let chosen = picker.entries.get(picker.selected).map(|entry| entry.id.clone()).unwrap_or_default();
set_device(app, picker.row, chosen);
return;
},
_ => {},
}
let page = app.settings.page.max(1) as isize;
let Some(picker) = app.settings.picker.as_mut() else { return };
match key.code
{
KeyCode::Up => picker.selected = if picker.selected == 0 { picker.entries.len() - 1 } else { picker.selected - 1 },
KeyCode::Down => picker.selected = (picker.selected + 1) % picker.entries.len(),
KeyCode::PageUp | KeyCode::PageDown =>
{
let direction: isize = if key.code == KeyCode::PageUp { -1 } else { 1 };
picker.offset = (picker.offset as isize + direction * page).max(0) as usize;
picker.selected = (picker.selected as isize + direction * page)
.clamp(0, picker.entries.len() as isize - 1) as usize;
},
KeyCode::Home => { picker.selected = 0; picker.offset = 0; },
KeyCode::End => { picker.selected = picker.entries.len() - 1; picker.offset = picker.entries.len(); },
_ => {},
}
}
fn handle_edit_key(app: &mut App, key: KeyEvent)
{
let count = app.settings.paths.len();
match key.code
{
KeyCode::Esc => app.settings.edit = None,
KeyCode::Enter => commit_edit(app),
KeyCode::Backspace => { if let Some(edit) = app.settings.edit.as_mut() { edit.pop(); } },
KeyCode::Up if count > 0 => app.settings.path = (app.settings.path + count - 1) % count,
KeyCode::Down if count > 0 => app.settings.path = (app.settings.path + 1) % count,
KeyCode::Tab if count > 0 =>
{
let chosen = app.settings.paths[app.settings.path].clone();
app.settings.edit = Some(chosen);
},
KeyCode::Char(c) =>
{
let numeric = matches!(app.settings.rows.get(app.settings.selected), Some(Row::Item(item))
if matches!(item.value, Value::Number(_)));
if let Some(edit) = app.settings.edit.as_mut()
&& (!numeric || c.is_ascii_digit() || (c == '-' && edit.is_empty()))
{
edit.push(c);
}
},
_ => {},
}
if matches!(key.code, KeyCode::Backspace | KeyCode::Tab | KeyCode::Char(_)) { app.settings.refresh_paths(); }
if app.settings.edit.is_none() { app.settings.paths.clear(); }
}
fn commit_edit(app: &mut App) {
let Some(edit) = app.settings.edit.take() else { return };
if selected_key(&app.settings) == Some("website") && !edit.trim().is_empty()
&& !misc::is_web_url(edit.trim())
{
app.notify("A website has to start with http:// or https://");
return;
}
if selected_key(&app.settings) == Some(consts::AVATAR_KEY)
{
let path = edit.trim().to_string();
if path.is_empty() && app.settings.avatar.is_none() { return; }
if !path.is_empty() && let Err(error) = crate::check_upload(&path, crate::Upload::Avatar)
{
app.notify(error);
return;
}
if let Some(Row::Item(item)) = app.settings.rows.get_mut(app.settings.selected)
{
item.value = Value::Avatar(Some(path));
item.changed = true;
}
return;
}
let Some(Row::Item(item)) = app.settings.rows.get_mut(app.settings.selected) else { return };
match &item.value
{
Value::Number(current) => match edit.trim().parse::<i64>()
{
Ok(number) if number != *current =>
{
item.value = Value::Number(number);
item.changed = true;
},
_ => {},
},
Value::Text(current) => if edit != *current
{
item.value = Value::Text(edit);
item.changed = true;
},
_ => {},
}
}
fn selected_key(settings: &Settings) -> Option<&str>
{
match settings.rows.get(settings.selected)?
{
Row::Item(item) => Some(item.key.as_str()),
Row::Header(_) | Row::Action(_) => None,
}
}
enum Selected
{
Toggle(bool),
Number(i64),
Text(String),
Action(&'static str),
#[cfg(feature = "client_voice")]
Volume(String, u32),
#[cfg(feature = "client_voice")]
Device(String, bool),
}
fn selected(app: &App) -> Option<Selected>
{
match app.settings.rows.get(app.settings.selected)?
{
Row::Header(_) => None,
Row::Action(label) => Some(Selected::Action(label)),
Row::Item(item) => Some(match &item.value
{
Value::Toggle { on, .. } => Selected::Toggle(*on),
Value::Number(number) => Selected::Number(*number),
Value::Text(text) => Selected::Text(text.clone()),
Value::Avatar(path) => Selected::Text(path.clone().unwrap_or_default()),
#[cfg(feature = "client_voice")]
Value::Volume(percent) => Selected::Volume(item.key.clone(), *percent),
#[cfg(feature = "client_voice")]
Value::Device { id, input } => Selected::Device(id.clone(), *input),
}),
}
}
fn adjust(app: &mut App, direction: i32)
{
if app.settings.readonly() { return; }
let row = app.settings.selected;
match selected(app)
{
Some(Selected::Toggle(on)) => if (direction > 0) != on { toggle(app) },
Some(Selected::Number(number)) =>
{
let next = number.saturating_add(direction as i64);
if let Some(Row::Item(item)) = app.settings.rows.get_mut(row)
{
item.value = Value::Number(next);
item.changed = true;
}
},
Some(Selected::Text(_)) | Some(Selected::Action(_)) => {},
#[cfg(feature = "client_voice")]
Some(Selected::Volume(key, percent)) =>
{
let next = if direction > 0
{
voice_options::clamp_volume(percent.saturating_add(consts::VOLUME_STEP))
} else
{
percent.saturating_sub(consts::VOLUME_STEP)
};
if next == percent { return; }
if let Some(Row::Item(item)) = app.settings.rows.get_mut(row) { item.value = Value::Volume(next); }
config::client_write_int(&key, next as i64);
apply_volume(&key, next);
},
#[cfg(feature = "client_voice")]
Some(Selected::Device(id, input)) =>
{
let entries = device_entries(app, input);
let current = entries.iter().position(|entry| entry.id == id).unwrap_or(0);
let next = (current as isize + direction as isize).rem_euclid(entries.len() as isize) as usize;
let chosen = entries[next].id.clone();
set_device(app, row, chosen);
},
None => {},
}
}
fn activate(app: &mut App)
{
if app.settings.readonly()
{
if let Some(url) = app.settings.link().map(str::to_owned)
{
super::open_link(&url);
app.notify(format!("Opening {url}"));
}
return;
}
let _row = app.settings.selected;
match selected(app)
{
Some(Selected::Toggle(_)) => toggle(app),
Some(Selected::Number(number)) => app.settings.edit = Some(number.to_string()),
Some(Selected::Text(text)) =>
{
app.settings.edit = Some(text);
app.settings.refresh_paths();
},
Some(Selected::Action(consts::RESTART_LABEL)) => restart(app),
Some(Selected::Action(_)) => save(app),
#[cfg(feature = "client_voice")]
Some(Selected::Volume(..)) => adjust(app, 1),
#[cfg(feature = "client_voice")]
Some(Selected::Device(id, input)) =>
{
let entries = device_entries(app, input);
app.settings.picker = Some(Picker
{
title: if input { " Input device " } else { " Output device " },
selected: entries.iter().position(|entry| entry.id == id).unwrap_or(0),
entries,
row: _row,
offset: 0,
});
},
None => {},
}
}
fn restart(app: &mut App)
{
if !app.settings.server() || app.settings.saving || app.settings.unsaved() { return; }
if !app.settings.confirm
{
app.settings.confirm = true;
return;
}
app.settings.restart = true;
app.settings.close();
}
fn save(app: &mut App)
{
if app.settings.profile() { return save_profile(app); }
if !app.settings.server() { return; }
let changed: Vec<ServerSetting> = app.settings.rows.iter().filter_map(|row|
{
let Row::Item(item) = row else { return None };
if !item.changed { return None; }
Some(ServerSetting
{
key: item.key.clone(),
value: match &item.value
{
Value::Toggle { on, .. } => SettingValue::Toggle(*on),
Value::Number(number) => SettingValue::Number(*number),
Value::Text(text) => SettingValue::Text(text.clone()),
#[allow(unreachable_patterns)] _ => return None,
},
section: String::new(),
description: String::new(),
restart: false,
})
}).collect();
if changed.is_empty() { return; }
let restart: Vec<&str> = app.settings.rows.iter().filter_map(|row| match row
{
Row::Item(item) if item.changed && item.restart => Some(item.key.as_str()),
_ => None,
}).collect();
app.settings.restart_note = (!restart.is_empty()).then(|| restart.join(", "));
app.settings.saving = true;
app.settings.save = Some(changed);
}
fn save_profile(app: &mut App)
{
if !app.settings.unsaved() { return; }
let mut profile = UserProfile::default();
let mut fields = false;
for row in &app.settings.rows
{
let Row::Item(item) = row else { continue };
match &item.value
{
Value::Text(text) =>
{
if let Some(field) = profile.field_mut(&item.key) { *field = text.clone(); }
fields |= item.changed;
},
Value::Avatar(Some(path)) if item.changed => app.settings.avatar_save = Some(path.clone()),
_ => {},
}
}
app.settings.saving = true;
if fields { app.settings.profile = Some(profile); }
}
fn toggle(app: &mut App)
{
let deferred = app.settings.deferred();
let changed = match app.settings.rows.get_mut(app.settings.selected)
{
Some(Row::Item(item)) => match &item.value
{
Value::Toggle { on, invert } =>
{
let (next, invert) = (!*on, *invert);
item.value = Value::Toggle { on: next, invert };
item.changed = item.changed || deferred;
Some((item.key.clone(), next, invert))
},
#[allow(unreachable_patterns)] _ => None,
},
_ => None,
};
let Some((key, next, invert)) = changed else { return };
if deferred { return; }
config::client_write_bool(&key, if invert { !next } else { next });
app.reload_theme();
#[cfg(feature = "client_voice")]
match key.as_str()
{
"noise_suppression" => voice_options::set_noise_suppression(next),
"automatic_gain" => voice_options::set_automatic_gain(next),
_ => {},
}
}
#[cfg(feature = "client_voice")]
fn set_device(app: &mut App, row: usize, chosen: String)
{
let Some(Row::Item(item)) = app.settings.rows.get_mut(row) else { return };
let Value::Device { id, input } = &item.value else { return };
if *id == chosen { return; }
let (key, input) = (item.key.clone(), *input);
item.value = Value::Device { id: chosen.clone(), input };
config::client_write(&key, &chosen);
voice_options::mark_devices_changed();
}
#[cfg(not(feature = "client_voice"))]
fn set_device(_app: &mut App, _row: usize, _chosen: String) {}
#[cfg(feature = "client_voice")]
fn device_entries(app: &App, input: bool) -> Vec<DeviceEntry>
{
let devices = if input { &app.settings.devices.input } else { &app.settings.devices.output };
let mut entries = vec![DeviceEntry { id: String::new(), label: String::from(consts::DEFAULT_DEVICE) }];
entries.extend(devices.iter().cloned());
let configured = config::read_config::<String>(if input { "input_device" } else { "output_device" });
if !configured.is_empty() && !entries.iter().any(|entry| entry.id == configured)
{
entries.push(DeviceEntry { label: configured.clone(), id: configured });
}
entries
}
#[cfg(feature = "client_voice")]
fn apply_volume(key: &str, percent: u32) {
match key
{
"input_volume" => voice_options::set_input_volume(percent),
"output_volume" => voice_options::set_output_volume(percent),
#[cfg(feature = "client_screen")]
"screen_volume" => screen_options::set_screen_volume(percent),
_ => {},
}
}