save config

This commit is contained in:
euzu
2025-08-30 13:42:38 +02:00
parent 21e85cf965
commit 959207c86e
9 changed files with 371 additions and 53 deletions
+2 -4
View File
@@ -18,11 +18,9 @@
margin-left: 0.5rem;
font-weight: bold;
cursor: pointer;
color: #666;
fill: var(--menu-button-warn-hover-color);
color: var(--modest-text-color);
&:hover {
color: red;
color: var(--menu-button-warn-hover-color);
}
}
}
@@ -21,4 +21,41 @@
.svg-icon {
pointer-events: none;
}
}
.tp__icon-button.tp__button-primary {
background-color: var(--text-button-primary-background-color);
color: var(--text-button-primary-color);
fill : var(--text-button-primary-color);
&:focus,
&:hover {
background-color: var(--text-button-primary-hover-background-color);
color: var(--text-button-primary-hover-color);
fill : var(--text-button-primary-hover-color);
outline: none;
}
}
.tp__icon-button.tp__button-secondary {
background-color: var(--text-button-secondary-background-color);
color: var(--text-button-secondary-color);
fill : var(--text-button-secondary-color);
&:focus,
&:hover {
background-color: var(--text-button-secondary-hover-background-color);
color: var(--text-button-secondary-hover-color);
fill : var(--text-button-secondary-hover-color);
outline: none;
}
}
.tp__icon-button.tp__button-active {
background-color: var(--text-button-active-background-color);
color: var(--text-button-active-color);
fill : var(--text-button-active-color);
&:focus,
&:hover {
background-color: var(--text-button-active-hover-background-color);
color: var(--text-button-active-hover-color);
fill : var(--text-button-active-hover-color);
outline: none;
}
}
@@ -47,5 +47,12 @@
gap: var(--gap-default);
color: var(--text-color);
pointer-events: none;
span {
color: var(--tag-color);
background-color: var(--tag-background-color);
padding: var(--padding-micro) var(--padding-mini);
border-radius: var(--border-radius);
}
}
}
@@ -91,24 +91,66 @@
}
.tp__schedules-config-view {
&__body {
display: flex;
flex-flow: column;
gap: var(--gap-default);
box-sizing: border-box;
overflow: auto;
}
&__schedule {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(size.$config-grid-cell-width, 1fr));
max-width: calc(size.$config-max-grid-cells * size.$config-grid-cell-width + 2 * 1rem);
display: flex;
flex-flow: column;
gap: var(--gap-default);
box-sizing: border-box;
padding: var(--padding-default);
border: 1px solid var(--card-border-color);
border-radius: var(--border-radius);
.tp__config-view__table {
th {
text-align: left;
}
th,td {
width: 1%;
white-space: nowrap;
}
th:last-child,
td:last-child {
width: auto;
}
td {
vertical-align: middle;
}
.tp__config-view__tags {
padding-left: var(--padding-default);
}
}
}
&__footer {
margin-top: var(--margin-default);
padding-top: var(--margin-default);
border-top : 1px solid var(--card-border-color);
svg {
width: 240px;
height: 60px;
width: 320px;
height: 90px;
}
fill: var(--label-color);
}
&__editor {
display: flex;
flex-flow: row wrap;
align-items: flex-end;
gap: var(--gap-default);
box-sizing: border-box;
.tp__icon-button {
svg {
width: 2.3rem;
}
}
}
&__delete-btn {
fill: var(--menu-button-warn-hover-color);
}
}
@@ -1,6 +1,7 @@
use shared::error::TuliproxError;
use shared::info_err;
use shared::model::{ConfigApiDto, HdHomeRunConfigDto, IpCheckConfigDto, MainConfigDto, MessagingConfigDto, ProxyConfigDto, ReverseProxyConfigDto, ScheduleConfigDto, VideoConfigDto, WebUiConfigDto};
use shared::model::{ConfigApiDto, HdHomeRunConfigDto, IpCheckConfigDto, MainConfigDto, MessagingConfigDto,
ProxyConfigDto, ReverseProxyConfigDto, SchedulesConfigDto, VideoConfigDto, WebUiConfigDto};
use std::fmt;
use std::str::FromStr;
@@ -71,7 +72,7 @@ impl fmt::Display for ConfigPage {
pub enum ConfigForm {
Main(bool, MainConfigDto),
Api(bool, ConfigApiDto),
Schedules(bool, ScheduleConfigDto),
Schedules(bool, SchedulesConfigDto),
Video(bool, VideoConfigDto),
Messaging(bool, MessagingConfigDto),
WebUi(bool, WebUiConfigDto),
File diff suppressed because one or more lines are too long
+7 -4
View File
@@ -2,6 +2,10 @@ use std::rc::Rc;
use yew::prelude::*;
use crate::app::components::{DropDownIconButton, DropDownOption};
fn map_selection(o: &Rc<DropDownOption>) -> Html {
html! {<span>{o.label.clone()}</span>}
}
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct SelectProps {
pub name: String,
@@ -19,13 +23,12 @@ pub struct SelectProps {
pub fn Select(props: &SelectProps) -> Html {
let button_ref = use_node_ref();
let selected_options = use_state(|| props.options.iter().filter(|o| o.selected)
.map(|o| o.label.clone()).collect::<Vec<Html>>());
let selected_options = use_state(Vec::new);
{
let set_selected_options = selected_options.clone();
use_effect_with(props.options.clone(), move |options: &Vec<Rc<DropDownOption>>| {
let selections = options.iter().filter(|o| o.selected)
.map(|o| o.label.clone()).collect::<Vec<Html>>();
.map(map_selection).collect::<Vec<Html>>();
set_selected_options.set(selections);
});
}
@@ -36,7 +39,7 @@ pub fn Select(props: &SelectProps) -> Html {
let set_selections = selected_options.clone();
Callback::from(move |(name, selections): (String, Vec<String>)| {
let selected_options: Vec<Rc<DropDownOption>> = options.iter().filter(|&o| selections.contains(&o.id)).cloned().collect();
set_selections.set(selected_options.iter().map(|o| o.label.clone()).collect());
set_selections.set(selected_options.iter().map(map_selection).collect());
onselect.emit((name, selected_options));
})
};
+17
View File
@@ -95,6 +95,23 @@ impl From<&ConfigDto> for MainConfigDto {
}
}
// This SchedulesConfigDto is a copy of ConfigDto schedules fields for form editing.
// It has no other purpose than editing and saving the schedules
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
pub struct SchedulesConfigDto {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schedules: Option<Vec<ScheduleConfigDto>>,
}
impl From<&ConfigDto> for SchedulesConfigDto {
fn from(config: &ConfigDto) -> Self {
Self {
schedules: config.schedules.clone(),
}
}
}
pub struct HdHomeRunDeviceOverview {
pub enabled: bool,
pub devices: Vec<String>,
+41
View File
@@ -1,4 +1,45 @@
pub fn validate_cron(expr: &str) -> Result<(), String> {
let parts: Vec<&str> = expr.split_whitespace().collect();
if parts.len() != 7 {
return Err("Cron expression must have 7 fields".into());
}
// helper: check valid range
fn check_field(val: &str, min: u32, max: u32) -> bool {
val == "*" ||
val.split(',').all(|chunk| {
if let Ok(num) = chunk.parse::<u32>() {
num >= min && num <= max
} else if let Some((from, to)) = chunk.split_once('-') {
if let (Ok(f), Ok(t)) = (from.parse::<u32>(), to.parse::<u32>()) {
f <= t && f >= min && t <= max
} else { false }
} else {
false
}
})
}
let ranges = [
(0, 59), // seconds
(0, 59), // minutes
(0, 23), // hours
(1, 31), // day of month
(1, 12), // month
(0, 6), // day of week
(1970, 2099), // year (example range)
];
for (field, (min, max)) in parts.iter().zip(ranges.iter()) {
if !check_field(field, *min, *max) {
return Err(format!("Invalid field '{}', expected {}-{}", field, min, max));
}
}
Ok(())
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ScheduleConfigDto {