Added fed artist page

This commit is contained in:
Ultradesu
2026-07-17 01:17:54 +03:00
parent 73ee4dab9d
commit a723fcc18e
14 changed files with 1049 additions and 140 deletions
+9 -17
View File
@@ -2,7 +2,7 @@ use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::{KeyCode, KeyEvent};
use crate::app::Runtime;
use crate::app::command::{self, Command, Parsed};
@@ -17,32 +17,22 @@ pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
match key.code {
KeyCode::Esc => cancel(state),
KeyCode::Enter => commit(state, runtime),
KeyCode::Backspace => {
if state.cmdline.input.pop().is_none() {
// Backspace on an empty line closes it, like vim.
cancel(state);
return;
// Backspace on an empty line closes it, like vim.
KeyCode::Backspace if state.cmdline.input.is_empty() => cancel(state),
_ => {
if state.cmdline.input.handle_key(key) {
after_change(state, runtime);
}
after_change(state, runtime);
}
KeyCode::Char(c) if is_typing(key) => {
state.cmdline.input.push(c);
after_change(state, runtime);
}
_ => {}
}
}
pub fn handle_paste(state: &mut AppState, runtime: &Runtime, pasted: &str) {
let cleaned: String = pasted.chars().filter(|c| !c.is_control()).collect();
state.cmdline.input.push_str(&cleaned);
state.cmdline.input.insert_str(&cleaned);
after_change(state, runtime);
}
fn is_typing(key: KeyEvent) -> bool {
key.modifiers.difference(KeyModifiers::SHIFT).is_empty()
}
/// Re-evaluate the input after every edit; live commands (search) take
/// effect immediately, while typing.
fn after_change(state: &mut AppState, runtime: &Runtime) {
@@ -101,6 +91,7 @@ pub(super) fn schedule_search(state: &mut AppState, runtime: &Runtime) {
state.search.loading = false;
state.search.results = None;
state.search.fed_tracks.clear();
state.search.fed_artists.clear();
state.search.fed_loading = false;
return;
}
@@ -123,6 +114,7 @@ pub(super) fn schedule_search(state: &mut AppState, runtime: &Runtime) {
// The same query also runs against the federated network (when the
// node is up); its results render as a separate, marked section.
state.search.fed_tracks.clear();
state.search.fed_artists.clear();
state.search.fed_loading = false;
if runtime.federation.settings().enabled {
state.search.fed_loading = true;
+8 -2
View File
@@ -89,10 +89,16 @@ pub enum AppEvent {
},
/// A status snapshot for the Federation tab.
FederationStatus(crate::federation::FedStatus),
/// Tracks found on the federated network for the live search.
/// Federated live-search results (artists a card can be opened for,
/// plus matching tracks).
FedSearchLoaded {
seq: u64,
result: Result<Vec<crate::federation::FedTrack>, String>,
result: Result<crate::federation::FedSearchResults, String>,
},
/// A federated artist card finished assembling.
FedArtistLoaded {
name: String,
result: Result<crate::federation::FedArtistCard, String>,
},
/// A federated track finished downloading and is ready to play.
FedPlayReady {
+147
View File
@@ -0,0 +1,147 @@
//! One-line text editing with a movable cursor, shared by every text input
//! (command line, edit forms, popup fields).
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
/// A single-line value plus a cursor position (in characters). Dereferences
/// to `&str`, so read paths treat it like the plain string it wraps.
#[derive(Debug, Clone, Default)]
pub struct LineEdit {
value: String,
cursor: usize,
}
impl LineEdit {
/// Starts with `value` and the cursor at its end.
pub fn new(value: impl Into<String>) -> Self {
let value = value.into();
let cursor = value.chars().count();
Self { value, cursor }
}
pub fn as_str(&self) -> &str {
&self.value
}
/// Cursor position in characters (0..=len).
pub fn cursor(&self) -> usize {
self.cursor
}
fn byte_index(&self, chars: usize) -> usize {
self.value
.char_indices()
.nth(chars)
.map(|(index, _)| index)
.unwrap_or(self.value.len())
}
pub fn clear(&mut self) {
self.value.clear();
self.cursor = 0;
}
pub fn insert(&mut self, c: char) {
let at = self.byte_index(self.cursor);
self.value.insert(at, c);
self.cursor += 1;
}
pub fn insert_str(&mut self, s: &str) {
let at = self.byte_index(self.cursor);
self.value.insert_str(at, s);
self.cursor += s.chars().count();
}
/// Removes the character before the cursor; `false` when at the start.
pub fn backspace(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
let at = self.byte_index(self.cursor - 1);
self.value.remove(at);
self.cursor -= 1;
true
}
/// Removes the character under the cursor.
pub fn delete(&mut self) {
if self.cursor < self.value.chars().count() {
let at = self.byte_index(self.cursor);
self.value.remove(at);
}
}
/// Applies one editing key (characters, backspace/delete, cursor
/// movement). Returns `false` for keys this editor does not handle
/// (Enter, Esc, Tab, ...), which the caller interprets itself.
pub fn handle_key(&mut self, key: KeyEvent) -> bool {
match key.code {
KeyCode::Char(c) if key.modifiers.difference(KeyModifiers::SHIFT).is_empty() => {
self.insert(c);
}
KeyCode::Backspace => {
self.backspace();
}
KeyCode::Delete => self.delete(),
KeyCode::Left => self.cursor = self.cursor.saturating_sub(1),
KeyCode::Right => self.cursor = (self.cursor + 1).min(self.value.chars().count()),
KeyCode::Home => self.cursor = 0,
KeyCode::End => self.cursor = self.value.chars().count(),
_ => return false,
}
true
}
}
impl std::ops::Deref for LineEdit {
type Target = str;
fn deref(&self) -> &str {
&self.value
}
}
impl std::fmt::Display for LineEdit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyEvent;
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn edits_at_cursor() {
let mut edit = LineEdit::new("hllo");
assert_eq!(edit.cursor(), 4);
edit.handle_key(key(KeyCode::Home));
edit.handle_key(key(KeyCode::Right));
edit.handle_key(key(KeyCode::Char('e')));
assert_eq!(edit.as_str(), "hello");
assert_eq!(edit.cursor(), 2);
edit.handle_key(key(KeyCode::End));
edit.handle_key(key(KeyCode::Backspace));
assert_eq!(edit.as_str(), "hell");
edit.handle_key(key(KeyCode::Home));
edit.handle_key(key(KeyCode::Delete));
assert_eq!(edit.as_str(), "ell");
}
#[test]
fn multibyte_safe() {
let mut edit = LineEdit::new("метл");
edit.handle_key(key(KeyCode::Left));
edit.insert('а');
assert_eq!(edit.as_str(), "метал");
edit.handle_key(key(KeyCode::End));
edit.insert('л');
assert_eq!(edit.as_str(), "металл");
}
}
+27 -2
View File
@@ -1,5 +1,6 @@
pub mod action;
mod cmdline;
pub mod input;
pub mod command;
pub mod event;
mod popup;
@@ -244,7 +245,7 @@ fn maintenance(state: &mut AppState, runtime: &mut Runtime) {
});
}
}
state::GlobalView::Search { .. } => {}
state::GlobalView::Search { .. } | state::GlobalView::FedArtist { .. } => {}
}
}
@@ -425,6 +426,17 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) {
let _ = tx.send(AppEvent::FedTicket(result));
});
}
Effect::FedOpenArtist(name) => {
let fed = Arc::clone(&runtime.federation);
let tx = runtime.event_tx.clone();
tokio::spawn(async move {
let result = fed
.artist_card(&name)
.await
.map_err(|err| format!("{err:#}"));
let _ = tx.send(AppEvent::FedArtistLoaded { name, result });
});
}
Effect::FedPlay(fed_track) => {
let fed = Arc::clone(&runtime.federation);
let tx = runtime.event_tx.clone();
@@ -900,7 +912,10 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
}
state.search.fed_loading = false;
match result {
Ok(tracks) => state.search.fed_tracks = tracks,
Ok(results) => {
state.search.fed_artists = results.artists;
state.search.fed_tracks = results.tracks;
}
Err(message) => tracing::warn!(%message, "federated search failed"),
}
}
@@ -920,6 +935,16 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
}
Err(message) => state.status_message = Some(format!("federation: {message}")),
},
AppEvent::FedArtistLoaded { name, result } => {
if let Some((current, data)) = &mut state.fed_artist_view
&& *current == name
{
*data = match result {
Ok(card) => state::Loadable::Ready(card),
Err(message) => state::Loadable::Failed(message),
};
}
}
AppEvent::FedTicket(result) => match result {
Ok(ticket) => {
state.popup = Some(state::Popup::FedText {
+12 -38
View File
@@ -5,7 +5,7 @@
use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::{KeyCode, KeyEvent};
use crate::app::Runtime;
use crate::app::event::AppEvent;
@@ -59,7 +59,7 @@ fn handle_fed_input(
state: &mut AppState,
runtime: &Runtime,
field: FedInputField,
mut input: String,
mut input: crate::app::input::LineEdit,
key: KeyEvent,
) {
match key.code {
@@ -85,15 +85,10 @@ fn handle_fed_input(
}
}
}
KeyCode::Backspace => {
input.pop();
_ => {
input.handle_key(key);
state.popup = Some(Popup::FedInput { field, input });
}
KeyCode::Char(c) if key.modifiers.difference(KeyModifiers::SHIFT).is_empty() => {
input.push(c);
state.popup = Some(Popup::FedInput { field, input });
}
_ => state.popup = Some(Popup::FedInput { field, input }),
}
}
@@ -101,11 +96,11 @@ fn handle_fed_input(
pub fn handle_paste(state: &mut AppState, pasted: &str) {
let cleaned: String = pasted.chars().filter(|c| !c.is_control()).collect();
match &mut state.popup {
Some(Popup::NewPlaylist { input, busy, .. }) if !*busy => input.push_str(&cleaned),
Some(Popup::FedInput { input, .. }) => input.push_str(&cleaned),
Some(Popup::NewPlaylist { input, busy, .. }) if !*busy => input.insert_str(&cleaned),
Some(Popup::FedInput { input, .. }) => input.insert_str(&cleaned),
Some(Popup::Edit { fields, focus, .. }) => {
if let Some(field) = fields.get_mut(*focus) {
field.value.push_str(&cleaned);
field.value.insert_str(&cleaned);
}
}
_ => {}
@@ -152,17 +147,11 @@ fn handle_edit(
let len = fields.len().max(1);
focus = (focus + len - 1) % len;
}
KeyCode::Backspace => {
_ => {
if let Some(field) = fields.get_mut(focus) {
field.value.pop();
field.value.handle_key(key);
}
}
KeyCode::Char(c) if key.modifiers.difference(KeyModifiers::SHIFT).is_empty() => {
if let Some(field) = fields.get_mut(focus) {
field.value.push(c);
}
}
_ => {}
}
state.popup = Some(Popup::Edit {
target,
@@ -393,7 +382,7 @@ fn handle_picker(
if cursor == 0 {
state.popup = Some(Popup::NewPlaylist {
for_track: Some(track),
input: String::new(),
input: crate::app::input::LineEdit::default(),
busy: false,
});
} else if let Some((id, title)) = options.get(cursor - 1).cloned() {
@@ -408,7 +397,7 @@ fn handle_name_entry(
state: &mut AppState,
runtime: &Runtime,
for_track: Option<TrackItem>,
mut input: String,
mut input: crate::app::input::LineEdit,
busy: bool,
key: KeyEvent,
) {
@@ -445,23 +434,8 @@ fn handle_name_entry(
busy: true,
});
}
KeyCode::Backspace => {
input.pop();
state.popup = Some(Popup::NewPlaylist {
for_track,
input,
busy: false,
});
}
KeyCode::Char(c) if key.modifiers.difference(KeyModifiers::SHIFT).is_empty() => {
input.push(c);
state.popup = Some(Popup::NewPlaylist {
for_track,
input,
busy: false,
});
}
_ => {
input.handle_key(key);
state.popup = Some(Popup::NewPlaylist {
for_track,
input,
+16 -5
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;
use crate::app::input::LineEdit;
use crate::art::ArtImage;
use crate::config::keymap::KeyContext;
use crate::library::models::{
@@ -69,6 +70,10 @@ pub enum GlobalView {
Search {
cursor: usize,
},
/// A federated artist card (data lives in `AppState::fed_artist_view`).
FedArtist {
cursor: usize,
},
}
/// The Global tab: the whole server library of artists.
@@ -308,14 +313,14 @@ pub enum DeleteTarget {
#[derive(Debug, Clone)]
pub struct EditField {
pub label: &'static str,
pub value: String,
pub value: LineEdit,
}
impl EditField {
pub fn new(label: &'static str, value: impl Into<String>) -> Self {
Self {
label,
value: value.into(),
value: LineEdit::new(value),
}
}
}
@@ -330,7 +335,7 @@ pub enum Popup {
/// added to it right after creation.
NewPlaylist {
for_track: Option<TrackItem>,
input: String,
input: LineEdit,
busy: bool,
},
/// Metadata edit form for a track, release, artist or playlist.
@@ -354,7 +359,7 @@ pub enum Popup {
/// One-line text entry on the Federation tab (network id, peer ticket).
FedInput {
field: FedInputField,
input: String,
input: LineEdit,
},
/// Wrapped read-only text (this peer's connection ticket).
FedText { title: String, text: String },
@@ -422,7 +427,7 @@ pub fn addable_playlists(state: &AppState) -> Vec<(i64, String)> {
#[derive(Debug, Default)]
pub struct Cmdline {
pub active: bool,
pub input: String,
pub input: LineEdit,
/// A live command (search) applied effects during this session; Esc
/// undoes them, Enter keeps them.
pub live: bool,
@@ -437,6 +442,9 @@ pub struct SearchState {
/// Tracks found on the federated network (empty while federation is
/// off); rendered as a separate, marked section.
pub fed_tracks: Vec<crate::federation::FedTrack>,
/// Artists a federated card can be opened for — from artist records and
/// from the artist names of matching tracks.
pub fed_artists: Vec<crate::federation::FedArtistHit>,
pub fed_loading: bool,
}
@@ -588,6 +596,9 @@ pub struct AppState {
pub logs: LogsTab,
pub queue_tab: QueueTab,
pub federation: FederationTab,
/// The one federated artist card being viewed (name + loading state);
/// opening another card replaces it.
pub fed_artist_view: Option<(String, Loadable<crate::federation::FedArtistCard>)>,
pub track_selection: TrackSelection,
/// Shift-J jump in flight: focus this (release, track) once the release
/// view finishes loading.
+120 -29
View File
@@ -49,6 +49,8 @@ pub enum Effect {
FedShowTicket,
/// Download (or resolve) a federated track and play it.
FedPlay(crate::federation::FedTrack),
/// Assemble the federated artist card (fan-out to the owning peers).
FedOpenArtist(String),
}
pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
@@ -199,7 +201,7 @@ pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
// The command line opens pre-filled with "/": typing continues
// the live search, exactly as if `:` then `/` were pressed.
state.cmdline.active = true;
state.cmdline.input = "/".to_string();
state.cmdline.input = crate::app::input::LineEdit::new("/");
state.cmdline.live = true;
state.search = SearchState::default();
state.active_tab = Tab::Global;
@@ -288,7 +290,7 @@ pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
Action::NewPlaylist => {
state.popup = Some(super::state::Popup::NewPlaylist {
for_track: None,
input: String::new(),
input: crate::app::input::LineEdit::default(),
busy: false,
});
None
@@ -548,7 +550,7 @@ fn selected_release_card(state: &AppState) -> Option<crate::library::models::Rel
let offset = cursor.checked_sub(results.artists.len())?;
results.releases.get(offset).cloned()
}
GlobalView::Release { .. } => None,
GlobalView::Release { .. } | GlobalView::FedArtist { .. } => None,
}
}
@@ -880,6 +882,7 @@ pub fn selected_track(state: &AppState) -> Option<TrackItem> {
let offset = cursor.checked_sub(results.artists.len() + results.releases.len())?;
results.tracks.get(offset).cloned()
}
GlobalView::FedArtist { .. } => None,
},
Tab::Playlists => {
let opened = state.playlists.opened.as_ref()?;
@@ -919,7 +922,7 @@ fn selected_release_id(state: &AppState) -> Option<i64> {
let offset = cursor.checked_sub(results.artists.len())?;
results.releases.get(offset).map(|r| r.id)
}
GlobalView::Release { .. } => None,
GlobalView::Release { .. } | GlobalView::FedArtist { .. } => None,
}
}
@@ -1183,7 +1186,9 @@ fn page_step(state: &AppState) -> isize {
lines
}
}
Some(GlobalView::Release { .. }) | Some(GlobalView::Search { .. }) => lines,
Some(GlobalView::Release { .. })
| Some(GlobalView::Search { .. })
| Some(GlobalView::FedArtist { .. }) => lines,
}
}
@@ -1343,6 +1348,7 @@ fn move_selection(state: &mut AppState, dx: isize, dy: isize) {
Some(GlobalView::Search { cursor }) => {
// Local results plus the federated section below them.
let total = (state.search.results.as_ref().map_or(0, |r| r.len())
+ state.search.fed_artists.len()
+ state.search.fed_tracks.len()) as isize;
if total == 0 {
return;
@@ -1351,6 +1357,25 @@ fn move_selection(state: &mut AppState, dx: isize, dy: isize) {
set_view_cursor(state, next as usize);
state.track_selection.clear();
}
Some(GlobalView::FedArtist { cursor }) => {
let total = fed_card_len(state) as isize;
if total == 0 {
return;
}
let next = (cursor as isize + dy).clamp(0, total - 1);
set_view_cursor(state, next as usize);
state.track_selection.clear();
}
}
}
/// Selectable rows of the open federated artist card (its tracks).
pub(crate) fn fed_card_len(state: &AppState) -> usize {
match &state.fed_artist_view {
Some((_, Loadable::Ready(card))) => {
card.releases.iter().map(|r| r.tracks.len()).sum()
}
_ => 0,
}
}
@@ -1359,7 +1384,8 @@ fn set_view_cursor(state: &mut AppState, value: usize) {
match view {
GlobalView::Artist { cursor, .. }
| GlobalView::Release { cursor, .. }
| GlobalView::Search { cursor } => *cursor = value,
| GlobalView::Search { cursor }
| GlobalView::FedArtist { cursor } => *cursor = value,
}
}
}
@@ -1398,8 +1424,11 @@ fn current_view_len(state: &AppState) -> usize {
_ => 0,
},
Some(GlobalView::Search { .. }) => {
state.search.results.as_ref().map_or(0, |r| r.len()) + state.search.fed_tracks.len()
state.search.results.as_ref().map_or(0, |r| r.len())
+ state.search.fed_artists.len()
+ state.search.fed_tracks.len()
}
Some(GlobalView::FedArtist { .. }) => fed_card_len(state),
}
}
@@ -1527,15 +1556,6 @@ fn select_current(state: &mut AppState) -> Option<Effect> {
not_yet(state, "Navigation in this view");
return None;
}
enum Outcome {
Push(GlobalView),
Play {
tracks: Vec<crate::library::models::TrackItem>,
start: usize,
},
PlayFed(crate::federation::FedTrack),
Nothing,
}
let outcome = match state.global.stack.last().copied() {
None => match state.global.artists.get(state.global.selected) {
Some(artist) => Outcome::Push(GlobalView::Artist {
@@ -1604,18 +1624,32 @@ fn select_current(state: &mut AppState) -> Option<Effect> {
start: cursor - artists - releases,
}
} else {
let fed_index = cursor - artists - releases - results.tracks.len();
match state.search.fed_tracks.get(fed_index) {
Some(fed) => Outcome::PlayFed(fed.clone()),
fed_outcome(state, cursor - artists - releases - results.tracks.len())
}
}
None => fed_outcome(state, cursor),
},
Some(GlobalView::FedArtist { cursor }) => {
match &state.fed_artist_view {
Some((name, Loadable::Ready(card))) => {
match card
.releases
.iter()
.flat_map(|release| release.tracks.iter().map(move |t| (release, t)))
.nth(cursor)
{
Some((release, track)) => {
match fed_track_from_card(name, release, track) {
Some(fed) => Outcome::PlayFed(fed),
None => Outcome::Nothing,
}
}
None => Outcome::Nothing,
}
}
_ => Outcome::Nothing,
}
None => match state.search.fed_tracks.get(cursor) {
Some(fed) => Outcome::PlayFed(fed.clone()),
None => Outcome::Nothing,
},
},
}
};
match outcome {
Outcome::Push(view) => {
@@ -1632,10 +1666,60 @@ fn select_current(state: &mut AppState) -> Option<Effect> {
state.status_message = Some(format!("federation: fetching \"{}\"", fed.title));
Some(Effect::FedPlay(fed))
}
Outcome::OpenFedArtist(name) => {
state.fed_artist_view = Some((name.clone(), Loadable::Loading));
state.global.stack.push(GlobalView::FedArtist { cursor: 0 });
state.active_tab = Tab::Global;
Some(Effect::FedOpenArtist(name))
}
Outcome::Nothing => None,
}
}
/// What Enter resolved to in the current view.
enum Outcome {
Push(GlobalView),
Play {
tracks: Vec<crate::library::models::TrackItem>,
start: usize,
},
PlayFed(crate::federation::FedTrack),
OpenFedArtist(String),
Nothing,
}
/// Enter inside the federated section of the search results: artists open
/// their card, tracks play.
fn fed_outcome(state: &AppState, fed_index: usize) -> Outcome {
let artists = &state.search.fed_artists;
if fed_index < artists.len() {
return Outcome::OpenFedArtist(artists[fed_index].name.clone());
}
match state.search.fed_tracks.get(fed_index - artists.len()) {
Some(fed) => Outcome::PlayFed(fed.clone()),
None => Outcome::Nothing,
}
}
/// A playable FedTrack out of a card row (first source; the rest are
/// fallbacks for a later improvement).
fn fed_track_from_card(
artist: &str,
release: &crate::federation::FedRelease,
track: &crate::federation::FedCardTrack,
) -> Option<crate::federation::FedTrack> {
let (owner, item_id) = track.sources.first()?.clone();
Some(crate::federation::FedTrack {
item_id,
owner,
own: false,
title: track.title.clone(),
artist_names: vec![artist.to_string()],
year: release.year,
duration_seconds: track.duration_seconds.map(|d| d.round() as i64),
})
}
/// Enter on the Federation tab: toggle switches, open text inputs, run
/// one-shot operations. The heavy lifting happens in perform_effect().
fn federation_select(state: &mut AppState) -> Option<Effect> {
@@ -1646,7 +1730,7 @@ fn federation_select(state: &mut AppState) -> Option<Effect> {
if !settings.enabled && settings.network_id.trim().is_empty() {
state.popup = Some(Popup::FedInput {
field: FedInputField::NetworkId,
input: String::new(),
input: crate::app::input::LineEdit::default(),
});
return None;
}
@@ -1656,7 +1740,9 @@ fn federation_select(state: &mut AppState) -> Option<Effect> {
FedRow::NetworkId => {
state.popup = Some(Popup::FedInput {
field: FedInputField::NetworkId,
input: state.federation.settings.network_id.clone(),
input: crate::app::input::LineEdit::new(
state.federation.settings.network_id.clone(),
),
});
None
}
@@ -1669,7 +1755,7 @@ fn federation_select(state: &mut AppState) -> Option<Effect> {
FedRow::Connect => {
state.popup = Some(Popup::FedInput {
field: FedInputField::ConnectTicket,
input: String::new(),
input: crate::app::input::LineEdit::default(),
});
None
}
@@ -1724,10 +1810,14 @@ fn go_back(state: &mut AppState) {
state.active_tab = origin;
return;
}
if let Some(popped) = state.global.stack.pop()
&& matches!(popped, GlobalView::Search { .. }) {
if let Some(popped) = state.global.stack.pop() {
if matches!(popped, GlobalView::Search { .. }) {
state.search = SearchState::default();
}
if matches!(popped, GlobalView::FedArtist { .. }) {
state.fed_artist_view = None;
}
}
}
_ => {}
}
@@ -1754,6 +1844,7 @@ fn reset_tab(state: &mut AppState, tab: Tab) {
state.search = SearchState::default();
}
state.global.stack.clear();
state.fed_artist_view = None;
}
Tab::Playlists => state.playlists.opened = None,
Tab::Federation => state.federation.cursor = 0,