From 55194f6015f1dddfc2d293913747985cb0bc6a02 Mon Sep 17 00:00:00 2001
From: Kubat <maelle.martin@proton.me>
Date: Sat, 9 Nov 2024 11:56:28 +0100
Subject: [PATCH] AMADEUS: Change the way we handle playlists

To visualize a specific playlist, we need to select it from the
playlists viewer. That thing is stored in the application and will be
viewed in place of the playlists' cards while selected, and won't be
stored in the navbar.

On the specific playlist viewer we can now deselect the playlist and go
back to the playlists' cards view.
---
 amadeus/i18n/en/amadeus.ftl              |   1 +
 amadeus/i18n/es-ES/amadeus.ftl           |   1 +
 amadeus/i18n/fr-FR/amadeus.ftl           |   1 +
 amadeus/rsc/icons/fontawesome/goback.svg |   1 +
 amadeus/src/app.rs                       |  35 +++++---
 amadeus/src/app/pages.rs                 |  49 +++++-----
 amadeus/src/app/pages/history.rs         |  21 ++---
 amadeus/src/app/pages/home.rs            |   4 +-
 amadeus/src/app/pages/playlist.rs        |  84 ------------------
 amadeus/src/app/pages/playlists.rs       | 108 ++++++++++++++++++++---
 amadeus/src/app/pages/queue.rs           |  65 ++++++--------
 amadeus/src/app/pages/search.rs          |  43 +++++----
 amadeus/src/icons.rs                     |   1 +
 13 files changed, 218 insertions(+), 196 deletions(-)
 create mode 100644 amadeus/rsc/icons/fontawesome/goback.svg
 delete mode 100644 amadeus/src/app/pages/playlist.rs

diff --git a/amadeus/i18n/en/amadeus.ftl b/amadeus/i18n/en/amadeus.ftl
index aaaeaad6..9c4207cc 100644
--- a/amadeus/i18n/en/amadeus.ftl
+++ b/amadeus/i18n/en/amadeus.ftl
@@ -15,6 +15,7 @@ page-id   = Page { $num }
 empty-queue     = Empty Queue
 empty-history   = Empty History
 empty-playlists = Empty playlists
+empty-playlist  = Empty playlists { $name }
 
 next-kara        = Next kara
 prev-kara        = Previous kara
diff --git a/amadeus/i18n/es-ES/amadeus.ftl b/amadeus/i18n/es-ES/amadeus.ftl
index 0aacad98..91078f9a 100644
--- a/amadeus/i18n/es-ES/amadeus.ftl
+++ b/amadeus/i18n/es-ES/amadeus.ftl
@@ -15,6 +15,7 @@ page-id   = Pagina { $num }
 empty-queue     = No hay nada en la cola de reproducción
 empty-history   = No has escuchado nada recientemente
 empty-playlists = No tienes listas de reproducción
+empty-playlist  = No hay nada en la lista { $name }
 
 next-kara        = Póxima kara
 prev-kara        = Previo kara
diff --git a/amadeus/i18n/fr-FR/amadeus.ftl b/amadeus/i18n/fr-FR/amadeus.ftl
index 8e1af80c..9b53636c 100644
--- a/amadeus/i18n/fr-FR/amadeus.ftl
+++ b/amadeus/i18n/fr-FR/amadeus.ftl
@@ -15,6 +15,7 @@ page-id   = Page { $num }
 empty-queue     = La queue est vide
 empty-history   = L'historique est vide
 empty-playlists = Il n'y a pas de listes de lecture de disponibles
+empty-playlist  = La liste de lecture { $name } est vide
 
 next-kara        = Kara suivant
 prev-kara        = Kara précédent
