mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-30 18:52:31 +01:00
Merge pull request #463 from euzu/feature/shared_stream_custom_response
Feature/shared stream custom response
This commit is contained in:
+3
-1
@@ -29,4 +29,6 @@ docker/tuliprox
|
||||
docker/resources
|
||||
|
||||
# Ignore VSCode settings and files
|
||||
/.vscode
|
||||
/.vscode
|
||||
|
||||
.claude
|
||||
|
||||
Generated
+3
-3
@@ -1096,7 +1096,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "frontend"
|
||||
version = "3.2.13"
|
||||
version = "3.2.14"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -3793,7 +3793,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shared"
|
||||
version = "3.2.13"
|
||||
version = "3.2.14"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bitflags 2.10.0",
|
||||
@@ -4356,7 +4356,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tuliprox"
|
||||
version = "3.2.13"
|
||||
version = "3.2.14"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-compression",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuliprox"
|
||||
version = "3.2.13"
|
||||
version = "3.2.14"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::endpoints::xtream_api::{get_xtream_player_api_stream_url, ApiStreamContext};
|
||||
use crate::api::model::UserSession;
|
||||
use crate::api::model::{UserSession};
|
||||
use crate::api::model::{
|
||||
create_channel_unavailable_stream, create_custom_video_stream_response,
|
||||
create_provider_connections_exhausted_stream, create_provider_stream,
|
||||
@@ -17,12 +17,12 @@ use crate::utils::request;
|
||||
use crate::utils::{debug_if_enabled, trace_if_enabled};
|
||||
use crate::BUILD_TIMESTAMP;
|
||||
use arc_swap::ArcSwapOption;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::{HeaderMap};
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
use log::{debug, error, log_enabled, trace, warn};
|
||||
use log::{debug, error, log_enabled, trace, warn};
|
||||
use reqwest::header::RETRY_AFTER;
|
||||
use serde::Serialize;
|
||||
use shared::model::{Claims, InputFetchMethod, PlaylistEntry, PlaylistItemType, StreamChannel, TargetType, UserConnectionPermission, XtreamCluster};
|
||||
@@ -373,16 +373,13 @@ async fn resolve_streaming_strategy(
|
||||
)
|
||||
};
|
||||
|
||||
let available = match allocation {
|
||||
ProviderAllocation::Exhausted
|
||||
| ProviderAllocation::Available(_) => true,
|
||||
ProviderAllocation::GracePeriod(_) => false,
|
||||
};
|
||||
|
||||
if available {
|
||||
ProviderStreamState::Available(Some(provider), url)
|
||||
} else {
|
||||
ProviderStreamState::GracePeriod(Some(provider), url)
|
||||
match allocation {
|
||||
ProviderAllocation::Exhausted => {
|
||||
let stream = create_provider_connections_exhausted_stream(&app_state.app_config, &[]);
|
||||
ProviderStreamState::Custom(stream)
|
||||
},
|
||||
ProviderAllocation::Available(_) => ProviderStreamState::Available(Some(provider), url),
|
||||
ProviderAllocation::GracePeriod(_) => ProviderStreamState::GracePeriod(Some(provider), url),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +415,7 @@ fn get_grace_period_millis(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||
async fn create_stream_response_details(
|
||||
app_state: &Arc<AppState>,
|
||||
stream_options: &StreamOptions,
|
||||
@@ -450,6 +447,22 @@ async fn create_stream_response_details(
|
||||
.as_ref()
|
||||
.and_then(|guard| guard.allocation.get_provider_name());
|
||||
|
||||
if let ProviderStreamState::GracePeriod(ref provider_grace_check, ref stream_url) = streaming_strategy.provider_stream_state {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(grace_period_millis)).await;
|
||||
if let Some(provider_name) = provider_grace_check.as_ref() {
|
||||
if app_state.active_provider.is_over_limit(provider_name).await {
|
||||
app_state.connection_manager.update_stream_detail(&fingerprint.addr, CustomVideoStreamType::ProviderConnectionsExhausted).await;
|
||||
debug!("Provider connections exhausted after grace period for provider: {provider_name}");
|
||||
let stream = create_provider_connections_exhausted_stream(&app_state.app_config, &[]);
|
||||
streaming_strategy.provider_stream_state = ProviderStreamState::Custom(stream);
|
||||
let provider_handle = streaming_strategy.provider_handle.take();
|
||||
app_state.connection_manager.release_provider_handle(provider_handle).await;
|
||||
}
|
||||
} else {
|
||||
streaming_strategy.provider_stream_state = ProviderStreamState::Available(provider_grace_check.clone(), stream_url.clone());
|
||||
}
|
||||
}
|
||||
|
||||
match streaming_strategy.provider_stream_state {
|
||||
// custom stream means we display our own stream like connection exhausted, channel-unavailable...
|
||||
ProviderStreamState::Custom(provider_stream) => {
|
||||
@@ -837,8 +850,7 @@ pub async fn stream_response(
|
||||
share_stream,
|
||||
connection_permission,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
).await;
|
||||
|
||||
if stream_details.has_stream() {
|
||||
// let content_length = get_stream_content_length(provider_response.as_ref());
|
||||
@@ -853,15 +865,20 @@ pub async fn stream_response(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
stream_channel.shared = share_stream;
|
||||
|
||||
let mut is_stream_shared = share_stream;
|
||||
if let Some((_header, _status_code, _url, Some(_custom_video))) = stream_details.stream_info.as_ref() {
|
||||
if stream_details.stream.is_some() {
|
||||
is_stream_shared = false;
|
||||
}
|
||||
}
|
||||
|
||||
stream_channel.shared = is_stream_shared;
|
||||
let stream =
|
||||
ActiveClientStream::new(stream_details, app_state, user, connection_permission, fingerprint, stream_channel, Some(session_token), req_headers)
|
||||
.await;
|
||||
let stream_resp = if share_stream {
|
||||
debug_if_enabled!(
|
||||
"Streaming shared stream request from {}",
|
||||
sanitize_sensitive_info(stream_url)
|
||||
);
|
||||
let stream_resp = if is_stream_shared {
|
||||
debug_if_enabled!("Streaming shared stream request from {}",sanitize_sensitive_info(stream_url));
|
||||
// Shared Stream response
|
||||
let shared_headers = provider_response
|
||||
.as_ref()
|
||||
@@ -898,9 +915,7 @@ pub async fn stream_response(
|
||||
);
|
||||
if log_enabled!(log::Level::Debug) {
|
||||
if session_url.eq(&stream_url) {
|
||||
debug!(
|
||||
"Streaming stream request from {}",
|
||||
sanitize_sensitive_info(stream_url)
|
||||
debug!("Streaming stream request from {}", sanitize_sensitive_info(stream_url)
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
@@ -977,9 +992,7 @@ async fn try_shared_stream_response_if_any(
|
||||
if let Some((stream, provider)) =
|
||||
SharedStreamManager::subscribe_shared_stream(app_state, stream_url, &fingerprint.addr).await
|
||||
{
|
||||
debug_if_enabled!(
|
||||
"Using shared stream {}",
|
||||
sanitize_sensitive_info(stream_url)
|
||||
debug_if_enabled!("Using shared stream {}", sanitize_sensitive_info(stream_url)
|
||||
);
|
||||
if let Some(headers) = app_state
|
||||
.shared_stream_manager
|
||||
@@ -991,6 +1004,7 @@ async fn try_shared_stream_response_if_any(
|
||||
axum::http::StatusCode::OK,
|
||||
)));
|
||||
let mut stream_details = StreamDetails::from_stream(stream);
|
||||
|
||||
stream_details.provider_name = provider;
|
||||
stream_channel.shared = true;
|
||||
let stream =
|
||||
|
||||
@@ -210,7 +210,7 @@ impl SharedStreamState {
|
||||
result = broadcast_rx.recv() => {
|
||||
match result {
|
||||
Ok(data) => {
|
||||
// Wenn der Client pausiert, einfach skippen oder warten
|
||||
// If the client press pause, skip
|
||||
if client_tx_clone.is_closed() {
|
||||
continue;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "frontend"
|
||||
version = "3.2.13"
|
||||
version = "3.2.14"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
shared = { version = "3.2.13", path = "../shared" }
|
||||
shared = { version = "3.2.14", path = "../shared" }
|
||||
chrono = "0"
|
||||
yew = "0.21"
|
||||
yew-router = "0.18"
|
||||
|
||||
@@ -274,8 +274,13 @@
|
||||
"WATCH": "Watch",
|
||||
"INPUT_TYPE": "Input Type",
|
||||
"EPG": "Epg",
|
||||
"EPG_SOURCE_URL": "EPG Source URL",
|
||||
"EPG_SOURCES": "EPG Sources",
|
||||
"ADD_EPG_SOURCE": "Add EPG Source",
|
||||
"PERSIST": "Persist",
|
||||
"ALIASES": "Aliases",
|
||||
"ALIAS_NAME": "Alias Name",
|
||||
"ADD_ALIAS": "Add Alias",
|
||||
"MAX_CON": "Max Con.",
|
||||
"MAX_CONNECTIONS": "Max Connections",
|
||||
"METHOD": "Method",
|
||||
@@ -320,14 +325,25 @@
|
||||
"SKIP_SERIES": "Skip Series",
|
||||
"ADD_WATCH": "Add Watch",
|
||||
"ADD_MAPPING": "Add Mapping",
|
||||
"SKIP_LIVE_DIRECT_SOURCE": "Skip Live Direct Source",
|
||||
"SKIP_VOD_DIRECT_SOURCE": "Skip Vod Direct Source",
|
||||
"SKIP_SERIES_DIRECT_SOURCE": "Skip Series Direct Source",
|
||||
"RESOLVE_VOD": "Resolve Vod",
|
||||
"RESOLVE_VOD_DELAY_SEC": "Resolve Vod Delay (sec)",
|
||||
"RESOLVE_SERIES": "Resolve Series",
|
||||
"RESOLVE_SERIES_DELAY_SEC": "Resolve Series Delay (sec)",
|
||||
"COPY_CREDENTIALS": "Copy Credentials"
|
||||
"RESOLVE_DELAY_SEC": "Resolve Delay (sec)",
|
||||
"COPY_CREDENTIALS": "Copy Credentials",
|
||||
"EXPORT_STYLE": "Export Style",
|
||||
"STRM_PROPS": "Properties",
|
||||
"ADD_PROPERTY": "Add Property",
|
||||
"TRAKT_API_KEY": "API-Key",
|
||||
"TRAKT_API_VERSION": "API-Version",
|
||||
"TRAKT_API_URL": "API-URL",
|
||||
"TRAKT_LISTS": "Lists",
|
||||
"TRAKT_USER": "Trakt User",
|
||||
"TRAKT_LIST_SLUG": "List Slug",
|
||||
"TRAKT_CATEGORY_NAME": "Category name",
|
||||
"TRAKT_CONTENT_TYPE": "Content Type",
|
||||
"TRAKT_FUZZY_MATCH_THRESHOLD": "Fuzzy matchthreshold",
|
||||
"ADD_TRAKT_LIST": "Add a Trakt list",
|
||||
"EPG_LOGO_OVERRIDE": "Logo Override",
|
||||
"API_CONFIGURATION" : "API Configuration",
|
||||
"HEADER_NAME": "Header Name",
|
||||
"HEADER_VALUE": "Header Value"
|
||||
},
|
||||
"TITLE": {
|
||||
"USER_BOUQUET_EDITOR": "Playlist Category Selection"
|
||||
@@ -337,7 +353,7 @@
|
||||
"INVALID_REGEXP": "Invalid regexp",
|
||||
"INVALID_FILETYPE": "Invalid filetype",
|
||||
"TARGET_NOT_EXISTS": "Target does not exist",
|
||||
"CONFIRM_DELETE": "You really want to delete?",
|
||||
"CONFIRM_DELETE": "You really want to delete it?",
|
||||
"CONFIG_CHANGED": "Configuration on server changed.",
|
||||
"USER_DELETED": "User successfully deleted",
|
||||
"SCHEDULE_NOT_SET": "Please enter a schedule before adding",
|
||||
@@ -369,11 +385,11 @@
|
||||
"SAVE": {
|
||||
"MAIN_CONFIG": {
|
||||
"SUCCESS": "Main config saved!",
|
||||
"FAIL": "Failed to save main config!"
|
||||
"FAIL": "Failed to save the main config!"
|
||||
},
|
||||
"BOUQUET": {
|
||||
"SUCCESS": "Successfully saved bouquet",
|
||||
"FAIL": "Failed to save bouquet!"
|
||||
"FAIL": "Failed to save the bouquet!"
|
||||
},
|
||||
"USER": {
|
||||
"SUCCESS": "Successfully saved user",
|
||||
@@ -384,7 +400,7 @@
|
||||
"TARGET_NOT_FOUND": "Target not found!",
|
||||
"MAX_CONNECTIONS_INVALID": "MaxConnections invalid",
|
||||
"NOTHING_TO_SAVE": "Nothing changed",
|
||||
"TARGET_NOT_SELECTED": "Playlist not selected"
|
||||
"TARGET_NOT_SELECTED": "Playlist isn't selected"
|
||||
}
|
||||
},
|
||||
"PLAYLIST_UPDATE": {
|
||||
@@ -405,7 +421,7 @@
|
||||
"INFO": {
|
||||
"RESTART_TO_APPLY_CHANGES": "You need to restart to apply changes.",
|
||||
"SCHEDULE_EXAMPLE": "Schedule example",
|
||||
"PROCESS_PARALLEL": "Process Parallel: If you have multiple providers you can enable it. Accessing your provider in parallel can cause a ban.",
|
||||
"PROCESS_PARALLEL": "Process Parallel: If you have multiple providers, you can enable it. Accessing your provider in parallel can cause a ban.",
|
||||
"WEB_ROOT": "Web-Root: Contains the web-ui files. They are normally placed in the same directory under ./web"
|
||||
},
|
||||
"HINT": {
|
||||
@@ -423,7 +439,7 @@
|
||||
"RESOURCE_REWRITE_DISABLE": "If activated, the URL rewrite of resources in reverse proxy mode is disabled",
|
||||
"DISABLED_HEADER": "Remove headers before sending requests to the upstream provider",
|
||||
"REFERER_HEADER": "If activated, the referer header is not sent to the provider",
|
||||
"X_HEADER": "If activated, headers starting with X- are removed before proxying the request",
|
||||
"X_HEADER": "If activated, headers starting with 'X-' are removed before proxying the request",
|
||||
"CUSTOM_HEADERS": "Headers listed here are removed before sending the upstream request",
|
||||
"THROTTLE": "Supported units are KB/s,MB/s,KiB/s,MiB/s,kbps,mbps,Mibps"
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"Delete"
|
||||
"Delete", "Remove"
|
||||
],
|
||||
"path": "M 4.1422511,6.2868364 H 2.7137817 a 0.7142347,0.7142347 0 0 1 0,-1.4284691 H 8.4276589 V 2.7142341 A 0.7142347,0.7142347 0 0 1 9.1418939,2 h 5.7138771 a 0.7142347,0.7142347 0 0 1 0.714235,0.7142341 v 2.1441332 h 5.713877 a 0.7142347,0.7142347 0 1 1 0,1.4284691 H 19.855414 V 21.285765 A 0.7142347,0.7142347 0 0 1 19.141179,22 H 4.8564857 A 0.7142347,0.7142347 0 0 1 4.1422511,21.285765 Z M 14.141537,4.8583673 v -1.42847 H 9.8561289 v 1.42847 z M 5.5707207,20.571531 H 18.426945 V 6.2868364 H 5.5707207 Z M 9.8561289,17.714592 A 0.7142347,0.7142347 0 0 1 9.1418939,17.000357 V 9.8580104 a 0.7142347,0.7142347 0 0 1 1.4284691,0 v 7.1423466 a 0.7142347,0.7142347 0 0 1 -0.7142341,0.714235 z m 4.2854081,0 A 0.7142347,0.7142347 0 0 1 13.427302,17.000357 V 9.8580104 a 0.7142347,0.7142347 0 0 1 1.428469,0 v 7.1423466 a 0.7142347,0.7142347 0 0 1 -0.714234,0.714235 z"
|
||||
},
|
||||
@@ -525,5 +525,9 @@
|
||||
{
|
||||
"keys": ["Accept"],
|
||||
"path": "m18 7-1.41-1.41-6.34 6.34 1.41 1.41zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12zM.41 13.41 6 19l1.41-1.41L1.83 12z"
|
||||
},
|
||||
{
|
||||
"keys": [ "Add" ],
|
||||
"path": "M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4V7zm-1-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"
|
||||
}
|
||||
]
|
||||
@@ -239,6 +239,7 @@
|
||||
--menu-button-hover-color: #ffffff;
|
||||
--menu-button-hover-background-color: rgba(32, 39, 51, 0.8);
|
||||
|
||||
--menu-button-warn-color: #b96665;
|
||||
--menu-button-warn-hover-color: #dd7573;
|
||||
--menu-button-warn-hover-background-color: #4c3434;
|
||||
|
||||
|
||||
@@ -80,3 +80,4 @@
|
||||
@forward "components/source_editor/source_editor_block";
|
||||
@forward "components/source_editor/input_form";
|
||||
@forward "components/cluster_flags";
|
||||
@forward "components/titled_card";
|
||||
|
||||
@@ -21,11 +21,10 @@
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
color: var(--modest-text-color);
|
||||
.svg-icon {
|
||||
fill: var(--menu-button-warn-hover-color);
|
||||
}
|
||||
fill: var(--menu-button-warn-color);
|
||||
&:hover {
|
||||
color: var(--menu-button-warn-hover-color);
|
||||
fill: var(--menu-button-warn-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.tp__titled-card {
|
||||
position: relative;
|
||||
padding: 1em 0.5em 0.5em 0.5em;
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.tp__titled-card__title {
|
||||
position: absolute;
|
||||
top: -0.7em;
|
||||
left: 15px;
|
||||
padding: 0 4px;
|
||||
z-index: 2;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
background: linear-gradient(to bottom, #00000000 50%, var(--card-background-color) 50%);
|
||||
}
|
||||
@@ -8,6 +8,8 @@ $toggle-switch-toggle-end-pos: calc($toggle-switch-width - ($toggle-switch-toggl
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: $toggle-switch-width;
|
||||
min-width: $toggle-switch-width;
|
||||
max-width: $toggle-switch-width;
|
||||
height: $toggle-switch-height;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
|
||||
@@ -59,11 +59,36 @@
|
||||
gap: var(--gap-large);
|
||||
background-color: var(--sub-card-background-color);
|
||||
overflow: hidden;
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__cols-2,
|
||||
&__cols-3 {
|
||||
display: grid;
|
||||
gap: var(--gap-default);
|
||||
|
||||
.tp__form-field {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__cols-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@media (max-width: size.$mobile-breakpoint) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
&__cols-3 {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
@media (max-width: size.$mobile-breakpoint) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tp__config-view-page {
|
||||
@@ -111,6 +136,7 @@
|
||||
gap: var(--gap-default);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__schedule {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
@@ -123,14 +149,16 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th,td {
|
||||
th, td {
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -141,28 +169,34 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: var(--margin-default);
|
||||
padding-top: var(--margin-default);
|
||||
border-top : 1px solid var(--card-border-color);
|
||||
border-top: 1px solid var(--card-border-color);
|
||||
|
||||
svg {
|
||||
width: 320px;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
fill: var(--label-color);
|
||||
}
|
||||
|
||||
&__editor {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: flex-end;
|
||||
gap: var(--gap-default);
|
||||
box-sizing: border-box;
|
||||
|
||||
.tp__icon-button {
|
||||
svg {
|
||||
width: 2.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__delete-btn {
|
||||
fill: var(--menu-button-warn-hover-color);
|
||||
}
|
||||
@@ -174,6 +208,7 @@
|
||||
border-color: var(--warn-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__inactive {
|
||||
button {
|
||||
border-color: var(--warn-color);
|
||||
@@ -205,8 +240,9 @@
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
color:var(--warn-color);
|
||||
color: var(--warn-color);
|
||||
fill: currentColor;
|
||||
|
||||
span {
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$form-width: 300px;
|
||||
$form-width: 380px;
|
||||
|
||||
.tp__source-editor-form-wrapper {
|
||||
position: absolute;
|
||||
@@ -38,4 +38,17 @@ $form-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tp__form-list__item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: var(--gap-small);
|
||||
align-items: center;
|
||||
.tp__icon-button {
|
||||
fill: var(--menu-button-warn-color);
|
||||
&:hover {
|
||||
fill: var(--menu-button-warn-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ mod websocket_status;
|
||||
mod floating_background;
|
||||
mod source_editor;
|
||||
mod cluster_flags_input;
|
||||
mod title_card;
|
||||
// pub use self::input::*;
|
||||
// pub use self::menu_item::*;
|
||||
// pub use self::popup_menu::*;
|
||||
@@ -92,6 +93,7 @@ pub(crate) use self::key_value_editor::*;
|
||||
pub(crate) use self::loading_screen::*;
|
||||
pub(crate) use self::websocket_status::*;
|
||||
pub(crate) use self::source_editor::*;
|
||||
pub(crate) use self::title_card::*;
|
||||
|
||||
//pub use self::number_input::*;
|
||||
//pub use self::date_input::*;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
use crate::app::components::{Card, TextButton};
|
||||
use crate::{edit_field_date, edit_field_number_i16, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::ConfigInputAliasDto;
|
||||
use yew::{function_component, html, use_reducer, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_ALIAS_NAME: &str = "LABEL.ALIAS_NAME";
|
||||
const LABEL_URL: &str = "LABEL.URL";
|
||||
const LABEL_USERNAME: &str = "LABEL.USERNAME";
|
||||
const LABEL_PASSWORD: &str = "LABEL.PASSWORD";
|
||||
const LABEL_PRIORITY: &str = "LABEL.PRIORITY";
|
||||
const LABEL_MAX_CONNECTIONS: &str = "LABEL.MAX_CONNECTIONS";
|
||||
const LABEL_EXP_DATE: &str = "LABEL.EXP_DATE";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: AliasFormState { form: ConfigInputAliasDto },
|
||||
action_name: AliasFormAction,
|
||||
fields {
|
||||
Name => name: String,
|
||||
Url => url: String,
|
||||
Username => username: Option<String>,
|
||||
Password => password: Option<String>,
|
||||
Priority => priority: i16,
|
||||
MaxConnections => max_connections: u16,
|
||||
ExpDate => exp_date: Option<i64>,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct AliasItemFormProps {
|
||||
pub on_submit: Callback<ConfigInputAliasDto>,
|
||||
pub on_cancel: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub initial: Option<ConfigInputAliasDto>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn AliasItemForm(props: &AliasItemFormProps) -> Html {
|
||||
let translate = use_translation();
|
||||
|
||||
let form_state: UseReducerHandle<AliasFormState> = use_reducer(|| {
|
||||
AliasFormState {
|
||||
form: props.initial.clone().unwrap_or_else(|| ConfigInputAliasDto {
|
||||
id: 0,
|
||||
name: String::new(),
|
||||
url: String::new(),
|
||||
username: None,
|
||||
password: None,
|
||||
priority: 0,
|
||||
max_connections: 1,
|
||||
exp_date: None,
|
||||
}),
|
||||
modified: false,
|
||||
}
|
||||
});
|
||||
|
||||
let handle_submit = {
|
||||
let form_state = form_state.clone();
|
||||
let on_submit = props.on_submit.clone();
|
||||
Callback::from(move |_| {
|
||||
let data = form_state.form.clone();
|
||||
if !data.name.trim().is_empty() && !data.url.trim().is_empty() {
|
||||
on_submit.emit(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let on_cancel = props.on_cancel.clone();
|
||||
Callback::from(move |_| {
|
||||
on_cancel.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card tp__item-form">
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_ALIAS_NAME), name, AliasFormAction::Name) }
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_URL), url, AliasFormAction::Url) }
|
||||
<div class="tp__config-view__cols-2">
|
||||
{ edit_field_text_option!(form_state, translate.t(LABEL_USERNAME), username, AliasFormAction::Username) }
|
||||
{ edit_field_text_option!(form_state, translate.t(LABEL_PASSWORD), password, AliasFormAction::Password, true) }
|
||||
{ edit_field_number_i16!(form_state, translate.t(LABEL_PRIORITY), priority, AliasFormAction::Priority) }
|
||||
{ edit_field_number_u16!(form_state, translate.t(LABEL_MAX_CONNECTIONS), max_connections, AliasFormAction::MaxConnections) }
|
||||
</div>
|
||||
{ edit_field_date!(form_state, translate.t(LABEL_EXP_DATE), exp_date, AliasFormAction::ExpDate) }
|
||||
|
||||
<div class="tp__form-page__toolbar">
|
||||
<TextButton
|
||||
class="secondary"
|
||||
name="cancel_alias"
|
||||
icon="Cancel"
|
||||
title={translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}
|
||||
/>
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="submit_alias"
|
||||
icon="Accept"
|
||||
title={translate.t("LABEL.SUBMIT")}
|
||||
onclick={handle_submit}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use crate::app::components::{Card, TextButton};
|
||||
use crate::{edit_field_bool, edit_field_number_i16, edit_field_text, generate_form_reducer};
|
||||
use shared::model::EpgSourceDto;
|
||||
use yew::{function_component, html, use_reducer, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_EPG_SOURCE_URL: &str = "LABEL.EPG_SOURCE_URL";
|
||||
const LABEL_EPG_PRIORITY: &str = "LABEL.PRIORITY";
|
||||
const LABEL_EPG_LOGO_OVERRIDE: &str = "LABEL.EPG_LOGO_OVERRIDE";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: EpgSourceFormState { form: EpgSourceDto },
|
||||
action_name: EpgSourceFormAction,
|
||||
fields {
|
||||
Url => url: String,
|
||||
Priority => priority: i16,
|
||||
LogoOverride => logo_override: bool,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct EpgSourceItemFormProps {
|
||||
pub on_submit: Callback<EpgSourceDto>,
|
||||
pub on_cancel: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub initial: Option<EpgSourceDto>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn EpgSourceItemForm(props: &EpgSourceItemFormProps) -> Html {
|
||||
let translate = use_translation();
|
||||
|
||||
let form_state: UseReducerHandle<EpgSourceFormState> = use_reducer(|| {
|
||||
EpgSourceFormState {
|
||||
form: props.initial.clone().unwrap_or_else(|| EpgSourceDto {
|
||||
url: String::new(),
|
||||
priority: 0,
|
||||
logo_override: false,
|
||||
}),
|
||||
modified: false,
|
||||
}
|
||||
});
|
||||
|
||||
let handle_submit = {
|
||||
let form_state = form_state.clone();
|
||||
let on_submit = props.on_submit.clone();
|
||||
Callback::from(move |_| {
|
||||
let data = form_state.form.clone();
|
||||
if !data.url.trim().is_empty() {
|
||||
on_submit.emit(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let on_cancel = props.on_cancel.clone();
|
||||
Callback::from(move |_| {
|
||||
on_cancel.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card tp__item-form">
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_EPG_SOURCE_URL), url, EpgSourceFormAction::Url) }
|
||||
{ edit_field_number_i16!(form_state, translate.t(LABEL_EPG_PRIORITY), priority, EpgSourceFormAction::Priority) }
|
||||
{ edit_field_bool!(form_state, translate.t(LABEL_EPG_LOGO_OVERRIDE), logo_override, EpgSourceFormAction::LogoOverride) }
|
||||
|
||||
<div class="tp__form-page__toolbar">
|
||||
<TextButton
|
||||
class="secondary"
|
||||
name="cancel_epg_source"
|
||||
icon="Cancel"
|
||||
title={translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}
|
||||
/>
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="submit_epg_source"
|
||||
icon="Accept"
|
||||
title={translate.t("LABEL.SUBMIT")}
|
||||
onclick={handle_submit}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,36 @@
|
||||
use crate::app::components::config::HasFormData;
|
||||
use crate::app::components::key_value_editor::KeyValueEditor;
|
||||
use crate::app::components::select::Select;
|
||||
use crate::app::components::{BlockId, BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, Panel, RadioButtonGroup, SourceEditorContext, TextButton};
|
||||
use crate::{config_field_child, edit_field_bool, edit_field_number_i16, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::{ConfigInputDto, ConfigInputOptionsDto, InputFetchMethod, InputType, StagedInputDto};
|
||||
use crate::app::components::{AliasItemForm, BlockId, BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, EpgSourceItemForm, IconButton, Panel, RadioButtonGroup, SourceEditorContext, TextButton, TitledCard};
|
||||
use crate::{config_field_child, edit_field_bool, edit_field_date, edit_field_number_i16, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::{ConfigInputAliasDto, ConfigInputDto, ConfigInputOptionsDto, EpgConfigDto, EpgSourceDto, InputFetchMethod, InputType, StagedInputDto};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use web_sys::MouseEvent;
|
||||
use yew::{classes, function_component, html, use_context, use_effect_with, use_memo, use_reducer, use_state, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_NAME: &str = "LABEL.NAME";
|
||||
const LABEL_INPUT_TYPE: &str = "LABEL.INPUT_TYPE";
|
||||
const LABEL_FETCH_METHOD: &str = "LABEL.METHOD";
|
||||
// const LABEL_HEADERS: &str = "LABEL.HEADERS";
|
||||
const LABEL_HEADERS: &str = "LABEL.HEADERS";
|
||||
const LABEL_URL: &str = "LABEL.URL";
|
||||
// const LABEL_EPG: &str = "LABEL.EPG";
|
||||
const LABEL_EPG_SOURCES: &str = "LABEL.EPG_SOURCES";
|
||||
const LABEL_USERNAME: &str = "LABEL.USERNAME";
|
||||
const LABEL_PASSWORD: &str = "LABEL.PASSWORD";
|
||||
const LABEL_PERSIST: &str = "LABEL.PERSIST";
|
||||
const LABEL_ENABLED: &str = "LABEL.ENABLED";
|
||||
// const LABEL_ALIASES: &str = "LABEL.ALIASES";
|
||||
const LABEL_ALIASES: &str = "LABEL.ALIASES";
|
||||
const LABEL_PRIORITY: &str = "LABEL.PRIORITY";
|
||||
const LABEL_MAX_CONNECTIONS: &str = "LABEL.MAX_CONNECTIONS";
|
||||
// const LABEL_ADD_HEADER: &str = "LABEL.HEADERS";
|
||||
const LABEL_XTREAM_SKIP_LIVE: &str = "LABEL.SKIP_LIVE";
|
||||
const LABEL_XTREAM_SKIP_VOD: &str = "LABEL.SKIP_VOD";
|
||||
const LABEL_XTREAM_SKIP_SERIES: &str = "LABEL.SKIP_SERIES";
|
||||
const LABEL_EXP_DATE: &str = "LABEL.EXP_DATE";
|
||||
const LABEL_ADD_EPG_SOURCE: &str = "LABEL.ADD_EPG_SOURCE";
|
||||
const LABEL_ADD_ALIAS: &str = "LABEL.ADD_ALIAS";
|
||||
const LABEL_SKIP: &str = "LABEL.SKIP";
|
||||
const LABEL_XTREAM_SKIP_LIVE: &str = "LABEL.LIVE";
|
||||
const LABEL_XTREAM_SKIP_VOD: &str = "LABEL.VOD";
|
||||
const LABEL_XTREAM_SKIP_SERIES: &str = "LABEL.SERIES";
|
||||
const LABEL_XTREAM_LIVE_STREAM_USE_PREFIX: &str = "LABEL.LIVE_STREAM_USE_PREFIX";
|
||||
const LABEL_XTREAM_LIVE_STREAM_WITHOUT_EXTENSION: &str = "LABEL.LIVE_STREAM_WITHOUT_EXTENSION";
|
||||
|
||||
@@ -34,28 +40,20 @@ enum InputFormPage {
|
||||
Main,
|
||||
Options,
|
||||
Staged,
|
||||
Advanced,
|
||||
}
|
||||
|
||||
impl Display for InputFormPage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", match *self {
|
||||
InputFormPage::Main => "Main".to_string(),
|
||||
InputFormPage::Options => "Options".to_string(),
|
||||
InputFormPage::Staged => "Staged".to_string(),
|
||||
InputFormPage::Main => "Main",
|
||||
InputFormPage::Options => "Options",
|
||||
InputFormPage::Staged => "Staged",
|
||||
InputFormPage::Advanced => "Advanced",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// generate_form_reducer!(
|
||||
// state: EpgConfigDtoFormState { form: EpgConfigDto },
|
||||
// action_name: EpgConfigDtoFormAction,
|
||||
// fields {
|
||||
// Enabled => enabled: bool,
|
||||
// PeriodMillis => period_millis: u64,
|
||||
// BurstSize => burst_size: u32,
|
||||
// }
|
||||
// );
|
||||
|
||||
generate_form_reducer!(
|
||||
state: ConfigInputOptionsDtoFormState { form: ConfigInputOptionsDto },
|
||||
action_name: ConfigInputOptionsFormAction,
|
||||
@@ -85,17 +83,16 @@ generate_form_reducer!(
|
||||
state: ConfigInputFormState { form: ConfigInputDto },
|
||||
action_name: ConfigInputFormAction,
|
||||
fields {
|
||||
Name => name: String,
|
||||
//InputType => input_type: InputType,
|
||||
// Headers => headers: HashMap<String, String>,
|
||||
Url => url: String,
|
||||
Username => username: Option<String>,
|
||||
Password => password: Option<String>,
|
||||
Persist => persist: Option<String>,
|
||||
Enabled => enabled: bool,
|
||||
Priority => priority: i16,
|
||||
MaxConnections => max_connections: u16,
|
||||
Method => method: InputFetchMethod,
|
||||
Name => name: String,
|
||||
Url => url: String,
|
||||
Username => username: Option<String>,
|
||||
Password => password: Option<String>,
|
||||
Persist => persist: Option<String>,
|
||||
Enabled => enabled: bool,
|
||||
Priority => priority: i16,
|
||||
MaxConnections => max_connections: u16,
|
||||
Method => method: InputFetchMethod,
|
||||
ExpDate => exp_date: Option<i64>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -107,7 +104,6 @@ pub struct ConfigInputViewProps {
|
||||
|
||||
#[function_component]
|
||||
pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
|
||||
let translate = use_translation();
|
||||
let source_editor_ctx = use_context::<SourceEditorContext>().expect("SourceEditorContext not found");
|
||||
let fetch_methods = use_memo((), |_| {
|
||||
@@ -139,6 +135,15 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
modified: false,
|
||||
});
|
||||
|
||||
// State for EPG sources, Aliases, and Headers
|
||||
let epg_sources_state = use_state(Vec::<EpgSourceDto>::new);
|
||||
let aliases_state = use_state(Vec::<ConfigInputAliasDto>::new);
|
||||
let headers_state = use_state(HashMap::<String, String>::new);
|
||||
|
||||
// State for showing item forms
|
||||
let show_epg_form_state = use_state(|| false);
|
||||
let show_alias_form_state = use_state(|| false);
|
||||
|
||||
let staged_input_types = use_memo(staged_input_state.form.input_type, |input_type| {
|
||||
let default_it = input_type;
|
||||
[
|
||||
@@ -159,42 +164,133 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
let input_form_state = input_form_state.clone();
|
||||
let input_options_state = input_options_state.clone();
|
||||
let staged_input_state = staged_input_state.clone();
|
||||
let epg_sources_state = epg_sources_state.clone();
|
||||
let aliases_state = aliases_state.clone();
|
||||
let headers_state = headers_state.clone();
|
||||
|
||||
let config_input = props.input.clone();
|
||||
|
||||
use_effect_with(config_input, move |cfg| {
|
||||
if let Some(input) = cfg {
|
||||
input_form_state.dispatch(ConfigInputFormAction::SetAll(input.as_ref().clone()));
|
||||
|
||||
input_options_state.dispatch(ConfigInputOptionsFormAction::SetAll(
|
||||
input
|
||||
.options
|
||||
.as_ref()
|
||||
.map_or_else(ConfigInputOptionsDto::default, |d| d.clone()),
|
||||
input.options.as_ref().map_or_else(ConfigInputOptionsDto::default, |d| d.clone()),
|
||||
));
|
||||
|
||||
staged_input_state.dispatch(StagedInputFormAction::SetAll(
|
||||
input
|
||||
.staged
|
||||
.as_ref()
|
||||
.map_or_else(StagedInputDto::default, |c| c.clone()),
|
||||
input.staged.as_ref().map_or_else(StagedInputDto::default, |c| c.clone()),
|
||||
));
|
||||
|
||||
// Load headers
|
||||
headers_state.set(input.headers.clone());
|
||||
|
||||
// Load EPG sources
|
||||
epg_sources_state.set(input.epg.as_ref().and_then(|epg| epg.sources.clone()).unwrap_or_default());
|
||||
|
||||
// Load aliases
|
||||
aliases_state.set(input.aliases.clone().unwrap_or_default());
|
||||
} else {
|
||||
input_form_state.dispatch(ConfigInputFormAction::SetAll(ConfigInputDto::default()));
|
||||
input_options_state.dispatch(ConfigInputOptionsFormAction::SetAll(
|
||||
ConfigInputOptionsDto::default(),
|
||||
));
|
||||
staged_input_state
|
||||
.dispatch(StagedInputFormAction::SetAll(StagedInputDto::default()));
|
||||
input_options_state.dispatch(ConfigInputOptionsFormAction::SetAll(ConfigInputOptionsDto::default()));
|
||||
staged_input_state.dispatch(StagedInputFormAction::SetAll(StagedInputDto::default()));
|
||||
headers_state.set(HashMap::new());
|
||||
epg_sources_state.set(Vec::new());
|
||||
aliases_state.set(Vec::new());
|
||||
}
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let handle_add_epg_item = {
|
||||
let epg_sources = epg_sources_state.clone();
|
||||
let show_epg_form = show_epg_form_state.clone();
|
||||
Callback::from(move |source: EpgSourceDto| {
|
||||
let mut sources = (*epg_sources).clone();
|
||||
sources.push(source);
|
||||
epg_sources.set(sources);
|
||||
show_epg_form.set(false);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_close_add_epg_item = {
|
||||
let show_epg_form = show_epg_form_state.clone();
|
||||
Callback::from(move |_| {
|
||||
show_epg_form.set(false);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_show_add_epg_item = {
|
||||
let show_epg_form = show_epg_form_state.clone();
|
||||
Callback::from(move |_| {
|
||||
show_epg_form.set(true);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_add_alias_item = {
|
||||
let aliases = aliases_state.clone();
|
||||
let show_alias_form = show_alias_form_state.clone();
|
||||
Callback::from(move |alias: ConfigInputAliasDto| {
|
||||
let mut items = (*aliases).clone();
|
||||
items.push(alias);
|
||||
aliases.set(items);
|
||||
show_alias_form.set(false);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_close_add_alias_item = {
|
||||
let show_alias_form = show_alias_form_state.clone();
|
||||
Callback::from(move |()| {
|
||||
show_alias_form.set(false);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_show_add_alias_item = {
|
||||
let show_alias_form = show_alias_form_state.clone();
|
||||
Callback::from(move |_| {
|
||||
show_alias_form.set(true);
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
let handle_remove_alias_list_item = {
|
||||
let alias_list = aliases_state.clone();
|
||||
Callback::from(move |(idx, e): (String, MouseEvent)| {
|
||||
e.prevent_default();
|
||||
if let Ok(index) = idx.parse::<usize>() {
|
||||
let mut items = (*alias_list).clone();
|
||||
if index < items.len() {
|
||||
items.remove(index);
|
||||
alias_list.set(items);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let handle_remove_epg_source = {
|
||||
let epg_list = epg_sources_state.clone();
|
||||
Callback::from(move |(idx, e): (String, MouseEvent)| {
|
||||
e.prevent_default();
|
||||
if let Ok(index) = idx.parse::<usize>() {
|
||||
let mut items = (*epg_list).clone();
|
||||
if index < items.len() {
|
||||
items.remove(index);
|
||||
epg_list.set(items);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let render_options = || {
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_LIVE), xtream_skip_live, ConfigInputOptionsFormAction::XtreamSkipLive) }
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_VOD), xtream_skip_vod, ConfigInputOptionsFormAction::XtreamSkipVod) }
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_SERIES), xtream_skip_series, ConfigInputOptionsFormAction::XtreamSkipSeries) }
|
||||
<TitledCard title={translate.t(LABEL_SKIP)}>
|
||||
<div class="tp__config-view__cols-3">
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_LIVE), xtream_skip_live, ConfigInputOptionsFormAction::XtreamSkipLive) }
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_VOD), xtream_skip_vod, ConfigInputOptionsFormAction::XtreamSkipVod) }
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_SERIES), xtream_skip_series, ConfigInputOptionsFormAction::XtreamSkipSeries) }
|
||||
</div>
|
||||
</TitledCard>
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_LIVE_STREAM_USE_PREFIX), xtream_live_stream_use_prefix, ConfigInputOptionsFormAction::XtreamLiveStreamUsePrefix) }
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_LIVE_STREAM_WITHOUT_EXTENSION), xtream_live_stream_without_extension, ConfigInputOptionsFormAction::XtreamLiveStreamWithoutExtension) }
|
||||
</Card>
|
||||
@@ -208,6 +304,7 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_text!(staged_input_state, translate.t(LABEL_URL), url, StagedInputFormAction::Url) }
|
||||
<div class="tp__config-view__cols-2">
|
||||
{ edit_field_text_option!(staged_input_state, translate.t(LABEL_USERNAME), username, StagedInputFormAction::Username) }
|
||||
{ edit_field_text_option!(staged_input_state, translate.t(LABEL_PASSWORD), password, StagedInputFormAction::Password, true) }
|
||||
{ config_field_child!(translate.t(LABEL_FETCH_METHOD), {
|
||||
@@ -247,6 +344,7 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
options={staged_input_types.clone()}
|
||||
/>
|
||||
}})}
|
||||
</div>
|
||||
|
||||
//{ edit_field_list!(staged_input_state, translate.t(LABEL_HEADERS), headers, StagedInputFormAction::Headers, translate.t(LABEL_ADD_HEADER)) }
|
||||
</Card>
|
||||
@@ -256,17 +354,28 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
let render_input = || {
|
||||
let input_method_selection = Rc::new(vec![input_form_state.form.method.to_string()]);
|
||||
let input_form_state_disp = input_form_state.clone();
|
||||
let aliases = aliases_state.clone();
|
||||
let show_alias_form = show_alias_form_state.clone();
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_bool!(input_form_state, translate.t(LABEL_ENABLED), enabled, ConfigInputFormAction::Enabled) }
|
||||
if *show_alias_form {
|
||||
<AliasItemForm
|
||||
on_submit={handle_add_alias_item}
|
||||
on_cancel={handle_close_add_alias_item}
|
||||
/>
|
||||
} else {
|
||||
<div class="tp__config-view__cols-2">
|
||||
{ edit_field_text!(input_form_state, translate.t(LABEL_NAME), name, ConfigInputFormAction::Name) }
|
||||
{ edit_field_bool!(input_form_state, translate.t(LABEL_ENABLED), enabled, ConfigInputFormAction::Enabled) }
|
||||
</div>
|
||||
{ edit_field_text!(input_form_state, translate.t(LABEL_URL), url, ConfigInputFormAction::Url) }
|
||||
<div class="tp__config-view__cols-2">
|
||||
{ edit_field_text_option!(input_form_state, translate.t(LABEL_USERNAME), username, ConfigInputFormAction::Username) }
|
||||
{ edit_field_text_option!(input_form_state, translate.t(LABEL_PASSWORD), password, ConfigInputFormAction::Password, true) }
|
||||
{ edit_field_number_u16!(input_form_state, translate.t(LABEL_MAX_CONNECTIONS), max_connections, ConfigInputFormAction::MaxConnections) }
|
||||
{ edit_field_number_i16!(input_form_state, translate.t(LABEL_PRIORITY), priority, ConfigInputFormAction::Priority) }
|
||||
// pub input_type: InputType,
|
||||
//{ edit_field_list!(input_form_state, translate.t(LABEL_HEADERS), headers, ConfigInputFormAction::Headers, translate.t(LABEL_ADD_HEADER)) }
|
||||
{ edit_field_number_u16!(input_form_state, translate.t(LABEL_MAX_CONNECTIONS), max_connections, ConfigInputFormAction::MaxConnections) }
|
||||
{ edit_field_number_i16!(input_form_state, translate.t(LABEL_PRIORITY), priority, ConfigInputFormAction::Priority) }
|
||||
{ edit_field_date!(input_form_state, translate.t(LABEL_EXP_DATE), exp_date, ConfigInputFormAction::ExpDate) }
|
||||
{ config_field_child!(translate.t(LABEL_FETCH_METHOD), {
|
||||
html! {
|
||||
<RadioButtonGroup
|
||||
@@ -280,10 +389,108 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
selected={input_method_selection}
|
||||
/>
|
||||
}})}
|
||||
</div>
|
||||
{ edit_field_text_option!(input_form_state, translate.t(LABEL_PERSIST), persist, ConfigInputFormAction::Persist) }
|
||||
// Aliases Section
|
||||
{ config_field_child!(translate.t(LABEL_ALIASES), {
|
||||
let aliases_list = aliases.clone();
|
||||
html! {
|
||||
<div class="tp__form-list">
|
||||
<div class="tp__form-list__items">
|
||||
{
|
||||
for (*aliases_list).iter().enumerate().map(|(idx, alias)| {
|
||||
html! {
|
||||
<div class="tp__form-list__item" key={format!("alias-{idx}")}>
|
||||
<IconButton
|
||||
name={idx.to_string()}
|
||||
icon="Delete"
|
||||
onclick={handle_remove_alias_list_item.clone()}/>
|
||||
<div class="tp__form-list__item-content">
|
||||
<span><strong>{&alias.name}</strong>{" - "}{&alias.url}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="add_alias"
|
||||
icon="Add"
|
||||
title={translate.t(LABEL_ADD_ALIAS)}
|
||||
onclick={handle_show_add_alias_item}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
}
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
// pub epg: Option<EpgConfigDto>,
|
||||
// pub aliases: Option<Vec<ConfigInputAliasDto>>,
|
||||
let render_advanced = || {
|
||||
let headers = headers_state.clone();
|
||||
let epg_sources = epg_sources_state.clone();
|
||||
let show_epg_form = show_epg_form_state.clone();
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
if *show_epg_form {
|
||||
<EpgSourceItemForm
|
||||
on_submit={handle_add_epg_item}
|
||||
on_cancel={handle_close_add_epg_item}
|
||||
/>
|
||||
} else {
|
||||
// Headers Section
|
||||
{ config_field_child!(translate.t(LABEL_HEADERS), {
|
||||
let headers_set = headers.clone();
|
||||
html! {
|
||||
<KeyValueEditor
|
||||
entries={(*headers).clone()}
|
||||
readonly={false}
|
||||
key_placeholder={translate.t("LABEL.HEADER_NAME")}
|
||||
value_placeholder={translate.t("LABEL.HEADER_VALUE")}
|
||||
on_change={Callback::from(move |new_headers: HashMap<String, String>| {
|
||||
headers_set.set(new_headers);
|
||||
})}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
|
||||
// EPG Sources Section
|
||||
{ config_field_child!(translate.t(LABEL_EPG_SOURCES), {
|
||||
let epg_sources_list = epg_sources.clone();
|
||||
|
||||
html! {
|
||||
<div class="tp__form-list">
|
||||
<div class="tp__form-list__items">
|
||||
{
|
||||
for (*epg_sources_list).iter().enumerate().map(|(idx, source)| {
|
||||
html! {
|
||||
<div class="tp__form-list__item" key={format!("epg-{idx}")}>
|
||||
<IconButton
|
||||
name={idx.to_string()}
|
||||
icon="Delete"
|
||||
onclick={handle_remove_epg_source.clone()} />
|
||||
<div class="tp__form-list__item-content">
|
||||
<span>{&source.url}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="add_epg_source"
|
||||
icon="Add"
|
||||
title={translate.t(LABEL_ADD_EPG_SOURCE)}
|
||||
onclick={handle_show_add_epg_item}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
}
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
@@ -293,6 +500,9 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
let input_form_state = input_form_state.clone();
|
||||
let input_options_state = input_options_state.clone();
|
||||
let staged_input_state = staged_input_state.clone();
|
||||
let headers_state = headers_state.clone();
|
||||
let epg_sources_state = epg_sources_state.clone();
|
||||
let aliases_state = aliases_state.clone();
|
||||
let block_id = props.block_id;
|
||||
Callback::from(move |_| {
|
||||
let mut input = input_form_state.data().clone();
|
||||
@@ -311,15 +521,42 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
Some(staged_input.clone())
|
||||
};
|
||||
|
||||
// Handle Headers
|
||||
input.headers = (*headers_state).clone();
|
||||
|
||||
// Handle EPG: update sources but preserve other fields if present
|
||||
let epg_sources = (*epg_sources_state).clone();
|
||||
if let Some(mut epg_cfg) = input.epg.take() {
|
||||
epg_cfg.sources = if epg_sources.is_empty() { None } else { Some(epg_sources) };
|
||||
input.epg = if epg_cfg.sources.is_some() || epg_cfg.smart_match.is_some() {
|
||||
Some(epg_cfg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
} else if !epg_sources.is_empty() {
|
||||
input.epg = Some(EpgConfigDto {
|
||||
sources: Some(epg_sources),
|
||||
..EpgConfigDto::default()
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Aliases
|
||||
let aliases = (*aliases_state).clone();
|
||||
input.aliases = if aliases.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(aliases)
|
||||
};
|
||||
|
||||
source_editor_ctx.on_form_change.emit((block_id, BlockInstance::Input(Rc::new(input))));
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
let handle_cancel = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
|
||||
let render_edit_mode = || {
|
||||
@@ -330,7 +567,8 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
for [
|
||||
InputFormPage::Main,
|
||||
InputFormPage::Options,
|
||||
InputFormPage::Staged
|
||||
InputFormPage::Staged,
|
||||
InputFormPage::Advanced
|
||||
].iter().map(|page| {
|
||||
let page_str = page.to_string();
|
||||
let active = *view_visible == page_str;
|
||||
@@ -360,6 +598,9 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
<Panel value={InputFormPage::Staged.to_string()} active={view_visible.to_string()}>
|
||||
{render_staged()}
|
||||
</Panel>
|
||||
<Panel value={InputFormPage::Advanced.to_string()} active={view_visible.to_string()}>
|
||||
{render_advanced()}
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -368,14 +609,14 @@ pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
html! {
|
||||
<div class="tp__source-editor-form tp__config-view-page">
|
||||
<div class="tp__source-editor-form__toolbar tp__form-page__toolbar">
|
||||
<TextButton class="primary" name="apply_input"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply_input}></TextButton>
|
||||
<TextButton class="secondary" name="cancel_input"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
<TextButton class="primary" name="apply_input"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply_input}></TextButton>
|
||||
</div>
|
||||
{ render_edit_mode() }
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,13 @@ mod output_form;
|
||||
mod editor_form;
|
||||
mod block_view;
|
||||
mod output_xtream_form;
|
||||
mod output_m3u_form;
|
||||
mod output_strm_form;
|
||||
mod output_hdhomerun_form;
|
||||
mod layout;
|
||||
mod epg_source_item_form;
|
||||
mod alias_item_form;
|
||||
mod trakt_list_item_form;
|
||||
|
||||
pub(crate) use editor_view::*;
|
||||
pub(crate) use editor_model::*;
|
||||
@@ -19,4 +25,10 @@ pub(crate) use target_form::*;
|
||||
pub(crate) use output_form::*;
|
||||
pub(crate) use editor_form::*;
|
||||
pub(crate) use block_view::*;
|
||||
pub(crate) use output_xtream_form::*;
|
||||
pub(crate) use output_xtream_form::*;
|
||||
pub(crate) use output_m3u_form::*;
|
||||
pub(crate) use output_strm_form::*;
|
||||
pub(crate) use output_hdhomerun_form::*;
|
||||
pub(crate) use epg_source_item_form::*;
|
||||
pub(crate) use alias_item_form::*;
|
||||
pub(crate) use trakt_list_item_form::*;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::app::components::{BlockId, BlockType, EditMode, SourceEditorContext, XtreamTargetOutputView};
|
||||
use crate::app::components::{BlockId, BlockType, EditMode, SourceEditorContext, XtreamTargetOutputView, M3uTargetOutputView, StrmTargetOutputView, HdHomeRunTargetOutputView};
|
||||
use shared::model::TargetOutputDto;
|
||||
use std::rc::Rc;
|
||||
use yew::{function_component, html, use_context, Html, Properties};
|
||||
@@ -21,7 +21,14 @@ pub fn ConfigOutputView(props: &ConfigOutputViewProps) -> Html {
|
||||
BlockType::InputXtream
|
||||
| BlockType::InputM3u
|
||||
| BlockType::Target => html! {},
|
||||
BlockType::OutputM3u => html! {},
|
||||
BlockType::OutputM3u => {
|
||||
let output = props.output.as_ref()
|
||||
.and_then(|to| if let TargetOutputDto::M3u(m3u) = &**to {
|
||||
Some(Rc::new(m3u.clone()))
|
||||
} else { None });
|
||||
|
||||
html! { <M3uTargetOutputView block_id={block_id} output={output} /> }
|
||||
}
|
||||
BlockType::OutputXtream => {
|
||||
let output = props.output.as_ref()
|
||||
.and_then(|to| if let TargetOutputDto::Xtream(xtream) = &**to {
|
||||
@@ -30,8 +37,22 @@ pub fn ConfigOutputView(props: &ConfigOutputViewProps) -> Html {
|
||||
|
||||
html! { <XtreamTargetOutputView block_id={block_id} output={output} /> }
|
||||
}
|
||||
BlockType::OutputHdHomeRun => html! {},
|
||||
BlockType::OutputStrm => html! {},
|
||||
BlockType::OutputHdHomeRun => {
|
||||
let output = props.output.as_ref()
|
||||
.and_then(|to| if let TargetOutputDto::HdHomeRun(hdhomerun) = &**to {
|
||||
Some(Rc::new(hdhomerun.clone()))
|
||||
} else { None });
|
||||
|
||||
html! { <HdHomeRunTargetOutputView block_id={block_id} output={output} /> }
|
||||
}
|
||||
BlockType::OutputStrm => {
|
||||
let output = props.output.as_ref()
|
||||
.and_then(|to| if let TargetOutputDto::Strm(strm) = &**to {
|
||||
Some(Rc::new(strm.clone()))
|
||||
} else { None });
|
||||
|
||||
html! { <StrmTargetOutputView block_id={block_id} output={output} /> }
|
||||
}
|
||||
}
|
||||
}
|
||||
EditMode::Inactive => html! {}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
use crate::app::components::config::HasFormData;
|
||||
use crate::app::components::select::Select;
|
||||
use crate::app::components::{BlockId, BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, SourceEditorContext, TextButton};
|
||||
use crate::{config_field_child, edit_field_text, generate_form_reducer};
|
||||
use shared::model::{HdHomeRunTargetOutputDto, TargetOutputDto, TargetType};
|
||||
use std::rc::Rc;
|
||||
use yew::{function_component, html, use_context, use_effect_with, use_memo, use_reducer, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_DEVICE: &str = "LABEL.DEVICE";
|
||||
const LABEL_USERNAME: &str = "LABEL.USERNAME";
|
||||
const LABEL_USE_OUTPUT: &str = "LABEL.USE_OUTPUT";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: HdHomeRunTargetOutputFormState { form: HdHomeRunTargetOutputDto },
|
||||
action_name: HdHomeRunTargetOutputFormAction,
|
||||
fields {
|
||||
Device => device: String,
|
||||
Username => username: String,
|
||||
UseOutput => use_output: Option<TargetType>,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct HdHomeRunTargetOutputViewProps {
|
||||
pub(crate) block_id: BlockId,
|
||||
pub(crate) output: Option<Rc<HdHomeRunTargetOutputDto>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn HdHomeRunTargetOutputView(props: &HdHomeRunTargetOutputViewProps) -> Html {
|
||||
let translate = use_translation();
|
||||
let source_editor_ctx = use_context::<SourceEditorContext>().expect("SourceEditorContext not found");
|
||||
|
||||
let output_form_state: UseReducerHandle<HdHomeRunTargetOutputFormState> =
|
||||
use_reducer(|| HdHomeRunTargetOutputFormState {
|
||||
form: HdHomeRunTargetOutputDto::default(),
|
||||
modified: false,
|
||||
});
|
||||
|
||||
let target_types = use_memo(output_form_state.form.use_output, |use_output| {
|
||||
let default_type = use_output.unwrap_or(TargetType::M3u);
|
||||
[
|
||||
TargetType::M3u,
|
||||
TargetType::Xtream,
|
||||
]
|
||||
.iter()
|
||||
.map(|t| DropDownOption {
|
||||
id: t.to_string(),
|
||||
label: html! { t.to_string() },
|
||||
selected: *t == default_type,
|
||||
}).collect::<Vec<DropDownOption>>()
|
||||
});
|
||||
|
||||
{
|
||||
let output_form_state = output_form_state.clone();
|
||||
let config_output = props.output.clone();
|
||||
|
||||
use_effect_with(config_output, move |cfg| {
|
||||
if let Some(output) = cfg {
|
||||
output_form_state.dispatch(HdHomeRunTargetOutputFormAction::SetAll(output.as_ref().clone()));
|
||||
} else {
|
||||
output_form_state.dispatch(HdHomeRunTargetOutputFormAction::SetAll(HdHomeRunTargetOutputDto::default()));
|
||||
}
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let render_output = || {
|
||||
let output_form_state_1 = output_form_state.clone();
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_text!(output_form_state, translate.t(LABEL_DEVICE), device, HdHomeRunTargetOutputFormAction::Device) }
|
||||
{ edit_field_text!(output_form_state, translate.t(LABEL_USERNAME), username, HdHomeRunTargetOutputFormAction::Username) }
|
||||
{ config_field_child!(translate.t(LABEL_USE_OUTPUT), {
|
||||
html! {
|
||||
<Select
|
||||
name={"use_output"}
|
||||
multi_select={false}
|
||||
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
|
||||
match selections {
|
||||
DropDownSelection::Empty => {
|
||||
output_form_state_1.dispatch(HdHomeRunTargetOutputFormAction::UseOutput(Some(TargetType::M3u)));
|
||||
}
|
||||
DropDownSelection::Single(option) => {
|
||||
output_form_state_1.dispatch(HdHomeRunTargetOutputFormAction::UseOutput(Some(option.parse::<TargetType>().unwrap_or(TargetType::M3u))));
|
||||
}
|
||||
DropDownSelection::Multi(options) => {
|
||||
if let Some(first) = options.first() {
|
||||
output_form_state_1.dispatch(HdHomeRunTargetOutputFormAction::UseOutput(Some(first.parse::<TargetType>().unwrap_or(TargetType::M3u))));
|
||||
}
|
||||
}
|
||||
}
|
||||
})}
|
||||
options={target_types.clone()}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
let handle_apply = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
let output_form_state = output_form_state.clone();
|
||||
let block_id = props.block_id;
|
||||
Callback::from(move |_| {
|
||||
let output = output_form_state.data().clone();
|
||||
source_editor_ctx.on_form_change.emit((block_id, BlockInstance::Output(Rc::new(TargetOutputDto::HdHomeRun(output)))));
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__source-editor-form tp__config-view-page">
|
||||
<div class="tp__source-editor-form__toolbar tp__form-page__toolbar">
|
||||
<TextButton class="secondary" name="cancel_hdhomerun_output"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
<TextButton class="primary" name="apply_hdhomerun_output"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply}></TextButton>
|
||||
</div>
|
||||
<div class="tp__input-form__body">
|
||||
{ render_output() }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use crate::app::components::config::HasFormData;
|
||||
use crate::app::components::{BlockId, BlockInstance, Card, EditMode, SourceEditorContext, TextButton};
|
||||
use crate::{edit_field_bool, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::{M3uTargetOutputDto, TargetOutputDto};
|
||||
use std::rc::Rc;
|
||||
use yew::{function_component, html, use_context, use_effect_with, use_reducer, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_FILENAME: &str = "LABEL.FILENAME";
|
||||
const LABEL_INCLUDE_TYPE_IN_URL: &str = "LABEL.INCLUDE_TYPE_IN_URL";
|
||||
const LABEL_MASK_REDIRECT_URL: &str = "LABEL.MASK_REDIRECT_URL";
|
||||
const LABEL_FILTER: &str = "LABEL.FILTER";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: M3uTargetOutputFormState { form: M3uTargetOutputDto },
|
||||
action_name: M3uTargetOutputFormAction,
|
||||
fields {
|
||||
Filename => filename: Option<String>,
|
||||
IncludeTypeInUrl => include_type_in_url: bool,
|
||||
MaskRedirectUrl => mask_redirect_url: bool,
|
||||
Filter => filter: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct M3uTargetOutputViewProps {
|
||||
pub(crate) block_id: BlockId,
|
||||
pub(crate) output: Option<Rc<M3uTargetOutputDto>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn M3uTargetOutputView(props: &M3uTargetOutputViewProps) -> Html {
|
||||
let translate = use_translation();
|
||||
let source_editor_ctx = use_context::<SourceEditorContext>().expect("SourceEditorContext not found");
|
||||
|
||||
let output_form_state: UseReducerHandle<M3uTargetOutputFormState> =
|
||||
use_reducer(|| M3uTargetOutputFormState {
|
||||
form: M3uTargetOutputDto::default(),
|
||||
modified: false,
|
||||
});
|
||||
|
||||
{
|
||||
let output_form_state = output_form_state.clone();
|
||||
let config_output = props.output.clone();
|
||||
|
||||
use_effect_with(config_output, move |cfg| {
|
||||
if let Some(output) = cfg {
|
||||
output_form_state.dispatch(M3uTargetOutputFormAction::SetAll(output.as_ref().clone()));
|
||||
} else {
|
||||
output_form_state.dispatch(M3uTargetOutputFormAction::SetAll(M3uTargetOutputDto::default()));
|
||||
}
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let render_output = || {
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_text_option!(output_form_state, translate.t(LABEL_FILENAME), filename, M3uTargetOutputFormAction::Filename) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_INCLUDE_TYPE_IN_URL), include_type_in_url, M3uTargetOutputFormAction::IncludeTypeInUrl) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_MASK_REDIRECT_URL), mask_redirect_url, M3uTargetOutputFormAction::MaskRedirectUrl) }
|
||||
{ edit_field_text_option!(output_form_state, translate.t(LABEL_FILTER), filter, M3uTargetOutputFormAction::Filter) }
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
let handle_apply = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
let output_form_state = output_form_state.clone();
|
||||
let block_id = props.block_id;
|
||||
Callback::from(move |_| {
|
||||
let output = output_form_state.data().clone();
|
||||
source_editor_ctx.on_form_change.emit((block_id, BlockInstance::Output(Rc::new(TargetOutputDto::M3u(output)))));
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__source-editor-form tp__config-view-page">
|
||||
<div class="tp__source-editor-form__toolbar tp__form-page__toolbar">
|
||||
<TextButton class="secondary" name="cancel_m3u_output"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
<TextButton class="primary" name="apply_m3u_output"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply}></TextButton>
|
||||
</div>
|
||||
<div class="tp__input-form__body">
|
||||
{ render_output() }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
use crate::app::components::config::HasFormData;
|
||||
use crate::app::components::select::Select;
|
||||
use crate::app::components::{BlockId, BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, Panel, SourceEditorContext, TextButton};
|
||||
use crate::{config_field_child, edit_field_bool, edit_field_list_option, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::{StrmExportStyle, StrmTargetOutputDto, TargetOutputDto};
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use yew::{classes, function_component, html, use_context, use_effect_with, use_memo, use_reducer, use_state, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_DIRECTORY: &str = "LABEL.DIRECTORY";
|
||||
const LABEL_USERNAME: &str = "LABEL.USERNAME";
|
||||
const LABEL_EXPORT_STYLE: &str = "LABEL.EXPORT_STYLE";
|
||||
const LABEL_FLAT: &str = "LABEL.FLAT";
|
||||
const LABEL_UNDERSCORE_WHITESPACE: &str = "LABEL.UNDERSCORE_WHITESPACE";
|
||||
const LABEL_CLEANUP: &str = "LABEL.CLEANUP";
|
||||
const LABEL_STRM_PROPS: &str = "LABEL.STRM_PROPS";
|
||||
const LABEL_FILTER: &str = "LABEL.FILTER";
|
||||
const LABEL_ADD_QUALITY_TO_FILENAME: &str = "LABEL.ADD_QUALITY_TO_FILENAME";
|
||||
const LABEL_ADD_PROPERTY: &str = "LABEL.ADD_PROPERTY";
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum StrmFormPage {
|
||||
Main,
|
||||
Options,
|
||||
}
|
||||
|
||||
impl Display for StrmFormPage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", match *self {
|
||||
StrmFormPage::Main => "Main",
|
||||
StrmFormPage::Options => "Options",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
generate_form_reducer!(
|
||||
state: StrmTargetOutputFormState { form: StrmTargetOutputDto },
|
||||
action_name: StrmTargetOutputFormAction,
|
||||
fields {
|
||||
Directory => directory: String,
|
||||
Username => username: Option<String>,
|
||||
Style => style: StrmExportStyle,
|
||||
Flat => flat: bool,
|
||||
UnderscoreWhitespace => underscore_whitespace: bool,
|
||||
Cleanup => cleanup: bool,
|
||||
StrmProps => strm_props: Option<Vec<String>>,
|
||||
Filter => filter: Option<String>,
|
||||
AddQualityToFilename => add_quality_to_filename: bool,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct StrmTargetOutputViewProps {
|
||||
pub(crate) block_id: BlockId,
|
||||
pub(crate) output: Option<Rc<StrmTargetOutputDto>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn StrmTargetOutputView(props: &StrmTargetOutputViewProps) -> Html {
|
||||
let translate = use_translation();
|
||||
let source_editor_ctx = use_context::<SourceEditorContext>().expect("SourceEditorContext not found");
|
||||
|
||||
let output_form_state: UseReducerHandle<StrmTargetOutputFormState> =
|
||||
use_reducer(|| StrmTargetOutputFormState {
|
||||
form: StrmTargetOutputDto::default(),
|
||||
modified: false,
|
||||
});
|
||||
|
||||
let view_visible = use_state(|| StrmFormPage::Main.to_string());
|
||||
|
||||
let on_tab_click = {
|
||||
let view_visible = view_visible.clone();
|
||||
Callback::from(move |page: StrmFormPage| view_visible.set(page.to_string()))
|
||||
};
|
||||
|
||||
let export_styles = use_memo(output_form_state.form.style, |style| {
|
||||
let default_style = *style;
|
||||
[
|
||||
StrmExportStyle::Kodi,
|
||||
StrmExportStyle::Plex,
|
||||
StrmExportStyle::Emby,
|
||||
StrmExportStyle::Jellyfin,
|
||||
]
|
||||
.iter()
|
||||
.map(|s| DropDownOption {
|
||||
id: s.to_string(),
|
||||
label: html! { s.to_string() },
|
||||
selected: *s == default_style,
|
||||
}).collect::<Vec<DropDownOption>>()
|
||||
});
|
||||
|
||||
{
|
||||
let output_form_state = output_form_state.clone();
|
||||
let config_output = props.output.clone();
|
||||
|
||||
use_effect_with(config_output, move |cfg| {
|
||||
if let Some(output) = cfg {
|
||||
output_form_state.dispatch(StrmTargetOutputFormAction::SetAll(output.as_ref().clone()));
|
||||
} else {
|
||||
output_form_state.dispatch(StrmTargetOutputFormAction::SetAll(StrmTargetOutputDto::default()));
|
||||
}
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let render_main = || {
|
||||
let output_form_state_1 = output_form_state.clone();
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_text!(output_form_state, translate.t(LABEL_DIRECTORY), directory, StrmTargetOutputFormAction::Directory) }
|
||||
{ edit_field_text_option!(output_form_state, translate.t(LABEL_USERNAME), username, StrmTargetOutputFormAction::Username) }
|
||||
{ config_field_child!(translate.t(LABEL_EXPORT_STYLE), {
|
||||
html! {
|
||||
<Select
|
||||
name={"export_style"}
|
||||
multi_select={false}
|
||||
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
|
||||
match selections {
|
||||
DropDownSelection::Empty => {
|
||||
output_form_state_1.dispatch(StrmTargetOutputFormAction::Style(StrmExportStyle::Kodi));
|
||||
}
|
||||
DropDownSelection::Single(option) => {
|
||||
output_form_state_1.dispatch(StrmTargetOutputFormAction::Style(option.parse::<StrmExportStyle>().unwrap_or(StrmExportStyle::Kodi)));
|
||||
}
|
||||
DropDownSelection::Multi(options) => {
|
||||
if let Some(first) = options.first() {
|
||||
output_form_state_1.dispatch(StrmTargetOutputFormAction::Style(first.parse::<StrmExportStyle>().unwrap_or(StrmExportStyle::Kodi)));
|
||||
}
|
||||
}
|
||||
}
|
||||
})}
|
||||
options={export_styles.clone()}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
{ edit_field_text_option!(output_form_state, translate.t(LABEL_FILTER), filter, StrmTargetOutputFormAction::Filter) }
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
let render_options = || {
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_FLAT), flat, StrmTargetOutputFormAction::Flat) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_UNDERSCORE_WHITESPACE), underscore_whitespace, StrmTargetOutputFormAction::UnderscoreWhitespace) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_CLEANUP), cleanup, StrmTargetOutputFormAction::Cleanup) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_ADD_QUALITY_TO_FILENAME), add_quality_to_filename, StrmTargetOutputFormAction::AddQualityToFilename) }
|
||||
{ edit_field_list_option!(output_form_state, translate.t(LABEL_STRM_PROPS), strm_props, StrmTargetOutputFormAction::StrmProps, translate.t(LABEL_ADD_PROPERTY)) }
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
let render_edit_mode = || {
|
||||
html! {
|
||||
<div class="tp__input-form__body">
|
||||
<div class="tp__tab-header">
|
||||
{
|
||||
for [
|
||||
StrmFormPage::Main,
|
||||
StrmFormPage::Options,
|
||||
].iter().map(|page| {
|
||||
let page_str = page.to_string();
|
||||
let active = *view_visible == page_str;
|
||||
let on_tab_click = {
|
||||
let on_tab_click = on_tab_click.clone();
|
||||
let page = *page;
|
||||
Callback::from(move |_| on_tab_click.emit(page))
|
||||
};
|
||||
html! {
|
||||
<button
|
||||
class={classes!("tp__tab-button", if active { "active" } else { "" })}
|
||||
onclick={on_tab_click}
|
||||
>
|
||||
{ page_str.clone() }
|
||||
</button>
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class="tp__input-form__body__pages">
|
||||
<Panel value={StrmFormPage::Main.to_string()} active={view_visible.to_string()}>
|
||||
{render_main()}
|
||||
</Panel>
|
||||
<Panel value={StrmFormPage::Options.to_string()} active={view_visible.to_string()}>
|
||||
{render_options()}
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
let handle_apply = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
let output_form_state = output_form_state.clone();
|
||||
let block_id = props.block_id;
|
||||
Callback::from(move |_| {
|
||||
let output = output_form_state.data().clone();
|
||||
source_editor_ctx.on_form_change.emit((block_id, BlockInstance::Output(Rc::new(TargetOutputDto::Strm(output)))));
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__source-editor-form tp__config-view-page">
|
||||
<div class="tp__source-editor-form__toolbar tp__form-page__toolbar">
|
||||
<TextButton class="secondary" name="cancel_strm_output"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
<TextButton class="primary" name="apply_strm_output"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply}></TextButton>
|
||||
</div>
|
||||
{ render_edit_mode() }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
use crate::app::components::config::HasFormData;
|
||||
use crate::app::components::{BlockId, BlockInstance, Card, EditMode, Panel, SourceEditorContext, TextButton};
|
||||
use crate::{ edit_field_bool, edit_field_number_u16, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::{ XtreamTargetOutputDto, TargetOutputDto};
|
||||
use crate::app::components::{BlockId, BlockInstance, Card, EditMode, IconButton, Panel, SourceEditorContext, TextButton, TraktListItemForm, TitledCard};
|
||||
use crate::{config_field_child, edit_field_bool, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::{TargetOutputDto, TraktApiConfigDto, TraktConfigDto, TraktContentType, TraktListConfigDto, XtreamTargetOutputDto};
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use web_sys::MouseEvent;
|
||||
use yew::{classes, function_component, html, use_context, use_effect_with, use_reducer, use_state, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_SKIP_LIVE_DIRECT_SOURCE: &str = "LABEL.SKIP_LIVE_DIRECT_SOURCE";
|
||||
const LABEL_SKIP_VOD_DIRECT_SOURCE: &str = "LABEL.SKIP_VOD_DIRECT_SOURCE";
|
||||
const LABEL_SKIP_SERIES_DIRECT_SOURCE: &str = "LABEL.SKIP_SERIES_DIRECT_SOURCE";
|
||||
const LABEL_RESOLVE_VOD: &str = "LABEL.RESOLVE_VOD";
|
||||
const LABEL_RESOLVE_VOD_DELAY: &str = "LABEL.RESOLVE_VOD_DELAY_SEC";
|
||||
const LABEL_RESOLVE_SERIES: &str = "LABEL.RESOLVE_SERIES";
|
||||
const LABEL_RESOLVE_SERIES_DELAY: &str = "LABEL.RESOLVE_SERIES_DELAY_SEC";
|
||||
const LABEL_SKIP_DIRECT_SOURCE: &str = "LABEL.SKIP_DIRECT_SOURCE";
|
||||
const LABEL_LIVE: &str = "LABEL.LIVE";
|
||||
const LABEL_VOD: &str = "LABEL.VOD";
|
||||
const LABEL_SERIES: &str = "LABEL.SERIES";
|
||||
const LABEL_RESOLVE: &str = "LABEL.RESOLVE";
|
||||
const LABEL_RESOLVE_DELAY_SEC: &str = "LABEL.RESOLVE_DELAY_SEC";
|
||||
const LABEL_FILTER: &str = "LABEL.FILTER";
|
||||
|
||||
const LABEL_TRAKT_API_KEY: &str = "LABEL.TRAKT_API_KEY";
|
||||
const LABEL_TRAKT_API_VERSION: &str = "LABEL.TRAKT_API_VERSION";
|
||||
const LABEL_TRAKT_API_URL: &str = "LABEL.TRAKT_API_URL";
|
||||
const LABEL_TRAKT_LISTS: &str = "LABEL.TRAKT_LISTS";
|
||||
const LABEL_ADD_TRAKT_LIST: &str = "LABEL.ADD_TRAKT_LIST";
|
||||
const LABEL_API_CONFIGURATION: &str = "LABEL.API_CONFIGURATION";
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum OutputFormPage {
|
||||
@@ -26,22 +31,20 @@ enum OutputFormPage {
|
||||
impl Display for OutputFormPage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", match *self {
|
||||
OutputFormPage::Main => "Main".to_string(),
|
||||
OutputFormPage::Trakt => "Trakt".to_string(),
|
||||
OutputFormPage::Main => "Main",
|
||||
OutputFormPage::Trakt => "Trakt",
|
||||
})
|
||||
}
|
||||
}
|
||||
//
|
||||
// generate_form_reducer!(
|
||||
// state: TraktConfigFormState { form: TraktConfigDto },
|
||||
// action_name: TraktConfigFormAction,
|
||||
// fields {
|
||||
// IgnoreLogo => ignore_logo: bool,
|
||||
// ShareLiveStreams => share_live_streams: bool,
|
||||
// RemoveDuplicates => remove_duplicates: bool,
|
||||
// ForceRedirect => force_redirect: Option<ClusterFlags>,
|
||||
// }
|
||||
// );
|
||||
generate_form_reducer!(
|
||||
state: TraktApiConfigFormState { form: TraktApiConfigDto },
|
||||
action_name: TraktApiConfigFormAction,
|
||||
fields {
|
||||
Key => key: String,
|
||||
Version => version: String,
|
||||
Url => url: String,
|
||||
}
|
||||
);
|
||||
|
||||
generate_form_reducer!(
|
||||
state: XtreamTargetOutputFormState { form: XtreamTargetOutputDto },
|
||||
@@ -74,11 +77,18 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
form: XtreamTargetOutputDto::default(),
|
||||
modified: false,
|
||||
});
|
||||
// let target_options_state: UseReducerHandle<TraktConfigFormState> =
|
||||
// use_reducer(|| TraktConfigFormState {
|
||||
// form: TraktConfigDto::default(),
|
||||
// modified: false,
|
||||
// });
|
||||
|
||||
let trakt_api_state: UseReducerHandle<TraktApiConfigFormState> =
|
||||
use_reducer(|| TraktApiConfigFormState {
|
||||
form: TraktApiConfigDto::default(),
|
||||
modified: false,
|
||||
});
|
||||
|
||||
// State for Trakt lists
|
||||
let trakt_lists_state = use_state(Vec::<TraktListConfigDto>::new);
|
||||
|
||||
// State for showing trakt list form
|
||||
let show_trakt_list_form_state = use_state(|| false);
|
||||
|
||||
let view_visible = use_state(|| OutputFormPage::Main.to_string());
|
||||
|
||||
@@ -89,33 +99,172 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
|
||||
{
|
||||
let output_form_state = output_form_state.clone();
|
||||
let trakt_api_state = trakt_api_state.clone();
|
||||
let trakt_lists_state = trakt_lists_state.clone();
|
||||
|
||||
let config_output = props.output.clone();
|
||||
|
||||
use_effect_with(config_output, move |cfg| {
|
||||
if let Some(target) = cfg {
|
||||
output_form_state.dispatch(XtreamTargetOutputFormAction::SetAll(target.as_ref().clone()));
|
||||
|
||||
// Load Trakt configuration
|
||||
if let Some(trakt) = &target.trakt {
|
||||
trakt_api_state.dispatch(TraktApiConfigFormAction::SetAll(trakt.api.clone()));
|
||||
trakt_lists_state.set(trakt.lists.clone());
|
||||
} else {
|
||||
trakt_api_state.dispatch(TraktApiConfigFormAction::SetAll(TraktApiConfigDto::default()));
|
||||
trakt_lists_state.set(Vec::new());
|
||||
}
|
||||
} else {
|
||||
output_form_state.dispatch(XtreamTargetOutputFormAction::SetAll(XtreamTargetOutputDto::default()));
|
||||
trakt_api_state.dispatch(TraktApiConfigFormAction::SetAll(TraktApiConfigDto::default()));
|
||||
trakt_lists_state.set(Vec::new());
|
||||
}
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let handle_add_trakt_list_item = {
|
||||
let trakt_list = trakt_lists_state.clone();
|
||||
let show_trakt_list_form = show_trakt_list_form_state.clone();
|
||||
|
||||
Callback::from(move |item: TraktListConfigDto| {
|
||||
let mut items = (*trakt_list).clone();
|
||||
items.push(item);
|
||||
trakt_list.set(items);
|
||||
show_trakt_list_form.set(false);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_remove_trakt_list_item = {
|
||||
let trakt_list = trakt_lists_state.clone();
|
||||
Callback::from(move |(idx, _e): (String, MouseEvent)| {
|
||||
if let Ok(index) = idx.parse::<usize>() {
|
||||
let mut items = (*trakt_list).clone();
|
||||
if index < items.len() {
|
||||
items.remove(index);
|
||||
trakt_list.set(items);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let handle_close_trakt_list_form = {
|
||||
let show_trakt_list_form = show_trakt_list_form_state.clone();
|
||||
Callback::from(move |()| {
|
||||
show_trakt_list_form.set(false);
|
||||
})
|
||||
};
|
||||
|
||||
let handle_show_trakt_list_form = {
|
||||
let show_trakt_list_form = show_trakt_list_form_state.clone();
|
||||
Callback::from(move |_name| {
|
||||
show_trakt_list_form.set(true);
|
||||
})
|
||||
};
|
||||
|
||||
let render_output = || {
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_SKIP_LIVE_DIRECT_SOURCE), skip_live_direct_source, XtreamTargetOutputFormAction::SkipLiveDirectSource) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_SKIP_VOD_DIRECT_SOURCE), skip_video_direct_source, XtreamTargetOutputFormAction::SkipVideoDirectSource) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_SKIP_SERIES_DIRECT_SOURCE), skip_series_direct_source, XtreamTargetOutputFormAction::SkipSeriesDirectSource) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_RESOLVE_VOD), resolve_vod, XtreamTargetOutputFormAction::ResolveVod) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_RESOLVE_SERIES), resolve_series, XtreamTargetOutputFormAction::ResolveSeries) }
|
||||
{ edit_field_number_u16!(output_form_state, translate.t(LABEL_RESOLVE_VOD_DELAY), resolve_vod_delay, XtreamTargetOutputFormAction::ResolveVodDelay) }
|
||||
{ edit_field_number_u16!(output_form_state, translate.t(LABEL_RESOLVE_SERIES_DELAY), resolve_series_delay, XtreamTargetOutputFormAction::ResolveSeriesDelay) }
|
||||
<TitledCard title={translate.t(LABEL_SKIP_DIRECT_SOURCE)}>
|
||||
<div class="tp__config-view__cols-3">
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_LIVE), skip_live_direct_source, XtreamTargetOutputFormAction::SkipLiveDirectSource) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_VOD), skip_video_direct_source, XtreamTargetOutputFormAction::SkipVideoDirectSource) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_SERIES), skip_series_direct_source, XtreamTargetOutputFormAction::SkipSeriesDirectSource) }
|
||||
</div>
|
||||
</TitledCard>
|
||||
<TitledCard title={translate.t(LABEL_RESOLVE)}>
|
||||
<div class="tp__config-view__cols-2">
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_VOD), resolve_vod, XtreamTargetOutputFormAction::ResolveVod) }
|
||||
{ edit_field_bool!(output_form_state, translate.t(LABEL_SERIES), resolve_series, XtreamTargetOutputFormAction::ResolveSeries) }
|
||||
</div>
|
||||
</TitledCard>
|
||||
<TitledCard title={translate.t(LABEL_RESOLVE_DELAY_SEC)}>
|
||||
<div class="tp__config-view__cols-2">
|
||||
{ edit_field_number_u16!(output_form_state, translate.t(LABEL_VOD), resolve_vod_delay, XtreamTargetOutputFormAction::ResolveVodDelay) }
|
||||
{ edit_field_number_u16!(output_form_state, translate.t(LABEL_SERIES), resolve_series_delay, XtreamTargetOutputFormAction::ResolveSeriesDelay) }
|
||||
</div>
|
||||
</TitledCard>
|
||||
{ edit_field_text_option!(output_form_state, translate.t(LABEL_FILTER), filter, XtreamTargetOutputFormAction::Filter) }
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
let render_trakt = || {
|
||||
let trakt_lists = trakt_lists_state.clone();
|
||||
let trakt_api = trakt_api_state.clone();
|
||||
let show_trakt_list_form = show_trakt_list_form_state.clone();
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
if *show_trakt_list_form {
|
||||
<TraktListItemForm
|
||||
on_submit={handle_add_trakt_list_item}
|
||||
on_cancel={handle_close_trakt_list_form}
|
||||
/>
|
||||
} else {
|
||||
// Trakt API Configuration
|
||||
<div class="tp__form-section">
|
||||
<h3>{translate.t(LABEL_API_CONFIGURATION)}</h3>
|
||||
{ edit_field_text!(trakt_api, translate.t(LABEL_TRAKT_API_KEY), key, TraktApiConfigFormAction::Key) }
|
||||
{ edit_field_text!(trakt_api, translate.t(LABEL_TRAKT_API_VERSION), version, TraktApiConfigFormAction::Version) }
|
||||
{ edit_field_text!(trakt_api, translate.t(LABEL_TRAKT_API_URL), url, TraktApiConfigFormAction::Url) }
|
||||
</div>
|
||||
|
||||
// Trakt Lists
|
||||
{ config_field_child!(translate.t(LABEL_TRAKT_LISTS), {
|
||||
let trakt_lists_list = trakt_lists.clone();
|
||||
html! {
|
||||
<div class="tp__form-list">
|
||||
<div class="tp__form-list__items">
|
||||
{
|
||||
for (*trakt_lists_list).iter().enumerate().map(|(idx, list)| {
|
||||
let content_type_str = match list.content_type {
|
||||
TraktContentType::Vod => "Vod",
|
||||
TraktContentType::Series => "Series",
|
||||
TraktContentType::Both => "Both",
|
||||
};
|
||||
html! {
|
||||
<div class="tp__form-list__item" key={format!("trakt-{idx}")}>
|
||||
<IconButton
|
||||
name={idx.to_string()}
|
||||
icon="Delete"
|
||||
onclick={handle_remove_trakt_list_item.clone()}/>
|
||||
<div class="tp__form-list__item-content">
|
||||
<span>
|
||||
<strong>{&list.user}</strong>
|
||||
{" / "}
|
||||
{&list.list_slug}
|
||||
{" - "}
|
||||
{&list.category_name}
|
||||
{" ("}
|
||||
{content_type_str}
|
||||
{", "}
|
||||
{list.fuzzy_match_threshold}
|
||||
{"%)"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="add_trakt_list"
|
||||
icon="Add"
|
||||
title={translate.t(LABEL_ADD_TRAKT_LIST)}
|
||||
onclick={handle_show_trakt_list_form}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
}
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
let render_edit_mode = || {
|
||||
html! {
|
||||
@@ -149,7 +298,7 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
{render_output()}
|
||||
</Panel>
|
||||
<Panel value={OutputFormPage::Trakt.to_string()} active={view_visible.to_string()}>
|
||||
{"TODO..."}
|
||||
{render_trakt()}
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,9 +308,23 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
let handle_apply_target = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
let output_form_state = output_form_state.clone();
|
||||
let trakt_api_state = trakt_api_state.clone();
|
||||
let trakt_lists_state = trakt_lists_state.clone();
|
||||
let block_id = props.block_id;
|
||||
Callback::from(move |_| {
|
||||
let output = output_form_state.data().clone();
|
||||
let mut output = output_form_state.data().clone();
|
||||
|
||||
// Handle Trakt configuration
|
||||
let trakt_lists = (*trakt_lists_state).clone();
|
||||
output.trakt = if trakt_lists.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(TraktConfigDto {
|
||||
api: trakt_api_state.data().clone(),
|
||||
lists: trakt_lists,
|
||||
})
|
||||
};
|
||||
|
||||
source_editor_ctx.on_form_change.emit((block_id, BlockInstance::Output(Rc::new(TargetOutputDto::Xtream(output)))));
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
@@ -176,14 +339,14 @@ pub fn XtreamTargetOutputView(props: &XtreamTargetOutputViewProps) -> Html {
|
||||
html! {
|
||||
<div class="tp__source-editor-form tp__config-view-page">
|
||||
<div class="tp__source-editor-form__toolbar tp__form-page__toolbar">
|
||||
<TextButton class="primary" name="apply_input"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply_target}></TextButton>
|
||||
<TextButton class="secondary" name="cancel_input"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
<TextButton class="primary" name="apply_input"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply_target}></TextButton>
|
||||
</div>
|
||||
{ render_edit_mode() }
|
||||
</div>
|
||||
|
||||
@@ -30,10 +30,10 @@ enum TargetFormPage {
|
||||
|
||||
impl Display for TargetFormPage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", match *self {
|
||||
TargetFormPage::Main => "Main".to_string(),
|
||||
TargetFormPage::Options => "Options".to_string(),
|
||||
})
|
||||
match *self {
|
||||
TargetFormPage::Main => write!(f, "Main"),
|
||||
TargetFormPage::Options => write!(f, "Options"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +139,10 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html {
|
||||
let target_options_state_1 = target_options_state.clone();
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
<div class="tp__config-view__cols-2">
|
||||
{ edit_field_bool!(target_options_state, translate.t(LABEL_IGNORE_LOGO), ignore_logo, ConfigTargetOptionsFormAction::IgnoreLogo) }
|
||||
{ edit_field_bool!(target_options_state, translate.t(LABEL_SHARE_LIVE_STREAMS), share_live_streams, ConfigTargetOptionsFormAction::ShareLiveStreams) }
|
||||
</div>
|
||||
{ edit_field_bool!(target_options_state, translate.t(LABEL_REMOVE_DUPLICATES), remove_duplicates, ConfigTargetOptionsFormAction::RemoveDuplicates) }
|
||||
{ config_field_child!(translate.t(LABEL_FORCE_REDIRECT), {
|
||||
html! {
|
||||
@@ -160,8 +162,10 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html {
|
||||
let target_form_state_1 = target_form_state.clone();
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
<div class="tp__config-view__cols-2">
|
||||
{ edit_field_bool!(target_form_state, translate.t(LABEL_ENABLED), enabled, ConfigTargetFormAction::Enabled) }
|
||||
{ edit_field_bool!(target_form_state, translate.t(LABEL_USE_MEMORY_CACHE), use_memory_cache, ConfigTargetFormAction::UseMemoryCache) }
|
||||
</div>
|
||||
{ edit_field_text!(target_form_state, translate.t(LABEL_NAME), name, ConfigTargetFormAction::Name) }
|
||||
{ edit_field_text!(target_form_state, translate.t(LABEL_FILTER), filter, ConfigTargetFormAction::Filter) }
|
||||
|
||||
@@ -260,14 +264,14 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html {
|
||||
html! {
|
||||
<div class="tp__source-editor-form tp__config-view-page">
|
||||
<div class="tp__source-editor-form_toolbar tp__form-page__toolbar">
|
||||
<TextButton class="primary" name="apply_input"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply_target}></TextButton>
|
||||
<TextButton class="secondary" name="cancel_input"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
<TextButton class="primary" name="apply_input"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply_target}></TextButton>
|
||||
</div>
|
||||
{ render_edit_mode() }
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
use crate::app::components::select::Select;
|
||||
use crate::app::components::{Card, DropDownOption, DropDownSelection, TextButton};
|
||||
use crate::{config_field_child, edit_field_number_u8, edit_field_text, generate_form_reducer};
|
||||
use shared::model::{TraktContentType, TraktListConfigDto};
|
||||
use yew::{function_component, html, use_memo, use_reducer, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_TRAKT_USER: &str = "LABEL.TRAKT_USER";
|
||||
const LABEL_TRAKT_LIST_SLUG: &str = "LABEL.TRAKT_LIST_SLUG";
|
||||
const LABEL_TRAKT_CATEGORY_NAME: &str = "LABEL.TRAKT_CATEGORY_NAME";
|
||||
const LABEL_TRAKT_CONTENT_TYPE: &str = "LABEL.TRAKT_CONTENT_TYPE";
|
||||
const LABEL_TRAKT_FUZZY_MATCH_THRESHOLD: &str = "LABEL.TRAKT_FUZZY_MATCH_THRESHOLD";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: TraktListFormState { form: TraktListConfigDto },
|
||||
action_name: TraktListFormAction,
|
||||
fields {
|
||||
User => user: String,
|
||||
ListSlug => list_slug: String,
|
||||
CategoryName => category_name: String,
|
||||
ContentType => content_type: TraktContentType,
|
||||
FuzzyMatchThreshold => fuzzy_match_threshold: u8,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct TraktListItemFormProps {
|
||||
pub on_submit: Callback<TraktListConfigDto>,
|
||||
pub on_cancel: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub initial: Option<TraktListConfigDto>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn TraktListItemForm(props: &TraktListItemFormProps) -> Html {
|
||||
let translate = use_translation();
|
||||
|
||||
let form_state: UseReducerHandle<TraktListFormState> = use_reducer(|| {
|
||||
TraktListFormState {
|
||||
form: props.initial.clone().unwrap_or_else(|| TraktListConfigDto {
|
||||
user: String::new(),
|
||||
list_slug: String::new(),
|
||||
category_name: String::new(),
|
||||
content_type: TraktContentType::Both,
|
||||
fuzzy_match_threshold: 80,
|
||||
}),
|
||||
modified: false,
|
||||
}
|
||||
});
|
||||
|
||||
let content_type_options = use_memo(form_state.form.content_type, |content_type| {
|
||||
let default_ct = content_type;
|
||||
vec![
|
||||
DropDownOption {
|
||||
id: "vod".to_string(),
|
||||
label: html! { "Vod" },
|
||||
selected: default_ct == &TraktContentType::Vod,
|
||||
},
|
||||
DropDownOption {
|
||||
id: "series".to_string(),
|
||||
label: html! { "Series" },
|
||||
selected: default_ct == &TraktContentType::Series,
|
||||
},
|
||||
DropDownOption {
|
||||
id: "both".to_string(),
|
||||
label: html! { "Both" },
|
||||
selected: default_ct == &TraktContentType::Both,
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
let handle_submit = {
|
||||
let form_state = form_state.clone();
|
||||
let on_submit = props.on_submit.clone();
|
||||
Callback::from(move |_| {
|
||||
let data = form_state.form.clone();
|
||||
if !data.user.trim().is_empty()
|
||||
&& !data.list_slug.trim().is_empty()
|
||||
&& !data.category_name.trim().is_empty() {
|
||||
on_submit.emit(data);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let handle_cancel = {
|
||||
let on_cancel = props.on_cancel.clone();
|
||||
Callback::from(move |_| {
|
||||
on_cancel.emit(());
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<Card class="tp__config-view__card tp__item-form">
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_USER), user, TraktListFormAction::User) }
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_LIST_SLUG), list_slug, TraktListFormAction::ListSlug) }
|
||||
{ edit_field_text!(form_state, translate.t(LABEL_TRAKT_CATEGORY_NAME), category_name, TraktListFormAction::CategoryName) }
|
||||
|
||||
{ config_field_child!(translate.t(LABEL_TRAKT_CONTENT_TYPE), {
|
||||
let form_state_ct = form_state.clone();
|
||||
html! {
|
||||
<Select
|
||||
name={"trakt_content_type"}
|
||||
multi_select={false}
|
||||
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
|
||||
if let DropDownSelection::Single(option) = selections {
|
||||
if let Ok(ct) = option.parse::<TraktContentType>() {
|
||||
form_state_ct.dispatch(TraktListFormAction::ContentType(ct));
|
||||
}
|
||||
}
|
||||
})}
|
||||
options={content_type_options.clone()}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
|
||||
{ edit_field_number_u8!(form_state, translate.t(LABEL_TRAKT_FUZZY_MATCH_THRESHOLD), fuzzy_match_threshold, TraktListFormAction::FuzzyMatchThreshold) }
|
||||
|
||||
<div class="tp__form-page__toolbar">
|
||||
<TextButton
|
||||
class="secondary"
|
||||
name="cancel_trakt_list"
|
||||
icon="Cancel"
|
||||
title={translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}
|
||||
/>
|
||||
<TextButton
|
||||
class="primary"
|
||||
name="submit_trakt_list"
|
||||
icon="Accept"
|
||||
title={translate.t("LABEL.SUBMIT")}
|
||||
onclick={handle_submit}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct TitledCardProps {
|
||||
pub title: AttrValue,
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
#[function_component(TitledCard)]
|
||||
pub fn titled_card(props: &TitledCardProps) -> Html {
|
||||
html! {
|
||||
<div class="tp__titled-card">
|
||||
<span class="tp__titled-card__title">{ props.title.clone() }</span>
|
||||
<div class="tp__titled-card__content">
|
||||
{ for props.children.iter() }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -246,14 +246,14 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
|
||||
</div>
|
||||
<div class="tp__proxy-user-credentials-form__toolbar tp__form-page__toolbar">
|
||||
<TextButton class="primary" name="save_user"
|
||||
icon="Save"
|
||||
title={ translate.t("LABEL.SAVE")}
|
||||
onclick={handle_save_user}></TextButton>
|
||||
<TextButton class="secondary" name="cancel_user"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
<TextButton class="primary" name="save_user"
|
||||
icon="Save"
|
||||
title={ translate.t("LABEL.SAVE")}
|
||||
onclick={handle_save_user}></TextButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "shared"
|
||||
version = "3.2.13"
|
||||
version = "3.2.14"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -24,7 +24,7 @@ impl EpgSourceDto {
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EpgConfigDto {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
const TRAKT_API_KEY: &str = "0183a05ad97098d87287fe46da4ae286f434f32e8e951caad4cc147c947d79a3";
|
||||
const TRAKT_API_VERSION: &str = "2";
|
||||
@@ -16,6 +18,29 @@ pub enum TraktContentType {
|
||||
Both,
|
||||
}
|
||||
|
||||
impl fmt::Display for TraktContentType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
TraktContentType::Vod => "Vod",
|
||||
TraktContentType::Series => "Series",
|
||||
TraktContentType::Both => "Both",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TraktContentType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"vod" => Ok(TraktContentType::Vod),
|
||||
"series" => Ok(TraktContentType::Series),
|
||||
"both" => Ok(TraktContentType::Both),
|
||||
_ => Err(format!("Invalid TraktContentType: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use enum_iterator::Sequence;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive( Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Sequence,
|
||||
PartialEq, Eq, Hash, Default)]
|
||||
@@ -12,4 +14,36 @@ pub enum StrmExportStyle {
|
||||
Emby,
|
||||
#[serde(rename = "jellyfin")]
|
||||
Jellyfin,
|
||||
}
|
||||
|
||||
impl StrmExportStyle {
|
||||
const KODI: &'static str = "Kodi";
|
||||
const PLEX: &'static str = "Plex";
|
||||
const EMBY: &'static str = "Emby";
|
||||
const JELLYFIN: &'static str = "Jellyfin";
|
||||
}
|
||||
|
||||
impl Display for StrmExportStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", match *self {
|
||||
Self::Kodi => Self::KODI,
|
||||
Self::Plex => Self::PLEX,
|
||||
Self::Emby => Self::EMBY,
|
||||
Self::Jellyfin => Self::JELLYFIN,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for StrmExportStyle {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
Self::KODI => Ok(Self::Kodi),
|
||||
Self::PLEX => Ok(Self::Plex),
|
||||
Self::EMBY => Ok(Self::Emby),
|
||||
Self::JELLYFIN => Ok(Self::Jellyfin),
|
||||
_ => Err(format!("Unknown StrmExportStyle: {}", s))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use enum_iterator::Sequence;
|
||||
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Hash)]
|
||||
@@ -31,3 +32,17 @@ impl Display for TargetType {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TargetType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
Self::M3U => Ok(Self::M3u),
|
||||
Self::XTREAM => Ok(Self::Xtream),
|
||||
Self::STRM => Ok(Self::Strm),
|
||||
Self::HDHOMERUN => Ok(Self::HdHomeRun),
|
||||
_ => Err(format!("Unknown TargetType: {}", s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user