Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/dashboard/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod gesture_cheat_sheet;
mod gesture_health;
mod gesture_recent;
mod layouts;
mod note_list_shared;
mod notes_graph;
mod notes_recent;
mod notes_tags;
Expand Down Expand Up @@ -361,8 +362,8 @@ impl WidgetRegistry {
);
reg.register(
"notes_recent",
WidgetFactory::new(NotesRecentWidget::new)
.with_settings_ui(NotesRecentWidget::settings_ui),
WidgetFactory::new(RecentNotesWidget::new_legacy)
.with_settings_ui(RecentNotesWidget::settings_ui),
);
reg.register(
"notes_tags",
Expand Down
136 changes: 136 additions & 0 deletions src/dashboard/widgets/note_list_shared.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use crate::dashboard::dashboard::DashboardContext;
use crate::plugins::note::Note;
use eframe::egui;
use std::time::SystemTime;

#[derive(Clone)]
pub struct CachedNoteEntry {
pub title: String,
pub slug: String,
pub tags: Vec<String>,
pub snippet: String,
}

#[derive(Default)]
pub struct CachedRecentNotes {
pub entries: Vec<CachedNoteEntry>,
pub last_notes_version: u64,
}

impl CachedRecentNotes {
pub fn new() -> Self {
Self {
entries: Vec::new(),
last_notes_version: u64::MAX,
}
}

pub fn refresh(&mut self, ctx: &DashboardContext<'_>, count: usize, filter_tag: Option<&str>) {
if self.last_notes_version == ctx.notes_version {
return;
}

let snapshot = ctx.data_cache.snapshot();
let mut notes: Vec<Note> = snapshot.notes.as_ref().clone();
if let Some(tag) = filter_tag.filter(|tag| !tag.trim().is_empty()) {
notes.retain(|note| note.tags.iter().any(|t| t.eq_ignore_ascii_case(tag)));
}

notes.sort_by(|a, b| modified_ts(b).cmp(&modified_ts(a)));
notes.truncate(count);

self.entries = notes
.iter()
.map(|note| CachedNoteEntry {
title: note.alias.as_ref().unwrap_or(&note.title).clone(),
slug: note.slug.clone(),
tags: note.tags.clone(),
snippet: note_snippet(note),
})
.collect();
self.last_notes_version = ctx.notes_version;
}
}

pub fn note_snippet(note: &Note) -> String {
let first_line = note
.content
.lines()
.skip_while(|line| line.starts_with("# ") || line.starts_with("Alias:"))
.find(|line| !line.trim().is_empty())
.unwrap_or_default();
let clean = first_line.trim();
if clean.len() > 120 {
format!("{}…", &clean[..120])
} else {
clean.to_string()
}
}

pub fn render_note_rows(
ui: &mut egui::Ui,
scroll_id: impl std::hash::Hash,
entries: &[CachedNoteEntry],
show_snippet: bool,
show_tags: bool,
no_notes_message: &str,
mut build_action: impl FnMut(&CachedNoteEntry) -> super::WidgetAction,
) -> Option<super::WidgetAction> {
if entries.is_empty() {
ui.label(no_notes_message);
return None;
}

let body_height = ui.text_style_height(&egui::TextStyle::Body);
let small_height = ui.text_style_height(&egui::TextStyle::Small);
let mut row_height = body_height + ui.spacing().item_spacing.y + 8.0;
if show_snippet {
row_height += small_height + 2.0;
}
if show_tags {
row_height += small_height + 2.0;
}

let mut clicked = None;
egui::ScrollArea::both()
.id_source(ui.id().with(scroll_id))
.auto_shrink([false; 2])
.show_rows(ui, row_height, entries.len(), |ui, range| {
for note in &entries[range] {
let mut clicked_row = false;
ui.vertical(|ui| {
clicked_row |= ui.add(egui::Button::new(&note.title).wrap(false)).clicked();
if show_snippet {
ui.add(
egui::Label::new(egui::RichText::new(&note.snippet).small())
.wrap(false),
);
}
if show_tags && !note.tags.is_empty() {
ui.add(
egui::Label::new(
egui::RichText::new(format!("#{}", note.tags.join(" #"))).small(),
)
.wrap(false),
);
}
ui.add_space(4.0);
});
if clicked_row {
clicked = Some(build_action(note));
}
}
});

clicked
}

fn modified_ts(note: &Note) -> u64 {
note.path
.metadata()
.and_then(|meta| meta.modified())
.ok()
.and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|duration| duration.as_secs())
.unwrap_or(0)
}
178 changes: 9 additions & 169 deletions src/dashboard/widgets/notes_recent.rs
Original file line number Diff line number Diff line change
@@ -1,61 +1,18 @@
use super::{
edit_typed_settings, Widget, WidgetAction, WidgetSettingsContext, WidgetSettingsUiResult,
};
use crate::actions::Action;
use super::{Widget, WidgetAction, WidgetSettingsContext, WidgetSettingsUiResult};
use crate::dashboard::dashboard::{DashboardContext, WidgetActivation};
use crate::plugins::note::Note;
use eframe::egui;
use serde::{Deserialize, Serialize};
use std::time::SystemTime;

fn default_count() -> usize {
5
}

