mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 02:12:00 +03:00
add search across all sheets and always-on-top toggle
Ctrl+F opens an overlay that searches every document's lines, not just the active one, and jumps to the hit on click. Always-on-top is a plain window pin, off by default, toggleable in settings.
This commit is contained in:
parent
6efae0c706
commit
9aca062add
7 changed files with 178 additions and 0 deletions
10
index.html
10
index.html
|
|
@ -16,6 +16,7 @@
|
|||
<button id="toggle-sidebar" title="Documents (Ctrl+B)">☰</button>
|
||||
<input id="doc-title" spellcheck="false" autocomplete="off" />
|
||||
<span class="spacer" data-tauri-drag-region></span>
|
||||
<button id="open-search" title="Search all sheets (Ctrl+F)">🔍</button>
|
||||
<button id="open-settings" title="Settings">⚙</button>
|
||||
<div id="win-controls">
|
||||
<button id="win-min" title="Minimize">─</button>
|
||||
|
|
@ -79,6 +80,9 @@
|
|||
<label class="row"><input id="set-autostart" type="checkbox" />
|
||||
<span data-i18n="autostart">Launch at startup</span>
|
||||
</label>
|
||||
<label class="row"><input id="set-alwaysontop" type="checkbox" />
|
||||
<span data-i18n="alwaysontop">Always on top</span>
|
||||
</label>
|
||||
<label><span data-i18n="fontsize">Font size</span>
|
||||
<input id="set-fontsize" type="number" min="10" max="32" />
|
||||
</label>
|
||||
|
|
@ -107,6 +111,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="search-overlay" class="hidden">
|
||||
<div class="search-box">
|
||||
<input id="search-input" type="text" autocomplete="off" spellcheck="false" />
|
||||
<div id="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-always-on-top",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-unregister",
|
||||
"global-shortcut:allow-is-registered",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const dict: Record<string, Record<string, string>> = {
|
|||
language: "Language",
|
||||
hotkey: "Global hotkey",
|
||||
autostart: "Launch at startup",
|
||||
alwaysontop: "Always on top",
|
||||
fontsize: "Font size",
|
||||
extensions: "Extensions folder",
|
||||
done: "Done",
|
||||
|
|
@ -52,6 +53,8 @@ const dict: Record<string, Record<string, string>> = {
|
|||
updateLater: "Later",
|
||||
updateInstalling: "Installing update…",
|
||||
updateFailed: "Update failed",
|
||||
searchPlaceholder: "Search all sheets…",
|
||||
searchEmpty: "No matches",
|
||||
},
|
||||
ru: {
|
||||
settings: "Настройки",
|
||||
|
|
@ -64,6 +67,7 @@ const dict: Record<string, Record<string, string>> = {
|
|||
language: "Язык",
|
||||
hotkey: "Глобальный хоткей",
|
||||
autostart: "Запуск при старте Windows",
|
||||
alwaysontop: "Поверх всех окон",
|
||||
fontsize: "Размер шрифта",
|
||||
extensions: "Папка расширений",
|
||||
done: "Готово",
|
||||
|
|
@ -104,6 +108,8 @@ const dict: Record<string, Record<string, string>> = {
|
|||
updateLater: "Позже",
|
||||
updateInstalling: "Установка обновления…",
|
||||
updateFailed: "Не удалось обновить",
|
||||
searchPlaceholder: "Поиск по всем листам…",
|
||||
searchEmpty: "Ничего не найдено",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
103
src/main.ts
103
src/main.ts
|
|
@ -111,6 +111,74 @@ function newDoc(content = ""): void {
|
|||
switchDoc(id);
|
||||
}
|
||||
|
||||
// ---------- search all sheets
|
||||
|
||||
interface SearchHit {
|
||||
docId: string;
|
||||
docTitle: string;
|
||||
line: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function searchAllSheets(query: string): SearchHit[] {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
const hits: SearchHit[] = [];
|
||||
for (const doc of data.docs) {
|
||||
const lines = (data.contents[doc.id] ?? "").split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].toLowerCase().includes(q)) {
|
||||
hits.push({ docId: doc.id, docTitle: doc.title, line: i + 1, text: lines[i] });
|
||||
if (hits.length >= 200) return hits;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
function renderSearchResults(query: string, hits: SearchHit[]): void {
|
||||
const el = $("#search-results");
|
||||
el.replaceChildren();
|
||||
if (query.trim() && hits.length === 0) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "search-empty";
|
||||
empty.textContent = t("searchEmpty");
|
||||
el.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const hit of hits) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "search-item";
|
||||
const docEl = document.createElement("div");
|
||||
docEl.className = "doc";
|
||||
docEl.textContent = hit.docTitle;
|
||||
const lineEl = document.createElement("div");
|
||||
lineEl.className = "line";
|
||||
lineEl.textContent = hit.text;
|
||||
item.append(docEl, lineEl);
|
||||
item.addEventListener("click", () => {
|
||||
if (hit.docId !== data.activeId) switchDoc(hit.docId);
|
||||
editor.goToLine(hit.line);
|
||||
closeSearch();
|
||||
});
|
||||
frag.appendChild(item);
|
||||
}
|
||||
el.appendChild(frag);
|
||||
}
|
||||
|
||||
function openSearch(): void {
|
||||
const input = $<HTMLInputElement>("#search-input");
|
||||
$("#search-overlay").classList.remove("hidden");
|
||||
input.value = "";
|
||||
renderSearchResults("", []);
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function closeSearch(): void {
|
||||
$("#search-overlay").classList.add("hidden");
|
||||
}
|
||||
|
||||
// ---------- settings
|
||||
|
||||
function applySettings(): void {
|
||||
|
|
@ -121,6 +189,7 @@ function applySettings(): void {
|
|||
document.documentElement.style.setProperty("--editor-font-size", `${settings.fontSize}px`);
|
||||
$("#editor-wrap").style.setProperty("--results-width", `${settings.resultsWidth}%`);
|
||||
setLang(settings.language);
|
||||
$<HTMLInputElement>("#search-input").placeholder = t("searchPlaceholder");
|
||||
engine.updateSettings({
|
||||
precision: settings.precision,
|
||||
groupSeparator: settings.groupSeparator,
|
||||
|
|
@ -139,6 +208,7 @@ function bindSettingsUI(): void {
|
|||
const langSel = $<HTMLSelectElement>("#set-lang");
|
||||
const hotkey = $<HTMLInputElement>("#set-hotkey");
|
||||
const autostart = $<HTMLInputElement>("#set-autostart");
|
||||
const alwaysOnTop = $<HTMLInputElement>("#set-alwaysontop");
|
||||
const fontSize = $<HTMLInputElement>("#set-fontsize");
|
||||
const resultsWidth = $<HTMLInputElement>("#set-resultswidth");
|
||||
|
||||
|
|
@ -149,6 +219,7 @@ function bindSettingsUI(): void {
|
|||
langSel.value = settings.language;
|
||||
hotkey.value = settings.hotkey;
|
||||
autostart.checked = settings.autostart;
|
||||
alwaysOnTop.checked = settings.alwaysOnTop;
|
||||
fontSize.value = String(settings.fontSize);
|
||||
resultsWidth.value = String(settings.resultsWidth);
|
||||
|
||||
|
|
@ -220,6 +291,11 @@ function bindSettingsUI(): void {
|
|||
await applyAutostart(settings.autostart);
|
||||
void saveSettings(settings);
|
||||
});
|
||||
alwaysOnTop.addEventListener("change", async () => {
|
||||
settings.alwaysOnTop = alwaysOnTop.checked;
|
||||
await applyAlwaysOnTop(settings.alwaysOnTop);
|
||||
void saveSettings(settings);
|
||||
});
|
||||
|
||||
const binDays = $<HTMLInputElement>("#set-bindays");
|
||||
binDays.value = String(settings.deletedRetentionDays);
|
||||
|
|
@ -339,6 +415,12 @@ async function applyAutostart(enabled: boolean): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function applyAlwaysOnTop(enabled: boolean): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
const { getCurrentWindow } = await import("@tauri-apps/api/window");
|
||||
await getCurrentWindow().setAlwaysOnTop(enabled).catch((e) => console.warn("always-on-top failed", e));
|
||||
}
|
||||
|
||||
async function refreshRates(force = false): Promise<void> {
|
||||
const info = $("#rates-info");
|
||||
info.classList.add("spin");
|
||||
|
|
@ -526,6 +608,22 @@ async function boot(): Promise<void> {
|
|||
}
|
||||
});
|
||||
|
||||
$("#open-search").addEventListener("click", () => openSearch());
|
||||
const searchInput = $<HTMLInputElement>("#search-input");
|
||||
searchInput.addEventListener("input", () => {
|
||||
renderSearchResults(searchInput.value, searchAllSheets(searchInput.value));
|
||||
});
|
||||
searchInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
closeSearch();
|
||||
} else if (e.key === "Enter") {
|
||||
$("#search-results").querySelector<HTMLElement>(".search-item")?.click();
|
||||
}
|
||||
});
|
||||
$("#search-overlay").addEventListener("mousedown", (e) => {
|
||||
if (e.target === $("#search-overlay")) closeSearch();
|
||||
});
|
||||
|
||||
if (isTauri()) {
|
||||
const { getCurrentWindow } = await import("@tauri-apps/api/window");
|
||||
const win = getCurrentWindow();
|
||||
|
|
@ -587,6 +685,10 @@ async function boot(): Promise<void> {
|
|||
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === "b") {
|
||||
e.preventDefault();
|
||||
$("#toggle-sidebar").click();
|
||||
}
|
||||
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === "f") {
|
||||
e.preventDefault();
|
||||
openSearch();
|
||||
}
|
||||
if (e.ctrlKey && e.shiftKey && e.code === "KeyC") {
|
||||
e.preventDefault();
|
||||
|
|
@ -637,6 +739,7 @@ async function boot(): Promise<void> {
|
|||
|
||||
await registerHotkey(null, settings.hotkey);
|
||||
if (settings.autostart) await applyAutostart(true);
|
||||
if (settings.alwaysOnTop) await applyAlwaysOnTop(true);
|
||||
void onAppQuit(() => flushAppData());
|
||||
void refreshRates();
|
||||
setInterval(() => void refreshRates(), 60 * 60 * 1000);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export interface SettingsData {
|
|||
dataDir: string;
|
||||
/** how long deleted sheets stay in backups/deleted */
|
||||
deletedRetentionDays: number;
|
||||
/** keep the window above others */
|
||||
alwaysOnTop: boolean;
|
||||
}
|
||||
|
||||
export const defaultSettingsData: SettingsData = {
|
||||
|
|
@ -44,6 +46,7 @@ export const defaultSettingsData: SettingsData = {
|
|||
resultsWidth: 42,
|
||||
dataDir: "",
|
||||
deletedRetentionDays: 14,
|
||||
alwaysOnTop: false,
|
||||
};
|
||||
|
||||
/** "" -> undefined for the Rust side (default app data dir) */
|
||||
|
|
|
|||
|
|
@ -433,6 +433,50 @@ body {
|
|||
.modal-btns button:hover { background: var(--hover); }
|
||||
.modal-btns button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
|
||||
/* ---------- search-all-sheets overlay */
|
||||
#search-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 12%;
|
||||
z-index: 200;
|
||||
}
|
||||
#search-overlay.hidden { display: none; }
|
||||
.search-box {
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
|
||||
width: min(440px, 90vw);
|
||||
max-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
#search-input {
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
#search-results { overflow-y: auto; padding: 6px; }
|
||||
.search-item {
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
.search-item:hover { background: var(--hover); }
|
||||
.search-item .doc { color: var(--fg-muted); font-size: 11px; margin-bottom: 2px; }
|
||||
.search-item .line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.search-empty { padding: 10px; color: var(--fg-muted); font-size: 13px; }
|
||||
|
||||
/* autocomplete */
|
||||
.cm-tooltip.cm-tooltip-autocomplete {
|
||||
background: var(--bg-soft);
|
||||
|
|
|
|||
|
|
@ -252,6 +252,17 @@ export class SumEditor {
|
|||
this.view.focus();
|
||||
}
|
||||
|
||||
/** put the caret on line n (1-based) and scroll it into view */
|
||||
goToLine(n: number): void {
|
||||
const doc = this.view.state.doc;
|
||||
const line = doc.line(Math.min(Math.max(n, 1), doc.lines));
|
||||
this.view.dispatch({
|
||||
selection: { anchor: line.from, head: line.to },
|
||||
scrollIntoView: true,
|
||||
});
|
||||
this.view.focus();
|
||||
}
|
||||
|
||||
private evaluate(text: string): void {
|
||||
this.results = this.engine.evaluateDocument(text);
|
||||
this.view.dispatch({ effects: setResults.of(this.results) });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue