From 0ad2eae1cc20877d3dcc6187b3ab8ac6fa928560 Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 4 Aug 2025 14:18:46 +0200 Subject: [PATCH] WebUI Source Explorer custom source download --- Cargo.lock | 1 + backend/src/api/endpoints/websocket_api.rs | 9 ++ backend/src/api/model/event_manager.rs | 3 +- backend/src/processing/processor/playlist.rs | 55 ++++--- backend/src/utils/step_measure.rs | 41 +++-- .../src/component/user-view/user-view.scss | 2 +- shared/src/model/web_socket.rs | 3 +- webui/Cargo.toml | 1 + webui/Trunk.toml | 1 + webui/public/assets/i18n/en.json | 6 +- webui/scss/_size.scss | 2 + webui/scss/_theme.scss | 37 ++++- webui/scss/app/components/_dialog.scss | 2 +- webui/scss/app/components/_filter.scss | 2 + webui/scss/app/components/_login.scss | 4 + .../components/dashboard/_action_card.scss | 7 + .../components/dashboard/_dashboard_view.scss | 6 +- .../app/components/dashboard/_stats_view.scss | 2 +- .../playlist/_playlist_explorer.scss | 4 +- .../playlist/_playlist_update_view.scss | 2 +- .../components/playlist/_source_selector.scss | 49 +++++- .../playlist/target/_target_table.scss | 4 + webui/src/app/components/dashboard/mod.rs | 2 + .../playlist_progress_status_card.rs | 34 +++++ .../app/components/dashboard/stats_view.rs | 5 +- webui/src/app/components/login.rs | 2 +- .../components/playlist/source_selector.rs | 141 ++++++++++++++++-- webui/src/model/event_message.rs | 3 +- webui/src/services/websocket_service.rs | 3 + 29 files changed, 366 insertions(+), 67 deletions(-) create mode 100644 webui/scss/_size.scss create mode 100644 webui/src/app/components/dashboard/playlist_progress_status_card.rs diff --git a/Cargo.lock b/Cargo.lock index d51dbd64c..a53f47c03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -909,6 +909,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "chrono", "futures", "futures-signals", "gloo-storage 0.3.0", diff --git a/backend/src/api/endpoints/websocket_api.rs b/backend/src/api/endpoints/websocket_api.rs index 1dd2a2a67..dc15a1d3c 100644 --- a/backend/src/api/endpoints/websocket_api.rs +++ b/backend/src/api/endpoints/websocket_api.rs @@ -230,6 +230,15 @@ async fn handle_event_message(socket: &mut WebSocket, event: EventMessage, handl .await .map_err(|e| format!("Playlist update event: {e} "))?; } + EventMessage::PlaylistUpdateProgress(target, msg) => { + let msg = ProtocolMessage::PlaylistUpdateProgressResponse(target, msg) + .to_bytes() + .map_err(|e| e.to_string())?; + socket + .send(Message::Binary(msg)) + .await + .map_err(|e| format!("Playlist update progress event: {e} "))?; + } } } } diff --git a/backend/src/api/model/event_manager.rs b/backend/src/api/model/event_manager.rs index 4dec7b578..7688f8688 100644 --- a/backend/src/api/model/event_manager.rs +++ b/backend/src/api/model/event_manager.rs @@ -9,7 +9,8 @@ pub enum EventMessage { ActiveUser(usize, usize), // user_count, connection count ActiveProvider(String, usize), // provider name, connections ConfigChange(ConfigType), - PlaylistUpdate(PlaylistUpdateState) + PlaylistUpdate(PlaylistUpdateState), + PlaylistUpdateProgress(String, String), } pub struct EventManager { diff --git a/backend/src/processing/processor/playlist.rs b/backend/src/processing/processor/playlist.rs index b6fc07294..84aab1d96 100644 --- a/backend/src/processing/processor/playlist.rs +++ b/backend/src/processing/processor/playlist.rs @@ -30,7 +30,8 @@ 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::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}; +use shared::model::{CounterModifier, FieldGetAccessor, FieldSetAccessor, InputType, ItemField, MsgKind, PlaylistEntry, + PlaylistGroup, PlaylistItem, PlaylistUpdateState, ProcessingOrder, UUIDType, XtreamCluster}; use shared::utils::default_as_default; use std::time::Instant; use crate::api::model::{EventManager, EventMessage}; @@ -233,7 +234,9 @@ fn is_target_enabled(target: &ConfigTarget, user_targets: &ProcessTargets) -> bo (!user_targets.enabled && target.enabled) || (user_targets.enabled && user_targets.has_target(target.id)) } -async fn process_source(client: Arc, cfg: Arc, source_idx: usize, user_targets: Arc) -> (Vec, Vec, Vec) { +async fn process_source(client: Arc, cfg: Arc, source_idx: usize, + user_targets: Arc, event_manager: Option>) + -> (Vec, Vec, Vec) { let sources = cfg.sources.load(); let mut errors = vec![]; let mut input_stats = HashMap::::new(); @@ -290,9 +293,11 @@ async fn process_source(client: Arc, cfg: Arc, sourc errors.push(notify_err!(format!("Source at index {source_idx} is empty: {}", source.inputs.iter().map(|i| i.name.as_str()).collect::>().join(", ")))); } else { debug_if_enabled!("Source has {} groups", source_playlists.iter().map(|fpl| fpl.playlistgroups.len()).sum::()); + let event_manager_clone = event_manager.clone(); for target in &source.targets { + let event_manager_clone = event_manager_clone.clone(); if is_target_enabled(target, &user_targets) { - match process_playlist_for_target(&cfg, Arc::clone(&client), &mut source_playlists, target, &mut input_stats, &mut errors).await { + match process_playlist_for_target(&cfg, Arc::clone(&client), &mut source_playlists, target, &mut input_stats, &mut errors, event_manager_clone).await { Ok(()) => { target_stats.push(TargetStats::success(&target.name)); } @@ -326,7 +331,7 @@ fn create_input_stat(group_count: usize, channel_count: usize, error_count: usiz } } -async fn process_sources(client: Arc, config: &Arc, user_targets: Arc) -> (Vec, Vec) { +async fn process_sources(client: Arc, config: &Arc, user_targets: Arc, event_manager: Option>) -> (Vec, Vec) { let mut handle_list = vec![]; let thread_num = config.config.load().threads; let sources = config.sources.load(); @@ -348,6 +353,7 @@ async fn process_sources(client: Arc, config: &Arc, let shared_stats = stats.clone(); let cfg = config.clone(); let usr_trgts = user_targets.clone(); + let event_manager = event_manager.clone(); if process_parallel { let http_client = Arc::clone(&client); let handles = &mut handle_list; @@ -356,7 +362,8 @@ async fn process_sources(client: Arc, config: &Arc, match tokio::runtime::Runtime::new() { Ok(rt) => { rt.block_on(async { - let (input_stats, target_stats, mut res_errors) = process_source(Arc::clone(&http_client), cfg, index, usr_trgts).await; + let (input_stats, target_stats, mut res_errors) = + process_source(Arc::clone(&http_client), cfg, index, usr_trgts, event_manager).await; shared_errors.lock().await.append(&mut res_errors); let process_stats = SourceStats::new(input_stats, target_stats); shared_stats.lock().await.push(process_stats); @@ -370,7 +377,7 @@ async fn process_sources(client: Arc, config: &Arc, handles.drain(..).for_each(|handle| { let _ = handle.join(); }); } } else { - let (input_stats, target_stats, mut res_errors) = process_source(Arc::clone(&client), cfg, index, usr_trgts).await; + let (input_stats, target_stats, mut res_errors) = process_source(Arc::clone(&client), cfg, index, usr_trgts, event_manager).await; shared_errors.lock().await.append(&mut res_errors); let process_stats = SourceStats::new(input_stats, target_stats); shared_stats.lock().await.push(process_stats); @@ -454,7 +461,8 @@ async fn process_playlist_for_target(app_config: &AppConfig, playlists: &mut [FetchedPlaylist<'_>], target: &ConfigTarget, stats: &mut HashMap, - errors: &mut Vec) -> Result<(), Vec> { + errors: &mut Vec, + event_manager: Option>) -> Result<(), Vec> { let pipe = get_processing_pipe(target); debug_if_enabled!("Processing order is {}", &target.processing_order); @@ -462,8 +470,16 @@ async fn process_playlist_for_target(app_config: &AppConfig, let mut processed_fetched_playlists: Vec = vec![]; debug!("Executing processing pipes"); + let broadcast_step = { + let event_manager = event_manager.clone(); + move |context: &str, msg: &str| { + if let Some(events) = &event_manager { + events.send_event(EventMessage::PlaylistUpdateProgress(context.to_owned(), msg.to_owned())); + } + } + }; - let mut step = StepMeasure::new("Pipes processed"); + let mut step = StepMeasure::new(&target.name, broadcast_step); for provider_fpl in playlists.iter_mut() { let mut processed_fpl = execute_pipe(target, &pipe, provider_fpl, &mut duplicates); playlist_resolve_series(app_config, Arc::clone(&client), target, errors, &pipe, provider_fpl, &mut processed_fpl).await; @@ -478,35 +494,35 @@ async fn process_playlist_for_target(app_config: &AppConfig, } processed_fetched_playlists.push(processed_fpl); } - - step.tick("Processed epg"); + step.tick("filter rename map"); let (new_epg, mut new_playlist) = process_epg(&mut processed_fetched_playlists); + step.tick("epg"); if new_playlist.is_empty() { + step.stop(""); info!("Playlist is empty: {}", &target.name); Ok(()) } else { // Process Trakt categories - step.tick("Processing Trakt categories"); trakt_playlist(&client, target, errors, &mut new_playlist).await; + step.tick("trakt categories"); - step.tick("Merged playlists"); let mut flat_new_playlist = flatten_groups(new_playlist); + step.tick("playlist merge"); - step.tick("Sorted playlists"); sort_playlist(target, &mut flat_new_playlist); - step.tick("Assigned channel number"); + step.tick("playlist sort"); assign_channel_no_playlist(&mut flat_new_playlist); - step.tick("Assigned channel counter"); + step.tick("assigning channel numbers"); map_playlist_counter(target, &mut flat_new_playlist); + step.tick("assigning channel counter"); - step.tick("Processed group watches"); let config = app_config.config.load(); process_watch(&config, &client, target, &flat_new_playlist); - step.tick("Persisting playlists"); + step.tick("group watches"); let result = persist_playlist(app_config, &mut flat_new_playlist, flatten_tvguide(&new_epg).as_ref(), target).await; - step.stop(); + step.stop("Persisting playlists"); result } } @@ -555,7 +571,8 @@ fn process_watch(cfg: &Config, client: &Arc, target: &ConfigTar pub async fn exec_processing(client: Arc, app_config: Arc, targets: Arc, event_manager: Option>) { let start_time = Instant::now(); - let (stats, errors) = process_sources(Arc::clone(&client), &app_config, targets.clone()).await; + let event_manager_clone = event_manager.clone(); + let (stats, errors) = process_sources(Arc::clone(&client), &app_config, targets.clone(), event_manager_clone).await; // log errors for err in &errors { error!("{}", err.message); diff --git a/backend/src/utils/step_measure.rs b/backend/src/utils/step_measure.rs index f5114ba43..63d23ab3d 100644 --- a/backend/src/utils/step_measure.rs +++ b/backend/src/utils/step_measure.rs @@ -17,32 +17,51 @@ fn format_duration(duration: Duration) -> String { } } +type StepMeasureCallback = Box; + pub struct StepMeasure { enabled: bool, - msg: String, + name: String, start: Instant, + step_start: Instant, + callback: StepMeasureCallback, } impl StepMeasure { - pub fn new(msg: &str) -> Self { + pub fn new(name: &str, cb: F) -> Self + where + F: Fn(&str, &str) + 'static + Send, + { Self { enabled: log_enabled!(Level::Debug), - msg: msg.to_string(), + name: name.to_owned(), start: Instant::now(), + step_start: Instant::now(), + callback: Box::new(cb), } } - pub fn tick(&mut self, msg: &str) { + pub fn tick(&mut self, step: &str) { if self.enabled { - debug!("{} in {}", self.msg, format_duration(self.start.elapsed())); - self.msg = msg.to_string(); - self.start = Instant::now(); + let msg = format!("{}: processed {step} in {}", self.name, format_duration(self.step_start.elapsed())); + debug!("{msg}"); + (self.callback)(&self.name, &msg); + self.step_start = Instant::now(); } } - pub fn stop(&mut self) { - if self.enabled && !self.msg.is_empty() { - debug!("{} in {}", self.msg, format_duration( self.start.elapsed())); + pub fn stop(&mut self, step: &str) { + if self.enabled { + if step.is_empty() { + debug!("{}: finished in {}", self.name, format_duration( self.start.elapsed())); + } else { + let msg = format!("{}: processed {step} in {}", self.name, format_duration(self.step_start.elapsed())); + let fmsg = format!("{}: finished in {}", self.name, format_duration( self.start.elapsed())); + debug!("{msg}"); + debug!("{fmsg}"); + (self.callback)(&self.name, &msg); + (self.callback)(&self.name, &fmsg); + } self.enabled = false; } } @@ -50,6 +69,6 @@ impl StepMeasure { impl Drop for StepMeasure { fn drop(&mut self) { - self.stop(); + self.stop(""); } } \ No newline at end of file diff --git a/frontend/src/component/user-view/user-view.scss b/frontend/src/component/user-view/user-view.scss index f45dd1ae4..a04e40623 100644 --- a/frontend/src/component/user-view/user-view.scss +++ b/frontend/src/component/user-view/user-view.scss @@ -15,7 +15,7 @@ dialog:not([open]) { @include common.textButton(); } min-height: 200px; - min-width: 400px; + min-width: 280px; } .label { diff --git a/shared/src/model/web_socket.rs b/shared/src/model/web_socket.rs index 50a93b494..cd996b9ba 100644 --- a/shared/src/model/web_socket.rs +++ b/shared/src/model/web_socket.rs @@ -81,7 +81,8 @@ pub enum ProtocolMessage { ActiveUserResponse(usize, usize), // user_count, connection count ActiveProviderResponse(String, usize), ConfigChangeResponse(ConfigType), - PlaylistUpdateResponse(PlaylistUpdateState) + PlaylistUpdateResponse(PlaylistUpdateState), + PlaylistUpdateProgressResponse(String, String), } impl ProtocolMessage { diff --git a/webui/Cargo.toml b/webui/Cargo.toml index a7f6b5d01..c57a4ac5f 100644 --- a/webui/Cargo.toml +++ b/webui/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] shared = { version = "0.1.0", path = "../shared" } +chrono = "0" yew = "0.21" yew-router = "0.18" yew-hooks = "0.3" diff --git a/webui/Trunk.toml b/webui/Trunk.toml index 65dac1f4f..d9dee414a 100644 --- a/webui/Trunk.toml +++ b/webui/Trunk.toml @@ -2,6 +2,7 @@ dist = "dist" [serve] +addresses = ["0.0.0.0"] port = 9899 [[proxy]] diff --git a/webui/public/assets/i18n/en.json b/webui/public/assets/i18n/en.json index 434ce6d27..ca52e5b9f 100644 --- a/webui/public/assets/i18n/en.json +++ b/webui/public/assets/i18n/en.json @@ -13,6 +13,7 @@ "CANCEL": "Cancel", "YES": "Yes", "CLOSE": "Close", + "DOWNLOAD": "Download", "SHOW_SELECTED": "Show selected", "SELECT_ALL": "Select All", "DESELECT_ALL": "Deselect all", @@ -151,6 +152,7 @@ "PLAYLISTS": "Playlists", "PLAYLIST_VIEWER": "Explore", "PLAYLIST_EXPLORER": "Playlist Browser", + "PLAYLIST_UPDATE": "Playlist Update", "SOURCES": "Sources", "SOURCE_PICKER": "Source Picker", "LIST": "List", @@ -273,7 +275,9 @@ "SUCCESS": "Successfully started playlist update!", "FAIL": "Playlist update failed!", "SUCCESS_FINISH": "Successfully updated playlist!", - "FAIL_FINISH": "Playlist update failed!" + "FAIL_FINISH": "Playlist update failed!", + "URL_MANDATORY": "Url is mandatory!", + "USERNAME_PASSWORD_MANDATORY": "Username and Password are mandatory!" }, "SOURCE_SELECTOR": { "MISSING_SELECTION": "No entry selected" diff --git a/webui/scss/_size.scss b/webui/scss/_size.scss new file mode 100644 index 000000000..a4aec2c33 --- /dev/null +++ b/webui/scss/_size.scss @@ -0,0 +1,2 @@ +$mobile-breakpoint: 720px; +$dashboard-mobile-breakpoint: 1280px; diff --git a/webui/scss/_theme.scss b/webui/scss/_theme.scss index 0f379e348..4341debbc 100644 --- a/webui/scss/_theme.scss +++ b/webui/scss/_theme.scss @@ -1,16 +1,18 @@ +@use './size'; + :root { - --mobile-size: 720px; - --dashboard-mobile-size: 1280px; + --mobile-size: size.$mobile-breakpoint; + --dashboard-mobile-size: size.$dashboard-mobile-breakpoint; --sidebar-width-expaned: 270px; --sidebar-width-collapsed: 42px; - --max-view-width: 1280px; + --max-view-width: size.$dashboard-mobile-breakpoint; --max-table-cell-width: 240px; --max-dialog-card-height: 300px; --media-size-mobile: 600px; --app-header-height: 56px; --scrollbar-width: 8px; --scrollbar-border-radius: 4px; - + --gap-small: 0.3rem; --gap-default: 0.5rem; --gap-large: 1rem; @@ -27,7 +29,31 @@ --margin-mini: 0.5rem; --margin-small: 0.75rem; --margin-default: 1rem; +} +@media (max-width: size.$mobile-breakpoint) { + :root { + --gap-small: 0.2rem; + --gap-default: 0.3rem; + --gap-large: 0.5rem; + --gap-larger: 0.75rem; + + --padding-micro: 0.2rem; + --padding-mini: 0.3rem; + --padding-small: 0.5rem; + --padding-default: 0.75rem; + --padding-large: 1rem; + --padding-larger: 1.25rem; + + --margin-micro: 0.2rem; + --margin-mini: 0.3rem; + --margin-small: 0.5rem; + --margin-default: 0.75rem; + + } +} + +:root { --primary-color: #124a9b; --secondary-color: #f67070; @@ -69,7 +95,7 @@ --text-button-active-background-color: #1e2d25; --text-button-active-color: #49dc7f; --text-button-active-hover-background-color: #1e2d25; - --text-button-active-hover-color: #ffffff; + --text-button-active-hover-color: #49dc7f; --toggle-switch-on-background-color: #36c55a; --toggle-switch-off-background-color: #3f3f45; @@ -246,7 +272,6 @@ --menu-icon-color-status: #F9E400; --toolbar-bg-color: #161b25; - } body[data-theme="bright"] { diff --git a/webui/scss/app/components/_dialog.scss b/webui/scss/app/components/_dialog.scss index 18a578e7c..e18921e61 100644 --- a/webui/scss/app/components/_dialog.scss +++ b/webui/scss/app/components/_dialog.scss @@ -15,7 +15,7 @@ dialog { } .tp__dialog { - min-width: 400px; + min-width: 300px; &__toolbar { display: flex; flex-flow: row nowrap; diff --git a/webui/scss/app/components/_filter.scss b/webui/scss/app/components/_filter.scss index bd9b2b085..d381dbc19 100644 --- a/webui/scss/app/components/_filter.scss +++ b/webui/scss/app/components/_filter.scss @@ -1,5 +1,6 @@ .tp__filter { overflow: hidden; + .tp__filter__code { font-family: monospace; background: #1e1e1e; @@ -7,6 +8,7 @@ padding: 1rem; border-radius: 8px; white-space: pre; + overflow: auto; .binary_op-wrapper{ white-space: pre; diff --git a/webui/scss/app/components/_login.scss b/webui/scss/app/components/_login.scss index 7472e160f..610fe3940 100644 --- a/webui/scss/app/components/_login.scss +++ b/webui/scss/app/components/_login.scss @@ -1,3 +1,4 @@ +@use "../../size"; .tp__login-view { display: flex; flex-flow: column; @@ -11,6 +12,9 @@ border-radius: var(--border-radius); background-color: var(--card-background-color); width: 400px; + @media(max-width: size.$mobile-breakpoint) { + width: 280px; + } &__header { display: flex; diff --git a/webui/scss/app/components/dashboard/_action_card.scss b/webui/scss/app/components/dashboard/_action_card.scss index 7a37037e5..0917cb80d 100644 --- a/webui/scss/app/components/dashboard/_action_card.scss +++ b/webui/scss/app/components/dashboard/_action_card.scss @@ -1,3 +1,5 @@ +@use "./../../../size"; + .tp__action-card { display: flex; flex-flow: row nowrap; @@ -45,5 +47,10 @@ white-space: nowrap; } } +} +@media (max-width: size.$mobile-breakpoint) { + .tp__action-card { + flex-flow: column; + } } \ No newline at end of file diff --git a/webui/scss/app/components/dashboard/_dashboard_view.scss b/webui/scss/app/components/dashboard/_dashboard_view.scss index 234dda564..d97a520d4 100644 --- a/webui/scss/app/components/dashboard/_dashboard_view.scss +++ b/webui/scss/app/components/dashboard/_dashboard_view.scss @@ -1,4 +1,4 @@ -$dashboard-mobile-size: 1280px; +@use "../../../size"; .tp__dashboard { display: flex; @@ -23,7 +23,7 @@ $dashboard-mobile-size: 1280px; grid-template-columns: 1fr 1fr; gap: var(--gap-larger); - @media (max-width: $dashboard-mobile-size) { + @media (max-width: size.$dashboard-mobile-breakpoint) { grid-template-columns: 1fr; } @@ -34,7 +34,7 @@ $dashboard-mobile-size: 1280px; &-stats { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--gap-larger); } } diff --git a/webui/scss/app/components/dashboard/_stats_view.scss b/webui/scss/app/components/dashboard/_stats_view.scss index c3ee32039..de898c5db 100644 --- a/webui/scss/app/components/dashboard/_stats_view.scss +++ b/webui/scss/app/components/dashboard/_stats_view.scss @@ -18,7 +18,7 @@ &-group { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--gap-larger); } } diff --git a/webui/scss/app/components/playlist/_playlist_explorer.scss b/webui/scss/app/components/playlist/_playlist_explorer.scss index 21285dc9b..76379e120 100644 --- a/webui/scss/app/components/playlist/_playlist_explorer.scss +++ b/webui/scss/app/components/playlist/_playlist_explorer.scss @@ -1,5 +1,3 @@ -$mobile-size: 720px; - .tp__playlist-explorer { display: flex; flex-flow: column; @@ -61,7 +59,7 @@ $mobile-size: 720px; &-list { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--gap-default); box-sizing: border-box; overflow-y: auto; diff --git a/webui/scss/app/components/playlist/_playlist_update_view.scss b/webui/scss/app/components/playlist/_playlist_update_view.scss index eb4ca5d76..1920451fa 100644 --- a/webui/scss/app/components/playlist/_playlist_update_view.scss +++ b/webui/scss/app/components/playlist/_playlist_update_view.scss @@ -16,7 +16,7 @@ &__body { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--gap-default); box-sizing: border-box; overflow-y: auto; diff --git a/webui/scss/app/components/playlist/_source_selector.scss b/webui/scss/app/components/playlist/_source_selector.scss index 7adf4af04..be83cc3da 100644 --- a/webui/scss/app/components/playlist/_source_selector.scss +++ b/webui/scss/app/components/playlist/_source_selector.scss @@ -1,3 +1,5 @@ +@use "../../../size"; + .tp__playlist-source-selector { &__source-picker .tp__collapse-panel__body { @@ -9,19 +11,62 @@ &__source-picker__body, &__source-picker__header { display: flex; - flex-flow: column; + flex-flow: row; gap: var(--gap-large); + @media (max-width: size.$mobile-breakpoint) { + flex-flow: row wrap; + } } &__source-picker__header { padding-bottom: var(--padding-small); - margin-bottom: var(--padding-micro); + margin-bottom: var(--margin-micro); border-bottom: 1px solid var(--card-border-color); } + &__source-picker__body { + margin-top: var(--margin-micro); + .tp__panel { + width: 100%; + } + } &__source-list { display: flex; flex-flow: row wrap; + gap: var(--gap-default); + } + + &__source-custom-options { + display: flex; + flex-flow: row nowrap; + gap: 0; + box-sizing: border-box; + + button:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + button:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } + + &__source-custom { + display: flex; + flex-flow: column; gap: var(--gap-default); + box-sizing: border-box; + width: 100%; + + &-body { + display: flex; + flex-flow: row wrap; + gap: var(--gap-default); + align-items: flex-end; + box-sizing: border-box; + width: 100%; + + } } } \ No newline at end of file diff --git a/webui/scss/app/components/playlist/target/_target_table.scss b/webui/scss/app/components/playlist/target/_target_table.scss index f6077f82e..65383676a 100644 --- a/webui/scss/app/components/playlist/target/_target_table.scss +++ b/webui/scss/app/components/playlist/target/_target_table.scss @@ -8,5 +8,9 @@ } .tp__filter { max-width: var(--max-table-cell-width); + overflow: hidden !important; + .tp__filter__code { + overflow: hidden !important; + } } } \ No newline at end of file diff --git a/webui/src/app/components/dashboard/mod.rs b/webui/src/app/components/dashboard/mod.rs index eb85365af..9caac5d9f 100644 --- a/webui/src/app/components/dashboard/mod.rs +++ b/webui/src/app/components/dashboard/mod.rs @@ -8,6 +8,7 @@ mod status_card; mod ipinfo_action_card; mod dashboard_view; mod stats_view; +mod playlist_progress_status_card; pub use self::action_card::*; pub use self::status_card::*; @@ -19,3 +20,4 @@ pub use self::ipinfo_action_card::*; pub use self::github_action_card::*; pub use self::dashboard_view::*; pub use self::stats_view::*; +pub use self::playlist_progress_status_card::*; diff --git a/webui/src/app/components/dashboard/playlist_progress_status_card.rs b/webui/src/app/components/dashboard/playlist_progress_status_card.rs new file mode 100644 index 000000000..9d3fab355 --- /dev/null +++ b/webui/src/app/components/dashboard/playlist_progress_status_card.rs @@ -0,0 +1,34 @@ +use yew::{function_component, html, use_effect_with, use_state, Html}; +use yew_i18n::use_translation; +use crate::app::components::StatusCard; +use crate::hooks::use_service_context; +use crate::model::EventMessage; + +#[function_component] +pub fn PlaylistProgressStatusCard() -> Html { + let services = use_service_context(); + let translate = use_translation(); + let data = use_state(|| "-".to_owned()); + + { + let services_ctx = services.clone(); + let data_clone = data.clone(); + use_effect_with((), move |_| { + let services_ctx = services_ctx.clone(); + let data_clone = data_clone.clone(); + let subid = services_ctx.event.subscribe(move |msg| { + if let EventMessage::PlaylistUpdateProgress(_target, msg) = msg { + data_clone.set(format!("[{}] {msg}", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"))); + } + }); + move || services_ctx.event.unsubscribe(subid) + }); + } + + html! { + + } +} \ No newline at end of file diff --git a/webui/src/app/components/dashboard/stats_view.rs b/webui/src/app/components/dashboard/stats_view.rs index 4f12c6bb9..029bcb3fd 100644 --- a/webui/src/app/components/dashboard/stats_view.rs +++ b/webui/src/app/components/dashboard/stats_view.rs @@ -1,6 +1,6 @@ use yew::prelude::*; use yew_i18n::use_translation; -use crate::app::components::{Card, StatusCard, StatusContext}; +use crate::app::components::{Card, PlaylistProgressStatusCard, StatusCard, StatusContext}; #[function_component] pub fn StatsView() -> Html { @@ -54,6 +54,9 @@ pub fn StatsView() -> Html { +
+ +
diff --git a/webui/src/app/components/login.rs b/webui/src/app/components/login.rs index 8f72d2203..99a6c306d 100644 --- a/webui/src/app/components/login.rs +++ b/webui/src/app/components/login.rs @@ -85,7 +85,7 @@ pub fn Login() -> Html {