mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-21 15:22:20 +02:00
added target form
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -77,4 +77,54 @@
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
gap: var(--gap-default);
|
||||
height: 2.5rem;
|
||||
align-items: center;
|
||||
|
||||
li {
|
||||
margin-bottom: var(--gap-small);
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@use "../../../size";
|
||||
@use "../../../size" as size;
|
||||
|
||||
.tp__proxy-user-credentials-form {
|
||||
display: flex;
|
||||
|
||||
@@ -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<ClusterFlags>,
|
||||
#[prop_or_default]
|
||||
pub on_change: Callback<(String, Option<ClusterFlags>)>,
|
||||
}
|
||||
|
||||
#[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<ClusterFlags>| {
|
||||
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! {
|
||||
<div class="tp__cluster-flags-input">
|
||||
<span onclick={handle_live_click} class={classes!("noselect", "tp__chip", "tp__cluster-flags-input-live", if flags.intersects(ClusterFlags::Live) {"active"} else {""})}>{ translate.t("LABEL.LIVE") }</span>
|
||||
<span onclick={handle_vod_click} class={classes!("noselect", "tp__chip", "tp__cluster-flags-input-vod", if flags.intersects(ClusterFlags::Vod) {"active"} else {""})}>{ translate.t("LABEL.VOD") }</span>
|
||||
<span onclick={handle_series_click} class={classes!("noselect", "tp__chip", "tp__cluster-flags-input-series", if flags.intersects(ClusterFlags::Series) {"active"} else {""})}>{ translate.t("LABEL.SERIES") }</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -50,8 +50,8 @@ macro_rules! config_field_bool {
|
||||
($config:expr, $label:expr, $field:ident) => {
|
||||
html! {
|
||||
<div class="tp__form-field tp__form-field__bool">
|
||||
<label>{$label}</label>
|
||||
<$crate::app::components::ToggleSwitch value={$config.$field} readonly={true} />
|
||||
<label>{$label}</label>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
@@ -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! {
|
||||
<div class="tp__form-field tp__form-field__bool">
|
||||
<label>{$label}</label>
|
||||
<$crate::app::components::ToggleSwitch
|
||||
value={(*instance).data().$field}
|
||||
value={instance.form.$field}
|
||||
readonly={false}
|
||||
on_change={Callback::from(move |value| instance.dispatch($action(value)))} />
|
||||
<label>{$label}</label>
|
||||
</div>
|
||||
}
|
||||
}};
|
||||
@@ -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<i64>| {
|
||||
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<i64>| {
|
||||
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<i64>| {
|
||||
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<i64>| {
|
||||
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<i64>| {
|
||||
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<i64>| {
|
||||
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<i64>| {
|
||||
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::<Vec<std::rc::Rc<$crate::app::components::Tag>>>();
|
||||
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::<Vec<std::rc::Rc<$crate::app::components::Tag>>>());
|
||||
html! {
|
||||
@@ -452,20 +452,21 @@ macro_rules! generate_form_reducer {
|
||||
type Action = $action_name;
|
||||
|
||||
fn reduce(self: std::rc::Rc<Self>, action: Self::Action) -> std::rc::Rc<Self> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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::<Vec<Rc<DropDownOption>>>(),
|
||||
}).collect::<Vec<DropDownOption>>(),
|
||||
);
|
||||
|
||||
let form_state: UseReducerHandle<SchedulesConfigFormState> = 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<Rc<DropDownOption>>)| {
|
||||
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!{ <Select name="target"
|
||||
multi_select={true}
|
||||
onselect={handle_target_selection}
|
||||
options={(*target_options).clone()}
|
||||
on_select={handle_target_selection}
|
||||
options={target_options.clone()}
|
||||
/>
|
||||
}})}
|
||||
<IconButton name="AddSchedule" icon="ScheduleAdd" class="tp__button-primary" onclick={handle_add_schedule} />
|
||||
|
||||
@@ -8,7 +8,6 @@ use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use crate::app::components::config::config_page::ConfigForm;
|
||||
use crate::app::components::config::config_view_context::ConfigViewContext;
|
||||
use crate::app::components::config::macros::HasFormData;
|
||||
|
||||
const LABEL_DOWNLOAD: &str = "LABEL.DOWNLOAD";
|
||||
const LABEL_ORGANIZE_INTO_DIRECTORIES: &str = "LABEL.ORGANIZE_INTO_DIRECTORIES";
|
||||
@@ -111,7 +110,7 @@ pub fn VideoConfigView() -> Html {
|
||||
html! {
|
||||
<div class="tp__config-view__tags">
|
||||
<ul>
|
||||
{ for download_state.form.headers.iter().map(|(k,v)| html!{ <li>{k}{":"} {v}</li> }) }
|
||||
{ for download_state.form.headers.iter().map(|(k,v)| html!{ <li>{"- "}{k}{": "} {v}</li> }) }
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::app::components::{AppIcon, Card, Chip};
|
||||
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_bool, config_field_child, config_field_hide, config_field_optional, edit_field_bool, edit_field_list_option, edit_field_number, edit_field_text, edit_field_text_option, generate_form_reducer, html_if};
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
@@ -7,6 +7,15 @@ use crate::app::components::{AppIcon, IconButton};
|
||||
use crate::app::components::popup_menu::PopupMenu;
|
||||
use crate::html_if;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DropDownSelection {
|
||||
Empty,
|
||||
Single(String),
|
||||
Multi(Vec<String>),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct DropDownOption {
|
||||
pub(crate) id: String,
|
||||
@@ -24,10 +33,10 @@ impl DropDownOption {
|
||||
pub struct DropDownIconButtonProps {
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub onselect: Callback<(String, Vec<String>)>,
|
||||
pub on_select: Callback<(String, DropDownSelection)>,
|
||||
#[prop_or_default]
|
||||
pub class: String,
|
||||
pub options: Vec<Rc<DropDownOption>>,
|
||||
pub options: Rc<Vec<DropDownOption>>,
|
||||
#[prop_or_default]
|
||||
pub multi_select: bool,
|
||||
#[prop_or_default]
|
||||
@@ -73,7 +82,7 @@ pub fn DropDownIconButton(props: &DropDownIconButtonProps) -> Html {
|
||||
let multi_select = props.multi_select;
|
||||
let name = props.name.clone();
|
||||
let selections = selections.clone();
|
||||
let onselect = props.onselect.clone();
|
||||
let onselect = props.on_select.clone();
|
||||
let set_is_open = popup_is_open.clone();
|
||||
Callback::from(move |(id, e): (String, MouseEvent)| {
|
||||
e.prevent_default();
|
||||
@@ -83,9 +92,13 @@ pub fn DropDownIconButton(props: &DropDownIconButtonProps) -> Html {
|
||||
selections.insert(id.clone());
|
||||
}
|
||||
let selected_options = if multi_select {
|
||||
selections.current().iter().map(Clone::clone).collect::<Vec<_>>()
|
||||
if selections.current().is_empty() {
|
||||
DropDownSelection::Empty
|
||||
} else {
|
||||
DropDownSelection::Multi(selections.current().iter().map(Clone::clone).collect::<Vec<_>>())
|
||||
}
|
||||
} else {
|
||||
vec![id.clone()]
|
||||
DropDownSelection::Single(id.clone())
|
||||
};
|
||||
onselect.emit((name.clone(), selected_options));
|
||||
if !multi_select {
|
||||
|
||||
@@ -48,10 +48,12 @@ mod websocket_status;
|
||||
|
||||
mod floating_background;
|
||||
mod source_editor;
|
||||
mod cluster_flags_input;
|
||||
// pub use self::input::*;
|
||||
// pub use self::menu_item::*;
|
||||
// pub use self::popup_menu::*;
|
||||
|
||||
pub use self::cluster_flags_input::*;
|
||||
pub use self::toggle_switch::*;
|
||||
pub use self::svg_icon::*;
|
||||
pub use self::icon_button::*;
|
||||
|
||||
@@ -3,7 +3,7 @@ use gloo_timers::callback::Timeout;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
use shared::model::SearchRequest;
|
||||
use crate::app::components::{AppIcon, DropDownIconButton, DropDownOption, IconButton};
|
||||
use crate::app::components::{AppIcon, DropDownIconButton, DropDownOption, DropDownSelection, IconButton};
|
||||
use crate::html_if;
|
||||
|
||||
const DEBOUNCE_TIMEOUT_MS: u32 = 500;
|
||||
@@ -19,7 +19,7 @@ pub struct SearchProps {
|
||||
#[prop_or_default]
|
||||
pub class: String,
|
||||
#[prop_or_default]
|
||||
pub options: Option<Vec<Rc<DropDownOption>>>,
|
||||
pub options: Option<Rc<Vec<DropDownOption>>>,
|
||||
pub onsearch: Option<Callback<SearchRequest>>,
|
||||
#[prop_or(3)]
|
||||
pub min_length: usize,
|
||||
@@ -114,7 +114,17 @@ pub fn Search(props: &SearchProps) -> Html {
|
||||
|
||||
let handle_options_click = {
|
||||
Callback::from(move |(_name, selections)| {
|
||||
search_fields.set(Some(Rc::new(selections)));
|
||||
match selections {
|
||||
DropDownSelection::Empty => {
|
||||
search_fields.set(None);
|
||||
}
|
||||
DropDownSelection::Multi(options) => {
|
||||
search_fields.set(Some(Rc::new(options)));
|
||||
}
|
||||
DropDownSelection::Single(option) => {
|
||||
search_fields.set(Some(Rc::new(vec![option])));
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -136,7 +146,7 @@ pub fn Search(props: &SearchProps) -> Html {
|
||||
html_if!(
|
||||
props.options.is_some(),
|
||||
{
|
||||
<DropDownIconButton multi_select={true} options={props.options.as_ref().unwrap().clone()} name="fields" icon="Popup" onselect={handle_options_click} />
|
||||
<DropDownIconButton multi_select={true} options={props.options.as_ref().unwrap().clone()} name="fields" icon="Popup" on_select={handle_options_click} />
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::{DropDownIconButton, DropDownOption};
|
||||
use crate::app::components::{DropDownIconButton, DropDownOption, DropDownSelection};
|
||||
|
||||
fn map_selection(o: &Rc<DropDownOption>) -> Html {
|
||||
fn map_selection(o: &DropDownOption) -> Html {
|
||||
html! {<span>{o.label.clone()}</span>}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ pub struct SelectProps {
|
||||
pub name: String,
|
||||
#[prop_or_default]
|
||||
pub icon: Option<String>,
|
||||
pub onselect: Callback<(String, Vec<Rc<DropDownOption>>)>,
|
||||
pub on_select: Callback<(String, DropDownSelection)>,
|
||||
#[prop_or_default]
|
||||
pub class: String,
|
||||
pub options: Vec<Rc<DropDownOption>>,
|
||||
pub options: Rc<Vec<DropDownOption>>,
|
||||
#[prop_or_default]
|
||||
pub multi_select: bool,
|
||||
}
|
||||
@@ -26,24 +26,13 @@ pub fn Select(props: &SelectProps) -> Html {
|
||||
let selected_options = use_state(Vec::new);
|
||||
{
|
||||
let set_selected_options = selected_options.clone();
|
||||
use_effect_with(props.options.clone(), move |options: &Vec<Rc<DropDownOption>>| {
|
||||
use_effect_with(props.options.clone(), move |options: &Rc<Vec<DropDownOption>>| {
|
||||
let selections = options.iter().filter(|o| o.selected)
|
||||
.map(map_selection).collect::<Vec<Html>>();
|
||||
set_selected_options.set(selections);
|
||||
});
|
||||
}
|
||||
|
||||
let handle_options_click = {
|
||||
let onselect = props.onselect.clone();
|
||||
let options = props.options.clone();
|
||||
let set_selections = selected_options.clone();
|
||||
Callback::from(move |(name, selections): (String, Vec<String>)| {
|
||||
let selected_options: Vec<Rc<DropDownOption>> = options.iter().filter(|&o| selections.contains(&o.id)).cloned().collect();
|
||||
set_selections.set(selected_options.iter().map(map_selection).collect());
|
||||
onselect.emit((name, selected_options));
|
||||
})
|
||||
};
|
||||
|
||||
let handle_click_button = {
|
||||
let button_ref = button_ref.clone();
|
||||
Callback::from(move |event: MouseEvent| {
|
||||
@@ -69,7 +58,7 @@ pub fn Select(props: &SelectProps) -> Html {
|
||||
options={props.options.clone()}
|
||||
name={props.name.clone()}
|
||||
icon={props.icon.as_ref().map_or_else(|| "Popup".to_owned(), |i|i.to_string())}
|
||||
onselect={handle_options_click} />
|
||||
on_select={props.on_select.clone()} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
use web_sys::MouseEvent;
|
||||
use yew::{classes, function_component, html, Callback, Html, Properties};
|
||||
use yew_i18n::use_translation;
|
||||
use crate::html_if;
|
||||
use crate::app::components::{Block, BlockInstance, BlockType, PortStatus};
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct BlockProps {
|
||||
pub(crate) block: Block,
|
||||
pub(crate) delete_mode: bool,
|
||||
pub(crate) delete_block: Callback<usize>,
|
||||
pub(crate) port_status: PortStatus,
|
||||
pub(crate) on_edit: Callback<usize>,
|
||||
pub(crate) on_mouse_down: Callback<(usize, MouseEvent)>,
|
||||
pub(crate) on_connection_drop: Callback<usize>, // to_id
|
||||
pub(crate) on_connection_start: Callback<usize>, // from_id
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn BlockView(props: &BlockProps) -> Html {
|
||||
|
||||
let translate = use_translation();
|
||||
|
||||
let delete_mode = props.delete_mode;
|
||||
let delete_block = props.delete_block.clone();
|
||||
let block = &props.block;
|
||||
let port_status = props.port_status;
|
||||
|
||||
let block_id = block.id;
|
||||
let block_type = block.block_type;
|
||||
let style = format!("position:absolute; left:{}px; top:{}px;", block.position.0, block.position.1);
|
||||
let from_id = block_id;
|
||||
let to_id = block_id;
|
||||
|
||||
let is_target = matches!(block_type, BlockType::Target);
|
||||
let is_input = !is_target && matches!(block_type, BlockType::InputM3u | BlockType::InputXtream);
|
||||
let is_output = !is_input && !is_target;
|
||||
|
||||
let port_style = match port_status {
|
||||
PortStatus::Valid => "tp__source-editor__port--valid",
|
||||
PortStatus::Invalid => "tp__source-editor__port--invalid",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
let handle_mouse_down = {
|
||||
let on_block_mouse_down = props.on_mouse_down.clone();
|
||||
Callback::from(move |e| on_block_mouse_down.emit((block_id, e)))
|
||||
};
|
||||
|
||||
let handle_edit = {
|
||||
let block_id = block_id;
|
||||
let on_edit = props.on_edit.clone();
|
||||
Callback::from(move |_| {
|
||||
on_edit.emit(block_id)
|
||||
})
|
||||
};
|
||||
|
||||
let (title, show_type) = {
|
||||
let dto_title = match &block.instance {
|
||||
BlockInstance::Input(dto) => dto.name.clone(),
|
||||
BlockInstance::Target(dto) => dto.name.clone(),
|
||||
BlockInstance::Output(_output) => {
|
||||
translate.t(&format!("SOURCE_EDITOR.BRICK_{}", block_type))
|
||||
}
|
||||
};
|
||||
if dto_title.is_empty() {
|
||||
(translate.t(&format!("SOURCE_EDITOR.BRICK_{}", block_type)), false)
|
||||
} else {
|
||||
(dto_title, true)
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class={format!("tp__source-editor__block no-select tp__source-editor__block-{}", block_type)} style={style}>
|
||||
<div class={"tp__source-editor__block-header"}>
|
||||
// Block handle (drag)
|
||||
<div class="tp__source-editor__block-handle" onmousedown={handle_mouse_down}/>
|
||||
// Delete button for block
|
||||
{
|
||||
html_if!(delete_mode, {
|
||||
<div class={"tp__source-editor__block-header-actions"}>
|
||||
<div class="tp__source-editor__block-delete" onclick={
|
||||
Callback::from(move |_| delete_block.emit(block_id))
|
||||
}></div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class={"tp__source-editor__block-content"} ondblclick={handle_edit}>
|
||||
<div class={"tp__source-editor__block-content-body"}>
|
||||
<div class="tp__source-editor__block-label">
|
||||
{ title }
|
||||
</div>
|
||||
{
|
||||
html_if!(show_type, {
|
||||
<span class="tp__source-editor__block-sub-label">{translate.t(&format!("SOURCE_EDITOR.BRICK_{}", block_type))}</span>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{html_if!(is_target || is_output, {
|
||||
// Left port
|
||||
<div
|
||||
class={classes!("tp__source-editor__block-port", "tp__source-editor__block-port--left", port_style)}
|
||||
onmouseup={{
|
||||
let on_connection_drop = props.on_connection_drop.clone();
|
||||
Callback::from(move |_| on_connection_drop.emit(to_id))
|
||||
}} />
|
||||
})}
|
||||
|
||||
{html_if!(is_target || is_input, {
|
||||
// Right port
|
||||
<div
|
||||
class="tp__source-editor__block-port tp__source-editor__block-port--right"
|
||||
onmousedown={{
|
||||
let on_connection_start = props.on_connection_start.clone();
|
||||
Callback::from(move |_| on_connection_start.emit(from_id))
|
||||
}} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,38 @@
|
||||
use yew::{function_component, html, Html};
|
||||
use yew::{classes, function_component, html, use_context, use_effect_with, use_state, Html};
|
||||
use crate::app::components::source_editor::input_form::ConfigInputView;
|
||||
use crate::app::components::{BlockInstance, EditMode, SourceEditorContext};
|
||||
use crate::app::components::source_editor::output_form::ConfigOutputView;
|
||||
use crate::app::components::source_editor::target_form::ConfigTargetView;
|
||||
|
||||
#[function_component]
|
||||
pub fn SourceEditorForm() -> Html {
|
||||
let source_editor_ctx =
|
||||
use_context::<SourceEditorContext>().expect("SourceEditorContext not found");
|
||||
let visible = use_state(|| false);
|
||||
|
||||
{
|
||||
let visible_set = visible.clone();
|
||||
use_effect_with(source_editor_ctx.edit_mode.clone(), move |edit_mode| {
|
||||
match **edit_mode {
|
||||
EditMode::Inactive => { visible_set.set(false); }
|
||||
EditMode::Active(_) => { visible_set.set(true); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
html! {
|
||||
<div class="tp__source-editor-form">
|
||||
<ConfigInputView></ConfigInputView>
|
||||
<div class={classes!("tp__source-editor-form", if *visible { "active" } else { "" }) }>
|
||||
{
|
||||
if let EditMode::Active(block) = &*source_editor_ctx.edit_mode {
|
||||
match &block.instance {
|
||||
BlockInstance::Input(input) => html! { <ConfigInputView block_id={block.id} input={Some(input.clone())}></ConfigInputView> },
|
||||
BlockInstance::Target(target) => html! { <ConfigTargetView block_id={block.id} target={Some(target.clone())}></ConfigTargetView> },
|
||||
BlockInstance::Output(output) => html! { <ConfigOutputView block_id={block.id} output={Some(output.clone())}></ConfigOutputView> },
|
||||
}
|
||||
} else {
|
||||
html!{}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yew::{Callback};
|
||||
use shared::model::{ConfigInputDto};
|
||||
use yew::{Callback, UseStateHandle};
|
||||
use shared::model::{ConfigInputDto, ConfigTargetDto, TargetOutputDto};
|
||||
|
||||
// ----------------- Data Models -----------------
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum BlockType {
|
||||
InputXtream,
|
||||
InputM3u,
|
||||
Target,
|
||||
OutputM3u,
|
||||
OutputXtream,
|
||||
OutputHdhomerun,
|
||||
OutputHdHomeRun,
|
||||
OutputStrm,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ impl BlockType {
|
||||
pub const TARGET: &'static str = "Target";
|
||||
pub const OUTPUT_M3U: &'static str = "OutputM3u";
|
||||
pub const OUTPUT_XTREAM: &'static str = "OutputXtream";
|
||||
pub const OUTPUT_HDHOMERUN: &'static str = "OutputHdhomerun";
|
||||
pub const OUTPUT_HDHOMERUN: &'static str = "OutputHdHomeRun";
|
||||
pub const OUTPUT_STRM: &'static str = "OutputStrm";
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ impl From<&str> for BlockType {
|
||||
BlockType::TARGET => BlockType::Target,
|
||||
BlockType::OUTPUT_M3U => BlockType::OutputM3u,
|
||||
BlockType::OUTPUT_XTREAM => BlockType::OutputXtream,
|
||||
BlockType::OUTPUT_HDHOMERUN => BlockType::OutputHdhomerun,
|
||||
BlockType::OUTPUT_HDHOMERUN => BlockType::OutputHdHomeRun,
|
||||
BlockType::OUTPUT_STRM => BlockType::OutputStrm,
|
||||
_ => BlockType::Target, // fallback
|
||||
}
|
||||
@@ -59,30 +59,54 @@ impl fmt::Display for BlockType {
|
||||
BlockType::Target => Self::TARGET,
|
||||
BlockType::OutputM3u => Self::OUTPUT_M3U,
|
||||
BlockType::OutputXtream => Self::OUTPUT_XTREAM,
|
||||
BlockType::OutputHdhomerun => Self::OUTPUT_HDHOMERUN,
|
||||
BlockType::OutputHdHomeRun => Self::OUTPUT_HDHOMERUN,
|
||||
BlockType::OutputStrm => Self::OUTPUT_STRM,
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type BlockId = usize;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Block {
|
||||
pub id: usize,
|
||||
pub id: BlockId,
|
||||
pub block_type: BlockType,
|
||||
pub position: (f32, f32),
|
||||
pub instance: BlockInstance,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Connection {
|
||||
pub from: usize,
|
||||
pub to: usize,
|
||||
pub from: BlockId,
|
||||
pub to: BlockId,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum PortStatus {
|
||||
Valid,
|
||||
Invalid,
|
||||
Inactive
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BlockInstance {
|
||||
Input(Rc<ConfigInputDto>),
|
||||
Target(Rc<ConfigTargetDto>),
|
||||
Output(Rc<TargetOutputDto>)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum EditMode {
|
||||
Inactive,
|
||||
Active(Block),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SourceEditorContext {
|
||||
pub input: Option<Rc<ConfigInputDto>>,
|
||||
pub on_form_change: Callback<ConfigInputDto>,
|
||||
pub on_form_change: Callback<(BlockId, BlockInstance)>,
|
||||
pub edit_mode: UseStateHandle<EditMode>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +1,57 @@
|
||||
use log::error;
|
||||
use std::rc::Rc;
|
||||
use crate::app::components::source_editor::editor_model::{Block, BlockType, Connection};
|
||||
use crate::app::components::source_editor::rules::can_connect;
|
||||
use crate::app::components::source_editor::sidebar::SourceEditorSidebar;
|
||||
use crate::html_if;
|
||||
use web_sys::{HtmlElement, MouseEvent};
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::model::ConfigInputDto;
|
||||
use crate::app::components::{SourceEditorContext, SourceEditorForm};
|
||||
use shared::model::{ConfigInputDto, ConfigTargetDto, HdHomeRunTargetOutputDto, M3uTargetOutputDto, StrmTargetOutputDto, TargetOutputDto, XtreamTargetOutputDto};
|
||||
use crate::app::components::{BlockId, BlockInstance, EditMode, PortStatus, SourceEditorContext, SourceEditorForm};
|
||||
use crate::app::components::source_editor::block::BlockView;
|
||||
|
||||
const PORT_INACTIVE: u32 = 0;
|
||||
const PORT_VALID: u32 = 1;
|
||||
const PORT_INVALID: u32 = 2;
|
||||
const BLOCK_WIDTH: f32 = 100.0;
|
||||
const BLOCK_HEIGHT: f32 = 50.0;
|
||||
const BLOCK_HEADER_HEIGHT: f32 = 12.0;
|
||||
const BLOCK_PORT_HEIGHT: f32 = 10.0;
|
||||
const BLOCK_MIDDLE_Y: f32 = (BLOCK_HEIGHT + BLOCK_HEADER_HEIGHT + BLOCK_PORT_HEIGHT) / 2.0;
|
||||
|
||||
fn create_instance(block_type: BlockType) -> BlockInstance {
|
||||
match block_type {
|
||||
BlockType::InputXtream => BlockInstance::Input(Rc::new(ConfigInputDto::default())),
|
||||
BlockType::InputM3u => BlockInstance::Input(Rc::new(ConfigInputDto::default())),
|
||||
BlockType::Target => {
|
||||
let mut dto = ConfigTargetDto::default();
|
||||
dto.name = String::new();
|
||||
BlockInstance::Target(Rc::new(dto))
|
||||
},
|
||||
BlockType::OutputM3u => BlockInstance::Output(Rc::new(TargetOutputDto::M3u(M3uTargetOutputDto::default()))),
|
||||
BlockType::OutputXtream => BlockInstance::Output(Rc::new(TargetOutputDto::Xtream(XtreamTargetOutputDto::default()))),
|
||||
BlockType::OutputHdHomeRun => BlockInstance::Output(Rc::new(TargetOutputDto::HdHomeRun(HdHomeRunTargetOutputDto::default()))),
|
||||
BlockType::OutputStrm => BlockInstance::Output(Rc::new(TargetOutputDto::Strm(StrmTargetOutputDto::default()))),
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- Component -----------------
|
||||
#[function_component]
|
||||
pub fn SourceEditor() -> Html {
|
||||
let translate = use_translation();
|
||||
let canvas_ref = use_node_ref();
|
||||
let blocks = use_state(Vec::<Block>::new);
|
||||
let connections = use_state(Vec::<Connection>::new);
|
||||
let next_id = use_state(|| 1usize);
|
||||
|
||||
// Dragging state
|
||||
let dragging_block = use_state(|| None as Option<usize>);
|
||||
let dragging_block = use_state(|| None);
|
||||
let drag_offset = use_state(|| (0.0f32, 0.0f32));
|
||||
let sidebar_drag_offset = use_state(|| (0.0f32, 0.0f32));
|
||||
|
||||
// Pending line for live connection
|
||||
let pending_line = use_state(|| None as Option<((f32, f32), (f32, f32))>);
|
||||
let pending_connection = use_state(|| None as Option<usize>);
|
||||
let pending_line = use_state(|| None);
|
||||
let pending_connection = use_state(|| None );
|
||||
|
||||
// Delete mode toggle
|
||||
let delete_mode = use_state(|| false);
|
||||
|
||||
// ----------------- Drag Start from Sidebar -----------------
|
||||
let on_drag_start = {
|
||||
let handle_drag_start = {
|
||||
let sidebar_drag_offset = sidebar_drag_offset.clone();
|
||||
Callback::from(move |e: DragEvent| {
|
||||
if let Some(target) = e.target_dyn_into::<HtmlElement>() {
|
||||
@@ -51,7 +67,7 @@ pub fn SourceEditor() -> Html {
|
||||
)};
|
||||
|
||||
// ----------------- Drop on Canvas -----------------
|
||||
let on_drop = {
|
||||
let handle_drop = {
|
||||
let blocks = blocks.clone();
|
||||
let next_id = next_id.clone();
|
||||
let canvas_ref = canvas_ref.clone();
|
||||
@@ -72,6 +88,7 @@ pub fn SourceEditor() -> Html {
|
||||
id: *next_id,
|
||||
block_type,
|
||||
position: (mouse_x - offset_x, mouse_y - offset_y),
|
||||
instance: create_instance(block_type),
|
||||
});
|
||||
blocks.set(current_blocks);
|
||||
next_id.set(*next_id + 1);
|
||||
@@ -79,24 +96,24 @@ pub fn SourceEditor() -> Html {
|
||||
}
|
||||
})
|
||||
};
|
||||
let on_drag_over = Callback::from(|e: DragEvent| e.prevent_default());
|
||||
let handle_drag_over = Callback::from(|e: DragEvent| e.prevent_default());
|
||||
|
||||
// ----------------- Connection logic -----------------
|
||||
let on_connection_start = {
|
||||
let handle_connection_start = {
|
||||
let pending_connection = pending_connection.clone();
|
||||
let pending_line = pending_line.clone();
|
||||
let blocks = blocks.clone();
|
||||
Callback::from(move |from_id: usize| {
|
||||
pending_connection.set(Some(from_id));
|
||||
if let Some(block) = (*blocks).iter().find(|b| b.id == from_id) {
|
||||
let x = block.position.0 + 100.0;
|
||||
let y = block.position.1 + 25.0;
|
||||
let x = block.position.0 + BLOCK_WIDTH;
|
||||
let y = block.position.1 + BLOCK_MIDDLE_Y;
|
||||
pending_line.set(Some(((x, y), (x, y))));
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_connection_drop = {
|
||||
let handle_connection_drop = {
|
||||
let pending_connection = pending_connection.clone();
|
||||
let connections = connections.clone();
|
||||
let pending_line = pending_line.clone();
|
||||
@@ -131,7 +148,7 @@ pub fn SourceEditor() -> Html {
|
||||
// Snap pending line end to target port
|
||||
if let Some(to_block) = (*blocks).iter().find(|b| b.id == to_id) {
|
||||
let x = to_block.position.0;
|
||||
let y = to_block.position.1 + 25.0;
|
||||
let y = to_block.position.1 + BLOCK_MIDDLE_Y;
|
||||
if let Some(((from_x, from_y), _)) = *pending_line {
|
||||
pending_line.set(Some(((from_x, from_y), (x, y))));
|
||||
}
|
||||
@@ -143,7 +160,7 @@ pub fn SourceEditor() -> Html {
|
||||
};
|
||||
|
||||
// ----------------- Drag block logic -----------------
|
||||
let on_block_mouse_down = {
|
||||
let handle_block_mouse_down = {
|
||||
let dragging_block = dragging_block.clone();
|
||||
let drag_offset = drag_offset.clone();
|
||||
let canvas_ref = canvas_ref.clone();
|
||||
@@ -163,7 +180,7 @@ pub fn SourceEditor() -> Html {
|
||||
};
|
||||
|
||||
// ----------------- Mouse move for both pending line and block drag -----------------
|
||||
let on_canvas_mouse_move = {
|
||||
let handle_canvas_mouse_move = {
|
||||
let pending_line = pending_line.clone();
|
||||
let dragging_block = dragging_block.clone();
|
||||
let drag_offset = drag_offset.clone();
|
||||
@@ -175,12 +192,12 @@ pub fn SourceEditor() -> Html {
|
||||
let mouse_x = e.client_x() as f32 - rect.left() as f32;
|
||||
let mouse_y = e.client_y() as f32 - rect.top() as f32;
|
||||
|
||||
// 1️⃣ Update pending line (snap to nearest port if close)
|
||||
// Update pending line (snap to nearest port if close)
|
||||
if let Some(((from_x, from_y), _)) = *pending_line {
|
||||
let mut snapped = (mouse_x, mouse_y);
|
||||
for block in (*blocks).iter() {
|
||||
let port_x = block.position.0;
|
||||
let port_y = block.position.1 + 25.0;
|
||||
let port_y = block.position.1 + BLOCK_MIDDLE_Y;
|
||||
let dx = mouse_x - port_x;
|
||||
let dy = mouse_y - port_y;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
@@ -191,7 +208,7 @@ pub fn SourceEditor() -> Html {
|
||||
pending_line.set(Some(((from_x, from_y), snapped)));
|
||||
}
|
||||
|
||||
// 2️⃣ Update dragging block
|
||||
// Update dragging block
|
||||
if let Some(block_id) = *dragging_block {
|
||||
let (offset_x, offset_y) = *drag_offset;
|
||||
let mut current_blocks = (*blocks).clone();
|
||||
@@ -204,14 +221,14 @@ pub fn SourceEditor() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let on_canvas_mouse_up = {
|
||||
let handle_canvas_mouse_up = {
|
||||
let dragging_block = dragging_block.clone();
|
||||
Callback::from(move |_e: MouseEvent| {
|
||||
dragging_block.set(None);
|
||||
})
|
||||
};
|
||||
|
||||
let on_canvas_right_click = {
|
||||
let handle_canvas_right_click = {
|
||||
let pending_connection = pending_connection.clone();
|
||||
let pending_line = pending_line.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
@@ -222,12 +239,12 @@ pub fn SourceEditor() -> Html {
|
||||
};
|
||||
|
||||
// ----------------- Delete handlers -----------------
|
||||
let toggle_delete_mode = {
|
||||
let handle_toggle_delete_mode = {
|
||||
let delete_mode = delete_mode.clone();
|
||||
Callback::from(move |_| delete_mode.set(!*delete_mode))
|
||||
};
|
||||
|
||||
let delete_block = {
|
||||
let handle_delete_block = {
|
||||
let blocks = blocks.clone();
|
||||
let connections = connections.clone();
|
||||
Callback::from(move |block_id: usize| {
|
||||
@@ -241,7 +258,7 @@ pub fn SourceEditor() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let delete_connection = {
|
||||
let handle_delete_connection = {
|
||||
let connections = connections.clone();
|
||||
Callback::from(move |(from, to): (usize, usize)| {
|
||||
let mut current_connections = (*connections).clone();
|
||||
@@ -250,11 +267,50 @@ pub fn SourceEditor() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let form_changed = Callback::<ConfigInputDto>::from(move |dto| error!("{dto:?}"));
|
||||
let get_port_status = {
|
||||
|block: &Block| {
|
||||
if let Some(from_id) = *pending_connection {
|
||||
if let Some(from_block) = (*blocks).iter().find(|b| b.id == from_id) {
|
||||
return if can_connect(from_block, block, &connections, &blocks) {
|
||||
PortStatus::Valid
|
||||
} else {
|
||||
PortStatus::Invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
PortStatus::Inactive
|
||||
}
|
||||
};
|
||||
|
||||
let form_changed = {
|
||||
let blocks = blocks.clone();
|
||||
Callback::<(BlockId, BlockInstance)>::from(move |(block_id, instance): (BlockId, BlockInstance)| {
|
||||
if let Some(block) = (*blocks).iter().find(|b| b.id == block_id) {
|
||||
let mut current_blocks = (*blocks).clone();
|
||||
current_blocks.retain(|b| b.id != block_id);
|
||||
let mut new_block = block.clone();
|
||||
new_block.instance = instance;
|
||||
current_blocks.push(new_block);
|
||||
blocks.set(current_blocks);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let edit_mode = use_state(|| EditMode::Inactive);
|
||||
|
||||
let handle_block_edit = {
|
||||
let edit_mode_set = edit_mode.clone();
|
||||
let blocks = blocks.clone();
|
||||
Callback::from(move |block_id: usize| {
|
||||
if let Some(block) = (*blocks).iter().find(|b| b.id == block_id) {
|
||||
edit_mode_set.set(EditMode::Active(block.clone()));
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let editor_context = SourceEditorContext {
|
||||
input: None,
|
||||
on_form_change: form_changed,
|
||||
edit_mode: edit_mode.clone(),
|
||||
};
|
||||
|
||||
// ----------------- Render -----------------
|
||||
@@ -265,28 +321,29 @@ pub fn SourceEditor() -> Html {
|
||||
|
||||
<SourceEditorSidebar
|
||||
delete_mode={*delete_mode}
|
||||
on_drag_start={on_drag_start.clone()}
|
||||
on_toggle_delete={toggle_delete_mode.clone()}
|
||||
on_drag_start={handle_drag_start.clone()}
|
||||
on_toggle_delete={handle_toggle_delete_mode.clone()}
|
||||
/>
|
||||
// Canvas
|
||||
<div class="tp__source-editor__canvas-wrapper">
|
||||
<div
|
||||
ref={canvas_ref.clone()}
|
||||
class="tp__source-editor__canvas graph-paper-advanced"
|
||||
ondrop={on_drop.clone()}
|
||||
ondragover={on_drag_over.clone()}
|
||||
onmousemove={on_canvas_mouse_move}
|
||||
onmouseup={on_canvas_mouse_up}
|
||||
oncontextmenu={on_canvas_right_click}>
|
||||
ondrop={handle_drop.clone()}
|
||||
ondragover={handle_drag_over.clone()}
|
||||
onmousemove={handle_canvas_mouse_move}
|
||||
onmouseup={handle_canvas_mouse_up}
|
||||
oncontextmenu={handle_canvas_right_click}>
|
||||
|
||||
// SVG for connections
|
||||
<svg class="tp__source-editor__connections">
|
||||
{ for (*connections).iter().filter_map(|c| {
|
||||
let from_block = (*blocks).iter().find(|b| b.id == c.from)?;
|
||||
let to_block = (*blocks).iter().find(|b| b.id == c.to)?;
|
||||
let from_x = from_block.position.0 + 100.0;
|
||||
let from_y = from_block.position.1 + 25.0;
|
||||
let from_x = from_block.position.0 + BLOCK_WIDTH;
|
||||
let from_y = from_block.position.1 + BLOCK_MIDDLE_Y;
|
||||
let to_x = to_block.position.0;
|
||||
let to_y = to_block.position.1 + 25.0;
|
||||
let to_y = to_block.position.1 + BLOCK_MIDDLE_Y;
|
||||
let dx = to_x - from_x;
|
||||
let ctrl = dx * 0.5;
|
||||
let d = format!(
|
||||
@@ -303,7 +360,7 @@ pub fn SourceEditor() -> Html {
|
||||
{ if *delete_mode {
|
||||
let mid_x = (from_x + to_x) / 2.0;
|
||||
let mid_y = (from_y + to_y) / 2.0;
|
||||
let on_delete_connection = delete_connection.clone();
|
||||
let on_delete_connection = handle_delete_connection.clone();
|
||||
html! {
|
||||
<circle cx={mid_x.to_string()} cy={mid_y.to_string()} r="6" fill="var(--source-editor-delete-color)" class="clickable"
|
||||
onclick={
|
||||
@@ -334,87 +391,21 @@ pub fn SourceEditor() -> Html {
|
||||
</svg>
|
||||
|
||||
// Render blocks
|
||||
{ for (*blocks).iter().map(|b| {
|
||||
let block_id = b.id;
|
||||
let style = format!("position:absolute; left:{}px; top:{}px;", b.position.0, b.position.1);
|
||||
let from_id = block_id;
|
||||
let to_id = block_id;
|
||||
let delete_mode = *delete_mode;
|
||||
let delete_block = delete_block.clone();
|
||||
|
||||
let is_target = matches!(b.block_type, BlockType::Target);
|
||||
let is_input = !is_target && matches!(b.block_type, BlockType::InputM3u | BlockType::InputXtream);
|
||||
let is_output = !is_input && !is_target;
|
||||
|
||||
let port_status = if let Some(from_id) = *pending_connection {
|
||||
if let Some(from_block) = (*blocks).iter().find(|b| b.id == from_id) {
|
||||
if can_connect(from_block, b, &connections, &blocks) {
|
||||
PORT_VALID
|
||||
} else {
|
||||
PORT_INVALID
|
||||
}
|
||||
} else {
|
||||
PORT_INACTIVE
|
||||
}
|
||||
} else {
|
||||
PORT_INACTIVE
|
||||
};
|
||||
|
||||
let port_style = match port_status {
|
||||
PORT_VALID => "tp__source-editor__port--valid",
|
||||
PORT_INVALID => "tp__source-editor__port--invalid",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
{ for (*blocks).iter().map(|b|{
|
||||
let port_status = get_port_status(b);
|
||||
html! {
|
||||
<div class={format!("tp__source-editor__block no-select tp__source-editor__brick-{}", b.block_type)}
|
||||
style={style}>
|
||||
// Block handle (drag)
|
||||
<div
|
||||
class="tp__source-editor__block-handle"
|
||||
onmousedown={{
|
||||
let on_block_mouse_down = on_block_mouse_down.clone();
|
||||
let block_id = b.id;
|
||||
Callback::from(move |e| on_block_mouse_down.emit((block_id, e)))
|
||||
}}>
|
||||
</div>
|
||||
|
||||
// Delete button for block
|
||||
{
|
||||
html_if!(delete_mode, {
|
||||
<div class="tp__source-editor__block-delete" onclick={
|
||||
let block_id = b.id;
|
||||
Callback::from(move |_| delete_block.emit(block_id))
|
||||
}></div>
|
||||
})
|
||||
}
|
||||
|
||||
{html_if!(is_target || is_output, {
|
||||
// Left port
|
||||
<div
|
||||
class={classes!("tp__source-editor__port", "tp__source-editor__port--left", port_style)}
|
||||
onmouseup={{
|
||||
let on_connection_drop = on_connection_drop.clone();
|
||||
Callback::from(move |_| on_connection_drop.emit(to_id))
|
||||
}} />
|
||||
})}
|
||||
// Block label
|
||||
<div class="tp__source-editor__block-label">
|
||||
{ translate.t(&format!("SOURCE_EDITOR.BRICK_{}", b.block_type)) }
|
||||
</div>
|
||||
|
||||
{html_if!(is_target || is_input, {
|
||||
// Right port
|
||||
<div
|
||||
class="tp__source-editor__port tp__source-editor__port--right"
|
||||
onmousedown={{
|
||||
let on_connection_start = on_connection_start.clone();
|
||||
Callback::from(move |_| on_connection_start.emit(from_id))
|
||||
}} />
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
}) }
|
||||
<BlockView
|
||||
block={b.clone()}
|
||||
delete_mode={*delete_mode}
|
||||
delete_block={handle_delete_block.clone()}
|
||||
port_status={port_status}
|
||||
on_edit={handle_block_edit.clone()}
|
||||
on_mouse_down={handle_block_mouse_down.clone()}
|
||||
on_connection_drop={handle_connection_drop.clone()}
|
||||
on_connection_start={handle_connection_start.clone()}
|
||||
/>
|
||||
}}) }
|
||||
</div>
|
||||
</div>
|
||||
<SourceEditorForm />
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
use crate::app::components::config::HasFormData;
|
||||
use crate::app::components::{Card, RadioButtonGroup, SourceEditorContext};
|
||||
use crate::{
|
||||
config_field_child, edit_field_bool, edit_field_number_i16, edit_field_number_u16,
|
||||
edit_field_text, edit_field_text_option, generate_form_reducer,
|
||||
};
|
||||
use shared::model::{
|
||||
ConfigInputDto, ConfigInputOptionsDto, InputFetchMethod, InputType, StagedInputDto,
|
||||
};
|
||||
use crate::app::components::select::Select;
|
||||
use crate::app::components::{BlockInstance, Card, DropDownOption, DropDownSelection, EditMode, Panel, RadioButtonGroup, SourceEditorContext, TextButton};
|
||||
use crate::{config_field_child, edit_field_bool, edit_field_number_i16, edit_field_number_u16, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use shared::model::{ConfigInputDto, ConfigInputOptionsDto, InputFetchMethod, InputType, StagedInputDto};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use yew::{
|
||||
function_component, html, use_context, use_effect_with, use_memo, use_reducer, Callback, Html,
|
||||
UseReducerHandle,
|
||||
};
|
||||
use yew::{classes, function_component, html, use_context, use_effect_with, use_memo, use_reducer, use_state, Callback, Html, Properties, UseReducerHandle};
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const LABEL_NAME: &str = "LABEL.NAME";
|
||||
const LABEL_INPUT_TYPE: &str = "LABEL.INPUT_TYPE";
|
||||
const LABEL_FETCH_METHOD: &str = "LABEL.FETCH_METHOD";
|
||||
const LABEL_FETCH_METHOD: &str = "LABEL.METHOD";
|
||||
// const LABEL_HEADERS: &str = "LABEL.HEADERS";
|
||||
const LABEL_URL: &str = "LABEL.URL";
|
||||
// const LABEL_EPG: &str = "LABEL.EPG";
|
||||
@@ -25,11 +19,9 @@ const LABEL_USERNAME: &str = "LABEL.USERNAME";
|
||||
const LABEL_PASSWORD: &str = "LABEL.PASSWORD";
|
||||
const LABEL_PERSIST: &str = "LABEL.PERSIST";
|
||||
const LABEL_ENABLED: &str = "LABEL.ENABLED";
|
||||
const LABEL_OPTIONS: &str = "LABEL.OPTIONS";
|
||||
// const LABEL_ALIASES: &str = "LABEL.ALIASES";
|
||||
const LABEL_PRIORITY: &str = "LABEL.PRIORITY";
|
||||
const LABEL_MAX_CONNECTIONS: &str = "LABEL.MAX_CONNECTIONS";
|
||||
const LABEL_STAGED: &str = "LABEL.STAGED";
|
||||
// const LABEL_ADD_HEADER: &str = "LABEL.HEADERS";
|
||||
const LABEL_XTREAM_SKIP_LIVE: &str = "LABEL.SKIP_LIVE";
|
||||
const LABEL_XTREAM_SKIP_VOD: &str = "LABEL.SKIP_VOD";
|
||||
@@ -37,6 +29,24 @@ const LABEL_XTREAM_SKIP_SERIES: &str = "LABEL.SKIP_SERIES";
|
||||
const LABEL_XTREAM_LIVE_STREAM_USE_PREFIX: &str = "LABEL.LIVE_STREAM_USE_PREFIX";
|
||||
const LABEL_XTREAM_LIVE_STREAM_WITHOUT_EXTENSION: &str = "LABEL.LIVE_STREAM_WITHOUT_EXTENSION";
|
||||
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum InputFormPage {
|
||||
Main,
|
||||
Options,
|
||||
Staged,
|
||||
}
|
||||
|
||||
impl Display for InputFormPage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{}", match *self {
|
||||
InputFormPage::Main => "Main".to_string(),
|
||||
InputFormPage::Options => "Options".to_string(),
|
||||
InputFormPage::Staged => "Staged".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// generate_form_reducer!(
|
||||
// state: EpgConfigDtoFormState { form: EpgConfigDto },
|
||||
// action_name: EpgConfigDtoFormAction,
|
||||
@@ -91,28 +101,29 @@ generate_form_reducer!(
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct ConfigInputViewProps {
|
||||
pub(crate) block_id: usize,
|
||||
pub(crate) input: Option<Rc<ConfigInputDto>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ConfigInputView() -> Html {
|
||||
pub fn ConfigInputView(props: &ConfigInputViewProps) -> Html {
|
||||
|
||||
let translate = use_translation();
|
||||
let source_editor_ctx =
|
||||
use_context::<SourceEditorContext>().expect("SourceEditorContext not found");
|
||||
let source_editor_ctx = use_context::<SourceEditorContext>().expect("SourceEditorContext not found");
|
||||
let fetch_methods = use_memo((), |_| {
|
||||
[InputFetchMethod::GET, InputFetchMethod::POST]
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
let input_types = use_memo((), |_| {
|
||||
[
|
||||
InputType::M3u,
|
||||
InputType::Xtream,
|
||||
InputType::M3uBatch,
|
||||
InputType::XtreamBatch,
|
||||
]
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
let view_visible = use_state(|| InputFormPage::Main.to_string());
|
||||
|
||||
let on_tab_click = {
|
||||
let view_visible = view_visible.clone();
|
||||
Callback::from(move |page: InputFormPage| view_visible.set(page.to_string()))
|
||||
};
|
||||
|
||||
let input_form_state: UseReducerHandle<ConfigInputFormState> =
|
||||
use_reducer(|| ConfigInputFormState {
|
||||
@@ -130,40 +141,31 @@ pub fn ConfigInputView() -> Html {
|
||||
modified: false,
|
||||
});
|
||||
|
||||
{
|
||||
let on_form_change = source_editor_ctx.on_form_change.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 staged_input_types = use_memo(staged_input_state.clone(), |staged_state| {
|
||||
let default_it = staged_state.form.input_type;
|
||||
[
|
||||
InputType::M3u,
|
||||
InputType::Xtream,
|
||||
// InputType::M3uBatch,
|
||||
// InputType::XtreamBatch,
|
||||
]
|
||||
.iter()
|
||||
.map(|t| DropDownOption {
|
||||
id: t.to_string(),
|
||||
label: html! { t.to_string() },
|
||||
selected: *t == default_it,
|
||||
}).collect::<Vec<DropDownOption>>()
|
||||
});
|
||||
|
||||
use_effect_with(
|
||||
(input_form_state, input_options_state, staged_input_state),
|
||||
move |(input_form, input_options, staged_input)| {
|
||||
let mut form = input_form.form.clone();
|
||||
form.options = if input_options.form.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(input_options.form.clone())
|
||||
};
|
||||
form.staged = if staged_input.form.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(staged_input.form.clone())
|
||||
};
|
||||
|
||||
let _modified =
|
||||
input_form.modified || input_options.modified || staged_input.modified;
|
||||
on_form_change.emit(input_form.form.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 config_input = source_editor_ctx.input.clone();
|
||||
let config_input = props.input.clone();
|
||||
|
||||
use_effect_with(config_input, move |cfg| {
|
||||
if let Some(input) = cfg {
|
||||
input_form_state.dispatch(ConfigInputFormAction::SetAll(input.as_ref().clone()));
|
||||
@@ -194,7 +196,6 @@ pub fn ConfigInputView() -> Html {
|
||||
let render_options = || {
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
<h1>{translate.t(LABEL_OPTIONS)}</h1>
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_LIVE), xtream_skip_live, ConfigInputOptionsFormAction::XtreamSkipLive) }
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_VOD), xtream_skip_vod, ConfigInputOptionsFormAction::XtreamSkipVod) }
|
||||
{ edit_field_bool!(input_options_state, translate.t(LABEL_XTREAM_SKIP_SERIES), xtream_skip_series, ConfigInputOptionsFormAction::XtreamSkipSeries) }
|
||||
@@ -205,15 +206,14 @@ pub fn ConfigInputView() -> Html {
|
||||
};
|
||||
|
||||
let render_staged = || {
|
||||
let staged_input_type_selection = Rc::new(vec![staged_input_state.form.method.to_string()]);
|
||||
let staged_method_selection = Rc::new(vec![staged_input_state.form.method.to_string()]);
|
||||
let staged_input_state_disp = staged_input_state.clone();
|
||||
let staged_input_state_1 = staged_input_state.clone();
|
||||
let staged_input_state_2 = staged_input_state.clone();
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
<h1>{translate.t(LABEL_STAGED)}</h1>
|
||||
{ edit_field_text!(staged_input_state, translate.t(LABEL_URL), url, StagedInputFormAction::Url) }
|
||||
{ edit_field_text_option!(staged_input_state, translate.t(LABEL_USERNAME), username, StagedInputFormAction::Username) }
|
||||
{ edit_field_text_option!(staged_input_state, translate.t(LABEL_PASSWORD), password, StagedInputFormAction::Password) }
|
||||
{ edit_field_text_option!(staged_input_state, translate.t(LABEL_PASSWORD), password, StagedInputFormAction::Password, true) }
|
||||
{ config_field_child!(translate.t(LABEL_FETCH_METHOD), {
|
||||
|
||||
html! {
|
||||
@@ -221,7 +221,7 @@ pub fn ConfigInputView() -> Html {
|
||||
multi_select={false} none_allowed={false}
|
||||
on_select={Callback::from(move |selections: Rc<Vec<String>>| {
|
||||
if let Some(first) = selections.first() {
|
||||
staged_input_state_disp.dispatch(StagedInputFormAction::Method(first.parse::<InputFetchMethod>().unwrap_or(InputFetchMethod::GET)));
|
||||
staged_input_state_1.dispatch(StagedInputFormAction::Method(first.parse::<InputFetchMethod>().unwrap_or(InputFetchMethod::GET)));
|
||||
}
|
||||
})}
|
||||
options={&fetch_methods}
|
||||
@@ -230,19 +230,28 @@ pub fn ConfigInputView() -> Html {
|
||||
}})}
|
||||
{ config_field_child!(translate.t(LABEL_INPUT_TYPE), {
|
||||
html! {
|
||||
<RadioButtonGroup
|
||||
multi_select={false} none_allowed={false}
|
||||
on_select={Callback::from(move |selections: Rc<Vec<String>>| {
|
||||
if let Some(first) = selections.first() {
|
||||
staged_input_state.dispatch(StagedInputFormAction::InputType(first.parse::<InputType>().unwrap_or(InputType::Xtream)));
|
||||
<Select
|
||||
name={"staged_input_types"}
|
||||
multi_select={false}
|
||||
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
|
||||
match selections {
|
||||
DropDownSelection::Empty => {
|
||||
staged_input_state_2.dispatch(StagedInputFormAction::InputType(InputType::Xtream));
|
||||
}
|
||||
DropDownSelection::Single(option) => {
|
||||
staged_input_state_2.dispatch(StagedInputFormAction::InputType(option.parse::<InputType>().unwrap_or(InputType::Xtream)));
|
||||
}
|
||||
DropDownSelection::Multi(options) => {
|
||||
if let Some(first) = options.first() {
|
||||
staged_input_state_2.dispatch(StagedInputFormAction::InputType(first.parse::<InputType>().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)) }
|
||||
</Card>
|
||||
}
|
||||
@@ -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! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ 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! {
|
||||
<RadioButtonGroup
|
||||
multi_select={false} none_allowed={false}
|
||||
on_select={Callback::from(move |selections: Rc<Vec<String>>| {
|
||||
if let Some(first) = selections.first() {
|
||||
input_form_state.dispatch(ConfigInputFormAction::InputType(first.parse::<InputType>().unwrap_or(InputType::Xtream)));
|
||||
}
|
||||
})}
|
||||
options={input_types.clone()}
|
||||
selected={input_input_type_selection}
|
||||
/>
|
||||
}})}
|
||||
// { config_field_child!(translate.t(LABEL_INPUT_TYPE), {
|
||||
// html! {
|
||||
// <RadioButtonGroup
|
||||
// multi_select={false} none_allowed={false}
|
||||
// on_select={Callback::from(move |selections: Rc<Vec<String>>| {
|
||||
// if let Some(first) = selections.first() {
|
||||
// input_form_state.dispatch(ConfigInputFormAction::InputType(first.parse::<InputType>().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<EpgConfigDto>,
|
||||
// pub aliases: Option<Vec<ConfigInputAliasDto>>,
|
||||
@@ -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! {
|
||||
<div class="tp__input-config-view__body tp__config-view-page__body">
|
||||
<div class="tp__input-form__body">
|
||||
<div class="tp__tab-header">
|
||||
{
|
||||
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! {
|
||||
<button
|
||||
class={classes!("tp__tab-button", if active { "active" } else { "" })}
|
||||
onclick={on_tab_click}
|
||||
>
|
||||
{ page_str.clone() }
|
||||
</button>
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class="tp__input-form__body__pages">
|
||||
<Panel value={InputFormPage::Main.to_string()} active={view_visible.to_string()}>
|
||||
{render_input()}
|
||||
</Panel>
|
||||
<Panel value={InputFormPage::Options.to_string()} active={view_visible.to_string()}>
|
||||
{render_options()}
|
||||
</Panel>
|
||||
<Panel value={InputFormPage::Staged.to_string()} active={view_visible.to_string()}>
|
||||
{render_staged()}
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__input-form-view tp__config-view-page">
|
||||
<div class="tp__input-form tp__config-view-page">
|
||||
<div class="tp__input-form__toolbar tp__form-page__toolbar">
|
||||
<TextButton class="primary" name="apply_input"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply_input}></TextButton>
|
||||
<TextButton class="secondary" name="cancel_input"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
</div>
|
||||
{ render_edit_mode() }
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<Rc<TargetOutputDto>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ConfigOutputView(props: &ConfigOutputViewProps) -> Html {
|
||||
html! {
|
||||
<div class="tp__output-form tp__config-view-page">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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<DragEvent>, label: String
|
||||
draggable={"true"}
|
||||
data-block-type={t.to_string()}
|
||||
ondragstart={on_drag_start}>
|
||||
|
||||
{ label }
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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<ConfigSortDto>,
|
||||
// pub rename: Option<Vec<ConfigRenameDto>>,
|
||||
// pub favourites: Option<Vec<ConfigFavouritesDto>>,
|
||||
|
||||
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<ClusterFlags>,
|
||||
}
|
||||
);
|
||||
|
||||
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<Vec<String>>,
|
||||
Watch => watch: Option<Vec<String>>,
|
||||
UseMemoryCache => use_memory_cache: bool,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct ConfigTargetViewProps {
|
||||
pub(crate) block_id: usize,
|
||||
pub(crate) target: Option<Rc<ConfigTargetDto>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html {
|
||||
let translate = use_translation();
|
||||
let source_editor_ctx = use_context::<SourceEditorContext>().expect("SourceEditorContext not found");
|
||||
|
||||
let target_form_state: UseReducerHandle<ConfigTargetFormState> =
|
||||
use_reducer(|| ConfigTargetFormState {
|
||||
form: ConfigTargetDto::default(),
|
||||
modified: false,
|
||||
});
|
||||
let target_options_state: UseReducerHandle<ConfigTargetOptionsFormState> =
|
||||
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<ConfigTargetFormState>| {
|
||||
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::<Vec<DropDownOption>>()
|
||||
});
|
||||
|
||||
{
|
||||
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! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ 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! {
|
||||
<ClusterFlagsInput
|
||||
name="force_redirect"
|
||||
value={target_options_state.form.force_redirect}
|
||||
on_change={Callback::from(move |(_name, flags):(String, Option<ClusterFlags>)| {
|
||||
target_options_state_1.dispatch(ConfigTargetOptionsFormAction::ForceRedirect(flags));
|
||||
})}
|
||||
/>
|
||||
}})}
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
let render_target = || {
|
||||
let target_form_state_1 = target_form_state.clone();
|
||||
html! {
|
||||
<Card class="tp__config-view__card">
|
||||
{ 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! {
|
||||
<Select
|
||||
name={"processing_order"}
|
||||
multi_select={false}
|
||||
on_select={Callback::from(move |(_, selections):(String, DropDownSelection)| {
|
||||
match selections {
|
||||
DropDownSelection::Empty => {
|
||||
target_form_state_1.dispatch(ConfigTargetFormAction::ProcessingOrder(ProcessingOrder::Frm));
|
||||
}
|
||||
DropDownSelection::Single(option) => {
|
||||
target_form_state_1.dispatch(ConfigTargetFormAction::ProcessingOrder(option.parse::<ProcessingOrder>().unwrap_or(ProcessingOrder::Frm)));
|
||||
}
|
||||
DropDownSelection::Multi(options) => {
|
||||
if let Some(first) = options.first() {
|
||||
target_form_state_1.dispatch(ConfigTargetFormAction::ProcessingOrder(first.parse::<ProcessingOrder>().unwrap_or(ProcessingOrder::Frm)));
|
||||
}
|
||||
}
|
||||
}
|
||||
})}
|
||||
options={processing_orders.clone()}
|
||||
/>
|
||||
}})}
|
||||
{ edit_field_list_option!(target_form_state, translate.t(LABEL_MAPPING), mapping, ConfigTargetFormAction::Mapping, translate.t(LABEL_ADD_MAPPING)) }
|
||||
{ edit_field_list_option!(target_form_state, translate.t(LABEL_WATCH), watch, ConfigTargetFormAction::Watch, translate.t(LABEL_ADD_WATCH)) }
|
||||
</Card>
|
||||
}
|
||||
};
|
||||
|
||||
let render_edit_mode = || {
|
||||
html! {
|
||||
<div class="tp__input-form__body">
|
||||
<div class="tp__tab-header">
|
||||
{
|
||||
for [
|
||||
TargetFormPage::Main,
|
||||
TargetFormPage::Options,
|
||||
].iter().map(|page| {
|
||||
let page_str = page.to_string();
|
||||
let active = *view_visible == page_str;
|
||||
let on_tab_click = {
|
||||
let on_tab_click = on_tab_click.clone();
|
||||
let page = *page;
|
||||
Callback::from(move |_| on_tab_click.emit(page))
|
||||
};
|
||||
html! {
|
||||
<button
|
||||
class={classes!("tp__tab-button", if active { "active" } else { "" })}
|
||||
onclick={on_tab_click}
|
||||
>
|
||||
{ page_str.clone() }
|
||||
</button>
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class="tp__input-form__body__pages">
|
||||
<Panel value={TargetFormPage::Main.to_string()} active={view_visible.to_string()}>
|
||||
{render_target()}
|
||||
</Panel>
|
||||
<Panel value={TargetFormPage::Options.to_string()} active={view_visible.to_string()}>
|
||||
{render_options()}
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
let handle_apply_target = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
let target_form_state = target_form_state.clone();
|
||||
let target_options_state = target_options_state.clone();
|
||||
let block_id = props.block_id;
|
||||
Callback::from(move |_| {
|
||||
let mut target = target_form_state.data().clone();
|
||||
let target_options = target_options_state.data();
|
||||
if !target_options.is_empty() {
|
||||
target.options = Some(target_options.clone());
|
||||
}
|
||||
source_editor_ctx.on_form_change.emit((block_id, BlockInstance::Target(Rc::new(target))));
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
let handle_cancel = {
|
||||
let source_editor_ctx = source_editor_ctx.clone();
|
||||
Callback::from(move |_| {
|
||||
source_editor_ctx.edit_mode.set(EditMode::Inactive);
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__target-form tp__config-view-page">
|
||||
<div class="tp__input-form__toolbar tp__form-page__toolbar">
|
||||
<TextButton class="primary" name="apply_input"
|
||||
icon="Accept"
|
||||
title={ translate.t("LABEL.OK")}
|
||||
onclick={handle_apply_target}></TextButton>
|
||||
<TextButton class="secondary" name="cancel_input"
|
||||
icon="Cancel"
|
||||
title={ translate.t("LABEL.CANCEL")}
|
||||
onclick={handle_cancel}></TextButton>
|
||||
</div>
|
||||
{ render_edit_mode() }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::rc::Rc;
|
||||
use crate::app::components::{DropDownOption, Search, TextButton, UserlistContext, UserlistPage};
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
@@ -9,9 +8,9 @@ pub fn UserlistList() -> Html {
|
||||
let translate = use_translation();
|
||||
let userlist_ctx = use_context::<UserlistContext>().expect("Userlist context not found");
|
||||
let search_fields = use_memo((), |_| vec![
|
||||
Rc::new(DropDownOption::new("username", html! { translate.t("LABEL.NAME") }, true)),
|
||||
Rc::new(DropDownOption::new("playlist", html! { translate.t("LABEL.PLAYLIST") }, false)),
|
||||
Rc::new(DropDownOption::new("server", html! { translate.t("LABEL.SERVER") }, false)),
|
||||
DropDownOption::new("username", html! { translate.t("LABEL.NAME") }, true),
|
||||
DropDownOption::new("playlist", html! { translate.t("LABEL.PLAYLIST") }, false),
|
||||
DropDownOption::new("server", html! { translate.t("LABEL.SERVER") }, false),
|
||||
]);
|
||||
|
||||
|
||||
@@ -44,7 +43,7 @@ pub fn UserlistList() -> Html {
|
||||
<div class="tp__userlist-list__header tp__list-list__header">
|
||||
<h1>{ translate.t("LABEL.USERS")}</h1>
|
||||
<div class="tp__userlist-list__header-toolbar">
|
||||
<Search min_length={1} onsearch={handle_search} options={(*search_fields).clone()}/>
|
||||
<Search min_length={1} onsearch={handle_search} options={search_fields.clone()}/>
|
||||
<TextButton class="primary" name="new_userlist"
|
||||
icon="PersonAdd"
|
||||
title={ translate.t("LABEL.NEW_USER")}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::rc::Rc;
|
||||
use crate::app::components::config::HasFormData;
|
||||
use crate::app::components::select::Select;
|
||||
use crate::app::components::userlist::proxy_type_input::ProxyTypeInput;
|
||||
use crate::app::components::{DropDownOption, DropDownSelection, TextButton, UserStatus};
|
||||
use crate::app::TargetUser;
|
||||
use crate::hooks::use_service_context;
|
||||
use crate::{config_field_child, config_field_custom, edit_field_bool, edit_field_date, edit_field_number, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use chrono::{Duration, Utc};
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::model::{ApiProxyServerInfoDto, ConfigTargetDto, ProxyType, ProxyUserCredentialsDto, ProxyUserStatus};
|
||||
use shared::utils::generate_random_string;
|
||||
use crate::app::TargetUser;
|
||||
use crate::{config_field_child, config_field_custom, edit_field_bool, edit_field_date, edit_field_number, edit_field_text, edit_field_text_option, generate_form_reducer};
|
||||
use crate::app::components::select::Select;
|
||||
use crate::app::components::{DropDownOption, TextButton, UserStatus};
|
||||
use crate::app::components::config::HasFormData;
|
||||
use crate::app::components::userlist::proxy_type_input::ProxyTypeInput;
|
||||
use crate::hooks::use_service_context;
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
const DEFAULT_MAX_CONNECTIONS: u32 = 1;
|
||||
const DEFAULT_EXPIRATION_DAYS: i64 = 365;
|
||||
@@ -59,29 +59,29 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
ProxyUserStatus::Banned,
|
||||
ProxyUserStatus::Trial,
|
||||
ProxyUserStatus::Disabled,
|
||||
ProxyUserStatus::Pending].iter().map(|s| Rc::new(DropDownOption {
|
||||
ProxyUserStatus::Pending].iter().map(|s| DropDownOption {
|
||||
id: s.to_string(),
|
||||
label: html! { <UserStatus status={Some(*s)} /> },
|
||||
selected: status.as_ref() == Some(s),
|
||||
})).collect::<Vec<Rc<DropDownOption>>>(),
|
||||
}).collect::<Vec<DropDownOption>>(),
|
||||
);
|
||||
|
||||
let targets = use_memo((props.targets.clone(), (*selected_target).clone()),
|
||||
|(targets, selected)|
|
||||
targets.iter().map(|t| Rc::new(DropDownOption {
|
||||
id: t.name.clone(),
|
||||
label: html! { t.name.clone() },
|
||||
selected: selected.as_ref().is_some_and(|ut: &String| ut == &t.name),
|
||||
})).collect::<Vec<Rc<DropDownOption>>>(),
|
||||
targets.iter().map(|t| DropDownOption {
|
||||
id: t.name.clone(),
|
||||
label: html! { t.name.clone() },
|
||||
selected: selected.as_ref().is_some_and(|ut: &String| ut == &t.name),
|
||||
}).collect::<Vec<DropDownOption>>(),
|
||||
);
|
||||
|
||||
let server = use_memo((props.server.clone(), form_state.data().server.clone()),
|
||||
|(server_list, user_server)|
|
||||
server_list.iter().map(|s| Rc::new(DropDownOption {
|
||||
id: s.name.to_string(),
|
||||
label: html! { s.name.clone() },
|
||||
selected: user_server.as_ref() == Some(&s.name),
|
||||
})).collect::<Vec<Rc<DropDownOption>>>(),
|
||||
server_list.iter().map(|s| DropDownOption {
|
||||
id: s.name.to_string(),
|
||||
label: html! { s.name.clone() },
|
||||
selected: user_server.as_ref() == Some(&s.name),
|
||||
}).collect::<Vec<DropDownOption>>(),
|
||||
);
|
||||
|
||||
{
|
||||
@@ -154,7 +154,7 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
};
|
||||
}
|
||||
} else {
|
||||
nothing_to_save();
|
||||
nothing_to_save();
|
||||
}
|
||||
} else {
|
||||
services.toastr.error(translate_clone.t("MESSAGES.SAVE.USER.TARGET_NOT_SELECTED"));
|
||||
@@ -175,27 +175,29 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
} else {*/ config_field_child!(translate.t("LABEL.PLAYLIST"), {
|
||||
html! { <Select name="target"
|
||||
multi_select={false}
|
||||
onselect={Callback::from(move |(_name, selections):(String, Vec<Rc<DropDownOption>>)| {
|
||||
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! { <Select name="status"
|
||||
multi_select={false}
|
||||
onselect={Callback::from(move |(_name, selections):(String, Vec<Rc<DropDownOption>>)| {
|
||||
if let Some(status_option) = selections.first() {
|
||||
if let Ok(status) = status_option.id.parse::<ProxyUserStatus>() {
|
||||
instance_status.dispatch(UserFormAction::Status(Some(status)));
|
||||
}
|
||||
}
|
||||
on_select={Callback::from(move |(_name, selections):(String, DropDownSelection)| {
|
||||
let status = match selections {
|
||||
DropDownSelection::Empty => None,
|
||||
DropDownSelection::Single(option) => option.parse::<ProxyUserStatus>().ok(),
|
||||
DropDownSelection::Multi(options) => options.first().as_ref().and_then(|f| f.parse::<ProxyUserStatus>().ok())
|
||||
};
|
||||
instance_status.dispatch(UserFormAction::Status(status));
|
||||
})}
|
||||
options={(*proxy_user_status).clone()}
|
||||
options={proxy_user_status.clone()}
|
||||
/>
|
||||
}})}
|
||||
{ if *update {
|
||||
@@ -218,14 +220,15 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html {
|
||||
html! {
|
||||
<Select name="server"
|
||||
multi_select={false}
|
||||
onselect={Callback::from(move |(_name, selections):(String, Vec<Rc<DropDownOption>>)| {
|
||||
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) }
|
||||
|
||||
@@ -17,6 +17,15 @@ pub struct ConfigTargetOptions {
|
||||
pub force_redirect: Option<ClusterFlags>,
|
||||
}
|
||||
|
||||
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<TargetType>,
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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<Self, Self::Err> {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user