diff --git a/backend/src/model/config/input.rs b/backend/src/model/config/input.rs index 98fe6467f..1898d040d 100644 --- a/backend/src/model/config/input.rs +++ b/backend/src/model/config/input.rs @@ -1,5 +1,5 @@ use crate::model::{macros, EpgConfig}; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::{TuliproxError}; use shared::{check_input_connections, info_err, write_if_some}; use shared::model::{ConfigInputAliasDto, ConfigInputDto, ConfigInputOptionsDto, InputFetchMethod, InputType, StagedInputDto}; use shared::utils::{get_base_url_from_str, get_credentials_from_url}; diff --git a/backend/src/processing/processor/playlist.rs b/backend/src/processing/processor/playlist.rs index 3af2f7bc7..86c7b40ed 100644 --- a/backend/src/processing/processor/playlist.rs +++ b/backend/src/processing/processor/playlist.rs @@ -28,7 +28,7 @@ use crate::utils::StepMeasure; use deunicode::deunicode; use log::{debug, error, info, log_enabled, trace, warn, Level}; use reqwest::Client; -use shared::error::{get_errors_notify_message, notify_err, TuliproxError, TuliproxErrorKind}; +use shared::error::{get_errors_notify_message, notify_err, TuliproxError}; use shared::foundation::filter::{get_field_value, set_field_value, ValueAccessor, ValueProvider}; use shared::model::{CounterModifier, FieldGetAccessor, FieldSetAccessor, InputType, ItemField, MsgKind, PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistUpdateState, ProcessingOrder, UUIDType, XtreamCluster}; diff --git a/backend/src/processing/processor/xtream.rs b/backend/src/processing/processor/xtream.rs index 74f75e79b..4cdeb8ed6 100644 --- a/backend/src/processing/processor/xtream.rs +++ b/backend/src/processing/processor/xtream.rs @@ -1,5 +1,5 @@ use shared::error::{info_err, notify_err}; -use shared::error::{str_to_io_error, TuliproxError, TuliproxErrorKind}; +use shared::error::{str_to_io_error, TuliproxError}; use crate::model::{AppConfig, Config, ConfigInput, InputSource}; use crate::model::{FetchedPlaylist}; use shared::model::{PlaylistEntry, PlaylistItem, PlaylistItemType, XtreamCluster}; diff --git a/backend/src/processing/processor/xtream_series.rs b/backend/src/processing/processor/xtream_series.rs index c94f19fe3..b08772a62 100644 --- a/backend/src/processing/processor/xtream_series.rs +++ b/backend/src/processing/processor/xtream_series.rs @@ -1,5 +1,5 @@ use shared::model::InputType; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::{TuliproxError}; use crate::model::{AppConfig, ConfigTarget}; use crate::model::{FetchedPlaylist}; use shared::model::{PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster}; diff --git a/backend/src/processing/processor/xtream_vod.rs b/backend/src/processing/processor/xtream_vod.rs index 4ffaabdf2..3cf8bcf22 100644 --- a/backend/src/processing/processor/xtream_vod.rs +++ b/backend/src/processing/processor/xtream_vod.rs @@ -1,5 +1,5 @@ use shared::model::InputType; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::{TuliproxError}; use crate::model::{AppConfig, ConfigTarget}; use crate::model::{FetchedPlaylist}; use shared::model::{PlaylistItem, PlaylistItemType, XtreamCluster}; diff --git a/backend/src/repository/epg_repository.rs b/backend/src/repository/epg_repository.rs index eda7cee78..74ae3c513 100644 --- a/backend/src/repository/epg_repository.rs +++ b/backend/src/repository/epg_repository.rs @@ -1,4 +1,4 @@ -use shared::error::{notify_err, TuliproxError, TuliproxErrorKind}; +use shared::error::{notify_err, TuliproxError}; use crate::model::{Config, ConfigTarget, TargetOutput}; use crate::model::Epg; use crate::repository::m3u_repository::m3u_get_epg_file_path; diff --git a/backend/src/repository/m3u_playlist_iterator.rs b/backend/src/repository/m3u_playlist_iterator.rs index 8d8c60205..28c2dee8b 100644 --- a/backend/src/repository/m3u_playlist_iterator.rs +++ b/backend/src/repository/m3u_playlist_iterator.rs @@ -1,5 +1,5 @@ use shared::error::info_err; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::{TuliproxError}; use crate::model::{AppConfig, ProxyUserCredentials}; use crate::model::{ConfigTarget}; use shared::model::{ConfigTargetOptions, M3uPlaylistItem, PlaylistItemType, ProxyType, TargetType, XtreamCluster}; diff --git a/backend/src/repository/playlist_repository.rs b/backend/src/repository/playlist_repository.rs index 8a618c2d5..2786f0bdd 100644 --- a/backend/src/repository/playlist_repository.rs +++ b/backend/src/repository/playlist_repository.rs @@ -1,5 +1,5 @@ use shared::error::info_err; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::{TuliproxError}; use crate::model::{AppConfig, ConfigTarget, TargetOutput}; use shared::model::{PlaylistGroup, PlaylistItemType}; use crate::model::Epg; diff --git a/backend/src/repository/storage.rs b/backend/src/repository/storage.rs index 797615f28..200e58758 100644 --- a/backend/src/repository/storage.rs +++ b/backend/src/repository/storage.rs @@ -1,5 +1,5 @@ use std::path::{Path, PathBuf}; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::{TuliproxError}; use crate::model::{Config}; use shared::error::{notify_err}; use crate::repository::storage_const; diff --git a/backend/src/repository/xtream_playlist_iterator.rs b/backend/src/repository/xtream_playlist_iterator.rs index d8839b9de..47c4a6445 100644 --- a/backend/src/repository/xtream_playlist_iterator.rs +++ b/backend/src/repository/xtream_playlist_iterator.rs @@ -3,7 +3,7 @@ use log::error; use serde_json::Value; use shared::model::{TargetType, XtreamCluster, XtreamPlaylistItem}; use shared::error::info_err; -use shared::error::{TuliproxError, TuliproxErrorKind}; +use shared::error::{TuliproxError}; use crate::model::{xtream_playlistitem_to_document, AppConfig, ProxyUserCredentials}; use crate::model::{ConfigTarget}; use crate::model::XtreamMappingOptions; diff --git a/backend/src/utils/trakt/client.rs b/backend/src/utils/trakt/client.rs index 73c5f616c..43adf51e6 100644 --- a/backend/src/utils/trakt/client.rs +++ b/backend/src/utils/trakt/client.rs @@ -3,7 +3,7 @@ use shared::error::TuliproxError; use reqwest::header::{HeaderMap, HeaderValue}; use std::sync::Arc; use log::{debug, info}; -use shared::error::{info_err, TuliproxErrorKind}; +use shared::error::{info_err}; use shared::utils::trim_last_slash; use super::errors::{handle_trakt_api_error}; diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 43d82d585..f6da81c0f 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -35,6 +35,7 @@ "USER_UI": "User UI", "WEB_UI_PATH": "Path prefix", "SCHEDULES": "Schedules", + "SCHEDULE": "Schedule", "SANITIZE_SENSITIVE_INFO": "Sanitize sensitive info", "ACTIVE_USER": "Active User", "RESOURCE_REWRITE_DISABLE": "Resource Rewrite disable", @@ -105,6 +106,7 @@ "XTREAM_BATCH": "xc batch", "HDHOMERUN": "hdhomerun", "STRM": "strm", + "HDHOMERUN_CONFIG": "HdHomerun", "PLAYLIST_BROWSER": "Playlist Browser", "RATE_LIMIT": "Rate Limit", "PERIOD_MILLIS": "Period Millis", @@ -199,14 +201,34 @@ "MAPPER": "Mapper", "SCRIPT": "Script", "COUNTER": "Counter", - "MAIN_CONFIG": "Main Config", "MAPPING_PATH": "Mapping dir", "USER_CONFIG_DIR": "User Config dir", "USER_ACCESS_CONTROL": "User Access Control", "SLEEP_TIMER_MINS": "Sleep Timer (mins)", "CONNECT_TIMEOUT_SECS": "Connect Timeout (secs)", "CONFIG_HOT_RELOAD": "Config Hot Reload", - "CUSTOM_STREAM_RESPONSE_PATH": "Custom Stream Response dir" + "CUSTOM_STREAM_RESPONSE_PATH": "Custom Stream Response dir", + "REVERSE_PROXY" : "Reverse Proxy", + "WEB_SEARCH": "Web Search", + "USER_UI_ENABLED": "User-UI Enabled", + "RESOURCE_REWRITE_DISABLED": "Resource Rewrite Disabled", + "DEVICE_AUTH": "Auth", + "TOKEN_TTL_MINS": "Token ttl (mins)", + "AUTH": "Auth", + "RETRY": "Retry", + "GRACE_PERIOD_MILLIS": "Grace Period (millis)", + "GRACE_PERIOD_TIMEOUT_SECS": "Grace Period Timeout (secs)", + "FORCED_RETRY_INTERVAL_SECS": "Forced Retry Interval (secs)", + "THROTTLE_KBPS": "Throttle kbps", + "FRIENDLY_NAME": "Friendly Name", + "MANUFACTURER": "Manufacturer", + "MODEL_NAME": "Model Name", + "MODEL_NUMBER": "Model Number", + "FIRMWARE_NAME": "Firmware Name", + "FIRMWARE_VERSION": "Firmware Version", + "DEVICE_TYPE": "Device Type", + "DEVICE_UDN": "Device UDN", + "TUNER_COUNT": "Tuner Count" }, "TABLE": { "EMPTY": "", diff --git a/frontend/public/assets/icons.json b/frontend/public/assets/icons.json index 8326bdcf3..68c139d2c 100644 --- a/frontend/public/assets/icons.json +++ b/frontend/public/assets/icons.json @@ -161,6 +161,24 @@ ], "path": "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" }, + { + "keys": [ + "MainConfig" + ], + "path": "M 3.9998451,3 C 2.8898606,3 2,3.8903952 2,5.000402 V 17.00023 c 0,1.100002 0.8898606,1.999885 1.9998451,1.999885 H 8.0000516 V 21 h 7.9998964 v -1.999885 h 3.99969 C 21.099614,19.000115 22,18.100232 22,17.00023 V 5.000402 C 22,3.8903952 21.099614,3 19.999638,3 Z m 0,2.000402 H 19.999638 V 17.00023 H 3.9998451 Z M 15.000026,6.0003443 v 3.9997707 h 1.500142 V 8.7500572 h 1.499625 V 7.250402 H 16.500168 V 6.0003443 Z M 6.0002065,7.250402 V 8.7500572 H 14.000103 V 7.250402 Z m 1.4996254,2.749713 v 1.250057 H 6.0002065 v 1.500172 h 1.4996254 v 1.250058 h 1.5001422 v -4.000287 z m 2.5000647,1.250057 v 1.500172 h 7.9998964 v -1.500172 z" + }, + { + "keys": [ + "ApiConfig", "Cloud" + ], + "path": "M12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6m0-2C9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96C18.67 6.59 15.64 4 12 4" + }, + { + "keys": [ + "SchedulesConfig", "Clock" + ], + "path": "M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8m.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z" + }, { "keys": [ "PersonAdd", "UserAdd" @@ -423,5 +441,33 @@ { "keys": ["Sidebar"], "path": "M 8.1236837,3.8212107 C 7.5708008,3.8269268 7.118481,3.8379635 6.7315009,3.8731747 6.2588505,3.9161892 5.9241545,3.9886579 5.6586683,4.0943323 L 5.413476,4.2115753 C 4.87529,4.5170295 4.4250032,4.9814043 4.110993,5.5518355 L 3.9854085,5.802297 C 3.8311896,6.1394698 3.733125,6.5682788 3.6816248,7.270455 3.629353,7.9831496 3.6289978,8.8947371 3.6289978,10.182801 v 3.634468 c 0,1.287982 3.552e-4,2.19957 0.052627,2.912251 0.0515,0.702312 0.1495648,1.131094 0.3037837,1.468199 l 0.1255845,0.250475 c 0.3140102,0.57039 0.7643214,1.034779 1.302483,1.340233 l 0.2451923,0.117325 c 0.2654862,0.105587 0.6002067,0.178034 1.0728326,0.221143 0.3866984,0.0352 0.8386508,0.04625 1.3909825,0.05197 z M 22,13.817269 c 0,1.258105 3.67e-4,2.256459 -0.05854,3.060135 -0.0523,0.712408 -0.153826,1.332866 -0.373177,1.902574 L 21.46663,19.02104 c -0.410899,0.898354 -1.03625,1.650188 -1.80367,2.174331 l -0.337292,0.210503 c -0.57036,0.323599 -1.193506,0.462342 -1.924307,0.528918 C 16.679747,22.000412 15.783607,22 14.654032,22 H 9.3460322 C 8.2164575,22 7.3202809,22.000409 6.5987395,21.934792 5.9590962,21.876534 5.4022942,21.762214 4.8908074,21.517742 L 4.6743229,21.405874 C 3.867944,20.94817 3.1929181,20.251451 2.7223987,19.396752 L 2.5334341,19.02104 C 2.2428898,18.385712 2.1183341,17.691585 2.0586037,16.877404 1.9996571,16.073728 2,15.075374 2,13.817269 V 10.182801 C 2,8.924573 1.9996571,7.9263015 2.0586037,7.1225846 2.1183218,6.3084586 2.242902,5.6143724 2.5334341,4.9789623 L 2.7223987,4.6032633 C 3.1929181,3.7485511 3.8679195,3.0518864 4.6743229,2.5941963 L 4.8908074,2.4809505 C 5.4021839,2.2366554 5.9592677,2.1234914 6.5987395,2.0652791 7.3202809,1.999618 8.2164575,2 9.3460322,2 h 5.3079968 c 1.129575,0 2.025715,-3.82e-4 2.747329,0.065279 0.730801,0.06652 1.353947,0.2052913 1.924307,0.5289173 l 0.337292,0.2105028 c 0.767298,0.5241014 1.392771,1.2759906 1.80367,2.1742631 L 21.56828,5.2200928 C 21.787631,5.789719 21.889161,6.4102588 21.941457,7.122585 22.000367,7.9263015 22,8.924573 22,10.182801 Z M 9.7514813,20.185422 h 4.9025477 c 1.156396,0 1.974765,-4.1e-4 2.614568,-0.05852 0.630249,-0.05743 1.015307,-0.166709 1.317939,-0.338468 l 0.224862,-0.139835 c 0.512185,-0.349792 0.928963,-0.851424 1.203181,-1.450873 L 20.119907,17.9246 c 0.09492,-0.295769 0.15995,-0.66848 0.198529,-1.195077 0.0523,-0.712682 0.05266,-1.624269 0.05266,-2.912252 v -3.63447 c 0,-1.2880639 -3.67e-4,-2.1996514 -0.05266,-2.912346 C 20.279857,6.7439661 20.214701,6.3711457 20.119907,6.0754185 L 20.014579,5.802297 C 19.740361,5.2028075 19.32346,4.7012296 18.811398,4.3514648 L 18.586536,4.2115753 C 18.283904,4.0397761 17.898969,3.9305412 17.268597,3.8731747 16.628794,3.8149487 15.810425,3.8145531 14.654029,3.8145531 H 9.7514813 c 1.22e-5,0.00223 0.0012,0.00443 0.0012,0.00666 z" + }, + { + "keys": ["MessagingConfig"], + "path": "M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92M18 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1M6 13c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m12 7.02c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1" + }, + { + "keys": ["ProxyConfig"], + "path": "M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2m6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56M12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96M4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56m2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8M12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96M14.34 14H9.66c-.09-.66-.16-1.32-.16-2s.07-1.35.16-2h4.68c.09.65.16 1.32.16 2s-.07 1.34-.16 2m.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56M16.36 14c.08-.66.14-1.32.14-2s-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2z" + }, + { + "keys": ["ReverseProxyConfig"], + "path": "M 12.101005,2 C 8.7979457,2 5.8584967,3.6976183 4.0200967,6.3083602 V 4.1486281 H 2 v 6.4464399 h 6.0775153 c -0.045959,-0.7346053 3e-7,-1.4462453 0,-2.1580668 -0.9964324,-0.00449 -1.9626546,0.00943 -2.9565445,0.00943 1.3939428,-2.5677673 4.0002101,-4.2978115 6.9800342,-4.2978115 3.757609,0 6.919351,2.750574 7.818348,6.4464393 H 22 C 21.060599,5.6958985 16.989937,2 12.101005,2 Z m -0.200443,3.8342186 c -1.9236549,0 -3.6794612,0.7620692 -5.0168348,2.0170454 H 8.5238157 V 8.5591453 C 8.9594988,8.2478745 9.4326204,7.9916129 9.9399712,7.806848 9.4840158,8.7040375 9.1344717,9.6735498 8.8912958,10.683901 l -0.066884,0.301452 -0.5031281,5.68e-4 H 4.8239592 C 4.4916292,11.89489 4.3092784,12.883646 4.3092784,13.917383 4.3092784,18.379078 7.705763,22 11.900562,22 c 4.202397,0 7.606942,-3.620921 7.606942,-8.082617 0,-4.4616972 -3.404545,-8.0831722 -7.606942,-8.0831722 z m 0.0079,1.6489471 c 0.630742,0.9699339 1.124882,2.044895 1.451651,3.2007353 h -2.903302 c 0.32677,-1.1558403 0.820912,-2.2308014 1.451651,-3.2007353 z m 1.96842,0.3236823 c 1.398265,0.5092149 2.560565,1.5433911 3.290096,2.877053 h -2.24147 C 14.682311,9.6735498 14.332765,8.7040375 13.876811,7.806848 Z m -7.8502607,4.493798 h 2.5681849 c -0.060789,0.533461 -0.1059637,1.067115 -0.1059637,1.616745 0,0.549626 0.045175,1.082725 0.1059637,1.61619 H 6.0266213 c -0.1215875,-0.517298 -0.1978336,-1.05848 -0.1978336,-1.61619 0,-0.557713 0.076246,-1.099449 0.1978336,-1.616745 z m 4.1033547,0 h 3.556831 c 0.0684,0.525379 0.121101,1.067115 0.121101,1.616745 0,0.549626 -0.0527,1.082725 -0.121101,1.61619 h -3.556831 c -0.06839,-0.533465 -0.121624,-1.066564 -0.121624,-1.61619 0,-0.54963 0.05322,-1.091366 0.121624,-1.616745 z m 5.091478,0 h 2.568707 c 0.121593,0.517296 0.197833,1.059032 0.197833,1.616745 0,0.55771 -0.07623,1.098892 -0.197833,1.61619 h -2.568707 c 0.06079,-0.533465 0.106485,-1.066564 0.106485,-1.61619 0,-0.54963 -0.04569,-1.083284 -0.106485,-1.616745 z m -8.5715778,4.849681 h 2.2414196 c 0.2431759,1.010346 0.59272,1.980416 1.0486754,2.877606 C 8.5417055,19.518719 7.379406,18.492068 6.6498762,17.150327 Z m 3.8068638,0 h 2.903302 c -0.326768,1.15584 -0.82091,2.230801 -1.451651,3.200733 -0.630738,-0.969932 -1.124881,-2.044893 -1.451651,-3.200733 z m 4.468746,0 h 2.24142 c -0.72953,1.33366 -1.89183,2.368392 -3.290095,2.877606 0.455954,-0.89719 0.8055,-1.86726 1.048675,-2.877606 z" + }, + { + "keys": ["WebUiConfig", "BrowserConfig"], + "path": "M 2.999817,3 C 1.8998169,3 0.99993896,3.8998778 0.99993896,4.9998777 V 17.000179 c 0,1.1 0.89987794,1.999878 1.99987804,1.999878 h 5.0002113 v 1.999878 h 8.0000287 v -1.999878 h 4.999695 c 1.1,0 1.990059,-0.899878 1.990059,-1.999878 L 23.000146,4.9998777 C 23.000146,3.8998778 22.099752,3 20.999752,3 Z m 0,1.9998777 H 11.9941 20.999752 V 17.000179 H 11.9941 2.999817 Z M 11.9941,17.000179 c 3.318,0 6.005835,-2.688151 6.005835,-6.000151 0,-3.3119998 -2.687835,-6.0001503 -6.005835,-6.0001503 -3.3120002,0 -5.9939496,2.6881505 -5.9939496,6.0001503 0,3.312 2.6819494,6.000151 5.9939496,6.000151 z M 11.9998,6.2240923 c 0.498,0.7200001 0.888184,1.5180826 1.146183,2.3760824 H 10.854134 C 11.112134,7.7421749 11.5018,6.9440924 11.9998,6.2240923 Z M 10.44589,6.463871 c -0.36,0.6660001 -0.6358565,1.3863039 -0.8278567,2.1363037 H 7.8480996 c 0.576,-0.9959998 1.4937742,-1.7583036 2.5977744,-2.1363037 z m 3.108337,0 c 1.104,0.3780001 2.021774,1.1463038 2.597774,2.1363037 H 14.382068 C 14.190068,7.8501749 13.914211,7.1298711 13.554211,6.463871 Z M 7.3561559,9.800102 h 2.0277834 c -0.048,0.395999 -0.083716,0.791926 -0.083716,1.199926 0,0.408 0.035716,0.803927 0.083716,1.199927 H 7.3561399 c -0.096,-0.384 -0.1560626,-0.785927 -0.1560626,-1.199927 0,-0.414 0.060063,-0.815926 0.1560626,-1.199926 z m 3.2401121,0 h 2.807581 c 0.054,0.389999 0.09612,0.791926 0.09612,1.199926 0,0.408 -0.04212,0.803927 -0.09612,1.199927 h -2.807581 c -0.054,-0.396 -0.09612,-0.791927 -0.09612,-1.199927 0,-0.408 0.04212,-0.809927 0.09612,-1.199926 z m 4.01991,0 h 2.027783 c 0.096,0.384 0.156063,0.785926 0.156063,1.199926 0,0.414 -0.06006,0.815927 -0.156063,1.199927 h -2.027783 c 0.048,-0.396 0.08372,-0.791927 0.08372,-1.199927 0,-0.408 -0.03572,-0.803927 -0.08372,-1.199926 z m -6.7680624,3.59978 h 1.7699177 c 0.1920002,0.75 0.4678567,1.470304 0.8278567,2.136303 C 9.3418738,15.158186 8.4240996,14.395882 7.8480996,13.399882 Z m 3.0060184,0 h 2.291849 c -0.257999,0.858 -0.648183,1.656082 -1.146183,2.376082 -0.498,-0.72 -0.887666,-1.518082 -1.145666,-2.376082 z m 3.52795,0 h 1.769917 c -0.576,0.99 -1.493774,1.758304 -2.597774,2.136303 0.36,-0.665999 0.635857,-1.386303 0.827857,-2.136303 z" + }, + { + "keys": ["IpCheckConfig"], + "path": "M 8.5545164,3 C 4.9327188,3 1.9998779,6.1277825 1.9998779,9.9814858 c 0,3.8537012 2.9328409,6.9814862 6.5546385,6.9814862 0.085911,0 0.1712063,-0.0027 0.2563151,-0.0062 -0.2139233,1.117761 0.110925,2.323437 0.9575644,3.152779 1.2867071,1.260418 3.2885421,1.172208 4.4731121,-0.196887 l 3.752225,-4.33617 0.581877,0.569991 c 0.32022,0.313679 0.823476,0.291638 1.118278,-0.04909 l 2.144055,-2.477885 -0.582393,-0.570508 0.536401,-0.6196 c 0.294803,-0.340727 0.273707,-0.875912 -0.04651,-1.189592 l -1.16427,-1.140499 c -0.32022,-0.3136779 -0.823475,-0.2916344 -1.118278,0.04909 l -0.535884,0.6196 -0.582394,-0.570508 -2.144055,2.477885 c -0.294804,0.340725 -0.273706,0.876429 0.04651,1.190109 l 0.581877,0.569991 -3.751709,4.336686 c -0.589602,0.681452 -1.596115,0.725545 -2.236556,0.09819 -0.60915,-0.596706 -0.676346,-1.594407 -0.17415,-2.276864 2.591603,-0.935177 4.456059,-3.543146 4.456059,-6.6119992 C 15.122591,6.1277825 12.182875,3 8.5545164,3 Z m 0.00672,1.4242024 C 9.1058165,5.261964 9.532255,6.1905588 9.8143879,7.1888915 H 7.308081 C 7.5902136,6.1905588 8.0166526,5.261964 8.5612345,4.4242024 Z M 6.8621134,4.703772 C 6.4684396,5.4787014 6.1661849,6.3162232 5.9562256,7.1888915 H 4.0209432 C 4.6508212,6.029988 5.6548474,5.1435965 6.8621134,4.703772 Z m 3.3982426,0 c 1.207265,0.4398245 2.211293,1.3331973 2.84117,2.4851195 H 11.166243 C 10.956283,6.3162232 10.654029,5.4787014 10.260356,4.703772 Z M 3.4829914,8.5851888 h 2.2174358 c -0.05249,0.4607689 -0.091984,0.9215646 -0.091984,1.396297 0,0.4747312 0.039495,0.9355272 0.091984,1.3962972 H 3.4829914 C 3.3780118,10.930976 3.3124592,10.463198 3.3124592,9.9814858 c 0,-0.4817131 0.065553,-0.9494914 0.1705322,-1.396297 z m 3.5429364,0 h 3.0706132 c 0.05906,0.4537871 0.104903,0.9215646 0.104903,1.396297 0,0.4747312 -0.04585,0.9355272 -0.104903,1.3962972 H 7.0259278 C 6.966877,10.917013 6.9210245,10.456217 6.9210245,9.9814858 c 0,-0.4747324 0.045853,-0.9425099 0.1049033,-1.396297 z m 4.3961142,0 h 2.217435 c 0.104979,0.4468056 0.170532,0.9145839 0.170532,1.396297 0,0.4817122 -0.06555,0.9494902 -0.170532,1.3962972 h -2.217435 c 0.05249,-0.46077 0.09198,-0.921566 0.09198,-1.3962972 0,-0.4747324 -0.0395,-0.9355281 -0.09198,-1.396297 z M 4.0209432,12.77408 h 1.9352824 c 0.2099593,0.872669 0.512214,1.71019 0.9058878,2.48512 -1.207266,-0.439825 -2.2112922,-1.326216 -2.8411702,-2.48512 z m 3.2871378,0 H 9.8143879 C 9.532255,13.772412 9.1058165,14.701007 8.5612345,15.538769 8.0166526,14.701007 7.5902136,13.772412 7.308081,12.77408 Z m 3.858162,0 h 1.935283 c -0.629877,1.151922 -1.633905,2.045295 -2.84117,2.48512 0.393673,-0.77493 0.695927,-1.612451 0.905887,-2.48512 z" + }, + { + "keys": ["VideoConfig"], + "path": "M14 5v14h-4V5zm6-2h-2v2h-2V3H8v2H6V3H4v18h2v-2h2v2h8v-2h2v2h2zm-4 6V7h2v2zM6 9V7h2v2zm10 4v-2h2v2zM6 13v-2h2v2zm10 4v-2h2v2zM6 17v-2h2v2z" + }, + { + "keys": ["HdHomerunConfig"], + "path": "m 16,4.2 c 1.5,0 3,0.6 4.2,1.7 L 21,5.1 C 19.6,3.7 17.8,3 16,3 14.2,3 12.4,3.7 11,5.1 l 0.8,0.8 C 13,4.8 14.5,4.2 16,4.2 m -3.3,2.5 0.8,0.8 c 0.7,-0.7 1.6,-1 2.5,-1 0.9,0 1.8,0.3 2.5,1 L 19.3,6.7 C 18.4,5.8 17.2,5.3 16,5.3 14.8,5.3 13.6,5.8 12.7,6.7 M 19,13 H 17 V 9 h -2 v 4 H 5 c -1.1,0 -2,0.9 -2,2 v 4 c 0,1.1 0.9,2 2,2 h 14 c 1.1,0 2,-0.9 2,-2 v -4 c 0,-1.1 -0.9,-2 -2,-2 m 0,6 H 5 V 15 H 19 Z M 6,16 h 2 v 2 H 6 Z" } ] \ No newline at end of file diff --git a/frontend/scss/app/_component.scss b/frontend/scss/app/_component.scss index 29ee2a235..88de7ae97 100644 --- a/frontend/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -64,4 +64,4 @@ @forward "components/accordion"; @forward "components/cvs_table"; @forward "components/config/config_view"; -@forward "components/config/main_config_view"; +@forward "components/tabset"; diff --git a/frontend/scss/app/components/_card.scss b/frontend/scss/app/components/_card.scss index 21d6d4a33..856c3e23b 100644 --- a/frontend/scss/app/components/_card.scss +++ b/frontend/scss/app/components/_card.scss @@ -6,4 +6,6 @@ padding: var(--padding-default); border: 1px solid var(--border-color); border-radius: var(--border-radius); + position: relative; + gap: var(--gap-default); } \ No newline at end of file diff --git a/frontend/scss/app/components/_tabset.scss b/frontend/scss/app/components/_tabset.scss new file mode 100644 index 000000000..c4f2d6cb6 --- /dev/null +++ b/frontend/scss/app/components/_tabset.scss @@ -0,0 +1,64 @@ +@use '../../size'; +.tp__tab-set { + display: flex; + flex-direction: column; + height: 100%; + box-sizing: border-box; + overflow: hidden; + + &__header { + display: flex; + flex-flow: row wrap; + border-bottom: 1px solid var(--border-color); + padding: 0 0 var(--padding-small) 0; + gap: var(--gap-default); + box-sizing: border-box; + } + + &__tab { + position: relative; + + .tp__text-button { + color: var(--modest-text-color); + } + + + &--active { + fill: var(--active-color); + color: var(--active-color); + .tp__text-button { + color: var(--active-color); + } + + } + + &-desktop { + display: block; + } + + &-mobile { + display: none; + } + + @media (max-width: size.$mobile-breakpoint) { + &-desktop { + display: none; + } + + &-mobile { + display: block; + } + } + } + + &__body { + flex: 1; + overflow: hidden; + box-sizing: border-box; + } + + &__panel { + height: 100%; + overflow: auto; + } +} \ No newline at end of file diff --git a/frontend/scss/app/components/config/_config_view.scss b/frontend/scss/app/components/config/_config_view.scss index 448351e1c..cad42430e 100644 --- a/frontend/scss/app/components/config/_config_view.scss +++ b/frontend/scss/app/components/config/_config_view.scss @@ -5,6 +5,8 @@ gap: var(--gap-default); padding: var(--padding-default) 0; box-sizing: border-box; + overflow: hidden; + &__header { display: flex; flex-flow: row nowrap; @@ -12,16 +14,39 @@ width: 100%; justify-content: space-between; align-items: center; + gap: var(--gap-default); } &__body { display: flex; flex-flow: column; - + overflow: hidden; gap: var(--gap-default); box-sizing: border-box; - overflow-y: auto; width: 100%; + + .tp__card { + overflow: hidden; + gap: var(--gap-large); + } + } + + &__tags { + display: flex; + flex-flow: row wrap; + gap: var(--gap-default); + height: 2.5rem; + align-items: center; + } + + &__card { + gap: var(--gap-large); + background-color: var(--table-alternate-background-color); + overflow: hidden; + h1 { + font-size: 1.2rem; + font-weight: bold; + } } } @@ -29,12 +54,13 @@ display: flex; gap: var(--gap-default); box-sizing: border-box; - //max-width: 400px; + min-width: 200px; &__text { flex-flow: column; + } - span { + &__value { display: flex; flex-flow: row nowrap; align-items: center; @@ -50,7 +76,6 @@ border-radius: var(--border-radius); box-sizing: border-box; white-space: nowrap; - } } &__bool { @@ -68,4 +93,52 @@ content: ":"; } } +} + +.tp__config-view-page { + display: flex; + flex-flow: column; + width: 100%; + gap: var(--gap-default); + padding: var(--padding-default) 0; + box-sizing: border-box; + overflow: auto; + + &__header { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: var(--gap-large); + box-sizing: border-box; + width: 100%; + } + + &__body { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: var(--gap-large); + box-sizing: border-box; + width: 100%; + } +} + +.tp__schedules-config-view { + &__schedule { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: var(--gap-default); + box-sizing: border-box; + padding: var(--padding-default); + border: 1px solid var(--card-border-color); + border-radius: var(--border-radius); + } + &__footer { + margin-top: var(--margin-default); + padding-top: var(--margin-default); + border-top : 1px solid var(--card-border-color); + svg { + width: 240px; + height: 60px; + } + fill: var(--label-color); + } } \ No newline at end of file diff --git a/frontend/scss/app/components/config/_main_config_view.scss b/frontend/scss/app/components/config/_main_config_view.scss deleted file mode 100644 index 740cdf35d..000000000 --- a/frontend/scss/app/components/config/_main_config_view.scss +++ /dev/null @@ -1,26 +0,0 @@ -.tp__main-config-view { - display: flex; - flex-flow: column; - width: 100%; - gap: var(--gap-default); - padding: var(--padding-default) 0; - box-sizing: border-box; - - &__header { - display: flex; - flex-flow: row nowrap; - box-sizing: border-box; - width: 100%; - justify-content: space-between; - align-items: center; - } - - &__body { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - gap: var(--gap-large); - box-sizing: border-box; - overflow-y: auto; - width: 100%; - } -} diff --git a/frontend/src/app/components/config/api_config_view.rs b/frontend/src/app/components/config/api_config_view.rs new file mode 100644 index 000000000..3646d89f4 --- /dev/null +++ b/frontend/src/app/components/config/api_config_view.rs @@ -0,0 +1,31 @@ +use crate::app::components::{NoContent}; +use crate::app::ConfigContext; +use yew::prelude::*; +use yew_i18n::use_translation; +use crate::{config_field}; + +#[function_component] +pub fn ApiConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("Config context not found"); + + html! { +
+
+ { + match config_ctx.config.as_ref() { + Some(config) => { + html!{ + <> + { config_field!(config.config.api, translate.t("LABEL.HOST"), host) } + { config_field!(config.config.api, translate.t("LABEL.PORT"), port) } + { config_field!(config.config.api, translate.t("LABEL.WEB_ROOT"), web_root) } + + }}, + None => html! { } + } + } +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/config_view.rs b/frontend/src/app/components/config/config_view.rs index 0062c1a99..a5697a9a1 100644 --- a/frontend/src/app/components/config/config_view.rs +++ b/frontend/src/app/components/config/config_view.rs @@ -1,20 +1,162 @@ -use crate::app::components::config::MainConfigView; +use std::fmt; +use std::str::FromStr; +use crate::app::components::config::{ApiConfigView, HdHomerunConfigView, IpCheckConfigView, MainConfigView, MessagingConfigView, ProxyConfigView, ReverseProxyConfigView, SchedulesConfigView, VideoConfigView, WebUiConfigView}; use yew::prelude::*; use yew_i18n::use_translation; -use crate::app::components::Card; +use shared::error::TuliproxError; +use shared::info_err; +use crate::app::components::{Card, TabItem, TabSet}; + +const MAIN_PAGE: &str = "main"; +const API_PAGE: &str = "api"; +const SCHEDULES_PAGE: &str = "schedules"; +const MESSAGING_PAGE: &str = "messaging"; +const WEBUI_PAGE: &str = "webui"; +const REVERSE_PROXY_PAGE: &str = "reverse_proxy"; +const HDHOMERUN_PAGE: &str = "hdhomerun"; +const PROXY_PAGE: &str = "proxy"; +const IPCHECK_PAGE: &str = "ipcheck"; +const VIDEO_PAGE: &str = "video"; + + +enum ConfigPage { + Main, + Api, + Schedules, + Video, + Messaging, + WebUi, + ReverseProxy, + HdHomerun, + Proxy, + IpCheck, +} + +impl FromStr for ConfigPage { + type Err = TuliproxError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + MAIN_PAGE => Ok(ConfigPage::Main), + API_PAGE => Ok(ConfigPage::Api), + SCHEDULES_PAGE => Ok(ConfigPage::Schedules), + VIDEO_PAGE => Ok(ConfigPage::Video), + MESSAGING_PAGE => Ok(ConfigPage::Messaging), + WEBUI_PAGE => Ok(ConfigPage::WebUi), + REVERSE_PROXY_PAGE => Ok(ConfigPage::ReverseProxy), + HDHOMERUN_PAGE => Ok(ConfigPage::HdHomerun), + PROXY_PAGE => Ok(ConfigPage::Proxy), + IPCHECK_PAGE => Ok(ConfigPage::IpCheck), + _ => Err(info_err!(format!("Unknown config page: {s}"))), + } + } +} + +impl fmt::Display for ConfigPage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + ConfigPage::Main => MAIN_PAGE, + ConfigPage::Api => API_PAGE, + ConfigPage::Schedules => SCHEDULES_PAGE, + ConfigPage::Video => VIDEO_PAGE, + ConfigPage::Messaging => MESSAGING_PAGE, + ConfigPage::WebUi => WEBUI_PAGE, + ConfigPage::ReverseProxy => REVERSE_PROXY_PAGE, + ConfigPage::HdHomerun => HDHOMERUN_PAGE, + ConfigPage::Proxy => PROXY_PAGE, + ConfigPage::IpCheck => IPCHECK_PAGE, + }; + write!(f, "{s}") + } +} #[function_component] pub fn ConfigView() -> Html { let translate = use_translation(); + let active_tab = use_state(|| ConfigPage::Main); + + let handle_tab_change = { + let active_tab = active_tab.clone(); + Callback::from(move |tab_id: String| { + if let Ok(page) = ConfigPage::from_str(&tab_id) { + active_tab.set(page); + } + }) + }; + + let tabs = vec![ + TabItem { + id: ConfigPage::Main.to_string(), + title: translate.t("LABEL.MAIN"), + icon: "MainConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::Api.to_string(), + title: translate.t("LABEL.API"), + icon: "ApiConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::Schedules.to_string(), + title: translate.t("LABEL.SCHEDULES"), + icon: "SchedulesConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::Messaging.to_string(), + title: translate.t("LABEL.MESSAGING"), + icon: "MessagingConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::WebUi.to_string(), + title: translate.t("LABEL.WEB_UI"), + icon: "WebUiConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::ReverseProxy.to_string(), + title: translate.t("LABEL.REVERSE_PROXY"), + icon: "ReverseProxyConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::HdHomerun.to_string(), + title: translate.t("LABEL.HDHOMERUN_CONFIG"), + icon: "HdHomerunConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::Proxy.to_string(), + title: translate.t("LABEL.PROXY"), + icon: "ProxyConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::IpCheck.to_string(), + title: translate.t("LABEL.IP_CHECK"), + icon: "IpCheckConfig".to_string(), + children: html! { }, + }, + TabItem { + id: ConfigPage::Video.to_string(), + title: translate.t("LABEL.VIDEO"), + icon: "VideoConfig".to_string(), + children: html! { }, + } + ]; html! {
-

