From f432e215ff6dbc285bb2cd3808b83d435f8012be Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 3 Nov 2025 21:19:35 +0100 Subject: [PATCH] added target form --- README.md | 20 +- frontend/public/assets/i18n/en.json | 6 +- frontend/public/assets/icons.json | 5 + frontend/scss/_theme.scss | 5 +- frontend/scss/app/_component.scss | 3 + .../scss/app/components/_cluster_flags.scss | 23 ++ frontend/scss/app/components/_tabset.scss | 52 ++- .../app/components/config/_config_view.scss | 4 + .../components/source_editor/_input_form.scss | 20 ++ .../source_editor/_source_editor.scss | 218 ++----------- .../source_editor/_source_editor_block.scss | 295 ++++++++++++++++++ .../source_editor/_source_editor_form.scss | 25 +- .../_proxy-user-credentials-form.scss | 2 +- .../src/app/components/cluster_flags_input.rs | 75 +++++ .../app/components/config/api_config_view.rs | 1 - .../config/hdhomerun_config_view.rs | 1 - .../config/hdhomerun_device_view.rs | 1 - .../components/config/ipcheck_config_view.rs | 1 - .../app/components/config/log_config_view.rs | 1 - frontend/src/app/components/config/macros.rs | 37 +-- .../app/components/config/main_config_view.rs | 1 - .../config/messaging_config_view.rs | 1 - .../components/config/proxy_config_view.rs | 1 - .../config/reverse_proxy_config_view.rs | 1 - .../config/schedules_config_view.rs | 23 +- .../components/config/video_config_view.rs | 3 +- .../components/config/webui_config_view.rs | 1 - .../app/components/drop_down_icon_button.rs | 23 +- frontend/src/app/components/mod.rs | 2 + frontend/src/app/components/search.rs | 18 +- frontend/src/app/components/select.rs | 23 +- .../src/app/components/source_editor/block.rs | 122 ++++++++ .../components/source_editor/editor_form.rs | 32 +- .../components/source_editor/editor_model.rs | 52 ++- .../components/source_editor/editor_view.rs | 237 +++++++------- .../components/source_editor/input_form.rs | 257 +++++++++------ .../src/app/components/source_editor/mod.rs | 3 + .../components/source_editor/output_form.rs | 17 + .../src/app/components/source_editor/rules.rs | 6 +- .../app/components/source_editor/sidebar.rs | 3 +- .../components/source_editor/target_form.rs | 273 ++++++++++++++++ frontend/src/app/components/userlist/list.rs | 9 +- .../userlist/proxy_user_credentials_form.rs | 91 +++--- shared/src/model/config/target.rs | 23 +- shared/src/model/processing_order.rs | 29 ++ 45 files changed, 1490 insertions(+), 556 deletions(-) create mode 100644 frontend/scss/app/components/_cluster_flags.scss create mode 100644 frontend/scss/app/components/source_editor/_input_form.scss create mode 100644 frontend/scss/app/components/source_editor/_source_editor_block.scss create mode 100644 frontend/src/app/components/cluster_flags_input.rs create mode 100644 frontend/src/app/components/source_editor/block.rs create mode 100644 frontend/src/app/components/source_editor/output_form.rs create mode 100644 frontend/src/app/components/source_editor/target_form.rs diff --git a/README.md b/README.md index 3eb90988b..b115deb72 100644 --- a/README.md +++ b/README.md @@ -655,13 +655,19 @@ Each input has the following attributes: + `xtream_live_stream_without_extension` default false, if set to true `.ts` extension is not added to the stream link. + `xtream_live_stream_use_prefix` default true, if set to true `/live/` prefix is added to the stream link. - `aliases` for alias definitions for the same provider with different credentials -- `staged` for side loading processed playlists. -Instead of fully configuring everything yourself, you can “stage” a source — meaning you provide a ready-made playlist -from somewhere else. The system will only use that staged playlist when updating. -For actual streaming and fetching details, it will still use the main provider’s settings. -In plain words: If you don’t want to deal with Tuliprox mapping, or you already have a playlist in another online tool, -you can plug that playlist in as a staged input. It won’t replace the main provider — it’s just there to update the list. -All streaming and proxying and info still come from the original provider configuration. +- `staged` for side loading processed playlists. + If you already have a provider configured but want to load the playlist from a different source — for example, +from another playlist editor — you can specify a staged DTO. + +Instead of fully configuring everything yourself, you can “stage” a source, meaning you provide a +ready-made playlist from somewhere else. During the update process, the playlist will be read from this staged source. + +However, regular playlist queries (such as streaming or fetching details) will still go through the +main provider. The staged source is only used temporarily — just for updating the playlist. + +In plain words: if you already have a playlist in another online tool or don’t want to deal with Tuliprox mapping, +you can plug that playlist in as a staged input. It won’t replace your main provider — it’s only there to update +the list. All streaming, proxying, and metadata still come from the provider’s configuration. `staged` has the following properties: - `type` is optional, default is `m3u`. Valid values are `m3u` and `xtream` diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 7ccb98193..00de3fefd 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -308,7 +308,9 @@ "SOURCE_EDITOR": "Sources", "SKIP_LIVE": "Skip Live", "SKIP_VOD": "Skip VOD", - "SKIP_SERIES": "Skip Series" + "SKIP_SERIES": "Skip Series", + "ADD_WATCH": "Add Watch", + "ADD_MAPPING": "Add Mapping" }, "TITLE": { "USER_BOUQUET_EDITOR": "User group editor" @@ -423,7 +425,7 @@ "BRICK_Target": "Target", "BRICK_OutputM3u": "M3u", "BRICK_OutputXtream": "Xtream", - "BRICK_OutputHdhomerun": "HDHR", + "BRICK_OutputHdHomeRun": "HDHR", "BRICK_OutputStrm": "STRM" }, "COUNTRY" : { diff --git a/frontend/public/assets/icons.json b/frontend/public/assets/icons.json index 0e5a97b52..6846ac8d8 100644 --- a/frontend/public/assets/icons.json +++ b/frontend/public/assets/icons.json @@ -515,5 +515,10 @@ { "keys": ["Nodes", "SourceEditor"], "path": "M15 16v1.26l-6-3v-3.17L11.7 8H16V2h-6v4.9L7.3 10H3v6h5l7 3.5V22h6v-6z" + }, + { + "keys": ["Accept"], + "path": "m18 7-1.41-1.41-6.34 6.34 1.41 1.41zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12zM.41 13.41 6 19l1.41-1.41L1.83 12z" } + ] \ No newline at end of file diff --git a/frontend/scss/_theme.scss b/frontend/scss/_theme.scss index af916ae18..8aa610100 100644 --- a/frontend/scss/_theme.scss +++ b/frontend/scss/_theme.scss @@ -84,7 +84,7 @@ --menu-text-active-color: var(--primary-color); --menu-item-active-background-color: rgba(45, 70, 120, 0.55); --icon-button-hover-color: #a8a8b3; - --card-background-color: rgba(18, 22, 33, 0.55); + --card-background-color: rgba(18, 22, 33, 0.9); --card-color: var(--modest-text-color); --sub-card-background-color: rgba(18, 22, 33, 0.8); @@ -355,7 +355,8 @@ --brick-output-strm-background-color: #e74c3c88; --brick-output-strm-shadow-color: #e74c3c80; - --brick-port-background-color: #486786; + --brick-port-background-color: #587293; + --brick-port-border-color: #ffffff; --brick-port-valid-background-color: #2ecc71; --brick-port-invalid-background-color: #e74c3c; --source-editor-delete-color: #FF0000FF; diff --git a/frontend/scss/app/_component.scss b/frontend/scss/app/_component.scss index 5b08f7407..a48bf4915 100644 --- a/frontend/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -77,3 +77,6 @@ @forward "components/floating_background"; @forward "components/source_editor/source_editor"; @forward "components/source_editor/source_editor_form"; +@forward "components/source_editor/source_editor_block"; +@forward "components/source_editor/input_form"; +@forward "components/cluster_flags"; diff --git a/frontend/scss/app/components/_cluster_flags.scss b/frontend/scss/app/components/_cluster_flags.scss new file mode 100644 index 000000000..65a56a07f --- /dev/null +++ b/frontend/scss/app/components/_cluster_flags.scss @@ -0,0 +1,23 @@ +.tp__cluster-flags-input { + display: flex; + flex-flow: row nowrap; + + span { + cursor: pointer; + } + + span:not(:first-child) { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } + span:not(:last-child) { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + + .tp__chip.active { + color: var(--tag-active-color); + background-color: var(--tag-active-background-color); + border-color: var(--tag-active-border-color); + } +} diff --git a/frontend/scss/app/components/_tabset.scss b/frontend/scss/app/components/_tabset.scss index 1fb0aff33..262e6127e 100644 --- a/frontend/scss/app/components/_tabset.scss +++ b/frontend/scss/app/components/_tabset.scss @@ -77,4 +77,54 @@ height: 100%; overflow: auto; } -} \ No newline at end of file +} + +/* Tabs header */ +.tp__tab-header { + display: flex; + border-bottom: 2px solid var(--card-border-color); + background-color: var(--card-background-color); + border-radius: 8px 8px 0 0; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.tp__tab-button { + flex: 1; + padding: 0.6rem 1.2rem; + font-weight: 500; + font-size: 0.95rem; + text-align: center; + cursor: pointer; + background: none; + border: none; + color: var(--text-color); + transition: all 0.25s ease; + position: relative; +} + +.tp__tab-button:hover { + color: var(--text-button-hover-color); + background-color: var(--text-button-hover-background-color); +} + +.tp__tab-button.active { + color: var(--text-button-primary-color); + background-color: var(--text-button-primary-background-color); + font-weight: 600; +} + +.tp__tab-button.active::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3px; + border-radius: 2px 2px 0 0; +} + +.tp__tab-button, +.tp__tab-button.active::after { + transition: all 0.25s ease; +} diff --git a/frontend/scss/app/components/config/_config_view.scss b/frontend/scss/app/components/config/_config_view.scss index 9182d80cf..432858e11 100644 --- a/frontend/scss/app/components/config/_config_view.scss +++ b/frontend/scss/app/components/config/_config_view.scss @@ -49,6 +49,10 @@ gap: var(--gap-default); height: 2.5rem; align-items: center; + + li { + margin-bottom: var(--gap-small); + } } &__card { diff --git a/frontend/scss/app/components/source_editor/_input_form.scss b/frontend/scss/app/components/source_editor/_input_form.scss new file mode 100644 index 000000000..3dc00de20 --- /dev/null +++ b/frontend/scss/app/components/source_editor/_input_form.scss @@ -0,0 +1,20 @@ +.tp__input-form { + + &__body { + background-color: var(--card-background-color); + border: 1px solid var(--card-border-color); + + display: flex; + flex-flow: column; + + &__pages { + display: flex; + flex-flow: column; + width: 100%; + + .tp__card { + border: none; + } + } + } +} \ No newline at end of file diff --git a/frontend/scss/app/components/source_editor/_source_editor.scss b/frontend/scss/app/components/source_editor/_source_editor.scss index e2ca03411..ec35728d5 100644 --- a/frontend/scss/app/components/source_editor/_source_editor.scss +++ b/frontend/scss/app/components/source_editor/_source_editor.scss @@ -1,10 +1,12 @@ .tp__source-editor { + position: relative; display: flex; flex-flow: row nowrap; box-sizing: border-box; width: 100%; height: 100%; gap: var(--gap-default); + font-size: 1rem; &__sidebar { display: flex; @@ -50,202 +52,29 @@ } } - &__canvas { + &__canvas-wrapper { position: relative; box-sizing: border-box; width: 100%; height: 100%; + overflow: hidden; + border: 1px solid var(--card-border-color); + } + + &__canvas, + &__connections { + position: absolute; + left: 0; + top:0; + box-sizing: border-box; + width: 100%; + height: 100%; + } + + &__canvas { } &__connections { - position: absolute; - box-sizing: border-box; - width:100%; - height:100%; - border: 1px solid var(--card-border-color); - } - - &__block { - position: absolute; - width: 100px; - height: 50px; - border: 1px solid var(--border-color); - background-color: var(--card-background-color); - display: flex; - align-items: center; - justify-content: space-between; - border-radius: 6px; - border-top-left-radius: 0 !important; - box-shadow: 0 1px 2px rgba(0,0,0,0.2); - cursor: default; - - &-label { - flex: 1; - text-align: center; - pointer-events: none; - } - - &-handle { - position: absolute; - width: 50%; - top: -13px; - left: 0; - height: 12px; - cursor: grab; - display: flex; - align-items: center; - justify-content: center; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - - &-label { - pointer-events: none; - } - } - - &-delete { - display: flex; - justify-content: center; - align-items: center; - position: absolute; - top: -13px; - right: 0; - height: 12px; - width: 12px; - border-radius: 50%; - cursor: pointer; - background-color: var(--source-editor-delete-color); - } - } - - &__port { - position: absolute; - width: 10px; - height: 10px; - border-radius: 50%; - cursor: crosshair; - - &--left { - left: -5px; - background-color: var(--brick-port-background-color); - } - - &--right { - right: -5px; - background-color: var(--brick-port-background-color); - } - - &--valid { - background-color: var(--brick-port-valid-background-color); - } - - &--invalid { - background-color: var(--brick-port-invalid-background-color); - } - } -} - -.tp__source-editor__brick { - - &-InputXtream, - &-InputM3u { - border-color: var(--brick-input-border-color); - background-color: var(--brick-input-background-color); - .tp__source-editor__block-handle { - background: repeating-linear-gradient( - -55deg, - var(--brick-input-background-color), - var(--brick-input-background-color) 10px, - var(--brick-input-border-color) 10px, - var(--brick-input-border-color) 20px - ); - border-left: 1px solid var(--brick-input-border-color); - border-top: 1px solid var(--brick-input-border-color); - border-right: 1px solid var(--brick-input-border-color); - - } - } - &-Target { - border-color: var(--brick-target-border-color); - background-color: var(--brick-target-background-color); - .tp__source-editor__block-handle { - background: repeating-linear-gradient( - -55deg, - var(--brick-target-background-color), - var(--brick-target-background-color) 10px, - var(--brick-target-border-color) 10px, - var(--brick-target-border-color) 20px - ); - border-left: 1px solid var(--brick-target-border-color); - border-top: 1px solid var(--brick-target-border-color); - border-right: 1px solid var(--brick-target-border-color); - } - } - - &-OutputM3u { - border-color: var(--brick-output-m3u-border-color); - background-color: var(--brick-output-m3u-background-color); - .tp__source-editor__block-handle { - background: repeating-linear-gradient( - -55deg, - var(--brick-output-m3u-background-color), - var(--brick-output-m3u-background-color) 10px, - var(--brick-output-m3u-border-color) 10px, - var(--brick-output-m3u-border-color) 20px - ); - border-left: 1px solid var(--brick-output-m3u-border-color); - border-top: 1px solid var(--brick-output-m3u-border-color); - border-right: 1px solid var(--brick-output-m3u-border-color); - } - } - - &-OutputXtream { - border-color: var(--brick-output-xtream-border-color); - background-color: var(--brick-output-xtream-background-color); - .tp__source-editor__block-handle { - background: repeating-linear-gradient( - -55deg, - var(--brick-output-xtream-background-color), - var(--brick-output-xtream-background-color) 10px, - var(--brick-output-xtream-border-color) 10px, - var(--brick-output-xtream-border-color) 20px - ); - border-left: 1px solid var(--brick-output-xtream-border-color); - border-top: 1px solid var(--brick-output-xtream-border-color); - border-right: 1px solid var(--brick-output-xtream-border-color); - } - } - &-OutputHdhomerun { - border-color: var(--brick-output-hdhomerun-border-color); - background-color: var(--brick-output-hdhomerun-background-color); - .tp__source-editor__block-handle { - background: repeating-linear-gradient( - -55deg, - var(--brick-output-hdhomerun-background-color), - var(--brick-output-hdhomerun-background-color) 10px, - var(--brick-output-hdhomerun-border-color) 10px, - var(--brick-output-hdhomerun-border-color) 20px - ); - border-left: 1px solid var(--brick-output-hdhomerun-border-color); - border-top: 1px solid var(--brick-output-hdhomerun-border-color); - border-right: 1px solid var(--brick-output-hdhomerun-border-color); - } - } - &-OutputStrm { - border-color: var(--brick-output-strm-border-color); - background-color: var(--brick-output-strm-background-color); - .tp__source-editor__block-handle { - background: repeating-linear-gradient( - -55deg, - var(--brick-output-strm-background-color), - var(--brick-output-strm-background-color) 10px, - var(--brick-output-strm-border-color) 10px, - var(--brick-output-strm-border-color) 20px - ); - border-left: 1px solid var(--brick-output-strm-border-color); - border-top: 1px solid var(--brick-output-strm-border-color); - border-right: 1px solid var(--brick-output-strm-border-color); - } } } @@ -261,4 +90,15 @@ .clickable { cursor: pointer; +} + +.tp__form-page__toolbar { + margin-top: var(--padding-default); + padding-top: var(--padding-default); + display: flex; + flex-flow: row; + gap: var(--gap-large); + box-sizing: border-box; + width: 100%; + border-top: 1px solid var(--card-border-color); } \ No newline at end of file diff --git a/frontend/scss/app/components/source_editor/_source_editor_block.scss b/frontend/scss/app/components/source_editor/_source_editor_block.scss new file mode 100644 index 000000000..66b456266 --- /dev/null +++ b/frontend/scss/app/components/source_editor/_source_editor_block.scss @@ -0,0 +1,295 @@ + +$block-width: 100px; +$block-header-height: 12px; +$block-content-height: 50px; +$port-size: 10px; + +.tp__source-editor__block { + position: absolute; + display: flex; + flex-flow: column; + align-items: center; + background-color: transparent; + border: none; + min-width: $block-width; + max-width: $block-width; + width: $block-width; + + &-header { + position: relative; + display: flex; + flex-flow: row nowrap; + width: 100%; + min-height: $block-header-height; + max-height: $block-header-height; + height: $block-header-height; + + &-actions { + display: flex; + flex-flow: row nowrap; + height: 100%; + flex: 1 1 auto; + justify-content: flex-end; + } + } + + &-handle { + width: 50%; + height: 100%; + cursor: grab; + display: flex; + align-items: center; + justify-content: center; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + + } + + &-content { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: $block-content-height; + max-height: $block-content-height; + height: $block-content-height; + border: 1px solid var(--border-color); + background-color: var(--card-background-color); + border-radius: 6px; + border-top-left-radius: 0 !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + cursor: pointer; + padding: 0 var(--padding-small); + box-sizing: border-box; + + &-body { + display: flex; + flex-flow: column; + align-items: center; + height: 100%; + width: 100%; + } + } + + &-label { + display: block; + width: 100%; + height: 100%; + text-overflow: ellipsis; + text-align: center; + pointer-events: none; + align-content: center; + overflow: hidden; + white-space: nowrap + } + + &-sub-label { + font-size: 0.8rem; + color: var(--modest-text-color); + text-align: right; + pointer-events: none; + align-self: flex-end; + } + + + &-delete { + display: flex; + justify-content: center; + align-items: center; + right: 0; + height: 12px; + width: 12px; + border-radius: 50%; + cursor: pointer; + background-color: var(--source-editor-delete-color); + } + + &-port { + position: absolute; + width: $port-size; + height: $port-size; + border-radius: 50%; + border: 1px solid var(--brick-port-border-color); + cursor: crosshair; + z-index: 2; + + &--left { + left: -5px; + background-color: var(--brick-port-background-color); + } + + &--right { + right: -5px; + background-color: var(--brick-port-background-color); + } + + &--valid { + background-color: var(--brick-port-valid-background-color); + } + + &--invalid { + background-color: var(--brick-port-invalid-background-color); + } + } +} + +.tp__source-editor__brick { + &-InputXtream, + &-InputM3u { + border-color: var(--brick-input-border-color); + background-color: var(--brick-input-background-color); + } + + &-Target { + border-color: var(--brick-target-border-color); + background-color: var(--brick-target-background-color); + } + + &-OutputM3u { + border-color: var(--brick-output-m3u-border-color); + background-color: var(--brick-output-m3u-background-color); + } + + &-OutputXtream { + border-color: var(--brick-output-xtream-border-color); + background-color: var(--brick-output-xtream-background-color); + } + + &-OutputHdHomeRun { + border-color: var(--brick-output-hdhomerun-border-color); + background-color: var(--brick-output-hdhomerun-background-color); + } + + &-OutputStrm { + border-color: var(--brick-output-strm-border-color); + background-color: var(--brick-output-strm-background-color); + } +} + +.tp__source-editor__block { + &-InputXtream, + &-InputM3u { + .tp__source-editor__block-content { + border-color: var(--brick-input-border-color); + background-color: var(--brick-input-background-color); + } + + .tp__source-editor__block-handle { + background: repeating-linear-gradient( + -55deg, + var(--brick-input-background-color), + var(--brick-input-background-color) 10px, + var(--brick-input-border-color) 10px, + var(--brick-input-border-color) 20px + ); + border-left: 1px solid var(--brick-input-border-color); + border-top: 1px solid var(--brick-input-border-color); + border-right: 1px solid var(--brick-input-border-color); + } + + } + + &-Target { + .tp__source-editor__block-content { + border-color: var(--brick-target-border-color); + background-color: var(--brick-target-background-color); + } + + .tp__source-editor__block-handle { + background: repeating-linear-gradient( + -55deg, + var(--brick-target-background-color), + var(--brick-target-background-color) 10px, + var(--brick-target-border-color) 10px, + var(--brick-target-border-color) 20px + ); + border-left: 1px solid var(--brick-target-border-color); + border-top: 1px solid var(--brick-target-border-color); + border-right: 1px solid var(--brick-target-border-color); + } + + } + + &-OutputM3u { + .tp__source-editor__block-content { + border-color: var(--brick-output-m3u-border-color); + background-color: var(--brick-output-m3u-background-color); + } + + .tp__source-editor__block-handle { + background: repeating-linear-gradient( + -55deg, + var(--brick-output-m3u-background-color), + var(--brick-output-m3u-background-color) 10px, + var(--brick-output-m3u-border-color) 10px, + var(--brick-output-m3u-border-color) 20px + ); + border-left: 1px solid var(--brick-output-m3u-border-color); + border-top: 1px solid var(--brick-output-m3u-border-color); + border-right: 1px solid var(--brick-output-m3u-border-color); + } + + } + + &-OutputXtream { + .tp__source-editor__block-content { + border-color: var(--brick-output-xtream-border-color); + background-color: var(--brick-output-xtream-background-color); + } + + .tp__source-editor__block-handle { + background: repeating-linear-gradient( + -55deg, + var(--brick-output-xtream-background-color), + var(--brick-output-xtream-background-color) 10px, + var(--brick-output-xtream-border-color) 10px, + var(--brick-output-xtream-border-color) 20px + ); + border-left: 1px solid var(--brick-output-xtream-border-color); + border-top: 1px solid var(--brick-output-xtream-border-color); + border-right: 1px solid var(--brick-output-xtream-border-color); + } + } + + &-OutputHdHomeRun { + .tp__source-editor__block-content { + border-color: var(--brick-output-hdhomerun-border-color); + background-color: var(--brick-output-hdhomerun-background-color); + } + + .tp__source-editor__block-handle { + background: repeating-linear-gradient( + -55deg, + var(--brick-output-hdhomerun-background-color), + var(--brick-output-hdhomerun-background-color) 10px, + var(--brick-output-hdhomerun-border-color) 10px, + var(--brick-output-hdhomerun-border-color) 20px + ); + border-left: 1px solid var(--brick-output-hdhomerun-border-color); + border-top: 1px solid var(--brick-output-hdhomerun-border-color); + border-right: 1px solid var(--brick-output-hdhomerun-border-color); + } + } + + &-OutputStrm { + .tp__source-editor__block-content { + border-color: var(--brick-output-strm-border-color); + background-color: var(--brick-output-strm-background-color); + } + + .tp__source-editor__block-handle { + background: repeating-linear-gradient( + -55deg, + var(--brick-output-strm-background-color), + var(--brick-output-strm-background-color) 10px, + var(--brick-output-strm-border-color) 10px, + var(--brick-output-strm-border-color) 20px + ); + border-left: 1px solid var(--brick-output-strm-border-color); + border-top: 1px solid var(--brick-output-strm-border-color); + border-right: 1px solid var(--brick-output-strm-border-color); + } + } + +} \ No newline at end of file diff --git a/frontend/scss/app/components/source_editor/_source_editor_form.scss b/frontend/scss/app/components/source_editor/_source_editor_form.scss index f68b2dd72..62df1592b 100644 --- a/frontend/scss/app/components/source_editor/_source_editor_form.scss +++ b/frontend/scss/app/components/source_editor/_source_editor_form.scss @@ -1,6 +1,27 @@ +$form-width: 300px; + .tp__source-editor-form { position: absolute; - left: 100%; - height: 100vh; + height: 100%; + top: 0; + left:100%; + width: $form-width; z-index: 10; + transition: all 0.4s ease; + overflow: auto; + + .tp__card { + background-color: var(--card-background-color); + } + + //.tp__form-field, + //.tp__input { + // flex-flow: row; + // align-items: center; + //} +} + + +.tp__source-editor-form.active { + left: calc(100% - $form-width); } \ No newline at end of file diff --git a/frontend/scss/app/components/userlist/_proxy-user-credentials-form.scss b/frontend/scss/app/components/userlist/_proxy-user-credentials-form.scss index ebbecea6d..70cd99fc2 100644 --- a/frontend/scss/app/components/userlist/_proxy-user-credentials-form.scss +++ b/frontend/scss/app/components/userlist/_proxy-user-credentials-form.scss @@ -1,4 +1,4 @@ -@use "../../../size"; +@use "../../../size" as size; .tp__proxy-user-credentials-form { display: flex; diff --git a/frontend/src/app/components/cluster_flags_input.rs b/frontend/src/app/components/cluster_flags_input.rs new file mode 100644 index 000000000..026a4e6d9 --- /dev/null +++ b/frontend/src/app/components/cluster_flags_input.rs @@ -0,0 +1,75 @@ +use shared::model::{ClusterFlags}; +use yew::prelude::*; +use yew_i18n::use_translation; + +#[derive(Properties, Clone, PartialEq, Debug)] +pub struct ClusterFlagsInputProps { + pub name: String, + #[prop_or_default] + pub value: Option, + #[prop_or_default] + pub on_change: Callback<(String, Option)>, +} + +#[function_component] +pub fn ClusterFlagsInput(props: &ClusterFlagsInputProps) -> Html { + let translate = use_translation(); + + let flags = use_state(|| props.value.clone().unwrap_or_else(ClusterFlags::all)); + { + let set_flags = flags.clone(); + use_effect_with(props.value.clone(), move |val| { + set_flags.set(val.clone().unwrap_or_else(ClusterFlags::all)); + }); + } + + let handle_change = { + let onchange = props.on_change.clone(); + let name = props.name.clone(); + Callback::from(move |new_flags: Option| { + let cluster_flags = if new_flags.is_none_or(|f| f.is_empty()) { + None + } else { + new_flags + }; + let name = name.clone(); + onchange.emit((name, cluster_flags)); + }) + }; + + let handle_flag_click = { + let current_flags = flags.clone(); + Callback::from(move |new_flag| { + let mut new_flags = (*current_flags).clone(); + new_flags.toggle(new_flag); + current_flags.set(new_flags.clone()); + if new_flags.is_empty() { + handle_change.emit(None); + } else { + handle_change.emit(Some(new_flags)); + } + }) + }; + + let handle_live_click = { + let handle_flag_click= handle_flag_click.clone(); + Callback::from(move |_| {handle_flag_click.emit(ClusterFlags::Live); }) + }; + let handle_vod_click = { + let handle_flag_click= handle_flag_click.clone(); + Callback::from(move |_| {handle_flag_click.emit(ClusterFlags::Vod); }) + }; + + let handle_series_click = { + let handle_flag_click= handle_flag_click.clone(); + Callback::from(move |_| {handle_flag_click.emit(ClusterFlags::Series); }) + }; + + html! { +
+ { translate.t("LABEL.LIVE") } + { translate.t("LABEL.VOD") } + { translate.t("LABEL.SERIES") } +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/api_config_view.rs b/frontend/src/app/components/config/api_config_view.rs index e1aae60d5..7f212f512 100644 --- a/frontend/src/app/components/config/api_config_view.rs +++ b/frontend/src/app/components/config/api_config_view.rs @@ -6,7 +6,6 @@ use crate::{config_field, config_field_empty, edit_field_number_u16, edit_field_ use crate::app::components::AppIcon; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::ConfigForm; -use crate::app::components::config::macros::HasFormData; const LABEL_HOST: &str = "LABEL.HOST"; const LABEL_PORT: &str = "LABEL.PORT"; diff --git a/frontend/src/app/components/config/hdhomerun_config_view.rs b/frontend/src/app/components/config/hdhomerun_config_view.rs index ecd0d7b2f..a4df235dc 100644 --- a/frontend/src/app/components/config/hdhomerun_config_view.rs +++ b/frontend/src/app/components/config/hdhomerun_config_view.rs @@ -8,7 +8,6 @@ use crate::{config_field_bool, edit_field_bool, generate_form_reducer, html_if}; use crate::app::components::config::config_page::ConfigForm; use crate::app::components::config::hdhomerun_device_view::{HdHomerunDeviceView}; use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::config::HasFormData; const LABEL_ENABLED: &str = "LABEL.ENABLED"; const LABEL_DEVICE_AUTH: &str = "LABEL.DEVICE_AUTH"; diff --git a/frontend/src/app/components/config/hdhomerun_device_view.rs b/frontend/src/app/components/config/hdhomerun_device_view.rs index c00baafb0..6fd6945d2 100644 --- a/frontend/src/app/components/config/hdhomerun_device_view.rs +++ b/frontend/src/app/components/config/hdhomerun_device_view.rs @@ -2,7 +2,6 @@ use yew::prelude::*; use yew_i18n::use_translation; use shared::model::{HdHomeRunDeviceConfigDto}; use crate::app::components::{Card, CollapsePanel, TextButton}; -use crate::app::components::config::macros::HasFormData; use crate::{config_field, edit_field_number_u16, edit_field_number_u8, edit_field_text, generate_form_reducer, html_if}; generate_form_reducer!( diff --git a/frontend/src/app/components/config/ipcheck_config_view.rs b/frontend/src/app/components/config/ipcheck_config_view.rs index 5672ad072..5243620da 100644 --- a/frontend/src/app/components/config/ipcheck_config_view.rs +++ b/frontend/src/app/components/config/ipcheck_config_view.rs @@ -5,7 +5,6 @@ use crate::app::context::ConfigContext; use crate::{config_field_empty, config_field_optional, edit_field_text_option, generate_form_reducer}; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::{ConfigForm}; -use crate::app::components::config::macros::HasFormData; const LABEL_URL: &str = "LABEL.URL"; const LABEL_URL_IPV4: &str = "LABEL.URL_IPV4"; const LABEL_URL_IPV6: &str = "LABEL.URL_IPV6"; diff --git a/frontend/src/app/components/config/log_config_view.rs b/frontend/src/app/components/config/log_config_view.rs index 9c6fa8bde..b4a4cc0d6 100644 --- a/frontend/src/app/components/config/log_config_view.rs +++ b/frontend/src/app/components/config/log_config_view.rs @@ -6,7 +6,6 @@ use crate::app::context::ConfigContext; use crate::{config_field_bool, config_field_child, edit_field_bool, generate_form_reducer}; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::{ConfigForm}; -use crate::app::components::config::macros::HasFormData; use crate::app::components::{Chip, RadioButtonGroup}; const LABEL_LOG_LEVEL: &str = "LABEL.LOG_LEVEL"; diff --git a/frontend/src/app/components/config/macros.rs b/frontend/src/app/components/config/macros.rs index 853547da1..aef18628c 100644 --- a/frontend/src/app/components/config/macros.rs +++ b/frontend/src/app/components/config/macros.rs @@ -50,8 +50,8 @@ macro_rules! config_field_bool { ($config:expr, $label:expr, $field:ident) => { html! {
- <$crate::app::components::ToggleSwitch value={$config.$field} readonly={true} /> +
} }; @@ -140,7 +140,7 @@ macro_rules! edit_field_text_option { hidden={$hidden} name={stringify!($field)} autocomplete={true} - value={(*instance).data().$field.as_ref().map_or_else(String::new, |v|v.to_string())} + value={instance.form.$field.as_ref().map_or_else(String::new, |v|v.to_string())} on_change={Callback::from(move |value: String| { instance.dispatch($action(if value.is_empty() { None @@ -171,7 +171,7 @@ macro_rules! edit_field_text { hidden={$hidden} name={stringify!($field)} autocomplete={true} - value={(*instance).data().$field.clone()} + value={instance.form.$field.clone()} on_change={Callback::from(move |value: String| { instance.dispatch($action(value)); })} @@ -187,11 +187,11 @@ macro_rules! edit_field_bool { let instance = $instance.clone(); html! {
- <$crate::app::components::ToggleSwitch - value={(*instance).data().$field} + value={instance.form.$field} readonly={false} on_change={Callback::from(move |value| instance.dispatch($action(value)))} /> +
} }}; @@ -206,7 +206,7 @@ macro_rules! edit_field_number { <$crate::app::components::number_input::NumberInput label={$label} name={stringify!($field)} - value={(*instance).data().$field as i64} + value={instance.form.$field as i64} on_change={Callback::from(move |value: Option| { match value { Some(value) => { @@ -231,7 +231,7 @@ macro_rules! edit_field_number_u8 { <$crate::app::components::number_input::NumberInput label={$label} name={stringify!($field)} - value={(*instance).data().$field as i64} + value={instance.form.$field as i64} on_change={Callback::from(move |value: Option| { match value { Some(value) => { @@ -256,7 +256,7 @@ macro_rules! edit_field_number_u16 { <$crate::app::components::number_input::NumberInput label={$label} name={stringify!($field)} - value={(*instance).data().$field as i64} + value={instance.form.$field as i64} on_change={Callback::from(move |value: Option| { match value { Some(value) => { @@ -282,7 +282,7 @@ macro_rules! edit_field_number_i16 { <$crate::app::components::number_input::NumberInput label={$label} name={stringify!($field)} - value={(*instance).data().$field as i64} + value={instance.form.$field as i64} on_change={Callback::from(move |value: Option| { match value { Some(value) => { @@ -307,7 +307,7 @@ macro_rules! edit_field_number_u64 { <$crate::app::components::number_input::NumberInput label={$label} name={stringify!($field)} - value={(*instance).data().$field as i64} + value={instance.form.$field as i64} on_change={Callback::from(move |value: Option| { match value { Some(value) => { @@ -332,7 +332,7 @@ macro_rules! edit_field_number_option { <$crate::app::components::number_input::NumberInput label={$label} name={stringify!($field)} - value={(*instance).data().$field.map(|v| v as i64)} + value={instance.form.$field.map(|v| v as i64)} on_change={Callback::from(move |value: Option| { match value { Some(value) => { @@ -357,7 +357,7 @@ macro_rules! edit_field_date { <$crate::app::components::date_input::DateInput label={$label} name={stringify!($field)} - value={(*instance).data().$field.clone()} + value={instance.form.$field.clone()} on_change={Callback::from(move |value: Option| { instance.dispatch($action(value)); })} @@ -371,7 +371,7 @@ macro_rules! edit_field_date { macro_rules! edit_field_list { ($instance:expr, $label:expr, $field:ident, $action:path, $placeholder:expr) => {{ let instance = $instance.clone(); - let tag_list = (*instance).data().$field.iter() + let tag_list = instance.form.$field.iter() .map(|s| std::rc::Rc::new($crate::app::components::Tag { label: s.clone(), class: None })) .collect::>>(); html! { @@ -394,7 +394,7 @@ macro_rules! edit_field_list { macro_rules! edit_field_list_option { ($instance:expr, $label:expr, $field:ident, $action:path, $placeholder:expr) => {{ let instance = $instance.clone(); - let tag_list = (*instance).data().$field.as_ref().map_or_else(Vec::new, |f| f.iter() + let tag_list = instance.form.$field.as_ref().map_or_else(Vec::new, |f| f.iter() .map(|s| std::rc::Rc::new($crate::app::components::Tag { label: s.clone(), class: None })) .collect::>>()); html! { @@ -452,20 +452,21 @@ macro_rules! generate_form_reducer { type Action = $action_name; fn reduce(self: std::rc::Rc, action: Self::Action) -> std::rc::Rc { - let mut new_data = self.$data_field.clone(); let mut modified = self.modified; - match action { + let new_data = match action { $( $action_name::$set_name(v) => { + let mut new_data = self.$data_field.clone(); new_data.$field_name = v; if !modified { modified = true; } + new_data }, )* $action_name::SetAll(v) => { - new_data = v; modified = false; + v }, - } + }; $state_name { $data_field: new_data, modified }.into() } } diff --git a/frontend/src/app/components/config/main_config_view.rs b/frontend/src/app/components/config/main_config_view.rs index 5cd232c34..d5c5050a1 100644 --- a/frontend/src/app/components/config/main_config_view.rs +++ b/frontend/src/app/components/config/main_config_view.rs @@ -4,7 +4,6 @@ use shared::model::MainConfigDto; use crate::app::context::ConfigContext; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::ConfigForm; -use crate::app::components::config::macros::HasFormData; use crate::{config_field_optional, config_field_bool, config_field, edit_field_text_option, edit_field_bool, generate_form_reducer, edit_field_number, edit_field_number_option, edit_field_text, edit_field_number_u8}; const LABEL_UPDATE_ON_BOOT: &str = "LABEL.UPDATE_ON_BOOT"; diff --git a/frontend/src/app/components/config/messaging_config_view.rs b/frontend/src/app/components/config/messaging_config_view.rs index 0c7ed850c..b81222703 100644 --- a/frontend/src/app/components/config/messaging_config_view.rs +++ b/frontend/src/app/components/config/messaging_config_view.rs @@ -1,6 +1,5 @@ use crate::app::components::config::config_page::ConfigForm; use crate::app::components::config::config_view_context::ConfigViewContext; -use crate::app::components::config::macros::HasFormData; use crate::app::components::{Card, Chip, RadioButtonGroup}; use crate::{config_field, config_field_bool, config_field_bool_empty, config_field_child, config_field_empty, config_field_hide, config_field_optional, edit_field_bool, edit_field_list, edit_field_text, edit_field_text_option, generate_form_reducer}; use shared::model::{MessagingConfigDto, MsgKind, PushoverMessagingConfigDto, RestMessagingConfigDto, TelegramMessagingConfigDto}; diff --git a/frontend/src/app/components/config/proxy_config_view.rs b/frontend/src/app/components/config/proxy_config_view.rs index 3a7d04089..3a97fad58 100644 --- a/frontend/src/app/components/config/proxy_config_view.rs +++ b/frontend/src/app/components/config/proxy_config_view.rs @@ -4,7 +4,6 @@ use shared::model::ProxyConfigDto; use crate::app::context::ConfigContext; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::ConfigForm; -use crate::app::components::config::macros::HasFormData; use crate::{ config_field, config_field_optional, config_field_optional_hide, edit_field_text, edit_field_text_option, generate_form_reducer, diff --git a/frontend/src/app/components/config/reverse_proxy_config_view.rs b/frontend/src/app/components/config/reverse_proxy_config_view.rs index 4a613866c..458e6ce76 100644 --- a/frontend/src/app/components/config/reverse_proxy_config_view.rs +++ b/frontend/src/app/components/config/reverse_proxy_config_view.rs @@ -6,7 +6,6 @@ use shared::model::{CacheConfigDto, GeoIpConfigDto, RateLimitConfigDto, ReverseP use crate::app::context::ConfigContext; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::ConfigForm; -use crate::app::components::config::macros::HasFormData; use crate::app::components::{Card}; use crate::{config_field, config_field_bool, config_field_custom, config_field_optional, edit_field_bool, edit_field_list, edit_field_number, edit_field_number_u64, edit_field_text, edit_field_text_option, generate_form_reducer}; diff --git a/frontend/src/app/components/config/schedules_config_view.rs b/frontend/src/app/components/config/schedules_config_view.rs index c4e44b014..75d26e1a6 100644 --- a/frontend/src/app/components/config/schedules_config_view.rs +++ b/frontend/src/app/components/config/schedules_config_view.rs @@ -1,7 +1,6 @@ -use std::rc::Rc; use std::str::FromStr; -use cron::Schedule; -use crate::app::components::{Card, Chip, DropDownOption, IconButton, NoContent}; +use cron::{Schedule}; +use crate::app::components::{Card, Chip, DropDownOption, DropDownSelection, IconButton, NoContent}; use crate::app::ConfigContext; use yew::prelude::*; use yew_i18n::use_translation; @@ -48,11 +47,11 @@ pub fn SchedulesConfigView() -> Html { let target_options = use_memo((selected_targets.clone(), all_targets.clone()), move |(selected, all_targets)| all_targets .iter() - .map(|t| Rc::new(DropDownOption { + .map(|t| DropDownOption { id: t.clone(), label: html! { t.clone() }, selected: (*selected).as_ref().is_some_and(|s| s.contains(t)), - })).collect::>>(), + }).collect::>(), ); let form_state: UseReducerHandle = use_reducer(|| { @@ -95,11 +94,11 @@ pub fn SchedulesConfigView() -> Html { let handle_target_selection = { let set_selected_targets = selected_targets.clone(); - Callback::from(move |(_name, selections):(String, Vec>)| { - if selections.is_empty() { - set_selected_targets.set(None); - } else { - set_selected_targets.set(Some(selections.iter().map(|t| t.id.clone()).collect())); + Callback::from(move |(_name, selections):(String, DropDownSelection)| { + match selections { + DropDownSelection::Empty => set_selected_targets.set(None), + DropDownSelection::Single(options) => set_selected_targets.set(Some(vec![options])), + DropDownSelection::Multi(options) => set_selected_targets.set(Some(options)), } }) }; @@ -221,8 +220,8 @@ pub fn SchedulesConfigView() -> Html { {config_field_child!(translate.t(LABEL_TARGETS), { html!{ { + staged_input_state_2.dispatch(StagedInputFormAction::InputType(InputType::Xtream)); + } + DropDownSelection::Single(option) => { + staged_input_state_2.dispatch(StagedInputFormAction::InputType(option.parse::().unwrap_or(InputType::Xtream))); + } + DropDownSelection::Multi(options) => { + if let Some(first) = options.first() { + staged_input_state_2.dispatch(StagedInputFormAction::InputType(first.parse::().unwrap_or(InputType::Xtream))); + } + } } })} - options={&input_types} - selected={staged_input_type_selection} + options={staged_input_types.clone()} /> }})} - // pub method: InputFetchMethod, - // pub input_type: InputType, + //{ edit_field_list!(staged_input_state, translate.t(LABEL_HEADERS), headers, StagedInputFormAction::Headers, translate.t(LABEL_ADD_HEADER)) } } @@ -250,18 +259,16 @@ pub fn ConfigInputView() -> Html { let render_input = || { let input_method_selection = Rc::new(vec![input_form_state.form.method.to_string()]); - let input_input_type_selection = Rc::new(vec![input_form_state.form.input_type.to_string()]); let input_form_state_disp = input_form_state.clone(); html! { { edit_field_bool!(input_form_state, translate.t(LABEL_ENABLED), enabled, ConfigInputFormAction::Enabled) } - { edit_field_text!(input_form_state, translate.t(LABEL_NAME), url, ConfigInputFormAction::Name) } + { edit_field_text!(input_form_state, translate.t(LABEL_NAME), name, ConfigInputFormAction::Name) } { edit_field_text!(input_form_state, translate.t(LABEL_URL), url, ConfigInputFormAction::Url) } { edit_field_text_option!(input_form_state, translate.t(LABEL_USERNAME), username, ConfigInputFormAction::Username) } - { edit_field_text_option!(input_form_state, translate.t(LABEL_PASSWORD), password, ConfigInputFormAction::Password) } + { edit_field_text_option!(input_form_state, translate.t(LABEL_PASSWORD), password, ConfigInputFormAction::Password, true) } // pub input_type: InputType, //{ edit_field_list!(input_form_state, translate.t(LABEL_HEADERS), headers, ConfigInputFormAction::Headers, translate.t(LABEL_ADD_HEADER)) } - { edit_field_text_option!(input_form_state, translate.t(LABEL_PERSIST), persist, ConfigInputFormAction::Persist) } { edit_field_number_i16!(input_form_state, translate.t(LABEL_PRIORITY), priority, ConfigInputFormAction::Priority) } { edit_field_number_u16!(input_form_state, translate.t(LABEL_MAX_CONNECTIONS), max_connections, ConfigInputFormAction::MaxConnections) } { config_field_child!(translate.t(LABEL_FETCH_METHOD), { @@ -277,19 +284,20 @@ pub fn ConfigInputView() -> Html { selected={input_method_selection} /> }})} - { config_field_child!(translate.t(LABEL_INPUT_TYPE), { - html! { - >| { - if let Some(first) = selections.first() { - input_form_state.dispatch(ConfigInputFormAction::InputType(first.parse::().unwrap_or(InputType::Xtream))); - } - })} - options={input_types.clone()} - selected={input_input_type_selection} - /> - }})} + // { config_field_child!(translate.t(LABEL_INPUT_TYPE), { + // html! { + // >| { + // if let Some(first) = selections.first() { + // input_form_state.dispatch(ConfigInputFormAction::InputType(first.parse::().unwrap_or(InputType::Xtream))); + // } + // })} + // options={input_types.clone()} + // selected={input_input_type_selection} + // /> + // }})} + { edit_field_text_option!(input_form_state, translate.t(LABEL_PERSIST), persist, ConfigInputFormAction::Persist) } // pub epg: Option, // pub aliases: Option>, @@ -297,18 +305,89 @@ pub fn ConfigInputView() -> Html { } }; + let handle_apply_input = { // source_editor_ctx.on_form_change.emit(); + let source_editor_ctx = source_editor_ctx.clone(); + let input_form_state = input_form_state.clone(); + let input_options_state = input_options_state.clone(); + let staged_input_state = staged_input_state.clone(); + let block_id = props.block_id; + Callback::from(move |_| { + let mut input = input_form_state.data().clone(); + let options = input_options_state.data(); + if !options.is_empty() { + input.options = Some(options.clone()); + } + let staged_input = staged_input_state.data(); + if !staged_input.is_empty() { + input.staged = Some(staged_input.clone()); + } + + source_editor_ctx.on_form_change.emit((block_id, BlockInstance::Input(Rc::new(input)))); + source_editor_ctx.edit_mode.set(EditMode::Inactive); + }) + }; + let handle_cancel = { + let source_editor_ctx = source_editor_ctx.clone(); + Callback::from(move |_| { + source_editor_ctx.edit_mode.set(EditMode::Inactive); + }) + }; + let render_edit_mode = || { html! { -
+
+
+ { + for [ + InputFormPage::Main, + InputFormPage::Options, + InputFormPage::Staged + ].iter().map(|page| { + let page_str = page.to_string(); + let active = *view_visible == page_str; + let on_tab_click = { + let on_tab_click = on_tab_click.clone(); + let page = *page; + Callback::from(move |_| on_tab_click.emit(page)) + }; + html! { + + } + }) + } +
+
+ {render_input()} + + {render_options()} + + {render_staged()} + +
} }; html! { -
+
+
+ + +
{ render_edit_mode() }
} diff --git a/frontend/src/app/components/source_editor/mod.rs b/frontend/src/app/components/source_editor/mod.rs index 27fca5f1d..35ca7f999 100644 --- a/frontend/src/app/components/source_editor/mod.rs +++ b/frontend/src/app/components/source_editor/mod.rs @@ -3,7 +3,10 @@ mod editor_model; mod sidebar; mod rules; mod input_form; +mod target_form; +mod output_form; mod editor_form; +mod block; pub use editor_view::*; pub use editor_form::*; diff --git a/frontend/src/app/components/source_editor/output_form.rs b/frontend/src/app/components/source_editor/output_form.rs new file mode 100644 index 000000000..aa8172c66 --- /dev/null +++ b/frontend/src/app/components/source_editor/output_form.rs @@ -0,0 +1,17 @@ +use shared::model::{TargetOutputDto}; +use std::rc::Rc; +use yew::{function_component, html, Html, Properties}; + +#[derive(Properties, PartialEq, Clone)] +pub struct ConfigOutputViewProps { + pub(crate) block_id: usize, + pub(crate) output: Option>, +} + +#[function_component] +pub fn ConfigOutputView(props: &ConfigOutputViewProps) -> Html { + html! { +
+
+ } +} diff --git a/frontend/src/app/components/source_editor/rules.rs b/frontend/src/app/components/source_editor/rules.rs index 594dfe66d..f9eabd8db 100644 --- a/frontend/src/app/components/source_editor/rules.rs +++ b/frontend/src/app/components/source_editor/rules.rs @@ -23,7 +23,7 @@ pub fn can_connect(from_block: &Block, to_block: &Block, connections: &[Connecti to_block.block_type, BlockType::OutputM3u | BlockType::OutputXtream - | BlockType::OutputHdhomerun + | BlockType::OutputHdHomeRun | BlockType::OutputStrm ); @@ -61,7 +61,7 @@ pub fn can_connect(from_block: &Block, to_block: &Block, connections: &[Connecti match out_block.block_type { BlockType::OutputM3u => count_m3u += 1, BlockType::OutputXtream => count_xtream += 1, - BlockType::OutputHdhomerun => count_hdhomerun += 1, + BlockType::OutputHdHomeRun => count_hdhomerun += 1, BlockType::OutputStrm => count_strm += 1, _ => {} } @@ -71,7 +71,7 @@ pub fn can_connect(from_block: &Block, to_block: &Block, connections: &[Connecti match to_block.block_type { BlockType::OutputM3u if count_m3u >= 1 => return false, BlockType::OutputXtream if count_xtream >= 1 => return false, - BlockType::OutputHdhomerun if count_hdhomerun >= 1 => return false, + BlockType::OutputHdHomeRun if count_hdhomerun >= 1 => return false, BlockType::OutputStrm if count_strm >= 4 => return false, _ => {} } diff --git a/frontend/src/app/components/source_editor/sidebar.rs b/frontend/src/app/components/source_editor/sidebar.rs index d132e4177..cf9f1af42 100644 --- a/frontend/src/app/components/source_editor/sidebar.rs +++ b/frontend/src/app/components/source_editor/sidebar.rs @@ -15,7 +15,7 @@ pub const BLOCK_TYPES_TARGET: [BlockType; 1] = [ pub const BLOCK_TYPES_OUTPUT: [BlockType; 4] = [ BlockType::OutputM3u, BlockType::OutputXtream, - BlockType::OutputHdhomerun, + BlockType::OutputHdHomeRun, BlockType::OutputStrm]; @@ -25,6 +25,7 @@ fn create_brick(t: &BlockType, on_drag_start: Callback, label: String draggable={"true"} data-block-type={t.to_string()} ondragstart={on_drag_start}> + { label }
} diff --git a/frontend/src/app/components/source_editor/target_form.rs b/frontend/src/app/components/source_editor/target_form.rs new file mode 100644 index 000000000..b81d14c46 --- /dev/null +++ b/frontend/src/app/components/source_editor/target_form.rs @@ -0,0 +1,273 @@ +use crate::app::components::config::HasFormData; +use crate::app::components::select::Select; +use crate::app::components::{BlockInstance, Card, ClusterFlagsInput, DropDownOption, DropDownSelection, EditMode, Panel, SourceEditorContext, TextButton}; +use crate::{config_field_child, edit_field_bool, edit_field_list_option, edit_field_text, generate_form_reducer}; +use shared::model::{ClusterFlags, ConfigTargetDto, ConfigTargetOptions, ProcessingOrder}; +use std::fmt::Display; +use std::rc::Rc; +use yew::{classes, function_component, html, use_context, use_effect_with, use_memo, use_reducer, use_state, Callback, Html, Properties, UseReducerHandle}; +use yew_i18n::use_translation; + +const LABEL_ENABLED: &str = "LABEL.ENABLED"; +const LABEL_NAME: &str = "LABEL.NAME"; +const LABEL_FILTER: &str = "LABEL.FILTER"; +const LABEL_MAPPING: &str = "LABEL.MAPPING"; +const LABEL_WATCH: &str = "LABEL.WATCH"; +const LABEL_ADD_MAPPING: &str = "LABEL.ADD_MAPPING"; +const LABEL_ADD_WATCH: &str = "LABEL.ADD_WATCH"; +const LABEL_USE_MEMORY_CACHE: &str = "LABEL.USE_MEMORY_CACHE"; +const LABEL_PROCESSING_ORDER: &str = "LABEL.PROCESSING_ORDER"; +const LABEL_IGNORE_LOGO: &str = "LABEL.IGNORE_LOGO"; +const LABEL_SHARE_LIVE_STREAMS: &str = "LABEL.SHARE_LIVE_STREAMS"; +const LABEL_REMOVE_DUPLICATES: &str = "LABEL.REMOVE_DUPLICATES"; +const LABEL_FORCE_REDIRECT: &str = "LABEL.FORCE_REDIRECT"; + +#[derive(Copy, Clone, PartialEq, Eq)] +enum TargetFormPage { + Main, + Options, +} + +impl Display for TargetFormPage { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", match *self { + TargetFormPage::Main => "Main".to_string(), + TargetFormPage::Options => "Options".to_string(), + }) + } +} + + +// pub sort: Option, +// pub rename: Option>, +// pub favourites: Option>, + +generate_form_reducer!( + state: ConfigTargetOptionsFormState { form: ConfigTargetOptions }, + action_name: ConfigTargetOptionsFormAction, + fields { + IgnoreLogo => ignore_logo: bool, + ShareLiveStreams => share_live_streams: bool, + RemoveDuplicates => remove_duplicates: bool, + ForceRedirect => force_redirect: Option, + } +); + +generate_form_reducer!( + state: ConfigTargetFormState { form: ConfigTargetDto }, + action_name: ConfigTargetFormAction, + fields { + Enabled => enabled: bool, + Name => name: String, + ProcessingOrder => processing_order: ProcessingOrder, + Filter => filter: String, + Mapping => mapping: Option>, + Watch => watch: Option>, + UseMemoryCache => use_memory_cache: bool, + } +); + +#[derive(Properties, PartialEq, Clone)] +pub struct ConfigTargetViewProps { + pub(crate) block_id: usize, + pub(crate) target: Option>, +} + +#[function_component] +pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html { + let translate = use_translation(); + let source_editor_ctx = use_context::().expect("SourceEditorContext not found"); + + let target_form_state: UseReducerHandle = + use_reducer(|| ConfigTargetFormState { + form: ConfigTargetDto::default(), + modified: false, + }); + let target_options_state: UseReducerHandle = + use_reducer(|| ConfigTargetOptionsFormState { + form: ConfigTargetOptions::default(), + modified: false, + }); + + let view_visible = use_state(|| TargetFormPage::Main.to_string()); + + let on_tab_click = { + let view_visible = view_visible.clone(); + Callback::from(move |page: TargetFormPage| view_visible.set(page.to_string())) + }; + + let processing_orders = use_memo(target_form_state.clone(), |target_state: &UseReducerHandle| { + let default_po = target_state.form.processing_order; + [ + ProcessingOrder::Frm, + ProcessingOrder::Fmr, + ProcessingOrder::Rfm, + ProcessingOrder::Rmf, + ProcessingOrder::Mfr, + ProcessingOrder::Mrf, + ] + .iter() + .map(|t| DropDownOption { + id: t.to_string(), + label: html! { t.to_string() }, + selected: *t == default_po, + }).collect::>() + }); + + { + let target_form_state = target_form_state.clone(); + let target_options_state = target_options_state.clone(); + + let config_target = props.target.clone(); + + use_effect_with(config_target, move |cfg| { + if let Some(target) = cfg { + target_form_state.dispatch(ConfigTargetFormAction::SetAll(target.as_ref().clone())); + target_options_state.dispatch(ConfigTargetOptionsFormAction::SetAll( + target.options.as_ref() + .map_or_else(ConfigTargetOptions::default, |d| d.clone()), + )); + } else { + target_form_state.dispatch(ConfigTargetFormAction::SetAll(ConfigTargetDto::default())); + target_options_state.dispatch(ConfigTargetOptionsFormAction::SetAll(ConfigTargetOptions::default())); + } + || () + }); + } + + let render_options = || { + let target_options_state_1 = target_options_state.clone(); + html! { + + { edit_field_bool!(target_options_state, translate.t(LABEL_IGNORE_LOGO), ignore_logo, ConfigTargetOptionsFormAction::IgnoreLogo) } + { edit_field_bool!(target_options_state, translate.t(LABEL_SHARE_LIVE_STREAMS), share_live_streams, ConfigTargetOptionsFormAction::ShareLiveStreams) } + { edit_field_bool!(target_options_state, translate.t(LABEL_REMOVE_DUPLICATES), remove_duplicates, ConfigTargetOptionsFormAction::RemoveDuplicates) } + { config_field_child!(translate.t(LABEL_FORCE_REDIRECT), { + html! { + )| { + target_options_state_1.dispatch(ConfigTargetOptionsFormAction::ForceRedirect(flags)); + })} + /> + }})} + + } + }; + + let render_target = || { + let target_form_state_1 = target_form_state.clone(); + html! { + + { edit_field_bool!(target_form_state, translate.t(LABEL_ENABLED), enabled, ConfigTargetFormAction::Enabled) } + { edit_field_bool!(target_form_state, translate.t(LABEL_USE_MEMORY_CACHE), use_memory_cache, ConfigTargetFormAction::UseMemoryCache) } + { edit_field_text!(target_form_state, translate.t(LABEL_NAME), name, ConfigTargetFormAction::Name) } + { edit_field_text!(target_form_state, translate.t(LABEL_FILTER), filter, ConfigTargetFormAction::Filter) } + + { config_field_child!(translate.t(LABEL_PROCESSING_ORDER), { + html! { + >)| { - if let Some(target_option) = selections.first() { - set_selected_target.set(Some(target_option.id.clone())); - } else { - set_selected_target.set(None); - } + on_select={Callback::from(move |(_name, selections):(String, DropDownSelection)| { + let target = match selections { + DropDownSelection::Empty => None, + DropDownSelection::Single(option) => Some(option), + DropDownSelection::Multi(options) => options.first().cloned(), + }; + set_selected_target.set(target); })} - options={(*targets).clone()} + options={targets.clone()} /> }})} //} { config_field_child!(translate.t("LABEL.STATUS"), { html! { >)| { - if let Some(server_option) = selections.first() { - instance_server.dispatch(UserFormAction::Server(Some(server_option.id.clone()))); - } else { - instance_server.dispatch(UserFormAction::Server(None)); - }; + on_select={Callback::from(move |(_name, selections):(String, DropDownSelection)| { + let server = match selections { + DropDownSelection::Empty => None, + DropDownSelection::Single(option) => Some(option.clone()), + DropDownSelection::Multi(options) => options.first().cloned(), + }; + instance_server.dispatch(UserFormAction::Server(server)); })} - options={(*server_list).clone()} + options={server_list.clone()} /> }})} { edit_field_number!(form_state, translate.t("LABEL.MAX_CONNECTIONS"), max_connections, UserFormAction::MaxConnections) } diff --git a/shared/src/model/config/target.rs b/shared/src/model/config/target.rs index 5d01a48f7..60bdd783b 100644 --- a/shared/src/model/config/target.rs +++ b/shared/src/model/config/target.rs @@ -17,6 +17,15 @@ pub struct ConfigTargetOptions { pub force_redirect: Option, } +impl ConfigTargetOptions { + pub fn is_empty(&self) -> bool { + !self.ignore_logo + && !self.share_live_streams + && !self.remove_duplicates + && (self.force_redirect.is_none() || self.force_redirect.is_some_and(|f| f.has_full_flags() || f.is_empty())) + } +} + #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] @@ -78,7 +87,7 @@ impl XtreamTargetOutputDto { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct M3uTargetOutputDto { #[serde(skip_serializing_if = "Option::is_none")] @@ -102,7 +111,7 @@ impl M3uTargetOutputDto { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct StrmTargetOutputDto { pub directory: String, @@ -134,6 +143,16 @@ pub struct HdHomeRunTargetOutputDto { pub use_output: Option, } +impl Default for HdHomeRunTargetOutputDto { + fn default() -> Self { + Self { + device: String::new(), + username: String::new(), + use_output: Some(TargetType::M3u), + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")] pub enum TargetOutputDto { diff --git a/shared/src/model/processing_order.rs b/shared/src/model/processing_order.rs index 79feaa0a4..a012191d8 100644 --- a/shared/src/model/processing_order.rs +++ b/shared/src/model/processing_order.rs @@ -1,4 +1,5 @@ use std::fmt::Display; +use std::str::FromStr; use enum_iterator::Sequence; #[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Default)] @@ -38,4 +39,32 @@ impl Display for ProcessingOrder { Self::Mrf => Self::MRF, }) } +} + +impl FromStr for ProcessingOrder { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let normalized = s.trim().to_lowercase(); + + match normalized.as_str() { + // Short codes + "frm" => Ok(Self::Frm), + "fmr" => Ok(Self::Fmr), + "rfm" => Ok(Self::Rfm), + "rmf" => Ok(Self::Rmf), + "mfr" => Ok(Self::Mfr), + "mrf" => Ok(Self::Mrf), + + // Accept the exact Display text (using your constants) + x if x == Self::FRM => Ok(Self::Frm), + x if x == Self::FMR => Ok(Self::Fmr), + x if x == Self::RFM => Ok(Self::Rfm), + x if x == Self::RMF => Ok(Self::Rmf), + x if x == Self::MFR => Ok(Self::Mfr), + x if x == Self::MRF => Ok(Self::Mrf), + + _ => Err("invalid processing order"), + } + } } \ No newline at end of file