Merge pull request #330 from euzu/featue/playlist_explorer_copy_url

Playlist explorer copy url
This commit is contained in:
euzu
2025-08-14 15:58:45 +02:00
committed by GitHub
8 changed files with 143 additions and 17 deletions
+3 -1
View File
@@ -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": "",
+4 -6
View File
@@ -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"
}
]
@@ -123,4 +123,8 @@
}
}
&__channel {
padding-left: var(--padding-micro);
}
}
@@ -110,7 +110,7 @@ pub fn EpgConfigView(props: &EpgConfigViewProps) -> Html {
};
let render_empty = || html! {
<div class="tp__messaging-config-view__body tp__config-view-page__body">
<div class="tp__epg-config-view__body tp__config-view-page__body">
{render_smart_match(None)}
{render_sources(None)}
</div>
@@ -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<Self, TuliproxError> {
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<UiPlaylistGroup>),
@@ -16,18 +54,81 @@ enum ExplorerLevel {
#[function_component]
pub fn PlaylistExplorer() -> Html {
let context = use_context::<PlaylistExplorerContext>().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::<Rc<CommonPlaylistItem>>);
let popup_anchor_ref = use_state(|| None::<web_sys::Element>);
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<CommonPlaylistItem>, MouseEvent)| {
if let Some(target) = event.target_dyn_into::<web_sys::Element>() {
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 {
<div class="tp__playlist-explorer__group">
<div class="tp__playlist-explorer__group-list">
{
group.channels.iter().map(|c| {
group.channels.iter().map(|chan| {
let chan_clone = chan.clone();
let popup_onclick = handle_popup_onclick.clone();
html! {
<span class="tp__playlist-explorer__item">
{render_channel_logo(c)} {c.title.clone()}
<span class="tp__playlist-explorer__item tp__playlist-explorer__channel">
<button class="tp__icon-button"
onclick={Callback::from(move |event: MouseEvent| popup_onclick.emit((chan_clone.clone(), event)))}>
<AppIcon name="Popup"></AppIcon>
</button>
{render_channel_logo(chan)}
{chan.title.clone()}
</span>
}
}).collect::<Html>()
@@ -182,6 +290,14 @@ pub fn PlaylistExplorer() -> Html {
}
}
</div>
<PopupMenu is_open={*popup_is_open} anchor_ref={(*popup_anchor_ref).clone()} on_close={handle_popup_close}>
{ html_if!((*context.playlist_request_type).as_ref() == Some(&PlaylistRequestType::Target), {
<MenuItem icon="Clipboard" name={ExplorerAction::CopyLinkTuliprox.to_string()} label={translate.t("LABEL.COPY_LINK_TULIPROX")} onclick={&handle_menu_click}></MenuItem>
})
}
<MenuItem icon="Clipboard" name={ExplorerAction::CopyLinkProvider.to_string()} label={translate.t("LABEL.COPY_LINK_PROVIDER")} onclick={&handle_menu_click}></MenuItem>
</PopupMenu>
</div>
}
}
}
@@ -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::<Rc<UiPlaylistCategories>>);
let playlist_req_type = use_state(|| None::<PlaylistRequestType>);
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! {
@@ -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));
});
+2 -1
View File
@@ -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<Rc<InputRow>>, Vec<Rc<ConfigTargetDto>>);
@@ -20,6 +20,7 @@ pub struct PlaylistEditorContext {
pub struct PlaylistExplorerContext {
pub active_page: UseStateHandle<PlaylistExplorerPage>,
pub playlist: UseStateHandle<Option<Rc<UiPlaylistCategories>>>,
pub playlist_request_type: UseStateHandle<Option<PlaylistRequestType>>,
}