{ translate.t("LABEL.CONFIG")}

+

{ translate.t("LABEL.CONFIG") }

- +
diff --git a/frontend/src/app/components/config/hdhomerung_config_view.rs b/frontend/src/app/components/config/hdhomerung_config_view.rs new file mode 100644 index 000000000..37610f761 --- /dev/null +++ b/frontend/src/app/components/config/hdhomerung_config_view.rs @@ -0,0 +1,75 @@ +use yew::prelude::*; +use yew_i18n::use_translation; +use shared::model::HdHomeRunDeviceConfigDto; +use crate::app::components::{Card}; +use crate::app::context::ConfigContext; +use crate::{config_field, config_field_bool, config_field_bool_empty}; + +#[function_component] +pub fn HdHomerunConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("ConfigContext not found"); + + let render_devices = |devices: &Vec| -> Html { + html!{ for devices.iter().map(|entry| html! { + +

{ translate.t("LABEL.DEVICE") }

+ {config_field!(entry, translate.t("LABEL.FRIENDLY_NAME"), friendly_name)} + {config_field!(entry, translate.t("LABEL.MANUFACTURER"), manufacturer)} + {config_field!(entry, translate.t("LABEL.MODEL_NAME"), model_name)} + {config_field!(entry, translate.t("LABEL.MODEL_NUMBER"), model_number)} + {config_field!(entry, translate.t("LABEL.FIRMWARE_NAME"), firmware_name)} + {config_field!(entry, translate.t("LABEL.FIRMWARE_VERSION"), firmware_version)} + {config_field!(entry, translate.t("LABEL.DEVICE_TYPE"), device_type)} + {config_field!(entry, translate.t("LABEL.DEVICE_UDN"), device_udn)} + {config_field!(entry, translate.t("LABEL.NAME"), name)} + {config_field!(entry, translate.t("LABEL.PORT"), port)} + {config_field!(entry, translate.t("LABEL.TUNER_COUNT"), tuner_count)} +
+ })} + }; + + let render_empty = || { + html! { + <> +
+ { config_field_bool_empty!(translate.t("LABEL.ENABLED")) } + { config_field_bool_empty!(translate.t("LABEL.DEVICE_AUTH")) } +
+
+ +

{translate.t("LABEL.DEVICES")}

+ +
+
+ + } + }; + + + html! { +
+ { + if let Some(config) = &config_ctx.config { + if let Some(hdhomerun) = &config.config.hdhomerun { + html! { + <> +
+ { config_field_bool!(hdhomerun, translate.t("LABEL.ENABLED"), enabled) } + { config_field_bool!(hdhomerun, translate.t("LABEL.DEVICE_AUTH"), auth) } +
+
+ {render_devices(&hdhomerun.devices)} +
+ + } + } else { + { render_empty() } + } + } else { + { render_empty() } + } + } +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/ipcheck_config_view.rs b/frontend/src/app/components/config/ipcheck_config_view.rs new file mode 100644 index 000000000..e8ddbe9b4 --- /dev/null +++ b/frontend/src/app/components/config/ipcheck_config_view.rs @@ -0,0 +1,37 @@ +use yew::prelude::*; +use yew_i18n::use_translation; +use crate::app::components::{NoContent}; +use crate::app::context::ConfigContext; +use crate::{config_field_optional}; + +#[function_component] +pub fn IpCheckConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("ConfigContext not found"); + + html! { +
+
+ { + if let Some(config) = &config_ctx.config { + if let Some(ipcheck) = &config.config.ipcheck { + html! { + <> + { config_field_optional!(ipcheck, translate.t("LABEL.URL"), url) } + { config_field_optional!(ipcheck, translate.t("LABEL.URL_IPV4"), url_ipv4) } + { config_field_optional!(ipcheck, translate.t("LABEL.URL_IPV6"), url_ipv6) } + { config_field_optional!(ipcheck, translate.t("LABEL.PATTERN_IPV4"), pattern_ipv4) } + { config_field_optional!(ipcheck, translate.t("LABEL.PATTERN_IPV6"), pattern_ipv6) } + + } + } else { + html! { } + } + } else { + html! { } + } + } +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/macros.rs b/frontend/src/app/components/config/macros.rs new file mode 100644 index 000000000..977a7e3ea --- /dev/null +++ b/frontend/src/app/components/config/macros.rs @@ -0,0 +1,110 @@ +#[macro_export] +macro_rules! config_field_optional { + ($config:expr, $label:expr, $field:ident) => { + html! { +
+ + + { + match $config.$field.as_ref() { + Some(value) => html! { &value }, + None => Html::default(), + } + } + +
+ } + }; +} + +#[macro_export] +macro_rules! config_field_optional_hide { + ($config:expr, $label:expr, $field:ident) => { + html! { +
+ + + <$crate::app::components::HideContent content={$config.$field.as_ref().map_or_else(String::new, |content| content.clone())}> + +
+ } + }; +} + +#[macro_export] +macro_rules! config_field_hide { + ($config:expr, $label:expr, $field:ident) => { + html! { +
+ + + <$crate::app::components::HideContent content={$config.$field.clone()}> + +
+ } + }; +} + +#[macro_export] +macro_rules! config_field_bool { + ($config:expr, $label:expr, $field:ident) => { + html! { +
+ + <$crate::app::components::ToggleSwitch value={$config.$field} readonly={true} /> +
+ } + }; +} + +#[macro_export] +macro_rules! config_field_bool_empty { + ($label:expr) => { + html! { +
+ + <$crate::app::components::ToggleSwitch value={false} readonly={true} /> +
+ } + }; +} + +#[macro_export] +macro_rules! config_field { + ($config:expr, $label:expr, $field:ident) => { + html! { +
+ + {$config.$field.to_string()} +
+ } + }; +} + +#[macro_export] +macro_rules! config_field_child { + ($config:expr, $label:expr, $body:block) => { + html! { +
+ + { $body } +
+ } + }; +} + +#[macro_export] +macro_rules! config_field_empty { + ($label:expr) => { + html! { +
+ + +
+ } + }; +} + +// pub use config_field; +// pub use config_field_bool; +// pub use config_field_optional; \ No newline at end of file diff --git a/frontend/src/app/components/config/main_config_view.rs b/frontend/src/app/components/config/main_config_view.rs index aa0bb937c..8b6c03129 100644 --- a/frontend/src/app/components/config/main_config_view.rs +++ b/frontend/src/app/components/config/main_config_view.rs @@ -1,46 +1,8 @@ -use crate::app::components::{NoContent, ToggleSwitch}; +use crate::app::components::{NoContent}; use crate::app::ConfigContext; use yew::prelude::*; use yew_i18n::use_translation; - -macro_rules! config_field_optional { - ($config:expr, $translate:expr, $label:expr, $field:ident) => { - html! { -
- - { - match &$config.config.$field { - Some(value) => value.to_string(), - None => String::new(), - } - } -
- } - }; -} - -macro_rules! config_field_bool { - ($config:expr, $translate:expr, $label:expr, $field:ident) => { - html! { -
- - -
- } - }; -} - -macro_rules! config_field { - ($config:expr, $translate:expr, $label:expr, $field:ident) => { - html! { -
- - {$config.config.$field.to_string()} -
- } - }; -} - +use crate::{config_field, config_field_bool, config_field_optional}; #[function_component] pub fn MainConfigView() -> Html { @@ -48,27 +10,24 @@ pub fn MainConfigView() -> Html { let config_ctx = use_context::().expect("Config context not found"); html! { -
-
-

