509 lines
23 KiB
Rust
509 lines
23 KiB
Rust
use super::app_drawer::App_drawer;
|
|
use super::gen_components::{
|
|
empty_message, on_shownotes_click, person_episode_item, Search_nav, UseScrollToTop,
|
|
};
|
|
use super::virtual_list::PersonEpisodeVirtualList;
|
|
use crate::components::audio::on_play_pause;
|
|
use crate::components::audio::AudioPlayer;
|
|
use crate::components::context::{AppState, ExpandedDescriptions, UIState};
|
|
use crate::components::gen_funcs::sanitize_html_with_blank_target;
|
|
use crate::requests::people_req::PersonEpisode;
|
|
use crate::requests::people_req::{self, PersonSubscription};
|
|
use i18nrs::yew::use_translation;
|
|
use yew::prelude::*;
|
|
use yew::{function_component, html, Html};
|
|
use yew_router::history::BrowserHistory;
|
|
use yewdux::prelude::*;
|
|
// use crate::components::gen_funcs::check_auth;
|
|
use crate::components::gen_funcs::format_datetime;
|
|
use crate::components::gen_funcs::match_date_format;
|
|
use crate::components::gen_funcs::parse_date;
|
|
use std::rc::Rc;
|
|
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
#[derive(Clone, PartialEq, Properties, Debug)]
|
|
struct PersonWithEpisodes {
|
|
person: PersonSubscription,
|
|
episodes: Vec<PersonEpisode>,
|
|
is_expanded: bool,
|
|
}
|
|
|
|
#[function_component(SubscribedPeople)]
|
|
pub fn subscribed_people() -> Html {
|
|
let (i18n, _) = use_translation();
|
|
|
|
let (desc_state, desc_dispatch) = use_store::<ExpandedDescriptions>();
|
|
let active_modal = use_state(|| None::<i32>);
|
|
let show_modal = use_state(|| false);
|
|
let active_clonedal = active_modal.clone();
|
|
let active_modal_clone = active_modal.clone();
|
|
let on_modal_open = Callback::from(move |episode_id: i32| {
|
|
active_modal_clone.set(Some(episode_id));
|
|
});
|
|
let active_modal_clone = active_modal.clone();
|
|
let on_modal_close = Callback::from(move |_| {
|
|
active_modal_clone.set(None);
|
|
});
|
|
|
|
// let error = use_state(|| None);
|
|
let (post_state, post_dispatch) = use_store::<AppState>();
|
|
let (audio_state, audio_dispatch) = use_store::<UIState>();
|
|
let loading = use_state(|| true);
|
|
let expanded_state = use_state(|| std::collections::HashMap::<i32, bool>::new());
|
|
let subscribed_people = use_state(|| Vec::<PersonWithEpisodes>::new());
|
|
let api_key = post_state
|
|
.auth_details
|
|
.as_ref()
|
|
.map(|ud| ud.api_key.clone());
|
|
let user_id = post_state.user_details.as_ref().map(|ud| ud.UserID.clone());
|
|
let server_name = post_state
|
|
.auth_details
|
|
.as_ref()
|
|
.map(|ud| ud.server_name.clone());
|
|
|
|
// Effect to fetch subscriptions on component mount
|
|
// Effect to fetch subscriptions on component mount
|
|
//
|
|
//
|
|
{
|
|
let api_key = api_key.clone();
|
|
let user_id = user_id.clone();
|
|
let server_name = server_name.clone();
|
|
let subscribed_people = subscribed_people.clone();
|
|
let loading = loading.clone();
|
|
|
|
use_effect_with((), move |_| {
|
|
if let (Some(server), Some(Some(key)), Some(uid)) =
|
|
(server_name.clone(), api_key.clone(), user_id.clone())
|
|
{
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
match people_req::call_get_person_subscriptions(&server, &key, uid).await {
|
|
Ok(subscriptions) => {
|
|
let people = subscriptions
|
|
.into_iter()
|
|
.map(|sub| PersonWithEpisodes {
|
|
person: sub,
|
|
episodes: vec![],
|
|
is_expanded: false,
|
|
})
|
|
.collect();
|
|
subscribed_people.set(people);
|
|
}
|
|
Err(e) => {
|
|
log::error!("Failed to fetch subscriptions: {}", e);
|
|
}
|
|
}
|
|
loading.set(false);
|
|
});
|
|
} else {
|
|
loading.set(false);
|
|
}
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
{
|
|
let api_key = api_key.clone();
|
|
let user_id = user_id.clone();
|
|
let server_name = server_name.clone();
|
|
let subscribed_people = subscribed_people.clone();
|
|
let expanded_state = expanded_state.clone();
|
|
|
|
use_effect_with(expanded_state.clone(), move |expanded_state| {
|
|
let api_key = api_key.clone();
|
|
let server_name = server_name.clone();
|
|
let user_id = user_id.clone();
|
|
let subscribed_people = subscribed_people.clone();
|
|
|
|
if let (Some(server), Some(Some(key)), Some(uid)) = (server_name, api_key, user_id) {
|
|
let people = (*subscribed_people).clone();
|
|
|
|
// Move people IDs into the async block instead of the whole people Vec
|
|
let person_ids: Vec<_> = people
|
|
.iter()
|
|
.filter(|person| {
|
|
expanded_state
|
|
.get(&person.person.personid)
|
|
.copied()
|
|
.unwrap_or(false)
|
|
&& person.episodes.is_empty()
|
|
})
|
|
.map(|person| (person.person.personid, person.person.name.clone()))
|
|
.collect();
|
|
|
|
for (person_id, person_name) in person_ids {
|
|
let server = server.clone();
|
|
let key = key.clone();
|
|
let subscribed_people = subscribed_people.clone();
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
// In the effect that loads episodes
|
|
match people_req::call_get_person_episodes(&server, &key, uid, person_id)
|
|
.await
|
|
{
|
|
Ok(new_episodes) => {
|
|
subscribed_people.set(
|
|
(*subscribed_people)
|
|
.clone()
|
|
.into_iter()
|
|
.map(|mut p| {
|
|
if p.person.personid == person_id {
|
|
web_sys::console::log_1(
|
|
&format!(
|
|
"Setting episodes for person {}: {:?}",
|
|
person_id, new_episodes
|
|
)
|
|
.into(),
|
|
);
|
|
p.episodes = new_episodes.clone();
|
|
}
|
|
p
|
|
})
|
|
.collect(),
|
|
);
|
|
// Log the updated state
|
|
web_sys::console::log_1(
|
|
&format!("Updated people state: {:?}", *subscribed_people)
|
|
.into(),
|
|
);
|
|
}
|
|
Err(e) => {
|
|
log::error!(
|
|
"Failed to fetch episodes for person {}: {}",
|
|
person_name,
|
|
e
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|| ()
|
|
});
|
|
}
|
|
let toggle_person = {
|
|
let expanded_state = expanded_state.clone();
|
|
Callback::from(move |person_id: i32| {
|
|
let mut new_state = (*expanded_state).clone();
|
|
let current_state = new_state.get(&person_id).copied().unwrap_or(false);
|
|
new_state.insert(person_id, !current_state);
|
|
expanded_state.set(new_state);
|
|
})
|
|
};
|
|
|
|
let people = (*subscribed_people).clone();
|
|
let render_audio = audio_state.clone();
|
|
let render_people = {
|
|
let people = people.clone();
|
|
let active_clonedal = active_clonedal.clone();
|
|
let no_people_found = i18n.t("people_subs.no_subscribed_people_found").to_string();
|
|
let subscribe_message = i18n.t("people_subs.subscribe_to_hosts_message").to_string();
|
|
let episode_count_text = i18n.t("people_subs.episode_count").to_string();
|
|
let shows_text = i18n.t("people_subs.shows").to_string();
|
|
let avatar_alt_text = i18n.t("people_subs.avatar_alt").to_string();
|
|
|
|
move || {
|
|
if people.is_empty() {
|
|
html! {
|
|
{ empty_message(
|
|
&no_people_found,
|
|
&subscribe_message
|
|
)}
|
|
}
|
|
} else {
|
|
html! {
|
|
<div>
|
|
{
|
|
people.into_iter().map(|person| {
|
|
let active_modal = active_clonedal.clone();
|
|
let is_expanded = *expanded_state.get(&person.person.personid).unwrap_or(&false);
|
|
html! {
|
|
<div key={person.person.personid}>
|
|
{ render_host_with_episodes(
|
|
&person.person,
|
|
person.episodes.clone(),
|
|
is_expanded,
|
|
toggle_person.reform(move |_| person.person.personid),
|
|
post_state.clone(),
|
|
post_dispatch.clone(),
|
|
render_audio.clone(),
|
|
desc_state.clone(),
|
|
desc_dispatch.clone(),
|
|
audio_dispatch.clone(),
|
|
*show_modal,
|
|
on_modal_open.clone(),
|
|
on_modal_close.clone(),
|
|
active_modal,
|
|
&episode_count_text,
|
|
&shows_text,
|
|
&avatar_alt_text,
|
|
)}
|
|
</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
html! {
|
|
<>
|
|
<div class="main-container">
|
|
<Search_nav />
|
|
<UseScrollToTop />
|
|
{
|
|
if *loading {
|
|
html! {
|
|
<div class="loading-animation">
|
|
<div class="frame1"></div>
|
|
<div class="frame2"></div>
|
|
<div class="frame3"></div>
|
|
<div class="frame4"></div>
|
|
<div class="frame5"></div>
|
|
<div class="frame6"></div>
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {
|
|
<div>
|
|
<h1 class="text-2xl item_container-text font-bold text-center mb-6">{&i18n.t("people_subs.subscribed_people")}</h1>
|
|
{ render_people() }
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
{
|
|
if let Some(audio_props) = &audio_state.currently_playing {
|
|
html! {
|
|
<AudioPlayer
|
|
src={audio_props.src.clone()}
|
|
title={audio_props.title.clone()}
|
|
description={audio_props.description.clone()}
|
|
release_date={audio_props.release_date.clone()}
|
|
artwork_url={audio_props.artwork_url.clone()}
|
|
duration={audio_props.duration.clone()}
|
|
episode_id={audio_props.episode_id.clone()}
|
|
duration_sec={audio_props.duration_sec.clone()}
|
|
start_pos_sec={audio_props.start_pos_sec.clone()}
|
|
end_pos_sec={audio_props.end_pos_sec.clone()}
|
|
offline={audio_props.offline.clone()}
|
|
is_youtube={audio_props.is_youtube.clone()}
|
|
/>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}
|
|
}
|
|
</div>
|
|
<App_drawer />
|
|
</>
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn get_proxied_image_url(server_name: &str, original_url: String) -> String {
|
|
let proxied_url = format!(
|
|
"{}/api/proxy/image?url={}",
|
|
server_name,
|
|
urlencoding::encode(&original_url)
|
|
);
|
|
web_sys::console::log_1(&format!("Proxied URL: {}", proxied_url).into());
|
|
proxied_url
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn render_host_with_episodes(
|
|
person: &PersonSubscription,
|
|
episodes: Vec<PersonEpisode>,
|
|
is_expanded: bool,
|
|
toggle_host_expanded: Callback<MouseEvent>,
|
|
state: Rc<AppState>,
|
|
dispatch: Dispatch<AppState>,
|
|
audio_state: Rc<UIState>,
|
|
desc_rc: Rc<ExpandedDescriptions>,
|
|
desc_state: Dispatch<ExpandedDescriptions>,
|
|
audio_dispatch: Dispatch<UIState>,
|
|
_show_modal: bool,
|
|
on_modal_open: Callback<i32>,
|
|
on_modal_close: Callback<MouseEvent>,
|
|
active_modal: UseStateHandle<Option<i32>>,
|
|
episode_count_text: &str,
|
|
shows_text: &str,
|
|
avatar_alt_text: &str,
|
|
) -> Html {
|
|
let _episode_count = episodes.len();
|
|
let history_clone = BrowserHistory::new();
|
|
let api_key = state.auth_details.as_ref().map(|ud| ud.api_key.clone());
|
|
let user_id = state.user_details.as_ref().map(|ud| ud.UserID.clone());
|
|
let server_name = state.auth_details.as_ref().map(|ud| ud.server_name.clone());
|
|
let proxied_url = get_proxied_image_url(&server_name.clone().unwrap(), person.image.clone());
|
|
|
|
let handle_click = {
|
|
let toggle_host_expanded = toggle_host_expanded.clone();
|
|
Callback::from(move |e: MouseEvent| {
|
|
e.stop_propagation(); // Stop event propagation
|
|
e.prevent_default(); // Prevent default behavior
|
|
toggle_host_expanded.emit(e);
|
|
})
|
|
};
|
|
|
|
html! {
|
|
<div key={person.personid}>
|
|
<div class="item-container border-solid border flex items-start mb-4 shadow-md rounded-lg h-full" onclick={handle_click}>
|
|
<div class="flex flex-col w-auto object-cover pl-4">
|
|
<img
|
|
src={format!("{}", proxied_url)}
|
|
alt={format!("{} {}", avatar_alt_text, person.name)}
|
|
class="person-avatar"
|
|
/>
|
|
</div>
|
|
<div class="flex flex-col p-4 space-y-2 flex-grow md:w-7/12">
|
|
<p class="item_container-text episode-title font-semibold cursor-pointer">
|
|
{ &person.name }
|
|
</p>
|
|
<hr class="my-2 border-t hidden md:block"/>
|
|
<p class="item_container-text">{ format!("{}: {}", episode_count_text, person.episode_count) }</p>
|
|
<p class="item_container-text text-sm">{ format!("{}: {}", shows_text, person.associatedpodcasts) }</p>
|
|
</div>
|
|
</div>
|
|
{ if is_expanded {
|
|
let episode_count = episodes.len();
|
|
web_sys::console::log_1(&format!("Attempting to render {} episodes", episode_count).into());
|
|
html! {
|
|
<div class="episodes-dropdown pl-4">
|
|
<PersonEpisodeVirtualList
|
|
episodes={episodes.clone()}
|
|
item_height={234.0}
|
|
search_state={state.clone()}
|
|
search_ui_state={audio_state.clone()}
|
|
dispatch={audio_dispatch.clone()}
|
|
search_dispatch={dispatch.clone()}
|
|
history={history_clone.clone()}
|
|
server_name={server_name.clone()}
|
|
user_id={user_id.clone()}
|
|
api_key={api_key.clone()}
|
|
/>
|
|
// Old episode rendering code (disabled for virtual scrolling)
|
|
{ if false {
|
|
let _ = episodes.iter().map(|episode| {
|
|
let id_string = episode.episodeid.to_string();
|
|
let desc_expanded = desc_rc.expanded_descriptions.contains(&id_string);
|
|
let episode_url_for_closure = episode.episodeurl.clone();
|
|
let episode_title_for_closure = episode.episodetitle.clone();
|
|
let episode_description_for_closure = episode.episodedescription.clone();
|
|
let episode_artwork_for_closure = episode.episodeartwork.clone();
|
|
let episode_duration_for_closure = episode.episodeduration.clone();
|
|
let listener_duration_for_closure = episode.listenduration.clone();
|
|
let episode_id_for_closure = episode.episodeid.clone();
|
|
let episode_is_youtube = Some(episode.is_youtube.clone());
|
|
let _completed = false;
|
|
let user_id_play = user_id.clone();
|
|
let server_name_play = server_name.clone();
|
|
let api_key_play = api_key.clone();
|
|
let audio_dispatch = audio_dispatch.clone();
|
|
|
|
let is_current_episode = audio_state
|
|
.currently_playing
|
|
.as_ref()
|
|
.map_or(false, |current| current.episode_id == episode.episodeid);
|
|
let is_playing = audio_state.audio_playing.unwrap_or(false);
|
|
|
|
|
|
let date_format = match_date_format(state.date_format.as_deref());
|
|
let datetime = parse_date(&episode.episodepubdate, &state.user_tz);
|
|
let format_release = format!(
|
|
"{}",
|
|
format_datetime(&datetime, &state.hour_preference, date_format)
|
|
);
|
|
|
|
let on_play_pause = on_play_pause(
|
|
episode_url_for_closure.clone(),
|
|
episode_title_for_closure.clone(),
|
|
episode_description_for_closure.clone(),
|
|
format_release.clone(),
|
|
episode_artwork_for_closure.clone().unwrap(),
|
|
episode_duration_for_closure.clone(),
|
|
episode_id_for_closure.clone(),
|
|
Some(listener_duration_for_closure.clone()),
|
|
api_key_play.unwrap().unwrap(),
|
|
user_id_play.unwrap(),
|
|
server_name_play.unwrap(),
|
|
audio_dispatch.clone(),
|
|
audio_state.clone(),
|
|
None,
|
|
episode_is_youtube.clone(),
|
|
);
|
|
|
|
let on_shownotes_click = on_shownotes_click(
|
|
history_clone.clone(),
|
|
dispatch.clone(),
|
|
Some(episode_id_for_closure.clone()),
|
|
Some(String::from("Not needed")),
|
|
Some(String::from("Not needed")),
|
|
Some(String::from("Not needed")),
|
|
true,
|
|
Some(true),
|
|
Some(false),
|
|
);
|
|
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
#[wasm_bindgen(js_namespace = window)]
|
|
fn toggleDescription(guid: &str, expanded: bool);
|
|
}
|
|
let toggle_expanded = {
|
|
let desc_dispatch = desc_state.clone();
|
|
let episode_guid = episode.episodeid.clone().to_string();
|
|
|
|
Callback::from(move |_: MouseEvent| {
|
|
let guid = episode_guid.clone();
|
|
desc_dispatch.reduce_mut(move |state| {
|
|
if state.expanded_descriptions.contains(&guid) {
|
|
state.expanded_descriptions.remove(&guid); // Collapse the description
|
|
toggleDescription(&guid, false); // Call JavaScript function
|
|
} else {
|
|
state.expanded_descriptions.insert(guid.clone()); // Expand the description
|
|
toggleDescription(&guid, true); // Call JavaScript function
|
|
}
|
|
});
|
|
})
|
|
};
|
|
let show_modal = *active_modal == Some(episode.episodeid);
|
|
person_episode_item(
|
|
Box::new(episode.clone()),
|
|
sanitize_html_with_blank_target(&episode.episodedescription),
|
|
desc_expanded,
|
|
&format_release,
|
|
on_play_pause,
|
|
on_shownotes_click,
|
|
toggle_expanded,
|
|
episode.episodeduration,
|
|
Some(episode.listenduration),
|
|
"people",
|
|
Callback::noop(),
|
|
false,
|
|
episode.episodeurl.clone(),
|
|
false,
|
|
show_modal,
|
|
on_modal_open.clone(),
|
|
on_modal_close.clone(),
|
|
is_current_episode,
|
|
is_playing,
|
|
)
|
|
}).collect::<Vec<_>>();
|
|
html! {}
|
|
} else {
|
|
html! {}
|
|
} }
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}}
|
|
</div>
|
|
}
|
|
}
|