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

Issue:
Cover image is duplicated when playback window is at bottom
**Describe the bug**
A duplicated cover image appears inside the playlist when the playback window position is at bottom.

**To Reproduce**
1. Set playback window at bottom: `playback_window_position = "Bottom"`
2. Play something

**Expected behaviour**
See a single image in the playback window, no duplicated ones.

**Screenshots**
<img width="1440" alt="Screenshot 6" src="https://github.com/aome510/spotify-player/assets/45423661/fd24c50a-aa96-40d9-97bb-f32e82fee105">


**Environment**

- OS: macOs
- Application version: 0.19.1 (up to date version)
- Application features: image, daemon

**Additional context**
```toml
theme = "neofusion"
client_id = "secret"
enable_media_control = true
cover_img_length = 14
page_size_in_rows = 14
playback_window_position = "Bottom"
play_icon = ""
pause_icon = ""

[device]
name = "spotify-player"
device_type = "speaker"
volume = 100
bitrate = 320
audio_cache = false
```

** Only happening in Kitty terminal (0.35.2).


Discussion / hints:
BTW, check this: https://github.com/diegoulloao/neofusion.sp
How frequent does this happen and did it ever happen with the previous version `v0.18.2`?

Related: https://github.com/aome510/spotify-player/issues/399. I thought I already fixed it before 🤔 
I didn’t check the previous version, I started to change the config after upgrade. I’ll give it a try.

It happens all the time.
@aome510 is also happening in the previous version `0.18.2`
I can also second this bug, I can replicate it on macOS and Linux , given the playback window is at the bottom.

Fix patch:
diff --git a/spotify_player/src/event/mod.rs b/spotify_player/src/event/mod.rs
index d9490a21..d816dc79 100644
--- a/spotify_player/src/event/mod.rs
+++ b/spotify_player/src/event/mod.rs
@@ -5,7 +5,7 @@ use crate::{
     key::{Key, KeySequence},
     state::*,
     ui::single_line_input::LineInput,
-    utils::{new_list_state, parse_uri},
+    utils::parse_uri,
 };
 
 #[cfg(feature = "lyric-finder")]
@@ -13,6 +13,7 @@ use crate::utils::map_join;
 use anyhow::{Context as _, Result};
 
 use clipboard::{execute_copy_command, get_clipboard_content};
+use tui::widgets::ListState;
 
 mod clipboard;
 mod page;
@@ -159,7 +160,7 @@ pub fn handle_action_in_context(
                 client_pub.send(ClientRequest::GetUserPlaylists)?;
                 ui.popup = Some(PopupState::UserPlaylistList(
                     PlaylistPopupAction::AddTrack(track.id),
-                    new_list_state(),
+                    ListState::default(),
                 ));
             }
             Action::ToggleLiked => {
@@ -196,7 +197,7 @@ pub fn handle_action_in_context(
                             album,
                             context.get_available_actions(data),
                         )),
-                        new_list_state(),
+                        ListState::default(),
                     ));
                 }
             }
@@ -320,7 +321,7 @@ fn handle_go_to_artist(artists: Vec<Artist>, ui: &mut UIStateGuard) {
         ui.popup = Some(PopupState::ArtistList(
             ArtistPopupAction::Browse,
             artists,
-            new_list_state(),
+            ListState::default(),
         ));
     }
 }
@@ -334,13 +335,13 @@ fn handle_show_actions_on_artist(
         let actions = construct_artist_actions(&artists[0], data);
         ui.popup = Some(PopupState::ActionList(
             Box::new(ActionListItem::Artist(artists[0].clone(), actions)),
-            new_list_state(),
+            ListState::default(),
         ));
     } else {
         ui.popup = Some(PopupState::ArtistList(
             ArtistPopupAction::ShowActions,
             artists,
-            new_list_state(),
+            ListState::default(),
         ));
     }
 }
@@ -428,7 +429,7 @@ fn handle_global_command(
                     let actions = command::construct_track_actions(&track, &data);
                     ui.popup = Some(PopupState::ActionList(
                         Box::new(ActionListItem::Track(track, actions)),
-                        new_list_state(),
+                        ListState::default(),
                     ));
                 }
             }
