feat(game): Step 7 UI — opt-in toggle, roster Playing line, Settings
Final step of game detection. Functional, plain styling (to art-direct). - Settings 'Games' category: opt-in 'show my game' toggle (SetGamePresenceEnabled, persisted), manual override picker (Auto / None / Pin current), per-game background picker+remove (reuses process_background + hashed game_background_path), and a non-Steam process->name mapping editor (add/remove, pushes SetGameProcessMap). - Roster: each peer card shows 'Playing <game>' under their name when they broadcast one; our own self card shows it too, marked '(not shared)' when broadcasting is off. - Startup: seeds SetGamePresenceEnabled + SetGameProcessMap from config. - Updated the settings-category navigation test for the new category. 395 lib tests green, clippy --all-targets clean, binary builds, and an 8s smoke launch starts the core + detector thread with no panic (detector logs nothing by design — privacy). Feature complete on Linux end-to-end (pending a coordinated GOSSIP_PROTO 3 redeploy to field-test presence with peers). Windows FFI still needs its cross-build pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+334
-5
@@ -45,10 +45,11 @@ pub enum SettingsCategory {
|
|||||||
Appearance,
|
Appearance,
|
||||||
Network,
|
Network,
|
||||||
Notifications,
|
Notifications,
|
||||||
|
Games,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SettingsCategory {
|
impl SettingsCategory {
|
||||||
const ALL: [SettingsCategory; 7] = [
|
const ALL: [SettingsCategory; 8] = [
|
||||||
SettingsCategory::Audio,
|
SettingsCategory::Audio,
|
||||||
SettingsCategory::Hotkeys,
|
SettingsCategory::Hotkeys,
|
||||||
SettingsCategory::Recording,
|
SettingsCategory::Recording,
|
||||||
@@ -56,6 +57,7 @@ impl SettingsCategory {
|
|||||||
SettingsCategory::Appearance,
|
SettingsCategory::Appearance,
|
||||||
SettingsCategory::Network,
|
SettingsCategory::Network,
|
||||||
SettingsCategory::Notifications,
|
SettingsCategory::Notifications,
|
||||||
|
SettingsCategory::Games,
|
||||||
];
|
];
|
||||||
|
|
||||||
fn label(self) -> &'static str {
|
fn label(self) -> &'static str {
|
||||||
@@ -67,6 +69,7 @@ impl SettingsCategory {
|
|||||||
SettingsCategory::Appearance => "Appearance",
|
SettingsCategory::Appearance => "Appearance",
|
||||||
SettingsCategory::Network => "Network",
|
SettingsCategory::Network => "Network",
|
||||||
SettingsCategory::Notifications => "Notifications",
|
SettingsCategory::Notifications => "Notifications",
|
||||||
|
SettingsCategory::Games => "Games",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +82,7 @@ impl SettingsCategory {
|
|||||||
SettingsCategory::Appearance => "Layout and theme",
|
SettingsCategory::Appearance => "Layout and theme",
|
||||||
SettingsCategory::Network => "Relay and privacy mode",
|
SettingsCategory::Network => "Relay and privacy mode",
|
||||||
SettingsCategory::Notifications => "Chimes and sounds",
|
SettingsCategory::Notifications => "Chimes and sounds",
|
||||||
|
SettingsCategory::Games => "Detection, presence, backgrounds",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,6 +346,47 @@ pub enum AppMessage {
|
|||||||
ShutdownCommandSent(bool),
|
ShutdownCommandSent(bool),
|
||||||
/// Fallback close if the core does not acknowledge shutdown promptly.
|
/// Fallback close if the core does not acknowledge shutdown promptly.
|
||||||
ShutdownTimeout,
|
ShutdownTimeout,
|
||||||
|
// --- Game detection (W17/W18) ---
|
||||||
|
/// Toggle broadcasting the detected game to peers (opt-in, default off).
|
||||||
|
ToggleGamePresence(bool),
|
||||||
|
/// Choose the manual detection override (Auto / None / the current game).
|
||||||
|
GameOverrideSelected(GameOverrideChoice),
|
||||||
|
/// Add-mapping form edits (executable basename → display name).
|
||||||
|
GameMapExeChanged(String),
|
||||||
|
GameMapNameChanged(String),
|
||||||
|
/// Commit the add-mapping form into the process map.
|
||||||
|
AddGameMapping,
|
||||||
|
/// Remove a process→name mapping by its executable key.
|
||||||
|
RemoveGameMapping(String),
|
||||||
|
/// Open the native picker to set a per-game background for the given game id.
|
||||||
|
PickGameBackground(String),
|
||||||
|
/// Result of the per-game background picker: (game id, chosen bytes or None).
|
||||||
|
GameBackgroundPicked(String, Option<Vec<u8>>),
|
||||||
|
/// Clear a per-game background mapping by game id.
|
||||||
|
RemoveGameBackground(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The manual game-detection override as shown in the Settings picker. Maps to a
|
||||||
|
/// [`crate::game::ManualOverride`] using the app's currently-detected game for the
|
||||||
|
/// `Current` choice (so "force this game" carries the live id + name).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum GameOverrideChoice {
|
||||||
|
/// Trust auto-detection (default).
|
||||||
|
Auto,
|
||||||
|
/// Force "not playing" — never broadcast a game.
|
||||||
|
None,
|
||||||
|
/// Pin the game currently detected (only offered while something is detected).
|
||||||
|
Current,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for GameOverrideChoice {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
GameOverrideChoice::Auto => "Auto-detect",
|
||||||
|
GameOverrideChoice::None => "None (don't show a game)",
|
||||||
|
GameOverrideChoice::Current => "Pin current game",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
|
fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
|
||||||
@@ -386,6 +431,13 @@ pub struct AppState {
|
|||||||
/// `None` = nothing detected. Independent of whether we broadcast it to peers
|
/// `None` = nothing detected. Independent of whether we broadcast it to peers
|
||||||
/// (that's `config.game_presence_enabled`).
|
/// (that's `config.game_presence_enabled`).
|
||||||
current_game: Option<crate::game::DetectedGame>,
|
current_game: Option<crate::game::DetectedGame>,
|
||||||
|
/// Add-mapping form state in the Games settings: the executable basename and
|
||||||
|
/// display name being entered for a new process→name mapping.
|
||||||
|
game_map_exe_input: String,
|
||||||
|
game_map_name_input: String,
|
||||||
|
/// Current manual-override selection shown in the Games settings picker.
|
||||||
|
/// Session-only (not persisted); defaults to Auto each launch.
|
||||||
|
game_override: GameOverrideChoice,
|
||||||
peers: HashMap<EndpointId, PeerState>,
|
peers: HashMap<EndpointId, PeerState>,
|
||||||
audio_levels: HashMap<EndpointId, f32>,
|
audio_levels: HashMap<EndpointId, f32>,
|
||||||
/// Peers we've locally muted (their audio isn't mixed into our output).
|
/// Peers we've locally muted (their audio isn't mixed into our output).
|
||||||
@@ -519,6 +571,10 @@ impl Default for AppState {
|
|||||||
let _ = controller.send(CoreCommand::SetRecordingMode(config.recording_mode));
|
let _ = controller.send(CoreCommand::SetRecordingMode(config.recording_mode));
|
||||||
let _ = controller.send(CoreCommand::SetPixelpassPath(config.pixelpass_path.clone()));
|
let _ = controller.send(CoreCommand::SetPixelpassPath(config.pixelpass_path.clone()));
|
||||||
let _ = controller.send(CoreCommand::SetPresenceMode(config.presence_mode));
|
let _ = controller.send(CoreCommand::SetPresenceMode(config.presence_mode));
|
||||||
|
// Game detection (W17/W18): seed the opt-in broadcast flag + the user's
|
||||||
|
// process→name mappings from config. The manual override starts at Auto.
|
||||||
|
let _ = controller.send(CoreCommand::SetGamePresenceEnabled(config.game_presence_enabled));
|
||||||
|
let _ = controller.send(CoreCommand::SetGameProcessMap(config.game_process_map.clone()));
|
||||||
for (peer, settings) in &config.peer_eq {
|
for (peer, settings) in &config.peer_eq {
|
||||||
if let Ok(id) = peer.parse::<EndpointId>() {
|
if let Ok(id) = peer.parse::<EndpointId>() {
|
||||||
let _ = controller.send(CoreCommand::SetPeerEq(id, *settings));
|
let _ = controller.send(CoreCommand::SetPeerEq(id, *settings));
|
||||||
@@ -571,6 +627,9 @@ impl Default for AppState {
|
|||||||
config,
|
config,
|
||||||
background_image,
|
background_image,
|
||||||
current_game: None,
|
current_game: None,
|
||||||
|
game_map_exe_input: String::new(),
|
||||||
|
game_map_name_input: String::new(),
|
||||||
|
game_override: GameOverrideChoice::Auto,
|
||||||
peers: HashMap::new(),
|
peers: HashMap::new(),
|
||||||
audio_levels: HashMap::new(),
|
audio_levels: HashMap::new(),
|
||||||
locally_muted: HashSet::new(),
|
locally_muted: HashSet::new(),
|
||||||
@@ -1611,6 +1670,126 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
state.config.background_dim = dim.clamp(0.0, 1.0);
|
state.config.background_dim = dim.clamp(0.0, 1.0);
|
||||||
state.config.save();
|
state.config.save();
|
||||||
}
|
}
|
||||||
|
AppMessage::ToggleGamePresence(enabled) => {
|
||||||
|
state.config.game_presence_enabled = enabled;
|
||||||
|
state.config.save();
|
||||||
|
// Core publishes/clears our game immediately (D8).
|
||||||
|
let _ = state.controller.send(CoreCommand::SetGamePresenceEnabled(enabled));
|
||||||
|
}
|
||||||
|
AppMessage::GameOverrideSelected(choice) => {
|
||||||
|
state.game_override = choice;
|
||||||
|
let override_ = match choice {
|
||||||
|
GameOverrideChoice::Auto => crate::game::ManualOverride::Auto,
|
||||||
|
GameOverrideChoice::None => crate::game::ManualOverride::ForceNone,
|
||||||
|
// "Pin current" carries the live detection; nothing detected = Auto.
|
||||||
|
GameOverrideChoice::Current => match &state.current_game {
|
||||||
|
Some(g) => crate::game::ManualOverride::Force(g.clone()),
|
||||||
|
None => crate::game::ManualOverride::Auto,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let _ = state.controller.send(CoreCommand::SetGameOverride(override_));
|
||||||
|
}
|
||||||
|
AppMessage::GameMapExeChanged(val) => {
|
||||||
|
state.game_map_exe_input = val;
|
||||||
|
}
|
||||||
|
AppMessage::GameMapNameChanged(val) => {
|
||||||
|
state.game_map_name_input = val;
|
||||||
|
}
|
||||||
|
AppMessage::AddGameMapping => {
|
||||||
|
let exe = crate::game::normalize_exe(&state.game_map_exe_input);
|
||||||
|
let name = state.game_map_name_input.trim().to_string();
|
||||||
|
if !exe.is_empty() && !name.is_empty() {
|
||||||
|
state.config.game_process_map.insert(exe, name);
|
||||||
|
state.config.save();
|
||||||
|
state.game_map_exe_input.clear();
|
||||||
|
state.game_map_name_input.clear();
|
||||||
|
let _ = state
|
||||||
|
.controller
|
||||||
|
.send(CoreCommand::SetGameProcessMap(state.config.game_process_map.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppMessage::RemoveGameMapping(exe) => {
|
||||||
|
if state.config.game_process_map.remove(&exe).is_some() {
|
||||||
|
state.config.save();
|
||||||
|
let _ = state
|
||||||
|
.controller
|
||||||
|
.send(CoreCommand::SetGameProcessMap(state.config.game_process_map.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppMessage::PickGameBackground(game_id) => {
|
||||||
|
// Native picker off the UI thread; result tagged with the game id.
|
||||||
|
return Task::perform(
|
||||||
|
async {
|
||||||
|
let handle = rfd::AsyncFileDialog::new()
|
||||||
|
.add_filter("Images", &["png", "jpg", "jpeg", "webp", "bmp"])
|
||||||
|
.set_title("Choose a per-game background")
|
||||||
|
.pick_file()
|
||||||
|
.await;
|
||||||
|
match handle {
|
||||||
|
Some(h) => Some(h.read().await),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |bytes| AppMessage::GameBackgroundPicked(game_id, bytes),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AppMessage::GameBackgroundPicked(game_id, picked) => {
|
||||||
|
if let Some(bytes) = picked {
|
||||||
|
match crate::background::process_background(&bytes) {
|
||||||
|
Ok(png) => match AppConfig::game_background_path(&game_id) {
|
||||||
|
Some(path) => {
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
match std::fs::write(&path, &png) {
|
||||||
|
Ok(()) => {
|
||||||
|
state
|
||||||
|
.config
|
||||||
|
.game_backgrounds
|
||||||
|
.insert(game_id.clone(), path.to_string_lossy().into_owned());
|
||||||
|
state.config.save();
|
||||||
|
// Apply at once if it's the running game.
|
||||||
|
if state.current_game.as_ref().map(|g| g.id.as_str())
|
||||||
|
== Some(game_id.as_str())
|
||||||
|
{
|
||||||
|
state.background_image = effective_background_bytes(
|
||||||
|
&state.config,
|
||||||
|
state.current_game.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
state.status_message = "Game background updated.".to_string();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
state.status_message =
|
||||||
|
format!("Couldn't save game background: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
state.status_message =
|
||||||
|
"Couldn't find a config directory to save the background."
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
state.status_message = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppMessage::RemoveGameBackground(game_id) => {
|
||||||
|
if let Some(path) = AppConfig::game_background_path(&game_id) {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
if state.config.game_backgrounds.remove(&game_id).is_some() {
|
||||||
|
state.config.save();
|
||||||
|
if state.current_game.as_ref().map(|g| g.id.as_str()) == Some(game_id.as_str()) {
|
||||||
|
state.background_image =
|
||||||
|
effective_background_bytes(&state.config, state.current_game.as_ref());
|
||||||
|
}
|
||||||
|
state.status_message = "Game background removed.".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
AppMessage::ToggleDrawerChat => {
|
AppMessage::ToggleDrawerChat => {
|
||||||
state.drawer_chat_open = !state.drawer_chat_open;
|
state.drawer_chat_open = !state.drawer_chat_open;
|
||||||
}
|
}
|
||||||
@@ -3236,6 +3415,126 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.spacing(10)
|
.spacing(10)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
.into(),
|
.into(),
|
||||||
|
SettingsCategory::Games => {
|
||||||
|
// What's running right now (local detection), for context.
|
||||||
|
let detected_label = match &state.current_game {
|
||||||
|
Some(g) => match &g.name {
|
||||||
|
Some(name) => format!("Currently detected: {name}"),
|
||||||
|
None => "Currently detected: (a game, no name available)".to_string(),
|
||||||
|
},
|
||||||
|
None => "Currently detected: nothing".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Manual override picker: Auto / None, plus "Pin current" when a
|
||||||
|
// game is detected.
|
||||||
|
let mut override_opts = vec![GameOverrideChoice::Auto, GameOverrideChoice::None];
|
||||||
|
if state.current_game.is_some() {
|
||||||
|
override_opts.push(GameOverrideChoice::Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-game background row for the running game (if any).
|
||||||
|
let game_bg_section: Element<'_, AppMessage> = match &state.current_game {
|
||||||
|
Some(g) => {
|
||||||
|
let has_bg = state.config.game_backgrounds.contains_key(&g.id);
|
||||||
|
let id_for_pick = g.id.clone();
|
||||||
|
let mut controls = row![
|
||||||
|
button(text("Set background for this game").size(13))
|
||||||
|
.on_press(AppMessage::PickGameBackground(id_for_pick)),
|
||||||
|
]
|
||||||
|
.spacing(8);
|
||||||
|
if has_bg {
|
||||||
|
let id_for_remove = g.id.clone();
|
||||||
|
controls = controls.push(
|
||||||
|
button(text("Remove").size(13))
|
||||||
|
.on_press(AppMessage::RemoveGameBackground(id_for_remove)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
controls.into()
|
||||||
|
}
|
||||||
|
None => text("Start a game to set its background.")
|
||||||
|
.size(12)
|
||||||
|
.color(color_subtext)
|
||||||
|
.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// The list of configured per-game backgrounds (by stable id).
|
||||||
|
let mut bg_list = column![].spacing(4).width(iced::Length::Fill);
|
||||||
|
for id in state.config.game_backgrounds.keys() {
|
||||||
|
let id_owned = id.clone();
|
||||||
|
bg_list = bg_list.push(
|
||||||
|
row![
|
||||||
|
text(id.clone()).size(12).width(iced::Length::Fill),
|
||||||
|
button(text("Remove").size(12))
|
||||||
|
.on_press(AppMessage::RemoveGameBackground(id_owned)),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.width(iced::Length::Fill),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The process→name mappings (non-Steam games), with an add form.
|
||||||
|
let mut map_list = column![].spacing(4).width(iced::Length::Fill);
|
||||||
|
for (exe, name) in &state.config.game_process_map {
|
||||||
|
let exe_owned = exe.clone();
|
||||||
|
map_list = map_list.push(
|
||||||
|
row![
|
||||||
|
text(format!("{exe} → {name}")).size(12).width(iced::Length::Fill),
|
||||||
|
button(text("Remove").size(12))
|
||||||
|
.on_press(AppMessage::RemoveGameMapping(exe_owned)),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.width(iced::Length::Fill),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
column![
|
||||||
|
section_header("Game presence"),
|
||||||
|
column![
|
||||||
|
checkbox(state.config.game_presence_enabled)
|
||||||
|
.label("Show the game I'm playing to people in the call")
|
||||||
|
.on_toggle(AppMessage::ToggleGamePresence),
|
||||||
|
text("Off by default. When on, your detected game appears next to your avatar for everyone in the room.")
|
||||||
|
.size(11).color(color_subtext),
|
||||||
|
vertical_space(6.0),
|
||||||
|
text(detected_label).size(12).color(color_subtext),
|
||||||
|
row![
|
||||||
|
text("Override:").size(13),
|
||||||
|
pick_list(
|
||||||
|
override_opts,
|
||||||
|
Some(state.game_override),
|
||||||
|
AppMessage::GameOverrideSelected,
|
||||||
|
),
|
||||||
|
].spacing(8),
|
||||||
|
].spacing(6).width(iced::Length::Fill),
|
||||||
|
vertical_space(section_gap),
|
||||||
|
section_header("Per-game background"),
|
||||||
|
column![
|
||||||
|
text("Give a game its own UI background; it switches automatically while you play. Falls back to your custom background (Appearance) otherwise.")
|
||||||
|
.size(11).color(color_subtext),
|
||||||
|
game_bg_section,
|
||||||
|
bg_list,
|
||||||
|
].spacing(8).width(iced::Length::Fill),
|
||||||
|
vertical_space(section_gap),
|
||||||
|
section_header("Non-Steam games"),
|
||||||
|
column![
|
||||||
|
text("Steam games are detected automatically. For other launchers, map an executable name to a display name.")
|
||||||
|
.size(11).color(color_subtext),
|
||||||
|
row![
|
||||||
|
text_input("executable (e.g. hl2_linux)", &state.game_map_exe_input)
|
||||||
|
.on_input(AppMessage::GameMapExeChanged)
|
||||||
|
.width(iced::Length::Fill),
|
||||||
|
text_input("shown name (e.g. Half-Life 2)", &state.game_map_name_input)
|
||||||
|
.on_input(AppMessage::GameMapNameChanged)
|
||||||
|
.width(iced::Length::Fill),
|
||||||
|
button(text("Add").size(13)).on_press(AppMessage::AddGameMapping),
|
||||||
|
].spacing(8).width(iced::Length::Fill),
|
||||||
|
map_list,
|
||||||
|
].spacing(8).width(iced::Length::Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let category_button = |category: SettingsCategory| -> Element<'_, AppMessage> {
|
let category_button = |category: SettingsCategory| -> Element<'_, AppMessage> {
|
||||||
@@ -3555,6 +3854,22 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_y(iced::alignment::Vertical::Center),
|
.align_y(iced::alignment::Vertical::Center),
|
||||||
|
// Our own detected game (W17/W18). When we're broadcasting it, it
|
||||||
|
// matches what peers see; otherwise it's marked "not shared".
|
||||||
|
{
|
||||||
|
let el: Element<'_, AppMessage> =
|
||||||
|
match state.current_game.as_ref().and_then(|g| g.name.as_deref()) {
|
||||||
|
Some(name) if state.config.game_presence_enabled => {
|
||||||
|
text(format!("Playing {name}")).size(11).color(color_blue).into()
|
||||||
|
}
|
||||||
|
Some(name) => text(format!("Playing {name} (not shared)"))
|
||||||
|
.size(11)
|
||||||
|
.color(color_subtext)
|
||||||
|
.into(),
|
||||||
|
None => text("").into(),
|
||||||
|
};
|
||||||
|
el
|
||||||
|
},
|
||||||
// Live "you're sharing" badge — only present while sharing.
|
// Live "you're sharing" badge — only present while sharing.
|
||||||
{
|
{
|
||||||
let el: Element<'_, AppMessage> = if state.self_sharing {
|
let el: Element<'_, AppMessage> = if state.self_sharing {
|
||||||
@@ -3702,10 +4017,20 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
let mut card_content = column![
|
let mut card_content = column![
|
||||||
row![
|
row![
|
||||||
avatar_view(&peer.avatar, &peer.name, &peer_id.to_string(), 38.0),
|
avatar_view(&peer.avatar, &peer.name, &peer_id.to_string(), 38.0),
|
||||||
column![
|
{
|
||||||
|
// Name + id, plus a "Playing <game>" line when the peer is
|
||||||
|
// broadcasting a game (game presence, W17).
|
||||||
|
let mut name_col = column![
|
||||||
text(&peer.name).size(16).color(color_text),
|
text(&peer.name).size(16).color(color_text),
|
||||||
text(format!("ID: {}", short_id(&peer_id.to_string()))).size(11).color(color_subtext)
|
text(format!("ID: {}", short_id(&peer_id.to_string()))).size(11).color(color_subtext),
|
||||||
],
|
];
|
||||||
|
if let Some(game) = &peer.game {
|
||||||
|
name_col = name_col.push(
|
||||||
|
text(format!("Playing {game}")).size(11).color(color_blue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
name_col
|
||||||
|
},
|
||||||
add_friend_el,
|
add_friend_el,
|
||||||
horizontal_space(),
|
horizontal_space(),
|
||||||
share_el,
|
share_el,
|
||||||
@@ -5544,10 +5869,14 @@ mod tests {
|
|||||||
let labels: Vec<_> = SettingsCategory::ALL.iter().map(|c| c.label()).collect();
|
let labels: Vec<_> = SettingsCategory::ALL.iter().map(|c| c.label()).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
labels,
|
labels,
|
||||||
vec!["Audio", "Hotkeys", "Recording", "Profile", "Appearance", "Network", "Notifications"]
|
vec![
|
||||||
|
"Audio", "Hotkeys", "Recording", "Profile", "Appearance", "Network",
|
||||||
|
"Notifications", "Games"
|
||||||
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(SettingsCategory::Audio.hint(), "Devices, mic gate, echo");
|
assert_eq!(SettingsCategory::Audio.hint(), "Devices, mic gate, echo");
|
||||||
assert_eq!(SettingsCategory::Profile.hint(), "Avatar and identity");
|
assert_eq!(SettingsCategory::Profile.hint(), "Avatar and identity");
|
||||||
|
assert_eq!(SettingsCategory::Games.hint(), "Detection, presence, backgrounds");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user