Merge branch 'feature/source_editor' into feature/resource-retry-config-by-dark_breakpoint

This commit is contained in:
euzu
2025-11-11 10:52:26 +01:00
4 changed files with 255 additions and 173 deletions
+5
View File
@@ -1,6 +1,11 @@
# Changelog
# 3.1.9 (2025-11-xx)
- Added `name` attribute to Staged Input.
- Real-time active provider connection monitoring (dashboard + websocket)
- Source editor: block selection, batch-mode UI and automatic layout
- Fixed SSL certificate field binding in configuration view
- 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.
# 3.1.8 (2025-11-06)
@@ -25,6 +25,18 @@ impl BlockType {
pub const OUTPUT_XTREAM: &'static str = "OutputXtream";
pub const OUTPUT_HDHOMERUN: &'static str = "OutputHdHomeRun";
pub const OUTPUT_STRM: &'static str = "OutputStrm";
pub fn is_input(&self) -> bool {
matches!(self, Self::InputXtream | Self::InputM3u)
}
pub fn is_target(&self) -> bool {
matches!(self, Self::Target)
}
// pub fn is_output(&self) -> bool {
// matches!(self, Self::OutputXtream | Self::OutputM3u | Self::OutputHdHomeRun | Self::OutputStrm)
// }
}
// Convert from String to BlockType
@@ -4,7 +4,7 @@ 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::cluster_layout;
use crate::app::components::source_editor::layout::layout;
const BLOCK_WIDTH: f32 = 100.0;
const BLOCK_HEIGHT: f32 = 50.0;
@@ -95,6 +95,10 @@ 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 {
@@ -119,7 +123,7 @@ pub fn SourceEditor() -> Html {
}
}
cluster_layout(&mut gen_blocks, &gen_connections);
layout(&mut gen_blocks, &gen_connections);
get_next_id.set(current_id);
blocks_set.set(gen_blocks);
connections_set.set(gen_connections);
@@ -1,190 +1,251 @@
use std::collections::{HashMap, HashSet};
use crate::app::components::{BlockId, BlockType, Block, Connection};
use crate::app::components::{Block, BlockId, Connection};
use std::collections::HashMap;
const BLOCK_SIZE: f32 = 50.0;
const GAP: f32 = 20.0;
const LAYER_DISTANCE: f32 = BLOCK_SIZE * 3.0;
const CANVAS_OFFSET: f32 = 10.0;
const ITERATIONS: usize = 5;
/// Cluster structure
struct Cluster {
targets: Vec<BlockId>,
inputs: Vec<BlockId>,
outputs_per_target: HashMap<BlockId, Vec<BlockId>>,
const Y_GAP: f32 = 25.0;
const X_GAP: f32 = 50.0;
const BLOCK_HEIGHT: f32 = 50.0;
const BLOCK_WIDTH: f32 = 200.0;
struct TargetBlock {
id: BlockId,
outputs: Option<Vec<BlockId>>,
height: f32,
position: (f32, f32),
}
/// Build clusters from connections
fn build_clusters(blocks: &[Block], connections: &[Connection]) -> Vec<Cluster> {
let mut clusters = Vec::new();
let mut visited_targets = HashSet::new();
let block_map: HashMap<BlockId, &Block> = blocks.iter().map(|b| (b.id, b)).collect();
impl TargetBlock {
pub fn new(id: BlockId, outputs: Option<Vec<BlockId>>) -> Self {
let height = Self::height(outputs.as_ref());
TargetBlock { id, outputs, height, position: (0.0, 0.0) }
}
for block in blocks {
if block.block_type != BlockType::Target || visited_targets.contains(&block.id) {
fn height(outputs: Option<&Vec<BlockId>>) -> f32 {
if let Some(outs) = outputs {
let len = outs.len() as f32;
((len * BLOCK_HEIGHT) + ((len - 1.0) * Y_GAP)).max(BLOCK_HEIGHT)
} else {
BLOCK_HEIGHT
}
}
pub fn set_position(&mut self, x: f32, y: f32, blocks: &mut [Block]) {
self.position = (x, y);
let out_x = x + BLOCK_WIDTH + X_GAP;
let mut out_y = y;
if let Some(outputs) = self.outputs.as_ref() {
for out in outputs {
blocks[*out as usize -1].position = (out_x, out_y);
out_y += BLOCK_HEIGHT + Y_GAP;
}
}
blocks[self.id as usize - 1].position = (x, y + (self.height - BLOCK_HEIGHT) / 2.0);
}
}
fn build_target_blocks(blocks: &mut [Block], connections: &[Connection]) -> Vec<TargetBlock> {
let mut out_edges: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
let mut in_edges: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
for c in connections {
out_edges.entry(c.from).or_default().push(c.to);
in_edges.entry(c.to).or_default().push(c.from);
}
blocks
.iter()
.filter(|b| b.block_type.is_target())
.map(|b| TargetBlock::new(b.id, out_edges.get(&b.id).cloned()))
.collect()
}
/// calcuates Barycenter for a Block, based on connected Blocks in given Order-Array
fn barycenter(id: BlockId, map: &HashMap<BlockId, Vec<BlockId>>, order: &[BlockId]) -> f32 {
if let Some(connected) = map.get(&id) {
let mut sum = 0.0;
let mut count = 0;
for &c in connected {
if let Some(pos) = order.iter().position(|&x| x == c) {
sum += pos as f32;
count += 1;
}
}
if count == 0 { f32::INFINITY } else { sum / count as f32 }
} else {
f32::INFINITY
}
}
/// Counts crossings
fn count_crossings(
input_order: &[BlockId],
target_order: &[BlockId],
connections: &[Connection],
) -> usize {
let input_index: HashMap<BlockId, usize> =
input_order.iter().enumerate().map(|(i, &id)| (id, i)).collect();
let target_index: HashMap<BlockId, usize> =
target_order.iter().enumerate().map(|(i, &id)| (id, i)).collect();
let mut count = 0;
for (i, c1) in connections.iter().enumerate() {
if !input_index.contains_key(&c1.from) || !target_index.contains_key(&c1.to) {
continue;
}
for c2 in &connections[i + 1..] {
if !input_index.contains_key(&c2.from) || !target_index.contains_key(&c2.to) {
continue;
}
let i1 = input_index[&c1.from];
let j1 = target_index[&c1.to];
let i2 = input_index[&c2.from];
let j2 = target_index[&c2.to];
let mut cluster = Cluster {
targets: vec![block.id],
inputs: Vec::new(),
outputs_per_target: HashMap::new(),
};
visited_targets.insert(block.id);
// Collect inputs connected to this target
for conn in connections.iter().filter(|c| c.to == block.id) {
if let Some(input) = block_map.get(&conn.from) {
if matches!(input.block_type, BlockType::InputXtream | BlockType::InputM3u) {
cluster.inputs.push(input.id);
}
if (i1 < i2 && j1 > j2) || (i1 > i2 && j1 < j2) {
count += 1;
}
}
// Collect outputs for this target
let outputs: Vec<BlockId> = connections.iter()
.filter(|c| c.from == block.id)
.filter_map(|c| block_map.get(&c.to).filter(|b| matches!(b.block_type,
BlockType::OutputXtream |
BlockType::OutputM3u |
BlockType::OutputHdHomeRun |
BlockType::OutputStrm)).map(|b| b.id))
.collect();
cluster.outputs_per_target.insert(block.id, outputs);
clusters.push(cluster);
}
clusters
count
}
/// Main hierarchical cluster layout function
pub fn cluster_layout(blocks: &mut [Block], connections: &[Connection]) {
let clusters = build_clusters(blocks, connections);
let mut block_map: HashMap<BlockId, &mut Block> = blocks.iter_mut().map(|b| (b.id, b)).collect();
let mut y_offset = CANVAS_OFFSET;
let mut placed_inputs = HashSet::new();
for cluster in clusters {
// Compute cluster height based on max(column heights)
let num_targets = cluster.targets.len();
let num_inputs = cluster.inputs.len();
let max_outputs = cluster.outputs_per_target.values().map(|v| v.len()).max().unwrap_or(0);
let cluster_height = ((num_targets.max(num_inputs).max(max_outputs)) as f32 * BLOCK_SIZE) +
(((num_targets.max(num_inputs).max(max_outputs)) -1) as f32 * GAP);
// --- Step 1: Place Targets (raw Y, ignore outputs for now)
let target_start_y = y_offset + (cluster_height - (num_targets as f32 * BLOCK_SIZE + (num_targets-1) as f32*GAP))/2.0;
let mut target_y_map = HashMap::new();
for (i, &target_id) in cluster.targets.iter().enumerate() {
let y = target_start_y + i as f32 * (BLOCK_SIZE + GAP) + BLOCK_SIZE/2.0;
if let Some(t) = block_map.get_mut(&target_id) {
t.position = (LAYER_DISTANCE + CANVAS_OFFSET, y);
target_y_map.insert(target_id, y);
}
}
// --- Step 2: Place Inputs (centered on connected targets)
let mut input_positions: HashMap<BlockId, f32> = HashMap::new();
let mut input_to_targets: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
for &input_id in &cluster.inputs {
let connected_targets: Vec<BlockId> = connections.iter()
.filter(|c| c.from == input_id && cluster.targets.contains(&c.to))
.map(|c| c.to)
.collect();
input_to_targets.insert(input_id, connected_targets.clone());
// Average Y of connected targets
let y = if !connected_targets.is_empty() {
connected_targets.iter().map(|t| target_y_map[t]).sum::<f32>() / connected_targets.len() as f32
} else {
cluster_height / 2.0 + y_offset
};
input_positions.insert(input_id, y);
}
// --- Step 3: Spread inputs exclusive to a single target
for &target_id in &cluster.targets {
let exclusive_inputs: Vec<BlockId> = input_to_targets.iter()
.filter(|(_, targets)| targets.len() == 1 && targets[0] == target_id)
.map(|(&id, _)| id)
.collect();
let count = exclusive_inputs.len();
if count > 1 {
let target_y = target_y_map[&target_id];
let total_height = count as f32 * BLOCK_SIZE + (count-1) as f32 * GAP;
let start_y = target_y - total_height / 2.0 + BLOCK_SIZE / 2.0;
for (i, input_id) in exclusive_inputs.iter().enumerate() {
input_positions.insert(*input_id, start_y + i as f32 * (BLOCK_SIZE + GAP));
}
}
}
// --- Step 4: Resolve overlaps iteratively
for _ in 0..ITERATIONS {
let mut sorted_inputs: Vec<(BlockId, f32)> = input_positions.iter().map(|(id, y)| (*id, *y)).collect();
sorted_inputs.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap());
for i in 1..sorted_inputs.len() {
let prev_y = sorted_inputs[i-1].1;
let curr_id = sorted_inputs[i].0;
let curr_y = sorted_inputs[i].1;
if curr_y - prev_y < BLOCK_SIZE + GAP {
let new_y = prev_y + BLOCK_SIZE + GAP;
if let Some(y_val) = input_positions.get_mut(&curr_id) {
*y_val = new_y;
}
}
}
}
// --- Step 5: Commit final input positions
for (&input_id, &y) in input_positions.iter() {
if let Some(b) = block_map.get_mut(&input_id) {
b.position = (CANVAS_OFFSET, y);
placed_inputs.insert(input_id);
}
}
// --- Step 6: Place Outputs (centered under target)
for (&target_id, outputs) in cluster.outputs_per_target.iter() {
let target_y = target_y_map[&target_id];
let output_count = outputs.len();
if output_count == 0 { continue; }
let outputs_height = output_count as f32 * BLOCK_SIZE + (output_count as f32 -1.0)*GAP;
let output_start_y = target_y - outputs_height / 2.0;
for (i, &out_id) in outputs.iter().enumerate() {
if let Some(b) = block_map.get_mut(&out_id) {
let y = output_start_y + i as f32 * (BLOCK_SIZE + GAP) + BLOCK_SIZE/2.0;
b.position = (2.0*LAYER_DISTANCE + CANVAS_OFFSET, y);
}
}
}
// --- Step 7: Update y_offset for next cluster
y_offset += cluster_height + GAP*2.0;
}
// --- Step 8: Place orphan inputs in a separate column
let orphan_inputs: Vec<BlockId> = block_map.keys()
.filter(|id| {
let b = block_map.get(id).unwrap();
matches!(b.block_type, BlockType::InputXtream | BlockType::InputM3u)
&& !placed_inputs.contains(id)
})
.cloned()
/// Barycentric Sort
pub fn barycentric_sort(
blocks: &[Block],
connections: &[Connection],
iterations: usize,
) -> (Vec<BlockId>, Vec<BlockId>) {
// Initiale Reihenfolge
let mut input_order: Vec<BlockId> = blocks
.iter()
.filter(|b| b.block_type.is_input())
.map(|b| b.id)
.collect();
let mut target_order: Vec<BlockId> = blocks
.iter()
.filter(|b| b.block_type.is_target())
.map(|b| b.id)
.collect();
for (i, input_id) in orphan_inputs.iter().enumerate() {
if let Some(b) = block_map.get_mut(input_id) {
let y = CANVAS_OFFSET + i as f32 * (BLOCK_SIZE + GAP) + BLOCK_SIZE / 2.0;
b.position = (CANVAS_OFFSET, y);
let mut input_to_targets: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
let mut target_to_inputs: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
for con in connections {
if blocks[con.from as usize - 1].block_type.is_input()
&& blocks[con.to as usize - 1].block_type.is_target()
{
input_to_targets.entry(con.from).or_default().push(con.to);
target_to_inputs.entry(con.to).or_default().push(con.from);
}
}
// Iterative Barycenter-Sortierung
for _ in 0..iterations {
// sort inputs by middle value of targets
input_order.sort_by(|&a, &b| {
barycenter(a, &input_to_targets, &target_order)
.partial_cmp(&barycenter(b, &input_to_targets, &target_order))
.unwrap()
});
// sort targets by middle value of inputs
target_order.sort_by(|&a, &b| {
barycenter(a, &target_to_inputs, &input_order)
.partial_cmp(&barycenter(b, &target_to_inputs, &input_order))
.unwrap()
});
}
// simple local cross optimisation for inputs
let mut improved = true;
for _ in 0..10 {
if !improved { break; }
improved = false;
for i in 0..input_order.len().saturating_sub(1) {
let mut swapped = input_order.clone();
swapped.swap(i, i + 1);
if count_crossings(&swapped, &target_order, connections) < count_crossings(&input_order, &target_order, connections) {
input_order.swap(i, i + 1);
improved = true;
}
}
}
// simple local cross optimisation for targets
improved = true;
for _ in 0..10 {
if !improved { break; }
improved = false;
for i in 0..target_order.len().saturating_sub(1) {
let mut swapped = target_order.clone();
swapped.swap(i, i + 1);
if count_crossings(&input_order, &swapped, connections) < count_crossings(&input_order, &target_order, connections) {
target_order.swap(i, i + 1);
improved = true;
}
}
}
(input_order, target_order)
}
pub fn layout(blocks: &mut [Block], connections: &[Connection]) {
let (input_order, target_order) = barycentric_sort(blocks, connections, 5);
let mut target_blocks = build_target_blocks(blocks, connections);
target_blocks.sort_by_key(|a| target_order.iter().position(|&id| id == a.id).unwrap());
let mut start_y = CANVAS_OFFSET;
let start_x = CANVAS_OFFSET + BLOCK_WIDTH + X_GAP;
for target_block in &mut target_blocks {
target_block.set_position(start_x, start_y, blocks);
start_y += target_block.height + Y_GAP;
}
let target_map: HashMap<BlockId, &TargetBlock> = target_blocks
.iter()
.map(|t| (t.id, t))
.collect();
let mut last_input_y = CANVAS_OFFSET;
for block_id in &input_order {
let connected_targets: Vec<&TargetBlock> = connections
.iter()
.filter(|c| c.from == *block_id)
.filter_map(|c| target_map.get(&c.to))
.copied()
.collect();
let desired_y = if connected_targets.is_empty() {
last_input_y
} else {
// Center Y-Position of target
let min_y = connected_targets
.iter()
.map(|t| t.position.1)
.fold(f32::INFINITY, |a, b| a.min(b));
let max_y = connected_targets
.iter()
.map(|t| t.position.1 + t.height)
.fold(f32::NEG_INFINITY, |a, b| a.max(b));
(min_y + max_y)/2.0 - BLOCK_HEIGHT/2.0
};
let mut final_y = desired_y;
// prevent overlap
if final_y < last_input_y {
final_y = last_input_y;
}
let block = &mut blocks[*block_id as usize -1];
block.position = (CANVAS_OFFSET, final_y);
last_input_y = final_y + BLOCK_HEIGHT + Y_GAP;
}
}