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..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"
@@ -469,5 +463,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..b48143d70 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,81 @@ 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();
+ 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);
|| {}
});
}
+ let copy_to_clipboard ={
+ let clipboard = clipboard.clone();
+ move |text: String| {
+ 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 +252,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 +290,14 @@ pub fn PlaylistExplorer() -> Html {
}
}
+
+
+ { html_if!((*context.playlist_request_type).as_ref() == 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..ae5a45951 100644
--- a/frontend/src/app/components/playlist/source_selector.rs
+++ b/frontend/src/app/components/playlist/source_selector.rs
@@ -45,9 +45,12 @@ 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;
+ 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);
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