From 4247503b87b39eaa829ec447257964b70bcdb647 Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 14 Aug 2025 15:18:50 +0200 Subject: [PATCH 1/2] Playlist explorer copy url --- frontend/public/assets/i18n/en.json | 4 +- frontend/public/assets/icons.json | 4 + .../playlist/_playlist_explorer.scss | 4 + .../components/playlist/epg_config_view.rs | 2 +- .../components/playlist/playlist_explorer.rs | 118 +++++++++++++++++- .../playlist/playlist_explorer_view.rs | 4 +- .../components/playlist/source_selector.rs | 2 + frontend/src/app/context.rs | 3 +- 8 files changed, 132 insertions(+), 9 deletions(-) diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 050be30c5..592de3ad4 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -241,7 +241,9 @@ "NORMALIZE_REGEX": "Normalize Regex", "NAME_PREFIX": "Name Prefix", "NAME_PREFIX_SEPARATOR":"Name Prefix Separator", - "STRIP": "Strip" + "STRIP": "Strip", + "COPY_LINK_TULIPROX": "Copy Virtual Id", + "COPY_LINK_PROVIDER": "Copy Provider Url" }, "TABLE": { "EMPTY": "", diff --git a/frontend/public/assets/icons.json b/frontend/public/assets/icons.json index 68c139d2c..d5cfc1d24 100644 --- a/frontend/public/assets/icons.json +++ b/frontend/public/assets/icons.json @@ -469,5 +469,9 @@ { "keys": ["HdHomerunConfig"], "path": "m 16,4.2 c 1.5,0 3,0.6 4.2,1.7 L 21,5.1 C 19.6,3.7 17.8,3 16,3 14.2,3 12.4,3.7 11,5.1 l 0.8,0.8 C 13,4.8 14.5,4.2 16,4.2 m -3.3,2.5 0.8,0.8 c 0.7,-0.7 1.6,-1 2.5,-1 0.9,0 1.8,0.3 2.5,1 L 19.3,6.7 C 18.4,5.8 17.2,5.3 16,5.3 14.8,5.3 13.6,5.8 12.7,6.7 M 19,13 H 17 V 9 h -2 v 4 H 5 c -1.1,0 -2,0.9 -2,2 v 4 c 0,1.1 0.9,2 2,2 h 14 c 1.1,0 2,-0.9 2,-2 v -4 c 0,-1.1 -0.9,-2 -2,-2 m 0,6 H 5 V 15 H 19 Z M 6,16 h 2 v 2 H 6 Z" + }, + { + "keys": ["Clipboard"], + "path": "M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2m-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1m7 18H5V4h2v3h10V4h2z" } ] \ No newline at end of file diff --git a/frontend/scss/app/components/playlist/_playlist_explorer.scss b/frontend/scss/app/components/playlist/_playlist_explorer.scss index 76379e120..7051293d6 100644 --- a/frontend/scss/app/components/playlist/_playlist_explorer.scss +++ b/frontend/scss/app/components/playlist/_playlist_explorer.scss @@ -123,4 +123,8 @@ } } + &__channel { + padding-left: var(--padding-micro); + } + } \ No newline at end of file diff --git a/frontend/src/app/components/playlist/epg_config_view.rs b/frontend/src/app/components/playlist/epg_config_view.rs index 02405733d..6f6c5aa3d 100644 --- a/frontend/src/app/components/playlist/epg_config_view.rs +++ b/frontend/src/app/components/playlist/epg_config_view.rs @@ -110,7 +110,7 @@ pub fn EpgConfigView(props: &EpgConfigViewProps) -> Html { }; let render_empty = || html! { -
+
{render_smart_match(None)} {render_sources(None)}
diff --git a/frontend/src/app/components/playlist/playlist_explorer.rs b/frontend/src/app/components/playlist/playlist_explorer.rs index 25b16da2c..2b5ddb41b 100644 --- a/frontend/src/app/components/playlist/playlist_explorer.rs +++ b/frontend/src/app/components/playlist/playlist_explorer.rs @@ -1,13 +1,51 @@ +use std::fmt::Display; use std::rc::Rc; +use std::str::FromStr; use wasm_bindgen::JsCast; use yew::platform::spawn_local; use crate::app::context::PlaylistExplorerContext; use yew::prelude::*; -use shared::model::{CommonPlaylistItem, SearchRequest, UiPlaylistGroup, XtreamCluster}; -use crate::app::components::{IconButton, NoContent, Search}; +use yew_hooks::use_clipboard; +use yew_i18n::use_translation; +use shared::create_tuliprox_error_result; +use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::model::{CommonPlaylistItem, PlaylistRequestType, SearchRequest, UiPlaylistGroup, XtreamCluster}; +use crate::app::components::{AppIcon, IconButton, NoContent, Search}; +use crate::app::components::menu_item::MenuItem; +use crate::app::components::popup_menu::PopupMenu; use crate::hooks::use_service_context; +use crate::html_if; use crate::model::{BusyStatus, EventMessage}; +#[derive(Debug, Clone, Eq, PartialEq)] +enum ExplorerAction { + CopyLinkTuliprox, + CopyLinkProvider, +} + +impl Display for ExplorerAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::CopyLinkTuliprox => "copy_link_tuliprox", + Self::CopyLinkProvider => "copy_link_provider", + }) + } +} + +impl FromStr for ExplorerAction { + type Err = TuliproxError; + + fn from_str(s: &str) -> Result { + if s.eq("copy_link_tuliprox") { + Ok(Self::CopyLinkTuliprox) + } else if s.eq("copy_link_provider") { + Ok(Self::CopyLinkProvider) + } else { + create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown InputType: {}", s) + } + } +} + enum ExplorerLevel { Categories, Group(Rc), @@ -16,18 +54,74 @@ enum ExplorerLevel { #[function_component] pub fn PlaylistExplorer() -> Html { let context = use_context::().expect("PlaylistExplorer context not found"); + let translate = use_translation(); let service_ctx = use_service_context(); let current_item = use_state(|| ExplorerLevel::Categories); let playlist = use_state(|| (*context.playlist).clone()); + let selected_channel = use_state(|| None::>); + let popup_anchor_ref = use_state(|| None::); + let popup_is_open = use_state(|| false); + let clipboard = use_clipboard(); + + let handle_popup_close = { + let set_is_open = popup_is_open.clone(); + Callback::from(move |()| { + set_is_open.set(false); + }) + }; + + let handle_popup_onclick = { + let set_selected_channel = selected_channel.clone(); + let set_anchor_ref = popup_anchor_ref.clone(); + let set_is_open = popup_is_open.clone(); + Callback::from(move |(dto, event): (Rc, MouseEvent)| { + if let Some(target) = event.target_dyn_into::() { + set_selected_channel.set(Some(dto.clone())); + set_anchor_ref.set(Some(target)); + set_is_open.set(true); + } + }) + }; { let set_playlist = playlist.clone(); + let set_current_item = current_item.clone(); use_effect_with(context.playlist.clone(), move |new_playlist| { + set_current_item.set(ExplorerLevel::Categories); set_playlist.set((**new_playlist).clone()); || {} }); } + let copy_to_clipboard ={ + let clipboard = clipboard.clone(); + move |text: String| { + let _ = clipboard.write_text(text); + } + }; + + let handle_menu_click = { + let popup_is_open_state = popup_is_open.clone(); + let selected_channel = selected_channel.clone(); + Callback::from(move |(name, _): (String, _)| { + if let Ok(action) = ExplorerAction::from_str(&name) { + match action { + ExplorerAction::CopyLinkTuliprox => { + if let Some(dto) = &*selected_channel { + copy_to_clipboard(dto.virtual_id.to_string()); + } + } + ExplorerAction::CopyLinkProvider => { + if let Some(dto) = &*selected_channel { + copy_to_clipboard(dto.url.clone()); + } + } + } + } + popup_is_open_state.set(false); + }) + }; + let handle_back_click = { let current_item = current_item.clone(); Callback::from(move |_| { @@ -151,10 +245,17 @@ pub fn PlaylistExplorer() -> Html {
{ - group.channels.iter().map(|c| { + group.channels.iter().map(|chan| { + let chan_clone = chan.clone(); + let popup_onclick = handle_popup_onclick.clone(); html! { - - {render_channel_logo(c)} {c.title.clone()} + + + {render_channel_logo(chan)} + {chan.title.clone()} } }).collect::() @@ -182,6 +283,13 @@ pub fn PlaylistExplorer() -> Html { } }
+ + { html_if!(*context.playlist_request_type == Some(PlaylistRequestType::Target), { + + }) + } + +
} } \ No newline at end of file diff --git a/frontend/src/app/components/playlist/playlist_explorer_view.rs b/frontend/src/app/components/playlist/playlist_explorer_view.rs index baf12839a..cc40d30aa 100644 --- a/frontend/src/app/components/playlist/playlist_explorer_view.rs +++ b/frontend/src/app/components/playlist/playlist_explorer_view.rs @@ -3,7 +3,7 @@ use crate::app::context::PlaylistExplorerContext; use std::rc::Rc; use yew::prelude::*; use yew_i18n::use_translation; -use shared::model::{UiPlaylistCategories}; +use shared::model::{PlaylistRequestType, UiPlaylistCategories}; use crate::app::components::playlist::playlist_explorer::PlaylistExplorer; #[function_component] @@ -12,6 +12,7 @@ pub fn PlaylistExplorerView() -> Html { let breadcrumbs = use_state(|| Rc::new(vec![translate.t("LABEL.PLAYLISTS"), translate.t("LABEL.LIST")])); let active_page = use_state(|| PlaylistExplorerPage::SourceSelector); let playlist = use_state(|| None::>); + let playlist_req_type = use_state(|| None::); let handle_breadcrumb_select = { let view_visible = active_page.clone(); @@ -38,6 +39,7 @@ pub fn PlaylistExplorerView() -> Html { let context = PlaylistExplorerContext { active_page: active_page.clone(), playlist: playlist.clone(), + playlist_request_type: playlist_req_type.clone(), }; html! { diff --git a/frontend/src/app/components/playlist/source_selector.rs b/frontend/src/app/components/playlist/source_selector.rs index 5b0f0fb78..bfb95403a 100644 --- a/frontend/src/app/components/playlist/source_selector.rs +++ b/frontend/src/app/components/playlist/source_selector.rs @@ -45,9 +45,11 @@ pub fn PlaylistSourceSelector() -> Html { set_loading.set(true); services.event.broadcast(EventMessage::Busy(BusyStatus::Show)); let set_loading = set_loading.clone(); + let req_type = request.rtype; spawn_local(async move { let playlist = services.playlist.get_playlist_categories(&request).await; playlist_explorer_ctx_clone.playlist.set(playlist); + playlist_explorer_ctx_clone.playlist_request_type.set(Some(req_type)); set_loading.set(false); services.event.broadcast(EventMessage::Busy(BusyStatus::Hide)); }); diff --git a/frontend/src/app/context.rs b/frontend/src/app/context.rs index d1775ebdb..8181eaf70 100644 --- a/frontend/src/app/context.rs +++ b/frontend/src/app/context.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use regex::Regex; use yew::UseStateHandle; -use shared::model::{AppConfigDto, ConfigTargetDto, ProxyUserCredentialsDto, SearchRequest, StatusCheck, UiPlaylistCategories}; +use shared::model::{AppConfigDto, ConfigTargetDto, PlaylistRequestType, ProxyUserCredentialsDto, SearchRequest, StatusCheck, UiPlaylistCategories}; use crate::app::components::{InputRow, PlaylistEditorPage, PlaylistExplorerPage, UserlistPage}; type SingleSource = (Vec>, Vec>); @@ -20,6 +20,7 @@ pub struct PlaylistEditorContext { pub struct PlaylistExplorerContext { pub active_page: UseStateHandle, pub playlist: UseStateHandle>>, + pub playlist_request_type: UseStateHandle>, } From 52a357a43f9bb09890be816616fd3c2a7cf69e20 Mon Sep 17 00:00:00 2001 From: euzu Date: Thu, 14 Aug 2025 15:52:02 +0200 Subject: [PATCH 2/2] Playlist explorer copy url --- frontend/public/assets/icons.json | 6 ------ .../app/components/playlist/playlist_explorer.rs | 14 +++++++++++--- .../src/app/components/playlist/source_selector.rs | 3 ++- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/frontend/public/assets/icons.json b/frontend/public/assets/icons.json index d5cfc1d24..d75aa5066 100644 --- a/frontend/public/assets/icons.json +++ b/frontend/public/assets/icons.json @@ -5,12 +5,6 @@ ], "path": "M 14.09194,7.6882684e-4 C 13.973764,9.5800672e-5 13.856738,1.8569152e-4 13.740338,0.00128559 10.134203,0.03533121 7.3722672,0.9073449 5.3120566,2.2058124 2.6199728,3.9025243 1.1166249,6.3549771 0.44196532,8.8193928 4.4918101,7.9934421 7.5738185,8.392102 10.393515,9.4901562 V 3.0538267 c 0,-1.0502871 0.717633,-1.3329492 1.494308,-0.8443969 l 5.939123,3.7362233 C 17.883676,5.8672459 17.940027,5.7886156 17.993602,5.7100075 18.773785,4.4409406 19.517515,2.4795189 20.024457,0.67721647 17.837484,0.22159145 15.864573,0.01086418 14.09194,7.6882684e-4 Z M 12.283121,4.6015429 c -0.352571,-0.00358 -0.610222,0.2693437 -0.610222,0.817525 V 18.544943 c 0,0.716278 0.702783,1.00662 1.134576,0.772566 L 23.366199,12.42229 c 0.321466,-0.209944 0.293977,-0.733308 0,-1.002527 L 12.807475,4.7777605 C 12.623215,4.6618552 12.443381,4.6031688 12.283121,4.6015429 Z M 4.6820187,10.631177 c -1.3670233,0.0065 -2.8875164,0.207303 -4.66075611,0.63924 -0.22262,3.841376 1.29470801,7.532859 4.53424051,9.96688 3.3072703,2.484918 8.4898149,3.647573 15.5090939,1.986968 -0.453335,-1.879096 -1.379185,-3.757389 -2.654289,-5.468942 l -5.522485,3.606515 c -0.568764,0.308297 -1.494308,-0.07453 -1.494308,-1.018031 V 12.036783 C 8.6043863,11.2147 6.6723813,10.706689 4.6820187,10.631177 Z" }, - { - "keys": [ - "ContentCopy" - ], - "path": "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" - }, { "keys": [ "DeleteSweep" diff --git a/frontend/src/app/components/playlist/playlist_explorer.rs b/frontend/src/app/components/playlist/playlist_explorer.rs index 2b5ddb41b..b48143d70 100644 --- a/frontend/src/app/components/playlist/playlist_explorer.rs +++ b/frontend/src/app/components/playlist/playlist_explorer.rs @@ -86,9 +86,16 @@ pub fn PlaylistExplorer() -> Html { { let set_playlist = playlist.clone(); let set_current_item = current_item.clone(); + let set_selected_channel = selected_channel.clone(); + let set_popup_is_open = popup_is_open.clone(); + let set_anchor_ref = popup_anchor_ref.clone(); use_effect_with(context.playlist.clone(), move |new_playlist| { set_current_item.set(ExplorerLevel::Categories); set_playlist.set((**new_playlist).clone()); + // Reset popup state and selection when the underlying data changes + set_selected_channel.set(None); + set_popup_is_open.set(false); + set_anchor_ref.set(None); || {} }); } @@ -96,7 +103,7 @@ pub fn PlaylistExplorer() -> Html { let copy_to_clipboard ={ let clipboard = clipboard.clone(); move |text: String| { - let _ = clipboard.write_text(text); + clipboard.write_text(text); } }; @@ -283,8 +290,9 @@ pub fn PlaylistExplorer() -> Html { } }
+ - { html_if!(*context.playlist_request_type == Some(PlaylistRequestType::Target), { + { html_if!((*context.playlist_request_type).as_ref() == Some(&PlaylistRequestType::Target), { }) } @@ -292,4 +300,4 @@ pub fn PlaylistExplorer() -> Html { } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/playlist/source_selector.rs b/frontend/src/app/components/playlist/source_selector.rs index bfb95403a..ae5a45951 100644 --- a/frontend/src/app/components/playlist/source_selector.rs +++ b/frontend/src/app/components/playlist/source_selector.rs @@ -46,8 +46,9 @@ pub fn PlaylistSourceSelector() -> Html { services.event.broadcast(EventMessage::Busy(BusyStatus::Show)); let set_loading = set_loading.clone(); let req_type = request.rtype; + let req = request; spawn_local(async move { - let playlist = services.playlist.get_playlist_categories(&request).await; + let playlist = services.playlist.get_playlist_categories(&req).await; playlist_explorer_ctx_clone.playlist.set(playlist); playlist_explorer_ctx_clone.playlist_request_type.set(Some(req_type)); set_loading.set(false);