{ translate.t("LABEL.MAIN_CONFIG") }

-
-
+
+
{ match config_ctx.config.as_ref() { Some(config) => { html!{ <> - { config_field_bool!(config, translate, "LABEL.UPDATE_ON_BOOT", update_on_boot) } - { config_field_bool!(config, translate, "LABEL.CONFIG_HOT_RELOAD", config_hot_reload) } - { config_field_bool!(config, translate, "LABEL.USER_ACCESS_CONTROL", user_access_control) } - { config_field!(config, translate, "LABEL.THREADS", threads) } - { config_field!(config, translate, "LABEL.WORKING_DIR", working_dir) } - { config_field_optional!(config, translate, "LABEL.MAPPING_PATH", mapping_path) } - { config_field_optional!(config, translate, "LABEL.BACKUP_DIR", backup_dir) } - { config_field_optional!(config, translate, "LABEL.USER_CONFIG_DIR", user_config_dir) } - { config_field_optional!(config, translate, "LABEL.SLEEP_TIMER_MINS", sleep_timer_mins) } - { config_field!(config, translate, "LABEL.CONNECT_TIMEOUT_SECS", connect_timeout_secs) } - { config_field_optional!(config, translate, "LABEL.CUSTOM_STREAM_RESPONSE_PATH", custom_stream_response_path) } + { config_field_bool!(config.config, translate.t("LABEL.UPDATE_ON_BOOT"), update_on_boot) } + { config_field_bool!(config.config, translate.t("LABEL.CONFIG_HOT_RELOAD"), config_hot_reload) } + { config_field_bool!(config.config, translate.t("LABEL.USER_ACCESS_CONTROL"), user_access_control) } + { config_field!(config.config, translate.t("LABEL.THREADS"), threads) } + { config_field!(config.config, translate.t("LABEL.WORKING_DIR"), working_dir) } + { config_field_optional!(config.config, translate.t("LABEL.MAPPING_PATH"), mapping_path) } + { config_field_optional!(config.config, translate.t("LABEL.BACKUP_DIR"), backup_dir) } + { config_field_optional!(config.config, translate.t("LABEL.USER_CONFIG_DIR"), user_config_dir) } + { config_field_optional!(config.config, translate.t("LABEL.SLEEP_TIMER_MINS"), sleep_timer_mins) } + { config_field!(config.config, translate.t("LABEL.CONNECT_TIMEOUT_SECS"), connect_timeout_secs) } + { config_field_optional!(config.config, translate.t("LABEL.CUSTOM_STREAM_RESPONSE_PATH"), custom_stream_response_path) } }}, None => html! { } diff --git a/frontend/src/app/components/config/messaging_config_view.rs b/frontend/src/app/components/config/messaging_config_view.rs new file mode 100644 index 000000000..e8939e749 --- /dev/null +++ b/frontend/src/app/components/config/messaging_config_view.rs @@ -0,0 +1,127 @@ +use crate::app::components::{Card, Chip}; +use crate::app::context::ConfigContext; +use crate::{config_field, config_field_child, config_field_empty, config_field_hide, config_field_optional}; +use shared::model::{PushoverMessagingConfigDto, RestMessagingConfigDto, TelegramMessagingConfigDto}; +use yew::prelude::*; +use yew_i18n::use_translation; + +#[function_component] +pub fn MessagingConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("ConfigContext not found"); + + let render_telegram = |telegram: Option<&TelegramMessagingConfigDto>| { + match telegram { + Some(entry) => html! { + +

{translate.t("LABEL.TELEGRAM")}

+ { config_field!(entry, translate.t("LABEL.BOT_TOKEN"), bot_token) } + { config_field_child!(entry, translate.t("LABEL.CHAT_ID"), { + html! { +
+ { + if entry.chat_ids.is_empty() { + html! {} + } else { + html! { for entry.chat_ids.iter().map(|t| html! { }) } + } + } +
+ } + })} +
+ }, + None => html! { + +

{translate.t("LABEL.TELEGRAM")}

+ { config_field_empty!(translate.t("LABEL.BOT_TOKEN")) } + { config_field_empty!(translate.t("LABEL.CHAT_IDS")) } +
+ }, + } + }; + + let render_rest = |rest: Option<&RestMessagingConfigDto>| { + match rest { + Some(entry) => html! { + +

{translate.t("LABEL.REST")}

+ { config_field!(entry, translate.t("LABEL.URL"), url) } +
+ }, + None => html! { + +

{translate.t("LABEL.REST")}

+ { config_field_empty!(translate.t("LABEL.URL")) } +
+ }, + } + }; + + let render_pushover = |pushover: Option<&PushoverMessagingConfigDto>| { + match pushover { + Some(entry) => html! { + +

{translate.t("LABEL.PUSHOVER")}

+ { config_field_optional!(entry, translate.t("LABEL.URL"), url) } + { config_field_hide!(entry, translate.t("LABEL.TOKEN"), token) } + { config_field!(entry, translate.t("LABEL.USER"), user) } +
+ }, + None => html! { + +

{translate.t("LABEL.PUSHOVER")}

+ { config_field_empty!(translate.t("LABEL.URL")) } + { config_field_empty!(translate.t("LABEL.TOKEN")) } + { config_field_empty!(translate.t("LABEL.USER")) } +
+ }, + } + }; + + let render_empty = || { + html! { + <> +
+ { config_field_empty!(translate.t("LABEL.NOTIFY_ON")) } +
+
+ {render_telegram(None)} + {render_rest(None)} + {render_pushover(None)} +
+ + } + }; + + html! { +
+ { + if let Some(config) = &config_ctx.config { + if let Some(messaging) = &config.config.messaging { + html! { + <> +
+ { config_field_child!(entry, translate.t("LABEL.NOTIFY_ON"), { + html! {
+ { for messaging.notify_on.iter().map(|t| html! { }) } +
} + })} +
+
+ {render_telegram(messaging.telegram.as_ref())} + {render_rest(messaging.rest.as_ref())} + {render_pushover(messaging.pushover.as_ref())} +
+ + } + } else { + {render_empty()} + } + } else { + {render_empty()} + } + } +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/mod.rs b/frontend/src/app/components/config/mod.rs index e4b85537e..90dc69523 100644 --- a/frontend/src/app/components/config/mod.rs +++ b/frontend/src/app/components/config/mod.rs @@ -1,4 +1,25 @@ +mod macros; + mod config_view; mod main_config_view; +mod api_config_view; +mod schedules_config_view; +mod messaging_config_view; +mod webui_config_view; +mod reverse_proxy_config_view; +mod hdhomerung_config_view; +mod proxy_config_view; +mod ipcheck_config_view; +mod video_config_view; + pub use config_view::*; -pub use main_config_view::*; \ No newline at end of file +pub use main_config_view::*; +pub use api_config_view::*; +pub use schedules_config_view::*; +pub use messaging_config_view::*; +pub use webui_config_view::*; +pub use reverse_proxy_config_view::*; +pub use hdhomerung_config_view::*; +pub use proxy_config_view::*; +pub use ipcheck_config_view::*; +pub use video_config_view::*; \ No newline at end of file diff --git a/frontend/src/app/components/config/proxy_config_view.rs b/frontend/src/app/components/config/proxy_config_view.rs new file mode 100644 index 000000000..7563f5096 --- /dev/null +++ b/frontend/src/app/components/config/proxy_config_view.rs @@ -0,0 +1,44 @@ +use yew::prelude::*; +use yew_i18n::use_translation; +use crate::app::context::ConfigContext; +use crate::{config_field, config_field_empty, config_field_optional, config_field_optional_hide}; + +#[function_component] +pub fn ProxyConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("ConfigContext not found"); + + let render_empty = || { + html! { +
+ { config_field_empty!(translate.t("LABEL.URL")) } + { config_field_empty!(translate.t("LABEL.USERNAME")) } + { config_field_empty!(translate.t("LABEL.PASSWORD")) } +
+ } + }; + + html! { +
+ { + if let Some(config) = &config_ctx.config { + if let Some(proxy) = &config.config.proxy { + html! { + <> +
+ { config_field!(proxy, translate.t("LABEL.URL"), url) } + { config_field_optional!(proxy, translate.t("LABEL.USERNAME"), username) } + { config_field_optional_hide!(proxy, translate.t("LABEL.PASSWORD"), password) } +
+ + } + } else { + { render_empty() } + } + } else { + { render_empty() } + } + } +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/reverse_proxy_config_view.rs b/frontend/src/app/components/config/reverse_proxy_config_view.rs new file mode 100644 index 000000000..1e359885e --- /dev/null +++ b/frontend/src/app/components/config/reverse_proxy_config_view.rs @@ -0,0 +1,125 @@ +use yew::prelude::*; +use yew_i18n::use_translation; +use shared::model::{CacheConfigDto, RateLimitConfigDto, StreamConfigDto}; +use crate::app::context::ConfigContext; +use crate::{config_field, config_field_bool, config_field_bool_empty, config_field_empty, config_field_optional}; +use crate::app::components::Card; + +#[function_component] +pub fn ReverseProxyConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("ConfigContext not found"); + + let render_cache = |config: Option<&CacheConfigDto>| { + match config { + Some(entry) => html! { + +

{translate.t("LABEL.CACHE")}

+ { config_field_bool!(entry, translate.t("LABEL.ENABLED"), enabled) } + { config_field_optional!(entry, translate.t("LABEL.SIZE"), size) } + { config_field_optional!(entry, translate.t("LABEL.DIRECTORY"), dir) } +
+ }, + None => html! { + +

{translate.t("LABEL.CACHE")}

+ { config_field_empty!(translate.t("LABEL.ENABLED")) } + { config_field_empty!(translate.t("LABEL.SIZE")) } + { config_field_empty!(translate.t("LABEL.DIRECTORY")) } +
+ }, + } + }; + let render_stream = |config: Option<&StreamConfigDto>| { + //pub buffer: Option, + match config { + Some(entry) => html! { + +

{translate.t("LABEL.STREAM")}

+ { config_field_bool!(entry, translate.t("LABEL.RETRY"), retry) } + { config_field_optional!(entry, translate.t("LABEL.THROTTLE"), throttle) } + { config_field!(entry, translate.t("LABEL.GRACE_PERIOD_MILLIS"), grace_period_millis) } + { config_field!(entry, translate.t("LABEL.GRACE_PERIOD_TIMEOUT_SECS"), grace_period_timeout_secs) } + { config_field!(entry, translate.t("LABEL.FORCED_RETRY_INTERVAL_SECS"), forced_retry_interval_secs) } + { config_field!(entry, translate.t("LABEL.THROTTLE_KBPS"), throttle_kbps) } +
+ }, + None => html! { + +

{translate.t("LABEL.STREAM")}

+ { config_field_empty!(translate.t("LABEL.RETRY")) } + { config_field_empty!(translate.t("LABEL.THROTTLE")) } + { config_field_empty!(translate.t("LABEL.GRACE_PERIOD_MILLIS")) } + { config_field_empty!(translate.t("LABEL.GRACE_PERIOD_TIMEOUT_SECS")) } + { config_field_empty!(translate.t("LABEL.FORCED_RETRY_INTERVAL_SECS")) } + { config_field_empty!(translate.t("LABEL.THROTTLE_KBPS")) } +
+ }, + } + }; + + let render_rate_limit = |config: Option<&RateLimitConfigDto>| { + match config { + Some(entry) => html! { + +

{translate.t("LABEL.RATE_LIMIT")}

+ { config_field_bool!(entry, translate.t("LABEL.ENABLED"), enabled) } + { config_field!(entry, translate.t("LABEL.PERIOD_MILLIS"), period_millis) } + { config_field!(entry, translate.t("LABEL.BURST_SIZE"), burst_size) } +
+ }, + None => html! { + +

{translate.t("LABEL.RATE_LIMIT")}

+ { config_field_empty!(translate.t("LABEL.ENABLED")) } + { config_field_empty!(translate.t("LABEL.PERIOD_MILLIS")) } + { config_field_empty!(translate.t("LABEL.BURST_SIZE")) } +
+ }, + } + }; + + let render_empty = || { + html! { + <> +
+ { config_field_bool_empty!(translate.t("LABEL.RESOURCE_REWRITE_DISABLED")) } + { config_field_bool_empty!(translate.t("LABEL.DISABLE_REFERER_HEADER")) } +
+
+ {render_cache(None)} + {render_rate_limit(None)} + {render_stream(None)} +
+ + } + }; + + html! { +
+ { + if let Some(config) = &config_ctx.config { + if let Some(reverse_proxy) = &config.config.reverse_proxy { + html! { + <> +
+ { config_field_bool!(reverse_proxy, translate.t("LABEL.RESOURCE_REWRITE_DISABLED"), resource_rewrite_disabled) } + { config_field_bool!(reverse_proxy, translate.t("LABEL.DISABLE_REFERER_HEADER"), disable_referer_header) } +
+
+ { render_cache(reverse_proxy.cache.as_ref()) } + { render_rate_limit(reverse_proxy.rate_limit.as_ref()) } + { render_stream(reverse_proxy.stream.as_ref()) } +
+ + } + } else { + {render_empty()} + } + } else { + {render_empty()} + } + } +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/schedules_config_view.rs b/frontend/src/app/components/config/schedules_config_view.rs new file mode 100644 index 000000000..e98acdbff --- /dev/null +++ b/frontend/src/app/components/config/schedules_config_view.rs @@ -0,0 +1,49 @@ +use crate::app::components::{Chip, NoContent}; +use crate::app::ConfigContext; +use yew::prelude::*; +use yew_i18n::use_translation; +use crate::{config_field, config_field_child}; + +#[function_component] +pub fn SchedulesConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("Config context not found"); + + html! { +
+
+ { + match config_ctx.config.as_ref() { + Some(config) => { + match config.config.schedules.as_ref() { + Some(schedules) => html! { + { for schedules.iter().map(|entry| html! { +
+ { config_field!(entry, translate.t("LABEL.SCHEDULE"), schedule) } + { config_field_child!(entry, translate.t("LABEL.TARGETS"), { + html! {
+ { match entry.targets.as_ref() { + None => html! { }, + Some(targets) => if targets.is_empty() { + html! { } + } else { + html! { for targets.iter().map(|t| html! { }) } + } + }} +
} + })} +
+ }) } + }, + None => html! { } + }}, + None => html! { } + } + } +
+ +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/video_config_view.rs b/frontend/src/app/components/config/video_config_view.rs new file mode 100644 index 000000000..c9677b520 --- /dev/null +++ b/frontend/src/app/components/config/video_config_view.rs @@ -0,0 +1,106 @@ +use crate::app::components::{Card, Chip}; +use crate::app::context::ConfigContext; +use crate::{config_field_bool, config_field_child, config_field_empty, config_field_optional}; +use shared::model::VideoDownloadConfigDto; +use yew::prelude::*; +use yew_i18n::use_translation; + +#[function_component] +pub fn VideoConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("ConfigContext not found"); + + let render_extensions = |extensions: &Vec| html! { + + { config_field_child!(entry, translate.t("LABEL.EXTENSIONS"), { + html! { +
+ { + html! { for extensions.iter().map(|t| html! { }) } + } +
+ }})} +
+ }; + + let render_download = |download: Option<&VideoDownloadConfigDto>| html! { + match download { + Some(entry) => html! { + +

{translate.t("LABEL.DOWNLOAD")}

+ { config_field_bool!(entry, translate.t("LABEL.ORGANIZE_INTO_DIRECTORIES"), organize_into_directories) } + { config_field_optional!(entry, translate.t("LABEL.DIRECTORY"), directory) } + { config_field_optional!(entry, translate.t("LABEL.EPISODE_PATTERN"), episode_pattern) } + { config_field_child!(entry.headers, translate.t("LABEL.HEADERS"), { + html! { +
+
    + { + if entry.headers.is_empty() { + html! {} + } else { + html! { + for entry.headers.iter().map(|(key, value)| html! { +
  • {key}{":"} {value}
  • + }) + } + } + } +
+
+ } + })} +
+ }, + None => html! { + +

{translate.t("LABEL.DOWNLOAD")}

+ { config_field_empty!(translate.t("LABEL.ORGANIZE_INTO_DIRECTORIES")) } + { config_field_empty!(translate.t("LABEL.DIRECTORY")) } + { config_field_empty!(translate.t("LABEL.EPISODE_PATTERN")) } + { config_field_empty!(translate.t("LABEL.HEADERS")) } +
+ }, + } + }; + + let render_empty = || { + html! { + <> +
+ { config_field_empty!(translate.t("LABEL.WEB_SEARCH")) } +
+
+ { config_field_empty!(translate.t("LABEL.EXTENSIONS")) } + { render_download(None) } +
+ + } + }; + + html! { +
+ { + if let Some(config) = &config_ctx.config { + if let Some(video) = &config.config.video { + html! { + <> +
+ { config_field_optional!(video, translate.t("LABEL.WEB_SEARCH"), web_search) } +
+
+ { render_extensions(&video.extensions) } + { render_download(video.download.as_ref()) } +
+ + } + } else { + { render_empty() } + } + } else { + { render_empty() } + } + } +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/config/webui_config_view.rs b/frontend/src/app/components/config/webui_config_view.rs new file mode 100644 index 000000000..c42a8e4d9 --- /dev/null +++ b/frontend/src/app/components/config/webui_config_view.rs @@ -0,0 +1,60 @@ +use crate::app::components::{Card, NoContent}; +use crate::app::context::ConfigContext; +use crate::{config_field, config_field_bool, config_field_empty, config_field_hide, config_field_optional}; +use yew::prelude::*; +use yew_i18n::use_translation; + +#[function_component] +pub fn WebUiConfigView() -> Html { + let translate = use_translation(); + let config_ctx = use_context::().expect("ConfigContext not found"); + + html! { +
+
+ { + if let Some(config) = &config_ctx.config { + if let Some(web_ui) = &config.config.web_ui { + html! { + <> + { config_field_bool!(web_ui, translate.t("LABEL.ENABLED"), enabled) } + { config_field_bool!(web_ui, translate.t("LABEL.USER_UI_ENABLED"), enabled) } + { config_field_optional!(web_ui, translate.t("LABEL.PATH"), path) } + { config_field_optional!(web_ui, translate.t("LABEL.PLAYER_SERVER"), player_server) } + +

{translate.t("LABEL.AUTH")}

+ { + match web_ui.auth.as_ref() { + Some(auth) => html!{ + <> + { config_field_bool!(auth, translate.t("LABEL.ENABLED"), enabled) } + { config_field!(auth, translate.t("LABEL.ISSUER"), issuer) } + { config_field_hide!(auth, translate.t("LABEL.SECRET"), secret) } + { config_field!(auth, translate.t("LABEL.TOKEN_TTL_MINS"), token_ttl_mins) } + { config_field_optional!(auth, translate.t("LABEL.USERFILE"), userfile) } + + }, + None => html!{ + <> + { config_field_empty!(translate.t("LABEL.ENABLED")) } + { config_field_empty!(translate.t("LABEL.ISSUER")) } + { config_field_empty!(translate.t("LABEL.SECRET")) } + { config_field_empty!(translate.t("LABEL.TOKEN_TTL_MINS")) } + { config_field_empty!(translate.t("LABEL.USERFILE")) } + + } + }} +
+ + } + } else { + html! { } + } + } else { + html! { } + } + } +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/mod.rs b/frontend/src/app/components/mod.rs index a1d527a2e..c8f3c5a95 100644 --- a/frontend/src/app/components/mod.rs +++ b/frontend/src/app/components/mod.rs @@ -34,6 +34,7 @@ mod accordion; mod accordion_panel; mod csv_table; mod config; +mod tabset; // pub use self::input::*; // pub use self::menu_item::*; // pub use self::popup_menu::*; @@ -68,4 +69,5 @@ pub use self::toastr::*; pub use self::drop_down_icon_button::*; pub use self::accordion::*; pub use self::accordion_panel::*; -pub use self::csv_table::*; \ No newline at end of file +pub use self::csv_table::*; +pub use self::tabset::*; \ No newline at end of file diff --git a/frontend/src/app/components/playlist/playlist_editor_page.rs b/frontend/src/app/components/playlist/playlist_editor_page.rs index b6d68c0e2..4de2ef36a 100644 --- a/frontend/src/app/components/playlist/playlist_editor_page.rs +++ b/frontend/src/app/components/playlist/playlist_editor_page.rs @@ -1,6 +1,6 @@ use std::fmt::Display; use std::str::FromStr; -use shared::error::{TuliproxError,TuliproxErrorKind}; +use shared::error::{TuliproxError}; use shared::info_err; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/frontend/src/app/components/playlist/playlist_explorer_page.rs b/frontend/src/app/components/playlist/playlist_explorer_page.rs index a843f9af8..de656a10c 100644 --- a/frontend/src/app/components/playlist/playlist_explorer_page.rs +++ b/frontend/src/app/components/playlist/playlist_explorer_page.rs @@ -1,6 +1,6 @@ use std::fmt::Display; use std::str::FromStr; -use shared::error::{TuliproxError,TuliproxErrorKind}; +use shared::error::{TuliproxError}; use shared::info_err; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/frontend/src/app/components/tabset.rs b/frontend/src/app/components/tabset.rs new file mode 100644 index 000000000..f412c7fd6 --- /dev/null +++ b/frontend/src/app/components/tabset.rs @@ -0,0 +1,139 @@ +use yew::prelude::*; +use crate::app::components::{IconButton, TextButton, Panel}; + +#[derive(Clone, Debug, PartialEq)] +pub struct TabItem { + pub id: String, + pub title: String, + pub icon: String, + pub children: Html, +} + +#[derive(Properties, Clone, PartialEq)] +pub struct TabSetProps { + pub tabs: Vec, + #[prop_or_default] + pub class: String, + #[prop_or_default] + pub active_tab: Option, + #[prop_or_default] + pub on_tab_change: Option>, +} + +#[function_component] +pub fn TabSet(props: &TabSetProps) -> Html { + let active_tab = use_state(|| { + props.active_tab.clone() + .or_else(|| props.tabs.first().map(|tab| tab.id.clone())) + .unwrap_or_default() + }); + + // Update active tab when prop changes + { + let active_tab_state = active_tab.clone(); + let prop_active = props.active_tab.clone(); + use_effect_with(prop_active, move |new_active| { + if let Some(new_tab) = new_active { + active_tab_state.set(new_tab.clone()); + } + }); + } + + let handle_tab_click = { + let active_tab_state = active_tab.clone(); + let on_change = props.on_tab_change.clone(); + Callback::from(move |tab_id: String| { + active_tab_state.set(tab_id.clone()); + if let Some(callback) = &on_change { + callback.emit(tab_id); + } + }) + }; + + let render_tab_buttons = { + let tabs = props.tabs.clone(); + let active_tab_id = (*active_tab).clone(); + let handle_click = handle_tab_click.clone(); + + html! { +
+ { + tabs.iter().map(|tab| { + let tab_id = tab.id.clone(); + let is_active = tab_id == active_tab_id; + let click_handler = handle_click.clone(); + + html! { +
+ // Desktop: TextButton +
+ +
+ + // Mobile: IconButton +
+ +
+
+ } + }).collect::() + } +
+ } + }; + + let render_tab_content = { + let tabs = props.tabs.clone(); + let active_tab_id = (*active_tab).clone(); + + html! { +
+ { + tabs.iter().map(|tab| { + html! { + + { tab.children.clone() } + + } + }).collect::() + } +
+ } + }; + + html! { +
+ { render_tab_buttons } + { render_tab_content } +
+ } +} \ No newline at end of file diff --git a/frontend/src/app/components/userlist/page.rs b/frontend/src/app/components/userlist/page.rs index 82fe97436..2694f3b5c 100644 --- a/frontend/src/app/components/userlist/page.rs +++ b/frontend/src/app/components/userlist/page.rs @@ -1,6 +1,6 @@ use std::fmt::Display; use std::str::FromStr; -use shared::error::{TuliproxError,TuliproxErrorKind}; +use shared::error::{TuliproxError}; use shared::info_err; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/frontend/src/model/explorer_source_type.rs b/frontend/src/model/explorer_source_type.rs index 60b41d6eb..de3ccff74 100644 --- a/frontend/src/model/explorer_source_type.rs +++ b/frontend/src/model/explorer_source_type.rs @@ -1,6 +1,6 @@ use std::fmt; use std::str::FromStr; -use shared::error::{info_err, TuliproxError, TuliproxErrorKind}; +use shared::error::{info_err, TuliproxError}; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ExplorerSourceType { diff --git a/frontend/src/model/view_type.rs b/frontend/src/model/view_type.rs index 6c0141e7a..5fe215c8b 100644 --- a/frontend/src/model/view_type.rs +++ b/frontend/src/model/view_type.rs @@ -1,14 +1,14 @@ use std::fmt; use std::str::FromStr; -use shared::error::{info_err, TuliproxError, TuliproxErrorKind}; +use shared::error::{info_err, TuliproxError}; -const DASHBOARD: &'static str = "dashboard"; -const STATS: &'static str = "stats"; -const USERS: &'static str = "users"; -const CONFIG: &'static str = "config"; -const PLAYLIST_UPDATE: &'static str = "playlist_update"; -const PLAYLIST_EDITOR: &'static str = "playlist_editor"; -const PLAYLIST_EXPLORER: &'static str = "playlist_explorer"; +const DASHBOARD: &str = "dashboard"; +const STATS: &str = "stats"; +const USERS: &str = "users"; +const CONFIG: &str = "config"; +const PLAYLIST_UPDATE: &str = "playlist_update"; +const PLAYLIST_EDITOR: &str = "playlist_editor"; +const PLAYLIST_EXPLORER: &str = "playlist_explorer"; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/shared/src/error/tuliprox_error.rs b/shared/src/error/tuliprox_error.rs index 2aedf8d6f..92bc9913e 100644 --- a/shared/src/error/tuliprox_error.rs +++ b/shared/src/error/tuliprox_error.rs @@ -9,7 +9,7 @@ macro_rules! get_errors_notify_message { } else { let text = $errors .iter() - .filter(|&err| err.kind == TuliproxErrorKind::Notify) + .filter(|&err| err.kind == $crate::error::TuliproxErrorKind::Notify) .map(|err| err.message.as_str()) .collect::>() .join("\r\n"); @@ -27,7 +27,7 @@ pub use get_errors_notify_message; #[macro_export] macro_rules! notify_err { ($text:expr) => { - TuliproxError::new(TuliproxErrorKind::Notify, $text) + $crate::error::TuliproxError::new($crate::error::TuliproxErrorKind::Notify, $text) }; } @@ -36,7 +36,7 @@ pub use notify_err; #[macro_export] macro_rules! info_err { ($text:expr) => { - TuliproxError::new(TuliproxErrorKind::Info, $text) + $crate::error::TuliproxError::new($crate::error::TuliproxErrorKind::Info, $text) }; } pub use info_err; @@ -45,7 +45,7 @@ pub use info_err; #[macro_export] macro_rules! create_tuliprox_error { ($kind: expr, $($arg:tt)*) => { - TuliproxError::new($kind, format!($($arg)*)) + $crate::error::TuliproxError::new($kind, format!($($arg)*)) } } pub use create_tuliprox_error; @@ -53,7 +53,7 @@ pub use create_tuliprox_error; #[macro_export] macro_rules! create_tuliprox_error_result { ($kind: expr, $($arg:tt)*) => { - Err(TuliproxError::new($kind, format!($($arg)*))) + Err($crate::error::TuliproxError::new($kind, format!($($arg)*))) } } pub use create_tuliprox_error_result; @@ -71,7 +71,7 @@ macro_rules! handle_tuliprox_error_result_list { }) .collect::>(); if !&errors.is_empty() { - return Err(TuliproxError::new($kind, errors.join("\n"))); + return Err($crate::error::TuliproxError::new($kind, errors.join("\n"))); } } } @@ -82,7 +82,7 @@ pub use handle_tuliprox_error_result_list; macro_rules! handle_tuliprox_error_result { ($kind:expr, $result: expr) => { if let Err(err) = $result { - return Err(TuliproxError::new($kind, err.to_string())); + return Err($crate::error::TuliproxError::new($kind, err.to_string())); } } } diff --git a/shared/src/model/config/api_proxy.rs b/shared/src/model/config/api_proxy.rs index ea6df8163..3a033f1e6 100644 --- a/shared/src/model/config/api_proxy.rs +++ b/shared/src/model/config/api_proxy.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use crate::error::{info_err, TuliproxError, TuliproxErrorKind}; +use crate::error::{info_err, TuliproxError}; use crate::model::{ProxyUserCredentialsDto}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] diff --git a/shared/src/model/config/cache.rs b/shared/src/model/config/cache.rs index 5ca65db1d..c9612a3f6 100644 --- a/shared/src/model/config/cache.rs +++ b/shared/src/model/config/cache.rs @@ -1,5 +1,5 @@ use std::path::PathBuf; -use crate::error::{TuliproxError, TuliproxErrorKind}; +use crate::error::{TuliproxError}; use crate::info_err; use crate::utils::parse_size_base_2; use path_clean::PathClean; diff --git a/shared/src/model/config/epg.rs b/shared/src/model/config/epg.rs index 000670c42..e94b96e4e 100644 --- a/shared/src/model/config/epg.rs +++ b/shared/src/model/config/epg.rs @@ -1,5 +1,5 @@ use crate::{info_err}; -use crate::error::{TuliproxError, TuliproxErrorKind}; +use crate::error::{TuliproxError}; use crate::model::EpgSmartMatchConfigDto; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] diff --git a/shared/src/model/messaging.rs b/shared/src/model/messaging.rs index d6affab47..4970fa2ac 100644 --- a/shared/src/model/messaging.rs +++ b/shared/src/model/messaging.rs @@ -1,3 +1,4 @@ +use std::fmt; #[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub enum MsgKind { @@ -9,4 +10,15 @@ pub enum MsgKind { Error, #[serde(rename = "watch")] Watch, +} +impl fmt::Display for MsgKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + MsgKind::Info => "Info", + MsgKind::Stats => "Stats", + MsgKind::Error => "Error", + MsgKind::Watch => "Watch", + }; + write!(f, "{s}") + } } \ No newline at end of file