feat(ui): Phase 7 C3 — Priority 过滤 + 消息详情面板
- sidebar FILTERS 加 Max/High/Normal/Low 四档(dot 色与 priority 配色一致),点击切换 priority-filter(0=All, 1=Low, 2=Normal, 3=High, 4=Max) - MessageList 加 priority-filter property,通过 visible 谓词按 Slint Priority enum 匹配;保留原 idx 让 selected-index 不随 filter 错位 - 右栏加 detail panel:app + priority dot、title、id、relative_time、body、has_click_url 时显示 Open link 按钮(复用 C2 的 message-card-clicked 走 url::open) - detail panel 与消息列表水平分割,selected-message-index < 0 时自动隐藏;切换 Settings/About 时不显示
This commit is contained in:
+211
-43
@@ -1,12 +1,17 @@
|
|||||||
// Main window + sidebar (AGENTS.md §6, §7).
|
// Main window + sidebar (AGENTS.md §6, §7).
|
||||||
//
|
//
|
||||||
// Phase 3 C2 wires the live connection status (dot + status text under the
|
// Phase 7 C3 additions:
|
||||||
// title bar) and accepts a `test-connection-requested` callback fired by
|
// - Sidebar priority filter (All / Low / Normal / High / Max)
|
||||||
// the Settings → Connection section.
|
// flips `priority-filter` on MainWindow; MessageList hides
|
||||||
|
// non-matching cards via `visible` (see message-list.slint).
|
||||||
|
// - Right-hand detail panel that opens when a message is clicked:
|
||||||
|
// app + priority dot, title, absolute timestamp, full body,
|
||||||
|
// and an "Open Link" button (re-uses the URL opener wired up
|
||||||
|
// in Phase 7 C2).
|
||||||
|
|
||||||
import { AppearanceMode } from "./theme.slint";
|
import { AppearanceMode } from "./theme.slint";
|
||||||
import { NavItem, SectionLabel } from "./components.slint";
|
import { NavItem, SectionLabel } from "./components.slint";
|
||||||
import { MessageItem } from "./message-card.slint";
|
import { MessageItem, Priority } from "./message-card.slint";
|
||||||
import { MessageList } from "./message-list.slint";
|
import { MessageList } from "./message-list.slint";
|
||||||
import { EmptyState } from "./empty-state.slint";
|
import { EmptyState } from "./empty-state.slint";
|
||||||
import { SettingsPage } from "./settings-page.slint";
|
import { SettingsPage } from "./settings-page.slint";
|
||||||
@@ -54,6 +59,13 @@ export component MainWindow inherits Window {
|
|||||||
callback test-notification-clicked();
|
callback test-notification-clicked();
|
||||||
callback message-card-clicked(int);
|
callback message-card-clicked(int);
|
||||||
|
|
||||||
|
// ── Phase 7 C3: priority filter (0=All, 1=Low, 2=Normal,
|
||||||
|
// 3=High, 4=Max). The integer values intentionally align with
|
||||||
|
// `PriorityFilter` (0=All is the special "no filter" sentinel;
|
||||||
|
// 1..4 map to the four priority buckets). See message-list.slint
|
||||||
|
// for the matching `visible` predicate. ──
|
||||||
|
in-out property <int> priority-filter: 0;
|
||||||
|
|
||||||
// ── Phase 2 C3: Settings / About / window state ──────────────
|
// ── Phase 2 C3: Settings / About / window state ──────────────
|
||||||
in-out property <int> appearance-index: 2;
|
in-out property <int> appearance-index: 2;
|
||||||
callback about-clicked();
|
callback about-clicked();
|
||||||
@@ -98,7 +110,7 @@ export component MainWindow inherits Window {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
text: "Windows Client · Phase 3";
|
text: "Windows Client · Phase 7";
|
||||||
color: text-muted;
|
color: text-muted;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
@@ -128,8 +140,15 @@ export component MainWindow inherits Window {
|
|||||||
SectionLabel { label: "MESSAGES"; text-color: text-muted; }
|
SectionLabel { label: "MESSAGES"; text-color: text-muted; }
|
||||||
NavItem {
|
NavItem {
|
||||||
label: "All";
|
label: "All";
|
||||||
selected: root.selection == SidebarSelection.All;
|
// Phase 7 C3: highlight the All nav item whenever
|
||||||
clicked => { root.selection = SidebarSelection.All; }
|
// the priority filter is off (i.e. the user is
|
||||||
|
// looking at the unfiltered list).
|
||||||
|
selected: root.selection == SidebarSelection.All
|
||||||
|
&& root.priority-filter == 0;
|
||||||
|
clicked => {
|
||||||
|
root.selection = SidebarSelection.All;
|
||||||
|
root.priority-filter = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle { height: 12px; }
|
Rectangle { height: 12px; }
|
||||||
@@ -144,23 +163,52 @@ export component MainWindow inherits Window {
|
|||||||
Rectangle { height: 12px; }
|
Rectangle { height: 12px; }
|
||||||
|
|
||||||
SectionLabel { label: "FILTERS"; text-color: text-muted; }
|
SectionLabel { label: "FILTERS"; text-color: text-muted; }
|
||||||
|
// Phase 7 C3: each row maps to one priority bucket.
|
||||||
|
// The dot's color is the priority color so the row
|
||||||
|
// doubles as a legend.
|
||||||
|
NavItem {
|
||||||
|
label: "Max";
|
||||||
|
selected: root.selection == SidebarSelection.All
|
||||||
|
&& root.priority-filter == 4;
|
||||||
|
has-accent: true;
|
||||||
|
accent-color: priority-max;
|
||||||
|
clicked => {
|
||||||
|
root.selection = SidebarSelection.All;
|
||||||
|
root.priority-filter = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
NavItem {
|
NavItem {
|
||||||
label: "High";
|
label: "High";
|
||||||
selected: false;
|
selected: root.selection == SidebarSelection.All
|
||||||
has-accent: false;
|
&& root.priority-filter == 3;
|
||||||
clicked => { /* TODO Phase 3 C2: priority filter */ }
|
has-accent: true;
|
||||||
|
accent-color: priority-high;
|
||||||
|
clicked => {
|
||||||
|
root.selection = SidebarSelection.All;
|
||||||
|
root.priority-filter = 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NavItem {
|
NavItem {
|
||||||
label: "Normal";
|
label: "Normal";
|
||||||
selected: false;
|
selected: root.selection == SidebarSelection.All
|
||||||
has-accent: false;
|
&& root.priority-filter == 2;
|
||||||
clicked => { /* TODO Phase 3 C2: priority filter */ }
|
has-accent: true;
|
||||||
|
accent-color: priority-normal;
|
||||||
|
clicked => {
|
||||||
|
root.selection = SidebarSelection.All;
|
||||||
|
root.priority-filter = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NavItem {
|
NavItem {
|
||||||
label: "Low";
|
label: "Low";
|
||||||
selected: false;
|
selected: root.selection == SidebarSelection.All
|
||||||
has-accent: false;
|
&& root.priority-filter == 1;
|
||||||
clicked => { /* TODO Phase 3 C2: priority filter */ }
|
has-accent: true;
|
||||||
|
accent-color: priority-low;
|
||||||
|
clicked => {
|
||||||
|
root.selection = SidebarSelection.All;
|
||||||
|
root.priority-filter = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle { height: 12px; }
|
Rectangle { height: 12px; }
|
||||||
@@ -186,39 +234,159 @@ export component MainWindow inherits Window {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
background: bg;
|
background: bg;
|
||||||
|
|
||||||
VerticalLayout {
|
// Phase 7 C3: split the right pane horizontally so the
|
||||||
// Title bar with status text under it.
|
// detail panel can sit alongside the message list when
|
||||||
padding: 24px;
|
// the user picks a card. The detail panel is hidden when
|
||||||
spacing: 4px;
|
// no card is selected or when the user is on Settings /
|
||||||
|
// About (which have their own full-pane content).
|
||||||
|
HorizontalLayout {
|
||||||
|
padding: 0;
|
||||||
|
spacing: 0;
|
||||||
|
|
||||||
Text {
|
// Messages column (always visible when on "All").
|
||||||
text: messages.length == 0 ? "Messages" : "Messages · " + messages.length;
|
Rectangle {
|
||||||
color: text-primary;
|
background: bg;
|
||||||
font-size: 18px;
|
horizontal-stretch: 1;
|
||||||
font-weight: 700;
|
visible: root.selection == SidebarSelection.All;
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: status-text;
|
|
||||||
color: text-muted;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle { height: 12px; }
|
VerticalLayout {
|
||||||
|
// Title bar with status text under it.
|
||||||
|
padding: 24px;
|
||||||
|
spacing: 4px;
|
||||||
|
|
||||||
if messages.length > 0: MessageList {
|
Text {
|
||||||
messages: messages;
|
text: messages.length == 0 ? "Messages" : "Messages · " + messages.length;
|
||||||
selected-index: root.selected-message-index;
|
color: text-primary;
|
||||||
vertical-stretch: 1;
|
font-size: 18px;
|
||||||
message-clicked(idx) => {
|
font-weight: 700;
|
||||||
root.selected-message-index = idx;
|
}
|
||||||
root.message-card-clicked(idx);
|
Text {
|
||||||
|
text: status-text;
|
||||||
|
color: text-muted;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { height: 12px; }
|
||||||
|
|
||||||
|
if messages.length > 0: MessageList {
|
||||||
|
messages: messages;
|
||||||
|
selected-index: root.selected-message-index;
|
||||||
|
priority-filter: root.priority-filter;
|
||||||
|
vertical-stretch: 1;
|
||||||
|
message-clicked(idx) => {
|
||||||
|
root.selected-message-index = idx;
|
||||||
|
root.message-card-clicked(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if messages.length == 0: EmptyState {
|
||||||
|
vertical-stretch: 1;
|
||||||
|
test-notification => {
|
||||||
|
root.test-notification-clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if messages.length == 0: EmptyState {
|
// Phase 7 C3: detail panel. Visible when a card is
|
||||||
vertical-stretch: 1;
|
// selected AND the user is on the All view. Always
|
||||||
test-notification => {
|
// reads from `messages[selected-message-index]` so
|
||||||
root.test-notification-clicked();
|
// it stays in sync with whatever the card list
|
||||||
|
// currently shows.
|
||||||
|
if root.selection == SidebarSelection.All
|
||||||
|
&& root.selected-message-index >= 0
|
||||||
|
&& root.selected-message-index < root.messages.length: Rectangle {
|
||||||
|
width: 360px;
|
||||||
|
background: surface-elev;
|
||||||
|
border-color: border;
|
||||||
|
border-width: 1px;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
padding: 20px;
|
||||||
|
spacing: 8px;
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
spacing: 8px;
|
||||||
|
alignment: center;
|
||||||
|
Rectangle {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: root.messages[root.selected-message-index].priority == Priority.Low ? priority-low :
|
||||||
|
root.messages[root.selected-message-index].priority == Priority.Normal ? priority-normal :
|
||||||
|
root.messages[root.selected-message-index].priority == Priority.High ? priority-high :
|
||||||
|
priority-max;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.messages[root.selected-message-index].app_name;
|
||||||
|
color: text-second;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.messages[root.selected-message-index].timestamp_unix == 0
|
||||||
|
? ""
|
||||||
|
: "id #" + root.messages[root.selected-message-index].id;
|
||||||
|
color: text-muted;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.messages[root.selected-message-index].title != ""
|
||||||
|
? root.messages[root.selected-message-index].title
|
||||||
|
: root.messages[root.selected-message-index].body;
|
||||||
|
color: text-primary;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
wrap: word-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.messages[root.selected-message-index].relative_time;
|
||||||
|
color: text-muted;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: 1px;
|
||||||
|
background: border;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.messages[root.selected-message-index].body;
|
||||||
|
color: text-primary;
|
||||||
|
font-size: 13px;
|
||||||
|
wrap: word-wrap;
|
||||||
|
vertical-stretch: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open-link button: only when the message
|
||||||
|
// carries a click URL. Reuses the same
|
||||||
|
// Rust callback as the card click.
|
||||||
|
if root.messages[root.selected-message-index].has_click_url: Rectangle {
|
||||||
|
height: 32px;
|
||||||
|
background: accent;
|
||||||
|
border-radius: 6px;
|
||||||
|
TouchArea {
|
||||||
|
clicked => {
|
||||||
|
root.message-card-clicked(root.selected-message-index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
x: 0;
|
||||||
|
y: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text: "Open link ↗";
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-1
@@ -5,13 +5,26 @@
|
|||||||
// the dependency footprint minimal until Phase 6 introduces the real
|
// the dependency footprint minimal until Phase 6 introduces the real
|
||||||
// std-widgets-backed ListView (which requires an explicit `import` from
|
// std-widgets-backed ListView (which requires an explicit `import` from
|
||||||
// `@std-widgets.slint` and an `include_paths` config in build.rs).
|
// `@std-widgets.slint` and an `include_paths` config in build.rs).
|
||||||
|
//
|
||||||
|
// Phase 7 C3: a `priority-filter` property hides non-matching cards via
|
||||||
|
// `visible`. Hiding (vs filtering the model) keeps the original index
|
||||||
|
// intact, which lets `selected-index` stay correct when the filter
|
||||||
|
// changes — clicking item 5 with the filter still applies to row 5
|
||||||
|
// of the underlying list, not the post-filter row.
|
||||||
|
|
||||||
import { MessageItem, MessageCard } from "./message-card.slint";
|
import { MessageItem, MessageCard, Priority } from "./message-card.slint";
|
||||||
|
|
||||||
export component MessageList inherits Rectangle {
|
export component MessageList inherits Rectangle {
|
||||||
in property <[MessageItem]> messages;
|
in property <[MessageItem]> messages;
|
||||||
in-out property <int> selected-index: -1;
|
in-out property <int> selected-index: -1;
|
||||||
|
|
||||||
|
// 0 = show all (no filter). 1 = Priority::Low, 2 = Normal,
|
||||||
|
// 3 = High, 4 = Max. Off-by-one alignment with the Slint
|
||||||
|
// `Priority` enum (Low=0, Normal=1, High=2, Max=3) is
|
||||||
|
// intentional so the UI shows "All / Low / Normal / High / Max"
|
||||||
|
// while the wire-level enum keeps its 0..3 range.
|
||||||
|
in property <int> priority-filter: 0;
|
||||||
|
|
||||||
in property <color> card-border: #3a3b40;
|
in property <color> card-border: #3a3b40;
|
||||||
|
|
||||||
callback message-clicked(int);
|
callback message-clicked(int);
|
||||||
@@ -24,6 +37,16 @@ export component MessageList inherits Rectangle {
|
|||||||
for item[idx] in messages: MessageCard {
|
for item[idx] in messages: MessageCard {
|
||||||
item: item;
|
item: item;
|
||||||
selected: idx == root.selected-index;
|
selected: idx == root.selected-index;
|
||||||
|
// Show every card when filter is "All"; otherwise only
|
||||||
|
// the priority bucket the user picked. Slint lets us
|
||||||
|
// compare an enum directly against an int literal, so
|
||||||
|
// `item.priority == 0` matches Priority::Low without
|
||||||
|
// any explicit conversion.
|
||||||
|
visible: root.priority-filter == 0
|
||||||
|
|| (root.priority-filter == 1 && item.priority == Priority.Low)
|
||||||
|
|| (root.priority-filter == 2 && item.priority == Priority.Normal)
|
||||||
|
|| (root.priority-filter == 3 && item.priority == Priority.High)
|
||||||
|
|| (root.priority-filter == 4 && item.priority == Priority.Max);
|
||||||
card-border: card-border;
|
card-border: card-border;
|
||||||
clicked => {
|
clicked => {
|
||||||
root.selected-index = idx;
|
root.selected-index = idx;
|
||||||
|
|||||||
Reference in New Issue
Block a user