use super::app_drawer::App_drawer; use super::gen_components::{FallbackImage, Search_nav, UseScrollToTop}; use crate::components::audio::AudioPlayer; use crate::components::click_events::create_on_title_click; use crate::components::context::{AppState, UIState}; use crate::components::gen_funcs::{ format_error_message, get_default_sort_direction, get_filter_preference, set_filter_preference, }; use crate::components::host_component::HostDropdown; use crate::components::podcast_layout::ClickedFeedURL; use crate::components::virtual_list::PodcastEpisodeVirtualList; use crate::requests::pod_req::{ call_add_category, call_add_podcast, call_adjust_skip_times, call_bulk_download_episodes, call_bulk_mark_episodes_completed, call_bulk_queue_episodes, call_bulk_save_episodes, call_check_podcast, call_clear_playback_speed, call_download_all_podcast, call_enable_auto_download, call_fetch_podcasting_2_pod_data, call_get_auto_download_status, call_get_feed_cutoff_days, call_get_merged_podcasts, call_get_play_episode_details, call_get_podcast_details, call_get_podcast_id_from_ep, call_get_podcast_id_from_ep_name, call_get_podcast_notifications_status, call_get_podcasts, call_get_rss_key, call_merge_podcasts, call_remove_category, call_remove_podcasts_name, call_remove_youtube_channel, call_set_playback_speed, call_toggle_podcast_notifications, call_unmerge_podcast, call_update_feed_cutoff_days, call_update_podcast_info, AddCategoryRequest, AutoDownloadRequest, BulkEpisodeActionRequest, ClearPlaybackSpeedRequest, DownloadAllPodcastRequest, FetchPodcasting2PodDataRequest, PlaybackSpeedRequest, PodcastDetails, PodcastValues, RemoveCategoryRequest, RemovePodcastValuesName, RemoveYouTubeChannelValues, SkipTimesRequest, UpdateFeedCutoffDaysRequest, }; use crate::requests::search_pods::call_get_podcast_details_dynamic; use crate::requests::search_pods::call_get_podcast_episodes; use crate::requests::setting_reqs::{ call_get_podcast_cover_preference, call_set_global_podcast_cover_preference, }; use htmlentity::entity::decode; use htmlentity::entity::ICodedDataTrait; use i18nrs::yew::use_translation; use std::collections::{HashMap, HashSet}; use std::rc::Rc; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; use web_sys::Element; use web_sys::{console, window, Event, HtmlInputElement, MouseEvent, UrlSearchParams}; use yew::prelude::*; use yew::Properties; use yew::{function_component, html, use_effect_with, use_node_ref, Callback, Html, TargetCast}; use yew_router::history::{BrowserHistory, History}; use yewdux::prelude::*; #[allow(dead_code)] fn add_icon() -> Html { html! { } } #[allow(dead_code)] fn payments_icon() -> Html { html! { } } #[allow(dead_code)] fn rss_icon() -> Html { html! { } } #[allow(dead_code)] fn website_icon() -> Html { html! { } } #[allow(dead_code)] fn trash_icon() -> Html { html! { } } #[allow(dead_code)] fn settings_icon() -> Html { html! { } } #[allow(dead_code)] fn download_icon() -> Html { html! { } } #[allow(dead_code)] fn no_icon() -> Html { html! {} } #[allow(dead_code)] fn play_icon() -> Html { html! { } } #[allow(dead_code)] fn pause_icon() -> Html { html! { } } #[derive(Properties, PartialEq)] pub struct Props { pub html: String, } #[allow(dead_code)] fn sanitize_html(html: &str) -> String { let cleaned_html = ammonia::clean(html); let decoded_data = decode(cleaned_html.as_bytes()); match decoded_data.to_string() { Ok(decoded_html) => decoded_html, Err(_) => String::from("Invalid HTML content"), } } #[allow(dead_code)] fn get_rss_base_url() -> String { let window = window().expect("no global `window` exists"); let location = window.location(); let current_url = location .href() .unwrap_or_else(|_| "Unable to retrieve URL".to_string()); if let Some(storage) = window.local_storage().ok().flatten() { if let Ok(Some(auth_state)) = storage.get_item("userAuthState") { if let Ok(json) = serde_json::from_str::(&auth_state) { if let Some(server_name) = json .get("auth_details") .and_then(|auth| auth.get("server_name")) .and_then(|name| name.as_str()) { return format!("{}/rss", server_name); } } } } // Fallback to using the current URL's origin format!( "{}/rss", current_url.split('/').take(3).collect::>().join("/") ) } #[allow(dead_code)] pub enum AppStateMsg { ExpandEpisode(String), CollapseEpisode(String), } impl Reducer for AppStateMsg { fn apply(self, mut state: Rc) -> Rc { let state_mut = Rc::make_mut(&mut state); match self { AppStateMsg::ExpandEpisode(guid) => { state_mut.expanded_descriptions.insert(guid); } AppStateMsg::CollapseEpisode(guid) => { state_mut.expanded_descriptions.remove(&guid); } } // Return the Rc itself, not a reference to it state } } #[derive(Clone, PartialEq)] #[allow(dead_code)] pub enum EpisodeSortDirection { NewestFirst, OldestFirst, ShortestFirst, LongestFirst, TitleAZ, TitleZA, } #[derive(Properties, PartialEq)] pub struct PodcastMergeSelectorProps { pub selected_podcasts: Vec, pub on_select: Callback>, pub available_podcasts: Vec, pub loading: bool, } #[function_component(PodcastMergeSelector)] pub fn podcast_merge_selector(props: &PodcastMergeSelectorProps) -> Html { let (_i18n, _) = use_translation(); let is_open = use_state(|| false); let dropdown_ref = use_node_ref(); // Handle clicking outside to close dropdown { let is_open = is_open.clone(); let dropdown_ref = dropdown_ref.clone(); use_effect_with(dropdown_ref.clone(), move |dropdown_ref| { let document = web_sys::window().unwrap().document().unwrap(); let dropdown_element = dropdown_ref.cast::(); let listener = wasm_bindgen::closure::Closure::wrap(Box::new(move |event: web_sys::Event| { if let Some(target) = event.target() { if let Some(dropdown) = &dropdown_element { if let Ok(node) = target.dyn_into::() { if !dropdown.contains(Some(&node)) { is_open.set(false); } } } } }) as Box); document .add_event_listener_with_callback("click", listener.as_ref().unchecked_ref()) .unwrap(); move || { document .remove_event_listener_with_callback("click", listener.as_ref().unchecked_ref()) .unwrap(); } }); } let toggle_dropdown = { let is_open = is_open.clone(); Callback::from(move |e: MouseEvent| { e.stop_propagation(); is_open.set(!*is_open); }) }; let toggle_podcast_selection = { let selected = props.selected_podcasts.clone(); let on_select = props.on_select.clone(); Callback::from(move |podcast_id: i32| { let mut new_selection = selected.clone(); if let Some(pos) = new_selection.iter().position(|&id| id == podcast_id) { new_selection.remove(pos); } else { new_selection.push(podcast_id); } on_select.emit(new_selection); }) }; let stop_propagation = Callback::from(|e: MouseEvent| { e.stop_propagation(); }); html! {
if *is_open && !props.loading { }
} } #[function_component(EpisodeLayout)] pub fn episode_layout() -> Html { let (i18n, _) = use_translation(); let is_added = use_state(|| false); let (state, _dispatch) = use_store::(); let (search_state, _search_dispatch) = use_store::(); let podcast_feed_results = search_state.podcast_feed_results.clone(); let clicked_podcast_info = search_state.clicked_podcast_info.clone(); // Capture i18n strings before they get moved - this is a large component with many strings let i18n_youtube_channel_successfully_removed = i18n .t("episodes_layout.youtube_channel_successfully_removed") .to_string(); let i18n_podcast_successfully_removed = i18n .t("episodes_layout.podcast_successfully_removed") .to_string(); let i18n_failed_to_remove_youtube_channel = i18n .t("episodes_layout.failed_to_remove_youtube_channel") .to_string(); let i18n_failed_to_remove_podcast = i18n .t("episodes_layout.failed_to_remove_podcast") .to_string(); let i18n_playback_speed_updated = i18n.t("episodes_layout.playback_speed_updated").to_string(); let i18n_error_updating_playback_speed = i18n .t("episodes_layout.error_updating_playback_speed") .to_string(); let i18n_podcast_successfully_added = i18n .t("episodes_layout.podcast_successfully_added") .to_string(); let i18n_failed_to_add_podcast = i18n.t("episodes_layout.failed_to_add_podcast").to_string(); let i18n_no_categories_available = i18n .t("episodes_layout.no_categories_available") .to_string(); // Additional i18n strings used throughout the component let i18n_category_name_cannot_be_empty = i18n .t("episodes_layout.category_name_cannot_be_empty") .to_string(); let i18n_loading_rss_key = i18n.t("episodes_layout.loading_rss_key").to_string(); let i18n_rss_feed_url = i18n.t("episodes_layout.rss_feed_url").to_string(); let i18n_rss_feed_note = i18n.t("episodes_layout.rss_feed_note").to_string(); let i18n_rss_feed_instruction = i18n.t("episodes_layout.rss_feed_instruction").to_string(); let i18n_rss_feed_warning = i18n.t("episodes_layout.rss_feed_warning").to_string(); let i18n_download_future_episodes = i18n .t("episodes_layout.download_future_episodes") .to_string(); let i18n_get_notifications_new_episodes = i18n .t("episodes_layout.get_notifications_new_episodes") .to_string(); let i18n_default_playback_speed = i18n.t("episodes_layout.default_playback_speed").to_string(); let i18n_playback_speed_description = i18n .t("episodes_layout.playback_speed_description") .to_string(); let i18n_auto_skip_intros_outros = i18n .t("episodes_layout.auto_skip_intros_outros") .to_string(); let i18n_start_skip_seconds = i18n.t("episodes_layout.start_skip_seconds").to_string(); let i18n_end_skip_seconds = i18n.t("episodes_layout.end_skip_seconds").to_string(); let i18n_youtube_download_limit = i18n.t("episodes_layout.youtube_download_limit").to_string(); let i18n_youtube_limit_description = i18n .t("episodes_layout.youtube_limit_description") .to_string(); let i18n_adjust_podcast_categories = i18n .t("episodes_layout.adjust_podcast_categories") .to_string(); let i18n_loading = i18n.t("episodes_layout.loading").to_string(); let i18n_new_category_placeholder = i18n .t("episodes_layout.new_category_placeholder") .to_string(); let i18n_download_all_confirmation = i18n .t("episodes_layout.download_all_confirmation") .to_string(); let i18n_yes_download_all = i18n.t("episodes_layout.yes_download_all").to_string(); let i18n_no_take_me_back = i18n.t("episodes_layout.no_take_me_back").to_string(); let i18n_delete_podcast_confirmation = i18n .t("episodes_layout.delete_podcast_confirmation") .to_string(); let i18n_yes_delete_podcast = i18n.t("episodes_layout.yes_delete_podcast").to_string(); let i18n_show_only = i18n.t("episodes_layout.show_only").to_string(); let i18n_showing_only_completed = i18n.t("episodes_layout.showing_only_completed").to_string(); let i18n_hide = i18n.t("episodes_layout.hide").to_string(); let i18n_hiding_completed = i18n.t("episodes_layout.hiding_completed").to_string(); let i18n_all = i18n.t("episodes_layout.all").to_string(); let i18n_showing_all_episodes = i18n.t("episodes_layout.showing_all_episodes").to_string(); let i18n_episode_count = i18n.t("episodes_layout.episode_count").to_string(); let i18n_authors = i18n.t("episodes_layout.authors").to_string(); let i18n_explicit = i18n.t("episodes_layout.explicit").to_string(); let i18n_yes = i18n.t("episodes_layout.yes").to_string(); let i18n_no = i18n.t("episodes_layout.no").to_string(); let i18n_search_episodes_placeholder = i18n .t("episodes_layout.search_episodes_placeholder") .to_string(); let i18n_newest_first = i18n.t("episodes_layout.newest_first").to_string(); let i18n_oldest_first = i18n.t("episodes_layout.oldest_first").to_string(); let i18n_shortest_first = i18n.t("episodes_layout.shortest_first").to_string(); let i18n_longest_first = i18n.t("episodes_layout.longest_first").to_string(); let i18n_title_az = i18n.t("episodes_layout.title_az").to_string(); let i18n_title_za = i18n.t("episodes_layout.title_za").to_string(); let i18n_clear_all = i18n.t("episodes_layout.clear_all").to_string(); let i18n_in_progress = i18n.t("episodes_layout.in_progress").to_string(); let i18n_exit_select = i18n.t("episodes_layout.exit_select").to_string(); let i18n_select = i18n.t("episodes_layout.select").to_string(); let i18n_deselect_all = i18n.t("episodes_layout.deselect_all").to_string(); let i18n_select_all = i18n.t("episodes_layout.select_all").to_string(); let i18n_select_unplayed = i18n.t("episodes_layout.select_unplayed").to_string(); let i18n_mark_complete = i18n.t("episodes_layout.mark_complete").to_string(); let i18n_queue_episodes = i18n.t("episodes_layout.queue_episodes").to_string(); let i18n_download_episodes = i18n.t("episodes_layout.download_episodes").to_string(); let i18n_no_episodes_found = i18n.t("episodes_layout.no_episodes_found").to_string(); let i18n_no_episodes_description = i18n .t("episodes_layout.no_episodes_description") .to_string(); let i18n_youtube_episode_limit_updated = i18n .t("episodes_layout.youtube_episode_limit_updated") .to_string(); let i18n_skip_times_adjusted = i18n.t("episodes_layout.skip_times_adjusted").to_string(); let i18n_error_adjusting_skip_times = i18n .t("episodes_layout.error_adjusting_skip_times") .to_string(); let i18n_playback_speed_reset_default = i18n .t("episodes_layout.playback_speed_reset_default") .to_string(); let i18n_error_resetting_playback_speed = i18n .t("episodes_layout.error_resetting_playback_speed") .to_string(); let loading = use_state(|| true); let page_state = use_state(|| PageState::Hidden); let episode_search_term = use_state(|| String::new()); // Initialize sort direction - will be updated when podcast_id changes let episode_sort_direction = use_state(|| Some(EpisodeSortDirection::NewestFirst)); let completed_filter_state = use_state(|| CompletedFilter::ShowAll); let show_in_progress = use_state(|| false); let notification_status = use_state(|| false); let feed_cutoff_days = use_state(|| 0); let feed_cutoff_days_input = use_state(|| "0".to_string()); let playback_speed = use_state(|| 1.0); let use_podcast_covers = use_state(|| false); let playback_speed_input = playback_speed.clone(); let playback_speed_clone = playback_speed.clone(); let rss_key_state = use_state(|| None::); // Bulk selection state let selected_episodes = use_state(|| HashSet::::new()); let is_selecting = use_state(|| false); let history = BrowserHistory::new(); // let node_ref = use_node_ref(); let user_id = search_state .user_details .as_ref() .map(|ud| ud.UserID.clone()); let api_key = search_state .auth_details .as_ref() .map(|ud| ud.api_key.clone()); let server_name = search_state .auth_details .as_ref() .map(|ud| ud.server_name.clone()); let podcast_added = search_state.podcast_added.unwrap_or_default(); let pod_url = use_state(|| String::new()); let new_category = use_state(|| String::new()); // Edit podcast form state let edit_feed_url = use_state(|| String::new()); let edit_username = use_state(|| String::new()); let edit_password = use_state(|| String::new()); let edit_podcast_name = use_state(|| String::new()); let edit_description = use_state(|| String::new()); let edit_author = use_state(|| String::new()); let edit_artwork_url = use_state(|| String::new()); let edit_website_url = use_state(|| String::new()); let edit_podcast_index_id = use_state(|| String::new()); // Merge podcast state let selected_podcasts_to_merge = use_state(|| Vec::::new()); let available_podcasts_for_merge = use_state(|| Vec::::new()); let current_merged_podcasts = use_state(|| Vec::::new()); let merged_podcast_details = use_state(|| HashMap::::new()); let loading_merge_data = use_state(|| false); // Pre-populate edit form when modal opens { let edit_feed_url = edit_feed_url.clone(); let edit_username = edit_username.clone(); let edit_password = edit_password.clone(); let edit_podcast_name = edit_podcast_name.clone(); let edit_description = edit_description.clone(); let edit_author = edit_author.clone(); let edit_artwork_url = edit_artwork_url.clone(); let edit_website_url = edit_website_url.clone(); let edit_podcast_index_id = edit_podcast_index_id.clone(); let clicked_podcast_info = clicked_podcast_info.clone(); let page_state = page_state.clone(); use_effect_with( (page_state.clone(), clicked_podcast_info.clone()), move |(page_state, podcast_info)| { if **page_state == PageState::EditPodcast { if let Some(info) = podcast_info { edit_feed_url.set(info.feedurl.clone()); edit_username.set(String::new()); // Username not available in current podcast info edit_password.set(String::new()); // Password not available in current podcast info edit_podcast_name.set(info.podcastname.clone()); edit_description.set(info.description.clone()); edit_author.set(info.author.clone()); edit_artwork_url.set(info.artworkurl.clone()); edit_website_url.set(info.websiteurl.clone()); edit_podcast_index_id.set(info.podcastindexid.to_string()); } } }, ); } let new_cat_in = new_category.clone(); let new_category_input = Callback::from(move |e: InputEvent| { if let Some(input_element) = e.target_dyn_into::() { let value = input_element.value(); // Get the value as a String new_cat_in.set(value); // Set the state with the String } }); // Add this near the start of the component let audio_dispatch = _dispatch.clone(); // Clear podcast metadata when component mounts use_effect_with((), move |_| { audio_dispatch.reduce_mut(|state| { state.podcast_value4value = None; state.podcast_funding = None; state.podcast_podroll = None; state.podcast_people = None; }); || () }); { let audio_dispatch = _dispatch.clone(); // Initial check when the component is mounted { let window = window().unwrap(); let width = window.inner_width().unwrap().as_f64().unwrap(); let new_is_mobile = width < 768.0; audio_dispatch.reduce_mut(|state| state.is_mobile = Some(new_is_mobile)); } // Resize event listener use_effect_with((), move |_| { let window = window().unwrap(); let closure_window = window.clone(); let closure = Closure::wrap(Box::new(move || { let width = closure_window.inner_width().unwrap().as_f64().unwrap(); let new_is_mobile = width < 768.0; audio_dispatch.reduce_mut(|state| state.is_mobile = Some(new_is_mobile)); }) as Box); window .add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref()) .unwrap(); closure.forget(); // Ensure the closure is not dropped prematurely || () }); } // On mount, check if the podcast is in the database let effect_user_id = user_id.clone(); let effect_api_key = api_key.clone(); let loading_ep = loading.clone(); { let is_added = is_added.clone(); let podcast = clicked_podcast_info.clone(); let user_id = effect_user_id.clone(); let api_key = effect_api_key.clone(); let server_name = server_name.clone(); let click_dispatch = _search_dispatch.clone(); let click_history = history.clone(); let pod_load_url = pod_url.clone(); let pod_loading_ep = loading.clone(); fn emit_click(callback: Callback) { callback.emit(MouseEvent::new("click").unwrap()); } use_effect_with( (api_key.clone(), user_id.clone(), server_name.clone()), move |(api_key, user_id, server_name)| { if let (Some(api_key), Some(user_id), Some(server_name)) = (api_key.clone(), user_id.clone(), server_name.clone()) { let is_added = is_added.clone(); if podcast.is_none() { let window = web_sys::window().expect("no global window exists"); let search_params = window.location().search().unwrap(); let url_params = UrlSearchParams::new_with_str(&search_params).unwrap(); let podcast_title = url_params.get("podcast_title").unwrap_or_default(); let podcast_url = url_params.get("podcast_url").unwrap_or_default(); let podcast_index_id = 0; if !podcast_title.is_empty() && !podcast_url.is_empty() { let podcast_info = ClickedFeedURL { podcastid: 0, podcastname: podcast_title.clone(), feedurl: podcast_url.clone(), description: String::new(), author: String::new(), artworkurl: String::new(), explicit: false, episodecount: 0, categories: None, websiteurl: String::new(), podcastindexid: podcast_index_id, is_youtube: Some(false), }; let api_key = api_key.clone(); let user_id = user_id.clone(); let server_name = server_name.clone(); spawn_local(async move { let added = call_check_podcast( &server_name, &api_key.clone().unwrap(), user_id, podcast_info.podcastname.as_str(), podcast_info.feedurl.as_str(), ) .await .unwrap_or_default() .exists; is_added.set(added); let podcast_details = call_get_podcast_details_dynamic( &server_name, &api_key.clone().unwrap(), user_id, podcast_info.podcastname.as_str(), podcast_info.feedurl.as_str(), podcast_info.podcastindexid, added, Some(false), ) .await .unwrap(); fn categories_to_string( categories: Option>, ) -> Option { categories.map(|map| { map.values().cloned().collect::>().join(", ") }) } let podcast_categories_str = categories_to_string(podcast_details.details.categories); // Execute the same process as when a podcast is clicked let on_title_click = create_on_title_click( click_dispatch, server_name, Some(Some(api_key.clone().unwrap())), &click_history, podcast_details.details.podcastindexid, podcast_details.details.podcastname, podcast_details.details.feedurl, podcast_details.details.description, podcast_details.details.author, podcast_details.details.artworkurl, podcast_details.details.explicit, podcast_details.details.episodecount, podcast_categories_str, // assuming no categories in local storage podcast_details.details.websiteurl, user_id, podcast_details.details.is_youtube.unwrap(), ); emit_click(on_title_click); let window = web_sys::window().expect("no global window exists"); let location = window.location(); let mut new_url = location.origin().unwrap(); new_url.push_str(&location.pathname().unwrap()); new_url.push_str("?podcast_title="); new_url.push_str(&urlencoding::encode(&podcast_info.podcastname)); new_url.push_str("&podcast_url="); new_url.push_str(&urlencoding::encode(&podcast_info.feedurl)); pod_load_url.set(new_url.clone()); }); } } else { let podcast = podcast.unwrap(); // Update the URL with query parameters let window = web_sys::window().expect("no global window exists"); let history = window.history().expect("should have a history"); let location = window.location(); let mut new_url = location.origin().unwrap(); new_url.push_str(&location.pathname().unwrap()); new_url.push_str("?podcast_title="); new_url.push_str(&urlencoding::encode(&podcast.podcastname)); new_url.push_str("&podcast_url="); new_url.push_str(&urlencoding::encode(&podcast.feedurl)); history .replace_state_with_url( &wasm_bindgen::JsValue::NULL, "", Some(&new_url), ) .expect("should push state"); let api_key = api_key.clone(); let user_id = user_id.clone(); let server_name = server_name.clone(); spawn_local(async move { let added = call_check_podcast( &server_name, &api_key.unwrap(), user_id, podcast.podcastname.as_str(), podcast.feedurl.as_str(), ) .await .unwrap_or_default() .exists; is_added.set(added); if *is_added.clone() != true { pod_loading_ep.set(false); } }); } } || () }, ); } let podcast_info = search_state.clicked_podcast_info.clone(); let load_link = loading.clone(); use_effect_with(podcast_info.clone(), { let pod_url = pod_url.clone(); move |podcast_info| { if let Some(info) = podcast_info { let window = window().expect("no global window exists"); let history = window.history().expect("should have a history"); let location = window.location(); let mut new_url = location.origin().unwrap(); new_url.push_str(&location.pathname().unwrap()); new_url.push_str("?podcast_title="); new_url.push_str(&urlencoding::encode(&info.podcastname)); new_url.push_str("&podcast_url="); new_url.push_str(&urlencoding::encode(&info.feedurl)); pod_url.set(new_url.clone()); load_link.set(false); history .replace_state_with_url(&JsValue::NULL, "", Some(&new_url)) .expect("should push state"); } || {} } }); let download_status = use_state(|| false); let podcast_id = use_state(|| 0); let start_skip = use_state(|| 0); let end_skip = use_state(|| 0); // Load merge-related data when edit modal opens { let available_podcasts_for_merge = available_podcasts_for_merge.clone(); let current_merged_podcasts = current_merged_podcasts.clone(); let merged_podcast_details = merged_podcast_details.clone(); let loading_merge_data = loading_merge_data.clone(); let page_state = page_state.clone(); let clicked_podcast_info = clicked_podcast_info.clone(); let api_key = api_key.clone(); let server_name = server_name.clone(); let user_id = user_id.clone(); let podcast_id = podcast_id.clone(); use_effect_with( (page_state.clone(), clicked_podcast_info.clone()), move |(page_state, podcast_info)| { if **page_state == PageState::EditPodcast { if let (Some(api_key), Some(server_name), Some(user_id), Some(_podcast_info)) = ( api_key.as_ref(), server_name.as_ref(), user_id.as_ref(), podcast_info.as_ref(), ) { loading_merge_data.set(true); // Load available podcasts let available_podcasts_for_merge = available_podcasts_for_merge.clone(); let current_merged_podcasts = current_merged_podcasts.clone(); let merged_podcast_details = merged_podcast_details.clone(); let loading_merge_data = loading_merge_data.clone(); let api_key = api_key.clone(); let server_name = server_name.clone(); let user_id = *user_id; let current_podcast_id = *podcast_id; spawn_local(async move { // Load all available podcasts (keep all for name lookups) match call_get_podcasts(&server_name, &api_key, &user_id).await { Ok(podcasts) => { available_podcasts_for_merge.set(podcasts); } Err(e) => { console::log_1( &format!("Error loading podcasts for merge: {}", e).into(), ); } } // Load current merged podcasts match call_get_merged_podcasts( &server_name, &api_key, current_podcast_id, ) .await { Ok(merged_ids) => { current_merged_podcasts.set(merged_ids.clone()); // Fetch details for each merged podcast let mut details_map = HashMap::new(); for &merged_id in &merged_ids { match call_get_podcast_details( &server_name, &api_key.as_ref().unwrap(), user_id, &merged_id, ) .await { Ok(details) => { details_map.insert(merged_id, details); } Err(e) => { console::log_1( &format!( "Error loading details for merged podcast {}: {}", merged_id, e ) .into(), ); } } } merged_podcast_details.set(details_map); } Err(e) => { console::log_1( &format!("Error loading merged podcasts: {}", e).into(), ); current_merged_podcasts.set(Vec::new()); } } loading_merge_data.set(false); }); } } }, ); } // Update sort direction when podcast_id changes to load per-podcast preferences { let episode_sort_direction = episode_sort_direction.clone(); let podcast_id_clone = podcast_id.clone(); use_effect_with(podcast_id_clone, move |podcast_id| { if **podcast_id > 0 { let preference_key = format!("podcast_{}", **podcast_id); let saved_preference = get_filter_preference(&preference_key); let new_direction = match saved_preference.as_deref() { Some("newest") => Some(EpisodeSortDirection::NewestFirst), Some("oldest") => Some(EpisodeSortDirection::OldestFirst), Some("shortest") => Some(EpisodeSortDirection::ShortestFirst), Some("longest") => Some(EpisodeSortDirection::LongestFirst), Some("title_az") => Some(EpisodeSortDirection::TitleAZ), Some("title_za") => Some(EpisodeSortDirection::TitleZA), _ => Some(EpisodeSortDirection::NewestFirst), // Default to newest first }; episode_sort_direction.set(new_direction); } || () }); } { let api_key = api_key.clone(); let server_name = server_name.clone(); let podcast_id = podcast_id.clone(); let download_status = download_status.clone(); let notification_effect = notification_status.clone(); // let episode_name = episode_name_pre.clone(); // let episode_url = episode_url_pre.clone(); let user_id = search_state.user_details.as_ref().map(|ud| ud.UserID); let effect_start_skip = start_skip.clone(); let effect_end_skip = end_skip.clone(); let effect_playback_speed = playback_speed.clone(); let effect_added = is_added.clone(); let feed_cutoff_days = feed_cutoff_days.clone(); let feed_cutoff_days_input = feed_cutoff_days_input.clone(); let audio_dispatch = _dispatch.clone(); let click_state = search_state.clone(); use_effect_with( ( click_state.podcast_feed_results.clone(), effect_added.clone(), ), move |_| { let episode_name: Option = click_state .podcast_feed_results .as_ref() .and_then(|results| results.episodes.get(0)) .and_then(|episode| episode.title.clone()); let episode_url: Option = click_state .podcast_feed_results .as_ref() .and_then(|results| results.episodes.get(0)) .and_then(|episode| episode.enclosure_url.clone()); let bool_true = *effect_added; // Dereference here if !bool_true { } else { let api_key = api_key.clone(); let server_name = server_name.clone(); let podcast_id = podcast_id.clone(); let download_status = download_status.clone(); let episode_name = episode_name; let episode_url = episode_url; let user_id = user_id.unwrap(); if episode_name.is_some() && episode_url.is_some() { wasm_bindgen_futures::spawn_local(async move { if let (Some(api_key), Some(server_name)) = (api_key.as_ref(), server_name.as_ref()) { match call_get_podcast_id_from_ep_name( &server_name, &api_key, episode_name.unwrap(), episode_url.unwrap(), user_id, ) .await { Ok(id) => { podcast_id.set(id); match call_get_auto_download_status( &server_name, user_id, &Some(api_key.clone().unwrap()), id, ) .await { Ok(status) => { download_status.set(status); } Err(e) => { web_sys::console::log_1( &format!( "Error getting auto-download status: {}", e ) .into(), ); } } match call_get_feed_cutoff_days( &server_name, &Some(api_key.clone().unwrap()), id, user_id, ) .await { Ok(days) => { feed_cutoff_days.set(days); feed_cutoff_days_input.set(days.to_string()); } Err(e) => { web_sys::console::log_1( &format!( "Error getting feed cutoff days: {}", e ) .into(), ); } } // Add notification status check here match call_get_podcast_notifications_status( server_name.clone(), api_key.clone().unwrap(), user_id, id, ) .await { Ok(status) => { notification_effect.set(status); } Err(e) => { web_sys::console::log_1( &format!( "Error getting notification status: {}", e ) .into(), ); } } match call_get_play_episode_details( &server_name, &Some(api_key.clone().unwrap()), user_id, id, // podcast_id false, // is_youtube (probably false for most podcasts, adjust if needed) ) .await { Ok((speed, start, end)) => { effect_start_skip.set(start); effect_end_skip.set(end); effect_playback_speed.set(speed as f64); } Err(e) => { web_sys::console::log_1( &format!( "Error getting auto-skip times: {}", e ) .into(), ); } } loading_ep.set(false); let chap_request = FetchPodcasting2PodDataRequest { podcast_id: id, user_id, }; match call_fetch_podcasting_2_pod_data( &server_name, &api_key, &chap_request, ) .await { Ok(response) => { // let chapters = response.chapters.clone(); // Clone chapters to avoid move issue let value = response.value.clone(); let funding = response.funding.clone(); let podroll = response.podroll.clone(); let people = response.people.clone(); audio_dispatch.reduce_mut(|state| { state.podcast_value4value = Some(value); state.podcast_funding = Some(funding); state.podcast_podroll = Some(podroll); state.podcast_people = Some(people); }); } Err(e) => { web_sys::console::log_1( &format!("Error fetching 2.0 data: {}", e) .into(), ); } } } Err(e) => { web_sys::console::log_1( &format!("Error getting podcast ID: {}", e).into(), ); } } } }); } } || () }, ); } // Load podcast cover preference when podcast_id changes { let use_podcast_covers = use_podcast_covers.clone(); let podcast_id = podcast_id.clone(); let api_key = api_key.clone(); let server_name = server_name.clone(); let user_id = user_id.clone(); use_effect_with(podcast_id.clone(), move |podcast_id| { if **podcast_id > 0 { let use_podcast_covers = use_podcast_covers.clone(); let podcast_id_val = **podcast_id; if let (Some(api_key), Some(server_name), Some(user_id)) = ( api_key.as_ref().and_then(|k| k.clone()), server_name.as_ref().map(|s| s.clone()), user_id.as_ref().cloned(), ) { wasm_bindgen_futures::spawn_local(async move { match call_get_podcast_cover_preference( &server_name, &api_key, user_id, Some(podcast_id_val), ) .await { Ok(current_preference) => { use_podcast_covers.set(current_preference); } Err(_) => { // If API call fails, default to false use_podcast_covers.set(false); } } }); } } || () }); } let open_in_new_tab = Callback::from(move |url: String| { let window = web_sys::window().unwrap(); window.open_with_url_and_target(&url, "_blank").unwrap(); }); // Function to handle link clicks let history_handle = history.clone(); let handle_click = Callback::from(move |event: MouseEvent| { if let Some(target) = event.target_dyn_into::() { if let Some(href) = target.get_attribute("href") { event.prevent_default(); if href.starts_with("http") { // External link, open in a new tab web_sys::window() .unwrap() .open_with_url_and_target(&href, "_blank") .unwrap(); } else { // Internal link, use Yew Router to navigate history_handle.push(href); } } } }); let node_ref = use_node_ref(); use_effect_with((), move |_| { if let Some(container) = node_ref.cast::() { if let Ok(links) = container.query_selector_all("a") { for i in 0..links.length() { if let Some(link) = links.item(i) { let link = link.dyn_into::().unwrap(); let handle_click_clone = handle_click.clone(); let listener = gloo_events::EventListener::new(&link, "click", move |event| { handle_click_clone .emit(event.clone().dyn_into::().unwrap()); }); listener.forget(); // Prevent listener from being dropped } } } } || () }); let delete_history = history.clone(); let delete_all_click = { let add_dispatch = _search_dispatch.clone(); let pod_values = clicked_podcast_info.clone(); let user_id_og = user_id.clone(); let api_key_clone = api_key.clone(); let server_name_clone = server_name.clone(); let app_dispatch = _search_dispatch.clone(); let call_is_added = is_added.clone(); let page_state = page_state.clone(); let i18n_youtube_channel_successfully_removed = i18n_youtube_channel_successfully_removed.clone(); let i18n_podcast_successfully_removed = i18n_podcast_successfully_removed.clone(); let i18n_failed_to_remove_youtube_channel = i18n_failed_to_remove_youtube_channel.clone(); let i18n_failed_to_remove_podcast = i18n_failed_to_remove_podcast.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); let i18n_youtube_channel_successfully_removed = i18n_youtube_channel_successfully_removed.clone(); let i18n_podcast_successfully_removed = i18n_podcast_successfully_removed.clone(); let i18n_failed_to_remove_youtube_channel = i18n_failed_to_remove_youtube_channel.clone(); let i18n_failed_to_remove_podcast = i18n_failed_to_remove_podcast.clone(); let hist = delete_history.clone(); let page_state = page_state.clone(); let pod_title_og = pod_values.clone().unwrap().podcastname.clone(); let pod_feed_url_og = pod_values.clone().unwrap().feedurl.clone(); let is_youtube = pod_values.clone().unwrap().is_youtube.unwrap_or(false); app_dispatch.reduce_mut(|state| state.is_loading = Some(true)); let is_added_inner = call_is_added.clone(); let call_dispatch = add_dispatch.clone(); let pod_title = pod_title_og.clone(); let pod_title_yt = pod_title_og.clone(); let pod_feed_url = pod_feed_url_og.clone(); let pod_feed_url_yt = pod_feed_url_og.clone(); let pod_feed_url_check = pod_feed_url_og.clone(); let user_id = user_id_og.clone().unwrap(); let podcast_values = RemovePodcastValuesName { podcast_name: pod_title, podcast_url: pod_feed_url, user_id, }; let remove_channel = RemoveYouTubeChannelValues { user_id, channel_name: pod_title_yt, channel_url: pod_feed_url_yt, }; let api_key_call = api_key_clone.clone(); let server_name_call = server_name_clone.clone(); let app_dispatch = app_dispatch.clone(); wasm_bindgen_futures::spawn_local(async move { let dispatch_wasm = call_dispatch.clone(); let api_key_wasm = api_key_call.clone().unwrap(); let server_name_wasm = server_name_call.clone(); let result = if pod_feed_url_check.starts_with("https://www.youtube.com") { call_remove_youtube_channel( &server_name_wasm.unwrap(), &api_key_wasm, &remove_channel, ) .await } else { call_remove_podcasts_name( &server_name_wasm.unwrap(), &api_key_wasm, &podcast_values, ) .await }; match result { Ok(success) => { if success { dispatch_wasm.reduce_mut(|state| { state.info_message = Some( if pod_feed_url_check.starts_with("https://www.youtube.com") { i18n_youtube_channel_successfully_removed } else { i18n_podcast_successfully_removed }, ) }); app_dispatch.reduce_mut(|state| state.is_loading = Some(false)); is_added_inner.set(false); app_dispatch.reduce_mut(|state| { state.podcast_added = Some(podcast_added); }); if pod_feed_url_check.starts_with("https://www.youtube.com") { hist.push("/podcasts"); } } else { dispatch_wasm.reduce_mut(|state| { state.error_message = Some(if is_youtube { i18n_failed_to_remove_youtube_channel } else { i18n_failed_to_remove_podcast }) }); app_dispatch.reduce_mut(|state| state.is_loading = Some(false)); } page_state.set(PageState::Hidden); } Err(e) => { let formatted_error = format_error_message(&e.to_string()); dispatch_wasm.reduce_mut(|state| { state.error_message = Some(format!("Error removing content: {:?}", formatted_error)) }); app_dispatch.reduce_mut(|state| state.is_loading = Some(false)); } } }); }) }; let download_server_name = server_name.clone(); let download_api_key = api_key.clone(); let download_dispatch = _search_dispatch.clone(); let app_state = search_state.clone(); let download_all_click = { let call_dispatch = download_dispatch.clone(); let server_name_copy = download_server_name.clone(); let api_key_copy = download_api_key.clone(); let user_id_copy = user_id.clone(); let search_call_state = app_state.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); let server_name = server_name_copy.clone(); let api_key = api_key_copy.clone(); let search_state = search_call_state.clone(); let call_down_dispatch = call_dispatch.clone(); wasm_bindgen_futures::spawn_local(async move { let episode_id = match search_state .podcast_feed_results .as_ref() .and_then(|results| results.episodes.get(0)) .and_then(|episode| episode.episode_id) { Some(id) => id, None => { eprintln!("No episode_id found"); return; } }; let is_youtube = match search_state .podcast_feed_results .as_ref() .and_then(|results| results.episodes.get(0)) .and_then(|episode| episode.is_youtube) { Some(id) => id, None => { eprintln!("No is_youtube info found"); return; } }; let ep_api_key = api_key.clone(); let ep_server_name = server_name.clone(); let ep_user_id = user_id_copy.clone(); match call_get_podcast_id_from_ep( &ep_server_name.unwrap(), &ep_api_key.unwrap(), episode_id, ep_user_id.unwrap(), Some(is_youtube), ) .await { Ok(podcast_id) => { let request = DownloadAllPodcastRequest { podcast_id, user_id: user_id_copy.unwrap(), }; match call_download_all_podcast( &server_name.unwrap(), &api_key.flatten(), &request, ) .await { Ok(success_message) => { call_down_dispatch.reduce_mut(|state| { state.info_message = Option::from(format!("{}", success_message)) }); } Err(e) => { let formatted_error = format_error_message(&e.to_string()); call_down_dispatch.reduce_mut(|state| { state.error_message = Option::from(format!("{}", formatted_error)) }); } } } Err(e) => { call_down_dispatch.reduce_mut(|state| { let formatted_error = format_error_message(&e.to_string()); state.error_message = Option::from(format!( "Failed to get podcast ID: {}", formatted_error )) }); } } }); }) }; // Define the state of the application #[derive(Clone, PartialEq)] enum PageState { Hidden, Shown, Download, Delete, RSSFeed, EditPodcast, } let button_content = if *is_added { trash_icon() } else { add_icon() }; let setting_content = if *is_added { settings_icon() } else { no_icon() }; let download_all = if *is_added { download_icon() } else { no_icon() }; let payment_icon = { payments_icon() }; let rss_icon = { rss_icon() }; let website_icon = { website_icon() }; let on_close_modal = { let page_state = page_state.clone(); Callback::from(move |_| { page_state.set(PageState::Hidden); }) }; let on_background_click = { let on_close_modal = on_close_modal.clone(); Callback::from(move |e: MouseEvent| { let target = e.target().unwrap(); let element = target.dyn_into::().unwrap(); if element.tag_name() == "DIV" { on_close_modal.emit(e); } }) }; let stop_propagation = Callback::from(|e: MouseEvent| { e.stop_propagation(); }); let toggle_edit_podcast = { let page_state = page_state.clone(); Callback::from(move |_: MouseEvent| { page_state.set(PageState::EditPodcast); }) }; let toggle_download = { let api_key = api_key.clone(); let server_name = server_name.clone(); let download_status = download_status.clone(); let podcast_id = podcast_id.clone(); let user_id = user_id.clone(); Callback::from(move |_| { let api_key = api_key.clone(); let server_name = server_name.clone(); let download_status = download_status.clone(); let auto_download = !*download_status; let pod_id_deref = *podcast_id.clone(); let user_id = user_id.clone().unwrap(); let request_data = AutoDownloadRequest { podcast_id: pod_id_deref, // Replace with the actual podcast ID user_id, auto_download, }; wasm_bindgen_futures::spawn_local(async move { if let (Some(api_key), Some(server_name)) = (api_key.as_ref(), server_name.as_ref()) { match call_enable_auto_download( &server_name, &api_key.clone().unwrap(), &request_data, ) .await { Ok(_) => { download_status.set(auto_download); } Err(e) => { web_sys::console::log_1( &format!("Error enabling/disabling downloads: {}", e).into(), ); } } } }); }) }; let playback_speed_input_handler = Callback::from(move |e: InputEvent| { if let Some(input) = e.target_dyn_into::() { let value = input.value().parse::().unwrap_or(1.0); // Constrain to reasonable values (0.5 to 3.0) let value = value.max(0.5).min(2.0); playback_speed_input.set(value); } }); // Create the save playback speed function let save_playback_speed = { let playback_speed = playback_speed.clone(); let api_key = api_key.clone(); let user_id = user_id.clone(); let server_name = server_name.clone(); let podcast_id = podcast_id.clone(); let dispatch = _search_dispatch.clone(); let i18n_playback_speed_updated = i18n_playback_speed_updated.clone(); let i18n_error_updating_playback_speed = i18n_error_updating_playback_speed.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); let i18n_playback_speed_updated = i18n_playback_speed_updated.clone(); let i18n_error_updating_playback_speed = i18n_error_updating_playback_speed.clone(); let call_dispatch = dispatch.clone(); let speed = *playback_speed; let api_key = api_key.clone(); let user_id = user_id.clone().unwrap(); let server_name = server_name.clone(); let podcast_id = *podcast_id; wasm_bindgen_futures::spawn_local(async move { if let (Some(api_key), Some(server_name)) = (api_key.as_ref(), server_name.as_ref()) { let request = PlaybackSpeedRequest { podcast_id, user_id, playback_speed: speed, }; match call_set_playback_speed(&server_name, &api_key, &request).await { Ok(_) => { call_dispatch.reduce_mut(|state| { state.info_message = Option::from(i18n_playback_speed_updated) }); } Err(e) => { web_sys::console::log_1( &format!("Error updating playback speed: {}", e).into(), ); call_dispatch.reduce_mut(|state| { state.error_message = Option::from(i18n_error_updating_playback_speed) }); } } } }); }) }; // Create the clear playback speed function let clear_playback_speed = { let api_key = api_key.clone(); let user_id = user_id.clone(); let server_name = server_name.clone(); let podcast_id = podcast_id.clone(); let dispatch = _search_dispatch.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); let i18n_playback_speed_reset_default = i18n_playback_speed_reset_default.clone(); let i18n_error_resetting_playback_speed = i18n_error_resetting_playback_speed.clone(); let call_dispatch = dispatch.clone(); let api_key = api_key.clone(); let user_id = user_id.clone().unwrap(); let server_name = server_name.clone(); let podcast_id = *podcast_id; wasm_bindgen_futures::spawn_local(async move { if let (Some(api_key), Some(server_name)) = (api_key.as_ref(), server_name.as_ref()) { let request = ClearPlaybackSpeedRequest { podcast_id, user_id, }; match call_clear_playback_speed(&server_name, &api_key, &request).await { Ok(_) => { call_dispatch.reduce_mut(|state| { state.info_message = Option::from(i18n_playback_speed_reset_default) }); } Err(e) => { web_sys::console::log_1( &format!("Error resetting playback speed: {}", e).into(), ); call_dispatch.reduce_mut(|state| { state.error_message = Option::from(i18n_error_resetting_playback_speed) }); } } } }); }) }; // Add this callback for handling input changes let feed_cutoff_days_input_handler = { let feed_cutoff_days_input = feed_cutoff_days_input.clone(); Callback::from(move |e: InputEvent| { if let Some(input) = e.target_dyn_into::() { feed_cutoff_days_input.set(input.value()); } }) }; // Add this callback for saving the feed cutoff days let save_feed_cutoff_days = { let dispatch_vid = _search_dispatch.clone(); let server_name = server_name.clone(); let api_key = api_key.clone(); let podcast_id = podcast_id.clone(); let feed_cutoff_days_input = feed_cutoff_days_input.clone(); let feed_cutoff_days = feed_cutoff_days.clone(); let user_id = search_state.user_details.as_ref().map(|ud| ud.UserID); Callback::from(move |e: MouseEvent| { e.prevent_default(); let i18n_youtube_episode_limit_updated = i18n_youtube_episode_limit_updated.clone(); let dispatch_wasm = dispatch_vid.clone(); // Extract the values directly without creating intermediate variables if let (Some(server_val), Some(key_val), Some(user_val)) = ( server_name.as_ref(), api_key.as_ref().and_then(|k| k.as_ref()), user_id, ) { let pod_id = *podcast_id; let days_str = (*feed_cutoff_days_input).clone(); let days = days_str.parse::().unwrap_or(0); let request_data = UpdateFeedCutoffDaysRequest { podcast_id: pod_id, user_id: user_val, feed_cutoff_days: days, }; // Clone everything needed for the async block let server_val = server_val.clone(); let key_val = key_val.clone(); let feed_cutoff_days = feed_cutoff_days.clone(); wasm_bindgen_futures::spawn_local(async move { match call_update_feed_cutoff_days(&server_val, &Some(key_val), &request_data) .await { Ok(_) => { feed_cutoff_days.set(days); dispatch_wasm.reduce_mut(|state| { state.info_message = Option::from(i18n_youtube_episode_limit_updated) }); // No need to update a ClickedFeedURL or PodcastInfo struct // Just update the state } Err(err) => { web_sys::console::log_1( &format!("Error updating feed cutoff days: {}", err).into(), ); dispatch_wasm.reduce_mut(|state| { state.error_message = Option::from(format!( "Error updating feed cutoff days: {:?}", err )) }); } } }); } }) }; let toggle_notifications = { let api_key = api_key.clone(); let server_name = server_name.clone(); let notification_status = notification_status.clone(); let podcast_id = podcast_id.clone(); let user_id = user_id.clone(); Callback::from(move |_| { let api_key = api_key.clone(); let server_name = server_name.clone(); let notification_status = notification_status.clone(); let enabled = !*notification_status; let pod_id_deref = *podcast_id.clone(); let user_id = user_id.clone().unwrap(); wasm_bindgen_futures::spawn_local(async move { if let (Some(api_key), Some(server_name)) = (api_key.as_ref(), server_name.as_ref()) { match call_toggle_podcast_notifications( server_name.clone(), api_key.clone().unwrap(), user_id, pod_id_deref, enabled, ) .await { Ok(_) => { notification_status.set(enabled); } Err(e) => { web_sys::console::log_1( &format!("Error toggling notifications: {}", e).into(), ); } } } }); }) }; let toggle_podcast_covers = { let api_key = api_key.clone(); let server_name = server_name.clone(); let use_podcast_covers = use_podcast_covers.clone(); let podcast_id = podcast_id.clone(); let user_id = user_id.clone(); let dispatch = _search_dispatch.clone(); Callback::from(move |_: MouseEvent| { let api_key = api_key.clone(); let server_name = server_name.clone(); let use_podcast_covers = use_podcast_covers.clone(); let new_setting = !*use_podcast_covers; let pod_id_deref = *podcast_id.clone(); let user_id = user_id.clone().unwrap(); let dispatch = dispatch.clone(); wasm_bindgen_futures::spawn_local(async move { if let (Some(api_key), Some(server_name)) = (api_key.as_ref(), server_name.as_ref()) { match call_set_global_podcast_cover_preference( server_name, &api_key.clone().unwrap(), user_id, new_setting, Some(pod_id_deref), ) .await { Ok(_) => { use_podcast_covers.set(new_setting); dispatch.reduce_mut(|state| { state.info_message = Some(format!( "Podcast cover preference {} for this podcast", if new_setting { "enabled" } else { "disabled" } )); }); } Err(e) => { dispatch.reduce_mut(|state| { state.error_message = Some(format!("Error updating podcast cover preference: {}", e)); }); } } } }); }) }; let start_skip_call = start_skip.clone(); let end_skip_call = end_skip.clone(); let start_skip_call_button = start_skip.clone(); let end_skip_call_button = end_skip.clone(); let skip_dispatch = _search_dispatch.clone(); // Save the skip times to the server let save_skip_times = { let start_skip = start_skip.clone(); let end_skip = end_skip.clone(); let api_key = api_key.clone(); let user_id = user_id.clone(); let server_name = server_name.clone(); let podcast_id = podcast_id.clone(); let skip_dispatch = skip_dispatch.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); let i18n_skip_times_adjusted = i18n_skip_times_adjusted.clone(); let i18n_error_adjusting_skip_times = i18n_error_adjusting_skip_times.clone(); let skip_call_dispatch = skip_dispatch.clone(); let start_skip = *start_skip; let end_skip = *end_skip; let api_key = api_key.clone(); let user_id = user_id.clone().unwrap(); let server_name = server_name.clone(); let podcast_id = *podcast_id; wasm_bindgen_futures::spawn_local(async move { if let (Some(api_key), Some(server_name)) = (api_key.as_ref(), server_name.as_ref()) { let request = SkipTimesRequest { podcast_id, start_skip, end_skip, user_id, }; match call_adjust_skip_times(&server_name, &api_key, &request).await { Ok(_) => { skip_call_dispatch.reduce_mut(|state| { state.info_message = Option::from(i18n_skip_times_adjusted) }); } Err(e) => { web_sys::console::log_1( &format!("Error updating skip times: {}", e).into(), ); skip_call_dispatch.reduce_mut(|state| { state.error_message = Option::from(i18n_error_adjusting_skip_times) }); } } } }); }) }; // let onclick_cat = new_category let app_dispatch_add = _search_dispatch.clone(); let onclick_add = { // let dispatch = dispatch.clone(); let server_name = server_name.clone(); let api_key = api_key.clone(); let user_id = user_id.clone(); // Assuming user_id is an Option or similar let podcast_id = podcast_id.clone(); // Assuming this is available in your context let new_category = new_category.clone(); // Assuming this is a state that stores the new category input Callback::from(move |event: web_sys::MouseEvent| { event.prevent_default(); // Prevent the default form submit or page reload behavior let app_dispatch = app_dispatch_add.clone(); if new_category.is_empty() { web_sys::console::log_1(&i18n_category_name_cannot_be_empty.clone().into()); return; } // let dispatch = dispatch.clone(); let server_name = server_name.clone().unwrap(); let api_key = api_key.clone().unwrap(); let user_id = user_id.clone().unwrap(); // Assuming user_id is Some(i32) let podcast_id = *podcast_id; // Assuming podcast_id is Some(i32) let category_name = (*new_category).clone(); let cat_name_dis = category_name.clone(); wasm_bindgen_futures::spawn_local(async move { let request_data = AddCategoryRequest { podcast_id, user_id, category: category_name, }; // Await the async function call let response = call_add_category(&server_name, &api_key, &request_data).await; // Match on the awaited response match response { Ok(_) => { app_dispatch.reduce_mut(|state| { if let Some(ref mut podcast_info) = state.clicked_podcast_info { if let Some(ref mut categories) = podcast_info.categories { // Add the new category to the HashMap categories.insert(cat_name_dis.clone(), cat_name_dis.clone()); } else { // Initialize the HashMap if it's None let mut new_map = HashMap::new(); new_map.insert(cat_name_dis.clone(), cat_name_dis); podcast_info.categories = Some(new_map); } } }); } Err(err) => { web_sys::console::log_1(&format!("Error adding category: {}", err).into()); } } }); }) }; let category_to_remove = use_state(|| None::); let onclick_remove = { let category_to_remove = category_to_remove.clone(); Callback::from(move |event: MouseEvent| { event.prevent_default(); let target = event.target_unchecked_into::(); let closest_button = target.closest("button").unwrap(); if let Some(button) = closest_button { if let Some(category) = button.get_attribute("data-category") { category_to_remove.set(Some(category)); } } }) }; let app_dispatch = _search_dispatch.clone(); { let category_to_remove = category_to_remove.clone(); let server_name = server_name.clone(); let api_key = api_key.clone(); let user_id = user_id; let podcast_id = *podcast_id; use_effect_with(category_to_remove, move |category_to_remove| { if let Some(category) = (**category_to_remove).clone() { let server_name = server_name.clone().unwrap(); let api_key = api_key.clone().unwrap(); let user_id = user_id.unwrap(); let category_request = category.clone(); wasm_bindgen_futures::spawn_local(async move { let request_data = RemoveCategoryRequest { podcast_id, user_id, category, }; // Your API call here let response = call_remove_category(&server_name, &api_key, &request_data).await; match response { Ok(_) => { app_dispatch.reduce_mut(|state| { if let Some(ref mut podcast_info) = state.clicked_podcast_info { if let Some(ref mut categories) = podcast_info.categories { // Filter the HashMap and collect back into HashMap *categories = categories .clone() .into_iter() .filter(|(_, cat)| cat != &category_request) // Ensure you're comparing correctly .collect(); } } }); } Err(err) => { web_sys::console::log_1( &format!("Error removing category: {}", err).into(), ); } } }); } || () }); } // Fetch RSS key when RSS feed modal is shown { let rss_key_state = rss_key_state.clone(); let server_name = search_state .auth_details .as_ref() .map(|ud| ud.server_name.clone()); let api_key = api_key.clone().flatten(); let page_state_clone = page_state.clone(); use_effect_with( (page_state_clone.clone(), rss_key_state.is_none()), move |(current_page_state, rss_key_is_none)| { if matches!(**current_page_state, PageState::RSSFeed) && *rss_key_is_none { if let (Some(server_name), Some(api_key), Some(user_id)) = (server_name.clone(), api_key.clone(), user_id.clone()) { let rss_key_state = rss_key_state.clone(); wasm_bindgen_futures::spawn_local(async move { match call_get_rss_key(&server_name, &Some(api_key), user_id).await { Ok(rss_key) => { rss_key_state.set(Some(rss_key)); } Err(e) => { web_sys::console::log_1( &format!("Failed to fetch RSS key: {}", e).into(), ); } } }); } } || () }, ); } let rss_feed_modal = { let rss_key_state_clone = rss_key_state.clone(); let rss_url = match (*rss_key_state_clone).as_ref() { Some(rss_key) => format!( "{}/{}?api_key={}&podcast_id={}", get_rss_base_url(), user_id.clone().unwrap_or_default(), rss_key, *podcast_id ), None => i18n_loading_rss_key.clone(), }; let copy_onclick = { let rss_url = rss_url.clone(); Callback::from(move |_| { if let Some(window) = web_sys::window() { let _ = window.navigator().clipboard().write_text(&rss_url); } }) }; html! { } }; // Define the modal components let clicked_feed = clicked_podcast_info.clone(); let podcast_option_model = html! { }; // Define the modal components let download_all_model = html! { }; // Define the modal components let delete_pod_model = html! { }; // Define the edit podcast modal let edit_podcast_modal = { html! {