From 7d701b3f1e0620f2fe2c6a5c28cad97260301516 Mon Sep 17 00:00:00 2001 From: ghasemi Date: Wed, 11 Oct 2023 18:10:45 +0330 Subject: [PATCH] nav view --- Cargo.lock | 103 +++---- .../down.sql | 1 + .../up.sql | 6 + src/body/artists.rs | 130 +++++---- src/body/collection/body.rs | 20 -- src/body/collection/button.rs | 18 +- src/body/collection/mod.rs | 16 +- src/body/collection/page.rs | 18 ++ src/body/download/albums.rs | 217 ++++++++------- src/body/download/mod.rs | 21 +- src/body/download/songs.rs | 253 +++++++++--------- src/body/merge/impl.rs | 76 ++++-- src/body/merge/mod.rs | 23 +- src/body/mod.rs | 153 ++++++----- src/common/constant.rs | 1 - src/common/state.rs | 14 +- src/common/window_action.rs | 4 - src/main.rs | 154 +++++------ src/now_playing/body.rs | 9 +- src/now_playing/bottom_widget.rs | 27 +- src/now_playing/mod.rs | 68 ++--- src/now_playing/now_playing.rs | 24 +- src/now_playing/playbin.rs | 2 +- src/schema.rs | 1 - src/song/mod.rs | 4 +- 25 files changed, 671 insertions(+), 692 deletions(-) create mode 100644 migrations/2023-10-16-142532_remove_navigation_type/down.sql create mode 100644 migrations/2023-10-16-142532_remove_navigation_type/up.sql delete mode 100644 src/body/collection/body.rs create mode 100644 src/body/collection/page.rs diff --git a/Cargo.lock b/Cargo.lock index be2dbaf..65327c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] @@ -102,24 +102,41 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ - "event-listener", + "event-listener 2.5.3", ] [[package]] name = "async-process" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ "async-io", "async-lock", - "autocfg", + "async-signal", "blocking", "cfg-if", - "event-listener", + "event-listener 3.0.0", "futures-lite", - "rustix 0.37.24", - "signal-hook", + "rustix 0.38.18", + "windows-sys", +] + +[[package]] +name = "async-signal" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a5415b7abcdc9cd7d63d6badba5288b2ca017e3fbd4173b8f405449f1a2399" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.18", + "signal-hook-registry", + "slab", "windows-sys", ] @@ -164,9 +181,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atomic_refcell" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f2bfe491d41d45507b8431da8274f7feeca64a49e86d980eed2937ec2ff020" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" [[package]] name = "autocfg" @@ -209,9 +226,9 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "blocking" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c4ef1f913d78636d78d538eec1f18de81e481f44b1be0a81060090530846e1" +checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" dependencies = [ "async-channel", "async-lock", @@ -493,30 +510,30 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "event-listener" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "2.5.3" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "fastrand" @@ -1539,9 +1556,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "lock_api" @@ -1732,9 +1749,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -2062,9 +2079,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -2197,14 +2214,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.17" +version = "0.38.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.8", + "linux-raw-sys 0.4.10", "windows-sys", ] @@ -2308,9 +2325,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" @@ -2382,16 +2399,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "signal-hook" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2624,7 +2631,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall", - "rustix 0.38.17", + "rustix 0.38.18", "windows-sys", ] @@ -2704,9 +2711,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", "bytes", diff --git a/migrations/2023-10-16-142532_remove_navigation_type/down.sql b/migrations/2023-10-16-142532_remove_navigation_type/down.sql new file mode 100644 index 0000000..d9a93fe --- /dev/null +++ b/migrations/2023-10-16-142532_remove_navigation_type/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/migrations/2023-10-16-142532_remove_navigation_type/up.sql b/migrations/2023-10-16-142532_remove_navigation_type/up.sql new file mode 100644 index 0000000..dff9dbb --- /dev/null +++ b/migrations/2023-10-16-142532_remove_navigation_type/up.sql @@ -0,0 +1,6 @@ +-- noinspection SqlWithoutWhere +delete +from bodies; + +alter table bodies + drop column navigation_type; diff --git a/src/body/artists.rs b/src/body/artists.rs index a806130..7c91e07 100644 --- a/src/body/artists.rs +++ b/src/body/artists.rs @@ -1,15 +1,15 @@ -use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; +use adw::NavigationPage; use adw::prelude::*; -use diesel::dsl::{count_distinct, count_star, min}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, update}; -use gtk::Orientation::Vertical; +use diesel::dsl::{count_distinct, count_star, min}; use gtk::{Image, Label, Separator}; +use gtk::Orientation::Vertical; use id3::TagLike; -use crate::body::{ALBUM, ARTIST, Body, BodyType, next_icon, popover_box, SONG}; -use crate::body::download::albums::albums; -use crate::body::merge::{KEY, MergeState}; +use crate::body::{ALBUM, ARTIST, Body, BodyType, handle_render, next_icon, SONG}; +use crate::body::download::albums::albums_page; +use crate::body::merge::{KEY, add_menu_merge_button, MergeState}; use crate::common::{FOLDER_MUSIC_ICON, ImagePathBuf, StyledLabelBuilder}; use crate::common::constant::INSENSITIVE_FG; use crate::common::state::State; @@ -21,65 +21,63 @@ use crate::schema::songs::{album, artist, id, path as song_path}; use crate::schema::songs::dsl::songs; use crate::song::{join_path, WithImage}; -pub fn artists(state: Rc) -> Body { - let artists = songs.inner_join(collections).group_by(artist) - .select((artist, count_distinct(album), count_star(), min(path), min(song_path))) - .get_results::<(Option, i64, i64, Option, Option)>(&mut get_connection()).unwrap(); - let title = Arc::new(String::from("Harborz")); - let subtitle = Rc::new(artists.len().number_plural(ARTIST)); - let artists_box = gtk::Box::builder().orientation(Vertical).build(); - let merge_state = MergeState::new(ARTIST, state.clone(), title.clone(), subtitle.clone(), artists_box.clone(), - |artists| { Box::new(artist.eq_any(artists)) }, || { Box::new(artist.is_null()) }, - |tag, artist_string| { tag.set_artist(artist_string); }, |song, artist_string| { - update(songs.filter(id.eq(song.id))).set(artist.eq(Some(artist_string))).execute(&mut get_connection()) - .unwrap(); - }, - ); - Body { - back_visible: false, - title, - subtitle, - popover_box: popover_box(state.clone(), merge_state.clone()), - body_type: BodyType::Artists, - params: Vec::new(), - scroll_adjustment: Cell::new(None), - widget: Box::new({ - for (artist_string, album_count, song_count, collection_path, artist_song_path) in artists { - let artist_string = artist_string.map(Arc::new); - let artist_row = gtk::Box::builder().spacing(8).build(); - if let Some(artist_string) = artist_string.clone() { - unsafe { artist_row.set_data(KEY, artist_string); } - } - artists_box.append(&artist_row); - artists_box.append(&Separator::builder().build()); - let logo = join_path(&collection_path.unwrap(), &artist_song_path.unwrap()).logo(); - artist_row.append(Image::builder().pixel_size(46).margin_start(8).build() - .set_or_default(&logo, FOLDER_MUSIC_ICON)); - merge_state.clone().handle_click(&artist_row, { - let artist_string = artist_string.clone(); - let state = state.clone(); - move || { - Rc::new(albums(vec![logo.to_str().map(|it| { Arc::new(String::from(it)) }), - artist_string.clone()], state.clone())).set_with_history(state.clone()); - } - }); - let artist_box = gtk::Box::builder().orientation(Vertical) - .margin_start(8).margin_end(4).margin_top(12).margin_bottom(12).build(); - artist_row.append(&artist_box); - artist_box.append(&Label::builder().label(&*or_none_arc(artist_string)).ellipsized().build()); - let count_box = gtk::Box::builder().spacing(4).name(INSENSITIVE_FG).build(); - artist_box.append(&count_box); - let album_count_box = gtk::Box::builder().spacing(4).build(); - count_box.append(&album_count_box); - album_count_box.append(&Label::builder().label(&album_count.to_string()).subscript().build()); - album_count_box.append(&Label::builder().label(album_count.plural(ALBUM)).subscript().build()); - let song_count_box = gtk::Box::builder().spacing(4).build(); - count_box.append(&song_count_box); - song_count_box.append(&Label::builder().label(&song_count.to_string()).subscript().build()); - song_count_box.append(&Label::builder().label(song_count.plural(SONG)).subscript().build()); - artist_row.append(&next_icon()); +const HARBORZ: &'static str = "Harborz"; + +pub fn artists_page(state: Rc) -> NavigationPage { + let body = Body::new(HARBORZ, state.clone(), Some("Artists"), Vec::new(), BodyType::Artists); + let heading = add_menu_merge_button(ARTIST, &body.menu_button, &body.popover_box); + let render = move || { + let artists_box = gtk::Box::builder().orientation(Vertical).build(); + let artists = songs.inner_join(collections).group_by(artist) + .select((artist, count_distinct(album), count_star(), min(path), min(song_path))) + .get_results::<(Option, i64, i64, Option, Option)>(&mut get_connection()).unwrap(); + let subtitle = artists.len().number_plural(ARTIST); + body.window_title.set_subtitle(&subtitle); + let merge_state = MergeState::new(ARTIST, heading.clone(), Arc::new(String::from(HARBORZ)), Rc::new(subtitle), + artists_box.clone(), &body.action_group, &body.header_bar, &body.menu_button, + |artists| { Box::new(artist.eq_any(artists)) }, || { Box::new(artist.is_null()) }, + |tag, artist_string| { tag.set_artist(artist_string); }, |song, artist_string| { + update(songs.filter(id.eq(song.id))).set(artist.eq(Some(artist_string))).execute(&mut get_connection()) + .unwrap(); + }, + ); + for (artist_string, album_count, song_count, collection_path, artist_song_path) in artists { + let artist_string = artist_string.map(Arc::new); + let artist_row = gtk::Box::builder().spacing(8).build(); + if let Some(artist_string) = artist_string.clone() { + unsafe { artist_row.set_data(KEY, artist_string); } } - merge_state.handle_pinch() - }), - } + artists_box.append(&artist_row); + artists_box.append(&Separator::builder().build()); + let logo = join_path(&collection_path.unwrap(), &artist_song_path.unwrap()).logo(); + artist_row.append(Image::builder().pixel_size(46).margin_start(8).build() + .set_or_default(&logo, FOLDER_MUSIC_ICON)); + merge_state.clone().handle_click(&artist_row, { + let state = state.clone(); + let artist_string = artist_string.clone(); + move || { + state.navigation_view.push(&albums_page(vec![logo.to_str().map(|it| { Arc::new(String::from(it)) }), + artist_string.clone()], state.clone(), None)); + } + }); + let artist_box = gtk::Box::builder().orientation(Vertical) + .margin_start(8).margin_end(4).margin_top(12).margin_bottom(12).build(); + artist_row.append(&artist_box); + artist_box.append(&Label::builder().label(&*or_none_arc(artist_string)).ellipsized().build()); + let count_box = gtk::Box::builder().spacing(4).name(INSENSITIVE_FG).build(); + artist_box.append(&count_box); + let album_count_box = gtk::Box::builder().spacing(4).build(); + count_box.append(&album_count_box); + album_count_box.append(&Label::builder().label(&album_count.to_string()).subscript().build()); + album_count_box.append(&Label::builder().label(album_count.plural(ALBUM)).subscript().build()); + let song_count_box = gtk::Box::builder().spacing(4).build(); + count_box.append(&song_count_box); + song_count_box.append(&Label::builder().label(&song_count.to_string()).subscript().build()); + song_count_box.append(&Label::builder().label(song_count.plural(SONG)).subscript().build()); + artist_row.append(&next_icon()); + } + body.scrolled_window.set_child(Some(&merge_state.handle_pinch())); + }; + handle_render(render, body.rerender); + body.navigation_page } diff --git a/src/body/collection/body.rs b/src/body/collection/body.rs deleted file mode 100644 index 1b71337..0000000 --- a/src/body/collection/body.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::cell::Cell; -use std::rc::Rc; -use std::sync::Arc; -use gtk::Orientation::Vertical; -use crate::body::{Body, BodyType}; -use crate::body::collection::add_collection_box; -use crate::common::state::State; - -pub fn collections(state: Rc) -> Body { - Body { - back_visible: true, - title: Arc::new(String::from("Harborz")), - subtitle: Rc::new(String::from("Collection")), - popover_box: gtk::Box::builder().orientation(Vertical).build(), - body_type: BodyType::Collections, - params: Vec::new(), - scroll_adjustment: Cell::new(None), - widget: Box::new(add_collection_box(state)), - } -} diff --git a/src/body/collection/button.rs b/src/body/collection/button.rs index 72bb4ab..81c7c39 100644 --- a/src/body/collection/button.rs +++ b/src/body/collection/button.rs @@ -1,19 +1,15 @@ use std::rc::Rc; use adw::prelude::*; -use gtk::Button; -use crate::body::BodyType; -use crate::body::collection::body::collections; +use gtk::{Button, MenuButton}; +use crate::body::collection::page::COLLECTION; use crate::common::state::State; -pub(in crate::body) fn create(state: Rc) -> Button { +pub(in crate::body) fn create(state: Rc, menu_button: &MenuButton) -> Button { let collection_button = Button::builder().label("Collection").build(); - collection_button.connect_clicked({ - move |_| { - if state.history.borrow().last().unwrap().0.body_type != BodyType::Collections { - Rc::new(collections(state.clone())).set_with_history(state.clone()); - } - state.menu_button.popdown(); - } + let menu_button = menu_button.clone(); + collection_button.connect_clicked(move |_| { + state.navigation_view.push_by_tag(COLLECTION); + menu_button.popdown(); }); collection_button } diff --git a/src/body/collection/mod.rs b/src/body/collection/mod.rs index 9f5bce2..b5a5a44 100644 --- a/src/body/collection/mod.rs +++ b/src/body/collection/mod.rs @@ -5,6 +5,7 @@ use std::time::Duration; use TryRecvError::{Disconnected, Empty}; use adw::gio::{Cancellable, File}; use adw::glib::{ControlFlow::*, timeout_add_local}; +use adw::NavigationPage; use adw::prelude::*; use async_std::task; use diesel::{delete, ExpressionMethods, insert_or_ignore_into, QueryDsl, RunQueryDsl}; @@ -14,6 +15,7 @@ use gtk::{Button, FileDialog, Label, ProgressBar}; use gtk::Orientation::{Horizontal, Vertical}; use log::error; use crate::body::collection::model::Collection; +use crate::body::{action_name, RERENDER}; use crate::common::{gtk_box, StyledLabelBuilder, StyledWidget}; use crate::common::constant::DESTRUCTIVE_ACTION; use crate::common::state::State; @@ -26,7 +28,7 @@ use crate::song::{import_songs, ImportProgress}; pub mod model; pub mod button; -pub mod body; +pub mod page; fn handle_progress>) + 'static>(collections_box: >k::Box, on_collection_end: F) -> Sender { @@ -87,11 +89,12 @@ fn add(collections_box: >k::Box, collection: Arc>, state: R remove_button.connect_clicked({ let collections_box = collections_box.clone(); move |_| { - get_connection().transaction(|connection| { - delete(collections.find(id)).execute(connection)?; - delete(bodies).execute(connection) - }).unwrap(); - state.history.borrow_mut().clear(); + delete(collections.find(id)).execute(&mut get_connection()).unwrap(); + let pages = state.navigation_view.navigation_stack(); + for navigation_page in pages.iter::().take((pages.n_items() - 1) as usize) { + // rerender all except last page which is Collection page + navigation_page.unwrap().activate_action(&action_name(RERENDER), None).unwrap(); + } collections_box.remove(&collection_box); } }); @@ -117,7 +120,6 @@ fn add_collection_box(state: Rc) -> gtk::Box { Ok(files) => { if let Some(files) = files { delete(bodies).execute(&mut get_connection()).unwrap(); - state.history.borrow_mut().clear(); let paths = files.into_iter() .map(|file| { file.unwrap().downcast::().unwrap().path().unwrap() }) .collect::>(); diff --git a/src/body/collection/page.rs b/src/body/collection/page.rs new file mode 100644 index 0000000..a9f3169 --- /dev/null +++ b/src/body/collection/page.rs @@ -0,0 +1,18 @@ +use std::rc::Rc; +use adw::{HeaderBar, NavigationPage, WindowTitle}; +use adw::prelude::*; +use gtk::Orientation::Vertical; +use gtk::ScrolledWindow; +use crate::body::{BodyType, create_navigation_page}; +use crate::body::collection::add_collection_box; +use crate::common::state::State; + +pub const COLLECTION: &'static str = "Collection"; + +pub fn collection_page(state: Rc) -> NavigationPage { + let child = gtk::Box::builder().orientation(Vertical).build(); + child.append(&HeaderBar::builder().title_widget(&WindowTitle::builder().title("Harborz").subtitle("Collection") + .build()).build()); + child.append(&ScrolledWindow::builder().vexpand(true).child(&add_collection_box(state)).build()); + create_navigation_page(&child, COLLECTION, Vec::new(), BodyType::Collections) +} diff --git a/src/body/download/albums.rs b/src/body/download/albums.rs index d062220..5a970bc 100644 --- a/src/body/download/albums.rs +++ b/src/body/download/albums.rs @@ -1,7 +1,7 @@ -use std::cell::Cell; use std::path::Path; use std::rc::Rc; use std::sync::Arc; +use adw::NavigationPage; use adw::prelude::*; use bytes::Bytes; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, update}; @@ -11,10 +11,10 @@ use gtk::Align::Center; use gtk::Orientation::Vertical; use id3::TagLike; use metadata_fetch::{ArtistSearch, DownloadArtistEvent::*, MetadataFetcher}; -use crate::body::{ALBUM, Body, BodyType, next_icon, popover_box, SONG}; -use crate::body::download::{append_download_button, METAL_ARCHIVES, save}; -use crate::body::download::songs::songs_body; -use crate::body::merge::{KEY, MergeState}; +use crate::body::{ALBUM, Body, BodyType, handle_render, next_icon, SONG}; +use crate::body::download::{append_download_button, handle_scroll, METAL_ARCHIVES, save}; +use crate::body::download::songs::songs_page; +use crate::body::merge::{KEY, add_menu_merge_button, MergeState}; use crate::common::{FOLDER_MUSIC_ICON, ImagePathBuf, StyledLabelBuilder}; use crate::common::constant::INSENSITIVE_FG; use crate::common::state::State; @@ -36,113 +36,112 @@ fn image_box(gtk_box: >k::Box) -> gtk::Box { gtk_box.first_child().and_downcast::().unwrap() } -pub fn albums(mut params: Vec>>, state: Rc) -> Body { - let statement = songs.inner_join(collections).group_by(album).order_by(min(year).desc()) - .select((album, count_star(), min(path), min(song_path), min(year), max(year))).into_boxed(); - let artist_string = params.pop().unwrap(); - let logo_or_photo = params.pop().unwrap(); - let albums = if let Some(artist_string) = &artist_string { - statement.filter(artist.eq(artist_string.as_ref())) - } else { - statement.filter(artist.is_null()) - }.get_results::<(Option, i64, Option, Option, Option, Option)>(&mut get_connection()) - .unwrap(); +pub fn albums_page(params: Vec>>, state: Rc, scroll_adjustment: Option) + -> NavigationPage { + let (artist_string, logo_or_photo) = { + let mut params = params.clone(); + (params.pop().unwrap(), params.pop().unwrap()) + }; let title = or_none_arc(artist_string.clone()); - let subtitle = Rc::new(albums.len().number_plural(ALBUM)); - let albums_box = gtk::Box::builder().orientation(Vertical).build(); - let merge_state = MergeState::new(ALBUM, state.clone(), title.clone(), subtitle.clone(), albums_box.clone(), - |albums| { Box::new(album.eq_any(albums)) }, || { Box::new(album.is_null()) }, - |tag, album_string| { tag.set_album(album_string); }, |song, album_string| { - update(songs.filter(id.eq(song.id))).set(album.eq(Some(album_string))).execute(&mut get_connection()) - .unwrap(); - }, - ); - Body { - back_visible: true, - title, - subtitle, - popover_box: { - let gtk_box = popover_box(state.clone(), merge_state.clone()); - let logo_or_photo = logo_or_photo.clone().unwrap(); - if let Some(artist_string) = artist_string.clone() { - append_download_button("logo & photo", >k_box, state.clone(), { - let artist_string = artist_string.clone(); - move |sender| { METAL_ARCHIVES.download_artist_logo_and_photo(&artist_string, sender); } - }, 2, move |search_result, handle_search_result, handle_bytes| { - match search_result { - SearchResult(search_result) => { handle_search_result(search_result); } - Logo(i, logo) => { - handle_bytes(i, logo, Box::new(|gtk_box, image| { image_box(gtk_box).prepend(image); }), 0); - } - Photo(i, photo) => { - handle_bytes(i, photo, Box::new(|gtk_box, image| { image_box(gtk_box).append(image); }), 1); - } - } - }, |ArtistSearch { name, genre, location }| { - let gtk_box = gtk::Box::builder().orientation(Vertical).spacing(4).hexpand(true).margin_start(4) - .build(); - let image_box = gtk::Box::builder().spacing(4).halign(Center).build(); - gtk_box.append(&image_box); - gtk_box.append(&Label::builder().label(&name).bold().wrap(true).build()); - gtk_box.append(&Label::builder().label(&genre).wrap(true).build()); - gtk_box.append(&Label::builder().label(&location).wrap(true).subscript().name(INSENSITIVE_FG) - .build()); - gtk_box - }, move |images_vec, i| { - save_option(logo_or_photo.sibling_logo(), images_vec[0].borrow_mut().remove(i)); - save_option(logo_or_photo.sibling_photo(), images_vec[1].borrow_mut().remove(i)); - }); + let body = Body::new(&*title, state.clone(), None, params, BodyType::Albums); + let heading = add_menu_merge_button(ALBUM, &body.menu_button, &body.popover_box); + if let Some(artist_string) = artist_string.clone() { + append_download_button("logo & photo", &body.popover_box, { + let artist_string = artist_string.clone(); + move |sender| { METAL_ARCHIVES.download_artist_logo_and_photo(&artist_string, sender); } + }, 2, move |search_result, handle_search_result, handle_bytes| { + match search_result { + SearchResult(search_result) => { handle_search_result(search_result); } + Logo(i, logo) => { + handle_bytes(i, logo, Box::new(|gtk_box, image| { image_box(gtk_box).prepend(image); }), 0); + } + Photo(i, photo) => { + handle_bytes(i, photo, Box::new(|gtk_box, image| { image_box(gtk_box).append(image); }), 1); + } } + }, |ArtistSearch { name, genre, location }| { + let gtk_box = gtk::Box::builder().orientation(Vertical).spacing(4).hexpand(true).margin_start(4) + .build(); + let image_box = gtk::Box::builder().spacing(4).halign(Center).build(); + gtk_box.append(&image_box); + gtk_box.append(&Label::builder().label(&name).bold().wrap(true).build()); + gtk_box.append(&Label::builder().label(&genre).wrap(true).build()); + gtk_box.append(&Label::builder().label(&location).wrap(true).subscript().name(INSENSITIVE_FG) + .build()); gtk_box - }, - body_type: BodyType::Albums, - params: vec![logo_or_photo, artist_string.clone()], - scroll_adjustment: Cell::new(None), - widget: Box::new({ - for (album_string, count, collection_path, album_song_path, min_year, max_year) in albums { - let album_string = album_string.map(Arc::new); - let album_row = gtk::Box::builder().spacing(8).build(); - if let Some(album_string) = album_string.clone() { - unsafe { album_row.set_data(KEY, album_string); } - } - albums_box.append(&album_row); - albums_box.append(&Separator::builder().build()); - let cover = join_path(&collection_path.unwrap(), &album_song_path.unwrap()).cover(); - album_row.append(Image::builder().pixel_size(46).margin_start(8).build() - .set_or_default(&cover, FOLDER_MUSIC_ICON)); - merge_state.clone().handle_click(&album_row, { - let album_string = album_string.clone(); - let artist_string = artist_string.clone(); - let state = state.clone(); - move || { - Rc::new(songs_body( - vec![cover.to_str().map(|it| { Arc::new(String::from(it)) }), artist_string.clone(), - album_string.clone()], state.clone(), - )).set_with_history(state.clone()); - } - }); - let album_box = gtk::Box::builder().orientation(Vertical) - .margin_start(8).margin_end(4).margin_top(12).margin_bottom(12).build(); - album_row.append(&album_box); - album_box.append(&Label::builder().label(&*or_none_arc(album_string)).ellipsized().build()); - let year_builder = Label::builder().name(INSENSITIVE_FG).ellipsized().subscript(); - let count_box = gtk::Box::builder().spacing(4).name(INSENSITIVE_FG).build(); - count_box.append(&Label::builder().label(&count.to_string()).subscript().build()); - count_box.append(&Label::builder().label(count.plural(SONG)).subscript().build()); - let info_box = if let Some(min_year) = min_year { - year_builder.label(&if min_year == max_year.unwrap() { - min_year.to_string() - } else { - format!("{min_year} to {}", max_year.unwrap()) - }) - } else { - year_builder - }.build(); - album_box.append(&info_box); - album_row.append(&count_box); - album_row.append(&next_icon()); + }, { + let logo_or_photo = logo_or_photo.clone().unwrap(); + move |images_vec, i| { + save_option(logo_or_photo.sibling_logo(), images_vec[0].borrow_mut().remove(i)); + save_option(logo_or_photo.sibling_photo(), images_vec[1].borrow_mut().remove(i)); } - merge_state.handle_pinch() - }), + }, body.menu_button.clone()); } + let adjustment = body.scrolled_window.vadjustment(); + let render = move || { + let statement = songs.inner_join(collections).group_by(album).order_by(min(year).desc()) + .select((album, count_star(), min(path), min(song_path), min(year), max(year))).into_boxed(); + let albums = if let Some(artist_string) = &artist_string { + statement.filter(artist.eq(artist_string.as_ref())) + } else { + statement.filter(artist.is_null()) + }.get_results::<(Option, i64, Option, Option, Option, Option)>(&mut get_connection()) + .unwrap(); + let subtitle = albums.len().number_plural(ALBUM); + body.window_title.set_subtitle(&subtitle); + let albums_box = gtk::Box::builder().orientation(Vertical).build(); + let merge_state = MergeState::new(ALBUM, heading.clone(), title.clone(), Rc::new(subtitle), + albums_box.clone(), &body.action_group, &body.header_bar, &body.menu_button, + |albums| { Box::new(album.eq_any(albums)) }, || { Box::new(album.is_null()) }, + |tag, album_string| { tag.set_album(album_string); }, |song, album_string| { + update(songs.filter(id.eq(song.id))).set(album.eq(Some(album_string))).execute(&mut get_connection()) + .unwrap(); + }, + ); + for (album_string, count, collection_path, album_song_path, min_year, max_year) in albums { + let album_string = album_string.map(Arc::new); + let album_row = gtk::Box::builder().spacing(8).build(); + if let Some(album_string) = album_string.clone() { + unsafe { album_row.set_data(KEY, album_string); } + } + albums_box.append(&album_row); + albums_box.append(&Separator::builder().build()); + let cover = join_path(&collection_path.unwrap(), &album_song_path.unwrap()).cover(); + album_row.append(Image::builder().pixel_size(46).margin_start(8).build() + .set_or_default(&cover, FOLDER_MUSIC_ICON)); + merge_state.clone().handle_click(&album_row, { + let album_string = album_string.clone(); + let artist_string = artist_string.clone(); + let state = state.clone(); + move || { + state.navigation_view.push(&songs_page(vec![cover.to_str().map(|it| { Arc::new(it.to_owned()) }), + artist_string.clone(), album_string.clone()], state.clone(), None)); + } + }); + let album_box = gtk::Box::builder().orientation(Vertical) + .margin_start(8).margin_end(4).margin_top(12).margin_bottom(12).build(); + album_row.append(&album_box); + album_box.append(&Label::builder().label(&*or_none_arc(album_string)).ellipsized().build()); + let year_builder = Label::builder().name(INSENSITIVE_FG).ellipsized().subscript(); + let count_box = gtk::Box::builder().spacing(4).name(INSENSITIVE_FG).build(); + count_box.append(&Label::builder().label(&count.to_string()).subscript().build()); + count_box.append(&Label::builder().label(count.plural(SONG)).subscript().build()); + let info_box = if let Some(min_year) = min_year { + year_builder.label(&if min_year == max_year.unwrap() { + min_year.to_string() + } else { + format!("{min_year} to {}", max_year.unwrap()) + }) + } else { + year_builder + }.build(); + album_box.append(&info_box); + album_row.append(&count_box); + album_row.append(&next_icon()); + } + body.scrolled_window.set_child(Some(&merge_state.clone().handle_pinch())); + }; + handle_scroll(scroll_adjustment, adjustment); + handle_render(render, body.rerender); + body.navigation_page } diff --git a/src/body/download/mod.rs b/src/body/download/mod.rs index 341a9e0..6ea3475 100644 --- a/src/body/download/mod.rs +++ b/src/body/download/mod.rs @@ -7,21 +7,20 @@ use std::sync::mpsc::{channel, Sender}; use std::sync::mpsc::TryRecvError::{Disconnected, Empty}; use std::time::Duration; use adw::gdk::gdk_pixbuf::Pixbuf; -use adw::glib::{timeout_add_local, Variant}; +use adw::glib::{timeout_add_local, timeout_add_local_once, Variant}; use adw::glib::ControlFlow::{Break, Continue}; use adw::prelude::*; use adw::Window; use bytes::{Buf, Bytes}; -use gtk::{Button, Image, Overlay}; +use gtk::{Adjustment, Button, Image, MenuButton, Overlay}; use log::{error, warn}; use once_cell::sync::Lazy; use metal_archives::MetalArchives; use crate::common::check_button_dialog::check_button_dialog; use crate::common::constant::SUGGESTED_ACTION; -use crate::common::state::State; -pub(super) mod albums; -pub(super) mod songs; +pub mod albums; +pub mod songs; static METAL_ARCHIVES: Lazy = Lazy::new(|| { MetalArchives::new() }); @@ -29,8 +28,8 @@ fn append_download_button) + 'static, S, HD: Fn(DR, Box>>)>, Box, Box, usize)>) + Clone + 'static, HS: Fn(S) -> gtk::Box + Clone + 'static, CO: Fn(&Vec>>>>, usize) + Clone + 'static ->(download_label: &'static str, gtk_box: >k::Box, state: Rc, download: D, image_vec_count: usize, - handle_download: HD, handle_search: HS, choose_option: CO) { +>(download_label: &'static str, gtk_box: >k::Box, download: D, image_vec_count: usize, handle_download: HD, + handle_search: HS, choose_option: CO, menu_button: MenuButton) { let download_button = Button::builder().label(format!("Download {download_label}")).build(); gtk_box.append(&download_button); download_button.connect_clicked(move |_| { @@ -125,7 +124,7 @@ fn append_download_button) + 'static, S, } } }); - state.menu_button.popdown(); + menu_button.popdown(); }); } @@ -139,3 +138,9 @@ fn save(path: impl AsRef, vec: Bytes) { Err(error) => { error!("error creating image [{error}]"); } } } + +fn handle_scroll(scroll_adjustment: Option, adjustment: Adjustment) { + if let Some(scroll_adjustment) = scroll_adjustment { + timeout_add_local_once(Duration::from_millis(150), move || { adjustment.set_value(scroll_adjustment); }); + } +} diff --git a/src/body/download/songs.rs b/src/body/download/songs.rs index c47c1e2..8ce3c63 100644 --- a/src/body/download/songs.rs +++ b/src/body/download/songs.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::fs::hard_link; use std::rc::Rc; use std::sync::Arc; +use adw::NavigationPage; use adw::gdk::pango::{AttrInt, AttrList}; use adw::gdk::pango::AttrType::Weight; use adw::gdk::pango::Weight::Bold; @@ -14,8 +15,8 @@ use gtk::Orientation::Vertical; use log::{error, warn}; use metadata_fetch::{AlbumSearch, MetadataFetcher}; use metadata_fetch::DownloadAlbumEvent::{Cover, SearchResult}; -use crate::body::{Body, BodyType, collection, SONG}; -use crate::body::download::{append_download_button, METAL_ARCHIVES, save}; +use crate::body::{action_name, Body, BodyType, handle_render, POP_DOWN, SONG}; +use crate::body::download::{append_download_button, handle_scroll, METAL_ARCHIVES, save}; use crate::common::constant::INSENSITIVE_FG; use crate::common::state::State; use crate::common::StyledLabelBuilder; @@ -25,143 +26,133 @@ use crate::db::get_connection; use crate::schema::config::dsl::config; use crate::song::{get_current_album, join_path}; -pub fn songs_body(mut params: Vec>>, state: Rc) -> Body { - let album_string = params.pop().unwrap(); - let artist_string = params.pop().unwrap(); - let cover = params.pop().unwrap(); - let current_album = get_current_album(artist_string.clone(), album_string.clone(), &mut get_connection()); - Body { - back_visible: true, - popover_box: { - let gtk_box = gtk::Box::builder().orientation(Vertical).build(); - gtk_box.append(&collection::button::create(state.clone())); - let select_cover = Button::builder().label("Choose cover") - .tooltip_text("Choose album cover from local files").build(); - gtk_box.append(&select_cover); - let cover = cover.clone().unwrap(); - select_cover.connect_clicked({ - let state = state.clone(); - let cover = cover.clone(); - move |_| { - let file_filter = FileFilter::new(); - file_filter.add_pixbuf_formats(); - let list_store = ListStore::new::(); - list_store.append(&file_filter); - FileDialog::builder().title("Album cover").accept_label("Choose").filters(&list_store).build() - .open(Some(&state.window), Cancellable::NONE, { - let cover = cover.clone(); - move |file| { - match file { - Ok(file) => { - if let Err(error) = hard_link(file.path().unwrap(), cover.as_ref()) { - error!("error creating hard link from [{file}] to [{cover}] [{error}]"); - } - } - Err(error) => { warn!("error choosing file [{error}]"); } +pub fn songs_page(params: Vec>>, state: Rc, scroll_adjustment: Option) + -> NavigationPage { + let (album_string, artist_string, cover) = { + let mut params = params.clone(); + (params.pop().unwrap(), params.pop().unwrap(), params.pop().unwrap()) + }; + let body = Body::new(&*or_none_arc(album_string.clone()), state.clone(), None, params, BodyType::Songs); + let cover = cover.clone().unwrap(); + let select_cover = Button::builder().label("Choose cover").tooltip_text("Choose album cover from local files") + .build(); + body.popover_box.append(&select_cover); + select_cover.connect_clicked({ + let state = state.clone(); + let cover = cover.clone(); + move |select_cover| { + let file_filter = FileFilter::new(); + file_filter.add_pixbuf_formats(); + let list_store = ListStore::new::(); + list_store.append(&file_filter); + FileDialog::builder().title("Album cover").accept_label("Choose").filters(&list_store).build() + .open(Some(&state.window), Cancellable::NONE, { + let cover = cover.clone(); + move |file| { + match file { + Ok(file) => { + if let Err(error) = hard_link(file.path().unwrap(), cover.as_ref()) { + error!("error creating hard link from [{file}] to [{cover}] [{error}]"); } } - }); - state.menu_button.popdown(); - } - }); - if let Some(artist_string) = artist_string.clone() { - if let Some(album_string) = album_string.clone() { - append_download_button("cover", >k_box, state.clone(), { - let artist_string = artist_string.clone(); - let album_string = album_string.clone(); - move |sender| { METAL_ARCHIVES.download_cover(&artist_string, &album_string, sender); } - }, 1, move |search_result, handle_search_result, handle_bytes| { - match search_result { - SearchResult(search_result) => { handle_search_result(search_result); } - Cover(i, cover) => { - handle_bytes(i, cover, Box::new(|gtk_box, image| { gtk_box.prepend(image); }), 0); - } + Err(error) => { warn!("error choosing file [{error}]"); } } - }, |AlbumSearch { artist, album, album_type }| { - let gtk_box = gtk::Box::builder().orientation(Vertical).spacing(4).hexpand(true).margin_start(4) - .build(); - gtk_box.append(&Label::builder().label(&album).bold().wrap(true).build()); - gtk_box.append(&Label::builder().label(&album_type).wrap(true).build()); - gtk_box.append(&Label::builder().label(&artist).wrap(true).subscript().name(INSENSITIVE_FG) - .build()); - gtk_box - }, move |images_vec, i| { - save(&*cover, images_vec[0].borrow_mut().remove(i).unwrap()); - }); + } + }); + select_cover.activate_action(&action_name(POP_DOWN), None).unwrap(); + } + }); + if let Some(artist_string) = artist_string.clone() { + if let Some(album_string) = album_string.clone() { + append_download_button("cover", &body.popover_box, { + let artist_string = artist_string.clone(); + let album_string = album_string.clone(); + move |sender| { METAL_ARCHIVES.download_cover(&artist_string, &album_string, sender); } + }, 1, move |search_result, handle_search_result, handle_bytes| { + match search_result { + SearchResult(search_result) => { handle_search_result(search_result); } + Cover(i, cover) => { + handle_bytes(i, cover, Box::new(|gtk_box, image| { gtk_box.prepend(image); }), 0); + } } + }, |AlbumSearch { artist, album, album_type }| { + let gtk_box = gtk::Box::builder().orientation(Vertical).spacing(4).hexpand(true).margin_start(4) + .build(); + gtk_box.append(&Label::builder().label(&album).bold().wrap(true).build()); + gtk_box.append(&Label::builder().label(&album_type).wrap(true).build()); + gtk_box.append(&Label::builder().label(&artist).wrap(true).subscript().name(INSENSITIVE_FG) + .build()); + gtk_box + }, move |images_vec, i| { + save(&*cover, images_vec[0].borrow_mut().remove(i).unwrap()); + }, body.menu_button.clone()); + } + } + let adjustment = body.scrolled_window.vadjustment(); + let render = move || { + let current_album = get_current_album(&artist_string, &album_string, &mut get_connection()); + body.window_title.set_subtitle(¤t_album.len().number_plural(SONG)); + let Config { current_song_id, .. } = config.get_result::(&mut get_connection()).unwrap(); + let current_song_id = Cell::new(current_song_id); + let grid = Grid::new(); + let song_id_to_labels = current_album.iter().enumerate().map(|(row, (song, collection))| { + let grid_row = (2 * row) as i32; + let separator_row = grid_row + 1; + let track_number_builder = Label::builder().margin_start(8).margin_end(8); + let track_number_label = if let Some(track_number) = song.track_number { + track_number_builder.label(&track_number.to_string()) + } else { + track_number_builder + }.build(); + grid.attach(&track_number_label, 0, grid_row, 1, 1); + grid.attach(&Separator::builder().build(), 0, separator_row, 1, 1); + let title_label = Label::builder().label(song.title_str()).ellipsized() + .margin_start(8).margin_end(8).margin_top(12).margin_bottom(12).build(); + grid.attach(&title_label, 1, grid_row, 1, 1); + grid.attach(&Separator::builder().build(), 1, separator_row, 1, 1); + let duration_label = Label::builder().label(&format(song.duration as u64)).subscript() + .margin_start(8).margin_end(8).build(); + grid.attach(&duration_label, 2, grid_row, 1, 1); + grid.attach(&Separator::builder().build(), 2, separator_row, 1, 1); + let labels = vec![track_number_label, title_label, duration_label]; + let path = Rc::new(join_path(&collection.path, &song.path)); + for label in &labels { + let gesture_click = GestureClick::new(); + gesture_click.connect_released({ + let state = state.clone(); + let path = path.clone(); + move |_, _, _, _| { state.window_actions.song_selected.activate(path.to_str().unwrap()); } + }); + label.add_controller(gesture_click); } - gtk_box - }, - body_type: BodyType::Songs, - params: vec![cover, artist_string, album_string.clone()], - title: or_none_arc(album_string), - subtitle: Rc::new(current_album.len().number_plural(SONG)), - scroll_adjustment: Cell::new(None), - widget: Box::new({ - let Config { current_song_id, .. } = config.get_result::(&mut get_connection()).unwrap(); - let current_song_id = Cell::new(current_song_id); - let grid = Grid::new(); - let song_id_to_labels = current_album.into_iter().enumerate().map(|(row, (song, collection))| { - let grid_row = (2 * row) as i32; - let separator_row = grid_row + 1; - let track_number_builder = Label::builder().margin_start(8).margin_end(8); - let track_number_label = if let Some(track_number) = song.track_number { - track_number_builder.label(&track_number.to_string()) - } else { - track_number_builder - }.build(); - grid.attach(&track_number_label, 0, grid_row, 1, 1); - grid.attach(&Separator::builder().build(), 0, separator_row, 1, 1); - let title_label = Label::builder().label(song.title_str()).ellipsized() - .margin_start(8).margin_end(8).margin_top(12).margin_bottom(12).build(); - grid.attach(&title_label, 1, grid_row, 1, 1); - grid.attach(&Separator::builder().build(), 1, separator_row, 1, 1); - let duration_label = Label::builder().label(&format(song.duration as u64)).subscript() - .margin_start(8).margin_end(8).build(); - grid.attach(&duration_label, 2, grid_row, 1, 1); - grid.attach(&Separator::builder().build(), 2, separator_row, 1, 1); - let collection_path_rc = Rc::new(collection.path); - let song_path_rc = Rc::new(song.path); - let labels = vec![track_number_label, title_label, duration_label]; - for label in &labels { - let gesture_click = GestureClick::new(); - gesture_click.connect_released({ - let state = state.clone(); - let collection_path_rc = collection_path_rc.clone(); - let song_path_rc = song_path_rc.clone(); - move |_, _, _, _| { - state.window_actions.song_selected.activate(join_path(&collection_path_rc, &song_path_rc) - .to_str().unwrap()); - } - }); - label.add_controller(gesture_click); + (song.id, labels) + }).collect::>(); + state.window_actions.stream_started.action.connect_activate(move |_, params| { + let started_song_id = params.unwrap().get::().unwrap(); + if let Some(labels) = song_id_to_labels.get(&started_song_id) { + for label in labels { + label.add_css_class("accent"); + let attr_list = label.attributes().unwrap_or_else(AttrList::new); + attr_list.insert(AttrInt::new_weight(Bold)); + label.set_attributes(Some(&attr_list)); } - (song.id, labels) - }).collect::>(); - state.window_actions.stream_started.action.connect_activate(move |_, params| { - let started_song_id = params.unwrap().get::().unwrap(); - if let Some(labels) = song_id_to_labels.get(&started_song_id) { - for label in labels { - label.add_css_class("accent"); - let attr_list = label.attributes().unwrap_or_else(AttrList::new); - attr_list.insert(AttrInt::new_weight(Bold)); - label.set_attributes(Some(&attr_list)); - } - if let Some(current_id) = current_song_id.get() { - if current_id != started_song_id { - current_song_id.set(Some(started_song_id)); - if let Some(labels) = song_id_to_labels.get(¤t_id) { - for label in labels { - label.remove_css_class("accent"); - label.set_attributes(label.attributes().unwrap_or_else(AttrList::new) - .filter(|it| { !(it.type_() == Weight) }).as_ref()); - } + if let Some(current_id) = current_song_id.get() { + if current_id != started_song_id { + current_song_id.set(Some(started_song_id)); + if let Some(labels) = song_id_to_labels.get(¤t_id) { + for label in labels { + label.remove_css_class("accent"); + label.set_attributes(label.attributes().unwrap_or_else(AttrList::new) + .filter(|it| { !(it.type_() == Weight) }).as_ref()); } } } } - }); - grid - }), - } + } + }); + body.scrolled_window.set_child(Some(&grid)); + }; + handle_scroll(scroll_adjustment, adjustment); + handle_render(render, body.rerender); + body.navigation_page } diff --git a/src/body/merge/impl.rs b/src/body/merge/impl.rs index 0c59089..5f30fdc 100644 --- a/src/body/merge/impl.rs +++ b/src/body/merge/impl.rs @@ -5,11 +5,12 @@ use std::sync::Arc; use std::sync::mpsc::{channel, TryRecvError::*}; use std::thread; use std::time::Duration; +use adw::{HeaderBar, Window}; +use adw::gio::{SimpleAction, SimpleActionGroup}; use adw::glib::{ControlFlow::*, timeout_add_local, Variant}; use adw::prelude::*; -use adw::Window; use diesel::{BoolExpressionMethods, QueryDsl, RunQueryDsl}; -use gtk::{Button, CheckButton, GestureClick, GestureZoom, Label, Overlay, ProgressBar}; +use gtk::{Button, CheckButton, GestureClick, GestureZoom, Label, MenuButton, Overlay, ProgressBar}; use gtk::EventSequenceState::Claimed; use gtk::PropagationPhase::Capture; use id3::ErrorKind::NoTag; @@ -19,10 +20,9 @@ use id3::Version::Id3v24; use log::error; use crate::body::collection::model::Collection; use crate::body::merge::{KEY, MergeButton, MergeState, Query}; -use crate::body::next_icon; +use crate::body::{action_name, CHANGE_SUBTITLE, CHANGE_TITLE, HEADER_BAR_START_MERGE, next_icon, START_MERGE}; use crate::common::check_button_dialog::check_button_dialog; use crate::common::constant::DESTRUCTIVE_ACTION; -use crate::common::state::State; use crate::common::StyledWidget; use crate::common::util::Plural; use crate::db::get_connection; @@ -31,29 +31,27 @@ use crate::schema::songs::dsl::songs; #[allow(unused_imports)] use crate::song::{Song, WithPath}; +const END_MERGE: &'static str = "end_merge"; + impl MergeState { pub(in crate::body) fn new< I: Fn(Vec>) -> Query + Send + Clone + 'static, N: Fn() -> Query + Send + Clone + 'static, T: Fn(&mut Tag, &str) + Send + Clone + 'static, M: Fn(Song, &str) + Send + Clone + 'static - >(string: &'static str, state: Rc, title: Arc, subtitle: Rc, entities_box: gtk::Box, - get_in_filter: I, is_null: N, set_tag: T, merge: M) -> Rc { + >(string: &'static str, heading: Rc, title: Arc, subtitle: Rc, entities_box: gtk::Box, + action_group: &SimpleActionGroup, header_bar: &HeaderBar, menu_button: &MenuButton, get_in_filter: I, + is_null: N, set_tag: T, merge: M) -> Rc { let cancel_button = Button::builder().label("Cancel").build(); let merge_button = Button::builder().label("Merge").build().suggested_action(); merge_button.disable(); - let heading = format!("Merge {string}s"); let description = format!("Choose the correct {string} name"); - let merge_menu_button = Button::builder().label(&heading).build(); let this = Rc::new(MergeState { entity: string, - state: state.clone(), title, subtitle, entities_box, merging: Cell::new(false), selected_for_merge: RefCell::new(HashSet::new()), - cancel_button: cancel_button.clone(), merge_button: merge_button.clone(), - merge_menu_button: merge_menu_button.clone(), }); cancel_button.connect_clicked({ let this = this.clone(); @@ -146,11 +144,38 @@ impl MergeState { ); } }); - merge_menu_button.connect_clicked({ + let start_merge = SimpleAction::new(START_MERGE, None); + action_group.remove_action(START_MERGE); + action_group.add_action(&start_merge); + start_merge.connect_activate({ let this = this.clone(); - move |_| { - this.start_merge(); - this.state.menu_button.popdown(); + move |_, _| { this.start_merge(); } + }); + let header_bar_start_merge = SimpleAction::new(HEADER_BAR_START_MERGE, None); + action_group.remove_action(HEADER_BAR_START_MERGE); + action_group.add_action(&header_bar_start_merge); + header_bar_start_merge.connect_activate({ + let header_bar = header_bar.clone(); + let cancel_button = cancel_button.clone(); + let menu_button = menu_button.clone(); + let merge_button = merge_button.clone(); + move |_, _| { + header_bar.set_show_back_button(false); + header_bar.pack_start(&cancel_button); + header_bar.remove(&menu_button); + header_bar.pack_end(&merge_button); + } + }); + let end_merge = SimpleAction::new(END_MERGE, None); + action_group.add_action(&end_merge); + end_merge.connect_activate({ + let header_bar = header_bar.clone(); + let menu_button = menu_button.clone(); + move |_, _| { + header_bar.remove(&cancel_button); + header_bar.set_show_back_button(true); + header_bar.remove(&merge_button); + header_bar.pack_end(&menu_button); } }); this @@ -166,11 +191,9 @@ impl MergeState { fn start_merge(&self) { if !self.merging.get() { self.merging.set(true); - self.state.header_bar.remove(&self.state.back_button); - self.state.header_bar.pack_start(&self.cancel_button); - self.state.header_bar.remove(&self.state.menu_button); - self.state.header_bar.pack_end(&self.merge_button); - self.state.window_actions.change_window_title.activate(format!("Merging {}s", self.entity)); + self.entities_box.activate_action(&action_name(HEADER_BAR_START_MERGE), None).unwrap(); + self.entities_box.activate_action(&action_name(CHANGE_TITLE), + Some(&format!("Merging {}s", self.entity).to_variant())).unwrap(); self.update_selected_count(); self.iterate_rows(|row| { row.remove(&row.last_child().unwrap()); @@ -180,12 +203,9 @@ impl MergeState { } } fn end_merge(&self) { - self.state.header_bar.remove(&self.cancel_button); - self.state.header_bar.pack_start(&self.state.back_button); - self.state.header_bar.remove(&self.merge_button); - self.state.header_bar.pack_end(&self.state.menu_button); - self.state.window_actions.change_window_title.activate(&*self.title); - self.state.window_actions.change_window_subtitle.activate(&*self.subtitle); + self.entities_box.activate_action(&action_name(END_MERGE), None).unwrap(); + self.entities_box.activate_action(&action_name(CHANGE_TITLE), Some(&self.title.to_variant())).unwrap(); + self.entities_box.activate_action(&action_name(CHANGE_SUBTITLE), Some(&self.subtitle.to_variant())).unwrap(); self.selected_for_merge.borrow_mut().clear(); self.merging.set(false); self.iterate_rows(|row| { @@ -196,8 +216,8 @@ impl MergeState { } fn update_selected_count(&self) { let count = self.selected_for_merge.borrow().len(); - self.state.window_actions.change_window_subtitle - .activate(format!("{} selected", count.number_plural(&self.entity))); + self.entities_box.activate_action(&action_name(CHANGE_SUBTITLE), + Some(&format!("{} selected", count.number_plural(&self.entity)).to_variant())).unwrap(); if count > 1 { self.merge_button.set_sensitive(true); self.merge_button.set_tooltip_text(None); diff --git a/src/body/merge/mod.rs b/src/body/merge/mod.rs index c8fcd94..bb51b9c 100644 --- a/src/body/merge/mod.rs +++ b/src/body/merge/mod.rs @@ -1,5 +1,3 @@ -mod r#impl; - use std::cell::{Cell, RefCell}; use std::collections::HashSet; use std::rc::Rc; @@ -9,22 +7,21 @@ use diesel::BoxableExpression; use diesel::dsl::InnerJoinQuerySource; use diesel::sql_types::Bool; use diesel::sqlite::Sqlite; -use gtk::Button; -use crate::common::state::State; +use gtk::{Button, MenuButton}; +use crate::body::{action_name, START_MERGE}; use crate::schema::collections::dsl::collections; use crate::schema::songs::dsl::songs; +mod r#impl; + pub(super) struct MergeState { entity: &'static str, - state: Rc, title: Arc, subtitle: Rc, merging: Cell, entities_box: gtk::Box, selected_for_merge: RefCell>, - cancel_button: Button, merge_button: Button, - pub merge_menu_button: Button, } pub(super) const KEY: &'static str = "key"; @@ -41,3 +38,15 @@ impl MergeButton for Button { self.set_tooltip_text(Some("Select 2 or more")); } } + +pub fn add_menu_merge_button(entity: &str, menu_button: &MenuButton, popover_box: >k::Box) -> Rc { + let heading = format!("Merge {entity}s"); + let merge_menu_button = Button::builder().label(&heading).build(); + let menu_button = menu_button.clone(); + merge_menu_button.connect_clicked(move |merge_menu_button| { + merge_menu_button.activate_action(&action_name(START_MERGE), None).unwrap(); + menu_button.popdown(); + }); + popover_box.append(&merge_menu_button); + Rc::new(heading) +} diff --git a/src/body/mod.rs b/src/body/mod.rs index ffd7ea3..1049dc0 100644 --- a/src/body/mod.rs +++ b/src/body/mod.rs @@ -1,22 +1,16 @@ -use std::cell::Cell; -use std::ops::Deref; use std::rc::Rc; use std::sync::Arc; +use adw::{HeaderBar, NavigationPage, WindowTitle}; +use adw::gio::{SimpleAction, SimpleActionGroup}; use adw::prelude::*; -use gtk::{Image, Label, Widget}; +use gtk::{Image, Label, MenuButton, Popover, ScrolledWindow, Widget}; use gtk::Orientation::Vertical; -use crate::body::download::albums::albums; -use crate::body::artists::artists; -use crate::body::collection::body::collections; -use crate::body::merge::MergeState; -use crate::body::download::songs::songs_body; -use crate::common::AdjustableScrolledWindow; use crate::common::state::State; pub mod collection; mod merge; pub mod artists; -mod download; +pub mod download; #[derive(diesel::Queryable, diesel::Selectable, Debug)] #[diesel(table_name = crate::schema::bodies)] @@ -25,11 +19,10 @@ pub struct BodyTable { pub id: i32, pub body_type: BodyType, pub scroll_adjustment: Option, - pub navigation_type: NavigationType, pub params: String, } -#[derive(Debug, PartialEq, diesel_derive_enum::DbEnum)] +#[derive(Debug, diesel_derive_enum::DbEnum)] pub enum BodyType { Artists, Albums, @@ -37,23 +30,6 @@ pub enum BodyType { Collections, } -#[derive(Debug, diesel_derive_enum::DbEnum)] -pub enum NavigationType { - History, - SongSelected, -} - -pub struct Body { - back_visible: bool, - title: Arc, - subtitle: Rc, - popover_box: gtk::Box, - pub body_type: BodyType, - pub params: Vec>>, - pub scroll_adjustment: Cell>, - pub widget: Box>, -} - fn next_icon() -> Image { Image::builder().icon_name("go-next-symbolic").margin_start(10).margin_end(8).build() } @@ -77,47 +53,94 @@ impl Castable for Option { const ARTIST: &'static str = "Artist"; const ALBUM: &'static str = "Album"; const SONG: &'static str = "Song"; +const RERENDER: &'static str = "rerender"; +pub const PARAMS: &'static str = "params"; +pub const BODY_TYPE: &'static str = "body_type"; -fn popover_box(state: Rc, merge_state: Rc) -> gtk::Box { - let gtk_box = gtk::Box::builder().orientation(Vertical).build(); - gtk_box.append(&collection::button::create(state.clone())); - gtk_box.append(&merge_state.merge_menu_button); - gtk_box +struct Body { + action_group: SimpleActionGroup, + rerender: SimpleAction, + menu_button: MenuButton, + window_title: WindowTitle, + header_bar: HeaderBar, + scrolled_window: ScrolledWindow, + popover_box: gtk::Box, + navigation_page: NavigationPage, } impl Body { - pub fn from_body_table(body_type: BodyType, params: Vec>, state: Rc) -> Self { - let params = params.into_iter().map(|it| { it.map(Arc::new) }).collect(); - match body_type { - BodyType::Artists => { artists(state) } - BodyType::Albums => { albums(params, state) } - BodyType::Songs => { songs_body(params, state) } - BodyType::Collections => { collections(state) } - } - } - pub fn put_to_history(self, scroll_adjustment: Option, state: Rc) { - self.scroll_adjustment.set(scroll_adjustment); - state.history.borrow_mut().push((Rc::new(self), true)); - } - pub fn set(self: Rc, state: Rc) { - state.back_button.set_visible(self.back_visible); - state.window_actions.change_window_title.activate(&*self.title); - state.window_actions.change_window_subtitle.activate(&*self.subtitle); - state.menu_button.set_visible(if self.popover_box.first_child() == None { - false - } else { - state.menu_button.popover().unwrap().set_child(Some(&self.popover_box)); - true + fn new(title: &str, state: Rc, tag: Option<&str>, params: Vec>>, body_type: BodyType) + -> Self { + let action_group = SimpleActionGroup::new(); + let rerender = SimpleAction::new(RERENDER, None); + action_group.add_action(&rerender); + let popover_box = gtk::Box::builder().orientation(Vertical).build(); + let menu_button = MenuButton::builder().icon_name("open-menu-symbolic").tooltip_text("Menu") + .popover(&Popover::builder().child(&popover_box).build()).build(); + popover_box.append(&collection::button::create(state, &menu_button)); + let child = gtk::Box::builder().orientation(Vertical).build(); + let window_title = WindowTitle::builder().title(title).build(); + let header_bar = HeaderBar::builder().title_widget(&window_title).build(); + child.append(&header_bar); + let scrolled_window = ScrolledWindow::builder().vexpand(true).build(); + child.append(&scrolled_window); + header_bar.pack_end(&menu_button); + let pop_down = SimpleAction::new(POP_DOWN, None); + action_group.add_action(&pop_down); + pop_down.connect_activate({ + let menu_button = menu_button.clone(); + move |_, _| { menu_button.popdown(); } }); - state.scrolled_window.set_child(Some((*self.widget).as_ref())); - } - pub fn set_with_history(self: Rc, state: Rc) { - self.clone().set(state.clone()); - let mut history = state.history.borrow_mut(); - if let Some((body, _)) = history.last() { - let Body { scroll_adjustment, .. } = body.deref(); - scroll_adjustment.set(state.scrolled_window.get_adjustment()); + let change_title = SimpleAction::new(CHANGE_TITLE, Some(&String::static_variant_type())); + action_group.add_action(&change_title); + change_title.connect_activate({ + let window_title = window_title.clone(); + move |_, entity| { window_title.set_title(entity.unwrap().str().unwrap()); } + }); + let change_subtitle = SimpleAction::new(CHANGE_SUBTITLE, Some(&String::static_variant_type())); + action_group.add_action(&change_subtitle); + change_subtitle.connect_activate({ + let window_title = window_title.clone(); + move |_, entity| { window_title.set_subtitle(entity.unwrap().str().unwrap()); } + }); + let navigation_page = create_navigation_page(&child, tag.unwrap_or(title), params, body_type); + navigation_page.insert_action_group(NAVIGATION_PAGE, Some(&action_group)); + Self { + action_group, + rerender, + menu_button, + window_title, + header_bar, + scrolled_window, + popover_box, + navigation_page, } - history.push((self, false)); } } + +const POP_DOWN: &'static str = "pop_down"; +const CHANGE_TITLE: &'static str = "change_title"; +const CHANGE_SUBTITLE: &'static str = "change_subtitle"; + +fn create_navigation_page(child: >k::Box, tag: &str, params: Vec>>, body_type: BodyType) + -> NavigationPage { + let navigation_page = NavigationPage::builder().child(child).title(tag).tag(tag).build(); + unsafe { + navigation_page.set_data(PARAMS, params); + navigation_page.set_data(BODY_TYPE, body_type); + } + navigation_page +} + +const NAVIGATION_PAGE: &'static str = "navigation_page"; +const START_MERGE: &'static str = "start_merge"; +const HEADER_BAR_START_MERGE: &'static str = "header_bar_start_merge"; + +fn action_name(name: &str) -> String { + format!("{NAVIGATION_PAGE}.{name}") +} + +fn handle_render(render: R, rerender: SimpleAction) { + render(); + rerender.connect_activate(move |_, _| { render(); }); +} diff --git a/src/common/constant.rs b/src/common/constant.rs index cad4307..bdd4332 100644 --- a/src/common/constant.rs +++ b/src/common/constant.rs @@ -1,5 +1,4 @@ pub const APP_ID: &str = "com.github.Harborz"; -pub const BACK_ICON: &str = "go-previous-symbolic"; pub const INSENSITIVE_FG: &str = "insensitive-fg"; pub const DESTRUCTIVE_ACTION: &str = "destructive-action"; pub const SUGGESTED_ACTION: &str = "suggested-action"; diff --git a/src/common/state.rs b/src/common/state.rs index 498d2c0..8e90cf2 100644 --- a/src/common/state.rs +++ b/src/common/state.rs @@ -1,17 +1,9 @@ -use std::cell::RefCell; -use std::rc::Rc; -use adw::{ApplicationWindow, HeaderBar}; -use gtk::{Button, MenuButton, ScrolledWindow}; -use crate::body::Body; +use adw::{ApplicationWindow, NavigationView}; use crate::common::window_action::WindowActions; pub struct State { pub window: ApplicationWindow, - pub header_body: gtk::Box, - pub header_bar: HeaderBar, - pub back_button: Button, + pub body: gtk::Box, pub window_actions: WindowActions, - pub menu_button: MenuButton, - pub scrolled_window: ScrolledWindow, - pub history: RefCell, bool)>>, + pub navigation_view: NavigationView, } diff --git a/src/common/window_action.rs b/src/common/window_action.rs index ffaf6c8..b248a1b 100644 --- a/src/common/window_action.rs +++ b/src/common/window_action.rs @@ -21,8 +21,6 @@ impl WindowAction { pub struct WindowActions { pub song_selected: WindowAction, pub stream_started: WindowAction, - pub change_window_title: WindowAction, - pub change_window_subtitle: WindowAction, } impl WindowActions { @@ -30,8 +28,6 @@ impl WindowActions { Self { song_selected: WindowAction::new::("song-selected", application_window), stream_started: WindowAction::new::("stream-started", application_window), - change_window_title: WindowAction::new::("change-window-title", application_window), - change_window_subtitle: WindowAction::new::("change-window-subtitle", application_window), } } } diff --git a/src/main.rs b/src/main.rs index d6ee994..a5950fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,33 @@ use std::cell::RefCell; use std::env::current_dir; -use std::ops::Deref; use std::rc::Rc; -use adw::{Application, ApplicationWindow, HeaderBar, WindowTitle}; +use std::sync::Arc; +use std::time::Duration; +use adw::{Application, ApplicationWindow, NavigationPage, NavigationView}; use adw::gdk::Display; -use adw::glib::{ExitCode, Propagation}; +use adw::glib::{ExitCode, Propagation, SignalHandlerId, timeout_add_local_once}; +use adw::glib::translate::FromGlib; use adw::prelude::*; -use diesel::{delete, ExpressionMethods, insert_into, QueryDsl, RunQueryDsl, update}; +use diesel::{delete, ExpressionMethods, insert_into, RunQueryDsl, update}; use diesel::migration::Result; use diesel_migrations::MigrationHarness; -use gtk::{Button, CssProvider, IconTheme, MenuButton, Popover, ScrolledWindow, style_context_add_provider_for_display, STYLE_PROVIDER_PRIORITY_APPLICATION}; +use gtk::{CssProvider, IconTheme, ScrolledWindow, style_context_add_provider_for_display, STYLE_PROVIDER_PRIORITY_APPLICATION}; use gtk::Align::Fill; use gtk::Orientation::Vertical; use log::info; use db::MIGRATIONS; -use crate::body::{Body, BodyTable, NavigationType::*}; -use crate::body::artists::artists; -use crate::common::AdjustableScrolledWindow; -use crate::common::constant::{APP_ID, BACK_ICON}; +use crate::body::{BodyTable, BodyType, BODY_TYPE, PARAMS}; +use crate::body::artists::artists_page; +use crate::body::collection::page::{COLLECTION, collection_page}; +use crate::body::download::albums::albums_page; +use crate::body::download::songs::songs_page; +use crate::common::constant::APP_ID; use crate::common::state::State; use crate::common::window_action::WindowActions; use crate::config::Config; use crate::db::get_connection; use crate::now_playing::playbin::{PLAYBIN, Playbin}; -use crate::schema::bodies::{body_type, navigation_type, params, scroll_adjustment}; +use crate::schema::bodies::{body_type, params, scroll_adjustment}; use crate::schema::bodies::dsl::bodies; use crate::schema::config::{current_song_position, maximized, window_height, window_width}; use crate::schema::config::dsl::config as config_table; @@ -36,6 +40,27 @@ mod config; mod body; mod song; +fn handle_scroll(scroll: Option, navigation_page: &NavigationPage) { + let signal_handler_id = Rc::new(RefCell::new(None::)); + *signal_handler_id.borrow_mut() = Some(navigation_page.connect_realize({ + let signal_handler_id = signal_handler_id.clone(); + move |navigation_page| { + if let Some(scroll) = scroll { + timeout_add_local_once(Duration::from_millis(50), { + let navigation_page = navigation_page.clone(); + move || { + navigation_page.child().unwrap().last_child().and_downcast::().unwrap() + .vadjustment().set_value(scroll); + } + }); + } + navigation_page.disconnect(unsafe { + SignalHandlerId::from_glib(signal_handler_id.borrow().as_ref().unwrap().as_raw()) + }); + } + })); +} + fn main() -> Result { std_logger::Config::logfmt().init(); gstreamer::init()?; @@ -43,9 +68,8 @@ fn main() -> Result { let application = Application::builder().application_id(APP_ID).build(); application.connect_activate(|application| { let config = config_table.get_result::(&mut get_connection()).unwrap(); - let header_body = gtk::Box::builder().orientation(Vertical).valign(Fill).build(); - let history_bodies = bodies.filter(navigation_type.eq(History)).get_results::(&mut get_connection()) - .unwrap(); + let body = gtk::Box::builder().orientation(Vertical).valign(Fill).build(); + let history_bodies = bodies.get_results::(&mut get_connection()).unwrap(); let css_provider = CssProvider::new(); css_provider.load_from_data("#accent-bg { background-color: @accent_bg_color; } \ #dialog-bg { background-color: @dialog_bg_color; } @@ -56,62 +80,36 @@ fn main() -> Result { let working_dir = current_dir().unwrap(); info!("working directory is [{}]", working_dir.to_str().unwrap()); IconTheme::for_display(&display).add_search_path(working_dir.join("icons")); - let window_title = WindowTitle::builder().build(); let window = ApplicationWindow::builder().application(application).title("Harborz").icon_name("Harborz") - .content(&header_body).default_width(config.window_width).default_height(config.window_height) + .content(&body).default_width(config.window_width).default_height(config.window_height) .maximized(config.maximized == 1).build(); let window_actions = WindowActions::new(&window); - let a = 0; let state = Rc::new(State { window, - header_body, - header_bar: HeaderBar::builder().title_widget(&window_title).build(), - back_button: Button::builder().icon_name(BACK_ICON).tooltip_text("Home").visible(history_bodies.len() > 1) - .build(), + body: body.clone(), window_actions, - menu_button: MenuButton::builder().icon_name("open-menu-symbolic").tooltip_text("Menu") - .popover(&Popover::new()).build(), - scrolled_window: ScrolledWindow::builder().vexpand(true).build(), - history: RefCell::new(Vec::new()), - }); - state.window_actions.change_window_title.action.connect_activate({ - let window_title = window_title.clone(); - move |_, variant| { window_title.set_title(variant.unwrap().str().unwrap()); } - }); - state.window_actions.change_window_subtitle.action.connect_activate(move |_, variant| { - window_title.set_subtitle(variant.unwrap().str().unwrap()); + navigation_view: NavigationView::new(), }); - state.header_body.append(&state.header_bar); - state.header_bar.pack_start(&state.back_button); - state.header_bar.pack_end(&state.menu_button); - let body = gtk::Box::builder().orientation(Vertical).build(); - state.header_body.append(&body); - body.append(&state.scrolled_window); - let song_selected_body: Rc>>> = Rc::new(RefCell::new(None)); - let (now_playing_body, bottom_widget, now_playing) - = now_playing::create(song_selected_body.clone(), state.clone(), &body); - body.append(&bottom_widget); - *song_selected_body.borrow_mut() = bodies.filter(navigation_type.eq(SongSelected)).limit(1) - .get_result::(&mut get_connection()).ok().map(|BodyTable { - body_type: body_body_type, params: body_params, scroll_adjustment: body_scroll_adjustment, .. - }| { - let body = Body::from_body_table(body_body_type, - serde_json::from_str::>>(&body_params).unwrap(), state.clone()); - body.scroll_adjustment.set(body_scroll_adjustment); - Rc::new(body) - }); - let empty_history = history_bodies.is_empty(); - for BodyTable { - body_type: body_body_type, params: body_params, scroll_adjustment: body_scroll_adjustment, .. - } in history_bodies { - Body::from_body_table(body_body_type, serde_json::from_str::>>(&body_params).unwrap(), - state.clone(), - ).put_to_history(body_scroll_adjustment, state.clone()); - } - if empty_history { - Rc::new(artists(state.clone())).set_with_history(state.clone()); - } else if let Some((body, _)) = state.history.borrow().last() { - body.clone().set(state.clone()); + state.body.append(&state.navigation_view); + let (now_playing_body, bottom_widget, now_playing) = now_playing::create(state.clone()); + state.body.append(&bottom_widget); + let artists_page = artists_page(state.clone()); + state.navigation_view.add(&artists_page); + let collection_page = collection_page(state.clone()); + state.navigation_view.add(&collection_page); + for body_table in history_bodies { + let body_params = serde_json::from_str::>>(&body_table.params).unwrap().into_iter() + .map(|it| { it.map(Arc::new) }).collect(); + let scroll = body_table.scroll_adjustment.map(|it| { it as f64 }); + match body_table.body_type { + BodyType::Artists => { handle_scroll(scroll, &artists_page); } + BodyType::Albums => { state.navigation_view.push(&albums_page(body_params, state.clone(), scroll)); } + BodyType::Songs => { state.navigation_view.push(&songs_page(body_params, state.clone(), scroll)); } + BodyType::Collections => { + state.navigation_view.push_by_tag(COLLECTION); + handle_scroll(scroll, &collection_page); + } + } } if config.now_playing_body_realized == 1 { now_playing.borrow().realize_body(state.clone(), &now_playing_body); @@ -125,26 +123,20 @@ fn main() -> Result { current_song_position.eq(PLAYBIN.get_position().unwrap_or(0) as i64) )).execute(&mut get_connection()).unwrap(); delete(bodies).execute(&mut get_connection()).unwrap(); - let history = state.history.borrow(); - if let Some((body, _)) = history.last() { - body.scroll_adjustment.set(state.scrolled_window.get_adjustment()); - } insert_into(bodies).values( - history.iter().map(|(body, _)| { (body, History) }) - .chain(song_selected_body.borrow().iter().map(|body| { (body, SongSelected) })) - .map(|(body, body_navigation_type)| { - let Body { - params: body_params, body_type: body_body_type, - scroll_adjustment: body_scroll_adjustment, .. - } = body.deref(); - let value: Vec> = (*body_params).iter() - .map(|param| { param.clone().map(|it| { (*it).to_owned() }) }).collect(); - (params.eq( - serde_json::to_string(&value).unwrap()), - body_type.eq(body_body_type), scroll_adjustment.eq(body_scroll_adjustment.get()), - navigation_type.eq(body_navigation_type) - ) - }).collect::>() + state.navigation_view.navigation_stack().iter::().map(|navigation_page| { + let navigation_page = navigation_page.unwrap(); + let (params_ref, body_type_ref) = unsafe { + (navigation_page.data::>>>(PARAMS).unwrap().as_ref(), + navigation_page.data::(BODY_TYPE).unwrap().as_ref()) + }; + (params.eq(serde_json::to_string(¶ms_ref.iter().map(Option::as_deref).collect::>()) + .unwrap()), + body_type.eq(body_type_ref), + scroll_adjustment.eq(Some(navigation_page.child().unwrap().last_child() + .and_downcast::().unwrap().vadjustment().value() as f32)), + ) + }).collect::>() ).execute(&mut get_connection()).unwrap(); Propagation::Proceed } diff --git a/src/now_playing/body.rs b/src/now_playing/body.rs index 34378f5..1740dd2 100644 --- a/src/now_playing/body.rs +++ b/src/now_playing/body.rs @@ -1,5 +1,6 @@ use std::cell::RefCell; use std::rc::Rc; +use adw::HeaderBar; use adw::prelude::*; use gtk::{Button, GestureSwipe, Image}; use gtk::Orientation::Vertical; @@ -7,8 +8,12 @@ use crate::common::StyledWidget; use crate::now_playing::now_playing::NowPlaying; use crate::now_playing::playbin::{PLAYBIN, Playbin}; -pub(super) fn create(now_playing: Rc>) -> (gtk::Box, GestureSwipe) { +pub(super) fn create(now_playing: Rc>) -> (gtk::Box, Button, GestureSwipe) { let body = gtk::Box::builder().orientation(Vertical).margin_bottom(48).build(); + let header_bar = HeaderBar::builder().title_widget(&now_playing.borrow().window_title).build(); + body.append(&header_bar); + let down_button = Button::builder().icon_name("go-down").build(); + header_bar.pack_start(&down_button); let image_and_song_info = gtk::Box::builder().orientation(Vertical).build(); body.append(&image_and_song_info); image_and_song_info.append(&now_playing.borrow().body_image); @@ -37,5 +42,5 @@ pub(super) fn create(now_playing: Rc>) -> (gtk::Box, Gesture controls.append(&skip_backward); controls.append(&now_playing.borrow().body_play_pause); controls.append(&skip_forward); - (body, skip_song_gesture) + (body, down_button, skip_song_gesture) } diff --git a/src/now_playing/bottom_widget.rs b/src/now_playing/bottom_widget.rs index 09d5577..8df6428 100644 --- a/src/now_playing/bottom_widget.rs +++ b/src/now_playing/bottom_widget.rs @@ -1,16 +1,13 @@ use std::cell::RefCell; use std::rc::Rc; use adw::prelude::*; -use gtk::{EventSequenceState, GestureClick, GestureLongPress, GestureSwipe, Label}; +use gtk::{GestureClick, GestureSwipe, Label}; use gtk::Orientation::Vertical; use gtk::PropagationPhase::Capture; -use crate::body::Body; -use crate::common::state::State; use crate::common::StyledLabelBuilder; use crate::now_playing::now_playing::NowPlaying; -pub(super) fn create(now_playing: Rc>, - song_selected_body: Rc>>>, state: Rc) -> (gtk::Box, GestureSwipe, GestureClick) { +pub(super) fn create(now_playing: Rc>) -> (gtk::Box, GestureSwipe, GestureClick) { let now_playing_and_progress = gtk::Box::builder().orientation(Vertical).name("dialog-bg").build(); now_playing_and_progress.append(&now_playing.borrow().progress_bar); let now_playing_and_play_pause = gtk::Box::builder().margin_start(8).margin_end(8).margin_top(8).margin_bottom(8) @@ -21,26 +18,6 @@ pub(super) fn create(now_playing: Rc>, let skip_song_gesture = GestureSwipe::builder().propagation_phase(Capture).build(); now_playing_box.add_controller(skip_song_gesture.clone()); now_playing_box.append(&now_playing.borrow().bottom_image); - let song_selected_body_gesture = GestureLongPress::new(); - song_selected_body_gesture.connect_pressed({ - let song_selected_body = song_selected_body.clone(); - move |gesture, _, _| { - if let Some(body) = song_selected_body.borrow().as_ref() { - let song_selected_body_realized = if let Some((last, _)) = state.history.borrow().last() { - Rc::ptr_eq(last, body) - } else { - return; - }; - if !song_selected_body_realized { - gesture.set_state(EventSequenceState::Claimed); - body.clone().set_with_history(state.clone()); - } - } - } - }); - now_playing.borrow().bottom_image.connect_realize(move |bottom_image| { - bottom_image.add_controller(song_selected_body_gesture.clone()); - }); let image_click = GestureClick::new(); now_playing.borrow().bottom_image.add_controller(image_click.clone()); let song_info = gtk::Box::builder().orientation(Vertical).margin_start(8).build(); diff --git a/src/now_playing/mod.rs b/src/now_playing/mod.rs index 5870fdd..6d06691 100644 --- a/src/now_playing/mod.rs +++ b/src/now_playing/mod.rs @@ -1,6 +1,5 @@ use std::cell::{Cell, RefCell}; use std::mem::forget; -use std::ops::Deref; use std::rc::Rc; use std::sync::Once; use std::time::Duration; @@ -14,11 +13,7 @@ use gstreamer::State::{Null, Paused, Playing}; use gtk::{EventSequenceState, ScrollType}; use log::warn; use mpris_player::{Metadata, PlaybackStatus}; -use crate::body::artists::artists; -use crate::body::Body; use crate::body::collection::model::Collection; -use crate::common::AdjustableScrolledWindow; -use crate::common::constant::BACK_ICON; use crate::common::gesture::{Direction, DirectionSwipe}; use crate::common::state::State; use crate::common::util::or_none; @@ -46,43 +41,29 @@ fn go_delta_song(velocity_x: f64) { PLAYBIN.go_delta_song(if velocity_x > 0.0 { -1 } else { 1 }, true); } -pub fn create(song_selected_body: Rc>>>, state: Rc, body: >k::Box) - -> (gtk::Box, gtk::Box, Rc>) { +pub fn create(state: Rc) -> (gtk::Box, gtk::Box, Rc>) { let now_playing = Rc::new(RefCell::new(NowPlaying::new())); - let (now_playing_body, body_swipe_gesture) = body::create(now_playing.clone()); - let (bottom_widget, bottom_swipe_gesture, image_click) - = bottom_widget::create(now_playing.clone(), song_selected_body.clone(), state.clone()); + let (now_playing_body, down_button, body_swipe_gesture) = body::create(now_playing.clone()); + let (bottom_widget, bottom_swipe_gesture, image_click) = bottom_widget::create(now_playing.clone()); let realize_bottom = { let now_playing = now_playing.clone(); let state = state.clone(); - let body = body.clone(); move || { update_now_playing_body_realized(false); - now_playing.borrow().update_other(state.clone(), BACK_ICON, &body); + now_playing.borrow().update_other(state.clone(), &state.body); } }; - let show_last_history = { - let state = state.clone(); - move || { - if let Some((body, adjust_scroll)) = state.history.borrow().last() { - body.clone().set(state.clone()); - let Body { scroll_adjustment: body_scroll_adjustment, .. } = body.deref(); - if *adjust_scroll { state.scrolled_window.adjust(&body_scroll_adjustment); } - } - } - }; - body_swipe_gesture.connect_direction_swipe({ + down_button.connect_clicked({ let realize_bottom = realize_bottom.clone(); - let show_last_history = show_last_history.clone(); - move |gesture, velocity_x, velocity_y, direction_swipe| { - if direction_swipe(Direction::Horizontal) { - gesture.set_state(EventSequenceState::Claimed); - go_delta_song(velocity_x); - } else if velocity_y > 0.0 && direction_swipe(Direction::Vertical) { - gesture.set_state(EventSequenceState::Claimed); - realize_bottom(); - show_last_history(); - } + move |_| { realize_bottom(); } + }); + body_swipe_gesture.connect_direction_swipe(move |gesture, velocity_x, velocity_y, direction_swipe| { + if direction_swipe(Direction::Horizontal) { + gesture.set_state(EventSequenceState::Claimed); + go_delta_song(velocity_x); + } else if velocity_y > 0.0 && direction_swipe(Direction::Vertical) { + gesture.set_state(EventSequenceState::Claimed); + realize_bottom(); } }); let realize_body = { @@ -110,21 +91,6 @@ pub fn create(song_selected_body: Rc>>>, state: Rc>>>, state: Rc>>>, state: Rc>>>, state: Rc(connection)?; update(config).set(current_song_id.eq(song.id)).execute(connection)?; let title = song.title_str().to_owned(); - now_playing.borrow_mut().set_song_info(state.clone(), &title, or_none(&song.artist)); + now_playing.borrow_mut().set_song_info(&title, or_none(&song.artist)); let cover = (&song, &collection).path().cover(); let art_url = now_playing.borrow_mut().set_album_image(cover); state.window_actions.stream_started.activate(song.id); @@ -253,7 +217,7 @@ pub fn create(song_selected_body: Rc>>>, state: Rc, other: bool) { + fn update_song_info(&self, other: bool) { let (song, artist) = if self.bottom_song.is_realized() != other { (&self.bottom_song, &self.bottom_artist) } else { - state.window_actions.change_window_title.activate(&self.song); - state.window_actions.change_window_subtitle.activate(&self.artist); + self.window_title.set_title(&self.song); + self.window_title.set_subtitle(&self.artist); (&self.body_song, &self.body_artist) }; song.set_label(&self.song); @@ -140,16 +143,16 @@ impl NowPlaying { }.set_label(&format(self.duration)); self.update_position(other); } - pub fn set_song_info(&mut self, state: Rc, song: &str, artist: &str) { + pub fn set_song_info(&mut self, song: &str, artist: &str) { self.song = String::from(song); self.artist = String::from(artist); - self.update_song_info(state, false); + self.update_song_info(false); } pub fn set_duration(&mut self) { self.duration = PLAYBIN.query_duration().map(ClockTime::nseconds).unwrap_or(0); self.update_duration_and_position(false); } - pub fn update_other(&self, state: Rc, icon_name: &str, body: >k::Box) { + pub fn update_other(&self, state: Rc, body: >k::Box) { self.update_image(true); self.update_duration_and_position(true); let (current_play_pause, other_play_pause) = if self.bottom_play_pause.is_realized() { @@ -160,14 +163,11 @@ impl NowPlaying { if let Some(tooltip) = current_play_pause.tooltip_text() { other_play_pause.change_state(if tooltip.as_str() == "Play" { PLAY } else { PAUSE }); } - self.update_song_info(state.clone(), true); - state.back_button.set_visible(true); - state.back_button.set_icon_name(icon_name); - state.header_body.remove(&state.header_body.last_child().unwrap()); - state.header_body.append(body); + self.update_song_info(true); + state.window.set_content(Some(body)); } pub fn realize_body(&self, state: Rc, body: >k::Box) { - self.update_other(state, "go-down", body); + self.update_other(state, body); } pub fn set_album_image(&mut self, cover: PathBuf) -> Option { let result = if cover.exists() { cover.to_str().map(|it| { format!("file:{it}") }) } else { None }; diff --git a/src/now_playing/playbin.rs b/src/now_playing/playbin.rs index 2e5ba29..506ee56 100644 --- a/src/now_playing/playbin.rs +++ b/src/now_playing/playbin.rs @@ -79,7 +79,7 @@ impl Playbin for Pipeline { .select((current_song_id, artist, album)) .get_result::<(Option, Option, Option)>(connection) { let song_collections - = get_current_album(artist_string.map(Rc::new), album_string.map(Rc::new), connection); + = get_current_album(&artist_string.map(Rc::new), &album_string.map(Rc::new), connection); let delta_song_index = song_collections.iter().position(|(song, _)| { song.id == current_song_id_int }) .unwrap() as i32 + delta; if delta_song_index >= 0 && delta_song_index < song_collections.len() as i32 { diff --git a/src/schema.rs b/src/schema.rs index 09acc39..5219f2e 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -5,7 +5,6 @@ diesel::table! { id -> Integer, body_type -> crate::body::BodyTypeMapping, scroll_adjustment -> Nullable, - navigation_type -> crate::body::NavigationTypeMapping, params -> Text, } } diff --git a/src/song/mod.rs b/src/song/mod.rs index 5de604a..68c4be3 100644 --- a/src/song/mod.rs +++ b/src/song/mod.rs @@ -140,7 +140,7 @@ pub fn import_songs(collection: Arc>, sender: Sender(connection) } -pub fn get_current_album(artist_string: Option>, album_string: Option>, +pub fn get_current_album(artist_string: &Option>, album_string: &Option>, connection: &mut PooledConnection>) -> Vec<(Song, Collection)> { let statement = songs.inner_join(collections).order_by((track_number, id)).into_boxed(); let artist_filtered_statement = if let Some(artist_string) = &artist_string {