fn default_true() -> bool {
true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotesRecentConfig {
#[serde(default = "default_count")]
pub count: usize,
#[serde(default = "default_true")]
pub show_snippet: bool,
#[serde(default = "default_true")]
pub show_tags: bool,
}

impl Default for NotesRecentConfig {
fn default() -> Self {
Self {
count: default_count(),
show_snippet: true,
show_tags: true,
}
}
}

#[derive(Clone)]
struct NoteSummary {
title: String,
slug: String,
tags: Vec<String>,
snippet: String,
}
pub use super::recent_notes::RecentNotesConfig as NotesRecentConfig;
use super::recent_notes::RecentNotesWidget;

pub struct NotesRecentWidget {
cfg: NotesRecentConfig,
cached: Vec<NoteSummary>,
last_notes_version: u64,
inner: RecentNotesWidget,
}

impl NotesRecentWidget {
pub fn new(cfg: NotesRecentConfig) -> Self {
Self {
cfg,
cached: Vec::new(),
last_notes_version: u64::MAX,
inner: RecentNotesWidget::new_legacy(cfg),
}
}

Expand All @@ -64,64 +21,7 @@ impl NotesRecentWidget {
value: &mut serde_json::Value,
ctx: &WidgetSettingsContext<'_>,
) -> WidgetSettingsUiResult {
edit_typed_settings(ui, value, ctx, |ui, cfg: &mut NotesRecentConfig, _ctx| {
let mut changed = false;
ui.horizontal(|ui| {
ui.label("Show");
changed |= ui
.add(egui::DragValue::new(&mut cfg.count).clamp_range(1..=50))
.changed();
ui.label("notes");
});
changed |= ui.checkbox(&mut cfg.show_snippet, "Show snippet").changed();
changed |= ui.checkbox(&mut cfg.show_tags, "Show tags").changed();
changed
})
}

fn modified_ts(note: &Note) -> u64 {
note.path
.metadata()
.and_then(|m| m.modified())
.ok()
.and_then(|m| m.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0)
}

fn snippet(note: &Note) -> String {
let first_line = note
.content
.lines()
.skip_while(|l| l.starts_with("# ") || l.starts_with("Alias:"))
.find(|l| !l.trim().is_empty())
.unwrap_or_default();
let clean = first_line.trim();
if clean.len() > 120 {
format!("{}…", &clean[..120])
} else {
clean.to_string()
}
}

fn refresh_cache(&mut self, ctx: &DashboardContext<'_>) {
if self.last_notes_version == ctx.notes_version {
return;
}
let snapshot = ctx.data_cache.snapshot();
let mut notes: Vec<Note> = snapshot.notes.as_ref().clone();
notes.sort_by(|a, b| Self::modified_ts(b).cmp(&Self::modified_ts(a)));
notes.truncate(self.cfg.count);
self.cached = notes
.iter()
.map(|note| NoteSummary {
title: note.alias.as_ref().unwrap_or(&note.title).clone(),
slug: note.slug.clone(),
tags: note.tags.clone(),
snippet: Self::snippet(note),
})
.collect();
self.last_notes_version = ctx.notes_version;
RecentNotesWidget::settings_ui(ui, value, ctx)
}
}

Expand All @@ -136,72 +36,12 @@ impl Widget for NotesRecentWidget {
&mut self,
ui: &mut egui::Ui,
ctx: &DashboardContext<'_>,
_activation: WidgetActivation,
activation: WidgetActivation,
) -> Option<WidgetAction> {
self.refresh_cache(ctx);

if self.cached.is_empty() {
ui.label("No notes found.");
return None;
}

let mut clicked = None;
let body_height = ui.text_style_height(&egui::TextStyle::Body);
let small_height = ui.text_style_height(&egui::TextStyle::Small);
let mut row_height = body_height + ui.spacing().item_spacing.y + 8.0;
if self.cfg.show_snippet {
row_height += small_height + 2.0;
}
if self.cfg.show_tags {
row_height += small_height + 2.0;
}
let scroll_id = ui.id().with("notes_recent_scroll");
egui::ScrollArea::both()
.id_source(scroll_id)
.auto_shrink([false; 2])
.show_rows(ui, row_height, self.cached.len(), |ui, range| {
for note in &self.cached[range] {
let mut clicked_row = false;
ui.vertical(|ui| {
clicked_row |= ui.add(egui::Button::new(&note.title).wrap(false)).clicked();
if self.cfg.show_snippet {
ui.add(
egui::Label::new(egui::RichText::new(&note.snippet).small())
.wrap(false),
);
}
if self.cfg.show_tags && !note.tags.is_empty() {
ui.add(
egui::Label::new(
egui::RichText::new(format!("#{}", note.tags.join(" #")))
.small(),
)
.wrap(false),
);
}
ui.add_space(4.0);
});
if clicked_row {
clicked = Some(WidgetAction {
action: Action {
label: note.title.clone(),
desc: "Note".into(),
action: format!("note:open:{}", note.slug),
args: None,
},
query_override: Some(format!("note open {}", note.slug)),
});
}
}
});

clicked
self.inner.render(ui, ctx, activation)
}

fn on_config_updated(&mut self, settings: &serde_json::Value) {
if let Ok(cfg) = serde_json::from_value::<NotesRecentConfig>(settings.clone()) {
self.cfg = cfg;
self.last_notes_version = u64::MAX;
}
self.inner.on_config_updated(settings);
}
}
Loading