This commit is contained in:
euzu
2025-07-07 17:55:27 +02:00
parent fc0eaa72d7
commit 5297916ce7
52 changed files with 918 additions and 149 deletions
+1 -2
View File
@@ -15,8 +15,7 @@ pub const EPG_TAG_ICON: &str = "icon";
// https://github.com/XMLTV/xmltv/blob/master/xmltv.dtd
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Default)]
#[derive(Debug, Clone, Eq, PartialEq,Default)]
pub enum XmlTagIcon {
#[default]
Undefined,
+2 -1
View File
@@ -27,7 +27,8 @@
--toolbar-button-hover-color: #ffffff;
--toolbar-button-hover-background-color: #004db4;
--popup-background-color: #282A36;
//--popup-background-color: #282A36;
--popup-background-color: #353535;
--popup-shadow-color: #06070a;
--popup-border-color: #646464;
--popup-color: #fff;
+17
View File
@@ -44,6 +44,15 @@ impl XtreamTargetOutputDto {
trakt.prepare();
}
}
pub fn has_any_option(&self) -> bool {
self.skip_live_direct_source
|| self.skip_video_direct_source
|| self.skip_series_direct_source
|| self.resolve_series
|| self.resolve_vod
|| self.trakt.is_some()
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
@@ -57,6 +66,14 @@ pub struct M3uTargetOutputDto {
pub mask_redirect_url: bool,
}
impl M3uTargetOutputDto {
pub fn has_any_option(&self) -> bool {
self.filename.is_some()
|| self.include_type_in_url
|| self.mask_redirect_url
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct StrmTargetOutputDto {
+1 -1
View File
@@ -28,7 +28,7 @@ wasm-bindgen-futures = "0"
[dependencies.web-sys]
version = "0.3"
features = ["Document", "Element", "Node", "Window", "HtmlCollection", "HtmlInputElement", "DomRect"]
features = ["Document", "Element", "Node", "Window", "HtmlCollection", "HtmlInputElement", "DomRect", "HtmlDialogElement"]
[dev-dependencies]
js-sys = "0.3"
+28 -15
View File
@@ -93,15 +93,15 @@
"XC": "xc",
"M3U": "m3u",
"XTREAM": "xtream",
"HDHOMERUN": "hdHomerun",
"HDHOMERUN": "hdhomerun",
"STRM": "strm",
"PLAYLIST_BROWSER": "Playlist Browser",
"RATE_LIMIT": "Rate Limit",
"PERIOD_MILLIS": "Period Millis",
"BURST_SIZE": "Burst Size",
"WEB_AUTH": "Auth",
"WEB_AUTH": "Auth",
"ISSUER": "Issuer",
"SECRET": "Secret",
"SECRET": "Secret",
"USERFILE": "Userfile",
"UI_ENABLED": "UI",
"PLAYER_SERVER": "Player Server",
@@ -146,20 +146,32 @@
"INCLUDE_TYPE_IN_URL": "Include type in url",
"MASK_REDIRECT_URL": "Mask redirect url",
"EDIT": "Edit",
"DELETE": "Delete"
"DELETE": "Delete",
"ACTIVE": "Active",
"DISABLED": "Disabled",
"IGNORE_LOGO": "Ignore logo",
"SHARE_LIVE_STREAMS": "Share live",
"REMOVE_DUPLICATES": "Remove duplicates",
"FORCE_REDIRECT": "Force redirect",
"FLAT": "Flat",
"UNDERSCORE_WHITESPACE": "Underscore whitespace",
"CLEANUP": "Cleanup",
"PROPERTIES": "Properties",
"DEVICE": "Device",
"USE_OUTPUT": "Use output"
},
"TABLE": {
"EMPTY": "",
"ID": "Id",
"ENABLED": "Enabled",
"NAME": "Name",
"OPTIONS": "Options",
"SORT": "Sort",
"FILTER": "Filter",
"OUTPUT": "Output",
"RENAME": "Rename",
"MAPPING": "Mapping",
"PROCESSING_ORDER": "Processing order",
"ID": "Id",
"ENABLED": "Enabled",
"NAME": "Name",
"OPTIONS": "Options",
"SORT": "Sort",
"FILTER": "Filter",
"OUTPUT": "Output",
"RENAME": "Rename",
"MAPPING": "Mapping",
"PROCESSING_ORDER": "Processing order",
"WATCH": "Watch"
},
"TITLE": {
@@ -170,6 +182,7 @@
"INVALID_REGEXP": "Invalid regexp",
"INVALID_FILETYPE": "Invalid filetype",
"TARGET_NOT_EXISTS": "Target does not exist",
"CONFIRM_DELETE": "You really want to delete?",
"DOWNLOAD": {
"SUCCESS": "Successfully downloaded",
"FAIL": "Failed to download!",
@@ -235,7 +248,7 @@
}
}
},
"SETUP":{
"SETUP": {
"LABEL": {
"NEXT": "Next",
"BACK": "Back",
+8
View File
@@ -350,5 +350,13 @@
{
"keys": ["Hamburger", "Menu", "Popup", "Bars"],
"path": "M 2,18.333002 H 21.9995 V 16.110835 H 2 Z M 2,12.777585 H 21.9995 V 10.555417 H 2 Z M 2,5 V 7.2221668 H 21.9995 V 5 Z"
},
{
"keys": ["Cancel"],
"path": "M 12,2 C 6.4699773,2 2,6.4699773 2,12 2,17.530023 6.4699773,22 12,22 17.529985,22 22,17.530023 22,12 22,6.4699773 17.529985,2 12,2 m 0,18.000023 C 7.5900039,20.000023 3.9999773,16.409996 3.9999773,12 3.9999773,7.5900039 7.5900039,3.9999773 12,3.9999773 c 4.409996,0 8.000023,3.5900266 8.000023,8.0000227 0,4.409996 -3.590027,8.000023 -8.000023,8.000023 M 15.589989,6.9999811 12,10.590008 8.4099734,6.9999811 6.9999811,8.4100112 10.590008,12 6.9999811,15.589989 8.4099734,17.000019 12,13.409992 l 3.589989,3.590027 1.41003,-1.41003 L 13.409992,12 17.000019,8.4100112 Z"
},
{
"keys": ["Ok"],
"path": "M 11.999981,2 C 6.4799846,2 2,6.4799959 2,11.999992 2,17.519989 6.4799846,21.999989 11.999981,21.999989 17.519978,21.999989 22,17.519989 22,11.999992 22,6.4799959 17.519978,2 11.999981,2 m 0,17.999989 c -4.4099877,0 -7.9999697,-3.589997 -7.9999697,-7.999997 C 4.0000113,7.5899971 7.5899933,4 11.999981,4 c 4.410026,0 8.000008,3.5899971 8.000008,7.999992 0,4.41 -3.589982,7.999997 -8.000008,7.999997 m 4.590007,-12.4199925 -6.58998,6.5899955 -2.5899957,-2.579999 -1.4100274,1.410001 4.0000231,3.999997 7.999969,-7.9999967 z"
}
]
+20 -7
View File
@@ -21,17 +21,18 @@
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
border-radius: var(--scrollbar-border-radius);
background-color: var(--scrollbar-track-color);
}
::-webkit-scrollbar {
width: 12px;
width: var(--scrollbar-width);
height: var(--scrollbar-width);
background-color: transparent;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
border-radius: var(--scrollbar-width);
//-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: var(--scrollbar-handle-color);
}
@@ -59,11 +60,23 @@ option {
cursor: pointer;
}
dialog {
padding: 0 !important;
}
.tp__delete_action {
color: var(--attention-color) !important;
fill: var(--attention-color) !important;
}
.svg-icon {
pointer-events: none;
width: 1rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
background: transparent;
}
li {
background: transparent;
}
+24 -6
View File
@@ -1,6 +1,10 @@
:root {
--max-view-width: 1280px;
--max-table-cell-width: 240px;
--media-size-mobile: 600px;
--app-header-height: 56px;
--scrollbar-width: 8px;
--scrollbar-border-radius: 4px;
--gap-small: 0.3rem;
--gap-default: 0.5rem;
--gap-large: 1rem;
@@ -8,13 +12,16 @@
--padding-micro: 0.2rem;
--padding-mini: 0.5rem;
--padding-small: 1rem;
--padding-default: 1.5rem;
--padding-large: 2rem;
--padding-small: 0.875rem;
--padding-default: 1rem;
--padding-large: 1.5rem;
--padding-larger: 2rem;
--primary-color: #124a9b;
--secondary-color: #f67070;
--action-item-background-color: #242427;
--action-item-color: #ffffff;
--text-color: #ffffff;
--modest-text-color: #A1A1AAFF;
@@ -28,6 +35,7 @@
--menu-text-color: #e2e2e5;
--menu-icon-color: #707079;
--menu-item-hover-background-color: var(--action-item-background-color);
--menu-item-hover-color: var(--action-item-color);
--menu-text-active-color: var(--primary-color);
--menu-item-active-background-color: var(--menu-item-hover-background-color);
--icon-button-hover-color: #8f8f97;
@@ -42,6 +50,10 @@
--text-button-primary-color: #ffffff;
--text-button-primary-hover-background-color: #2f68be;
--text-button-primary-hover-color: #ffffff;
--text-button-secondary-background-color: #e53d3d;
--text-button-secondary-color: #ffffff;
--text-button-secondary-hover-background-color: var(--secondary-color);
--text-button-secondary-hover-color: #ffffff;
--toggle-switch-on-background-color: #36c55a;
--toggle-switch-off-background-color: #3f3f45;
@@ -49,9 +61,10 @@
--table-header-background-color: #242427;
--table-card-background-color: #242427;
--table-alternate-background-color: #1b1b1e;
--discord-color: #5764f0;
--documenation-color: #2b944b;
--documentation-color: #2b944b;
--app-header-background-color: #18181b;
--app-header-color: #73d6fe;
@@ -74,7 +87,7 @@
--tag-inactive-background-color: #3a3a3a;
--popup-background-color: #18181b;
--popup-background-color: #313133; //#18181b;
--popup-shadow: 0 3px 8px #06070a;
--popup-border-color: #2f2f31;
--popup-color: #fff;
@@ -83,7 +96,12 @@
--attention-color: #f67070;
--warn-color: #b88e22;
--active-color: #34d399;
--output-xtream-color: #06b6d4;
--output-m3u-color: #10b981;
--output-hdhomerun-color: #6366f1;
--output-strm-color: #f43f5e;
--hint-color: #888888;
@@ -207,7 +225,7 @@ body[data-theme="bright"] {
--text-button-primary-hover-color: #ffffff;
--discord-color: #5865f2;
--documenation-color: #22c55e;
--documentation-color: #22c55e;
--app-header-background-color: #e0e7ff;
--app-header-color: #2563eb;
+2 -1
View File
@@ -20,8 +20,9 @@
display: flex;
flex-flow: column;
box-sizing: border-box;
padding: 0 var(--padding-large);
padding: 0 var(--padding-larger);
align-items: center;
overflow: hidden;
}
}
}
+2
View File
@@ -4,6 +4,8 @@
justify-content: space-between;
padding-left: 2rem;
height: var(--app-header-height);
min-height: var(--app-header-height);
max-height: var(--app-header-height);
background-color: var(--app-header-background-color);
border-bottom: 1px solid var(--border-color);
fill: var(--menu-icon-color);
+7 -2
View File
@@ -1,3 +1,5 @@
@forward "app_header";
@forward "app";
@forward "components/input";
@forward "components/menu_item";
@forward "components/icon_button";
@@ -17,14 +19,17 @@
@forward "components/playlist/playlist_view";
@forward "components/playlist/assistant/playlist_assistant";
@forward "components/reveal_content";
@forward "app_header";
@forward "app";
@forward "components/playlist/target/target_watch";
@forward "components/playlist/target/target_options";
@forward "components/playlist/target/target_output";
@forward "components/playlist/target/xtream_output";
@forward "components/playlist/target/m3u_output";
@forward "components/playlist/target/strm_output";
@forward "components/playlist/target/hdhomerun_output";
@forward "components/playlist/target/target_table";
@forward "components/playlist/playlist_mappings";
@forward "components/playlist/playlist_processing";
@forward "components/tag_list";
@forward "components/chip";
@forward "components/popup_menu";
@forward "components/dialog";
@@ -17,8 +17,7 @@
span {
flex: 1 1 auto;
}
span,
svg {
span {
pointer-events: none;
}
svg {
+23
View File
@@ -0,0 +1,23 @@
dialog {
display: flex;
flex-flow: column;
gap: var(--gap-large);
padding: var(--padding-larger);
background-color: var(--card-background-color);
color: var(--card-color);
border: none;
border-radius: var(--border-radius);
filter: drop-shadow(6px 6px 6px var(--popup-shadow-color));
&::backdrop {
background: rgba(0, 0, 0, 0.3);
}
}
.tp__dialog {
min-width: 400px;
&__toolbar {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
}
@@ -11,10 +11,6 @@
height: min-content;
width: min-content;
svg {
pointer-events: none;
}
&:focus,
&:hover {
fill: var(--icon-button-hover-color);
+2 -2
View File
@@ -12,7 +12,6 @@
height: 1.3rem;
svg {
width: 1.3rem;
pointer-events: none;
}
label {
pointer-events: none;
@@ -20,6 +19,7 @@
}
&:hover {
background-color: var(--menu-item-hover-background-color)
background-color: var(--menu-item-hover-background-color);
color: var(--menu-item-hover-color)
}
}
+2 -12
View File
@@ -1,22 +1,12 @@
.tp__popup-menu {
display: flex;
flex-flow: column;
padding: 8px;
padding: var(--padding-mini);
border: 1px solid var(--border-color);
border-radius: 4px;
border-radius: var(--border-radius);
background-color: var(--popup-background-color);
color: var(--popup-color);
box-shadow: var(--popup-shadow);
z-index: 9999;
ul {
margin: 0;
padding: 0;
list-style: none;
background: transparent;
}
li {
background: transparent;
}
}
+5 -5
View File
@@ -64,11 +64,11 @@
width: 2rem;
}
&_update { svg { path { fill: var(--menu-icon-color-update); } } }
&_user { svg { path { fill: var(--menu-icon-color-user); } } }
&_apiserver { svg { path { fill: var(--menu-icon-color-api_server); } } }
&_mainconfig { svg { path { fill: var(--menu-icon-color-main_config); } } }
&_status { svg { path { fill: var(--menu-icon-color-status); } } }
&_update { fill: var(--menu-icon-color-update); }
&_user { fill: var(--menu-icon-color-user); }
&_apiserver { fill: var(--menu-icon-color-api_server); }
&_mainconfig { fill: var(--menu-icon-color-main_config);}
&_status { fill: var(--menu-icon-color-status); }
* {
+14 -2
View File
@@ -12,31 +12,43 @@
&__container {
display: flex;
flex-flow: column;
overflow-x: auto;
overflow: auto;
padding-right: 1px;
}
&__table {
width: 100%;
border-collapse: collapse;
position: relative;
thead {
tr {
th {
position: sticky;
top: 0;
z-index: 2;
font-size: 1.1rem;
font-weight: bold;
//text-align: center;
padding: 0.875rem;
padding: var(--padding-default);
background-color: var(--table-header-background-color);
box-shadow: inset 0 -1px 0 var(--border-color);
}
}
}
tbody {
tr {
&:nth-child(even) {
background-color: var(--table-alternate-background-color);
}
&:nth-child(odd) {
}
&:not(:last-child) {
border-bottom: 1px solid var(--border-color);
}
td {
vertical-align: top;
padding: 0.875rem;
}
}
+12 -2
View File
@@ -20,8 +20,7 @@
width: 1.5rem;
}
span,
svg {
span {
pointer-events: none;
}
@@ -42,4 +41,15 @@
fill : var(--text-button-primary-hover-color);
outline: none;
}
}
.tp__text-button.tp__button-secondary {
background-color: var(--text-button-secondary-background-color);
color: var(--text-button-secondary-color);
fill : var(--text-button-secondary-color);
&:hover {
background-color: var(--text-button-secondary-hover-background-color);
color: var(--text-button-secondary-hover-color);
fill : var(--text-button-secondary-hover-color);
outline: none;
}
}
@@ -6,7 +6,7 @@ $dashboard-mobile-size: 1280px;
box-sizing: border-box;
flex: 1 1 auto;
width: 100%;
max-width: 1280px;
max-width: var(--max-view-width);
&__header {
display: flex;
@@ -44,7 +44,7 @@ $dashboard-mobile-size: 1280px;
}
.tp__documentation {
fill: var(--documenation-color);
fill: var(--documentation-color);
svg {
transform: scale(1.4);
@@ -0,0 +1,20 @@
.tp__playlist-mappings {
li {
display: flex;
flex-flow: row nowrap;
gap: var(--gap-small);
align-items: center;
background: transparent;
white-space: nowrap;
padding: var(--padding-micro) var(--padding-mini);
border-radius: var(--border-radius);
&:hover {
cursor: pointer;
background-color: var(--menu-item-hover-background-color);
color: var(--menu-item-hover-color)
}
}
}
@@ -0,0 +1,6 @@
.tp__playlist-processing {
display: flex;
flex-flow: row;
justify-content: center;
align-items: center;
}
@@ -4,9 +4,10 @@
box-sizing: border-box;
flex: 1 1 auto;
width: 100%;
max-width: 1280px;
//max-width: var(--max-view-width);
padding: var(--padding-default) 0;
gap: var(--gap-default);
overflow: hidden;
&__body {
display: flex;
@@ -14,6 +15,7 @@
box-sizing: border-box;
flex: 1 1 auto;
width: 100%;
overflow: hidden;
}
}
@@ -22,9 +24,10 @@
display: flex;
flex-flow: column;
box-sizing: border-box;
overflow: hidden;
flex: 1 1 auto;
width: 100%;
max-width: 1280px;
//max-width: var(--max-view-width);
gap: var(--gap-default);
&__header {
@@ -36,4 +39,13 @@
justify-content: space-between;
align-items: center;
}
&__body {
display: flex;
flex-flow: column;
box-sizing: border-box;
flex: 1 1 auto;
width: 100%;
overflow: hidden;
}
}
@@ -1,3 +1,3 @@
.tp__strm_output {
.tp__strm-output {
}
@@ -0,0 +1,49 @@
.tp__target-options__panel {
display: flex;
flex-flow: column;
align-items: flex-start;
gap: var(--gap-default);
box-sizing: border-box;
max-width: var(--max-table-cell-width);
height: 100%;
overflow: hidden;
}
.tp__target-options {
display: flex;
flex-flow: column;
gap: var(--gap-default);
&__has_options {
.tp__collapse-panel__header {
font-weight: bold;
color: var(--active-color);
&:hover {
color: var(--menu-button-hover-color);
}
}
}
&__section {
display: flex;
flex-flow: column;
gap: var(--gap-small);
justify-content: center;
padding: var(--padding-micro) var(--padding-mini);
.tp__tag_list {
flex-flow: row nowrap;
}
}
&__label {
font-weight: bold;
white-space: nowrap;
vertical-align: middle;
color: var(--modest-text-color);
&:after {
content: ":";
}
}
}
@@ -1,37 +1,96 @@
.tp__target_output {
.tp__target-output {
display: flex;
flex-flow: column;
gap: var(--gap-small);
gap: var(--gap-default);
max-width: var(--max-table-cell-width);
overflow: hidden;
box-sizing: border-box;
.tp__collapse-panel__header-title {
font-weight: bold;
}
}
.tp__target_output__output {
.tp__target-output__output {
display: flex;
justify-content: center;
flex-flow: column;
gap: var(--gap-small);
box-sizing: border-box;
&__section {
display: flex;
flex-flow: column;
gap: var(--gap-small);
justify-content: center;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--padding-micro) var(--padding-mini);
background-color: var(--table-card-background-color);
overflow: hidden;
.tp__tag_list {
flex-flow: row nowrap;
}
}
li {
overflow: hidden;
text-overflow: ellipsis;
}
}
&__row {
flex-flow: row;
}
&__label {
font-size: 1.1rem;
font-weight: bold;
white-space: nowrap;
vertical-align: middle;
color: var(--modest-text-color);
&:after {
content: ":";
}
}
}
.tp__target-output__xtream.tp__target-output__has_options .tp__collapse-panel__header {
color: var(--output-xtream-color);
}
.tp__target-output__m3u.tp__target-output__has_options .tp__collapse-panel__header {
color: var(--output-m3u-color);
}
.tp__target-output__strm .tp__collapse-panel__header {
color: var(--output-strm-color);
}
.tp__target-output__hdhomerun .tp__collapse-panel__header {
color: var(--output-hdhomerun-color);
}
.tp__target-output__xtream .tp__collapse-panel__header,
.tp__target-output__m3u .tp__collapse-panel__header,
.tp__target-output__strm .tp__collapse-panel__header,
.tp__target-output__hdhomerun .tp__collapse-panel__header {
&:hover {
color: var(--menu-button-hover-color);
}
}
.tp__xtream-output {
border-right: 3px solid var(--output-xtream-color);
padding-right: 1px;
}
.tp__m3u-output {
border-right: 3px solid var(--output-m3u-color);
padding-right: 1px;
}
.tp__hdhomerun-output {
border-right: 3px solid var(--output-hdhomerun-color);
padding-right: 1px;
}
.tp__strm-output {
border-right: 3px solid var(--output-strm-color);
padding-right: 1px;
.tp__target-output__output__section {
}
}
@@ -1,4 +1,8 @@
.tp__target-table {
display: flex;
flex-flow: column;
box-sizing: border-box;
overflow: hidden;
.tp__icon-button {
width: 2rem;
}
@@ -0,0 +1,13 @@
.tp__target-watch {
display: flex;
flex-flow: column;
color: var(--modest-text-color);
li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&::before {
content: " - ";
}
}
}
-9
View File
@@ -35,12 +35,3 @@ body {
background-color: inherit;
}
dialog {
border: none;
border-radius: var(--border-radius);
background-color: var(--card-background-color);
filter: drop-shadow(6px 6px 6px var(--popup-shadow-color));
&::backdrop {
background: rgba(0, 0, 0, 0.3); /* Schwarzer, halbtransparenter Hintergrund */
}
}
+6 -13
View File
@@ -1,4 +1,3 @@
use std::rc::Rc;
use wasm_bindgen::JsCast;
use web_sys::{ MouseEvent};
use yew::{classes, function_component, html, Callback, Html, Properties};
@@ -8,22 +7,16 @@ pub fn convert_bool_to_chip_style(value: bool) -> Option<String> {
Option::from((if value { "active" } else { "inactive" }).to_string())
}
#[derive(Clone, PartialEq, Debug)]
pub struct Tag {
pub label: String,
pub class: Option<String>,
}
#[derive(Properties, Clone, PartialEq)]
pub struct ChipProps {
pub(crate) tag: Rc<Tag>,
pub label: String,
pub class: Option<String>,
#[prop_or(false)]
removable: bool,
pub removable: bool,
#[prop_or_else(|| Callback::noop())]
on_remove: Callback<String>,
pub on_remove: Callback<String>,
}
#[function_component]
pub fn Chip(props: &ChipProps) -> Html {
@@ -45,8 +38,8 @@ pub fn Chip(props: &ChipProps) -> Html {
};
html! {
<span class={classes!("tp__chip", props.tag.class.clone())}>
{ &props.tag.label }
<span class={classes!("tp__chip", props.class.clone())}>
{ &props.label }
if props.removable {
<span class="tp__remove" onclick={handle_remove}>{"×"}</span>
}
+9 -2
View File
@@ -5,8 +5,13 @@ use crate::app::components::AppIcon;
pub struct CollapsePanelProps {
#[prop_or(true)]
pub expanded: bool,
#[prop_or_default]
pub title: String,
#[prop_or_default]
pub title_content: Option<Html>,
pub children: Children,
#[prop_or_default]
pub class: Option<String>,
}
#[function_component]
@@ -19,9 +24,11 @@ pub fn CollapsePanel(props: &CollapsePanelProps) -> Html {
};
html! {
<div class={classes!("tp__collapse-panel", if *expanded {""} else {"tp__collapsed"})}>
<div class={classes!("tp__collapse-panel", if *expanded {""} else {"tp__collapsed"}, props.class.as_ref().map(ToString::to_string))}>
<div class="tp__collapse-panel__header" onclick={toggle}>
<span>{ props.title.clone() }</span>
<span class="tp__collapse-panel__header-title">
{ props.title_content.clone().unwrap_or_else(|| html! { &props.title }) }
</span>
<AppIcon name={ if *expanded { "ChevronUp" } else {"ChevronDown"} }/>
</div>
<div class="tp__collapse-panel__body">
@@ -0,0 +1,56 @@
use yew::prelude::*;
use web_sys::HtmlDialogElement;
use yew_i18n::use_translation;
use crate::app::components::TextButton;
#[derive(Properties, PartialEq)]
pub struct ConfirmDialogProps {
pub title: String,
pub ok_caption: String,
pub cancel_caption: String,
pub on_confirm: Callback<bool>,
}
#[function_component]
pub fn ConfirmDialog(props: &ConfirmDialogProps) -> Html {
let dialog_ref = use_node_ref();
let translate = use_translation();
{
let dialog_ref = dialog_ref.clone();
use_effect(move || {
if let Some(dialog) = dialog_ref.cast::<HtmlDialogElement>() {
let _ = dialog.show_modal();
}
|| ()
});
}
let on_result = {
let dialog_ref = dialog_ref.clone();
let on_confirm = props.on_confirm.clone();
move |result: bool| {
if let Some(dialog) = dialog_ref.cast::<HtmlDialogElement>() {
dialog.close();
}
on_confirm.emit(result);
}
};
let on_ok = {
let on_result = on_result.clone();
Callback::from(move |_| on_result(true))
};
let on_cancel = Callback::from(move |_| on_result(false));
html! {
<dialog ref={dialog_ref} class="tp__dialog tp__confirm-dialog">
<h2>{ &props.title }</h2>
<div class="tp__dialog__toolbar">
<TextButton style="secondary" name="cancel" icon="Cancel" onclick={on_cancel} title={translate.t(&props.cancel_caption)} />
<TextButton style="primary" name="ok" icon="Ok" onclick={on_ok} title={translate.t(&props.ok_caption)} />
</div>
</dialog>
}
}
+5 -3
View File
@@ -20,6 +20,7 @@ mod reveal_content;
mod tag_list;
mod chip;
mod popup_menu;
mod confirm_dialog;
pub use self::svg_icon::*;
// pub use self::input::*;
@@ -41,6 +42,7 @@ pub use self::breadcrumbs::*;
pub use self::playlist::*;
pub use self::table::*;
pub use self::toggle_switch::*;
use self::chip::*;
use self::tag_list::*;
use self::popup_menu::*;
pub use self::chip::*;
pub use self::tag_list::*;
pub use self::popup_menu::*;
pub use self::confirm_dialog::*;
@@ -0,0 +1,24 @@
use yew::prelude::*;
use crate::app::components::AppIcon;
#[derive(Properties, PartialEq, Clone)]
pub struct PlaylistMappingsProps {
pub mappings: Option<Vec<String>>,
}
#[function_component]
pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html {
html! {
<div class="tp__playlist-mappings">
<ul>
{
match props.mappings.as_ref() {
Some(vec) => vec.iter().map(|item| html! { <li>{ item } <AppIcon name="Link" /></li> }).collect::<Html>(),
None => html! {},
}
}
</ul>
</div>
}
}
+5 -1
View File
@@ -6,6 +6,8 @@ mod page;
mod target_table;
mod target;
pub mod playlist_view;
mod processing;
mod mappings;
pub use self::assistant::*;
pub use self::list::*;
@@ -13,4 +15,6 @@ pub use self::create::*;
pub use self::context::*;
pub use self::page::*;
pub use self::target_table::*;
pub use self::target::*;
pub use self::target::*;
pub use self::processing::*;
pub use self::mappings::*;
@@ -0,0 +1,30 @@
use std::rc::Rc;
use yew::prelude::*;
use shared::model::ProcessingOrder;
use crate::app::components::{Tag, TagList};
#[derive(Properties, PartialEq, Clone)]
pub struct PlaylistProcessingProps {
pub order: ProcessingOrder,
}
#[function_component]
pub fn PlaylistProcessing(props: &PlaylistProcessingProps) -> Html {
let tags = use_memo(props.order, |order| {
let text = match order {
ProcessingOrder::Frm => vec!["F", "R", "M"],
ProcessingOrder::Fmr => vec!["F", "M", "R"],
ProcessingOrder::Rfm => vec!["R", "F", "M"],
ProcessingOrder::Rmf => vec!["R", "M", "F"],
ProcessingOrder::Mfr => vec!["M", "F", "R"],
ProcessingOrder::Mrf => vec!["M", "R", "F"],
};
text.iter().map(|s| Rc::new(Tag { label: s.to_string(), class: None })).collect::<Vec<Rc<Tag>>>()
});
html! {
<div class="tp__playlist-processing">
<TagList tags={(*tags).clone()} />
</div>
}
}
@@ -1,4 +1,5 @@
use yew::prelude::*;
use yew_i18n::use_translation;
use shared::model::HdHomeRunTargetOutputDto;
#[derive(Properties, PartialEq, Clone)]
@@ -8,9 +9,21 @@ pub struct HdHomeRunOutputProps {
#[function_component]
pub fn HdHomeRunOutput(props: &HdHomeRunOutputProps) -> Html {
let translator = use_translation();
html! {
<div class="tp__hdhomerun_output tp__target_output__output">
<div class="tp__hdhomerun-output tp__target-output__output">
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.DEVICE")}</span>
<span>{ props.output.device.clone() }</span>
</div>
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.USERNAME")}</span>
<span>{ props.output.username.clone() }</span>
</div>
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.USE_OUTPUT")}</span>
<span>{ props.output.use_output.map_or_else(String::new, |o| o.to_string()) }</span>
</div>
</div>
}
}
}
@@ -2,7 +2,8 @@ use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
use shared::model::M3uTargetOutputDto;
use crate::app::components::chip::{convert_bool_to_chip_style, Chip, Tag};
use crate::app::components::chip::{convert_bool_to_chip_style};
use crate::app::components::Tag;
use crate::app::components::tag_list::TagList;
#[derive(Properties, PartialEq, Clone)]
@@ -26,12 +27,12 @@ pub fn M3uOutput(props: &M3uOutputProps) -> Html {
html! {
<div class="tp__m3u_output tp__target_output__output">
<div class="tp__target_output__output__section">
<span class="tp__target_output__output__label">{translator.t("LABEL.FILENAME")}</span>
<div class="tp__m3u-output tp__target-output__output">
<div class="tp__target-output__output__section">
<span class="tp__target-output__output__label">{translator.t("LABEL.FILENAME")}</span>
{ props.output.filename.as_ref().map(|f| html! {<span>{ f }</span>}) }
</div>
<div class="tp__target_output__output__section">
<div class="tp__target-output__output__section">
<TagList tags={(*tags).clone()} />
</div>
</div>
@@ -3,9 +3,13 @@ mod m3u_output;
mod strm_output;
mod hdhomerun_output;
mod target_output;
mod target_options;
mod target_watch;
pub use self::xtream_output::*;
pub use self::m3u_output::*;
pub use self::strm_output::*;
pub use self::hdhomerun_output::*;
pub use self::target_output::*;
pub use self::target_options::*;
pub use self::target_watch::*;
@@ -1,5 +1,8 @@
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
use shared::model::{StrmTargetOutputDto};
use crate::app::components::{convert_bool_to_chip_style, Tag, TagList};
#[derive(Properties, PartialEq, Clone)]
pub struct StrmOutputProps {
@@ -8,9 +11,46 @@ pub struct StrmOutputProps {
#[function_component]
pub fn StrmOutput(props: &StrmOutputProps) -> Html {
let translator = use_translation();
let tags = {
let output = props.output.clone();
let translate = translator.clone();
use_memo(output, move |output| {
vec![
Rc::new(Tag { class: convert_bool_to_chip_style(output.flat), label: translate.t("LABEL.FLAT") }),
Rc::new(Tag { class: convert_bool_to_chip_style(output.underscore_whitespace), label: translate.t("LABEL.UNDERSCORE_WHITESPACE") }),
Rc::new(Tag { class: convert_bool_to_chip_style(output.cleanup), label: translate.t("LABEL.CLEANUP") }),
]
})
};
html! {
<div class="tp__strm_output tp__target_output__output">
<div class="tp__strm-output tp__target-output__output">
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.DIRECTORY")}</span>
<span>{ props.output.directory.clone() }</span>
</div>
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.USERNAME")}</span>
{ props.output.username.as_ref().map(|f| html! {<span>{ f }</span>}) }
</div>
<div class="tp__target-output__output__section">
<TagList tags={(*tags).clone()} />
</div>
{
match props.output.strm_props.as_ref() {
None => html! {},
Some(props) => html! {
<div class="tp__target-output__output__section">
<span class="tp__target-output__output__label">{translator.t("LABEL.PROPERTIES")}</span>
<ul>
{ props.iter().map(|p| html! { <li>{p}</li> }).collect::<Html>() }
</ul>
</div>
}
}
}
</div>
}
}
}
@@ -0,0 +1,82 @@
use crate::app::components::{convert_bool_to_chip_style, CollapsePanel, Tag, TagList};
use shared::model::{ClusterFlags, ConfigTargetDto};
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct TargetOptionsProps {
pub target: Rc<ConfigTargetDto>,
}
#[function_component]
pub fn TargetOptions(props: &TargetOptionsProps) -> Html {
let translate = use_translation();
let tags = use_memo(props.target.clone(), |target| {
let (flags, options, redirect) = match target.options.as_ref() {
None => (
vec![false, false, false, false, false, false],
vec![
(false, "LABEL.IGNORE_LOGO"),
(false, "LABEL.SHARE_LIVE_STREAMS"),
(false, "LABEL.REMOVE_DUPLICATES"),
],
vec![
(false, "LABEL.LIVE"),
(false, "LABEL.VOD"),
(false, "LABEL.SERIES"),
],
),
Some(options) => {
let force_redirect = match options.force_redirect {
None => vec![
(false, "LABEL.LIVE"),
(false, "LABEL.VOD"),
(false, "LABEL.SERIES"),
],
Some(force_redirect) => vec![
(force_redirect.contains(ClusterFlags::Live), "LABEL.LIVE"),
(force_redirect.contains(ClusterFlags::Vod), "LABEL.VOD"),
(force_redirect.contains(ClusterFlags::Series), "LABEL.SERIES"),
],
};
(
vec![options.ignore_logo, options.share_live_streams, options.remove_duplicates, force_redirect[0].0, force_redirect[1].0, force_redirect[2].0],
vec![
(options.ignore_logo, "LABEL.IGNORE_LOGO"),
(options.share_live_streams, "LABEL.SHARE_LIVE_STREAMS"),
(options.remove_duplicates, "LABEL.REMOVE_DUPLICATES"),
],
force_redirect,
)
}
};
(
flags.iter().any(|&v| v),
options.iter().map(|(o, t)| Rc::new(Tag { class: convert_bool_to_chip_style(*o), label: translate.t(t) })).collect::<Vec<Rc<Tag>>>(),
redirect.iter().map(|(o, t)| Rc::new(Tag { class: convert_bool_to_chip_style(*o), label: translate.t(t) })).collect::<Vec<Rc<Tag>>>(),
)
});
let has_options = tags.0;
let opts: Vec<Rc<Tag>> = (&tags.1).clone();
let redirect: Vec<Rc<Tag>> = (&tags.2).clone();
html! {
<CollapsePanel expanded={false} class={format!("tp__target-options__panel{}", if has_options { " tp__target-options__has_options"} else {""})}
title={translate.t("LABEL.SETTINGS")}>
<div class="tp__target-options">
<div class="tp__target-options__section">
<TagList tags={opts} />
</div>
<div class="tp__target-options__section">
<span class="tp__target-options__label">{translate.t("LABEL.FORCE_REDIRECT")}</span>
<TagList tags={redirect} />
</div>
</div>
</CollapsePanel>
}
}
@@ -12,28 +12,31 @@ pub struct TargetOutputProps {
#[function_component]
pub fn TargetOutput(props: &TargetOutputProps) -> Html {
let translate = use_translation();
html! {
<div class="tp__target_output">
<div class="tp__target-output">
{
props.target.output.iter().map(|output| {
props.target.output.iter().map(|output: &TargetOutputDto| {
match output {
TargetOutputDto::Xtream(xc) => html! {
<CollapsePanel expanded={false} title={translate.t("LABEL.XTREAM")}>
<CollapsePanel class={format!("tp__target-output__xtream{}", if xc.has_any_option() { " tp__target-output__has_options" } else {""}) }
expanded={false} title={translate.t("LABEL.XTREAM")}>
<XtreamOutput output={xc.clone()} />
</CollapsePanel>
},
TargetOutputDto::M3u(m3u) => html! {
<CollapsePanel expanded={false} title={translate.t("LABEL.M3U")}>
<CollapsePanel class={format!("tp__target-output__m3u{}", if m3u.has_any_option() { " tp__target-output__has_options" } else {""}) }
expanded={false} title={translate.t("LABEL.M3U")}>
<M3uOutput output={m3u.clone()}/>
</CollapsePanel>
},
TargetOutputDto::Strm(strm) => html! {
<CollapsePanel expanded={false} title={translate.t("LABEL.STRM")}>
<CollapsePanel class="tp__target-output__strm" expanded={false} title={translate.t("LABEL.STRM")}>
<StrmOutput output={strm.clone()}/>
</CollapsePanel>
},
TargetOutputDto::HdHomeRun(hdhr) => html! {
<CollapsePanel expanded={false} title={translate.t("LABEL.HDHOMERUN")}>
<CollapsePanel class="tp__target-output__hdhomerun" expanded={false} title={translate.t("LABEL.HDHOMERUN")}>
<HdHomeRunOutput output={hdhr.clone()}/>
</CollapsePanel>
},
@@ -0,0 +1,25 @@
use crate::app::components::{convert_bool_to_chip_style, CollapsePanel, Tag, TagList};
use shared::model::{ClusterFlags, ConfigTargetDto};
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct TargetWatchProps {
pub target: Rc<ConfigTargetDto>,
}
#[function_component]
pub fn TargetWatch(props: &TargetWatchProps) -> Html {
let translate = use_translation();
match props.target.watch.as_ref() {
None => html! {},
Some(watch) => html! {
<div class="tp__target-watch">
<ul>
{ watch.iter().map(|item| html! { <li>{ item }</li> }).collect::<Html>() }
</ul>
</div>
}
}
}
@@ -2,7 +2,8 @@ use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
use shared::model::XtreamTargetOutputDto;
use crate::app::components::chip::{convert_bool_to_chip_style, Tag};
use crate::app::components::chip::{convert_bool_to_chip_style};
use crate::app::components::Tag;
use crate::app::components::tag_list::{TagList};
#[derive(Properties, PartialEq, Clone)]
@@ -39,13 +40,13 @@ pub fn XtreamOutput(props: &XtreamOutputProps) -> Html {
};
html! {
<div class="tp__xtream_output tp__target_output__output">
<div class="tp__target_output__output__section">
<span class="tp__target_output__output__label">{translator.t("LABEL.SKIP_DIRECT_SOURCE")}</span>
<div class="tp__xtream-output tp__target-output__output">
<div class="tp__target-output__output__section">
<span class="tp__target-output__output__label">{translator.t("LABEL.SKIP_DIRECT_SOURCE")}</span>
<TagList tags={(*tags_skip_direct_source).clone()} />
</div>
<div class="tp__target_output__output__section">
<span class="tp__target_output__output__label">{translator.t("LABEL.RESOLVE")}</span>
<div class="tp__target-output__output__section">
<span class="tp__target-output__output__label">{translator.t("LABEL.RESOLVE")}</span>
<TagList tags={(*tags_resolve).clone()} />
</div>
</div>
@@ -54,7 +55,7 @@ pub fn XtreamOutput(props: &XtreamOutputProps) -> Html {
/*
<div class="tp__tp__target_output_row"><label>{translate.t("LABEL.RESOVE")}</label></div>
<div class="tp__tp__target-output_row"><label>{translate.t("LABEL.RESOVE")}</label></div>
#[serde(default = "default_as_true")]
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1,24 +1,28 @@
use std::fmt::Display;
use crate::app::components::popup_menu::PopupMenu;
use crate::app::components::reveal_content::RevealContent;
use crate::app::components::{AppIcon, Table, TableDefinition, TargetOutput, ToggleSwitch};
use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, PlaylistMappings, PlaylistProcessing, Table, TableDefinition, TargetOptions, TargetOutput, TargetWatch};
use crate::hooks::use_service_context;
use shared::model::ConfigTargetDto;
use shared::model::{ConfigTargetDto};
use std::future;
use std::rc::Rc;
use log::info;
use std::str::FromStr;
use yew::platform::spawn_local;
use yew::prelude::*;
use yew::suspense::use_future;
use yew_i18n::use_translation;
use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind};
use crate::app::components::menu_item::MenuItem;
use crate::services::{DialogResult, DialogService};
const HEADERS: [&str; 11] = [
"TABLE.EMPTY",
"TABLE.ENABLED",
"TABLE.NAME",
"TABLE.OUTPUT",
"TABLE.OPTIONS",
"TABLE.SORT",
"TABLE.FILTER",
"TABLE.OUTPUT",
"TABLE.RENAME",
"TABLE.MAPPING",
"TABLE.PROCESSING_ORDER",
@@ -29,6 +33,7 @@ const HEADERS: [&str; 11] = [
pub fn TargetTable() -> Html {
let translate = use_translation();
let services = use_service_context();
let dialog = use_context::<DialogService>().expect("Dialog service not found");
let popup_anchor_ref = use_state(|| None::<web_sys::Element>);
let popup_is_open = use_state(|| false);
let selected_dto = use_state(|| None::<Rc<ConfigTargetDto>>);
@@ -70,6 +75,7 @@ pub fn TargetTable() -> Html {
};
let render_data_cell = {
let translator = translate.clone();
let popup_onclick = handle_popup_onclick.clone();
Callback::<(usize, usize, Rc<ConfigTargetDto>), Html>::from(
move |(row, col, dto): (usize, usize, Rc<ConfigTargetDto>)| {
@@ -84,16 +90,18 @@ pub fn TargetTable() -> Html {
</button>
}
}
1 => html! { <ToggleSwitch readonly={true} value={&dto.enabled} /> },
1 => html! { <Chip class={ convert_bool_to_chip_style(dto.enabled) }
label={if dto.enabled {translator.t("LABEL.ACTIVE")} else { translator.t("LABEL.DISABLED")} }
/> },
2 => html! { &dto.name.to_string() },
3 => html! { <RevealContent>{"Hello"}</RevealContent> },
4 => html! { <RevealContent>{dto.sort.as_ref().map_or_else(String::new, |s| format!("{s:?}"))}</RevealContent> },
5 => html! { &dto.filter.clone() },
6 => html! { <TargetOutput target={Rc::clone(&dto)} /> },
3 => html! { <TargetOutput target={Rc::clone(&dto)} /> },
4 => html! { <TargetOptions target={Rc::clone(&dto)} /> },
5 => dto.sort.as_ref().map_or_else(|| html! {}, |s| html! { <RevealContent>{format!("{s:?}")}</RevealContent> }),
6 => html! { &dto.filter.clone() },
7 => html! { <RevealContent>{"Hello"}</RevealContent> },
8 => html! { dto.mapping.as_ref().map_or_else(String::new, |m| format!("{m:?}")) },
9 => html! { &dto.processing_order.to_string() },
10 => html! { dto.watch.as_ref().map_or_else(String::new, |w| format!("{w:?}"))},
8 => html! { <PlaylistMappings mappings={dto.mapping.clone()} /> },
9 => html! { <PlaylistProcessing order={dto.processing_order} /> },
10 => html! { <TargetWatch target={Rc::clone(&dto)} /> },
_ => html! {""},
}
})
@@ -140,8 +148,24 @@ pub fn TargetTable() -> Html {
let handle_menu_edit = {
let popup_is_open_state = popup_is_open.clone();
let confirm = dialog.clone();
let translate = translate.clone();
Callback::from(move |name:String| {
info!("Menu selected {name}");
if let Ok(action) = TableAction::from_str(&name) {
match action {
TableAction::Edit => {}
TableAction::Delete => {
let confirm = confirm.clone();
let translator = translate.clone();
spawn_local(async move {
let result = confirm.confirm(&translator.t("MESSAGES.CONFIRM_DELETE")).await;
if result == DialogResult::Ok {
// TODO edit
}
});
}
}
}
popup_is_open_state.set(false);
})
};
@@ -154,8 +178,8 @@ pub fn TargetTable() -> Html {
<>
<Table::<ConfigTargetDto> definition={(*table_definition).as_ref().unwrap().clone()} />
<PopupMenu is_open={*popup_is_open} anchor_ref={(*popup_anchor_ref).clone()} on_close={handle_popup_close}>
<MenuItem icon="Edit" name="edit" label={translate.t("LABEL.EDIT")} onclick={&handle_menu_edit}></MenuItem>
<MenuItem style="tp__delete_action" icon="Delete" name="delete" label={translate.t("LABEL.DELETE")} onclick={&handle_menu_edit}></MenuItem>
<MenuItem icon="Edit" name={TableAction::Edit.to_string()} label={translate.t("LABEL.EDIT")} onclick={&handle_menu_edit}></MenuItem>
<MenuItem icon="Delete" name={TableAction::Delete.to_string()} label={translate.t("LABEL.DELETE")} onclick={&handle_menu_edit} style="tp__delete_action"></MenuItem>
</PopupMenu>
</>
}
@@ -167,3 +191,34 @@ pub fn TargetTable() -> Html {
</div>
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
enum TableAction {
Edit,
Delete,
}
impl Display for TableAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Self::Edit => "edit",
Self::Delete => "delete",
})
}
}
impl FromStr for TableAction {
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, TuliproxError> {
if s.eq("edit") {
Ok(Self::Edit)
} else if s.eq("delete") {
Ok(Self::Delete)
} else {
create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown InputType: {}", s)
}
}
}
+13 -4
View File
@@ -1,7 +1,13 @@
use web_sys::HtmlInputElement;
use yew::prelude::*;
use std::rc::Rc;
use crate::app::components::chip::{Chip, Tag};
use crate::app::components::chip::{Chip};
#[derive(Clone, PartialEq, Debug)]
pub struct Tag {
pub label: String,
pub class: Option<String>,
}
#[derive(Properties, Clone, PartialEq)]
pub struct TagListProps {
@@ -29,9 +35,9 @@ pub fn tag_list(props: &TagListProps) -> Html {
let on_remove = {
let tag_state = tag_state.clone();
let on_change = on_change.clone();
Callback::from(move |tag: Rc<Tag>| {
Callback::from(move |tag_label: String| {
let mut updated = (*tag_state).clone();
updated.retain(|t| t != &tag);
updated.retain(|t| t.label != tag_label);
on_change.emit(updated.clone());
tag_state.set(updated);
})
@@ -66,7 +72,10 @@ pub fn tag_list(props: &TagListProps) -> Html {
html! {
<div class="tp__tag_list">
{ for (*tag_state).iter().map(|tag| html! { <Chip tag={tag.clone()} /> }) }
{ for (*tag_state).iter().map(|tag| html! {
<Chip label={tag.label.clone()} class={tag.class.clone()} removable={props.removable} on_remove={on_remove.clone()}/>
})
}
{
if allow_add {
html! {
+5 -2
View File
@@ -14,8 +14,9 @@ use crate::app::components::{Authentication, Home, Login, Preferences};
use crate::error::Error;
use crate::hooks::IconDefinition;
use crate::model::WebConfig;
use crate::provider::dialog_provider::DialogProvider;
use crate::services::request_get;
pub use crate::app::components::ConfirmDialog;
fn flatten_json(value: &Value, prefix: String, map: &mut HashMap<String, serde_json::Value>) {
match value {
Value::Object(obj) => {
@@ -132,7 +133,9 @@ pub fn App() -> Html {
<IconContextProvider icons={icons.clone()}>
<I18nProvider supported_languages={supported_languages} translations={transl.clone()}>
<Authentication>
<Switch<AppRoute> render={switch} />
<DialogProvider>
<Switch<AppRoute> render={switch} />
</DialogProvider>
</Authentication>
</I18nProvider>
</IconContextProvider>
+59
View File
@@ -0,0 +1,59 @@
use yew::{function_component, html, use_effect_with, use_state, Callback, Children, ContextProvider, Html, Properties};
use crate::app::ConfirmDialog;
use crate::services::{ConfirmRequest, DialogResult, DialogService};
#[derive(Properties, PartialEq)]
pub struct ConfirmProviderProps {
pub children: Children,
}
#[function_component]
pub fn DialogProvider(props: &ConfirmProviderProps) -> Html {
let service = use_state(DialogService::new);
let confirm_request = use_state(|| None::<ConfirmRequest>);
{
let service = service.clone();
let request = confirm_request.clone();
use_effect_with((),
move |_| {
service.register_confirm(Callback::from(move |req: ConfirmRequest| {
request.set(Some(req));
}));
|| ()
},
);
}
let on_confirm = {
let request = confirm_request.clone();
Callback::from(move |result: bool| {
if let Some(req) = &*request {
if let Some(cb) = req.resolve.borrow_mut().take() {
cb( if result {DialogResult::Ok} else { DialogResult::Cancel});
}
}
request.set(None);
})
};
html! {
<ContextProvider<DialogService> context={(*service).clone()}>
{ for props.children.iter() }
{
if let Some(req) = &*confirm_request {
html! {
<ConfirmDialog
title={req.title.clone()}
ok_caption={req.ok_caption.clone()}
cancel_caption={req.cancel_caption.clone()}
on_confirm={on_confirm.clone()}
/>
}
} else {
html! {}
}
}
</ContextProvider<DialogService>>
}
}
+2 -1
View File
@@ -1,2 +1,3 @@
pub mod icon_context_provider;
pub mod service_context_provider;
pub mod service_context_provider;
pub mod dialog_provider;
+3 -1
View File
@@ -49,7 +49,9 @@ impl ConfigService {
return;
}
let result = match request_get::<AppConfigDto>(CONFIG_PATH).await {
Ok(cfg) => Some(Rc::new(cfg)),
Ok(cfg) => {
Some(Rc::new(cfg))
},
Err(err) => {
error!("{err}");
None
+92
View File
@@ -0,0 +1,92 @@
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use yew::{Callback};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DialogResult {
Ok,
Cancel,
}
#[derive(Clone)]
pub struct DialogFuture {
result: Rc<RefCell<Option<DialogResult>>>,
waker: Rc<RefCell<Option<Waker>>>,
}
impl DialogFuture {
fn new() -> (Self, impl Fn(DialogResult) + 'static) {
let result = Rc::new(RefCell::new(None));
let waker = Rc::new(RefCell::new(None));
let result_clone = result.clone();
let waker_clone = waker.clone();
let resolve = move |value: DialogResult| {
*result_clone.borrow_mut() = Some(value);
if let Some(the_waker) = waker_clone.borrow_mut().take() {
Waker::wake(the_waker);
}
};
(
DialogFuture { result, waker },
resolve,
)
}
}
impl Future for DialogFuture {
type Output = DialogResult;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(value) = self.result.borrow_mut().take() {
Poll::Ready(value)
} else {
*self.waker.borrow_mut() = Some(cx.waker().clone());
Poll::Pending
}
}
}
#[derive(Clone)]
pub struct ConfirmRequest {
pub title: String,
pub ok_caption: String,
pub cancel_caption: String,
pub resolve: Rc<RefCell<Option<Box<dyn Fn(DialogResult)>>>>,
}
#[derive(Default, Clone, PartialEq)]
pub struct DialogService {
inner: Rc<RefCell<Option<Callback<ConfirmRequest>>>>,
}
impl DialogService {
pub fn new() -> Self {
Self::default()
}
pub fn register_confirm(&self, cb: Callback<ConfirmRequest>) {
*self.inner.borrow_mut() = Some(cb);
}
pub fn confirm(&self, title: &str) -> DialogFuture {
let (future, resolver) = DialogFuture::new();
let request = ConfirmRequest {
title: title.to_string(),
ok_caption: "LABEL.OK".into(),
cancel_caption: "LABEL.CANCEL".into(),
resolve: Rc::new(RefCell::new(Some(Box::new(resolver)))),
};
if let Some(cb) = &*self.inner.borrow() {
cb.emit(request);
}
future
}
}
+2
View File
@@ -2,6 +2,7 @@ mod auth_service;
mod config_service;
mod requests;
mod status_service;
mod dialog_service;
pub use requests::{
limit, request_delete, request_get, request_post, request_put
@@ -11,3 +12,4 @@ pub use self::auth_service::*;
pub use self::config_service::*;
pub use self::requests::*;
pub use self::status_service::*;
pub use self::dialog_service::*;