Sync with FRID protocol v5. Improved direct content id search
This commit is contained in:
+9
-3
@@ -211,15 +211,21 @@ fn execute(state: &mut AppState, runtime: &mut Runtime, command: Command) {
|
||||
}
|
||||
|
||||
fn open_frid_link(state: &mut AppState, runtime: &Runtime, link: String) {
|
||||
let Some(content_id) = crate::share::parse_frid_content_id(&link) else {
|
||||
let Some(link) = crate::share::parse_frid_link(&link) else {
|
||||
state.status_message = Some("usage: :open frid://<content_id>".into());
|
||||
return;
|
||||
};
|
||||
state.status_message = Some("federation: opening shared track…".into());
|
||||
state.status_message = Some(match link.label.as_deref() {
|
||||
Some(label) => format!("federation: opening \"{label}\"…"),
|
||||
None => "federation: opening shared track…".into(),
|
||||
});
|
||||
let federation = Arc::clone(&runtime.federation);
|
||||
let tx = runtime.event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let event = match federation.track_by_content_id(&content_id).await {
|
||||
let event = match federation
|
||||
.track_by_content_id(&link.content_id, link.label.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(track) => AppEvent::EnqueueTracks {
|
||||
tracks: vec![crate::federation::pending_track(&track)],
|
||||
next: false,
|
||||
|
||||
+107
-2
@@ -16,7 +16,7 @@ use crate::app::state::{
|
||||
};
|
||||
use crate::library::models::{ReleaseEdit, TrackEdit, TrackItem};
|
||||
|
||||
pub fn handle_key(state: &mut AppState, runtime: &Runtime, key: KeyEvent) {
|
||||
pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
|
||||
let Some(popup) = state.popup.take() else {
|
||||
return;
|
||||
};
|
||||
@@ -43,7 +43,13 @@ pub fn handle_key(state: &mut AppState, runtime: &Runtime, key: KeyEvent) {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
} => handle_track_info(state, tracks, cursor, scroll, key),
|
||||
} => handle_track_info(state, runtime, tracks, cursor, scroll, key),
|
||||
Popup::TrackArtists {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
selected,
|
||||
} => handle_track_artists(state, runtime, tracks, cursor, scroll, selected, key),
|
||||
Popup::LogDetail(entry) => match key.code {
|
||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {}
|
||||
_ => state.popup = Some(Popup::LogDetail(entry)),
|
||||
@@ -307,6 +313,7 @@ fn handle_confirm_delete(
|
||||
|
||||
fn handle_track_info(
|
||||
state: &mut AppState,
|
||||
runtime: &mut Runtime,
|
||||
tracks: Vec<TrackItem>,
|
||||
cursor: usize,
|
||||
scroll: usize,
|
||||
@@ -315,6 +322,32 @@ fn handle_track_info(
|
||||
let len = tracks.len();
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {}
|
||||
KeyCode::Char('a') => {
|
||||
let artists = tracks
|
||||
.get(cursor.min(len.saturating_sub(1)))
|
||||
.map(crate::app::update::track_artist_refs)
|
||||
.unwrap_or_default();
|
||||
match artists.len() {
|
||||
0 => {
|
||||
state.status_message = Some("this track has no artists".into());
|
||||
state.popup = Some(Popup::TrackInfo {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
});
|
||||
}
|
||||
// A single artist opens directly; the popup closes.
|
||||
1 => open_artist(state, runtime, &artists[0]),
|
||||
_ => {
|
||||
state.popup = Some(Popup::TrackArtists {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
selected: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
state.popup = Some(Popup::TrackInfo {
|
||||
tracks,
|
||||
@@ -373,6 +406,78 @@ fn handle_track_info(
|
||||
}
|
||||
}
|
||||
|
||||
/// The artist picker over the track info: j/k choose, Enter jumps to the
|
||||
/// artist page, Esc returns to the info view.
|
||||
fn handle_track_artists(
|
||||
state: &mut AppState,
|
||||
runtime: &mut Runtime,
|
||||
tracks: Vec<TrackItem>,
|
||||
cursor: usize,
|
||||
scroll: usize,
|
||||
selected: usize,
|
||||
key: KeyEvent,
|
||||
) {
|
||||
let artists = tracks
|
||||
.get(cursor.min(tracks.len().saturating_sub(1)))
|
||||
.map(crate::app::update::track_artist_refs)
|
||||
.unwrap_or_default();
|
||||
if artists.is_empty() {
|
||||
state.popup = Some(Popup::TrackInfo {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let last = artists.len() - 1;
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('a') => {
|
||||
state.popup = Some(Popup::TrackInfo {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
});
|
||||
}
|
||||
KeyCode::Enter => open_artist(state, runtime, &artists[selected.min(last)]),
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
state.popup = Some(Popup::TrackArtists {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
selected: selected.saturating_sub(1),
|
||||
});
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
state.popup = Some(Popup::TrackArtists {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
selected: (selected + 1).min(last),
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
state.popup = Some(Popup::TrackArtists {
|
||||
tracks,
|
||||
cursor,
|
||||
scroll,
|
||||
selected: selected.min(last),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the popup and jumps to the artist's page (local or federated).
|
||||
fn open_artist(
|
||||
state: &mut AppState,
|
||||
runtime: &mut Runtime,
|
||||
artist: &crate::library::models::ArtistRef,
|
||||
) {
|
||||
state.track_selection.clear();
|
||||
if let Some(effect) = crate::app::update::open_artist_ref(state, artist) {
|
||||
super::perform_effect(state, runtime, effect);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_to_clipboard(text: &str) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
||||
@@ -509,6 +509,14 @@ pub enum Popup {
|
||||
cursor: usize,
|
||||
scroll: usize,
|
||||
},
|
||||
/// Artist picker over the info popup ('a' with several artists):
|
||||
/// Enter jumps to the chosen artist's page, Esc returns to the info.
|
||||
TrackArtists {
|
||||
tracks: Vec<TrackItem>,
|
||||
cursor: usize,
|
||||
scroll: usize,
|
||||
selected: usize,
|
||||
},
|
||||
/// Full, wrapped view of one log entry (Enter on the Logs tab).
|
||||
LogDetail(crate::config::logging::LogEntry),
|
||||
/// One-line text entry on the Federation tab (network id, peer ticket).
|
||||
|
||||
@@ -1179,6 +1179,51 @@ fn open_release_for_track(state: &mut AppState, track: &TrackItem) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Jumps to an artist page from anywhere: the local view when the artist is
|
||||
/// in the library, the federated card otherwise. Returns the effect that
|
||||
/// starts the federated fetch, if one is needed.
|
||||
pub(crate) fn open_artist_ref(
|
||||
state: &mut AppState,
|
||||
artist: &crate::library::models::ArtistRef,
|
||||
) -> Option<Effect> {
|
||||
let origin = state.active_tab;
|
||||
state.active_tab = Tab::Global;
|
||||
state.artist_fed_button = false;
|
||||
let effect = if artist.id >= 0 {
|
||||
match state.global.stack.last() {
|
||||
Some(GlobalView::Artist { id, .. }) if *id == artist.id => {}
|
||||
_ => state.global.stack.push(GlobalView::Artist {
|
||||
id: artist.id,
|
||||
cursor: 0,
|
||||
}),
|
||||
}
|
||||
None
|
||||
} else {
|
||||
state.fed_artist_view = Some((artist.name.clone(), Loadable::Loading));
|
||||
state.global.stack.push(GlobalView::FedArtist { cursor: 0 });
|
||||
Some(Effect::FedOpenArtist(artist.name.clone()))
|
||||
};
|
||||
if origin != Tab::Global {
|
||||
state.jump_origin = Some((origin, state.global.stack.len() - 1));
|
||||
}
|
||||
effect
|
||||
}
|
||||
|
||||
/// The artists of a track as shown in the info popup: main artists first,
|
||||
/// then featured, without duplicates.
|
||||
pub(crate) fn track_artist_refs(track: &TrackItem) -> Vec<crate::library::models::ArtistRef> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut refs: Vec<crate::library::models::ArtistRef> = Vec::new();
|
||||
for artist in track.artists.iter().chain(track.featured_artists.iter()) {
|
||||
let key = music_dht::normalize_name(&artist.name);
|
||||
if key.is_empty() || !seen.insert(key) {
|
||||
continue;
|
||||
}
|
||||
refs.push(artist.clone());
|
||||
}
|
||||
refs
|
||||
}
|
||||
|
||||
/// Insert tracks after the playing one (`next`) or at the end. Keeps the
|
||||
/// gapless prefetch index pointing at the same track if items shift.
|
||||
pub fn enqueue_tracks(state: &mut AppState, tracks: Vec<TrackItem>, next: bool) {
|
||||
@@ -2024,6 +2069,12 @@ pub(crate) fn fed_appears_on_tracks(state: &AppState) -> Vec<crate::federation::
|
||||
/// Federated tracks covered by the active visual selection, or the single
|
||||
/// one under the cursor in a federated context.
|
||||
pub(crate) fn selected_fed_tracks(state: &AppState) -> Vec<crate::federation::FedTrack> {
|
||||
// Federated cursors and selections live in Global-tab views only; on any
|
||||
// other tab a stale Global cursor must not shadow that tab's own
|
||||
// selection (e.g. `i` on a queue or playlist track).
|
||||
if state.active_tab != Tab::Global {
|
||||
return Vec::new();
|
||||
}
|
||||
// An active Shift-V range in a federated scope.
|
||||
if let Some(scope) = state.track_selection.scope.clone() {
|
||||
match scope {
|
||||
|
||||
Reference in New Issue
Block a user