From 5297916ce7b2095d7b8aee31ddecf007ab41397f Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 7 Jul 2025 17:55:27 +0200 Subject: [PATCH] webui --- backend/src/model/xmltv.rs | 3 +- frontend/src/scss/_theme.scss | 3 +- shared/src/model/config/target.rs | 17 ++++ webui/Cargo.toml | 2 +- webui/public/assets/i18n/en.json | 43 ++++++--- webui/public/assets/icons.json | 8 ++ webui/scss/_default.scss | 27 ++++-- webui/scss/_theme.scss | 30 ++++-- webui/scss/app/_app.scss | 3 +- webui/scss/app/_app_header.scss | 2 + webui/scss/app/_component.scss | 9 +- .../scss/app/components/_collapse_panel.scss | 3 +- webui/scss/app/components/_dialog.scss | 23 +++++ webui/scss/app/components/_icon_button.scss | 4 - webui/scss/app/components/_menu_item.scss | 4 +- webui/scss/app/components/_popup_menu.scss | 14 +-- webui/scss/app/components/_preferences.scss | 10 +- webui/scss/app/components/_table.scss | 16 +++- webui/scss/app/components/_text_button.scss | 14 ++- .../components/dashboard/_dashboard_view.scss | 4 +- .../playlist/_playlist_mappings.scss | 20 ++++ .../playlist/_playlist_processing.scss | 6 ++ .../components/playlist/_playlist_view.scss | 16 +++- .../playlist/target/_strm_output.scss | 2 +- .../playlist/target/_target_options.scss | 49 ++++++++++ .../playlist/target/_target_output.scss | 71 ++++++++++++-- .../playlist/target/_target_table.scss | 4 + .../playlist/target/_target_watch.scss | 13 +++ webui/scss/main.scss | 9 -- webui/src/app/components/chip.rs | 19 ++-- webui/src/app/components/collapse_panel.rs | 11 ++- webui/src/app/components/confirm_dialog.rs | 56 +++++++++++ webui/src/app/components/mod.rs | 8 +- webui/src/app/components/playlist/mappings.rs | 24 +++++ webui/src/app/components/playlist/mod.rs | 6 +- .../src/app/components/playlist/processing.rs | 30 ++++++ .../playlist/target/hdhomerun_output.rs | 19 +++- .../components/playlist/target/m3u_output.rs | 11 ++- .../src/app/components/playlist/target/mod.rs | 4 + .../components/playlist/target/strm_output.rs | 44 ++++++++- .../playlist/target/target_options.rs | 82 +++++++++++++++++ .../playlist/target/target_output.rs | 15 +-- .../playlist/target/target_watch.rs | 25 +++++ .../playlist/target/xtream_output.rs | 15 +-- .../app/components/playlist/target_table.rs | 85 ++++++++++++++--- webui/src/app/components/tag_list.rs | 17 +++- webui/src/app/mod.rs | 7 +- webui/src/provider/dialog_provider.rs | 59 ++++++++++++ webui/src/provider/mod.rs | 3 +- webui/src/services/config_service.rs | 4 +- webui/src/services/dialog_service.rs | 92 +++++++++++++++++++ webui/src/services/mod.rs | 2 + 52 files changed, 918 insertions(+), 149 deletions(-) create mode 100644 webui/scss/app/components/_dialog.scss create mode 100644 webui/scss/app/components/playlist/_playlist_mappings.scss create mode 100644 webui/scss/app/components/playlist/_playlist_processing.scss create mode 100644 webui/scss/app/components/playlist/target/_target_options.scss create mode 100644 webui/scss/app/components/playlist/target/_target_watch.scss create mode 100644 webui/src/app/components/confirm_dialog.rs create mode 100644 webui/src/app/components/playlist/mappings.rs create mode 100644 webui/src/app/components/playlist/processing.rs create mode 100644 webui/src/app/components/playlist/target/target_options.rs create mode 100644 webui/src/app/components/playlist/target/target_watch.rs create mode 100644 webui/src/provider/dialog_provider.rs create mode 100644 webui/src/services/dialog_service.rs diff --git a/backend/src/model/xmltv.rs b/backend/src/model/xmltv.rs index d21f6049f..8dac73391 100644 --- a/backend/src/model/xmltv.rs +++ b/backend/src/model/xmltv.rs @@ -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, diff --git a/frontend/src/scss/_theme.scss b/frontend/src/scss/_theme.scss index fcc544b10..f209ba4b5 100644 --- a/frontend/src/scss/_theme.scss +++ b/frontend/src/scss/_theme.scss @@ -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; diff --git a/shared/src/model/config/target.rs b/shared/src/model/config/target.rs index acf8d4f9a..04e12d589 100644 --- a/shared/src/model/config/target.rs +++ b/shared/src/model/config/target.rs @@ -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 { diff --git a/webui/Cargo.toml b/webui/Cargo.toml index da08fc805..6c95b6844 100644 --- a/webui/Cargo.toml +++ b/webui/Cargo.toml @@ -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" diff --git a/webui/public/assets/i18n/en.json b/webui/public/assets/i18n/en.json index f0b9a1ed0..5d367a858 100644 --- a/webui/public/assets/i18n/en.json +++ b/webui/public/assets/i18n/en.json @@ -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", diff --git a/webui/public/assets/icons.json b/webui/public/assets/icons.json index 1aa0210db..60b33ebd7 100644 --- a/webui/public/assets/icons.json +++ b/webui/public/assets/icons.json @@ -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" } ] \ No newline at end of file diff --git a/webui/scss/_default.scss b/webui/scss/_default.scss index 4f015564f..414980fc7 100644 --- a/webui/scss/_default.scss +++ b/webui/scss/_default.scss @@ -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; } \ No newline at end of file diff --git a/webui/scss/_theme.scss b/webui/scss/_theme.scss index caee6a3c2..8c03be2ec 100644 --- a/webui/scss/_theme.scss +++ b/webui/scss/_theme.scss @@ -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; diff --git a/webui/scss/app/_app.scss b/webui/scss/app/_app.scss index 3ed524109..46f6b9a16 100644 --- a/webui/scss/app/_app.scss +++ b/webui/scss/app/_app.scss @@ -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; } } } \ No newline at end of file diff --git a/webui/scss/app/_app_header.scss b/webui/scss/app/_app_header.scss index 1db63209d..66c3e8cce 100644 --- a/webui/scss/app/_app_header.scss +++ b/webui/scss/app/_app_header.scss @@ -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); diff --git a/webui/scss/app/_component.scss b/webui/scss/app/_component.scss index c5ece47bd..03b864d85 100644 --- a/webui/scss/app/_component.scss +++ b/webui/scss/app/_component.scss @@ -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"; diff --git a/webui/scss/app/components/_collapse_panel.scss b/webui/scss/app/components/_collapse_panel.scss index 3723746c9..220712047 100644 --- a/webui/scss/app/components/_collapse_panel.scss +++ b/webui/scss/app/components/_collapse_panel.scss @@ -17,8 +17,7 @@ span { flex: 1 1 auto; } - span, - svg { + span { pointer-events: none; } svg { diff --git a/webui/scss/app/components/_dialog.scss b/webui/scss/app/components/_dialog.scss new file mode 100644 index 000000000..e7818431b --- /dev/null +++ b/webui/scss/app/components/_dialog.scss @@ -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; + } +} \ No newline at end of file diff --git a/webui/scss/app/components/_icon_button.scss b/webui/scss/app/components/_icon_button.scss index 1d04aa36e..08682f9a4 100644 --- a/webui/scss/app/components/_icon_button.scss +++ b/webui/scss/app/components/_icon_button.scss @@ -11,10 +11,6 @@ height: min-content; width: min-content; - svg { - pointer-events: none; - } - &:focus, &:hover { fill: var(--icon-button-hover-color); diff --git a/webui/scss/app/components/_menu_item.scss b/webui/scss/app/components/_menu_item.scss index 850091352..0afca8783 100644 --- a/webui/scss/app/components/_menu_item.scss +++ b/webui/scss/app/components/_menu_item.scss @@ -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) } } \ No newline at end of file diff --git a/webui/scss/app/components/_popup_menu.scss b/webui/scss/app/components/_popup_menu.scss index 9603e6482..0c0998691 100644 --- a/webui/scss/app/components/_popup_menu.scss +++ b/webui/scss/app/components/_popup_menu.scss @@ -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; - } } diff --git a/webui/scss/app/components/_preferences.scss b/webui/scss/app/components/_preferences.scss index d930c3af6..cb593c2eb 100644 --- a/webui/scss/app/components/_preferences.scss +++ b/webui/scss/app/components/_preferences.scss @@ -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); } * { diff --git a/webui/scss/app/components/_table.scss b/webui/scss/app/components/_table.scss index bd28ab45a..1d15970cd 100644 --- a/webui/scss/app/components/_table.scss +++ b/webui/scss/app/components/_table.scss @@ -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; } } diff --git a/webui/scss/app/components/_text_button.scss b/webui/scss/app/components/_text_button.scss index a33215a30..fa2fb026d 100644 --- a/webui/scss/app/components/_text_button.scss +++ b/webui/scss/app/components/_text_button.scss @@ -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; + } } \ No newline at end of file diff --git a/webui/scss/app/components/dashboard/_dashboard_view.scss b/webui/scss/app/components/dashboard/_dashboard_view.scss index ccd3713bd..b592fbc00 100644 --- a/webui/scss/app/components/dashboard/_dashboard_view.scss +++ b/webui/scss/app/components/dashboard/_dashboard_view.scss @@ -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); diff --git a/webui/scss/app/components/playlist/_playlist_mappings.scss b/webui/scss/app/components/playlist/_playlist_mappings.scss new file mode 100644 index 000000000..c0b8e1691 --- /dev/null +++ b/webui/scss/app/components/playlist/_playlist_mappings.scss @@ -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) + } + } + +} \ No newline at end of file diff --git a/webui/scss/app/components/playlist/_playlist_processing.scss b/webui/scss/app/components/playlist/_playlist_processing.scss new file mode 100644 index 000000000..97113abda --- /dev/null +++ b/webui/scss/app/components/playlist/_playlist_processing.scss @@ -0,0 +1,6 @@ +.tp__playlist-processing { + display: flex; + flex-flow: row; + justify-content: center; + align-items: center; +} diff --git a/webui/scss/app/components/playlist/_playlist_view.scss b/webui/scss/app/components/playlist/_playlist_view.scss index 65252a27f..d8b1ad212 100644 --- a/webui/scss/app/components/playlist/_playlist_view.scss +++ b/webui/scss/app/components/playlist/_playlist_view.scss @@ -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; + } } \ No newline at end of file diff --git a/webui/scss/app/components/playlist/target/_strm_output.scss b/webui/scss/app/components/playlist/target/_strm_output.scss index c29130b00..1ea359a3a 100644 --- a/webui/scss/app/components/playlist/target/_strm_output.scss +++ b/webui/scss/app/components/playlist/target/_strm_output.scss @@ -1,3 +1,3 @@ -.tp__strm_output { +.tp__strm-output { } \ No newline at end of file diff --git a/webui/scss/app/components/playlist/target/_target_options.scss b/webui/scss/app/components/playlist/target/_target_options.scss new file mode 100644 index 000000000..04ea54b5c --- /dev/null +++ b/webui/scss/app/components/playlist/target/_target_options.scss @@ -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: ":"; + } + } +} diff --git a/webui/scss/app/components/playlist/target/_target_output.scss b/webui/scss/app/components/playlist/target/_target_output.scss index 7c16fa3fa..d3989f45f 100644 --- a/webui/scss/app/components/playlist/target/_target_output.scss +++ b/webui/scss/app/components/playlist/target/_target_output.scss @@ -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 { + + } } \ No newline at end of file diff --git a/webui/scss/app/components/playlist/target/_target_table.scss b/webui/scss/app/components/playlist/target/_target_table.scss index c3dca7a55..8d22e2af8 100644 --- a/webui/scss/app/components/playlist/target/_target_table.scss +++ b/webui/scss/app/components/playlist/target/_target_table.scss @@ -1,4 +1,8 @@ .tp__target-table { + display: flex; + flex-flow: column; + box-sizing: border-box; + overflow: hidden; .tp__icon-button { width: 2rem; } diff --git a/webui/scss/app/components/playlist/target/_target_watch.scss b/webui/scss/app/components/playlist/target/_target_watch.scss new file mode 100644 index 000000000..6c4268a12 --- /dev/null +++ b/webui/scss/app/components/playlist/target/_target_watch.scss @@ -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: " - "; + } + } +} \ No newline at end of file diff --git a/webui/scss/main.scss b/webui/scss/main.scss index f6b4b6dfd..27cd39f4f 100644 --- a/webui/scss/main.scss +++ b/webui/scss/main.scss @@ -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 */ - } -} diff --git a/webui/src/app/components/chip.rs b/webui/src/app/components/chip.rs index 729fd61af..2330241db 100644 --- a/webui/src/app/components/chip.rs +++ b/webui/src/app/components/chip.rs @@ -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 { Option::from((if value { "active" } else { "inactive" }).to_string()) } -#[derive(Clone, PartialEq, Debug)] -pub struct Tag { - pub label: String, - pub class: Option, -} - #[derive(Properties, Clone, PartialEq)] pub struct ChipProps { - pub(crate) tag: Rc, + pub label: String, + pub class: Option, #[prop_or(false)] - removable: bool, + pub removable: bool, #[prop_or_else(|| Callback::noop())] - on_remove: Callback, + pub on_remove: Callback, } - #[function_component] pub fn Chip(props: &ChipProps) -> Html { @@ -45,8 +38,8 @@ pub fn Chip(props: &ChipProps) -> Html { }; html! { - - { &props.tag.label } + + { &props.label } if props.removable { {"×"} } diff --git a/webui/src/app/components/collapse_panel.rs b/webui/src/app/components/collapse_panel.rs index c8071d7f7..c9bac8bf2 100644 --- a/webui/src/app/components/collapse_panel.rs +++ b/webui/src/app/components/collapse_panel.rs @@ -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, pub children: Children, + #[prop_or_default] + pub class: Option, } #[function_component] @@ -19,9 +24,11 @@ pub fn CollapsePanel(props: &CollapsePanelProps) -> Html { }; html! { -
+
- { props.title.clone() } + + { props.title_content.clone().unwrap_or_else(|| html! { &props.title }) } +
diff --git a/webui/src/app/components/confirm_dialog.rs b/webui/src/app/components/confirm_dialog.rs new file mode 100644 index 000000000..1f9f5faf3 --- /dev/null +++ b/webui/src/app/components/confirm_dialog.rs @@ -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, +} + +#[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::() { + 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::() { + 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! { + +