@@ -444,16 +445,16 @@ fn handle_global_command(
             client_pub.send(ClientRequest::GetUserPlaylists)?;
             ui.popup = Some(PopupState::UserPlaylistList(
                 PlaylistPopupAction::Browse,
-                new_list_state(),
+                ListState::default(),
             ));
         }
         Command::BrowseUserFollowedArtists => {
             client_pub.send(ClientRequest::GetUserFollowedArtists)?;
-            ui.popup = Some(PopupState::UserFollowedArtistList(new_list_state()));
+            ui.popup = Some(PopupState::UserFollowedArtistList(ListState::default()));
         }
         Command::BrowseUserSavedAlbums => {
             client_pub.send(ClientRequest::GetUserSavedAlbums)?;
-            ui.popup = Some(PopupState::UserSavedAlbumList(new_list_state()));
+            ui.popup = Some(PopupState::UserSavedAlbumList(ListState::default()));
         }
         Command::TopTrackPage => {
             ui.new_page(PageState::Context {
@@ -500,7 +501,7 @@ fn handle_global_command(
         Command::BrowsePage => {
             ui.new_page(PageState::Browse {
                 state: BrowsePageUIState::CategoryList {
-                    state: new_list_state(),
+                    state: ListState::default(),
                 },
             });
             client_pub.send(ClientRequest::GetBrowseCategories)?;
@@ -576,7 +577,7 @@ fn handle_global_command(
             }
         }
         Command::SwitchDevice => {
-            ui.popup = Some(PopupState::DeviceList(new_list_state()));
+            ui.popup = Some(PopupState::DeviceList(ListState::default()));
             client_pub.send(ClientRequest::GetDevices)?;
         }
         Command::SwitchTheme => {
@@ -588,7 +589,7 @@ fn handle_global_command(
                 themes.insert(0, theme);
             }
 
-            ui.popup = Some(PopupState::ThemeList(themes, new_list_state()));
+            ui.popup = Some(PopupState::ThemeList(themes, ListState::default()));
         }
         #[cfg(feature = "streaming")]
         Command::RestartIntegratedClient => {
diff --git a/spotify_player/src/event/page.rs b/spotify_player/src/event/page.rs
index cf0da228..a27c7da3 100644
--- a/spotify_player/src/event/page.rs
+++ b/spotify_player/src/event/page.rs
@@ -327,7 +327,7 @@ fn handle_command_for_browse_page(
                         ui.new_page(PageState::Browse {
                             state: BrowsePageUIState::CategoryPlaylistList {
                                 category: categories[selected].clone(),
-                                state: new_list_state(),
+                                state: ListState::default(),
                             },
                         });
                     }
diff --git a/spotify_player/src/event/popup.rs b/spotify_player/src/event/popup.rs
index 1ecee195..339cf633 100644
--- a/spotify_player/src/event/popup.rs
+++ b/spotify_player/src/event/popup.rs
@@ -76,7 +76,7 @@ pub fn handle_key_sequence_for_popup(
                             };
                             ui.popup = Some(PopupState::ActionList(
                                 Box::new(ActionListItem::Artist(artists[id].clone(), actions)),
-                                new_list_state(),
+                                ListState::default(),
                             ));
                         }
                     }
diff --git a/spotify_player/src/event/window.rs b/spotify_player/src/event/window.rs
index ced624bb..a5a07a8d 100644
--- a/spotify_player/src/event/window.rs
+++ b/spotify_player/src/event/window.rs
@@ -250,7 +250,7 @@ fn handle_playlist_modify_command(
             actions.push(Action::DeleteFromPlaylist);
             ui.popup = Some(PopupState::ActionList(
                 Box::new(ActionListItem::Track(tracks[id].clone(), actions)),
-                new_list_state(),
+                ListState::default(),
             ));
             return Ok(true);
         }
@@ -325,7 +325,7 @@ fn handle_command_for_track_table_window(
             let actions = command::construct_track_actions(filtered_tracks[id], data);
             ui.popup = Some(PopupState::ActionList(
                 Box::new(ActionListItem::Track(tracks[id].clone(), actions)),
-                new_list_state(),
+                ListState::default(),
             ));
         }
         Command::AddSelectedItemToQueue => {
@@ -369,7 +369,7 @@ pub fn handle_command_for_track_list_window(
             let actions = command::construct_track_actions(tracks[id], data);
             ui.popup = Some(PopupState::ActionList(
                 Box::new(ActionListItem::Track(tracks[id].clone(), actions)),
-                new_list_state(),
+                ListState::default(),
             ));
         }
         Command::AddSelectedItemToQueue => {
@@ -407,7 +407,7 @@ pub fn handle_command_for_artist_list_window(
             let actions = construct_artist_actions(artists[id], data);
             ui.popup = Some(PopupState::ActionList(
                 Box::new(ActionListItem::Artist(artists[id].clone(), actions)),
-                new_list_state(),
+                ListState::default(),
             ));
         }
         _ => return Ok(false),
@@ -443,7 +443,7 @@ pub fn handle_command_for_album_list_window(
             let actions = construct_album_actions(albums[id], data);
             ui.popup = Some(PopupState::ActionList(
                 Box::new(ActionListItem::Album(albums[id].clone(), actions)),
-                new_list_state(),
+                ListState::default(),
             ));
         }
         Command::AddSelectedItemToQueue => {
@@ -481,7 +481,7 @@ pub fn handle_command_for_playlist_list_window(
             let actions = construct_playlist_actions(playlists[id], data);
             ui.popup = Some(PopupState::ActionList(
                 Box::new(ActionListItem::Playlist(playlists[id].clone(), actions)),
-                new_list_state(),
+                ListState::default(),
             ));
         }
         _ => return Ok(false),
diff --git a/spotify_player/src/state/ui/page.rs b/spotify_player/src/state/ui/page.rs
index f10ac914..3506364f 100644
--- a/spotify_player/src/state/ui/page.rs
+++ b/spotify_player/src/state/ui/page.rs
@@ -1,4 +1,4 @@
-use crate::{state::model::*, ui::single_line_input::LineInput, utils};
+use crate::{state::model::*, ui::single_line_input::LineInput};
 use tui::widgets::{ListState, TableState};
 
 #[derive(Clone, Debug)]
@@ -229,9 +229,9 @@ impl PageState {
 impl LibraryPageUIState {
     pub fn new() -> Self {
         Self {
-            playlist_list: utils::new_list_state(),
-            saved_album_list: utils::new_list_state(),
-            followed_artist_list: utils::new_list_state(),
+            playlist_list: ListState::default(),
+            saved_album_list: ListState::default(),
+            followed_artist_list: ListState::default(),
             focus: LibraryFocusState::Playlists,
         }
     }
@@ -240,10 +240,10 @@ impl LibraryPageUIState {
 impl SearchPageUIState {
     pub fn new() -> Self {
         Self {
-            track_list: utils::new_list_state(),
-            album_list: utils::new_list_state(),
-            artist_list: utils::new_list_state(),
-            playlist_list: utils::new_list_state(),
+            track_list: ListState::default(),
+            album_list: ListState::default(),
+            artist_list: ListState::default(),
+            playlist_list: ListState::default(),
             focus: SearchFocusState::Input,
         }
     }
@@ -266,28 +266,28 @@ impl ContextPageType {
 impl ContextPageUIState {
     pub fn new_playlist() -> Self {
         Self::Playlist {
-            track_table: utils::new_table_state(),
+            track_table: TableState::default(),
         }
     }
 
     pub fn new_album() -> Self {
         Self::Album {
-            track_table: utils::new_table_state(),
+            track_table: TableState::default(),
         }
     }
 
     pub fn new_artist() -> Self {
         Self::Artist {
-            top_track_table: utils::new_table_state(),
-            album_table: utils::new_table_state(),
-            related_artist_list: utils::new_list_state(),
+            top_track_table: TableState::default(),
+            album_table: TableState::default(),
+            related_artist_list: ListState::default(),
             focus: ArtistFocusState::TopTracks,
         }
     }
 
     pub fn new_tracks() -> Self {
         Self::Tracks {
-            track_table: utils::new_table_state(),
+            track_table: TableState::default(),
         }
     }
 }
diff --git a/spotify_player/src/ui/mod.rs b/spotify_player/src/ui/mod.rs
index a5a03cb1..6d5e67e2 100644
--- a/spotify_player/src/ui/mod.rs
+++ b/spotify_player/src/ui/mod.rs
@@ -82,14 +82,15 @@ fn clean_up(mut terminal: Terminal) -> Result<()> {
 
 /// Render the application
 fn render_application(frame: &mut Frame, state: &SharedState, ui: &mut UIStateGuard, rect: Rect) {
-    // rendering order: shortcut help popup -> playback window -> other popups -> main layout
+    // rendering order: playback window -> shortcut help popup -> other popups -> main layout
 
-    let rect = popup::render_shortcut_help_popup(frame, ui, rect);
-
-    // render playback window before other popups to ensure no popup is rendered on top
-    // of the playback window
+    // render playback window before other popups and windows to ensure nothing is rendered on top
+    // of the playback window, which is to avoid "duplicated images" issue
+    // See: https://github.com/aome510/spotify-player/issues/498
     let rect = playback::render_playback_window(frame, state, ui, rect);
 
+    let rect = popup::render_shortcut_help_popup(frame, ui, rect);
+
     let (rect, is_active) = popup::render_popup(frame, state, ui, rect);
 
     render_main_layout(is_active, frame, state, ui, rect);
diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs
index b888d538..8ff1db45 100644
--- a/spotify_player/src/ui/playback.rs
+++ b/spotify_player/src/ui/playback.rs
@@ -64,9 +64,10 @@ pub fn render_playback_window(
                             };
 
                             if needs_clear {
-                                // clear the image's area to ensure no remaining artifacts before rendering the image
+                                // clear the image's both new and old areas to ensure no remaining artifacts before rendering the image
                                 // See: https://github.com/aome510/spotify-player/issues/389
-                                frame.render_widget(Clear, cover_img_rect);
+                                clear_area(frame, ui.last_cover_image_render_info.render_area);
+                                clear_area(frame, cover_img_rect);
                             } else {
                                 if !ui.last_cover_image_render_info.rendered {
                                     if let Err(err) = render_playback_cover_image(state, ui) {
@@ -118,14 +119,7 @@ pub fn render_playback_window(
         #[cfg(feature = "image")]
         {
             if ui.last_cover_image_render_info.rendered {
-                let rect = ui.last_cover_image_render_info.render_area;
-                // reset the `skip` state of cells in the cover image area
-                // in order to render the "No playback found" message
-                for x in rect.left()..rect.right() {
-                    for y in rect.top()..rect.bottom() {
-                        frame.buffer_mut().get_mut(x, y).set_skip(false);
-                    }
-                }
+                clear_area(frame, ui.last_cover_image_render_info.render_area);
                 ui.last_cover_image_render_info = Default::default();
             }
         }
@@ -144,6 +138,15 @@ pub fn render_playback_window(
     other_rect
 }
 
+#[cfg(feature = "image")]
+fn clear_area(frame: &mut Frame, rect: Rect) {
+    for x in rect.left()..rect.right() {
+        for y in rect.top()..rect.bottom() {
+            frame.buffer_mut().get_mut(x, y).reset();
+        }
+    }
+}
+
 fn construct_playback_text(
     ui: &UIStateGuard,
     track: &rspotify_model::FullTrack,
diff --git a/spotify_player/src/ui/utils.rs b/spotify_player/src/ui/utils.rs
index 1917b122..0a6424f2 100644
--- a/spotify_player/src/ui/utils.rs
+++ b/spotify_player/src/ui/utils.rs
@@ -73,12 +73,14 @@ pub fn construct_list_widget<'a>(
     )
 }
 
-// Adjust the `selected` position of a `ListState` if that position is out of index
+/// adjust the `selected` position of a `ListState` if that position is invalid
 fn adjust_list_state(state: &mut ListState, len: usize) {
     if let Some(p) = state.selected() {
         if p >= len {
             state.select(if len > 0 { Some(len - 1) } else { Some(0) });
         }
+    } else if len > 0 {
+        state.select(Some(0));
     }
 }
 
@@ -93,12 +95,14 @@ pub fn render_list_window(
     frame.render_stateful_widget(widget, rect, state);
 }
 
-// Adjust the `selected` position of a `TableState` if that position is out of index
+/// adjust the `selected` position of a `TableState` if that position is invalid
 fn adjust_table_state(state: &mut TableState, len: usize) {
     if let Some(p) = state.selected() {
         if p >= len {
             state.select(if len > 0 { Some(len - 1) } else { Some(0) });
         }
+    } else if len > 0 {
+        state.select(Some(0));
     }
 }
 
diff --git a/spotify_player/src/utils.rs b/spotify_player/src/utils.rs
index 10b4c63a..894d445d 100644
--- a/spotify_player/src/utils.rs
+++ b/spotify_player/src/utils.rs
@@ -1,25 +1,11 @@
 use std::borrow::Cow;
 
-use tui::widgets::*;
-
 /// formats a time duration into a "{minutes}:{seconds}" format
 pub fn format_duration(duration: &chrono::Duration) -> String {
     let secs = duration.num_seconds();
     format!("{}:{:02}", secs / 60, secs % 60)
 }
 
-pub fn new_list_state() -> ListState {
-    let mut state = ListState::default();
-    state.select(Some(0));
-    state
-}
-
-pub fn new_table_state() -> TableState {
-    let mut state = TableState::default();
-    state.select(Some(0));
-    state
-}
-
 pub fn map_join<T, F>(v: &[T], f: F, sep: &str) -> String
 where
     F: Fn(&T) -> &str,


Test patch: