mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-28 09:42:26 +01:00
webui
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
* {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: " - ";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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! {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod icon_context_provider;
|
||||
pub mod service_context_provider;
|
||||
pub mod service_context_provider;
|
||||
pub mod dialog_provider;
|
||||
@@ -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
|
||||
|
||||
@@ -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,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::*;
|
||||
|
||||
Reference in New Issue
Block a user