mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-26 08:42:16 +01:00
Merge pull request #414 from euzu/feature/multi_strm_output
New Features - Support for multiple stream outputs of the same type - New pad() function for text/number padding with configurable alignment - New format() function for simple text placeholder substitution Editor - Smoother block positioning, drag and edit behaviors
This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
- More robust connection-state and provider-handle management
|
||||
- Streamlined event notifications and provider-count reporting
|
||||
- Added configurable `reverse_proxy.resource_retry` (UI + server) to tune max attempts, base delay, and exponential backoff multiplier for proxied resources.
|
||||
- Multi Strm outputs with same type is now allowed.
|
||||
- Added new mapper function `pad(text | number, number, char, optional position: "<" | ">" | "^")`
|
||||
- Added new mapper function `format` for simple in-text replacement like `format("Hello {}! Hello {}!", "Bob", "World")`
|
||||
|
||||
# 3.1.8 (2025-11-06)
|
||||
- Fixed HLS streaming issues caused by session eviction and incorrect headers.
|
||||
|
||||
@@ -1268,8 +1268,12 @@ It is whitespace-tolerant and uses familiar programming concepts with a custom s
|
||||
- first(a)
|
||||
- template(a)
|
||||
- replace(text, match, replacement)
|
||||
- pad(text | number, number, char, optional position: "<" | ">" | "^")
|
||||
- format(fmt_text, ...args)
|
||||
Field names are: `name`, `title"`, `caption"`, `group"`, `id"`, `chno"`, `logo"`, `logo_small"`, `parent_code"`, `audio_track"`, `time_shift" | "url"`, `epg_channel_id"`, `epg_id`.
|
||||
When you use Regular expressions it could be that your match contains multiple results. The builtin function `first` returns the first match.
|
||||
Format is very simple and only supports in text replacement like `format("Hello {}! Hello {}!", "Bob", "World")`
|
||||
When you use Regular expressions it could be that your match contains multiple results.
|
||||
The builtin function `first` returns the first match.
|
||||
Example `print(uppercase("hello"))`. output is only visible in `trace` log level you can enable it like `log_level: debug,tuliprox::foundation::mapper=trace` in config
|
||||
- Assignment assigns an expression result. variable or field.
|
||||
```dsl
|
||||
|
||||
@@ -520,5 +520,4 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -363,8 +363,10 @@
|
||||
|
||||
--source-editor-line-color: #ffffff;
|
||||
--source-editor-pending-line-color: yellow;
|
||||
}
|
||||
|
||||
--selection-area-border-color: rgba(0, 120, 215, 0.9);
|
||||
--selection-area-background-color: rgba(0, 120, 215, 0.12);
|
||||
}
|
||||
|
||||
body[data-theme="bright"] {
|
||||
--primary-color: rgba(10, 120, 255, 0.95);
|
||||
|
||||
@@ -79,6 +79,13 @@
|
||||
&__connections {
|
||||
}
|
||||
|
||||
&__selection-rect {
|
||||
border: 1px dashed var(--selection-area-border-color);
|
||||
background: var(--selection-area-background-color);
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.graph-paper-advanced {
|
||||
@@ -94,6 +101,11 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.selection_mode * {
|
||||
cursor: crosshair !important;
|
||||
}
|
||||
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ $port-size: 10px;
|
||||
.tp__source-editor__block {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
left: 0;
|
||||
top: 0;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
@@ -13,6 +15,13 @@ $port-size: 10px;
|
||||
min-width: $block-width;
|
||||
max-width: $block-width;
|
||||
width: $block-width;
|
||||
box-sizing: border-box;
|
||||
|
||||
&-editing {
|
||||
.tp__source-editor__block-content {
|
||||
border-color: red !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-selected {
|
||||
filter: sepia(1);
|
||||
@@ -194,7 +203,7 @@ $port-size: 10px;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
&-label {
|
||||
color: var(--text-color);
|
||||
color: var(--modest-text-color);
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
width: 50%;
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::app::components::{Block, BlockId, BlockInstance, BlockType, PortStatu
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct BlockProps {
|
||||
pub(crate) block: Block,
|
||||
pub(crate) edited:bool,
|
||||
pub(crate) selected:bool,
|
||||
pub(crate) delete_mode: bool,
|
||||
pub(crate) delete_block: Callback<BlockId>,
|
||||
@@ -28,7 +29,7 @@ pub fn BlockView(props: &BlockProps) -> Html {
|
||||
|
||||
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 style = format!("transform: translate({}px, {}px)", block.position.0, block.position.1);
|
||||
let from_id = block_id;
|
||||
let to_id = block_id;
|
||||
|
||||
@@ -81,11 +82,11 @@ pub fn BlockView(props: &BlockProps) -> Html {
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class={format!("tp__source-editor__block no-select tp__source-editor__block-{}{}", block_type, if props.selected {" tp__source-editor__block-selected"} else {""})}
|
||||
<div id={format!("block-{block_id}")} class={format!("tp__source-editor__block no-select tp__source-editor__block-{}{}{}", block_type, if props.edited {" tp__source-editor__block-editing"} else {""}, if props.selected {" tp__source-editor__block-selected"} else {""})}
|
||||
style={style}>
|
||||
<div class={"tp__source-editor__block-header"}>
|
||||
// Block handle (drag)
|
||||
<div class="tp__source-editor__block-handle" onmousedown={handle_mouse_down} />
|
||||
<div class="tp__source-editor__block-handle" onmousedown={handle_mouse_down.clone()} />
|
||||
// Delete button for block
|
||||
{
|
||||
html_if!(delete_mode, {
|
||||
@@ -97,7 +98,7 @@ pub fn BlockView(props: &BlockProps) -> Html {
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div class={if is_batch { "tp__source-editor__block-content tp__source-editor__block-batch" } else { "tp__source-editor__block-content" }} ondblclick={handle_edit}>
|
||||
<div class={if is_batch { "tp__source-editor__block-content tp__source-editor__block-batch" } else { "tp__source-editor__block-content" }} onmousedown={handle_mouse_down} ondblclick={handle_edit}>
|
||||
<div class={"tp__source-editor__block-content-body"}>
|
||||
<div class="tp__source-editor__block-label">
|
||||
{ title }
|
||||
|
||||
@@ -4,6 +4,11 @@ use serde::{Deserialize, Serialize};
|
||||
use yew::{Callback, UseStateHandle};
|
||||
use shared::model::{ConfigInputDto, ConfigTargetDto, InputType, TargetOutputDto};
|
||||
|
||||
pub const BLOCK_WIDTH: f32 = 100.0;
|
||||
pub const BLOCK_HEIGHT: f32 = 50.0;
|
||||
pub const BLOCK_HEADER_HEIGHT: f32 = 12.0;
|
||||
pub const BLOCK_PORT_HEIGHT: f32 = 10.0;
|
||||
|
||||
// ----------------- Data Models -----------------
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum BlockType {
|
||||
@@ -98,13 +103,50 @@ pub struct Block {
|
||||
pub instance: BlockInstance,
|
||||
}
|
||||
|
||||
impl Block {
|
||||
/// Liefert die Bounding-Box des Blocks unter Berücksichtigung der Canvas-Verschiebung.
|
||||
pub fn bounds(&self, canvas_offset: (f32, f32)) -> (f32, f32, f32, f32) {
|
||||
let (ox, oy) = canvas_offset;
|
||||
let bx = self.position.0 + ox;
|
||||
let by = self.position.1 + oy;
|
||||
let b_left = bx;
|
||||
let b_top = by;
|
||||
let b_right = bx + BLOCK_WIDTH;
|
||||
let b_bottom = by + (BLOCK_HEIGHT + BLOCK_HEADER_HEIGHT + BLOCK_PORT_HEIGHT);
|
||||
(b_left, b_top, b_right, b_bottom)
|
||||
}
|
||||
|
||||
/// Prüft, ob der Block von einem Auswahlrechteck getroffen wird.
|
||||
pub fn intersects_rect(
|
||||
&self,
|
||||
rect_start: (f32, f32),
|
||||
rect_end: (f32, f32),
|
||||
canvas_offset: (f32, f32),
|
||||
) -> bool {
|
||||
let (b_left, b_top, b_right, b_bottom) = self.bounds(canvas_offset);
|
||||
|
||||
// Rechteck normalisieren
|
||||
let (x1, y1) = rect_start;
|
||||
let (x2, y2) = rect_end;
|
||||
let r_left = x1.min(x2);
|
||||
let r_top = y1.min(y2);
|
||||
let r_right = x1.max(x2);
|
||||
let r_bottom = y1.max(y2);
|
||||
|
||||
b_right >= r_left &&
|
||||
b_left <= r_right &&
|
||||
b_bottom >= r_top &&
|
||||
b_top <= r_bottom
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Connection {
|
||||
pub from: BlockId,
|
||||
pub to: BlockId,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum PortStatus {
|
||||
Valid,
|
||||
@@ -112,8 +154,6 @@ pub enum PortStatus {
|
||||
Inactive
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BlockInstance {
|
||||
Input(Rc<ConfigInputDto>),
|
||||
|
||||
@@ -1,16 +1,159 @@
|
||||
use std::rc::Rc;
|
||||
use web_sys::{HtmlElement, MouseEvent};
|
||||
use yew::prelude::*;
|
||||
use shared::model::{ConfigInputDto, ConfigTargetDto, HdHomeRunTargetOutputDto, M3uTargetOutputDto, StrmTargetOutputDto, TargetOutputDto, XtreamTargetOutputDto};
|
||||
use crate::app::components::{can_connect, Block, BlockId, BlockInstance, BlockType, BlockView, Connection, EditMode, InputRow, PortStatus, SourceEditorContext, SourceEditorForm, SourceEditorSidebar};
|
||||
use crate::app::{PlaylistContext};
|
||||
use crate::app::components::source_editor::layout::layout;
|
||||
use crate::app::components::{can_connect, Block, BlockId, BlockInstance, BlockType, BlockView, Connection, EditMode, InputRow, PortStatus, SourceEditorContext, SourceEditorForm, SourceEditorSidebar, BLOCK_HEADER_HEIGHT, BLOCK_HEIGHT, BLOCK_PORT_HEIGHT, BLOCK_WIDTH};
|
||||
use crate::app::PlaylistContext;
|
||||
use shared::model::{ConfigInputDto, ConfigTargetDto, HdHomeRunTargetOutputDto, M3uTargetOutputDto, StrmTargetOutputDto, TargetOutputDto, XtreamTargetOutputDto};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, HtmlElement, MouseEvent};
|
||||
use yew::prelude::*;
|
||||
|
||||
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;
|
||||
const BLOCK_MIDDLE_Y: f32 = (BLOCK_HEIGHT + BLOCK_HEADER_HEIGHT + BLOCK_PORT_HEIGHT) / 2.0;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct DragState {
|
||||
pub block_id: Option<BlockId>,
|
||||
pub drag_offset: (f32, f32),
|
||||
pub sidebar_drag_offset: (f32, f32),
|
||||
pub dragging_group: Vec::<BlockId>,
|
||||
}
|
||||
|
||||
impl Default for DragState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
block_id: None,
|
||||
drag_offset: (0.0f32, 0.0f32),
|
||||
dragging_group: Vec::<BlockId>::new(),
|
||||
sidebar_drag_offset: (0.0f32, 0.0f32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DragState {
|
||||
pub fn with_sidebar_drag_offset(&self, x: f32, y: f32) -> Self {
|
||||
Self { sidebar_drag_offset: (x, y), ..self.clone() }
|
||||
}
|
||||
|
||||
pub fn with_drag_block_offset_and_group(&self, block_id: BlockId, drag_offset: (f32, f32), dragging_group: Vec<BlockId>) -> Self {
|
||||
Self { block_id: Some(block_id), drag_offset, dragging_group, sidebar_drag_offset: self.sidebar_drag_offset }
|
||||
}
|
||||
|
||||
pub(crate) fn reset_dragging(&self) -> DragState {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct SelectionState {
|
||||
pub is_selecting: bool,
|
||||
pub selection_rect: Option<(f32, f32, f32, f32)>, // x,y,w,h relative to canva;
|
||||
pub selection_start: (f32, f32),
|
||||
pub selected_blocks: Vec::<BlockId>,
|
||||
pub group_initial_positions: Vec::<(BlockId, (f32, f32))>,
|
||||
pub group_anchor_mouse: (f32, f32),
|
||||
}
|
||||
|
||||
impl Default for SelectionState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_selecting: false,
|
||||
selection_rect: None,
|
||||
selection_start: (0.0f32, 0.0f32),
|
||||
selected_blocks: Vec::<BlockId>::new(),
|
||||
group_initial_positions: Vec::<(BlockId, (f32, f32))>::new(),
|
||||
group_anchor_mouse: (0.0f32, 0.0f32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionState {
|
||||
pub fn reset_selection(&self) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn with_blocks_group_initial_pos_and_group_anchor_mouse(&self, selected_blocks: Vec<BlockId>,
|
||||
group_initial_positions: Vec<(BlockId, (f32, f32))>,
|
||||
group_anchor_mouse: (f32, f32)) -> Self {
|
||||
Self {
|
||||
is_selecting: self.is_selecting,
|
||||
selection_rect: self.selection_rect,
|
||||
selection_start: self.selection_start,
|
||||
selected_blocks,
|
||||
group_initial_positions,
|
||||
group_anchor_mouse,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_selecting_start_rect_and_blocks(&self, is_selecting: bool,
|
||||
selection_start: (f32, f32),
|
||||
selection_rect: Option<(f32, f32, f32, f32)>,
|
||||
selected_blocks: Vec::<BlockId>) -> Self {
|
||||
Self {
|
||||
is_selecting,
|
||||
selection_rect,
|
||||
selection_start,
|
||||
selected_blocks,
|
||||
group_initial_positions: self.group_initial_positions.clone(),
|
||||
group_anchor_mouse: self.group_anchor_mouse,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_selecting_start_and_rect(&self, is_selecting: bool,
|
||||
selection_start: (f32, f32),
|
||||
selection_rect: Option<(f32, f32, f32, f32)>) -> Self {
|
||||
Self {
|
||||
is_selecting,
|
||||
selection_rect,
|
||||
selection_start,
|
||||
selected_blocks: self.selected_blocks.clone(),
|
||||
group_initial_positions: self.group_initial_positions.clone(),
|
||||
group_anchor_mouse: self.group_anchor_mouse,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn with_rect_and_blocks(&self, selection_rect: Option<(f32, f32, f32, f32)>,
|
||||
selected_blocks: Vec::<BlockId>) -> Self {
|
||||
Self {
|
||||
is_selecting: self.is_selecting,
|
||||
selection_rect,
|
||||
selection_start: self.selection_start,
|
||||
selected_blocks,
|
||||
group_initial_positions: self.group_initial_positions.clone(),
|
||||
group_anchor_mouse: self.group_anchor_mouse,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_selecting_rect_and_group_initial_positions(&self, is_selecting: bool,
|
||||
selection_rect: Option<(f32, f32, f32, f32)>,
|
||||
group_initial_positions: Vec<(BlockId, (f32, f32))>) -> Self {
|
||||
Self {
|
||||
is_selecting,
|
||||
selection_rect,
|
||||
selection_start: self.selection_start,
|
||||
selected_blocks: self.selected_blocks.clone(),
|
||||
group_initial_positions,
|
||||
group_anchor_mouse: self.group_anchor_mouse,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_cleared_blocks(&self, block_id: BlockId) -> Self {
|
||||
let updated_selections: Vec<BlockId> = self.selected_blocks
|
||||
.iter()
|
||||
.filter(|&&id| id != block_id)
|
||||
.map(|&id| if id > block_id { id - 1 } else { id })
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
is_selecting: self.is_selecting,
|
||||
selection_rect: self.selection_rect,
|
||||
selection_start: self.selection_start,
|
||||
selected_blocks: updated_selections,
|
||||
group_initial_positions: self.group_initial_positions.clone(),
|
||||
group_anchor_mouse: self.group_anchor_mouse,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_instance(block_type: BlockType) -> BlockInstance {
|
||||
match block_type {
|
||||
@@ -19,7 +162,7 @@ fn create_instance(block_type: BlockType) -> BlockInstance {
|
||||
BlockType::Target => {
|
||||
let dto = ConfigTargetDto { name: String::new(), ..Default::default() };
|
||||
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()))),
|
||||
@@ -32,38 +175,36 @@ fn create_instance(block_type: BlockType) -> BlockInstance {
|
||||
pub fn SourceEditor() -> Html {
|
||||
let canvas_ref = use_node_ref();
|
||||
let playlist_ctx = use_context::<PlaylistContext>().expect("Playlist context not found");
|
||||
let blocks = use_state(Vec::<Block>::new);
|
||||
let connections = use_state(Vec::<Connection>::new);
|
||||
let force_update = use_state(|| 0);
|
||||
let blocks = use_mut_ref(Vec::<Block>::new);
|
||||
let connections = use_mut_ref(Vec::<Connection>::new);
|
||||
let next_id = use_state(|| 1 as BlockId);
|
||||
|
||||
let block_elements = use_mut_ref(HashMap::<BlockId, HtmlElement>::new);
|
||||
let connection_elements = use_mut_ref(HashMap::<(BlockId, BlockId), Element>::new);
|
||||
// ----------------- virtual canvas offset -----------------
|
||||
let canvas_offset = use_state(|| (0.0f32, 0.0f32));
|
||||
let is_panning = use_state(|| false);
|
||||
let pan_start = use_state(|| (0.0f32, 0.0f32));
|
||||
|
||||
// Dragging state
|
||||
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));
|
||||
|
||||
let drag_state = use_state(DragState::default);
|
||||
// Selection / marquee states
|
||||
let selection_state = use_state(SelectionState::default);
|
||||
// Pending line for live connection
|
||||
let pending_line = use_state(|| None);
|
||||
let pending_connection = use_state(|| None );
|
||||
|
||||
let pending_connection = use_state(|| None);
|
||||
// Delete mode toggle
|
||||
let delete_mode = use_state(|| false);
|
||||
|
||||
let cursor_grabbing = use_state(|| false);
|
||||
|
||||
{
|
||||
let playlists = playlist_ctx.clone();
|
||||
let get_next_id = next_id.clone();
|
||||
let blocks_set = blocks.clone();
|
||||
let connections_set = connections.clone();
|
||||
let blocks_ref = blocks.clone();
|
||||
let connections_ref = connections.clone();
|
||||
use_effect_with(playlists.sources.clone(), move |sources| {
|
||||
if let Some(entries) = sources.as_ref() {
|
||||
let mut gen_blocks = vec![];
|
||||
let mut gen_connections = vec![];
|
||||
let mut gen_blocks = blocks_ref.borrow_mut();
|
||||
let mut gen_connections = connections_ref.borrow_mut();
|
||||
let mut current_id = *get_next_id;
|
||||
for (inputs, targets) in entries.as_ref() {
|
||||
let mut input_ids = vec![];
|
||||
@@ -85,7 +226,7 @@ pub fn SourceEditor() -> Html {
|
||||
}
|
||||
}
|
||||
for target_config in targets {
|
||||
let target_id = current_id;
|
||||
let target_id = current_id;
|
||||
current_id += 1;
|
||||
let block = Block {
|
||||
id: target_id,
|
||||
@@ -95,20 +236,15 @@ pub fn SourceEditor() -> Html {
|
||||
};
|
||||
gen_blocks.push(block);
|
||||
input_ids.iter().for_each(|input_id| gen_connections.push(Connection { from: *input_id, to: target_id }));
|
||||
// Test connections
|
||||
// gen_connections.push(Connection {from: 1, to: 14});
|
||||
// gen_connections.push(Connection {from: 17, to: 2});
|
||||
|
||||
|
||||
for output in &target_config.output {
|
||||
|
||||
let (block_instance, block_type) = match output {
|
||||
let (block_instance, block_type) = match output {
|
||||
TargetOutputDto::Xtream(dto) => (BlockInstance::Output(Rc::new(TargetOutputDto::Xtream(dto.clone()))), BlockType::OutputXtream),
|
||||
TargetOutputDto::M3u(dto) => (BlockInstance::Output(Rc::new(TargetOutputDto::M3u(dto.clone()))), BlockType::OutputM3u),
|
||||
TargetOutputDto::Strm(dto) => (BlockInstance::Output(Rc::new(TargetOutputDto::Strm(dto.clone()))), BlockType::OutputStrm),
|
||||
TargetOutputDto::HdHomeRun(dto) => (BlockInstance::Output(Rc::new(TargetOutputDto::HdHomeRun(dto.clone()))), BlockType::OutputHdHomeRun),
|
||||
};
|
||||
let output_id = current_id;
|
||||
let output_id = current_id;
|
||||
current_id += 1;
|
||||
|
||||
let block = Block {
|
||||
@@ -121,23 +257,36 @@ pub fn SourceEditor() -> Html {
|
||||
gen_connections.push(Connection { from: target_id, to: output_id });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
layout(&mut gen_blocks, &gen_connections);
|
||||
get_next_id.set(current_id);
|
||||
blocks_set.set(gen_blocks);
|
||||
connections_set.set(gen_connections);
|
||||
}
|
||||
|
||||
||{}
|
||||
|| {}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let handle_layout = {
|
||||
let blocks_clone = blocks.clone();
|
||||
let connections_clone = connections.clone();
|
||||
let force_update = force_update.clone();
|
||||
Callback::from(move |_| {
|
||||
let mut new_blocks = blocks_clone.borrow_mut();
|
||||
let new_connections = connections_clone.borrow_mut();
|
||||
layout(&mut new_blocks, &new_connections);
|
||||
|
||||
force_update.set(*force_update + 1);
|
||||
})
|
||||
};
|
||||
|
||||
// ----------------- Drag Start from Sidebar -----------------
|
||||
let handle_drag_start = {
|
||||
let sidebar_drag_offset = sidebar_drag_offset.clone();
|
||||
let drag_state = drag_state.clone();
|
||||
let cursor_grabbing = cursor_grabbing.clone();
|
||||
let selection_state = selection_state.clone();
|
||||
Callback::from(move |e: DragEvent| {
|
||||
selection_state.set((*selection_state).reset_selection());
|
||||
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();
|
||||
@@ -145,18 +294,19 @@ pub fn SourceEditor() -> Html {
|
||||
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));
|
||||
drag_state.set((*drag_state).with_sidebar_drag_offset(offset_x, offset_y));
|
||||
cursor_grabbing.set(true);
|
||||
}
|
||||
}
|
||||
)};
|
||||
)
|
||||
};
|
||||
|
||||
// ----------------- Drop on Canvas -----------------
|
||||
let handle_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();
|
||||
let drag_state = drag_state.clone();
|
||||
let canvas_offset = canvas_offset.clone(); // <-- add canvas_offset
|
||||
let cursor_grabbing = cursor_grabbing.clone();
|
||||
|
||||
@@ -168,12 +318,12 @@ pub fn SourceEditor() -> Html {
|
||||
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 (offset_x, offset_y) = drag_state.sidebar_drag_offset;
|
||||
let (ox, oy) = *canvas_offset; // <-- include canvas offset
|
||||
|
||||
let block_type = BlockType::from(data.as_str());
|
||||
|
||||
let mut current_blocks = (*blocks).clone();
|
||||
let mut current_blocks = blocks.borrow_mut();
|
||||
current_blocks.push(Block {
|
||||
id: *next_id,
|
||||
block_type,
|
||||
@@ -183,7 +333,6 @@ pub fn SourceEditor() -> Html {
|
||||
),
|
||||
instance: create_instance(block_type),
|
||||
});
|
||||
blocks.set(current_blocks);
|
||||
next_id.set(*next_id + 1);
|
||||
}
|
||||
}
|
||||
@@ -207,7 +356,7 @@ pub fn SourceEditor() -> Html {
|
||||
let canvas_offset = canvas_offset.clone();
|
||||
Callback::from(move |from_id: BlockId| {
|
||||
pending_connection.set(Some(from_id));
|
||||
if let Some(block) = (*blocks).get(from_id as usize -1) {
|
||||
if let Some(block) = blocks.borrow().get(from_id as usize - 1) {
|
||||
let (ox, oy) = *canvas_offset;
|
||||
let x = block.position.0 + BLOCK_WIDTH + ox;
|
||||
let y = block.position.1 + BLOCK_MIDDLE_Y + oy;
|
||||
@@ -224,82 +373,116 @@ pub fn SourceEditor() -> Html {
|
||||
Callback::from(move |to_id: BlockId| {
|
||||
if let Some(from_id) = *pending_connection {
|
||||
if from_id != to_id {
|
||||
let current_blocks = (*blocks).clone();
|
||||
let current_blocks = blocks.borrow_mut();
|
||||
if let (Some(from_block), Some(to_block)) = (
|
||||
current_blocks.get(from_id as usize -1),
|
||||
current_blocks.get(to_id as usize -1),
|
||||
current_blocks.get(from_id as usize - 1),
|
||||
current_blocks.get(to_id as usize - 1),
|
||||
) {
|
||||
// ✅ Check connection rules before adding
|
||||
if can_connect(from_block, to_block, &connections, &blocks) {
|
||||
let mut current_connections = (*connections).clone();
|
||||
// Check connection rules before adding
|
||||
if can_connect(from_block, to_block, &connections.borrow(), ¤t_blocks) {
|
||||
let mut current_connections = connections.borrow_mut();
|
||||
current_connections.push(Connection { from: from_id, to: to_id });
|
||||
connections.set(current_connections);
|
||||
//} else {
|
||||
// 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 + BLOCK_MIDDLE_Y;
|
||||
// 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 -----------------
|
||||
// ----------------- Drag block logic -----------------
|
||||
let handle_block_mouse_down = {
|
||||
let dragging_block = dragging_block.clone();
|
||||
let drag_offset = drag_offset.clone();
|
||||
let drag_state = drag_state.clone();
|
||||
let canvas_ref = canvas_ref.clone();
|
||||
let blocks = blocks.clone();
|
||||
let cursor_grabbing = cursor_grabbing.clone();
|
||||
let selection_state = selection_state.clone();
|
||||
|
||||
Callback::from(move |(block_id, e): (BlockId, MouseEvent)| {
|
||||
e.prevent_default();
|
||||
let ctrl_key = e.ctrl_key();
|
||||
if let Some(canvas) = canvas_ref.cast::<HtmlElement>() {
|
||||
cursor_grabbing.set(true);
|
||||
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).get(block_id as usize -1) {
|
||||
drag_offset.set((mouse_x - block.position.0, mouse_y - block.position.1));
|
||||
dragging_block.set(Some(block_id));
|
||||
if let Some(block) = blocks.borrow().get(block_id as usize - 1) {
|
||||
// For Group-Drag: save all blocks
|
||||
let mut initials = vec![];
|
||||
let mut group_ids = vec![];
|
||||
|
||||
// if block is not in selection list, than only select this block
|
||||
let mut new_selection = None;
|
||||
if !selection_state.selected_blocks.contains(&block_id) {
|
||||
new_selection = Some(block_id);
|
||||
} else {
|
||||
let current_blocks = blocks.borrow();
|
||||
for id in &selection_state.selected_blocks {
|
||||
if let Some(b) = current_blocks.get(*id as usize - 1) {
|
||||
initials.push((*id, b.position));
|
||||
group_ids.push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drag_state.set((*drag_state).with_drag_block_offset_and_group(block_id, (mouse_x - block.position.0, mouse_y - block.position.1), group_ids));
|
||||
|
||||
let new_blocks = match new_selection {
|
||||
Some(block) => if ctrl_key {
|
||||
let mut new_list = selection_state.selected_blocks.clone();
|
||||
new_list.push(block);
|
||||
new_list
|
||||
} else { vec![block] },
|
||||
None => selection_state.selected_blocks.clone(),
|
||||
};
|
||||
selection_state.set((*selection_state).with_blocks_group_initial_pos_and_group_anchor_mouse(new_blocks, initials, (mouse_x, mouse_y)));
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// ----------------- Canvas mouse down (start panning if clicking the canvas itself) -----------------
|
||||
// ----------------- Canvas mouse down (start panning or marquee selection) -----------------
|
||||
let handle_canvas_mouse_down = {
|
||||
let is_panning = is_panning.clone();
|
||||
let selection_state = selection_state.clone();
|
||||
let pan_start = pan_start.clone();
|
||||
let canvas_ref = canvas_ref.clone();
|
||||
let cursor_grabbing = cursor_grabbing.clone();
|
||||
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
// Start panning with right mouse button if the click target is exactly the canvas element.
|
||||
if e.button() == 2 {
|
||||
if let Some(target) = e.target_dyn_into::<web_sys::Element>() {
|
||||
if let Some(canvas) = canvas_ref.cast::<web_sys::Element>() {
|
||||
let tag = target.tag_name().to_lowercase();
|
||||
// accept both the canvas <div> itself and its <svg> background as valid pan targets
|
||||
if target.is_same_node(Some(&canvas)) || tag == "svg" {
|
||||
e.prevent_default();
|
||||
let mouse_button = e.button();
|
||||
if mouse_button != 0 && mouse_button != 2 {
|
||||
return;
|
||||
}
|
||||
if let Some(target) = e.target_dyn_into::<web_sys::Element>() {
|
||||
if let Some(canvas) = canvas_ref.cast::<web_sys::Element>() {
|
||||
let tag = target.tag_name().to_lowercase();
|
||||
if target.is_same_node(Some(&canvas)) || tag == "svg" {
|
||||
e.prevent_default();
|
||||
|
||||
if e.button() == 0 { // left button
|
||||
if selection_state.is_selecting {
|
||||
selection_state.set((*selection_state).reset_selection());
|
||||
} else {
|
||||
// selection area mode
|
||||
if let Some(rect_el) = canvas_ref.cast::<HtmlElement>() {
|
||||
let rect = rect_el.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 e.ctrl_key() {
|
||||
selection_state.set((*selection_state)
|
||||
.with_selecting_start_and_rect(true, (mouse_x, mouse_y), Some((mouse_x, mouse_y, 0.0, 0.0))));
|
||||
} else {
|
||||
selection_state.set((*selection_state)
|
||||
.with_selecting_start_rect_and_blocks(true, (mouse_x, mouse_y), Some((mouse_x, mouse_y, 0.0, 0.0)), vec![]));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if e.button() == 2 { // right button
|
||||
selection_state.set((*selection_state).reset_selection());
|
||||
// Right button panning
|
||||
cursor_grabbing.set(true);
|
||||
is_panning.set(true);
|
||||
pan_start.set((e.client_x() as f32, e.client_y() as f32));
|
||||
@@ -310,42 +493,52 @@ pub fn SourceEditor() -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
// ----------------- Mouse move for both pending line, block drag, and canvas panning -----------------
|
||||
// ----------------- Mouse move for pending line, block drag, canvas panning, marquee update -----------------
|
||||
let handle_canvas_mouse_move = {
|
||||
let pending_line = pending_line.clone();
|
||||
let dragging_block = dragging_block.clone();
|
||||
let drag_offset = drag_offset.clone();
|
||||
let drag_state = drag_state.clone();
|
||||
let blocks = blocks.clone();
|
||||
let connections = connections.clone();
|
||||
let canvas_ref = canvas_ref.clone();
|
||||
let is_panning = is_panning.clone();
|
||||
let selection_state = selection_state.clone();
|
||||
let pan_start = pan_start.clone();
|
||||
let canvas_offset = canvas_offset.clone();
|
||||
let last_frame = RefCell::new(0.0);
|
||||
let block_elements_ref = block_elements.clone();
|
||||
let conn_elements_ref = connection_elements.clone();
|
||||
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
// If currently panning the canvas, update the canvas offset and exit early.
|
||||
let now = web_sys::js_sys::Date::now();
|
||||
if now - *last_frame.borrow() < 16.0 { return; }
|
||||
*last_frame.borrow_mut() = now;
|
||||
|
||||
let client_x = e.client_x();
|
||||
let client_y = e.client_y();
|
||||
|
||||
// Panning (right mouse)
|
||||
if *is_panning {
|
||||
let (start_x, start_y) = *pan_start;
|
||||
let dx = e.client_x() as f32 - start_x;
|
||||
let dy = e.client_y() as f32 - start_y;
|
||||
let dx = client_x as f32 - start_x;
|
||||
let dy = client_y as f32 - start_y;
|
||||
let (ox, oy) = *canvas_offset;
|
||||
canvas_offset.set((ox + dx, oy + dy));
|
||||
// Update pan start for smooth continuous panning
|
||||
pan_start.set((e.client_x() as f32, e.client_y() as f32));
|
||||
pan_start.set((client_x as f32, client_y as f32));
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
let mouse_x = client_x as f32 - rect.left() as f32;
|
||||
let mouse_y = client_y as f32 - rect.top() as f32;
|
||||
|
||||
// Update pending line (snap to nearest port if close)
|
||||
// Pending line snap
|
||||
if let Some(((from_x, from_y), _)) = *pending_line {
|
||||
let mut snapped = (mouse_x, mouse_y);
|
||||
let (ox, oy) = *canvas_offset;
|
||||
for block in (*blocks).iter() {
|
||||
for block in blocks.borrow().iter() {
|
||||
let port_x = block.position.0 + ox;
|
||||
let port_y = block.position.1 + BLOCK_MIDDLE_Y + oy;
|
||||
let port_y = block.position.1 + BLOCK_MIDDLE_Y + oy;
|
||||
let dx = mouse_x - port_x;
|
||||
let dy = mouse_y - port_y;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
@@ -356,29 +549,148 @@ pub fn SourceEditor() -> Html {
|
||||
pending_line.set(Some(((from_x, from_y), snapped)));
|
||||
}
|
||||
|
||||
// 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.get_mut(block_id as usize -1) {
|
||||
block.position = (mouse_x - offset_x, mouse_y - offset_y);
|
||||
if selection_state.is_selecting {
|
||||
// compute normalized rect
|
||||
let (start_x, start_y) = selection_state.selection_start;
|
||||
let x = start_x.min(mouse_x);
|
||||
let y = start_y.min(mouse_y);
|
||||
let w = (mouse_x - start_x).abs();
|
||||
let h = (mouse_y - start_y).abs();
|
||||
let ctrl_key = e.ctrl_key();
|
||||
|
||||
// Update selected_blocks: block intersects rect?
|
||||
let mut sel = Vec::new();
|
||||
if ctrl_key {
|
||||
selection_state.selected_blocks.iter().for_each(|b| sel.push(*b));
|
||||
}
|
||||
blocks.borrow().iter().filter(|b| b.intersects_rect((x, y), (x + w, y + h), *canvas_offset))
|
||||
.for_each(|b| sel.push(b.id));
|
||||
|
||||
selection_state.set((*selection_state).with_rect_and_blocks(Some((x, y, w, h)), sel));
|
||||
}
|
||||
|
||||
// Update dragging block (Single or Group)
|
||||
if let Some(block_id) = drag_state.block_id {
|
||||
let mut block_elements = block_elements_ref.borrow_mut();
|
||||
if block_elements.is_empty() {
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(document) = window.document() {
|
||||
for block_id in &drag_state.dragging_group {
|
||||
if let Some(el) = document.get_element_by_id(&format!("block-{block_id}")) {
|
||||
let div = el.dyn_into::<HtmlElement>().unwrap();
|
||||
block_elements.insert(*block_id, div);
|
||||
}
|
||||
}
|
||||
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = block_elements.entry(block_id) {
|
||||
if let Some(el) = document.get_element_by_id(&format!("block-{block_id}")) {
|
||||
let div = el.dyn_into::<HtmlElement>().unwrap();
|
||||
e.insert(div);
|
||||
}
|
||||
}
|
||||
|
||||
let mut conn_elements = conn_elements_ref.borrow_mut();
|
||||
// find connections
|
||||
for conn in connections.borrow().iter() {
|
||||
if block_id == conn.from || block_id == conn.to
|
||||
|| drag_state.dragging_group.contains(&conn.from) || drag_state.dragging_group.contains(&conn.to) {
|
||||
if let Some(el) = document.get_element_by_id(&format!("conn-{}-{}", conn.from, conn.to)) {
|
||||
let path_el = el.dyn_into::<Element>().unwrap();
|
||||
conn_elements.insert((conn.from, conn.to), path_el);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut moved_blocks = HashSet::new();
|
||||
|
||||
// If the dragged block is member of a selection -> move group
|
||||
if (*drag_state.dragging_group).contains(&block_id) && !selection_state.group_initial_positions.is_empty() {
|
||||
let (anchor_x, anchor_y) = selection_state.group_anchor_mouse;
|
||||
let dx = mouse_x - anchor_x;
|
||||
let dy = mouse_y - anchor_y;
|
||||
|
||||
let (ox, oy) = *canvas_offset;
|
||||
let mut current_blocks = blocks.borrow_mut();
|
||||
for (id, (ix, iy)) in &selection_state.group_initial_positions {
|
||||
if let Some(b) = current_blocks.get_mut(*id as usize - 1) {
|
||||
b.position = (ix + dx, iy + dy);
|
||||
moved_blocks.insert(b.id);
|
||||
if let Some(div) = block_elements.get(&b.id) {
|
||||
div.style().set_property("transform", &format!("translate({}px,{}px)", ix + dx + ox, iy + dy + oy)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single drag block
|
||||
let (ox, oy) = *canvas_offset;
|
||||
let (offset_x, offset_y) = drag_state.drag_offset;
|
||||
let mut current_blocks = blocks.borrow_mut();
|
||||
if let Some(block) = current_blocks.get_mut(block_id as usize - 1) {
|
||||
block.position = (mouse_x - offset_x, mouse_y - offset_y);
|
||||
moved_blocks.insert(block.id);
|
||||
if let Some(div) = block_elements.get(&block_id) {
|
||||
div.style().set_property("transform", &format!("translate({}px,{}px)", mouse_x - offset_x + ox, mouse_y - offset_y + oy)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !moved_blocks.is_empty() {
|
||||
let mut move_connections = HashMap::<String, (BlockId, BlockId)>::new();
|
||||
for conn in connections.borrow().iter() {
|
||||
if moved_blocks.contains(&conn.from) || moved_blocks.contains(&conn.to) {
|
||||
move_connections.insert(format!("conn-{}-{}", conn.from, conn.to), (conn.from, conn.to));
|
||||
}
|
||||
}
|
||||
let current_blocks = blocks.borrow();
|
||||
let conn_elements = conn_elements_ref.borrow();
|
||||
let (ox, oy) = *canvas_offset;
|
||||
for (from, to) in move_connections.values() {
|
||||
if let Some(path_el) = conn_elements.get(&(*from, *to)) {
|
||||
if let (Some(from_block), Some(to_block)) =
|
||||
(¤t_blocks.get(*from as usize - 1), ¤t_blocks.get(*to as usize - 1)) {
|
||||
let from_x = from_block.position.0 + BLOCK_WIDTH + ox;
|
||||
let from_y = from_block.position.1 + BLOCK_MIDDLE_Y + oy;
|
||||
let to_x = to_block.position.0 + ox;
|
||||
let to_y = to_block.position.1 + BLOCK_MIDDLE_Y + oy;
|
||||
let dx = to_x - from_x;
|
||||
let ctrl = dx * 0.5;
|
||||
let new_d = format!(
|
||||
"M {} {} C {} {}, {} {}, {} {}",
|
||||
from_x, from_y,
|
||||
from_x + ctrl, from_y,
|
||||
to_x - ctrl, to_y,
|
||||
to_x, to_y
|
||||
);
|
||||
path_el.set_attribute("d", &new_d).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
blocks.set(current_blocks);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
let handle_canvas_mouse_up = {
|
||||
let dragging_block = dragging_block.clone();
|
||||
let drag_state = drag_state.clone();
|
||||
let is_panning = is_panning.clone();
|
||||
let selection_state = selection_state.clone();
|
||||
let cursor_grabbing = cursor_grabbing.clone();
|
||||
let block_elements = block_elements.clone();
|
||||
let connection_elements = connection_elements.clone();
|
||||
Callback::from(move |_e: MouseEvent| {
|
||||
block_elements.borrow_mut().clear();
|
||||
connection_elements.borrow_mut().clear();
|
||||
// Stop any block dragging and stop panning
|
||||
dragging_block.set(None);
|
||||
if drag_state.block_id.is_some() {
|
||||
drag_state.set((*drag_state).reset_dragging());
|
||||
}
|
||||
is_panning.set(false);
|
||||
cursor_grabbing.set(false);
|
||||
selection_state.set((*selection_state).with_selecting_rect_and_group_initial_positions(false, None, vec![]));
|
||||
})
|
||||
};
|
||||
|
||||
@@ -398,25 +710,28 @@ pub fn SourceEditor() -> Html {
|
||||
Callback::from(move |_| delete_mode.set(!*delete_mode))
|
||||
};
|
||||
|
||||
// Deleting a Block means updating the following block ids,
|
||||
// because a BlockId is the index in the blocks list.
|
||||
let handle_delete_block = {
|
||||
let blocks = blocks.clone();
|
||||
let connections = connections.clone();
|
||||
let next_id = next_id.clone();
|
||||
let selection_state = selection_state.clone();
|
||||
Callback::from(move |block_id: BlockId| {
|
||||
let mut current_blocks = (*blocks).clone();
|
||||
let mut current_blocks = blocks.borrow_mut();
|
||||
current_blocks.retain(|b| b.id != block_id);
|
||||
|
||||
let mut current_connections = (*connections).clone();
|
||||
let mut current_connections = connections.borrow_mut();
|
||||
current_connections.retain(|c| c.from != block_id && c.to != block_id);
|
||||
|
||||
for block in &mut current_blocks {
|
||||
for block in current_blocks.iter_mut() {
|
||||
if block.id >= block_id {
|
||||
block.id -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 2: Connections anpassen
|
||||
for conn in &mut current_connections {
|
||||
// udpate connection ids
|
||||
for conn in current_connections.iter_mut() {
|
||||
if conn.from >= block_id {
|
||||
conn.from -= 1;
|
||||
}
|
||||
@@ -426,30 +741,28 @@ pub fn SourceEditor() -> Html {
|
||||
}
|
||||
|
||||
let max_id = current_blocks.iter().map(|b| b.id).max().unwrap_or(0);
|
||||
next_id.set(max_id+1);
|
||||
blocks.set(current_blocks);
|
||||
connections.set(current_connections);
|
||||
next_id.set(max_id + 1);
|
||||
|
||||
selection_state.set((*selection_state).with_cleared_blocks(block_id));
|
||||
})
|
||||
};
|
||||
|
||||
let handle_delete_connection = {
|
||||
let connections = connections.clone();
|
||||
Callback::from(move |(from, to): (BlockId, BlockId)| {
|
||||
let mut current_connections = (*connections).clone();
|
||||
current_connections.retain(|c| !(c.from == from && c.to == to));
|
||||
connections.set(current_connections);
|
||||
connections.borrow_mut().retain(|c| !(c.from == from && c.to == to));
|
||||
})
|
||||
};
|
||||
|
||||
let get_port_status = {
|
||||
|block: &Block| {
|
||||
if let Some(from_id) = *pending_connection {
|
||||
if let Some(from_block) = (*blocks).get(from_id as usize -1) {
|
||||
return if can_connect(from_block, block, &connections, &blocks) {
|
||||
if let Some(from_block) = blocks.borrow().get(from_id as usize - 1) {
|
||||
return if can_connect(from_block, block, &connections.borrow(), &blocks.borrow()) {
|
||||
PortStatus::Valid
|
||||
} else {
|
||||
PortStatus::Invalid
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
PortStatus::Inactive
|
||||
@@ -459,11 +772,10 @@ pub fn SourceEditor() -> Html {
|
||||
let form_changed = {
|
||||
let blocks = blocks.clone();
|
||||
Callback::<(BlockId, BlockInstance)>::from(move |(block_id, instance): (BlockId, BlockInstance)| {
|
||||
let mut current_blocks = (*blocks).clone();
|
||||
if let Some(block) = current_blocks.get_mut(block_id as usize -1) {
|
||||
block.instance = instance;
|
||||
}
|
||||
blocks.set(current_blocks);
|
||||
let mut current_blocks = blocks.borrow_mut();
|
||||
if let Some(block) = current_blocks.get_mut(block_id as usize - 1) {
|
||||
block.instance = instance;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -472,9 +784,11 @@ pub fn SourceEditor() -> Html {
|
||||
let handle_block_edit = {
|
||||
let edit_mode_set = edit_mode.clone();
|
||||
let blocks = blocks.clone();
|
||||
let selection_state = selection_state.clone();
|
||||
Callback::from(move |block_id: BlockId| {
|
||||
if let Some(block) = (*blocks).get(block_id as usize -1 ) {
|
||||
if let Some(block) = blocks.borrow().get(block_id as usize - 1) {
|
||||
edit_mode_set.set(EditMode::Active(block.clone()));
|
||||
selection_state.set((*selection_state).reset_selection());
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -484,11 +798,12 @@ pub fn SourceEditor() -> Html {
|
||||
edit_mode: edit_mode.clone(),
|
||||
};
|
||||
|
||||
let selected_block_id = match *edit_mode {
|
||||
let edited_block_id = match *edit_mode {
|
||||
EditMode::Inactive => 0,
|
||||
EditMode::Active(ref b) => b.id,
|
||||
};
|
||||
let grabbed = *cursor_grabbing;
|
||||
let selection_mode = selection_state.is_selecting;
|
||||
// ----------------- Render -----------------
|
||||
html! {
|
||||
<ContextProvider<SourceEditorContext> context={editor_context}>
|
||||
@@ -498,12 +813,13 @@ pub fn SourceEditor() -> Html {
|
||||
delete_mode={*delete_mode}
|
||||
on_drag_start={handle_drag_start.clone()}
|
||||
on_toggle_delete={handle_toggle_delete_mode.clone()}
|
||||
on_layout={handle_layout.clone()}
|
||||
/>
|
||||
// Canvas
|
||||
<div class="tp__source-editor__canvas-wrapper">
|
||||
<div
|
||||
ref={canvas_ref.clone()}
|
||||
class={classes!("tp__source-editor__canvas", "graph-paper-advanced", if grabbed {"grabbed"} else {""})}
|
||||
class={classes!("tp__source-editor__canvas", "graph-paper-advanced", if grabbed {"grabbed"} else {""}, if selection_mode {"selection_mode"} else {""})}
|
||||
ondrop={handle_drop.clone()}
|
||||
ondragend={handle_drag_end.clone()}
|
||||
ondragover={handle_drag_over.clone()}
|
||||
@@ -513,10 +829,11 @@ pub fn SourceEditor() -> Html {
|
||||
oncontextmenu={handle_canvas_right_click.clone()}>
|
||||
|
||||
// SVG for connections
|
||||
<svg class={classes!("tp__source-editor__connections", if grabbed {"grabbed"} else {""})}>
|
||||
{ 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)?;
|
||||
<svg class={classes!("tp__source-editor__connections", if grabbed {"grabbed"} else {""}, if selection_mode {"selection_mode"} else {""})}>
|
||||
{ for connections.borrow().iter().filter_map(|c| {
|
||||
let current_blocks = blocks.borrow();
|
||||
let from_block = current_blocks.get(c.from as usize -1)?;
|
||||
let to_block = current_blocks.get(c.to as usize -1)?;
|
||||
let (ox, oy) = *canvas_offset; // Apply virtual canvas offset
|
||||
let from_x = from_block.position.0 + BLOCK_WIDTH + ox;
|
||||
let from_y = from_block.position.1 + BLOCK_MIDDLE_Y + oy;
|
||||
@@ -534,7 +851,7 @@ pub fn SourceEditor() -> Html {
|
||||
|
||||
Some(html! {
|
||||
<g>
|
||||
<path d={d} stroke="var(--source-editor-line-color)" fill="transparent" stroke-width="2"/>
|
||||
<path id={format!("conn-{}-{}", c.from, c.to)} d={d} stroke="var(--source-editor-line-color)" 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;
|
||||
@@ -555,7 +872,7 @@ pub fn SourceEditor() -> Html {
|
||||
})
|
||||
}) }
|
||||
|
||||
// Pending line (straight, yellow)
|
||||
// Pending line straight
|
||||
{ if let Some(((x1, y1), (x2, y2))) = *pending_line {
|
||||
html! {
|
||||
<line
|
||||
@@ -568,17 +885,32 @@ pub fn SourceEditor() -> Html {
|
||||
} else { html!{} } }
|
||||
</svg>
|
||||
|
||||
// Render blocks
|
||||
{ for (*blocks).iter().map(|b|{
|
||||
// Selection rectangle overlay
|
||||
{
|
||||
if let Some((x, y, w, h)) = selection_state.selection_rect {
|
||||
let style = format!("position:absolute; left:{}px; top:{}px; width:{}px; height:{}px; pointer-events:none;", x, y, w, h);
|
||||
html! {
|
||||
<div class="tp__source-editor__selection-rect" style={style}></div>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
|
||||
// Render blocks with canvas offset
|
||||
{ for blocks.borrow().iter().map(|b|{
|
||||
let port_status = get_port_status(b);
|
||||
let (ox, oy) = *canvas_offset; // Apply virtual offset to each block
|
||||
let mut shifted_block = b.clone();
|
||||
let block_id = shifted_block.id;
|
||||
shifted_block.position = (b.position.0 + ox, b.position.1 + oy);
|
||||
let is_block_selected = selection_state.selected_blocks.contains(&block_id);
|
||||
html! {
|
||||
<BlockView
|
||||
key={block_id}
|
||||
block={shifted_block}
|
||||
selected={selected_block_id == block_id}
|
||||
edited={edited_block_id == block_id}
|
||||
selected={is_block_selected}
|
||||
delete_mode={*delete_mode}
|
||||
delete_block={handle_delete_block.clone()}
|
||||
port_status={port_status}
|
||||
@@ -595,3 +927,4 @@ pub fn SourceEditor() -> Html {
|
||||
</ContextProvider<SourceEditorContext>>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -246,6 +246,6 @@ pub fn layout(blocks: &mut [Block], connections: &[Connection]) {
|
||||
|
||||
let block = &mut blocks[*block_id as usize -1];
|
||||
block.position = (CANVAS_OFFSET, final_y);
|
||||
last_input_y = final_y + BLOCK_HEIGHT + Y_GAP;
|
||||
last_input_y = final_y + BLOCK_HEIGHT + Y_GAP*2.0;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ pub struct SourceEditorSidebarProps {
|
||||
pub on_toggle_delete: Callback<(String, MouseEvent)>,
|
||||
#[prop_or_default]
|
||||
pub on_drag_start: Callback<DragEvent>,
|
||||
#[prop_or_default]
|
||||
pub on_layout: Callback<(String, MouseEvent)>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
@@ -49,6 +51,7 @@ pub fn SourceEditorSidebar(props: &SourceEditorSidebarProps) -> Html {
|
||||
// Sidebar
|
||||
<div class="tp__source-editor__sidebar">
|
||||
<div class="tp__source-editor__sidebar-actions">
|
||||
<IconButton name="layout" icon="Nodes" onclick={props.on_layout.clone()} />
|
||||
// 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>
|
||||
|
||||
@@ -18,7 +18,7 @@ block_expr = { "{" ~ statements ~ "}" }
|
||||
condition = { function_call | var_access | field_access }
|
||||
assignment = { (field_access | identifier) ~ "=" ~ expression }
|
||||
expression = { assignment | map_block | match_block | function_call | regex_expr | string_literal | number | var_access | field_access | null | block_expr }
|
||||
function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" | "number" | "first" | "template" | "replace" }
|
||||
function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" | "number" | "first" | "template" | "replace" | "pad" | "format"}
|
||||
function_call = { function_name ~ "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" }
|
||||
any_match = { "_" }
|
||||
match_case_key = { any_match | identifier }
|
||||
|
||||
+153
-14
@@ -1,21 +1,22 @@
|
||||
#![allow(clippy::empty_docs)]
|
||||
|
||||
use crate::foundation::filter::{ValueAccessor};
|
||||
use crate::error::{create_tuliprox_error_result, info_err, TuliproxError, TuliproxErrorKind};
|
||||
use crate::foundation::filter::ValueAccessor;
|
||||
use crate::foundation::mapper::EvalResult::{AnyValue, Failure, Named, Number, Undefined, Value};
|
||||
use crate::model::{PatternTemplate, TemplateValue};
|
||||
use crate::utils::Capitalize;
|
||||
use log::{debug, trace};
|
||||
use pest::iterators::{Pair, Pairs};
|
||||
use pest::Parser;
|
||||
use pest_derive::Parser;
|
||||
use regex::Regex;
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use pest::Parser;
|
||||
use crate::error::{TuliproxError, TuliproxErrorKind, create_tuliprox_error_result, info_err};
|
||||
use crate::foundation::mapper::EvalResult::{AnyValue, Failure, Named, Number, Undefined, Value};
|
||||
use crate::model::{PatternTemplate, TemplateValue};
|
||||
use crate::utils::Capitalize;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar_inline = r##"
|
||||
@@ -39,7 +40,7 @@ block_expr = { "{" ~ statements ~ "}" }
|
||||
condition = { function_call | var_access | field_access }
|
||||
assignment = { (field_access | identifier) ~ "=" ~ expression }
|
||||
expression = { assignment | map_block | match_block | function_call | regex_expr | string_literal | number | var_access | field_access | null | block_expr }
|
||||
function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" | "number" | "first" | "template" | "replace" }
|
||||
function_name = { "concat" | "uppercase" | "lowercase" | "capitalize" | "trim" | "print" | "number" | "first" | "template" | "replace" | "pad" | "format" }
|
||||
function_call = { function_name ~ "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" }
|
||||
any_match = { "_" }
|
||||
match_case_key = { any_match | identifier }
|
||||
@@ -118,6 +119,8 @@ pub enum BuiltInFunction {
|
||||
First,
|
||||
Template,
|
||||
Replace,
|
||||
Pad,
|
||||
Format,
|
||||
}
|
||||
|
||||
impl FromStr for BuiltInFunction {
|
||||
@@ -135,6 +138,8 @@ impl FromStr for BuiltInFunction {
|
||||
"first" => Ok(Self::First),
|
||||
"template" => Ok(Self::Template),
|
||||
"replace" => Ok(Self::Replace),
|
||||
"pad" => Ok(Self::Pad),
|
||||
"format" => Ok(Self::Format),
|
||||
_ => create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown function {s}"),
|
||||
}
|
||||
}
|
||||
@@ -153,6 +158,8 @@ impl Display for BuiltInFunction {
|
||||
Self::First => "first",
|
||||
Self::Template => "template",
|
||||
Self::Replace => "replace",
|
||||
Self::Pad => "pad",
|
||||
Self::Format => "format",
|
||||
}.to_owned();
|
||||
write!(f, "{str}")
|
||||
}
|
||||
@@ -620,7 +627,7 @@ impl<'a> MapperContext<'a> {
|
||||
}
|
||||
Some(hash_map)
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,13 +703,18 @@ impl<'a> MapperContext<'a> {
|
||||
if args.len() > 1 {
|
||||
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Function accepts only one argument {:?}, {} given", name, args.len());
|
||||
}
|
||||
},
|
||||
}
|
||||
BuiltInFunction::Replace => {
|
||||
if args.len() != 3 {
|
||||
if args.len() != 3 {
|
||||
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Function accepts three arguments {:?}, {} given", name, args.len());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
BuiltInFunction::Pad => {
|
||||
if !(args.len() == 3 || args.len() == 4) {
|
||||
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Function accepts three or four arguments {:?}, {} given", name, args.len());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for expr_id in args {
|
||||
self.validate_expr(*expr_id, identifiers)?;
|
||||
@@ -931,8 +943,22 @@ fn concat_args(args: &Vec<EvalResult>) -> Vec<Cow<'_, str>> {
|
||||
|
||||
macro_rules! extract_evaluated_arg_value {
|
||||
($evaluated_args:expr, $index:expr) => {{
|
||||
let evaluated_arg = &$evaluated_args[$index];
|
||||
match evaluated_arg {
|
||||
if $index >= $evaluated_args.len() {
|
||||
None
|
||||
} else {
|
||||
let evaluated_arg = &$evaluated_args[$index];
|
||||
match evaluated_arg {
|
||||
Value(value) => Some(value),
|
||||
Named(values) => values.first().map(|(_key, val)| val),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! extract_arg_value {
|
||||
($evaluated_arg:expr) => {{
|
||||
match $evaluated_arg {
|
||||
Value(value) => Some(value),
|
||||
Named(values) => values.first().map(|(_key, val)| val),
|
||||
_ => None,
|
||||
@@ -1122,6 +1148,98 @@ impl Expression {
|
||||
evaluated_args[0].clone()
|
||||
}
|
||||
}
|
||||
BuiltInFunction::Pad => {
|
||||
let value = match &evaluated_args[0] {
|
||||
Number(value) => Some(value.to_string()),
|
||||
Value(value) => Some(value.clone()),
|
||||
Named(values) => values.first().map(|(_key, val)| val.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let width = match &evaluated_args[1] {
|
||||
Number(value) => {
|
||||
if value.is_nan() || value.is_infinite() {
|
||||
0
|
||||
} else {
|
||||
value.abs().min(usize::MAX as f64) as usize
|
||||
}
|
||||
}
|
||||
Value(value) => value.parse::<usize>().ok().unwrap_or(0),
|
||||
Named(values) => values.first().and_then(|(_key, val)| val.parse::<usize>().ok()).unwrap_or(0),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let fill = match &evaluated_args[2] {
|
||||
Number(value) => Some(value.to_string()),
|
||||
Value(value) => Some(value.clone()),
|
||||
Named(values) => values.first().map(|(_key, val)| val.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let align = extract_evaluated_arg_value!(evaluated_args, 3); // "<", ">", "^"
|
||||
|
||||
if let Some(text) = value {
|
||||
let fill_char = fill.and_then(|s| s.chars().next()).unwrap_or(' ');
|
||||
|
||||
let padded = if width <= text.len() {
|
||||
text.clone()
|
||||
} else {
|
||||
let pad = width - text.len();
|
||||
if let Some(al) = align {
|
||||
match al.as_str() {
|
||||
"^" => {
|
||||
let left = pad / 2;
|
||||
let right = pad - left;
|
||||
format!(
|
||||
"{}{}{}",
|
||||
fill_char.to_string().repeat(left),
|
||||
text,
|
||||
fill_char.to_string().repeat(right)
|
||||
)
|
||||
}
|
||||
"<" => format!("{}{}", text, fill_char.to_string().repeat(pad)),
|
||||
_ => format!("{}{}", fill_char.to_string().repeat(pad), text),
|
||||
}
|
||||
} else {
|
||||
format!("{}{}", fill_char.to_string().repeat(pad), text)
|
||||
}
|
||||
};
|
||||
|
||||
Value(padded)
|
||||
} else {
|
||||
Undefined
|
||||
}
|
||||
}
|
||||
BuiltInFunction::Format => {
|
||||
let fmt_pattern = extract_evaluated_arg_value!(evaluated_args, 0);
|
||||
|
||||
if let Some(fmt_str) = fmt_pattern {
|
||||
let args: Vec<Option<&String>> = evaluated_args.iter()
|
||||
.skip(1)
|
||||
.map(|v| extract_arg_value!(v))
|
||||
.collect();
|
||||
|
||||
let mut formatted = String::new();
|
||||
let mut arg_iter = args.iter();
|
||||
let mut chars = fmt_str.chars().peekable();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '{' && chars.peek() == Some(&'}') {
|
||||
chars.next();
|
||||
if let Some(Some(arg)) = arg_iter.next() {
|
||||
write!(formatted, "{}", arg).unwrap();
|
||||
} else {
|
||||
formatted.push_str("{}");
|
||||
}
|
||||
} else {
|
||||
formatted.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
Value(formatted)
|
||||
} else {
|
||||
Undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1381,6 +1499,27 @@ mod tests {
|
||||
}
|
||||
"#;
|
||||
let mapper = MapperScript::parse(script, None).expect("Parsing failed");
|
||||
println!("Program: {mapper:?}");
|
||||
println!("Program: {mapper:?}")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mapper_format() {
|
||||
let dsl = r#"
|
||||
@Name = pad(1000, 10, 0);
|
||||
@Title = format("Hello {} how {}", "a", "b");
|
||||
"#;
|
||||
|
||||
let mapper = MapperScript::parse(dsl, None).expect("Parsing failed");
|
||||
let mut channels: Vec<PlaylistItem> = vec![
|
||||
("D", "HD"),
|
||||
].into_iter().map(|(name, quality)| PlaylistItem { header: PlaylistItemHeader { title: format!("Chanel {name} [{quality}]"), ..Default::default() } }).collect::<Vec<PlaylistItem>>();
|
||||
|
||||
for pli in &mut channels {
|
||||
let mut accessor = ValueAccessor {
|
||||
pli,
|
||||
};
|
||||
mapper.eval(&mut accessor, None);
|
||||
println!("Result: {pli:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ impl ConfigTargetDto {
|
||||
let mut hdhomerun_needs_m3u = false;
|
||||
let mut hdhomerun_needs_xtream = false;
|
||||
|
||||
let mut strm_export_styles = vec![];
|
||||
//let mut strm_export_styles = vec![];
|
||||
let mut strm_directories: Vec<&str> = vec![];
|
||||
|
||||
for target_output in &mut self.output {
|
||||
@@ -276,10 +276,10 @@ impl ConfigTargetDto {
|
||||
if has_username {
|
||||
strm_needs_xtream = true;
|
||||
}
|
||||
if strm_export_styles.contains(&strm_output.style) {
|
||||
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "strm outputs with same export style are not allowed: {}", self.name);
|
||||
}
|
||||
strm_export_styles.push(strm_output.style);
|
||||
// if strm_export_styles.contains(&strm_output.style) {
|
||||
// return create_tuliprox_error_result!(TuliproxErrorKind::Info, "strm outputs with same export style are not allowed: {}", self.name);
|
||||
// }
|
||||
// strm_export_styles.push(strm_output.style);
|
||||
if strm_directories.contains(&strm_output.directory.as_str()) {
|
||||
return create_tuliprox_error_result!(TuliproxErrorKind::Info, "strm outputs with same export directory are not allowed: {}", self.name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user