diff --git a/amadeus/rsc/icons/fontawesome/goback.svg b/amadeus/rsc/icons/fontawesome/goback.svg
new file mode 100644
index 00000000..30ecb488
--- /dev/null
+++ b/amadeus/rsc/icons/fontawesome/goback.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>
diff --git a/amadeus/src/app.rs b/amadeus/src/app.rs
index 003e1056..7f98e9e6 100644
--- a/amadeus/src/app.rs
+++ b/amadeus/src/app.rs
@@ -74,6 +74,9 @@ pub struct AppModel {
     /// Informations about the lektord server.
     lektord_state: LektordState,
 
+    /// The selected playlist in the playlists tab.
+    selected_playlist: Option<KId>,
+
     /// The store where we cache the playlists, queue, history, etc.
     store: Store,
 
@@ -176,6 +179,10 @@ pub enum Message {
     OpenKaraInfo(KId),
     ToggleContextPage(ContextPage),
 
+    // Playlist selection stuff
+    SelectPlaylist(KId),
+    UnSelectPlaylist,
+
     // Update the configuration
     UpdateConfig(ConfigMessage),
 
@@ -223,6 +230,7 @@ impl Application for AppModel {
             context_page: Default::default(),
 
             lektord_state: LektordState::Disconnected,
+            selected_playlist: None,
             store: Store::default(),
             search_filter: Filter::default(),
             search_results: Vec::default(),
@@ -335,16 +343,15 @@ impl Application for AppModel {
     /// Describes the interface based on the current state of the application model.
     fn view(&self) -> Element<Self::Message> {
         let page = match self.nav.active_data::<Page>().copied() {
-            Some(Page::Home) => pages::home::view().into(),
-            Some(Page::Queue) => pages::queue::view(&self.store).into(),
-            Some(Page::History) => pages::history::view(&self.store).into(),
-            Some(Page::Search) => pages::search::view(self).into(),
-            Some(Page::Playlists) => pages::playlists::view(&self.store).into(),
-            Some(Page::Playlist(id)) => pages::playlist::view(&self.store, id).into(),
-            page => pages::not_found(page).into(),
+            Some(Page::Home) => pages::home::view(),
+            Some(Page::Queue) => pages::queue::view(&self.store),
+            Some(Page::History) => pages::history::view(&self.store),
+            Some(Page::Search) => pages::search::view(self),
+            Some(Page::Playlists) => pages::playlists::view(&self.store, self.selected_playlist),
+            page => pages::not_found(page),
         };
 
-        widget::column()
+        widget::column::with_capacity(2)
             .push(page)
             .push(bottom_bar::view(self))
             .apply(widget::container)
@@ -415,14 +422,22 @@ impl Application for AppModel {
                 cosmic::command::future(async move {
                     requests::search_karas(&*config.read().await, SearchFrom::Database, filters)
                         .await
-                        .map(Message::QueryWithFiltersResults)
-                        .map(cosmic::app::message::app)
+                        .map(|res| cosmic::app::message::app(Message::QueryWithFiltersResults(res)))
                         .unwrap_or_else(|err| {
                             log::error!("failed to query with filters: {err}");
                             cosmic::app::message::none()
                         })
                 })
             }
+
+            Message::SelectPlaylist(kid) => {
+                self.selected_playlist = Some(kid);
+                Task::none()
+            }
+            Message::UnSelectPlaylist => {
+                self.selected_playlist = None;
+                Task::none()
+            }
         }
     }
 
diff --git a/amadeus/src/app/pages.rs b/amadeus/src/app/pages.rs
index c79b3c02..2957cd3c 100644
--- a/amadeus/src/app/pages.rs
+++ b/amadeus/src/app/pages.rs
@@ -8,16 +8,13 @@ use cosmic::{
         Alignment, Length,
     },
     prelude::*,
-    style, theme,
-    widget::{self, icon, nav_bar, Icon},
+    style, theme, widget,
 };
 use derive_more::Display;
-use lektor_payloads::KId;
 use std::marker;
 
 pub mod history;
 pub mod home;
-pub mod playlist;
 pub mod playlists;
 pub mod queue;
 pub mod search;
@@ -41,27 +38,26 @@ pub enum Page {
 
     #[display("{}", fl!("playlists"))]
     Playlists,
-
-    #[display("{}", fl!("playlist"))]
-    Playlist(KId),
 }
 
 impl Page {
     /// Get the icon associated with the page, to be displayed along with the [nav_bar::Model].
-    pub fn icon(&self) -> Icon {
-        let icon = match self {
+    pub fn icon(&self) -> widget::Icon {
+        match self {
             Page::Home => crate::icons::USER,
             Page::Queue => crate::icons::QUEUE,
-            Page::History => crate::icons::HISTORY,
             Page::Search => crate::icons::SEARCH,
-            Page::Playlists | Page::Playlist(_) => crate::icons::ARCHIVE,
-        };
-        icon::icon(icon::from_svg_bytes(icon).symbolic(true))
+            Page::History => crate::icons::HISTORY,
+            Page::Playlists => crate::icons::ARCHIVE,
+        }
+        .apply(widget::icon::from_svg_bytes)
+        .symbolic(true)
+        .apply(widget::icon::icon)
     }
 }
 
 /// Get the navigation bar for the pages.
-pub fn nav_bar_model() -> nav_bar::Model {
+pub fn nav_bar_model() -> widget::nav_bar::Model {
     macro_rules! insert {
         ($b:expr, $page:ident) => {
             $b.text(Page::$page.to_string())
@@ -69,7 +65,7 @@ pub fn nav_bar_model() -> nav_bar::Model {
                 .data(Page::$page)
         };
     }
-    nav_bar::Model::builder()
+    widget::nav_bar::Model::builder()
         .insert(|b| insert!(b, Home).activate())
         .insert(|b| insert!(b, Queue))
         .insert(|b| insert!(b, History))
@@ -79,7 +75,7 @@ pub fn nav_bar_model() -> nav_bar::Model {
 }
 
 /// We got a page that was not implemented, or no page at all.
-pub fn not_found<'a>(page: Option<Page>) -> impl Into<Element<'a, Message>> {
+pub fn not_found<'a>(page: Option<Page>) -> Element<'a, Message> {
     widget::column()
         .push(widget::text::title1(fl!("error-found")))
         .push_maybe(
@@ -91,13 +87,14 @@ pub fn not_found<'a>(page: Option<Page>) -> impl Into<Element<'a, Message>> {
         .height(Length::Fill)
         .align_x(Horizontal::Center)
         .align_y(Vertical::Center)
+        .into()
 }
 
 /// Helper for the icons at the right of the title in pages.
 #[must_use]
 struct PageViewControl<'a> {
     icon: &'static [u8],
-    message: Message,
+    message: Option<Message>,
     on_non_empty_content: bool,
     is_destructive_icon: bool,
     marker: marker::PhantomData<Element<'a, Message>>,
@@ -105,16 +102,24 @@ struct PageViewControl<'a> {
 
 impl<'a> PageViewControl<'a> {
     /// Create a new control with an icon and an associated message.
-    pub fn new(icon: &'static [u8], message: Message) -> Self {
+    pub fn new(icon: &'static [u8]) -> Self {
         Self {
             icon,
-            message,
+            message: None,
             on_non_empty_content: false,
             is_destructive_icon: false,
             marker: marker::PhantomData,
         }
     }
 
+    /// Set the message to use when the button will be pressed.
+    pub fn message(self, message: Message) -> Self {
+        Self {
+            message: Some(message),
+            ..self
+        }
+    }
+
     /// Make the button a destructive one.
     pub fn destructive(self) -> Self {
         Self {
@@ -146,7 +151,11 @@ impl<'a> PageViewControl<'a> {
         let button = widget::icon::from_svg_bytes(icon)
             .symbolic(true)
             .apply(widget::button::icon)
-            .on_press_maybe((!page_is_empty || !on_non_empty_content).then_some(message))
+            .on_press_maybe(
+                (!page_is_empty || !on_non_empty_content)
+                    .then_some(message)
+                    .flatten(),
+            )
             .padding(space_xxs)
             .width(32)
             .height(32);
diff --git a/amadeus/src/app/pages/history.rs b/amadeus/src/app/pages/history.rs
index 7f768e05..2c67b24b 100644
--- a/amadeus/src/app/pages/history.rs
+++ b/amadeus/src/app/pages/history.rs
@@ -4,26 +4,22 @@ use crate::{
         pages::{PageView, PageViewControl},
         LektordCommand, Message,
     },
-    fl,
+    fl, icons,
     store::Store,
 };
 use cosmic::{prelude::*, widget};
 
 /// Display the history page.
-pub fn view(store: &Store) -> impl Into<Element<Message>> {
+pub fn view(store: &Store) -> Element<Message> {
     PageView::default()
         .titles(fl!("history"), fl!("empty-history"))
         .controls([
-            PageViewControl::new(
-                crate::icons::RETRY,
-                Message::SendCommand(LektordCommand::HistoryGet),
-            ),
-            PageViewControl::new(
-                crate::icons::METEOR,
-                Message::SendCommand(LektordCommand::HistoryClear),
-            )
-            .destructive()
-            .need_content(),
+            PageViewControl::new(icons::RETRY)
+                .message(Message::SendCommand(LektordCommand::HistoryGet)),
+            PageViewControl::new(icons::METEOR)
+                .message(Message::SendCommand(LektordCommand::HistoryClear))
+                .destructive()
+                .need_content(),
         ])
         .push_when(!store.iter_history().is_empty(), || {
             (store.iter_history()).fold(widget::list_column(), |list, kid| {
@@ -31,4 +27,5 @@ pub fn view(store: &Store) -> impl Into<Element<Message>> {
             })
         })
         .view()
+        .into()
 }
diff --git a/amadeus/src/app/pages/home.rs b/amadeus/src/app/pages/home.rs
index 71d31c08..96e01a1c 100644
--- a/amadeus/src/app/pages/home.rs
+++ b/amadeus/src/app/pages/home.rs
@@ -5,6 +5,6 @@ use crate::{
 use cosmic::prelude::*;
 
 /// Display the home page.
-pub fn view<'a>() -> impl Into<Element<'a, Message>> {
-    PageView::default().title(fl!("home")).view()
+pub fn view<'a>() -> Element<'a, Message> {
+    PageView::default().title(fl!("home")).view().into()
 }
diff --git a/amadeus/src/app/pages/playlist.rs b/amadeus/src/app/pages/playlist.rs
deleted file mode 100644
index 007ba72c..00000000
--- a/amadeus/src/app/pages/playlist.rs
+++ /dev/null
@@ -1,84 +0,0 @@
-use crate::{
-    app::{
-        kard,
-        pages::{PageView, PageViewControl},
-        LektordCommand, Message,
-    },
-    fl,
-    store::Store,
-};
-use cosmic::{prelude::*, style, widget};
-use lektor_payloads::{KId, PlaylistInfo};
-
-/// Display the page about a specific playlist.
-pub fn view(store: &Store, id: KId) -> impl Into<Element<Message>> {
-    let Some(infos) = store.playlist(id) else {
-        todo!()
-    };
-
-    let name = infos
-        .name()
-        .map(|name| fl!("playlist", name = name))
-        .unwrap_or_else(|| "untitled".to_string());
-
-    let owners = infos
-        .infos()
-        .into_iter()
-        .flat_map(PlaylistInfo::owners)
-        .map(|owner| -> Element<'_, Message> {
-            widget::text::body(owner)
-                .apply(widget::button::custom)
-                .class(style::Button::Transparent)
-                .into()
-        });
-
-    let (created_at, updated_at) = infos
-        .infos()
-        .map(|infos| (infos.created_at(), infos.updated_at()))
-        .unwrap_or_default();
-
-    let infos = widget::settings::section()
-        .add(widget::settings::item(
-            "owners",
-            widget::row::with_children(owners.collect()),
-        ))
-        .add(widget::settings::item(
-            "created at",
-            widget::text::body(created_at.format("%Y-%m-%d %H:%M:%S").to_string()),
-        ))
-        .add(widget::settings::item(
-            "last modified at",
-            widget::text::body(updated_at.format("%Y-%m-%d %H:%M:%S").to_string()),
-        ));
-
-    let display_playlist_content = || {
-        (store.iter_playlist_content(id)).fold(widget::list_column(), |list, kid| {
-            list.add(kard::view(store.get(kid)))
-        })
-    };
-
-    PageView::default()
-        .titles(&name, format!("The playlist {name} is empty"))
-        .controls([
-            PageViewControl::new(
-                crate::icons::SHUFFLE,
-                Message::SendCommand(LektordCommand::PlaylistShuffleContent(id)),
-            )
-            .need_content(),
-            PageViewControl::new(
-                crate::icons::RETRY,
-                Message::SendCommand(LektordCommand::PlaylistGetContent(id)),
-            ),
-            PageViewControl::new(
-                crate::icons::METEOR,
-                Message::SendCommand(LektordCommand::PlaylistDelete(id)),
-            )
-            .destructive(),
-        ])
-        .push_and_ignore_for_empty(infos)
-        .push_when(
-            !store.iter_playlist_content(id).is_empty(),
-            display_playlist_content,
-        )
-        .view()
-}
diff --git a/amadeus/src/app/pages/playlists.rs b/amadeus/src/app/pages/playlists.rs
index 51745b73..cb397585 100644
--- a/amadeus/src/app/pages/playlists.rs
+++ b/amadeus/src/app/pages/playlists.rs
@@ -1,33 +1,117 @@
 use crate::{
     app::{
+        kard,
         pages::{PageView, PageViewControl},
         LektordCommand, Message,
     },
-    fl,
+    fl, icons,
     playlist::Playlist,
     store::Store,
 };
-use cosmic::{prelude::*, widget};
+use cosmic::{prelude::*, style, widget};
+use lektor_payloads::{KId, PlaylistInfo};
 
-fn view_playlist_card(playlist: &Playlist) -> impl Into<Element<Message>> {
-    widget::text(playlist.name().unwrap_or("untitled"))
+/// Display a card that summerize the playlist, when pressed will select the given playlist.
+fn view_playlist_card(playlist: &Playlist) -> Option<Element<Message>> {
+    let infos = playlist.infos()?;
+    let card = widget::text(infos.name())
+        .apply(widget::button::custom)
+        .on_press(Message::SelectPlaylist(infos.local_id()));
+    Some(card.into())
 }
 
-/// Display all playlists in a page.
-pub fn view(store: &Store) -> impl Into<Element<Message>> {
+/// Display all the cards for all the available playlists.
+fn view_playlists_cards(store: &Store) -> Element<Message> {
     PageView::default()
         .titles(fl!("playlists"), fl!("empty-playlists"))
-        .controls([PageViewControl::new(
-            crate::icons::RETRY,
-            Message::SendCommand(LektordCommand::PlaylistsGet),
-        )])
+        .controls([PageViewControl::new(crate::icons::RETRY)
+            .message(Message::SendCommand(LektordCommand::PlaylistsGet))])
         .push_when(store.iter_playlists().count() != 0, || {
             store
                 .iter_playlists()
-                .map(view_playlist_card)
-                .map(Into::into)
+                .flat_map(view_playlist_card)
                 .collect::<Vec<_>>()
                 .apply(widget::flex_row)
         })
         .view()
+        .into()
+}
+
+/// Display the page about a specific playlist.
+fn view_playlist_content(store: &Store, id: KId) -> Element<Message> {
+    let Some(infos) = store.playlist(id) else {
+        todo!()
+    };
+
+    let name = infos
+        .name()
+        .map(|name| fl!("playlist", name = name))
+        .unwrap_or_else(|| "untitled".to_string());
+
+    let owners = infos
+        .infos()
+        .into_iter()
+        .flat_map(PlaylistInfo::owners)
+        .map(|owner| -> Element<'_, Message> {
+            widget::text::body(owner)
+                .apply(widget::button::custom)
+                .class(style::Button::Transparent)
+                .into()
+        });
+
+    let (created_at, updated_at) = infos
+        .infos()
+        .map(|infos| (infos.created_at(), infos.updated_at()))
+        .unwrap_or_default();
+
+    let infos = widget::settings::section()
+        .add(widget::settings::item(
+            "owners",
+            widget::row::with_children(owners.collect()),
+        ))
+        .add(widget::settings::item(
+            "created at",
+            widget::text::body(created_at.format("%Y-%m-%d %H:%M:%S").to_string()),
+        ))
+        .add(widget::settings::item(
+            "last modified at",
+            widget::text::body(updated_at.format("%Y-%m-%d %H:%M:%S").to_string()),
+        ));
+
+    let display_playlist_content = || {
+        (store.iter_playlist_content(id)).fold(widget::list_column(), |list, kid| {
+            list.add(kard::view(store.get(kid)))
+        })
+    };
+
+    PageView::default()
+        .titles(&name, fl!("empty-playlist", name = name.clone()))
+        .controls([
+            PageViewControl::new(icons::GOBACK).message(Message::UnSelectPlaylist),
+            PageViewControl::new(icons::SHUFFLE)
+                .message(Message::SendCommand(
+                    LektordCommand::PlaylistShuffleContent(id),
+                ))
+                .need_content(),
+            PageViewControl::new(icons::RETRY)
+                .message(Message::SendCommand(LektordCommand::PlaylistGetContent(id))),
+            PageViewControl::new(icons::METEOR)
+                .message(Message::SendCommand(LektordCommand::PlaylistDelete(id)))
+                .destructive(),
+        ])
+        .push_and_ignore_for_empty(infos)
+        .push_when(
+            !store.iter_playlist_content(id).is_empty(),
+            display_playlist_content,
+        )
+        .view()
+        .into()
+}
+
+/// Display all playlists in a page, or the selected playlist.
+pub fn view(store: &Store, selected_playlist: Option<KId>) -> Element<Message> {
+    match selected_playlist {
+        Some(id) => view_playlist_content(store, id),
+        None => view_playlists_cards(store),
+    }
 }
diff --git a/amadeus/src/app/pages/queue.rs b/amadeus/src/app/pages/queue.rs
index bc7ab1e8..9ca0a568 100644
--- a/amadeus/src/app/pages/queue.rs
+++ b/amadeus/src/app/pages/queue.rs
@@ -1,6 +1,6 @@
 use crate::{
     app::{kard, pages::PageView, LektordCommand, Message},
-    fl,
+    fl, icons,
     store::Store,
 };
 use cosmic::{prelude::*, style, widget};
@@ -13,21 +13,17 @@ fn view_queue_level(store: &Store, level: Priority) -> impl IntoIterator<Item =
         widget::text::title2(format!("{} {level}", fl!("queue"))).class(style::Text::Default),
         false,
         vec![
-            PageViewControl::new(
-                crate::icons::SHUFFLE,
-                Message::SendCommand(LektordCommand::QueueLevelShuffle(level)),
-            )
-            .need_content(),
-            PageViewControl::new(
-                crate::icons::RETRY,
-                Message::SendCommand(LektordCommand::QueueLevelGet(level)),
-            ),
-            PageViewControl::new(
-                crate::icons::METEOR,
-                Message::SendCommand(LektordCommand::QueueLevelClear(level)),
-            )
-            .need_content()
-            .destructive(),
+            PageViewControl::new(icons::SHUFFLE)
+                .message(Message::SendCommand(LektordCommand::QueueLevelShuffle(
+                    level,
+                )))
+                .need_content(),
+            PageViewControl::new(icons::RETRY)
+                .message(Message::SendCommand(LektordCommand::QueueLevelGet(level))),
+            PageViewControl::new(icons::METEOR)
+                .message(Message::SendCommand(LektordCommand::QueueLevelClear(level)))
+                .need_content()
+                .destructive(),
         ],
         store.iter_queue_level(level).is_empty(),
     );
@@ -42,31 +38,23 @@ fn view_queue_level(store: &Store, level: Priority) -> impl IntoIterator<Item =
 }
 
 /// Displays the page with the queue.
-pub fn view(store: &Store) -> impl Into<Element<Message>> {
+pub fn view(store: &Store) -> Element<Message> {
     PageView::default()
         .titles(fl!("queue"), fl!("empty-queue"))
         .controls([
-            PageViewControl::new(
-                crate::icons::SHUFFLE,
-                Message::SendCommand(LektordCommand::QueueShuffle),
-            )
-            .need_content(),
-            PageViewControl::new(
-                crate::icons::RETRY,
-                Message::SendCommand(LektordCommand::QueueGet),
-            ),
-            PageViewControl::new(
-                crate::icons::CROP,
-                Message::SendCommand(LektordCommand::QueueCrop),
-            )
-            .need_content()
-            .destructive(),
-            PageViewControl::new(
-                crate::icons::METEOR,
-                Message::SendCommand(LektordCommand::QueueClear),
-            )
-            .need_content()
-            .destructive(),
+            PageViewControl::new(icons::SHUFFLE)
+                .message(Message::SendCommand(LektordCommand::QueueShuffle))
+                .need_content(),
+            PageViewControl::new(icons::RETRY)
+                .message(Message::SendCommand(LektordCommand::QueueGet)),
+            PageViewControl::new(icons::CROP)
+                .message(Message::SendCommand(LektordCommand::QueueCrop))
+                .need_content()
+                .destructive(),
+            PageViewControl::new(icons::METEOR)
+                .message(Message::SendCommand(LektordCommand::QueueClear))
+                .need_content()
+                .destructive(),
         ])
         .extend(
             (PRIORITY_VALUES.iter().rev())
@@ -76,4 +64,5 @@ pub fn view(store: &Store) -> impl Into<Element<Message>> {
                 .flatten(),
         )
         .view()
+        .into()
 }
diff --git a/amadeus/src/app/pages/search.rs b/amadeus/src/app/pages/search.rs
index c305e7a5..3e1b6fcb 100644
--- a/amadeus/src/app/pages/search.rs
+++ b/amadeus/src/app/pages/search.rs
@@ -1,6 +1,13 @@
 //! Contains everything to implement search from Amadeus.
 
-use crate::app::{kard, pages::PageView, AppModel, Message};
+use crate::{
+    app::{
+        kard,
+        pages::{PageView, PageViewControl},
+        AppModel, Message,
+    },
+    icons,
+};
 use cosmic::{iced::Alignment, prelude::*, style, theme, widget};
 use lektor_payloads::KaraBy;
 use std::{
@@ -9,8 +16,6 @@ use std::{
     sync::atomic::{AtomicUsize, Ordering},
 };
 
-use super::PageViewControl;
-
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct FilterAtom(FilterAtomId, KaraBy);
 
@@ -20,14 +25,6 @@ pub struct FilterAtomId(usize);
 #[derive(Debug, Default)]
 pub struct Filter(String, Vec<FilterAtom>);
 
-macro_rules! icon {
-    ($icon:ident) => {
-        widget::icon::from_svg_bytes(crate::icons::$icon)
-            .symbolic(true)
-            .apply(widget::icon)
-    };
-}
-
 impl FromStr for FilterAtom {
     type Err = Infallible;
 
@@ -45,6 +42,14 @@ impl FilterAtomId {
 
 impl FilterAtom {
     fn view(&self) -> Element<Message> {
+        macro_rules! icon {
+            ($icon:ident) => {
+                widget::icon::from_svg_bytes(icons::$icon)
+                    .symbolic(true)
+                    .apply(widget::icon)
+            };
+        }
+
         let (icon, text) = match &self.1 {
             KaraBy::Id(id) => (icon!(HASHTAG), id.to_string()),
             KaraBy::Query(query) => (icon!(FILTER), query.clone()),
@@ -113,21 +118,22 @@ impl Filter {
     }
 
     pub fn remove(&mut self, id: FilterAtomId) {
-        if let Some(idx) = (self.1.iter().enumerate())
+        _ = (self.1.iter().enumerate())
             .find_map(|(idx, FilterAtom(atom, _))| (*atom == id).then_some(idx))
-        {
-            self.1.remove(idx);
-        }
+            .map(|idx| self.1.remove(idx));
     }
 }
 
 /// Display the search page.
-pub fn view(app: &AppModel) -> impl Into<Element<Message>> {
+pub fn view(app: &AppModel) -> Element<Message> {
     PageView::default()
         .custom_title(app.search_filter.view_staging_row())
         .controls([
-            PageViewControl::new(crate::icons::FILTER, Message::QueryWithFilters).need_content(),
-            PageViewControl::new(crate::icons::METEOR, Message::ClearFilters)
+            PageViewControl::new(icons::FILTER)
+                .message(Message::QueryWithFilters)
+                .need_content(),
+            PageViewControl::new(icons::METEOR)
+                .message(Message::ClearFilters)
                 .need_content()
                 .destructive(),
         ])
@@ -139,4 +145,5 @@ pub fn view(app: &AppModel) -> impl Into<Element<Message>> {
             })
         })
         .view()
+        .into()
 }
diff --git a/amadeus/src/icons.rs b/amadeus/src/icons.rs
index 28320f56..05dfb909 100644
--- a/amadeus/src/icons.rs
+++ b/amadeus/src/icons.rs
@@ -47,3 +47,4 @@ icon!(PLAY:         "fontawesome/play");
 icon!(PAUSE:        "fontawesome/pause");
 icon!(RETRY:        "fontawesome/retry");
 icon!(CROP:         "fontawesome/crop");
+icon!(GOBACK:       "fontawesome/goback");
-- 
GitLab