Collections (#58)
Reviewed-on: https://codeberg.org/avery42/delfin/pulls/58 Co-authored-by: Avery <git@avery.cafe> Co-committed-by: Avery <git@avery.cafe>
This commit is contained in:
@@ -42,6 +42,7 @@ relm4-icons = { version = "0.7.0-alpha.2", features = [
|
||||
"warning",
|
||||
"folder-filled",
|
||||
"loupe",
|
||||
"library",
|
||||
] }
|
||||
reqwest = { version = "0.11.20", features = ["json"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
|
@@ -16,7 +16,7 @@ use crate::{
|
||||
config,
|
||||
globals::CONFIG,
|
||||
jellyfin_api::api_client::ApiClient,
|
||||
library::{Library, LibraryOutput, LIBRARY_BROKER},
|
||||
library::{collection::Collection, Library, LibraryOutput, LIBRARY_BROKER},
|
||||
locales::tera_tr,
|
||||
media_details::MediaDetails,
|
||||
meson_config::APP_ID,
|
||||
@@ -60,6 +60,7 @@ pub struct App {
|
||||
account_list: Controller<AccountList>,
|
||||
library: Option<Controller<Library>>,
|
||||
media_details: Option<Controller<MediaDetails>>,
|
||||
collection: Option<Controller<Collection>>,
|
||||
video_player: OnceCell<Controller<VideoPlayer>>,
|
||||
server: Option<config::Server>,
|
||||
account: Option<config::Account>,
|
||||
@@ -73,6 +74,7 @@ pub enum AppInput {
|
||||
ServerSelected(config::Server),
|
||||
AccountSelected(config::Server, config::Account),
|
||||
ShowDetails(BaseItemDto),
|
||||
ShowCollection(BaseItemDto),
|
||||
PlayVideo(BaseItemDto),
|
||||
SignOut,
|
||||
SetThemeDark(bool),
|
||||
@@ -162,6 +164,7 @@ impl Component for App {
|
||||
account_list,
|
||||
library: None,
|
||||
media_details: None,
|
||||
collection: None,
|
||||
video_player: OnceCell::new(),
|
||||
server: None,
|
||||
account: None,
|
||||
@@ -228,6 +231,22 @@ impl Component for App {
|
||||
self.media_details = Some(media_details);
|
||||
}
|
||||
}
|
||||
AppInput::ShowCollection(collection) => {
|
||||
if let (Some(api_client), Some(server), Some(account)) =
|
||||
(&self.api_client, &self.server, &self.account)
|
||||
{
|
||||
let collection = Collection::builder()
|
||||
.launch((
|
||||
api_client.clone(),
|
||||
collection,
|
||||
server.clone(),
|
||||
account.clone(),
|
||||
))
|
||||
.detach();
|
||||
navigation.push(collection.widget());
|
||||
self.collection = Some(collection);
|
||||
}
|
||||
}
|
||||
AppInput::PlayVideo(item) => {
|
||||
if self.video_player.get().is_none() {
|
||||
let video_player = VideoPlayer::builder()
|
||||
|
@@ -1,67 +1,67 @@
|
||||
use anyhow::{bail, Context, Ok, Result};
|
||||
use anyhow::{Context, Ok, Result};
|
||||
use jellyfin_api::types::{BaseItemDto, BaseItemDtoQueryResult};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::jellyfin_api::{api_client::ApiClient, models::collection_type::CollectionType};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserView {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub collection_type: CollectionType,
|
||||
}
|
||||
|
||||
impl TryFrom<BaseItemDto> for UserView {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: BaseItemDto) -> std::result::Result<Self, Self::Error> {
|
||||
if let (Some(id), Some(name)) = (value.id, value.name.clone()) {
|
||||
return Ok(Self {
|
||||
id,
|
||||
name,
|
||||
collection_type: value.collection_type.into(),
|
||||
});
|
||||
}
|
||||
|
||||
bail!("UserView was missing required properties: {value:#?}");
|
||||
}
|
||||
}
|
||||
use crate::jellyfin_api::{
|
||||
api_client::ApiClient,
|
||||
models::{collection_type::CollectionType, user_view::UserView},
|
||||
};
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn get_user_views(&self) -> Result<Vec<UserView>> {
|
||||
let url = self
|
||||
pub async fn get_user_views(
|
||||
&self,
|
||||
start_index: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
) -> Result<(Vec<UserView>, usize)> {
|
||||
let mut url = self
|
||||
.root
|
||||
.join(&format!("Users/{}/Views", self.account.id))
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
if let Some(start_index) = start_index {
|
||||
query_pairs.append_pair("StartIndex", &start_index.to_string());
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
query_pairs.append_pair("Limit", &limit.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let res: BaseItemDtoQueryResult = self.client.get(url).send().await?.json().await?;
|
||||
|
||||
let items = res.items.ok_or(anyhow::anyhow!("No items returned"))?;
|
||||
items
|
||||
.iter()
|
||||
.map(|item| UserView::try_from(item.clone()))
|
||||
.collect()
|
||||
let items = res.items.context("No items returned")?;
|
||||
let total_record_count = res
|
||||
.total_record_count
|
||||
.context("Total record count not returned")?;
|
||||
|
||||
let items: Result<Vec<UserView>, _> =
|
||||
items.into_iter().map(|item| item.try_into()).collect();
|
||||
|
||||
Ok((items?, total_record_count as usize))
|
||||
}
|
||||
|
||||
pub async fn get_view_items(
|
||||
pub async fn get_collection_items(
|
||||
&self,
|
||||
view: &UserView,
|
||||
collection: &BaseItemDto,
|
||||
start_index: usize,
|
||||
limit: usize,
|
||||
) -> Result<(Vec<BaseItemDto>, usize)> {
|
||||
let collection_type = CollectionType::from(collection.collection_type.clone());
|
||||
|
||||
let mut url = self
|
||||
.root
|
||||
.join(&format!("Users/{}/Items", self.account.id))
|
||||
.unwrap();
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("ParentId", &view.id.to_string())
|
||||
.append_pair("ParentId", &collection.id.unwrap().to_string())
|
||||
.append_pair("SortBy", "SortName,ProductionYear")
|
||||
.append_pair("SortOrder", "Ascending")
|
||||
.append_pair("Recursive", "true")
|
||||
.append_pair("StartIndex", &start_index.to_string())
|
||||
.append_pair("Limit", &limit.to_string());
|
||||
|
||||
if let Some(item_type) = view.collection_type.item_type() {
|
||||
if let Some(item_type) = collection_type.item_type() {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("IncludeItemTypes", &item_type.to_string());
|
||||
}
|
||||
|
@@ -1,2 +1,3 @@
|
||||
pub mod collection_type;
|
||||
pub mod display_preferences;
|
||||
pub mod user_view;
|
||||
|
61
delfin/src/jellyfin_api/models/user_view.rs
Normal file
61
delfin/src/jellyfin_api/models/user_view.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use anyhow::bail;
|
||||
use jellyfin_api::types::BaseItemDto;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::tr;
|
||||
|
||||
use super::collection_type::CollectionType;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UserView(BaseItemDto);
|
||||
|
||||
impl UserView {
|
||||
pub fn id(&self) -> Uuid {
|
||||
self.0.id.unwrap()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.0
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or(tr!("library-unnamed-collection").to_string())
|
||||
}
|
||||
|
||||
pub fn collection_type(&self) -> CollectionType {
|
||||
self.0.collection_type.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<BaseItemDto> for UserView {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(item: BaseItemDto) -> std::result::Result<Self, Self::Error> {
|
||||
if item.id.is_none() {
|
||||
bail!("Item was missing ID: {item:#?}");
|
||||
}
|
||||
Ok(Self(item))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserView> for BaseItemDto {
|
||||
fn from(val: UserView) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FilterSupported {
|
||||
fn filter_supported(self) -> Self;
|
||||
}
|
||||
|
||||
impl FilterSupported for Vec<UserView> {
|
||||
fn filter_supported(self) -> Self {
|
||||
self.into_iter()
|
||||
.filter(|view| {
|
||||
matches!(
|
||||
view.collection_type(),
|
||||
CollectionType::Movies | CollectionType::TvShows
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
@@ -34,6 +34,20 @@ impl ApiClient {
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
pub fn get_collection_thumbnail_url(&self, item: &BaseItemDto) -> Result<String> {
|
||||
let item_id = match item.parent_backdrop_item_id.or(item.id) {
|
||||
Some(item_id) => item_id,
|
||||
None => bail!("Missing parent backdrop item ID"),
|
||||
};
|
||||
|
||||
let mut url = self.root.join(&format!("Items/{item_id}/Images/Primary"))?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("fillWidth", "700")
|
||||
.append_pair("quality", "100");
|
||||
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
pub fn get_parent_or_item_backdrop_url(&self, item: &BaseItemDto) -> Result<String> {
|
||||
let item_id = match item.parent_backdrop_item_id.or(item.id) {
|
||||
Some(item_id) => item_id,
|
||||
|
@@ -1,91 +1,151 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use adw::prelude::*;
|
||||
use anyhow::Result;
|
||||
use gtk::prelude::*;
|
||||
use jellyfin_api::types::BaseItemDto;
|
||||
use relm4::prelude::*;
|
||||
|
||||
use crate::{
|
||||
jellyfin_api::{api::views::UserView, api_client::ApiClient},
|
||||
library::media_page::{MediaPage, MediaPageInput},
|
||||
borgar::borgar_menu::{BorgarMenu, BorgarMenuAuth},
|
||||
config::{Account, Server},
|
||||
jellyfin_api::api_client::ApiClient,
|
||||
library::{
|
||||
media_page::{MediaPage, MediaPageInput},
|
||||
media_tile::MediaTileDisplay,
|
||||
},
|
||||
tr,
|
||||
utils::empty_component::EmptyComponent,
|
||||
};
|
||||
|
||||
use super::{media_fetcher::Fetcher, media_page::MediaPageInit};
|
||||
|
||||
pub struct Collection {
|
||||
media_page: Controller<MediaPage<ViewItemsFetcher, EmptyComponent>>,
|
||||
initialized: bool,
|
||||
api_client: Arc<ApiClient>,
|
||||
collection: BaseItemDto,
|
||||
borgar_menu: Controller<BorgarMenu>,
|
||||
media_page: Controller<MediaPage<CollectionItemsFetcher, EmptyComponent>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CollectionInput {
|
||||
Visible,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for Collection {
|
||||
type Init = (Arc<ApiClient>, UserView);
|
||||
impl Component for Collection {
|
||||
type Init = (Arc<ApiClient>, BaseItemDto, Server, Account);
|
||||
type Input = CollectionInput;
|
||||
type Output = ();
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
gtk::Box {}
|
||||
adw::NavigationPage {
|
||||
#[name = "toolbar_view"]
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ToolbarView {
|
||||
add_top_bar = &adw::HeaderBar {
|
||||
pack_end = model.borgar_menu.widget(),
|
||||
pack_end = >k::Button::from_icon_name("refresh") {
|
||||
set_tooltip: tr!("library-collection-refresh-button"),
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(CollectionInput::Refresh);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
#[wrap(Some)]
|
||||
set_content = model.media_page.widget(),
|
||||
},
|
||||
|
||||
set_title: &model.collection
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap_or(tr!("library-unnamed-collection"))
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: &Self::Root,
|
||||
_sender: ComponentSender<Self>,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let (api_client, view) = init;
|
||||
|
||||
let fetcher = ViewItemsFetcher {
|
||||
api_client: api_client.clone(),
|
||||
view,
|
||||
};
|
||||
let (api_client, collection, server, account) = init;
|
||||
|
||||
let model = Collection {
|
||||
media_page: MediaPage::builder()
|
||||
.launch(MediaPageInit {
|
||||
api_client,
|
||||
fetcher,
|
||||
empty_component: None,
|
||||
})
|
||||
api_client: api_client.clone(),
|
||||
collection: collection.clone(),
|
||||
borgar_menu: BorgarMenu::builder()
|
||||
.launch(Some(BorgarMenuAuth {
|
||||
api_client: api_client.clone(),
|
||||
server,
|
||||
account,
|
||||
}))
|
||||
.detach(),
|
||||
initialized: false,
|
||||
media_page: new_media_page(&api_client, collection),
|
||||
};
|
||||
root.append(model.media_page.widget());
|
||||
|
||||
model.media_page.emit(MediaPageInput::NextPage);
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
|
||||
fn update_with_view(
|
||||
&mut self,
|
||||
widgets: &mut Self::Widgets,
|
||||
message: Self::Input,
|
||||
sender: ComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
match message {
|
||||
CollectionInput::Visible if !self.initialized => {
|
||||
self.initialized = true;
|
||||
self.media_page.emit(MediaPageInput::NextPage);
|
||||
CollectionInput::Refresh => {
|
||||
let toolbar_view = &widgets.toolbar_view;
|
||||
let media_page = new_media_page(&self.api_client, self.collection.clone());
|
||||
toolbar_view.set_content(Some(media_page.widget()));
|
||||
media_page.emit(MediaPageInput::NextPage);
|
||||
self.media_page = media_page;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.update_view(widgets, sender);
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewItemsFetcher {
|
||||
api_client: Arc<ApiClient>,
|
||||
view: UserView,
|
||||
fn new_media_page(
|
||||
api_client: &Arc<ApiClient>,
|
||||
collection: BaseItemDto,
|
||||
) -> Controller<MediaPage<CollectionItemsFetcher, EmptyComponent>> {
|
||||
let fetcher = CollectionItemsFetcher {
|
||||
api_client: api_client.clone(),
|
||||
collection,
|
||||
};
|
||||
MediaPage::builder()
|
||||
.launch(MediaPageInit {
|
||||
api_client: api_client.clone(),
|
||||
fetcher,
|
||||
empty_component: None,
|
||||
media_tile_display: Some(MediaTileDisplay::CoverLarge),
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
|
||||
impl Fetcher for ViewItemsFetcher {
|
||||
struct CollectionItemsFetcher {
|
||||
api_client: Arc<ApiClient>,
|
||||
collection: BaseItemDto,
|
||||
}
|
||||
|
||||
impl Fetcher for CollectionItemsFetcher {
|
||||
async fn fetch(&self, start_index: usize, limit: usize) -> Result<(Vec<BaseItemDto>, usize)> {
|
||||
self.api_client
|
||||
.get_view_items(&self.view, start_index, limit)
|
||||
.get_collection_items(&self.collection, start_index, limit)
|
||||
.await
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
self.view.name.clone()
|
||||
self.collection
|
||||
.name
|
||||
.as_ref()
|
||||
.unwrap_or(tr!("library-unnamed-collection"))
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
85
delfin/src/library/collections.rs
Normal file
85
delfin/src/library/collections.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gtk::prelude::*;
|
||||
use jellyfin_api::types::BaseItemDto;
|
||||
use relm4::prelude::*;
|
||||
|
||||
use crate::{
|
||||
jellyfin_api::{api_client::ApiClient, models::user_view::FilterSupported},
|
||||
library::{
|
||||
media_page::{MediaPageInit, MediaPageInput},
|
||||
media_tile::MediaTileDisplay,
|
||||
},
|
||||
utils::empty_component::EmptyComponent,
|
||||
};
|
||||
|
||||
use super::{media_fetcher::Fetcher, media_page::MediaPage};
|
||||
|
||||
pub struct Collections {
|
||||
media_page: Controller<MediaPage<CollectionsFetcher, EmptyComponent>>,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for Collections {
|
||||
type Init = Arc<ApiClient>;
|
||||
type Input = ();
|
||||
type Output = ();
|
||||
|
||||
view! {
|
||||
gtk::Box {
|
||||
append = model.media_page.widget(),
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
api_client: Self::Init,
|
||||
root: &Self::Root,
|
||||
_sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let fetcher = CollectionsFetcher {
|
||||
api_client: api_client.clone(),
|
||||
};
|
||||
|
||||
let model = Collections {
|
||||
media_page: MediaPage::builder()
|
||||
.launch(MediaPageInit {
|
||||
api_client,
|
||||
fetcher,
|
||||
empty_component: None,
|
||||
media_tile_display: Some(MediaTileDisplay::CollectionWide),
|
||||
})
|
||||
.detach(),
|
||||
};
|
||||
|
||||
model.media_page.emit(MediaPageInput::NextPage);
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
}
|
||||
|
||||
struct CollectionsFetcher {
|
||||
api_client: Arc<ApiClient>,
|
||||
}
|
||||
|
||||
impl Fetcher for CollectionsFetcher {
|
||||
async fn fetch(&self, start_index: usize, limit: usize) -> Result<(Vec<BaseItemDto>, usize)> {
|
||||
let (collections, total) = self
|
||||
.api_client
|
||||
.get_user_views(Some(start_index), Some(limit))
|
||||
.await?;
|
||||
let collections = collections
|
||||
.filter_supported()
|
||||
.into_iter()
|
||||
.map(|view| view.into())
|
||||
.collect();
|
||||
// TODO: numbering will be off
|
||||
Ok((collections, total))
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
"Collections".into()
|
||||
}
|
||||
}
|
@@ -5,12 +5,13 @@ use relm4::{
|
||||
gtk, prelude::*, Component, ComponentParts, ComponentSender, Controller, SimpleComponent,
|
||||
};
|
||||
|
||||
use crate::jellyfin_api::api::views::UserView;
|
||||
use crate::jellyfin_api::api_client::ApiClient;
|
||||
use crate::jellyfin_api::models::display_preferences::{DisplayPreferences, HomeSection};
|
||||
use crate::jellyfin_api::models::user_view::UserView;
|
||||
|
||||
use super::home_sections::continue_watching::HomeSectionContinueWatching;
|
||||
use super::home_sections::latest::HomeSectionLatest;
|
||||
use super::home_sections::my_media::{HomeSectionMyMedia, HomeSectionMyMediaInit};
|
||||
use super::home_sections::next_up::HomeSectionNextUp;
|
||||
use super::library_container::LibraryContainer;
|
||||
|
||||
@@ -18,18 +19,13 @@ enum HomeSectionController {
|
||||
ContinueWatching(Controller<HomeSectionContinueWatching>),
|
||||
Latest(Controller<HomeSectionLatest>),
|
||||
NextUp(Controller<HomeSectionNextUp>),
|
||||
MyMedia(Controller<HomeSectionMyMedia>),
|
||||
}
|
||||
|
||||
pub struct Home {
|
||||
_sections: Vec<HomeSectionController>,
|
||||
sections: Vec<HomeSectionController>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HomeInput {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HomeOutput {}
|
||||
|
||||
pub struct HomeInit {
|
||||
pub api_client: Arc<ApiClient>,
|
||||
pub display_preferences: DisplayPreferences,
|
||||
@@ -38,9 +34,9 @@ pub struct HomeInit {
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for Home {
|
||||
type Input = HomeInput;
|
||||
type Output = HomeOutput;
|
||||
type Init = HomeInit;
|
||||
type Input = ();
|
||||
type Output = ();
|
||||
|
||||
view! {
|
||||
gtk::ScrolledWindow {
|
||||
@@ -63,7 +59,7 @@ impl SimpleComponent for Home {
|
||||
root: &Self::Root,
|
||||
_sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let mut model = Home { _sections: vec![] };
|
||||
let mut model = Home { sections: vec![] };
|
||||
|
||||
let widgets = view_output!();
|
||||
let sections_container = &widgets.sections_container;
|
||||
@@ -94,7 +90,7 @@ impl Home {
|
||||
.launch(api_client.clone())
|
||||
.detach();
|
||||
sections_container.append(section.widget());
|
||||
self._sections
|
||||
self.sections
|
||||
.push(HomeSectionController::ContinueWatching(section));
|
||||
}
|
||||
HomeSection::LatestMedia => {
|
||||
@@ -102,14 +98,25 @@ impl Home {
|
||||
.launch((api_client.clone(), user_views.clone()))
|
||||
.detach();
|
||||
sections_container.append(section.widget());
|
||||
self._sections.push(HomeSectionController::Latest(section));
|
||||
self.sections.push(HomeSectionController::Latest(section));
|
||||
}
|
||||
HomeSection::NextUp => {
|
||||
let section = HomeSectionNextUp::builder()
|
||||
.launch(api_client.clone())
|
||||
.detach();
|
||||
sections_container.append(section.widget());
|
||||
self._sections.push(HomeSectionController::NextUp(section));
|
||||
self.sections.push(HomeSectionController::NextUp(section));
|
||||
}
|
||||
HomeSection::MyMedia | HomeSection::MyMediaSmall => {
|
||||
let section = HomeSectionMyMedia::builder()
|
||||
.launch(HomeSectionMyMediaInit {
|
||||
api_client: api_client.clone(),
|
||||
user_views: user_views.clone(),
|
||||
small: matches!(section, HomeSection::MyMediaSmall),
|
||||
})
|
||||
.detach();
|
||||
sections_container.append(section.widget());
|
||||
self.sections.push(HomeSectionController::MyMedia(section));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@@ -10,7 +10,11 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
jellyfin_api::{
|
||||
api::views::UserView, api_client::ApiClient, models::collection_type::CollectionType,
|
||||
api_client::ApiClient,
|
||||
models::{
|
||||
collection_type::CollectionType,
|
||||
user_view::{FilterSupported, UserView},
|
||||
},
|
||||
},
|
||||
library::media_list::{
|
||||
MediaList, MediaListInit, MediaListOutput, MediaListType, MediaListTypeLatestParams,
|
||||
@@ -56,22 +60,14 @@ impl Component for HomeSectionLatest {
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
let user_views: Vec<&UserView> = user_views
|
||||
.iter()
|
||||
.filter(|view| {
|
||||
matches!(
|
||||
view.collection_type,
|
||||
CollectionType::Movies | CollectionType::TvShows
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let user_views = user_views.filter_supported();
|
||||
|
||||
for view in user_views {
|
||||
let row = LatestRow::builder()
|
||||
.launch((api_client.clone(), view.clone()))
|
||||
.forward(sender.input_sender(), identity);
|
||||
root.append(row.widget());
|
||||
model.rows.insert(view.id.to_string(), row);
|
||||
model.rows.insert(view.id().to_string(), row);
|
||||
}
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
@@ -124,7 +120,7 @@ impl SimpleComponent for LatestRow {
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
let title_text = match view.collection_type {
|
||||
let title_text = match view.collection_type() {
|
||||
CollectionType::Movies => tr!("library-section-title.latest-movies").to_string(),
|
||||
CollectionType::TvShows => tr!("library-section-title.latest-shows").to_string(),
|
||||
CollectionType::Music => tr!("library-section-title.latest-music").to_string(),
|
||||
@@ -137,7 +133,7 @@ impl SimpleComponent for LatestRow {
|
||||
let media_list = MediaList::builder()
|
||||
.launch(MediaListInit {
|
||||
api_client,
|
||||
list_type: MediaListType::Latest(MediaListTypeLatestParams { view_id: view.id }),
|
||||
list_type: MediaListType::Latest(MediaListTypeLatestParams { view_id: view.id() }),
|
||||
label: title_text.to_string(),
|
||||
})
|
||||
.forward(sender.input_sender(), |o| o.into());
|
||||
|
@@ -1,3 +1,4 @@
|
||||
pub mod continue_watching;
|
||||
pub mod latest;
|
||||
pub mod my_media;
|
||||
pub mod next_up;
|
||||
|
74
delfin/src/library/home_sections/my_media.rs
Normal file
74
delfin/src/library/home_sections/my_media.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
|
||||
use crate::{
|
||||
jellyfin_api::{
|
||||
api_client::ApiClient,
|
||||
models::user_view::{FilterSupported, UserView},
|
||||
},
|
||||
library::media_list::{MediaList, MediaListInit, MediaListOutput, MediaListType},
|
||||
tr,
|
||||
};
|
||||
|
||||
pub struct HomeSectionMyMedia {
|
||||
media_list: AsyncController<MediaList>,
|
||||
}
|
||||
|
||||
pub struct HomeSectionMyMediaInit {
|
||||
pub api_client: Arc<ApiClient>,
|
||||
pub user_views: Vec<UserView>,
|
||||
pub small: bool,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl Component for HomeSectionMyMedia {
|
||||
type Init = HomeSectionMyMediaInit;
|
||||
type Input = MediaListOutput;
|
||||
type Output = ();
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 16,
|
||||
|
||||
model.media_list.widget(),
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: &Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let HomeSectionMyMediaInit {
|
||||
api_client,
|
||||
user_views,
|
||||
small,
|
||||
} = init;
|
||||
|
||||
let user_views = user_views.filter_supported();
|
||||
|
||||
let media_list = MediaList::builder()
|
||||
.launch(MediaListInit {
|
||||
api_client,
|
||||
list_type: MediaListType::MyMedia { user_views, small },
|
||||
label: tr!("library-section-title.my-media").to_string(),
|
||||
})
|
||||
.forward(sender.input_sender(), |m| m);
|
||||
|
||||
let model = HomeSectionMyMedia { media_list };
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>, root: &Self::Root) {
|
||||
match message {
|
||||
MediaListOutput::Empty(_) => root.set_visible(false),
|
||||
}
|
||||
}
|
||||
}
|
53
delfin/src/library/media_button.rs
Normal file
53
delfin/src/library/media_button.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use gtk::prelude::*;
|
||||
use jellyfin_api::types::BaseItemDto;
|
||||
use relm4::prelude::*;
|
||||
|
||||
use crate::{
|
||||
app::{AppInput, APP_BROKER},
|
||||
jellyfin_api::models::collection_type::CollectionType,
|
||||
tr,
|
||||
};
|
||||
|
||||
use super::media_tile::MediaTileDisplay;
|
||||
|
||||
pub struct MediaButton;
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for MediaButton {
|
||||
type Init = (BaseItemDto, MediaTileDisplay);
|
||||
type Input = ();
|
||||
type Output = ();
|
||||
|
||||
view! {
|
||||
gtk::Button {
|
||||
add_css_class: "pill",
|
||||
set_width_request: display.width(),
|
||||
set_margin_bottom: 10,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ButtonContent {
|
||||
set_label: media.name.as_ref().unwrap_or(tr!("library-unnamed-collection")),
|
||||
set_icon_name: &collection_type.icon(),
|
||||
set_halign: gtk::Align::Center,
|
||||
},
|
||||
|
||||
connect_clicked[media] => move |_| {
|
||||
APP_BROKER.send(AppInput::ShowCollection(media.clone()));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: &Self::Root,
|
||||
_sender: relm4::ComponentSender<Self>,
|
||||
) -> relm4::ComponentParts<Self> {
|
||||
let (media, display) = init;
|
||||
|
||||
let collection_type: CollectionType = media.clone().collection_type.into();
|
||||
|
||||
let model = MediaButton;
|
||||
let widgets = view_output!();
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
}
|
@@ -7,19 +7,42 @@ use relm4::{prelude::*, ComponentParts};
|
||||
|
||||
use crate::{globals::SHIFT_STATE, jellyfin_api::api_client::ApiClient};
|
||||
|
||||
use super::media_tile::{MediaTile, MediaTileDisplay};
|
||||
use super::{
|
||||
media_button::MediaButton,
|
||||
media_tile::{MediaTile, MediaTileDisplay},
|
||||
};
|
||||
|
||||
const MIN_PADDING: i32 = 24;
|
||||
|
||||
pub(crate) enum MediaCarouselItem {
|
||||
Tile(AsyncController<MediaTile>),
|
||||
Button(Controller<MediaButton>),
|
||||
}
|
||||
|
||||
impl MediaCarouselItem {
|
||||
fn widget(&self) -> >k::Widget {
|
||||
match self {
|
||||
MediaCarouselItem::Tile(media_tile) => media_tile.widget().upcast_ref(),
|
||||
MediaCarouselItem::Button(media_button) => media_button.widget().upcast_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum MediaCarouselType {
|
||||
Tiles,
|
||||
Buttons,
|
||||
}
|
||||
|
||||
pub(crate) struct MediaCarousel {
|
||||
media_tile_display: MediaTileDisplay,
|
||||
media_tiles: Vec<AsyncController<MediaTile>>,
|
||||
media_tiles: Vec<MediaCarouselItem>,
|
||||
pages: Vec<gtk::Box>,
|
||||
}
|
||||
|
||||
pub(crate) struct MediaCarouselInit {
|
||||
pub(crate) media: Vec<BaseItemDto>,
|
||||
pub(crate) media_tile_display: MediaTileDisplay,
|
||||
pub(crate) carousel_type: MediaCarouselType,
|
||||
pub(crate) api_client: Arc<ApiClient>,
|
||||
pub(crate) label: String,
|
||||
}
|
||||
@@ -142,15 +165,23 @@ impl Component for MediaCarousel {
|
||||
api_client,
|
||||
media,
|
||||
media_tile_display,
|
||||
carousel_type,
|
||||
label,
|
||||
} = init;
|
||||
|
||||
let media_tiles: Vec<AsyncController<MediaTile>> = media
|
||||
let media_tiles = media
|
||||
.iter()
|
||||
.map(|media| {
|
||||
MediaTile::builder()
|
||||
.launch((media.clone(), media_tile_display, api_client.clone()))
|
||||
.detach()
|
||||
.map(|media| match carousel_type {
|
||||
MediaCarouselType::Tiles => MediaCarouselItem::Tile(
|
||||
MediaTile::builder()
|
||||
.launch((media.clone(), media_tile_display, api_client.clone()))
|
||||
.detach(),
|
||||
),
|
||||
MediaCarouselType::Buttons => MediaCarouselItem::Button(
|
||||
MediaButton::builder()
|
||||
.launch((media.clone(), media_tile_display))
|
||||
.detach(),
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -192,7 +223,7 @@ impl Component for MediaCarousel {
|
||||
}
|
||||
self.pages.clear();
|
||||
|
||||
let media_tile_chunks: Vec<&[AsyncController<MediaTile>]> =
|
||||
let media_tile_chunks: Vec<&[MediaCarouselItem]> =
|
||||
self.media_tiles.chunks(tiles_per_page as usize).collect();
|
||||
|
||||
for chunk in media_tile_chunks {
|
||||
|
@@ -8,10 +8,12 @@ use relm4::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::jellyfin_api::{api::latest::GetNextUpOptions, api_client::ApiClient};
|
||||
use crate::jellyfin_api::{
|
||||
api::latest::GetNextUpOptions, api_client::ApiClient, models::user_view::UserView,
|
||||
};
|
||||
|
||||
use super::{
|
||||
media_carousel::{MediaCarousel, MediaCarouselInit},
|
||||
media_carousel::{MediaCarousel, MediaCarouselInit, MediaCarouselType},
|
||||
media_tile::MediaTileDisplay,
|
||||
};
|
||||
|
||||
@@ -20,11 +22,15 @@ enum MediaListContents {
|
||||
Carousel(Controller<MediaCarousel>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MediaListType {
|
||||
ContinueWatching,
|
||||
Latest(MediaListTypeLatestParams),
|
||||
NextUp,
|
||||
MyMedia {
|
||||
user_views: Vec<UserView>,
|
||||
small: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -78,7 +84,7 @@ impl MediaList {
|
||||
sender: &AsyncComponentSender<Self>,
|
||||
) -> Self {
|
||||
let api_client = Arc::clone(&init.api_client);
|
||||
let list_type = init.list_type;
|
||||
let list_type = &init.list_type;
|
||||
let label = init.label.clone();
|
||||
|
||||
let media = match list_type {
|
||||
@@ -94,16 +100,32 @@ impl MediaList {
|
||||
.get_next_up(GetNextUpOptions::default())
|
||||
.await
|
||||
.expect("Error getting continue watching."),
|
||||
|
||||
MediaListType::MyMedia {
|
||||
user_views,
|
||||
small: _,
|
||||
} => user_views
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|view| view.into())
|
||||
.collect(),
|
||||
};
|
||||
if media.is_empty() {
|
||||
sender
|
||||
.output(MediaListOutput::Empty(get_view_id(&list_type)))
|
||||
.output(MediaListOutput::Empty(get_view_id(list_type)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let media_tile_display = match list_type {
|
||||
MediaListType::ContinueWatching | MediaListType::NextUp => MediaTileDisplay::Wide,
|
||||
MediaListType::Latest(_) => MediaTileDisplay::Cover,
|
||||
MediaListType::MyMedia { small, .. } if *small => MediaTileDisplay::Buttons,
|
||||
MediaListType::MyMedia { .. } => MediaTileDisplay::CollectionWide,
|
||||
};
|
||||
|
||||
let carousel_type = match list_type {
|
||||
MediaListType::MyMedia { small, .. } if *small => MediaCarouselType::Buttons,
|
||||
_ => MediaCarouselType::Tiles,
|
||||
};
|
||||
|
||||
let contents = {
|
||||
@@ -111,6 +133,7 @@ impl MediaList {
|
||||
.launch(MediaCarouselInit {
|
||||
media,
|
||||
media_tile_display,
|
||||
carousel_type,
|
||||
api_client,
|
||||
label,
|
||||
})
|
||||
@@ -127,7 +150,9 @@ impl MediaList {
|
||||
|
||||
fn get_view_id(list_type: &MediaListType) -> Option<Uuid> {
|
||||
match list_type {
|
||||
MediaListType::ContinueWatching | MediaListType::NextUp => None,
|
||||
MediaListType::ContinueWatching | MediaListType::NextUp | MediaListType::MyMedia { .. } => {
|
||||
None
|
||||
}
|
||||
MediaListType::Latest(params) => Some(params.view_id),
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ where
|
||||
fetcher: MediaFetcher<F>,
|
||||
empty_component: Option<Controller<EmptyComponent>>,
|
||||
media_grid: Option<Controller<MediaGrid>>,
|
||||
media_tile_display: MediaTileDisplay,
|
||||
state: FetcherState,
|
||||
count: Option<FetcherCount>,
|
||||
}
|
||||
@@ -33,6 +34,7 @@ pub struct MediaPageInit<F, C: Component> {
|
||||
pub api_client: Arc<ApiClient>,
|
||||
pub fetcher: F,
|
||||
pub empty_component: Option<Controller<C>>,
|
||||
pub media_tile_display: Option<MediaTileDisplay>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -167,6 +169,7 @@ where
|
||||
api_client,
|
||||
fetcher,
|
||||
empty_component,
|
||||
media_tile_display,
|
||||
} = init;
|
||||
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
@@ -187,6 +190,7 @@ where
|
||||
fetcher,
|
||||
empty_component,
|
||||
media_grid: None,
|
||||
media_tile_display: media_tile_display.unwrap_or(MediaTileDisplay::Wide),
|
||||
state: FetcherState::Empty,
|
||||
count: None,
|
||||
};
|
||||
@@ -221,7 +225,7 @@ where
|
||||
let media_grid = MediaGrid::builder()
|
||||
.launch(MediaGridInit {
|
||||
media: display.items.clone(),
|
||||
media_tile_display: MediaTileDisplay::CoverLarge,
|
||||
media_tile_display: self.media_tile_display,
|
||||
api_client: self.api_client.clone(),
|
||||
})
|
||||
.detach();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
|
||||
use gdk::Texture;
|
||||
use jellyfin_api::types::BaseItemDto;
|
||||
use jellyfin_api::types::{BaseItemDto, BaseItemKind};
|
||||
use relm4::{
|
||||
gtk::{self, gdk, gdk_pixbuf, glib::markup_escape_text, prelude::*},
|
||||
prelude::{AsyncComponent, AsyncComponentParts},
|
||||
@@ -15,13 +15,15 @@ use crate::{
|
||||
utils::{item_name::ItemName, playable::get_next_playable_media},
|
||||
};
|
||||
|
||||
use super::LIBRARY_BROKER;
|
||||
use super::{LibraryInput, LIBRARY_BROKER};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum MediaTileDisplay {
|
||||
Cover,
|
||||
CoverLarge,
|
||||
Wide,
|
||||
CollectionWide,
|
||||
Buttons,
|
||||
}
|
||||
|
||||
impl MediaTileDisplay {
|
||||
@@ -30,6 +32,8 @@ impl MediaTileDisplay {
|
||||
Self::Cover => 133,
|
||||
Self::CoverLarge => 175,
|
||||
Self::Wide => 300,
|
||||
Self::CollectionWide => 300,
|
||||
Self::Buttons => 300,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +42,8 @@ impl MediaTileDisplay {
|
||||
Self::Cover => 200,
|
||||
Self::CoverLarge => 262,
|
||||
Self::Wide => 175,
|
||||
Self::CollectionWide => 175,
|
||||
Self::Buttons => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +73,7 @@ pub struct MediaTile {
|
||||
#[derive(Debug)]
|
||||
pub enum MediaTileInput {
|
||||
Play,
|
||||
ShowDetails,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -107,8 +114,10 @@ impl AsyncComponent for MediaTile {
|
||||
},
|
||||
|
||||
add_controller = gtk::EventControllerMotion {
|
||||
connect_enter[root] => move |_, _, _| {
|
||||
root.add_css_class("hover");
|
||||
connect_enter[root, media] => move |_, _, _| {
|
||||
if !matches!(media.type_, Some(BaseItemKind::CollectionFolder)) {
|
||||
root.add_css_class("hover");
|
||||
}
|
||||
},
|
||||
connect_leave[root] => move |_| {
|
||||
root.remove_css_class("hover");
|
||||
@@ -172,8 +181,8 @@ impl AsyncComponent for MediaTile {
|
||||
set_markup: &get_item_label(&model.media),
|
||||
|
||||
add_controller = gtk::GestureClick {
|
||||
connect_pressed => move |_, _, _, _| {
|
||||
APP_BROKER.send(AppInput::ShowDetails(media.clone()));
|
||||
connect_pressed[sender] => move |_, _, _, _| {
|
||||
sender.input(MediaTileInput::ShowDetails);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -214,14 +223,33 @@ impl AsyncComponent for MediaTile {
|
||||
) {
|
||||
match message {
|
||||
MediaTileInput::Play => {
|
||||
match get_next_playable_media(self.api_client.clone(), self.media.clone()).await {
|
||||
Some(media) => APP_BROKER.send(AppInput::PlayVideo(media)),
|
||||
match self.media.type_ {
|
||||
Some(BaseItemKind::CollectionFolder) => {
|
||||
APP_BROKER.send(AppInput::ShowCollection(self.media.clone()));
|
||||
}
|
||||
_ => {
|
||||
let mut message = "No playable media found".to_string();
|
||||
if let Some(name) = self.media.name.as_ref() {
|
||||
message += &format!(" for {name}");
|
||||
}
|
||||
LIBRARY_BROKER.send(super::LibraryInput::Toast(message))
|
||||
match get_next_playable_media(self.api_client.clone(), self.media.clone())
|
||||
.await
|
||||
{
|
||||
Some(media) => APP_BROKER.send(AppInput::PlayVideo(media)),
|
||||
_ => {
|
||||
let mut message = "No playable media found".to_string();
|
||||
if let Some(name) = self.media.name.as_ref() {
|
||||
message += &format!(" for {name}");
|
||||
}
|
||||
LIBRARY_BROKER.send(LibraryInput::Toast(message))
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
MediaTileInput::ShowDetails => {
|
||||
match self.media.type_ {
|
||||
Some(BaseItemKind::CollectionFolder) => {
|
||||
APP_BROKER.send(AppInput::ShowCollection(self.media.clone()));
|
||||
}
|
||||
_ => {
|
||||
APP_BROKER.send(AppInput::ShowDetails(self.media.clone()));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -252,6 +280,9 @@ async fn get_thumbnail(
|
||||
MediaTileDisplay::Cover | MediaTileDisplay::CoverLarge => {
|
||||
api_client.get_parent_or_item_thumbnail_url(media)
|
||||
}
|
||||
MediaTileDisplay::CollectionWide | MediaTileDisplay::Buttons => {
|
||||
api_client.get_collection_thumbnail_url(media)
|
||||
}
|
||||
};
|
||||
let img_url = match img_url {
|
||||
Ok(img_url) => img_url,
|
||||
@@ -274,7 +305,7 @@ async fn get_thumbnail(
|
||||
|
||||
let resized = gdk_pixbuf::Pixbuf::new(
|
||||
gdk_pixbuf::Colorspace::Rgb,
|
||||
false,
|
||||
true,
|
||||
8,
|
||||
tile_display.width(),
|
||||
tile_display.height(),
|
||||
|
@@ -1,7 +1,9 @@
|
||||
mod collection;
|
||||
pub mod collection;
|
||||
pub mod collections;
|
||||
mod home;
|
||||
mod home_sections;
|
||||
mod library_container;
|
||||
mod media_button;
|
||||
mod media_carousel;
|
||||
mod media_fetcher;
|
||||
mod media_grid;
|
||||
@@ -16,11 +18,7 @@ use relm4::{
|
||||
binding::BoolBinding,
|
||||
ComponentController, RelmObjectExt, SharedState,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use adw::prelude::*;
|
||||
use gtk::glib;
|
||||
@@ -31,9 +29,8 @@ use crate::{
|
||||
borgar::borgar_menu::{BorgarMenu, BorgarMenuAuth},
|
||||
config::{Account, Server},
|
||||
jellyfin_api::{
|
||||
api::views::UserView,
|
||||
api_client::ApiClient,
|
||||
models::{collection_type::CollectionType, display_preferences::DisplayPreferences},
|
||||
models::{display_preferences::DisplayPreferences, user_view::UserView},
|
||||
},
|
||||
media_details::MEDIA_DETAILS_REFRESH_QUEUED,
|
||||
tr,
|
||||
@@ -44,7 +41,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use self::{
|
||||
collection::{Collection, CollectionInput},
|
||||
collections::Collections,
|
||||
home::{Home, HomeInit},
|
||||
search::{
|
||||
search_bar::SearchBar,
|
||||
@@ -69,7 +66,7 @@ pub struct Library {
|
||||
state: LibraryState,
|
||||
search_results: Controller<SearchResults>,
|
||||
home: Option<Controller<Home>>,
|
||||
collections: HashMap<Uuid, Controller<Collection>>,
|
||||
collections: Option<Controller<Collections>>,
|
||||
searching: BoolBinding,
|
||||
// Store previous view stack child so we can go back from search
|
||||
previous_stack_child: Arc<RwLock<String>>,
|
||||
@@ -78,7 +75,6 @@ pub struct Library {
|
||||
#[derive(Debug)]
|
||||
pub enum LibraryInput {
|
||||
SetLibraryState(LibraryState),
|
||||
MediaSelected(BaseItemDto),
|
||||
Refresh,
|
||||
Shown,
|
||||
ViewStackChildVisible(String),
|
||||
@@ -291,7 +287,7 @@ impl Component for Library {
|
||||
state: LibraryState::Loading,
|
||||
search_results: SearchResults::builder().launch(api_client).detach(),
|
||||
home: None,
|
||||
collections: HashMap::default(),
|
||||
collections: None,
|
||||
searching: BoolBinding::default(),
|
||||
previous_stack_child: Arc::new(RwLock::new("home".into())),
|
||||
};
|
||||
@@ -363,11 +359,6 @@ impl Component for Library {
|
||||
}
|
||||
};
|
||||
}
|
||||
LibraryInput::MediaSelected(media) => {
|
||||
sender
|
||||
.output(LibraryOutput::PlayVideo(Box::new(media)))
|
||||
.unwrap();
|
||||
}
|
||||
LibraryInput::Refresh => {
|
||||
let view_stack = &widgets.view_stack;
|
||||
|
||||
@@ -378,8 +369,8 @@ impl Component for Library {
|
||||
if let Some(home) = self.home.take() {
|
||||
view_stack.remove(home.widget());
|
||||
}
|
||||
for (_id, collection) in self.collections.drain() {
|
||||
view_stack.remove(collection.widget());
|
||||
if let Some(collections) = self.collections.take() {
|
||||
view_stack.remove(collections.widget());
|
||||
}
|
||||
|
||||
self.initial_fetch(&sender);
|
||||
@@ -392,12 +383,6 @@ impl Component for Library {
|
||||
*LIBRARY_REFRESH_QUEUED.write() = false;
|
||||
}
|
||||
LibraryInput::ViewStackChildVisible(name) => {
|
||||
if let Ok(id) = Uuid::parse_str(&name) {
|
||||
if let Some(collection) = self.collections.get(&id) {
|
||||
collection.emit(CollectionInput::Visible);
|
||||
}
|
||||
}
|
||||
|
||||
if name != "search" {
|
||||
self.searching.set_value(false);
|
||||
}
|
||||
@@ -462,15 +447,18 @@ impl Library {
|
||||
}
|
||||
}
|
||||
|
||||
match tokio::try_join!(async { api_client.get_user_views().await }, async {
|
||||
api_client
|
||||
// We might eventually want client-specific settings, but for
|
||||
// now use the Jellyfin ("emby") client settings
|
||||
.get_user_display_preferences("emby")
|
||||
.await
|
||||
}) {
|
||||
match tokio::try_join!(
|
||||
async { api_client.get_user_views(None, None).await },
|
||||
async {
|
||||
api_client
|
||||
// We might eventually want client-specific settings, but for
|
||||
// now use the Jellyfin ("emby") client settings
|
||||
.get_user_display_preferences("emby")
|
||||
.await
|
||||
}
|
||||
) {
|
||||
Ok((user_views, display_preferences)) => {
|
||||
LibraryCommandOutput::LibraryLoaded(user_views, display_preferences)
|
||||
LibraryCommandOutput::LibraryLoaded(user_views.0, display_preferences)
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error loading library: {err}");
|
||||
@@ -506,34 +494,20 @@ impl Library {
|
||||
);
|
||||
self.home = Some(home);
|
||||
|
||||
let user_views: Vec<&UserView> = user_views
|
||||
.iter()
|
||||
.filter(|view| {
|
||||
matches!(
|
||||
view.collection_type,
|
||||
CollectionType::Movies | CollectionType::TvShows
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// TODO: handle overflow when user has too many collections
|
||||
// For now we limit them to 5, user can change order in Jellyfin settings
|
||||
for &view in user_views.iter().take(5) {
|
||||
let collection = Collection::builder()
|
||||
.launch((self.api_client.clone(), view.clone()))
|
||||
.detach();
|
||||
|
||||
view_stack.add_titled_with_icon(
|
||||
collection.widget(),
|
||||
Some(&view.id.to_string()),
|
||||
&view.name.clone(),
|
||||
&view.collection_type.icon(),
|
||||
);
|
||||
|
||||
self.collections.insert(view.id, collection);
|
||||
}
|
||||
|
||||
view_stack.set_visible_child_name("home");
|
||||
|
||||
let collections = Collections::builder()
|
||||
.launch(self.api_client.clone())
|
||||
.detach();
|
||||
|
||||
view_stack.add_titled_with_icon(
|
||||
collections.widget(),
|
||||
Some("collections"),
|
||||
"Collections",
|
||||
"library",
|
||||
);
|
||||
|
||||
self.collections = Some(collections);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,7 @@ use crate::{
|
||||
library::{
|
||||
media_fetcher::Fetcher,
|
||||
media_page::{MediaPage, MediaPageInit, MediaPageInput},
|
||||
media_tile::MediaTileDisplay,
|
||||
},
|
||||
tr,
|
||||
};
|
||||
@@ -78,6 +79,7 @@ impl Component for SearchResults {
|
||||
.launch(self.api_client.clone())
|
||||
.detach(),
|
||||
),
|
||||
media_tile_display: Some(MediaTileDisplay::CoverLarge),
|
||||
})
|
||||
.detach();
|
||||
|
||||
|
@@ -15,6 +15,7 @@ library-section-title =
|
||||
.latest-movies = Latest Movies
|
||||
.latest-shows = Latest Shows
|
||||
.latest-music = Latest Music
|
||||
.my-media = My Media
|
||||
library-episode-name-with-season-and-episode = S{ $seasonNumber }:E{ $episodeNumber } - { $episodeName }
|
||||
library-series-and-episode-name = { $seriesName } - { $episodeName }
|
||||
library-media-tile-unnamed-item = Unnamed Item
|
||||
@@ -23,3 +24,5 @@ library-search-empty =
|
||||
.title = Start typing to search
|
||||
.description = Or, try one of these suggestions:
|
||||
library-search-title = Results for “{ $searchText }”
|
||||
library-unnamed-collection = Unnamed Collection
|
||||
library-collection-refresh-button = Refresh collection
|
||||
|
Reference in New Issue
Block a user