Restore bDS2 task panel styling
This commit is contained in:
@@ -8,7 +8,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
|
|
||||||
## Available Features
|
## Available Features
|
||||||
|
|
||||||
- Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars with locale-aware post dates, calendar months, and relative-dated entity lists, row deletion, dialogs, embedded Wry previews, a live Pico CSS theme editor, direct Preferences navigation, and per-project restart restoration of the active activity, shell visibility, and open editor tabs. Its shared task UI keeps queued work off blocking workers, shows every active local or remote task plus recent history, and supports cooperative cancellation without oversubscribing background work.
|
- Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars with locale-aware post dates, calendar months, and relative-dated entity lists, row deletion, dialogs, embedded Wry previews, a live Pico CSS theme editor, direct Preferences navigation, and per-project restart restoration of the active activity, shell visibility, and open editor tabs. Its shared task UI keeps queued work off blocking workers, presents active local and remote work plus recent history in bDS2-style grouped progress cards, and supports cooperative cancellation without oversubscribing background work.
|
||||||
- Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, cursor-preserving link and media insertion, live link/backlink graphs, media, and batch gallery-image import.
|
- Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, cursor-preserving link and media insertion, live link/backlink graphs, media, and batch gallery-image import.
|
||||||
- Media import including HEIC/HEIF decoding, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
|
- Media import including HEIC/HEIF decoding, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
|
||||||
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
|
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use iced::widget::text::Shaping;
|
use iced::widget::text::Shaping;
|
||||||
use iced::widget::{Space, button, column, container, row, scrollable, text};
|
use iced::widget::{Space, button, column, container, progress_bar, row, scrollable, text};
|
||||||
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
|
use iced::{Alignment, Background, Border, Color, Element, Font, Length, Theme};
|
||||||
|
|
||||||
use bds_core::engine::git::GitCommit;
|
use bds_core::engine::git::GitCommit;
|
||||||
use bds_core::engine::task::TaskStatus;
|
use bds_core::engine::task::TaskStatus;
|
||||||
@@ -14,7 +14,11 @@ use crate::state::tabs::{Tab, TabType};
|
|||||||
use crate::views::post_editor::ResolvedPostLink;
|
use crate::views::post_editor::ResolvedPostLink;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
fn task_row(snapshot: &TaskSnapshot, locale: UiLocale) -> Element<'static, Message> {
|
fn task_row(
|
||||||
|
snapshot: &TaskSnapshot,
|
||||||
|
locale: UiLocale,
|
||||||
|
is_group_child: bool,
|
||||||
|
) -> Element<'static, Message> {
|
||||||
let status = match &snapshot.status {
|
let status = match &snapshot.status {
|
||||||
TaskStatus::Pending => t(locale, "tasks.statusPending"),
|
TaskStatus::Pending => t(locale, "tasks.statusPending"),
|
||||||
TaskStatus::Running if snapshot.cancellation_requested => {
|
TaskStatus::Running if snapshot.cancellation_requested => {
|
||||||
@@ -25,32 +29,132 @@ fn task_row(snapshot: &TaskSnapshot, locale: UiLocale) -> Element<'static, Messa
|
|||||||
TaskStatus::Failed(error) => tw(locale, "tasks.statusFailed", &[("error", error)]),
|
TaskStatus::Failed(error) => tw(locale, "tasks.statusFailed", &[("error", error)]),
|
||||||
TaskStatus::Cancelled => t(locale, "tasks.statusCancelled"),
|
TaskStatus::Cancelled => t(locale, "tasks.statusCancelled"),
|
||||||
};
|
};
|
||||||
let progress = snapshot
|
|
||||||
.progress
|
let header = row![
|
||||||
.map(|value| format!(" ({:.0}%)", value * 100.0))
|
text(snapshot.label.clone())
|
||||||
.unwrap_or_default();
|
|
||||||
let phase = snapshot
|
|
||||||
.message
|
|
||||||
.as_deref()
|
|
||||||
.map(|message| format!(" — {message}"))
|
|
||||||
.unwrap_or_default();
|
|
||||||
let label = text(format!(
|
|
||||||
"{} — {}{}{}",
|
|
||||||
snapshot.label, status, progress, phase
|
|
||||||
))
|
|
||||||
.size(11)
|
.size(11)
|
||||||
.shaping(Shaping::Advanced)
|
.shaping(Shaping::Advanced)
|
||||||
.color(Color::from_rgb(0.70, 0.70, 0.75));
|
.font(Font {
|
||||||
let mut content = row![label].align_y(Alignment::Center).spacing(8);
|
weight: iced::font::Weight::Semibold,
|
||||||
|
..Font::DEFAULT
|
||||||
|
}),
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
text(status)
|
||||||
|
.size(10)
|
||||||
|
.shaping(Shaping::Advanced)
|
||||||
|
.color(task_status_color(&snapshot.status)),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center);
|
||||||
|
|
||||||
|
let mut rows: Vec<Element<'static, Message>> = vec![header.into()];
|
||||||
|
if let Some(message) = snapshot
|
||||||
|
.message
|
||||||
|
.as_deref()
|
||||||
|
.filter(|message| !message.is_empty())
|
||||||
|
{
|
||||||
|
rows.push(
|
||||||
|
text(message.to_string())
|
||||||
|
.size(11)
|
||||||
|
.shaping(Shaping::Advanced)
|
||||||
|
.color(Color::from_rgb8(0x9D, 0xA5, 0xB4))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(progress) = snapshot.progress {
|
||||||
|
rows.push(
|
||||||
|
row![
|
||||||
|
progress_bar(0.0..=1.0, progress.clamp(0.0, 1.0))
|
||||||
|
.height(Length::Fixed(6.0))
|
||||||
|
.style(task_progress_style),
|
||||||
|
text(format!("{:.0}%", progress * 100.0))
|
||||||
|
.size(10)
|
||||||
|
.color(Color::from_rgb8(0x9D, 0xA5, 0xB4)),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
if snapshot.is_cancellable {
|
if snapshot.is_cancellable {
|
||||||
content = content.push(Space::with_width(Length::Fill)).push(
|
rows.push(
|
||||||
button(text(t(locale, "tasks.cancelTask")).size(11))
|
row![
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
button(text(t(locale, "tasks.cancelTask")).size(10))
|
||||||
.on_press(Message::CancelTask(snapshot.source, snapshot.id))
|
.on_press(Message::CancelTask(snapshot.source, snapshot.id))
|
||||||
.padding([3, 8])
|
.padding([3, 8])
|
||||||
.style(inputs::secondary_button),
|
.style(inputs::secondary_button),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
content.into()
|
|
||||||
|
let card: Element<'static, Message> = container(
|
||||||
|
iced::widget::Column::with_children(rows)
|
||||||
|
.spacing(6)
|
||||||
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(8)
|
||||||
|
.style(task_entry_style)
|
||||||
|
.into();
|
||||||
|
|
||||||
|
if is_group_child {
|
||||||
|
row![Space::with_width(Length::Fixed(16.0)), card]
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
card
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn task_status_color(status: &TaskStatus) -> Color {
|
||||||
|
match status {
|
||||||
|
TaskStatus::Running => Color::from_rgb8(0x73, 0xC9, 0x91),
|
||||||
|
TaskStatus::Pending => Color::from_rgb8(0xCC, 0xA7, 0x00),
|
||||||
|
TaskStatus::Failed(_) => Color::from_rgb8(0xF4, 0x87, 0x71),
|
||||||
|
TaskStatus::Completed | TaskStatus::Cancelled => Color::from_rgb8(0x9D, 0xA5, 0xB4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn task_entry_style(_theme: &Theme) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(Color::from_rgb8(0x25, 0x25, 0x26))),
|
||||||
|
border: Border {
|
||||||
|
color: Color::from_rgb8(0x3C, 0x3C, 0x3C),
|
||||||
|
width: 1.0,
|
||||||
|
radius: 4.0.into(),
|
||||||
|
},
|
||||||
|
..container::Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn task_progress_style(_theme: &Theme) -> progress_bar::Style {
|
||||||
|
progress_bar::Style {
|
||||||
|
background: Background::Color(Color::from_rgb8(0x3C, 0x3C, 0x3C)),
|
||||||
|
bar: Background::Color(Color::from_rgb8(0x0E, 0x63, 0x9C)),
|
||||||
|
border: Border {
|
||||||
|
radius: 3.0.into(),
|
||||||
|
..Border::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn task_group_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||||
|
let background = match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => Color::from_rgb8(0x2D, 0x2D, 0x30),
|
||||||
|
_ => Color::from_rgb8(0x25, 0x25, 0x26),
|
||||||
|
};
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(background)),
|
||||||
|
text_color: Color::from_rgb8(0xCC, 0xCC, 0xCC),
|
||||||
|
border: Border {
|
||||||
|
color: Color::from_rgb8(0x3C, 0x3C, 0x3C),
|
||||||
|
width: 1.0,
|
||||||
|
radius: 4.0.into(),
|
||||||
|
},
|
||||||
|
..button::Style::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn group_progress(members: &[&TaskSnapshot]) -> Option<f32> {
|
fn group_progress(members: &[&TaskSnapshot]) -> Option<f32> {
|
||||||
@@ -66,6 +170,28 @@ fn group_progress(members: &[&TaskSnapshot]) -> Option<f32> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn task_group_meta(members: &[&TaskSnapshot], locale: UiLocale) -> String {
|
||||||
|
let mut parts = vec![format!(
|
||||||
|
"{:.0}%",
|
||||||
|
group_progress(members).unwrap_or_default() * 100.0
|
||||||
|
)];
|
||||||
|
let running = members
|
||||||
|
.iter()
|
||||||
|
.filter(|member| matches!(member.status, TaskStatus::Running))
|
||||||
|
.count();
|
||||||
|
let pending = members
|
||||||
|
.iter()
|
||||||
|
.filter(|member| matches!(member.status, TaskStatus::Pending))
|
||||||
|
.count();
|
||||||
|
if running > 0 {
|
||||||
|
parts.push(format!("{running} {}", t(locale, "tasks.statusRunning")));
|
||||||
|
}
|
||||||
|
if pending > 0 {
|
||||||
|
parts.push(format!("{pending} {}", t(locale, "tasks.statusPending")));
|
||||||
|
}
|
||||||
|
parts.join(" · ")
|
||||||
|
}
|
||||||
|
|
||||||
/// Panel background style.
|
/// Panel background style.
|
||||||
fn panel_style(_theme: &Theme) -> container::Style {
|
fn panel_style(_theme: &Theme) -> container::Style {
|
||||||
container::Style {
|
container::Style {
|
||||||
@@ -232,7 +358,7 @@ pub fn view(
|
|||||||
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
||||||
for snapshot in &visible {
|
for snapshot in &visible {
|
||||||
let Some(group_id) = snapshot.group_id.as_ref() else {
|
let Some(group_id) = snapshot.group_id.as_ref() else {
|
||||||
items.push(task_row(snapshot, locale));
|
items.push(task_row(snapshot, locale, false));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !rendered_groups.insert(group_id.clone()) {
|
if !rendered_groups.insert(group_id.clone()) {
|
||||||
@@ -243,27 +369,37 @@ pub fn view(
|
|||||||
.copied()
|
.copied()
|
||||||
.filter(|member| member.group_id.as_ref() == Some(group_id))
|
.filter(|member| member.group_id.as_ref() == Some(group_id))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let progress = group_progress(&members)
|
|
||||||
.map(|progress| format!(" ({:.0}%)", progress * 100.0));
|
|
||||||
let collapsed = collapsed_task_groups.contains(group_id);
|
let collapsed = collapsed_task_groups.contains(group_id);
|
||||||
let group_name = snapshot.group_name.as_deref().unwrap_or(group_id);
|
let group_name = snapshot.group_name.as_deref().unwrap_or(group_id);
|
||||||
items.push(
|
items.push(
|
||||||
button(
|
button(
|
||||||
row![
|
row![
|
||||||
text(if collapsed { "\u{25b8}" } else { "\u{25be}" }).size(11),
|
text(if collapsed { "\u{25b8}" } else { "\u{25be}" }).size(11),
|
||||||
text(format!("{}{}", group_name, progress.unwrap_or_default()))
|
text(format!("{} ({})", group_name, members.len()))
|
||||||
.size(11)
|
.size(11)
|
||||||
|
.font(Font {
|
||||||
|
weight: iced::font::Weight::Semibold,
|
||||||
|
..Font::DEFAULT
|
||||||
|
}),
|
||||||
|
text(task_group_meta(&members, locale))
|
||||||
|
.size(10)
|
||||||
|
.color(Color::from_rgb8(0x9D, 0xA5, 0xB4)),
|
||||||
]
|
]
|
||||||
.spacing(6),
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center),
|
||||||
)
|
)
|
||||||
.on_press(Message::ToggleTaskGroup(group_id.clone()))
|
.on_press(Message::ToggleTaskGroup(group_id.clone()))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.padding([3, 6])
|
.padding(8)
|
||||||
.style(inputs::disclosure_button)
|
.style(task_group_button_style)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
if !collapsed {
|
if !collapsed {
|
||||||
items.extend(members.into_iter().map(|member| task_row(member, locale)));
|
items.extend(
|
||||||
|
members
|
||||||
|
.into_iter()
|
||||||
|
.map(|member| task_row(member, locale, true)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scrollable(
|
scrollable(
|
||||||
@@ -469,4 +605,63 @@ mod progress_tests {
|
|||||||
|
|
||||||
assert_eq!(group_progress(&[&complete, &pending]), Some(0.5));
|
assert_eq!(group_progress(&[&complete, &pending]), Some(0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn task_group_header_matches_bds2_progress_and_status_summary() {
|
||||||
|
let complete = TaskSnapshot {
|
||||||
|
id: 1,
|
||||||
|
source: crate::state::navigation::TaskSource::Local,
|
||||||
|
label: "Complete".into(),
|
||||||
|
group_id: Some("group".into()),
|
||||||
|
group_name: Some("Build".into()),
|
||||||
|
status: TaskStatus::Completed,
|
||||||
|
progress: Some(1.0),
|
||||||
|
message: None,
|
||||||
|
cancellation_requested: false,
|
||||||
|
is_cancellable: false,
|
||||||
|
};
|
||||||
|
let running = TaskSnapshot {
|
||||||
|
id: 2,
|
||||||
|
label: "Running".into(),
|
||||||
|
status: TaskStatus::Running,
|
||||||
|
progress: Some(0.25),
|
||||||
|
is_cancellable: true,
|
||||||
|
..complete.clone()
|
||||||
|
};
|
||||||
|
let pending = TaskSnapshot {
|
||||||
|
id: 3,
|
||||||
|
label: "Pending".into(),
|
||||||
|
status: TaskStatus::Pending,
|
||||||
|
progress: None,
|
||||||
|
is_cancellable: true,
|
||||||
|
..complete.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
task_group_meta(&[&complete, &running, &pending], UiLocale::En),
|
||||||
|
"42% · 1 Running · 1 Pending"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn task_cards_use_bds2_surface_and_status_palette() {
|
||||||
|
let style = task_entry_style(&Theme::Dark);
|
||||||
|
assert_eq!(
|
||||||
|
style.background,
|
||||||
|
Some(Background::Color(Color::from_rgb8(0x25, 0x25, 0x26)))
|
||||||
|
);
|
||||||
|
assert_eq!(style.border.color, Color::from_rgb8(0x3C, 0x3C, 0x3C));
|
||||||
|
assert_eq!(style.border.width, 1.0);
|
||||||
|
let group_style = task_group_button_style(&Theme::Dark, button::Status::Active);
|
||||||
|
assert_eq!(group_style.background, style.background);
|
||||||
|
assert_eq!(group_style.border, style.border);
|
||||||
|
assert_eq!(
|
||||||
|
task_status_color(&TaskStatus::Running),
|
||||||
|
Color::from_rgb8(0x73, 0xC9, 0x91)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
task_status_color(&TaskStatus::Pending),
|
||||||
|
Color::from_rgb8(0xCC, 0xA7, 0x00)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user