Repository: aome510/spotify-player (language: rust)

Issue:
Lagginess while scrolling through liked tracks
**Describe the bug**
Scrolling with arrows is laggy on liked tracks list. This doesn't happen on shorter track lists. Wondering what would be causing lagginess though?

**Environment**
- OS: macOS 13
- Application version: Latest
- Application features: default


Discussion / hints:
What is the size of your liked track list?
Around 8000 tracks I would say? But I don't think the size should matter in scrolling through the view? I would expect that behavior to be decoupled from the size of the playlist.
> Around 8000 tracks I would say? But I don't think the size should matter in scrolling through the view? I would expect that behavior to be decoupled from the size of the playlist.

yeah, it should not. I can try to reproduce by duplicating my local data.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5e449f73..4d4554ba 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3979,7 +3979,6 @@ dependencies = [
  "tracing",
  "tracing-subscriber",
  "ttl_cache",
- "unicode-width",
  "viuer",
  "winit",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 81f48c08..5cb61c97 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,2 +1,6 @@
 [workspace]
 members = ["spotify_player", "lyric_finder"]
+resolver = "2"
+
+[profile.release]
+debug = 1
diff --git a/docs/config.md b/docs/config.md
index f2f55d35..144a9e83 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -37,7 +37,6 @@ All configuration files should be placed inside the application's configuration
 | `playback_refresh_duration_in_ms`    | the duration (in ms) between two consecutive playback refreshes               | `0`                                                        |
 | `cover_image_refresh_duration_in_ms` | the duration (in ms) between two cover image refreshes (`image` feature only) | `2000`                                                     |
 | `page_size_in_rows`                  | a page's size expressed as a number of rows (for page-navigation commands)    | `20`                                                       |
-| `track_table_item_max_len`           | the maximum length of a column in a track table                               | `32`                                                       |
 | `enable_media_control`               | enable application media control support (`media-control` feature only)       | `true` (Linux), `false` (Windows and MacOS)                |
 | `enable_streaming`                   | create a device for streaming (streaming feature only)                        | `Always`                                                   |
 | `enable_cover_image_cache`           | store album's cover images in the cache folder                                | `true`                                                     |
diff --git a/examples/app.toml b/examples/app.toml
index 68486542..a80a065e 100644
--- a/examples/app.toml
+++ b/examples/app.toml
@@ -12,7 +12,6 @@ app_refresh_duration_in_ms = 32
 playback_refresh_duration_in_ms = 0
 cover_image_refresh_duration_in_ms = 2000
 page_size_in_rows = 20
-track_table_item_max_len = 32
 enable_media_control = false
 enable_streaming = "Always"
 enable_cover_image_cache = true
diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml
index 3196e4d8..5ef248b8 100644
--- a/spotify_player/Cargo.toml
+++ b/spotify_player/Cargo.toml
@@ -27,7 +27,6 @@ serde = { version = "1.0.188", features = ["derive"] }
 tokio = { version = "1.32.0", features = ["rt", "rt-multi-thread", "macros", "time"] }
 toml = "0.7.6"
 ratatui = "0.23.0"
-unicode-width = "0.1.10"
 rand = "0.8.5"
 maybe-async = "0.2.7"
 async-trait = "0.1.73"
@@ -40,7 +39,7 @@ souvlaki = { version = "0.6.1", optional = true }
 winit = { version = "0.28.6", optional = true }
 viuer = { version = "0.6.2", optional = true }
 image = { version = "0.24.7", optional = true }
-notify-rust = { version = "4.9.0", optional = true, default_features = false, features = ["d"] }
+notify-rust = { version = "4.9.0", optional = true, default-features = false, features = ["d"] }
 flume = "0.11.0"
 serde_json = "1.0.105"
 once_cell = "1.18.0"
diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs
index 52832c97..2c022103 100644
--- a/spotify_player/src/client/mod.rs
+++ b/spotify_player/src/client/mod.rs
@@ -292,15 +292,17 @@ impl Client {
             }
             ClientRequest::GetUserSavedTracks => {
                 let tracks = self.current_user_saved_tracks().await?;
-                state.data.write().caches.context.insert(
+                let mut data = state.data.write();
+                data.user_data.saved_tracks =
+                    tracks.iter().map(|t| (t.id.uri(), t.clone())).collect();
+                data.caches.context.insert(
                     USER_LIKED_TRACKS_ID.uri.to_owned(),
                     Context::Tracks {
-                        tracks: tracks.clone(),
+                        tracks,
                         desc: "User's liked tracks".to_string(),
                     },
                     *CACHE_DURATION,
                 );
-                state.data.write().user_data.saved_tracks = tracks;
             }
             ClientRequest::GetUserRecentlyPlayedTracks => {
                 let uri = &USER_RECENTLY_PLAYED_TRACKS_ID.uri;
@@ -960,7 +962,12 @@ impl Client {
                         .current_user_saved_tracks_add([track.id.as_ref()])
                         .await?;
                     // update the in-memory `user_data`
-                    state.data.write().user_data.saved_tracks.insert(0, track);
+                    state
+                        .data
+                        .write()
+                        .user_data
+                        .saved_tracks
+                        .insert(track.id.uri(), track);
                 }
             }
             Item::Album(album) => {
@@ -1025,13 +1032,9 @@ impl Client {
     pub async fn delete_from_library(&self, state: &SharedState, id: ItemId) -> Result<()> {
         match id {
             ItemId::Track(id) => {
-                state
-                    .data
-                    .write()
-                    .user_data
-                    .saved_tracks
-                    .retain(|t| t.id != id);
+                let uri = id.uri();
                 self.spotify.current_user_saved_tracks_delete([id]).await?;
+                state.data.write().user_data.saved_tracks.remove(&uri);
             }
             ItemId::Album(id) => {
                 state
diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs
index f12303ba..72b7fd66 100644
--- a/spotify_player/src/config/mod.rs
+++ b/spotify_player/src/config/mod.rs
@@ -46,8 +46,6 @@ pub struct AppConfig {
 
     pub page_size_in_rows: usize,
 
-    pub track_table_item_max_len: usize,
-
     // icon configs
     pub play_icon: String,
     pub pause_icon: String,
@@ -211,8 +209,6 @@ impl Default for AppConfig {
 
             page_size_in_rows: 20,
 
-            track_table_item_max_len: 32,
-
             pause_icon: "▌▌".to_string(),
             play_icon: "▶".to_string(),
             liked_icon: "♥".to_string(),
diff --git a/spotify_player/src/state/data.rs b/spotify_player/src/state/data.rs
index 111f62c2..2b6ad21e 100644
--- a/spotify_player/src/state/data.rs
+++ b/spotify_player/src/state/data.rs
@@ -25,7 +25,7 @@ pub struct UserData {
     pub playlists: Vec<Playlist>,
     pub followed_artists: Vec<Artist>,
     pub saved_albums: Vec<Album>,
-    pub saved_tracks: Vec<Track>,
+    pub saved_tracks: HashMap<String, Track>,
 }
 
 /// the application's caches
@@ -85,6 +85,6 @@ impl UserData {
 
     /// checks if a track is a liked track
     pub fn is_liked_track(&self, track: &Track) -> bool {
-        self.saved_tracks.iter().any(|t| t.id == track.id)
+        self.saved_tracks.contains_key(&track.id.uri())
     }
 }
diff --git a/spotify_player/src/ui/page.rs b/spotify_player/src/ui/page.rs
index b6756e05..435a7a19 100644
--- a/spotify_player/src/ui/page.rs
+++ b/spotify_player/src/ui/page.rs
@@ -684,12 +684,12 @@ pub fn render_track_table_window(
 ) -> Result<()> {
     // get the current playing track's URI to decorate such track (if exists) in the track table
     let mut playing_track_uri = "".to_string();
-    let mut active_desc = "";
+    let mut playing_id = "";
     if let Some(ref playback) = state.player.read().playback {
         if let Some(rspotify_model::PlayableItem::Track(ref track)) = playback.item {
             playing_track_uri = track.id.as_ref().map(|id| id.uri()).unwrap_or_default();
 
-            active_desc = if playback.is_playing {
+            playing_id = if playback.is_playing {
                 &state.app_config.play_icon
             } else {
                 &state.app_config.pause_icon
@@ -697,14 +697,13 @@ pub fn render_track_table_window(
         }
     }
 
-    let item_max_len = state.app_config.track_table_item_max_len;
     let n_tracks = tracks.len();
     let rows = tracks
         .into_iter()
         .enumerate()
         .map(|(id, t)| {
             let (id, style) = if playing_track_uri == t.id.uri() {
-                (active_desc.to_string(), ui.theme.current_playing())
+                (playing_id.to_string(), ui.theme.current_playing())
             } else {
                 ((id + 1).to_string(), Style::default())
             };
@@ -715,12 +714,9 @@ pub fn render_track_table_window(
                     ""
                 }),
                 Cell::from(id),
-                Cell::from(crate::utils::truncate_string(t.name.clone(), item_max_len)),
-                Cell::from(crate::utils::truncate_string(
-                    t.artists_info(),
-                    item_max_len,
-                )),
-                Cell::from(crate::utils::truncate_string(t.album_info(), item_max_len)),
+                Cell::from(t.name.clone()),
+                Cell::from(t.artists_info()),
+                Cell::from(t.album_info()),
                 Cell::from(crate::utils::format_duration(&t.duration)),
             ])
             .style(style)
@@ -747,6 +743,7 @@ pub fn render_track_table_window(
             Constraint::Percentage(30),
             Constraint::Percentage(20),
         ])
+        .column_spacing(2)
         .highlight_style(ui.theme.selection_style(is_active));
 
     match ui.current_page_mut() {
diff --git a/spotify_player/src/utils.rs b/spotify_player/src/utils.rs
index 073f6f7e..11197125 100644
--- a/spotify_player/src/utils.rs
+++ b/spotify_player/src/utils.rs
@@ -1,7 +1,6 @@
 use std::borrow::Cow;
 
 use tui::widgets::*;
-use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
 
 /// formats a time duration into a "{minutes}:{seconds}" format
 pub fn format_duration(duration: &chrono::Duration) -> String {
@@ -9,32 +8,6 @@ pub fn format_duration(duration: &chrono::Duration) -> String {
     format!("{}:{:02}", secs / 60, secs % 60)
 }
 
-/// truncates a string whose length exceeds a given `max_len` length.
-/// Such string will be appended with `...` at the end.
-pub fn truncate_string(s: String, max_len: usize) -> String {
-    let len = UnicodeWidthStr::width(s.as_str());
-    if len > max_len {
-        // get the longest prefix of the string such that its unicode width
-        // is still within the `max_len` limit
-        let mut s: String = s
-            .chars()
-            .fold(("".to_owned(), 0_usize), |(mut cs, cw), c| {
-                let w = UnicodeWidthChar::width(c).unwrap_or(2);
-                if cw + w + 3 > max_len {
-                    (cs, cw)
-                } else {
-                    cs.push(c);
-                    (cs, cw + w)
-                }
-            })
-            .0;
-        s.push_str("...");
-        s
-    } else {
-        s
-    }
-}
-
 pub fn new_list_state() -> ListState {
     let mut state = ListState::default();
     state.select(Some(0));


Test patch: