Connected Devices: fixed UI settings. fixed pairing protocol

This commit is contained in:
Ultradesu
2026-07-24 02:21:24 +03:00
parent fa58b2ce43
commit e08eb71ab7
12 changed files with 869 additions and 177 deletions
+2 -1
View File
@@ -56,10 +56,11 @@ pub enum AppEvent {
track_id: i64,
liked: bool,
},
/// Liked federated item ids for the ♥ markers.
/// Liked federated item ids and content ids for the ♥ markers.
FedLikesLoaded(Result<Vec<String>, String>),
FedLikeToggled {
item_id: String,
content_id: Option<String>,
liked: bool,
},
/// A release fetched for queueing (a / shift-a on a release).
+28 -4
View File
@@ -459,13 +459,12 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) {
for fed in fed_tracks {
match library.toggle_fed_like(&fed) {
Ok(liked) => {
if let Err(err) =
devices.record_fed_like(fed.content_id.as_deref(), liked)
{
if let Err(err) = devices.record_fed_like(&fed, liked) {
tracing::warn!(%err, title = %fed.title, "recording synced federated like failed");
}
let _ = tx.send(AppEvent::FedLikeToggled {
item_id: fed.item_id.clone(),
content_id: fed.content_id.clone(),
liked,
});
}
@@ -524,6 +523,15 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) {
let _ = tx.send(AppEvent::FedTicket(result));
});
}
Effect::DeviceShowInvite
| Effect::DeviceConnectInvite(_)
| Effect::DeviceSyncNow
| Effect::DeviceSetName(_)
| Effect::DeviceRevoke(_)
if !state.connected_devices_enabled() =>
{
state.status_message = Some("enable federation before using connected devices".into());
}
Effect::DeviceShowInvite => {
let fed = Arc::clone(&runtime.federation);
let devices = Arc::clone(&runtime.devices);
@@ -1393,6 +1401,8 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
device_id: request.device_id,
name: request.name,
client_version: request.client_version,
requester_group_id: request.requester_group_id,
requester_group_active_devices: request.requester_group_active_devices,
});
state.federation.devices = Some(runtime.devices.status());
}
@@ -1703,11 +1713,25 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
Ok(ids) => state.fed_likes = ids.into_iter().collect(),
Err(message) => tracing::warn!(%message, "federated likes load failed"),
},
AppEvent::FedLikeToggled { item_id, liked } => {
AppEvent::FedLikeToggled {
item_id,
content_id,
liked,
} => {
if liked {
state.fed_likes.insert(item_id);
if let Some(content_id) =
content_id.and_then(|id| music_dht::normalize_content_id(&id))
{
state.fed_likes.insert(content_id);
}
} else {
state.fed_likes.remove(&item_id);
if let Some(content_id) =
content_id.and_then(|id| music_dht::normalize_content_id(&id))
{
state.fed_likes.remove(&content_id);
}
}
// The virtual Likes playlist is stale now; refetch on next open.
state.playlist_views.remove(&state::LIKES_PLAYLIST_ID);
+28 -3
View File
@@ -66,6 +66,8 @@ pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
device_id,
name,
client_version,
requester_group_id,
requester_group_active_devices,
} => handle_device_pairing(
state,
runtime,
@@ -73,6 +75,8 @@ pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
device_id,
name,
client_version,
requester_group_id,
requester_group_active_devices,
key,
),
Popup::ConfirmDeviceRevoke { device_id, name } => {
@@ -165,11 +169,14 @@ fn handle_device_pairing(
device_id: String,
name: String,
client_version: String,
requester_group_id: Option<String>,
requester_group_active_devices: usize,
key: KeyEvent,
) {
let group_conflict = requester_group_id.is_some() && requester_group_active_devices > 1;
match key.code {
KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('q') => {
if let Err(err) = runtime.devices.answer_pairing(&request_id, false) {
if let Err(err) = runtime.devices.answer_pairing(&request_id, false, false) {
state.status_message = Some(format!("pairing: {err:#}"));
} else {
state.status_message = Some("device pairing denied".to_string());
@@ -177,10 +184,26 @@ fn handle_device_pairing(
state.federation.devices = Some(runtime.devices.status());
}
KeyCode::Char('y') => {
if let Err(err) = runtime.devices.answer_pairing(&request_id, true) {
if let Err(err) = runtime
.devices
.answer_pairing(&request_id, true, group_conflict)
{
state.status_message = Some(format!("pairing: {err:#}"));
} else {
state.status_message = Some(format!("device \"{name}\" accepted"));
state.status_message = Some(if group_conflict {
format!("device \"{name}\" accepted; joining its sync group")
} else {
format!("device \"{name}\" accepted")
});
}
state.federation.devices = Some(runtime.devices.status());
}
KeyCode::Char('c') if group_conflict => {
if let Err(err) = runtime.devices.answer_pairing(&request_id, true, false) {
state.status_message = Some(format!("pairing: {err:#}"));
} else {
state.status_message =
Some(format!("device \"{name}\" accepted into this sync group"));
}
state.federation.devices = Some(runtime.devices.status());
}
@@ -190,6 +213,8 @@ fn handle_device_pairing(
device_id,
name,
client_version,
requester_group_id,
requester_group_active_devices,
});
}
}
+31 -2
View File
@@ -536,6 +536,8 @@ pub enum Popup {
device_id: String,
name: String,
client_version: String,
requester_group_id: Option<String>,
requester_group_active_devices: usize,
},
/// Confirmation before revoking a trusted device.
ConfirmDeviceRevoke { device_id: String, name: String },
@@ -819,8 +821,8 @@ pub struct AppState {
pub playlist_views: HashMap<i64, Loadable<PlaylistDetail>>,
/// Liked track ids, for the ♥ markers everywhere tracks are shown.
pub likes: std::collections::HashSet<i64>,
/// Liked federated tracks (DHT item ids) — likes that reference peers'
/// tracks without downloading them.
/// Liked federated tracks (DHT item ids and content ids) — likes that
/// reference peers' tracks without downloading them.
pub fed_likes: std::collections::HashSet<String>,
pub likes_loaded: bool,
pub logs: LogsTab,
@@ -848,3 +850,30 @@ pub struct AppState {
/// every view that shows artwork.
pub art: HashMap<String, ArtState>,
}
impl AppState {
pub fn connected_devices_enabled(&self) -> bool {
self.federation.settings.enabled && !self.federation.settings.network_id.trim().is_empty()
}
pub fn fed_track_liked(&self, fed: &crate::federation::FedTrack) -> bool {
self.fed_likes.contains(&fed.item_id)
|| fed
.content_id
.as_deref()
.and_then(music_dht::normalize_content_id)
.is_some_and(|content_id| self.fed_likes.contains(&content_id))
}
pub fn fed_card_track_liked(&self, track: &crate::federation::FedCardTrack) -> bool {
track
.content_id
.as_deref()
.and_then(music_dht::normalize_content_id)
.is_some_and(|content_id| self.fed_likes.contains(&content_id))
|| track
.sources
.iter()
.any(|(_, item_id)| self.fed_likes.contains(item_id))
}
}
+32 -6
View File
@@ -292,16 +292,14 @@ pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
None
} else {
let should_like = track_ids.iter().any(|id| !state.likes.contains(id))
|| fed_tracks
.iter()
.any(|fed| !state.fed_likes.contains(&fed.item_id));
|| fed_tracks.iter().any(|fed| !state.fed_track_liked(fed));
let toggles: Vec<i64> = track_ids
.into_iter()
.filter(|id| state.likes.contains(id) != should_like)
.collect();
let fed_toggles: Vec<crate::federation::FedTrack> = fed_tracks
.into_iter()
.filter(|fed| state.fed_likes.contains(&fed.item_id) != should_like)
.filter(|fed| state.fed_track_liked(fed) != should_like)
.collect();
let total = toggles.len() + fed_toggles.len();
state.status_message = Some(if should_like {
@@ -2344,6 +2342,9 @@ fn federation_select(state: &mut AppState) -> Option<Effect> {
None
}
SettingsRow::DeviceName => {
if !require_connected_devices_enabled(state) {
return None;
}
let name = state
.federation
.devices
@@ -2356,16 +2357,32 @@ fn federation_select(state: &mut AppState) -> Option<Effect> {
});
None
}
SettingsRow::DeviceInvite => Some(Effect::DeviceShowInvite),
SettingsRow::DeviceInvite => {
if !require_connected_devices_enabled(state) {
return None;
}
Some(Effect::DeviceShowInvite)
}
SettingsRow::DeviceConnect => {
if !require_connected_devices_enabled(state) {
return None;
}
state.popup = Some(Popup::FedInput {
field: FedInputField::ConnectInvite,
input: crate::app::input::LineEdit::default(),
});
None
}
SettingsRow::DeviceSyncNow => Some(Effect::DeviceSyncNow),
SettingsRow::DeviceSyncNow => {
if !require_connected_devices_enabled(state) {
return None;
}
Some(Effect::DeviceSyncNow)
}
SettingsRow::Device(index) => {
if !require_connected_devices_enabled(state) {
return None;
}
let Some(device) = state
.federation
.devices
@@ -2433,6 +2450,15 @@ fn federation_select(state: &mut AppState) -> Option<Effect> {
}
}
fn require_connected_devices_enabled(state: &mut AppState) -> bool {
if state.connected_devices_enabled() {
true
} else {
state.status_message = Some("enable federation before using connected devices".to_string());
false
}
}
/// A freshly created play context: drop the stale pre-shuffle snapshot and,
/// if shuffle is on, shuffle everything after the chosen track right away.
pub(super) fn on_new_queue(state: &mut AppState) {