mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-16 04:42:18 +02:00
Merge branch 'euzu:develop' into feature/remove_x_header_from_upstream_request
This commit is contained in:
@@ -386,7 +386,6 @@ async fn xtream_get_item_for_stream_id_from_memory(
|
||||
if let Some(pl_item) = item {
|
||||
let mut xc_item = pl_item.clone();
|
||||
xc_item.provider_id = mapping.provider_id;
|
||||
xc_item.item_type = PlaylistItemType::Catchup;
|
||||
Ok(xc_item)
|
||||
} else {
|
||||
Err(str_to_io_error(&format!("Failed to read xtream item for id {virtual_id}")))
|
||||
@@ -452,7 +451,6 @@ pub async fn xtream_get_item_for_stream_id(
|
||||
let cluster = try_cluster!(xtream_cluster, mapping.item_type, virtual_id)?;
|
||||
let mut item = xtream_read_item_for_stream_id(app_config, mapping.parent_virtual_id, &storage_path, cluster)?;
|
||||
item.provider_id = mapping.provider_id;
|
||||
item.item_type = PlaylistItemType::Catchup;
|
||||
Ok(item)
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -304,7 +304,8 @@
|
||||
"PROPRIETARY_DISCOVERY": "Proprietary Discovery",
|
||||
"COUNTRY": "Country",
|
||||
"GEOIP": "Geo-IP",
|
||||
"UPDATE_GEOIP_DB": "Update Geo-IP db"
|
||||
"UPDATE_GEOIP_DB": "Update Geo-IP db",
|
||||
"SOURCE_EDITOR": "Sources"
|
||||
},
|
||||
"TITLE": {
|
||||
"USER_BOUQUET_EDITOR": "User group editor"
|
||||
@@ -413,6 +414,15 @@
|
||||
"WELCOME": "Welcome to tuliprox setup. This wizard will guide you through the initial setup process."
|
||||
}
|
||||
},
|
||||
"SOURCE_EDITOR": {
|
||||
"BRICK_InputXtream": "Xtream",
|
||||
"BRICK_InputM3u": "M3u",
|
||||
"BRICK_Target": "Target",
|
||||
"BRICK_OutputM3u": "M3u",
|
||||
"BRICK_OutputXtream": "Xtream",
|
||||
"BRICK_OutputHdhomerun": "HDHR",
|
||||
"BRICK_OutputStrm": "STRM"
|
||||
},
|
||||
"COUNTRY" : {
|
||||
"Loopback": "Loopback",
|
||||
"LAN":"LAN",
|
||||
|
||||
@@ -511,5 +511,9 @@
|
||||
{
|
||||
"keys": ["Disconnect"],
|
||||
"path": "M3.42 2.36L2.01 3.78 4.1 5.87C2.79 7.57 2 9.69 2 12c0 3.7 2.01 6.92 4.99 8.65l1-1.73C5.61 17.53 4 14.96 4 12c0-1.76.57-3.38 1.53-4.69l1.43 1.44C6.36 9.68 6 10.8 6 12c0 2.22 1.21 4.15 3 5.19l1-1.74c-1.19-.7-2-1.97-2-3.45 0-.65.17-1.25.44-1.79l1.58 1.58L10 12c0 1.1.9 2 2 2l.21-.02 7.52 7.52 1.41-1.41L3.42 2.36zm14.29 11.46c.18-.57.29-1.19.29-1.82 0-3.31-2.69-6-6-6-.63 0-1.25.11-1.82.29l1.72 1.72c.03 0 .06-.01.1-.01 2.21 0 4 1.79 4 4 0 .04-.01.07-.01.11l1.72 1.71zM12 4c4.42 0 8 3.58 8 8 0 1.2-.29 2.32-.77 3.35l1.49 1.49C21.53 15.4 22 13.76 22 12c0-5.52-4.48-10-10-10-1.76 0-3.4.48-4.84 1.28l1.48 1.48C9.66 4.28 10.8 4 12 4z"
|
||||
},
|
||||
{
|
||||
"keys": ["Nodes", "SourceEditor"],
|
||||
"path": "M15 16v1.26l-6-3v-3.17L11.7 8H16V2h-6v4.9L7.3 10H3v6h5l7 3.5V22h6v-6z"
|
||||
}
|
||||
]
|
||||
@@ -92,7 +92,20 @@ li {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tp__full-height {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--hint-color);
|
||||
}
|
||||
|
||||
.no-select {
|
||||
user-select: none; /* Standard */
|
||||
-webkit-user-select: none; /* Chrome, Safari, Opera */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE/Edge */
|
||||
}
|
||||
@@ -319,6 +319,60 @@
|
||||
--floating-particle-gradient-color-1: rgba(255,255,255,0.3);
|
||||
--floating-particle-gradient-color-2: rgba(255,255,255,0);
|
||||
|
||||
|
||||
/* INPUT BLOCKS — cool blues */
|
||||
--brick-input-border-color: #4a90e2cc;
|
||||
--brick-input-background-color: #4a90e255;
|
||||
--brick-input-shadow-color: #4a90e280;
|
||||
|
||||
/* TARGET BLOCKS — balanced green/teal */
|
||||
--brick-target-border-color: #2aa198cc; /* Solarized teal */
|
||||
--brick-target-background-color: #2aa19855;
|
||||
--brick-target-shadow-color: #2aa19880;
|
||||
|
||||
/* GENERIC OUTPUT — warm amber */
|
||||
--brick-output-border-color: #f39c12cc;
|
||||
--brick-output-background-color: #f39c1255;
|
||||
--brick-output-shadow-color: #f39c1280;
|
||||
|
||||
/* INDIVIDUAL OUTPUT TYPES — distinct but cohesive */
|
||||
--brick-output-xtream-border-color: #0099ffcc; /* vivid sky blue */
|
||||
--brick-output-xtream-background-color: #0099ff44;
|
||||
--brick-output-xtream-shadow-color: #0099ff80;
|
||||
|
||||
--brick-output-m3u-border-color: #8bc34acc; /* light green */
|
||||
--brick-output-m3u-background-color: #8bc34a44;
|
||||
--brick-output-m3u-shadow-color: #8bc34a80;
|
||||
|
||||
--brick-output-hdhomerun-border-color: #9b59b6cc; /* purple */
|
||||
--brick-output-hdhomerun-background-color: #9b59b655;
|
||||
--brick-output-hdhomerun-shadow-color: #9b59b680;
|
||||
|
||||
--brick-output-strm-border-color: #e74c3ccc; /* strong red */
|
||||
--brick-output-strm-background-color: #e74c3c55;
|
||||
--brick-output-strm-shadow-color: #e74c3c80;
|
||||
|
||||
//--brick-input-border-color: #569cd6aa;
|
||||
//--brick-input-background-color: #569cd688;
|
||||
//--brick-target-border-color: #65a181aa;
|
||||
//--brick-target-background-color: #65a18188;
|
||||
//--brick-output-border-color: #FF9D23aa;
|
||||
//--brick-output-background-color: #FF9D2388;
|
||||
//
|
||||
//--brick-output-xtream-border-color: #06b6d4aa;
|
||||
//--brick-output-xtream-background-color: #06b6d488;
|
||||
//--brick-output-m3u-border-color: #10b981aa;
|
||||
//--brick-output-m3u-background-color: #10b98188;
|
||||
//--brick-output-hdhomerun-border-color: #6366f1aa;
|
||||
//--brick-output-hdhomerun-background-color: #6366f188;
|
||||
//--brick-output-strm-border-color: #f43f5eaa;
|
||||
//--brick-output-strm-background-color: #f43f5e88;
|
||||
|
||||
--brick-port-background-color: #486786;
|
||||
--brick-port-valid-background-color: #2ecc71;
|
||||
--brick-port-invalid-background-color: #e74c3c;
|
||||
--source-editor-delete-color: #FF0000FF;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
padding: 0 var(--padding-larger);
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,3 +75,4 @@
|
||||
@forward "components/websocket_status";
|
||||
@forward "components/playlist/epg_view";
|
||||
@forward "components/floating_background";
|
||||
@forward "components/source_editor";
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
.tp__source-editor {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: var(--gap-default);
|
||||
|
||||
&__sidebar {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
gap: var(--gap-small);
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
box-sizing: border-box;
|
||||
gap: var(--gap-small);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
border-radius: 0;
|
||||
|
||||
&-active {
|
||||
color: var(--text-button-active-color);
|
||||
background-color: var(--text-button-active-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
&-bricks {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
gap: var(--gap-large);
|
||||
|
||||
&-group {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
gap: var(--gap-small);
|
||||
}
|
||||
|
||||
.tp__source-editor__brick {
|
||||
border: 1px solid var(--border-color);
|
||||
cursor: grab;
|
||||
padding: var(--padding-small) var(--padding-default);
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__canvas {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.graph-paper-advanced {
|
||||
background-image:
|
||||
linear-gradient(#e0e0e060 1px, transparent 1px),
|
||||
linear-gradient(90deg, #e0e0e060 1px, transparent 1px),
|
||||
linear-gradient(#c0c0c044 2px, transparent 2px), /* horizontal thick */
|
||||
linear-gradient(90deg, #c0c0c044 2px, transparent 2px); /* vertical thick */
|
||||
background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -158,7 +158,7 @@ pub fn StreamsTable(props: &StreamsTableProps) -> Html {
|
||||
"CHANNEL" => html! {dto.channel.title.as_str()},
|
||||
"GROUP" => html! {dto.channel.group.as_str()},
|
||||
"CLIENT_IP" => html! { strip_port(&dto.client_ip)},
|
||||
"COUNTRY" => html! { dto.country.as_ref().map_or_else(String::new, |c| t_safe(&translate, &format!("COUNTRY.{c}"))) },
|
||||
"COUNTRY" => html! { dto.country.as_ref().map_or_else(String::new, |c| t_safe(&translate, &format!("COUNTRY.{c}")).unwrap_or_else(||c.to_string())) },
|
||||
"PROVIDER" => html! {dto.provider.as_str()},
|
||||
"SHARED" => html! { <ToggleSwitch value={dto.channel.shared} readonly={true} /> },
|
||||
"USER_AGENT" => html! { dto.user_agent.as_str() },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::app::components::{AppIcon, DashboardView, EpgView, IconButton, InputRow, Panel, PlaylistEditorView, PlaylistExplorerView, PlaylistUpdateView, Sidebar, StatsView, StreamsView, ToastrView, UserlistView, WebsocketStatus};
|
||||
use crate::app::components::{AppIcon, DashboardView, EpgView, IconButton, InputRow, Panel, PlaylistEditorView, PlaylistExplorerView, PlaylistUpdateView, Sidebar, SourceEditor, StatsView, StreamsView, ToastrView, UserlistView, WebsocketStatus};
|
||||
use crate::app::context::{ConfigContext, PlaylistContext, StatusContext};
|
||||
use crate::hooks::{use_server_status, use_service_context};
|
||||
use crate::model::{EventMessage, ViewType};
|
||||
@@ -8,7 +8,7 @@ use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use yew::suspense::use_future;
|
||||
use yew_i18n::use_translation;
|
||||
use crate::app::components::config::ConfigView;
|
||||
use crate::app::components::config::{ConfigView};
|
||||
use crate::app::components::loading_indicator::{BusyIndicator};
|
||||
use crate::provider::DialogProvider;
|
||||
use crate::services::{ToastCloseMode, ToastOptions};
|
||||
@@ -181,6 +181,9 @@ pub fn Home() -> Html {
|
||||
<Panel class="tp__full-width" value={ViewType::Config.to_string()} active={view_visible.to_string()}>
|
||||
<ConfigView/>
|
||||
</Panel>
|
||||
<Panel class="tp__full-width tp__full-height" value={ViewType::SourceEditor.to_string()} active={view_visible.to_string()}>
|
||||
<SourceEditor/>
|
||||
</Panel>
|
||||
<Panel class="tp__full-width" value={ViewType::PlaylistUpdate.to_string()} active={view_visible.to_string()}>
|
||||
<PlaylistUpdateView/>
|
||||
</Panel>
|
||||
|
||||
@@ -47,7 +47,7 @@ mod theme;
|
||||
mod websocket_status;
|
||||
|
||||
mod floating_background;
|
||||
|
||||
mod source_editor;
|
||||
// pub use self::input::*;
|
||||
// pub use self::menu_item::*;
|
||||
// pub use self::popup_menu::*;
|
||||
@@ -89,5 +89,7 @@ pub use self::cell_value::*;
|
||||
pub use self::key_value_editor::*;
|
||||
pub use self::loading_screen::*;
|
||||
pub use self::websocket_status::*;
|
||||
pub use self::source_editor::*;
|
||||
|
||||
//pub use self::number_input::*;
|
||||
//pub use self::date_input::*;
|
||||
|
||||
@@ -139,6 +139,7 @@ pub fn Sidebar(props: &SidebarProps) -> Html {
|
||||
<CollapsePanel title={translate.t("LABEL.SETTINGS")}>
|
||||
<MenuItem class={if *active_menu == ViewType::Config { "active" } else {""}} icon="Config" name={ViewType::Config.to_string()} label={translate.t("LABEL.CONFIG")} onclick={&handle_menu_click}></MenuItem>
|
||||
<MenuItem class={if *active_menu == ViewType::Users { "active" } else {""}} icon="UserOutline" name={ViewType::Users.to_string()} label={translate.t("LABEL.USER")} onclick={&handle_menu_click}></MenuItem>
|
||||
<MenuItem class={if *active_menu == ViewType::SourceEditor { "active" } else {""}} icon="SourceEditor" name={ViewType::SourceEditor.to_string()} label={translate.t("LABEL.SOURCE_EDITOR")} onclick={&handle_menu_click}></MenuItem>
|
||||
</CollapsePanel>
|
||||
<CollapsePanel title={translate.t("LABEL.PLAYLIST")}>
|
||||
<MenuItem class={if *active_menu == ViewType::PlaylistUpdate { "active" } else {""}} icon="Refresh" name={ViewType::PlaylistUpdate.to_string()} label={translate.t("LABEL.UPDATE")} onclick={&handle_menu_click}></MenuItem>
|
||||
@@ -159,6 +160,7 @@ pub fn Sidebar(props: &SidebarProps) -> Html {
|
||||
<span class="tp__app-sidebar__content-space"></span>
|
||||
<IconButton class={if *active_menu == ViewType::Config { "active" } else {""}} icon="Config" name={ViewType::Config.to_string()} onclick={&handle_menu_click}></IconButton>
|
||||
<IconButton class={if *active_menu == ViewType::Users { "active" } else {""}} icon="UserOutline" name={ViewType::Users.to_string()} onclick={&handle_menu_click}></IconButton>
|
||||
<IconButton class={if *active_menu == ViewType::SourceEditor { "active" } else {""}} icon="SourceEditor" name={ViewType::SourceEditor.to_string()} onclick={&handle_menu_click}></IconButton>
|
||||
<span class="tp__app-sidebar__content-space"></span>
|
||||
<IconButton class={if *active_menu == ViewType::PlaylistUpdate { "active" } else {""}} icon="Refresh" name={ViewType::PlaylistUpdate.to_string()} onclick={&handle_menu_click}></IconButton>
|
||||
<IconButton class={if *active_menu == ViewType::PlaylistEditor { "active" } else {""}} icon="PlayArrowOutline" name={ViewType::PlaylistEditor.to_string()} onclick={&handle_menu_click}></IconButton>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
use std::fmt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ----------------- Data Models -----------------
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum BlockType {
|
||||
InputXtream,
|
||||
InputM3u,
|
||||
Target,
|
||||
OutputM3u,
|
||||
OutputXtream,
|
||||
OutputHdhomerun,
|
||||
OutputStrm,
|
||||
}
|
||||
|
||||
// Define string constants
|
||||
impl BlockType {
|
||||
pub const INPUT_XTREAM: &'static str = "InputXtream";
|
||||
pub const INPUT_M3U: &'static str = "InputM3u";
|
||||
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_STRM: &'static str = "OutputStrm";
|
||||
}
|
||||
|
||||
// Convert from String to BlockType
|
||||
impl From<&str> for BlockType {
|
||||
fn from(s: &str) -> Self {
|
||||
match s {
|
||||
BlockType::INPUT_XTREAM => BlockType::InputXtream,
|
||||
BlockType::INPUT_M3U => BlockType::InputM3u,
|
||||
BlockType::TARGET => BlockType::Target,
|
||||
BlockType::OUTPUT_M3U => BlockType::OutputM3u,
|
||||
BlockType::OUTPUT_XTREAM => BlockType::OutputXtream,
|
||||
BlockType::OUTPUT_HDHOMERUN => BlockType::OutputHdhomerun,
|
||||
BlockType::OUTPUT_STRM => BlockType::OutputStrm,
|
||||
_ => BlockType::Target, // fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for BlockType {
|
||||
fn from(s: String) -> Self {
|
||||
BlockType::from(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Display trait using constants
|
||||
impl fmt::Display for BlockType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
BlockType::InputXtream => Self::INPUT_XTREAM,
|
||||
BlockType::InputM3u => Self::INPUT_M3U,
|
||||
BlockType::Target => Self::TARGET,
|
||||
BlockType::OutputM3u => Self::OUTPUT_M3U,
|
||||
BlockType::OutputXtream => Self::OUTPUT_XTREAM,
|
||||
BlockType::OutputHdhomerun => Self::OUTPUT_HDHOMERUN,
|
||||
BlockType::OutputStrm => Self::OUTPUT_STRM,
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Block {
|
||||
pub id: usize,
|
||||
pub block_type: BlockType,
|
||||
pub position: (f32, f32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Connection {
|
||||
pub from: usize,
|
||||
pub to: usize,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod source_editor;
|
||||
mod editor_model;
|
||||
mod sidebar;
|
||||
mod rules;
|
||||
|
||||
pub use source_editor::*;
|
||||
pub(crate) use editor_model::*;
|
||||
@@ -0,0 +1,82 @@
|
||||
use crate::app::components::{Block, BlockType, Connection};
|
||||
|
||||
/// Determines whether two blocks can be connected based on explicit editor rules.
|
||||
/// Allowed: Input → Target, Target → Output.
|
||||
/// Target can have multiple Inputs.
|
||||
/// Output can only have one Target.
|
||||
/// Target can connect to:
|
||||
/// - 1x OutputM3u
|
||||
/// - 1x OutputXtream
|
||||
/// - 1x OutputHdhomerun
|
||||
/// - up to 4x OutputStrm
|
||||
pub fn can_connect(from_block: &Block, to_block: &Block, connections: &[Connection], blocks: &[Block]) -> bool {
|
||||
// 1️⃣ Prevent self-connection
|
||||
if from_block.id == to_block.id {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2️⃣ Identify block categories
|
||||
let is_input = matches!(from_block.block_type, BlockType::InputM3u | BlockType::InputXtream);
|
||||
let is_target = matches!(from_block.block_type, BlockType::Target);
|
||||
let to_is_target = matches!(to_block.block_type, BlockType::Target);
|
||||
let to_is_output = matches!(
|
||||
to_block.block_type,
|
||||
BlockType::OutputM3u
|
||||
| BlockType::OutputXtream
|
||||
| BlockType::OutputHdhomerun
|
||||
| BlockType::OutputStrm
|
||||
);
|
||||
|
||||
// 3️⃣ Only allow Input → Target OR Target → Output
|
||||
let valid_direction = (is_input && to_is_target) || (is_target && to_is_output);
|
||||
if !valid_direction {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4️⃣ Prevent duplicate connection
|
||||
if connections.iter().any(|c| c.from == from_block.id && c.to == to_block.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5️⃣ Output can have only one incoming connection
|
||||
if to_is_output {
|
||||
let has_input_already = connections.iter().any(|c| c.to == to_block.id);
|
||||
if has_input_already {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 6️⃣ Per-target output type limits
|
||||
if is_target && to_is_output {
|
||||
let from_id = from_block.id;
|
||||
|
||||
// Count how many connections this Target already has to each output type
|
||||
let mut count_m3u = 0;
|
||||
let mut count_xtream = 0;
|
||||
let mut count_hdhomerun = 0;
|
||||
let mut count_strm = 0;
|
||||
|
||||
for conn in connections.iter().filter(|c| c.from == from_id) {
|
||||
if let Some(out_block) = blocks.iter().find(|b| b.id == conn.to) {
|
||||
match out_block.block_type {
|
||||
BlockType::OutputM3u => count_m3u += 1,
|
||||
BlockType::OutputXtream => count_xtream += 1,
|
||||
BlockType::OutputHdhomerun => count_hdhomerun += 1,
|
||||
BlockType::OutputStrm => count_strm += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::OutputStrm if count_strm >= 4 => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Passed all checks
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use crate::app::components::{BlockType, CollapsePanel, IconButton};
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
|
||||
|
||||
pub const BLOCK_TYPES_INPUT: [BlockType; 2] = [
|
||||
BlockType::InputXtream,
|
||||
BlockType::InputM3u
|
||||
];
|
||||
|
||||
pub const BLOCK_TYPES_TARGET: [BlockType; 1] = [
|
||||
BlockType::Target,
|
||||
];
|
||||
|
||||
pub const BLOCK_TYPES_OUTPUT: [BlockType; 4] = [
|
||||
BlockType::OutputM3u,
|
||||
BlockType::OutputXtream,
|
||||
BlockType::OutputHdhomerun,
|
||||
BlockType::OutputStrm];
|
||||
|
||||
|
||||
fn create_brick(t: &BlockType, on_drag_start: Callback<DragEvent>, label: String) -> Html {
|
||||
html! {
|
||||
<div class={format!("tp__source-editor__brick tp__source-editor__brick-{t}")}
|
||||
draggable={"true"}
|
||||
data-block-type={t.to_string()}
|
||||
ondragstart={on_drag_start}>
|
||||
{ label }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct SourceEditorSidebarProps {
|
||||
#[prop_or_default]
|
||||
pub delete_mode: bool,
|
||||
#[prop_or_default]
|
||||
pub on_toggle_delete: Callback<(String, MouseEvent)>,
|
||||
#[prop_or_default]
|
||||
pub on_drag_start: Callback<DragEvent>,
|
||||
}
|
||||
|
||||
|
||||
#[function_component]
|
||||
pub fn SourceEditorSidebar(props: &SourceEditorSidebarProps) -> Html {
|
||||
let translate = use_translation();
|
||||
|
||||
html! {
|
||||
// Sidebar
|
||||
<div class="tp__source-editor__sidebar">
|
||||
<div class="tp__source-editor__sidebar-actions">
|
||||
// Delete mode toggle button
|
||||
<IconButton class={if props.delete_mode {"tp__source-editor__sidebar-actions-active"} else {""} } name="toggle_delete" icon="Delete" onclick={props.on_toggle_delete.clone()} />
|
||||
</div>
|
||||
<div class="tp__source-editor__sidebar-bricks">
|
||||
<CollapsePanel title={translate.t("LABEL.INPUTS")}>
|
||||
<div class="tp__source-editor__sidebar-bricks-group">
|
||||
{for BLOCK_TYPES_INPUT.iter().map(|t| create_brick(t, props.on_drag_start.clone(), translate.t(&format!("SOURCE_EDITOR.BRICK_{t}"))))}
|
||||
</div>
|
||||
</CollapsePanel>
|
||||
<CollapsePanel title={translate.t("LABEL.TARGETS")}>
|
||||
<div class="tp__source-editor__sidebar-bricks-group">
|
||||
{for BLOCK_TYPES_TARGET.iter().map(|t| create_brick(t, props.on_drag_start.clone(), translate.t(&format!("SOURCE_EDITOR.BRICK_{t}"))))}
|
||||
</div>
|
||||
</CollapsePanel>
|
||||
<CollapsePanel title={translate.t("LABEL.OUTPUT")}>
|
||||
<div class="tp__source-editor__sidebar-bricks-group">
|
||||
{for BLOCK_TYPES_OUTPUT.iter().map(|t| create_brick(t, props.on_drag_start.clone(), translate.t(&format!("SOURCE_EDITOR.BRICK_{t}"))))}
|
||||
</div>
|
||||
</CollapsePanel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
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;
|
||||
|
||||
const PORT_INACTIVE: u32 = 0;
|
||||
const PORT_VALID: u32 = 1;
|
||||
const PORT_INVALID: u32 = 2;
|
||||
|
||||
// ----------------- 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 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>);
|
||||
|
||||
// Delete mode toggle
|
||||
let delete_mode = use_state(|| false);
|
||||
|
||||
// ----------------- Drag Start from Sidebar -----------------
|
||||
let on_drag_start = {
|
||||
let sidebar_drag_offset = sidebar_drag_offset.clone();
|
||||
Callback::from(move |e: DragEvent| {
|
||||
if let Some(target) = e.target_dyn_into::<HtmlElement>() {
|
||||
let block_type = target.get_attribute("data-block-type").unwrap_or_default();
|
||||
e.data_transfer().unwrap().set_data("text/plain", &block_type).unwrap();
|
||||
// Store mouse offset inside the element
|
||||
let rect = target.get_bounding_client_rect();
|
||||
let offset_x = e.client_x() as f32 - rect.left() as f32;
|
||||
let offset_y = e.client_y() as f32 - rect.top() as f32;
|
||||
sidebar_drag_offset.set((offset_x, offset_y));
|
||||
}
|
||||
}
|
||||
)};
|
||||
|
||||
// ----------------- Drop on Canvas -----------------
|
||||
let on_drop = {
|
||||
let blocks = blocks.clone();
|
||||
let next_id = next_id.clone();
|
||||
let canvas_ref = canvas_ref.clone();
|
||||
let sidebar_drag_offset = sidebar_drag_offset.clone();
|
||||
Callback::from(move |e: DragEvent| {
|
||||
e.prevent_default();
|
||||
if let Some(canvas) = canvas_ref.cast::<HtmlElement>() {
|
||||
if let Ok(data) = e.data_transfer().unwrap().get_data("text/plain") {
|
||||
let rect = canvas.get_bounding_client_rect();
|
||||
let mouse_x = e.client_x() as f32 - rect.left() as f32;
|
||||
let mouse_y = e.client_y() as f32 - rect.top() as f32;
|
||||
let (offset_x, offset_y) = *sidebar_drag_offset;
|
||||
|
||||
let block_type = BlockType::from(data.as_str());
|
||||
|
||||
let mut current_blocks = (*blocks).clone();
|
||||
current_blocks.push(Block {
|
||||
id: *next_id,
|
||||
block_type,
|
||||
position: (mouse_x - offset_x, mouse_y - offset_y),
|
||||
});
|
||||
blocks.set(current_blocks);
|
||||
next_id.set(*next_id + 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let on_drag_over = Callback::from(|e: DragEvent| e.prevent_default());
|
||||
|
||||
// ----------------- Connection logic -----------------
|
||||
let on_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;
|
||||
pending_line.set(Some(((x, y), (x, y))));
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_connection_drop = {
|
||||
let pending_connection = pending_connection.clone();
|
||||
let connections = connections.clone();
|
||||
let pending_line = pending_line.clone();
|
||||
let blocks = blocks.clone();
|
||||
Callback::from(move |to_id: usize| {
|
||||
if let Some(from_id) = *pending_connection {
|
||||
if from_id != to_id {
|
||||
let current_blocks = (*blocks).clone();
|
||||
if let (Some(from_block), Some(to_block)) = (
|
||||
current_blocks.iter().find(|b| b.id == from_id),
|
||||
current_blocks.iter().find(|b| b.id == to_id),
|
||||
) {
|
||||
// ✅ Check connection rules before adding
|
||||
if can_connect(from_block, to_block, &connections, &blocks) {
|
||||
let mut current_connections = (*connections).clone();
|
||||
current_connections.push(Connection { from: from_id, to: to_id });
|
||||
connections.set(current_connections);
|
||||
} else {
|
||||
// ❌ (Optional) visual feedback
|
||||
web_sys::console::log_1(
|
||||
&format!(
|
||||
"Connection from {:?} to {:?} not allowed",
|
||||
from_block.block_type, to_block.block_type
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
if let Some(((from_x, from_y), _)) = *pending_line {
|
||||
pending_line.set(Some(((from_x, from_y), (x, y))));
|
||||
}
|
||||
}
|
||||
|
||||
pending_connection.set(None);
|
||||
pending_line.set(None);
|
||||
})
|
||||
};
|
||||
|
||||
// ----------------- Drag block logic -----------------
|
||||
let on_block_mouse_down = {
|
||||
let dragging_block = dragging_block.clone();
|
||||
let drag_offset = drag_offset.clone();
|
||||
let canvas_ref = canvas_ref.clone();
|
||||
let blocks = blocks.clone();
|
||||
Callback::from(move |(block_id, e): (usize, MouseEvent)| {
|
||||
e.prevent_default();
|
||||
if let Some(canvas) = canvas_ref.cast::<HtmlElement>() {
|
||||
let rect = canvas.get_bounding_client_rect();
|
||||
let mouse_x = e.client_x() as f32 - rect.left() as f32;
|
||||
let mouse_y = e.client_y() as f32 - rect.top() as f32;
|
||||
if let Some(block) = (*blocks).iter().find(|b| b.id == block_id) {
|
||||
drag_offset.set((mouse_x - block.position.0, mouse_y - block.position.1));
|
||||
dragging_block.set(Some(block_id));
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// ----------------- Mouse move for both pending line and block drag -----------------
|
||||
let on_canvas_mouse_move = {
|
||||
let pending_line = pending_line.clone();
|
||||
let dragging_block = dragging_block.clone();
|
||||
let drag_offset = drag_offset.clone();
|
||||
let blocks = blocks.clone();
|
||||
let canvas_ref = canvas_ref.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
if let Some(canvas) = canvas_ref.cast::<HtmlElement>() {
|
||||
let rect = canvas.get_bounding_client_rect();
|
||||
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)
|
||||
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 dx = mouse_x - port_x;
|
||||
let dy = mouse_y - port_y;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
if dist < 10.0 { // Snap distance threshold
|
||||
snapped = (port_x, port_y);
|
||||
}
|
||||
}
|
||||
pending_line.set(Some(((from_x, from_y), snapped)));
|
||||
}
|
||||
|
||||
// 2️⃣ Update dragging block
|
||||
if let Some(block_id) = *dragging_block {
|
||||
let (offset_x, offset_y) = *drag_offset;
|
||||
let mut current_blocks = (*blocks).clone();
|
||||
if let Some(block) = current_blocks.iter_mut().find(|b| b.id == block_id) {
|
||||
block.position = (mouse_x - offset_x, mouse_y - offset_y);
|
||||
}
|
||||
blocks.set(current_blocks);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let on_canvas_mouse_up = {
|
||||
let dragging_block = dragging_block.clone();
|
||||
Callback::from(move |_e: MouseEvent| {
|
||||
dragging_block.set(None);
|
||||
})
|
||||
};
|
||||
|
||||
let on_canvas_right_click = {
|
||||
let pending_connection = pending_connection.clone();
|
||||
let pending_line = pending_line.clone();
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.prevent_default(); // prevent default browser context menu
|
||||
pending_connection.set(None);
|
||||
pending_line.set(None);
|
||||
})
|
||||
};
|
||||
|
||||
// ----------------- Delete handlers -----------------
|
||||
let toggle_delete_mode = {
|
||||
let delete_mode = delete_mode.clone();
|
||||
Callback::from(move |_| delete_mode.set(!*delete_mode))
|
||||
};
|
||||
|
||||
let delete_block = {
|
||||
let blocks = blocks.clone();
|
||||
let connections = connections.clone();
|
||||
Callback::from(move |block_id: usize| {
|
||||
let mut current_blocks = (*blocks).clone();
|
||||
current_blocks.retain(|b| b.id != block_id);
|
||||
blocks.set(current_blocks);
|
||||
|
||||
let mut current_connections = (*connections).clone();
|
||||
current_connections.retain(|c| c.from != block_id && c.to != block_id);
|
||||
connections.set(current_connections);
|
||||
})
|
||||
};
|
||||
|
||||
let delete_connection = {
|
||||
let connections = connections.clone();
|
||||
Callback::from(move |(from, to): (usize, usize)| {
|
||||
let mut current_connections = (*connections).clone();
|
||||
current_connections.retain(|c| !(c.from == from && c.to == to));
|
||||
connections.set(current_connections);
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
// ----------------- Render -----------------
|
||||
html! {
|
||||
<>
|
||||
<span>{"WORK IN PROGRESS - NOT FINALIZED !!!"}</span>
|
||||
<div class="tp__source-editor">
|
||||
|
||||
<SourceEditorSidebar
|
||||
delete_mode={*delete_mode}
|
||||
on_drag_start={on_drag_start.clone()}
|
||||
on_toggle_delete={toggle_delete_mode.clone()}
|
||||
/>
|
||||
// Canvas
|
||||
<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}>
|
||||
|
||||
// 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 to_x = to_block.position.0;
|
||||
let to_y = to_block.position.1 + 25.0;
|
||||
let dx = to_x - from_x;
|
||||
let ctrl = dx * 0.5;
|
||||
let d = format!(
|
||||
"M {} {} C {} {}, {} {}, {} {}",
|
||||
from_x, from_y,
|
||||
from_x + ctrl, from_y,
|
||||
to_x - ctrl, to_y,
|
||||
to_x, to_y
|
||||
);
|
||||
|
||||
Some(html! {
|
||||
<g>
|
||||
<path d={d} stroke="white" fill="transparent" stroke-width="2"/>
|
||||
{ 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();
|
||||
html! {
|
||||
<circle cx={mid_x.to_string()} cy={mid_y.to_string()} r="6" fill="var(--source-editor-delete-color)" class="clickable"
|
||||
onclick={
|
||||
let from = c.from;
|
||||
let to = c.to;
|
||||
Callback::from(move |_| on_delete_connection.emit((from, to)))
|
||||
}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
html!{}
|
||||
} }
|
||||
</g>
|
||||
})
|
||||
}) }
|
||||
|
||||
// Pending line (straight, yellow)
|
||||
{ if let Some(((x1, y1), (x2, y2))) = *pending_line {
|
||||
html! {
|
||||
<line
|
||||
x1={x1.to_string()} y1={y1.to_string()}
|
||||
x2={x2.to_string()} y2={y2.to_string()}
|
||||
stroke="yellow"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="4 2" />
|
||||
}
|
||||
} else { 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",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
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>
|
||||
}
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ const PLAYLIST_UPDATE: &str = "playlist_update";
|
||||
const PLAYLIST_EDITOR: &str = "playlist_editor";
|
||||
const PLAYLIST_EXPLORER: &str = "playlist_explorer";
|
||||
const PLAYLIST_EPG: &str = "playlist_epg";
|
||||
const SOURCE_EDITOR: &str = "source_editor";
|
||||
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -20,6 +21,7 @@ pub enum ViewType {
|
||||
Streams,
|
||||
Users,
|
||||
Config,
|
||||
SourceEditor,
|
||||
PlaylistUpdate,
|
||||
PlaylistEditor,
|
||||
PlaylistExplorer,
|
||||
@@ -36,6 +38,7 @@ impl FromStr for ViewType {
|
||||
STREAMS => Ok(ViewType::Streams),
|
||||
USERS => Ok(ViewType::Users),
|
||||
CONFIG => Ok(ViewType::Config),
|
||||
SOURCE_EDITOR => Ok(ViewType::SourceEditor),
|
||||
PLAYLIST_UPDATE => Ok(ViewType::PlaylistUpdate),
|
||||
PLAYLIST_EDITOR => Ok(ViewType::PlaylistEditor),
|
||||
PLAYLIST_EXPLORER => Ok(ViewType::PlaylistExplorer),
|
||||
@@ -53,6 +56,7 @@ impl fmt::Display for ViewType {
|
||||
ViewType::Streams => STREAMS,
|
||||
ViewType::Users => USERS,
|
||||
ViewType::Config => CONFIG,
|
||||
ViewType::SourceEditor => SOURCE_EDITOR,
|
||||
ViewType::PlaylistUpdate => PLAYLIST_UPDATE,
|
||||
ViewType::PlaylistEditor => PLAYLIST_EDITOR,
|
||||
ViewType::PlaylistExplorer => PLAYLIST_EXPLORER,
|
||||
|
||||
@@ -33,12 +33,12 @@ where
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn t_safe(i18n: &YewI18n, key: &str) -> String {
|
||||
pub fn t_safe(i18n: &YewI18n, key: &str) -> Option<String> {
|
||||
let result = i18n.t(key);
|
||||
|
||||
if result.starts_with("Unable to find the key") {
|
||||
key.to_string()
|
||||
None
|
||||
} else {
|
||||
result
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user