{ &props.title }

+
+ + +
+
+ } +} diff --git a/webui/src/app/components/mod.rs b/webui/src/app/components/mod.rs index f2b6c8f2c..62d17b2f4 100644 --- a/webui/src/app/components/mod.rs +++ b/webui/src/app/components/mod.rs @@ -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::*; diff --git a/webui/src/app/components/playlist/mappings.rs b/webui/src/app/components/playlist/mappings.rs new file mode 100644 index 000000000..fa547cb98 --- /dev/null +++ b/webui/src/app/components/playlist/mappings.rs @@ -0,0 +1,24 @@ +use yew::prelude::*; +use crate::app::components::AppIcon; + +#[derive(Properties, PartialEq, Clone)] +pub struct PlaylistMappingsProps { + pub mappings: Option>, +} + +#[function_component] +pub fn PlaylistMappings(props: &PlaylistMappingsProps) -> Html { + + html! { +
+
    + { + match props.mappings.as_ref() { + Some(vec) => vec.iter().map(|item| html! {
  • { item }
  • }).collect::(), + None => html! {}, + } + } +
+
+ } +} \ No newline at end of file diff --git a/webui/src/app/components/playlist/mod.rs b/webui/src/app/components/playlist/mod.rs index 8f8d118d3..836cab5a4 100644 --- a/webui/src/app/components/playlist/mod.rs +++ b/webui/src/app/components/playlist/mod.rs @@ -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::*; \ No newline at end of file +pub use self::target::*; +pub use self::processing::*; +pub use self::mappings::*; \ No newline at end of file diff --git a/webui/src/app/components/playlist/processing.rs b/webui/src/app/components/playlist/processing.rs new file mode 100644 index 000000000..067971657 --- /dev/null +++ b/webui/src/app/components/playlist/processing.rs @@ -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::>>() + }); + + html! { +
+ +
+ } +} \ No newline at end of file diff --git a/webui/src/app/components/playlist/target/hdhomerun_output.rs b/webui/src/app/components/playlist/target/hdhomerun_output.rs index 0171ecfee..d0aa64206 100644 --- a/webui/src/app/components/playlist/target/hdhomerun_output.rs +++ b/webui/src/app/components/playlist/target/hdhomerun_output.rs @@ -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! { -
+
+
+ {translator.t("LABEL.DEVICE")} + { props.output.device.clone() } +
+
+ {translator.t("LABEL.USERNAME")} + { props.output.username.clone() } +
+
+ {translator.t("LABEL.USE_OUTPUT")} + { props.output.use_output.map_or_else(String::new, |o| o.to_string()) } +
} -} \ No newline at end of file +} diff --git a/webui/src/app/components/playlist/target/m3u_output.rs b/webui/src/app/components/playlist/target/m3u_output.rs index cca577cac..b30db54db 100644 --- a/webui/src/app/components/playlist/target/m3u_output.rs +++ b/webui/src/app/components/playlist/target/m3u_output.rs @@ -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! { -
-
- {translator.t("LABEL.FILENAME")} +
+
+ {translator.t("LABEL.FILENAME")} { props.output.filename.as_ref().map(|f| html! {{ f }}) }
-
+
diff --git a/webui/src/app/components/playlist/target/mod.rs b/webui/src/app/components/playlist/target/mod.rs index 041256928..1fab7202d 100644 --- a/webui/src/app/components/playlist/target/mod.rs +++ b/webui/src/app/components/playlist/target/mod.rs @@ -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::*; diff --git a/webui/src/app/components/playlist/target/strm_output.rs b/webui/src/app/components/playlist/target/strm_output.rs index 87914e70b..59d7bf036 100644 --- a/webui/src/app/components/playlist/target/strm_output.rs +++ b/webui/src/app/components/playlist/target/strm_output.rs @@ -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! { -
+
+
+ {translator.t("LABEL.DIRECTORY")} + { props.output.directory.clone() } +
+
+ {translator.t("LABEL.USERNAME")} + { props.output.username.as_ref().map(|f| html! {{ f }}) } +
+
+ +
+ { + match props.output.strm_props.as_ref() { + None => html! {}, + Some(props) => html! { +
+ {translator.t("LABEL.PROPERTIES")} +
    + { props.iter().map(|p| html! {
  • {p}
  • }).collect::() } +
+
+ } + } + }
} -} \ No newline at end of file +} diff --git a/webui/src/app/components/playlist/target/target_options.rs b/webui/src/app/components/playlist/target/target_options.rs new file mode 100644 index 000000000..5bd27a2a4 --- /dev/null +++ b/webui/src/app/components/playlist/target/target_options.rs @@ -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, +} + +#[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::>>(), + redirect.iter().map(|(o, t)| Rc::new(Tag { class: convert_bool_to_chip_style(*o), label: translate.t(t) })).collect::>>(), + ) + }); + + let has_options = tags.0; + let opts: Vec> = (&tags.1).clone(); + let redirect: Vec> = (&tags.2).clone(); + + html! { + +
+
+ +
+
+ {translate.t("LABEL.FORCE_REDIRECT")} + +
+
+
+ } +} \ No newline at end of file diff --git a/webui/src/app/components/playlist/target/target_output.rs b/webui/src/app/components/playlist/target/target_output.rs index 8728a7e95..ce194b95b 100644 --- a/webui/src/app/components/playlist/target/target_output.rs +++ b/webui/src/app/components/playlist/target/target_output.rs @@ -12,28 +12,31 @@ pub struct TargetOutputProps { #[function_component] pub fn TargetOutput(props: &TargetOutputProps) -> Html { let translate = use_translation(); + html! { -
+
{ - props.target.output.iter().map(|output| { + props.target.output.iter().map(|output: &TargetOutputDto| { match output { TargetOutputDto::Xtream(xc) => html! { - + }, TargetOutputDto::M3u(m3u) => html! { - + }, TargetOutputDto::Strm(strm) => html! { - + }, TargetOutputDto::HdHomeRun(hdhr) => html! { - + }, diff --git a/webui/src/app/components/playlist/target/target_watch.rs b/webui/src/app/components/playlist/target/target_watch.rs new file mode 100644 index 000000000..6941e9453 --- /dev/null +++ b/webui/src/app/components/playlist/target/target_watch.rs @@ -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, +} + +#[function_component] +pub fn TargetWatch(props: &TargetWatchProps) -> Html { + let translate = use_translation(); + match props.target.watch.as_ref() { + None => html! {}, + Some(watch) => html! { +
+
    + { watch.iter().map(|item| html! {
  • { item }
  • }).collect::() } +
+
+ } + } +} \ No newline at end of file diff --git a/webui/src/app/components/playlist/target/xtream_output.rs b/webui/src/app/components/playlist/target/xtream_output.rs index b08a21268..cfe9d945f 100644 --- a/webui/src/app/components/playlist/target/xtream_output.rs +++ b/webui/src/app/components/playlist/target/xtream_output.rs @@ -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! { -
-
- {translator.t("LABEL.SKIP_DIRECT_SOURCE")} +
+
+ {translator.t("LABEL.SKIP_DIRECT_SOURCE")}
-
- {translator.t("LABEL.RESOLVE")} +
+ {translator.t("LABEL.RESOLVE")}
@@ -54,7 +55,7 @@ pub fn XtreamOutput(props: &XtreamOutputProps) -> Html { /* -
+
#[serde(default = "default_as_true")] #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/webui/src/app/components/playlist/target_table.rs b/webui/src/app/components/playlist/target_table.rs index f67e62f9b..c87913280 100644 --- a/webui/src/app/components/playlist/target_table.rs +++ b/webui/src/app/components/playlist/target_table.rs @@ -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::().expect("Dialog service not found"); let popup_anchor_ref = use_state(|| None::); let popup_is_open = use_state(|| false); let selected_dto = use_state(|| None::>); @@ -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), Html>::from( move |(row, col, dto): (usize, usize, Rc)| { @@ -84,16 +90,18 @@ pub fn TargetTable() -> Html { } } - 1 => html! { }, + 1 => html! { }, 2 => html! { &dto.name.to_string() }, - 3 => html! { {"Hello"} }, - 4 => html! { {dto.sort.as_ref().map_or_else(String::new, |s| format!("{s:?}"))} }, - 5 => html! { &dto.filter.clone() }, - 6 => html! { }, + 3 => html! { }, + 4 => html! { }, + 5 => dto.sort.as_ref().map_or_else(|| html! {}, |s| html! { {format!("{s:?}")} }), + 6 => html! { &dto.filter.clone() }, 7 => html! { {"Hello"} }, - 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! { }, + 9 => html! { }, + 10 => html! { }, _ => 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 { <> definition={(*table_definition).as_ref().unwrap().clone()} /> - - + + } @@ -167,3 +191,34 @@ pub fn TargetTable() -> Html {
} } + + + +#[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 { + 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) + } + } +} \ No newline at end of file diff --git a/webui/src/app/components/tag_list.rs b/webui/src/app/components/tag_list.rs index 0b5a29bce..7f215d47f 100644 --- a/webui/src/app/components/tag_list.rs +++ b/webui/src/app/components/tag_list.rs @@ -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, +} #[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| { + 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! {
- { for (*tag_state).iter().map(|tag| html! { }) } + { for (*tag_state).iter().map(|tag| html! { + + }) + } { if allow_add { html! { diff --git a/webui/src/app/mod.rs b/webui/src/app/mod.rs index 42ade4021..2b44e9106 100644 --- a/webui/src/app/mod.rs +++ b/webui/src/app/mod.rs @@ -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) { match value { Value::Object(obj) => { @@ -132,7 +133,9 @@ pub fn App() -> Html { - render={switch} /> + + render={switch} /> + diff --git a/webui/src/provider/dialog_provider.rs b/webui/src/provider/dialog_provider.rs new file mode 100644 index 000000000..fc644bae7 --- /dev/null +++ b/webui/src/provider/dialog_provider.rs @@ -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::); + + { + 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! { + context={(*service).clone()}> + { for props.children.iter() } + { + if let Some(req) = &*confirm_request { + html! { + + } + } else { + html! {} + } + } + > + } +} diff --git a/webui/src/provider/mod.rs b/webui/src/provider/mod.rs index 1b0891af8..99c4a2a73 100644 --- a/webui/src/provider/mod.rs +++ b/webui/src/provider/mod.rs @@ -1,2 +1,3 @@ pub mod icon_context_provider; -pub mod service_context_provider; \ No newline at end of file +pub mod service_context_provider; +pub mod dialog_provider; \ No newline at end of file diff --git a/webui/src/services/config_service.rs b/webui/src/services/config_service.rs index 9fdad1327..1fc8cae27 100644 --- a/webui/src/services/config_service.rs +++ b/webui/src/services/config_service.rs @@ -49,7 +49,9 @@ impl ConfigService { return; } let result = match request_get::(CONFIG_PATH).await { - Ok(cfg) => Some(Rc::new(cfg)), + Ok(cfg) => { + Some(Rc::new(cfg)) + }, Err(err) => { error!("{err}"); None diff --git a/webui/src/services/dialog_service.rs b/webui/src/services/dialog_service.rs new file mode 100644 index 000000000..19ae2808c --- /dev/null +++ b/webui/src/services/dialog_service.rs @@ -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>>, + waker: Rc>>, +} + +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 { + 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>>>, +} + +#[derive(Default, Clone, PartialEq)] +pub struct DialogService { + inner: Rc>>>, +} + +impl DialogService { + pub fn new() -> Self { + Self::default() + } + + pub fn register_confirm(&self, cb: Callback) { + *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 + } +} diff --git a/webui/src/services/mod.rs b/webui/src/services/mod.rs index 014608d49..adc0b36c9 100644 --- a/webui/src/services/mod.rs +++ b/webui/src/services/mod.rs @@ -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::*;