mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-10 21:31:59 +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>
|
<button id="toggle-sidebar" title="Documents (Ctrl+B)">☰</button>
|
||||||
<input id="doc-title" spellcheck="false" autocomplete="off" />
|
<input id="doc-title" spellcheck="false" autocomplete="off" />
|
||||||
<span class="spacer" data-tauri-drag-region></span>
|
<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>
|
<button id="open-settings" title="Settings">⚙</button>
|
||||||
<div id="win-controls">
|
<div id="win-controls">
|
||||||
<button id="win-min" title="Minimize">─</button>
|
<button id="win-min" title="Minimize">─</button>
|
||||||
|
|
@ -79,6 +80,9 @@
|
||||||
<label class="row"><input id="set-autostart" type="checkbox" />
|
<label class="row"><input id="set-autostart" type="checkbox" />
|
||||||
<span data-i18n="autostart">Launch at startup</span>
|
<span data-i18n="autostart">Launch at startup</span>
|
||||||
</label>
|
</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>
|
<label><span data-i18n="fontsize">Font size</span>
|
||||||
<input id="set-fontsize" type="number" min="10" max="32" />
|
<input id="set-fontsize" type="number" min="10" max="32" />
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -107,6 +111,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"core:window:allow-minimize",
|
"core:window:allow-minimize",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-set-always-on-top",
|
||||||
"global-shortcut:allow-register",
|
"global-shortcut:allow-register",
|
||||||
"global-shortcut:allow-unregister",
|
"global-shortcut:allow-unregister",
|
||||||
"global-shortcut:allow-is-registered",
|
"global-shortcut:allow-is-registered",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const dict: Record<string, Record<string, string>> = {
|
||||||
language: "Language",
|
language: "Language",
|
||||||
hotkey: "Global hotkey",
|
hotkey: "Global hotkey",
|
||||||
autostart: "Launch at startup",
|
autostart: "Launch at startup",
|
||||||
|
alwaysontop: "Always on top",
|
||||||
fontsize: "Font size",
|
fontsize: "Font size",
|
||||||
extensions: "Extensions folder",
|
extensions: "Extensions folder",
|
||||||
done: "Done",
|
done: "Done",
|
||||||
|
|
@ -52,6 +53,8 @@ const dict: Record<string, Record<string, string>> = {
|
||||||
updateLater: "Later",
|
updateLater: "Later",
|
||||||
updateInstalling: "Installing update…",
|
updateInstalling: "Installing update…",
|
||||||
updateFailed: "Update failed",
|
updateFailed: "Update failed",
|
||||||
|
searchPlaceholder: "Search all sheets…",
|
||||||
|
searchEmpty: "No matches",
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
settings: "Настройки",
|
settings: "Настройки",
|
||||||
|
|
@ -64,6 +67,7 @@ const dict: Record<string, Record<string, string>> = {
|
||||||
language: "Язык",
|
language: "Язык",
|
||||||
hotkey: "Глобальный хоткей",
|
hotkey: "Глобальный хоткей",
|
||||||
autostart: "Запуск при старте Windows",
|
autostart: "Запуск при старте Windows",
|
||||||
|
alwaysontop: "Поверх всех окон",
|
||||||
fontsize: "Размер шрифта",
|
fontsize: "Размер шрифта",
|
||||||
extensions: "Папка расширений",
|
extensions: "Папка расширений",
|
||||||
done: "Готово",
|
done: "Готово",
|
||||||
|
|
@ -104,6 +108,8 @@ const dict: Record<string, Record<string, string>> = {
|
||||||
updateLater: "Позже",
|
updateLater: "Позже",
|
||||||
updateInstalling: "Установка обновления…",
|
updateInstalling: "Установка обновления…",
|
||||||
updateFailed: "Не удалось обновить",
|
updateFailed: "Не удалось обновить",
|
||||||
|
searchPlaceholder: "Поиск по всем листам…",
|
||||||
|
searchEmpty: "Ничего не найдено",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
103
src/main.ts
103
src/main.ts
|
|
@ -111,6 +111,74 @@ function newDoc(content = ""): void {
|
||||||
switchDoc(id);
|
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
|
// ---------- settings
|
||||||
|
|
||||||
function applySettings(): void {
|
function applySettings(): void {
|
||||||
|
|
@ -121,6 +189,7 @@ function applySettings(): void {
|
||||||
document.documentElement.style.setProperty("--editor-font-size", `${settings.fontSize}px`);
|
document.documentElement.style.setProperty("--editor-font-size", `${settings.fontSize}px`);
|
||||||
$("#editor-wrap").style.setProperty("--results-width", `${settings.resultsWidth}%`);
|
$("#editor-wrap").style.setProperty("--results-width", `${settings.resultsWidth}%`);
|
||||||
setLang(settings.language);
|
setLang(settings.language);
|
||||||
|
$<HTMLInputElement>("#search-input").placeholder = t("searchPlaceholder");
|
||||||
engine.updateSettings({
|
engine.updateSettings({
|
||||||
precision: settings.precision,
|
precision: settings.precision,
|
||||||
groupSeparator: settings.groupSeparator,
|
groupSeparator: settings.groupSeparator,
|
||||||
|
|
@ -139,6 +208,7 @@ function bindSettingsUI(): void {
|
||||||
const langSel = $<HTMLSelectElement>("#set-lang");
|
const langSel = $<HTMLSelectElement>("#set-lang");
|
||||||
const hotkey = $<HTMLInputElement>("#set-hotkey");
|
const hotkey = $<HTMLInputElement>("#set-hotkey");
|
||||||
const autostart = $<HTMLInputElement>("#set-autostart");
|
const autostart = $<HTMLInputElement>("#set-autostart");
|
||||||
|
const alwaysOnTop = $<HTMLInputElement>("#set-alwaysontop");
|
||||||
const fontSize = $<HTMLInputElement>("#set-fontsize");
|
const fontSize = $<HTMLInputElement>("#set-fontsize");
|
||||||
const resultsWidth = $<HTMLInputElement>("#set-resultswidth");
|
const resultsWidth = $<HTMLInputElement>("#set-resultswidth");
|
||||||
|
|
||||||
|
|
@ -149,6 +219,7 @@ function bindSettingsUI(): void {
|
||||||
langSel.value = settings.language;
|
langSel.value = settings.language;
|
||||||
hotkey.value = settings.hotkey;
|
hotkey.value = settings.hotkey;
|
||||||
autostart.checked = settings.autostart;
|
autostart.checked = settings.autostart;
|
||||||
|
alwaysOnTop.checked = settings.alwaysOnTop;
|
||||||
fontSize.value = String(settings.fontSize);
|
fontSize.value = String(settings.fontSize);
|
||||||
resultsWidth.value = String(settings.resultsWidth);
|
resultsWidth.value = String(settings.resultsWidth);
|
||||||
|
|
||||||
|
|
@ -220,6 +291,11 @@ function bindSettingsUI(): void {
|
||||||
await applyAutostart(settings.autostart);
|
await applyAutostart(settings.autostart);
|
||||||
void saveSettings(settings);
|
void saveSettings(settings);
|
||||||
});
|
});
|
||||||
|
alwaysOnTop.addEventListener("change", async () => {
|
||||||
|
settings.alwaysOnTop = alwaysOnTop.checked;
|
||||||
|
await applyAlwaysOnTop(settings.alwaysOnTop);
|
||||||
|
void saveSettings(settings);
|
||||||
|
});
|
||||||
|
|
||||||
const binDays = $<HTMLInputElement>("#set-bindays");
|
const binDays = $<HTMLInputElement>("#set-bindays");
|
||||||
binDays.value = String(settings.deletedRetentionDays);
|
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> {
|
async function refreshRates(force = false): Promise<void> {
|
||||||
const info = $("#rates-info");
|
const info = $("#rates-info");
|
||||||
info.classList.add("spin");
|
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()) {
|
if (isTauri()) {
|
||||||
const { getCurrentWindow } = await import("@tauri-apps/api/window");
|
const { getCurrentWindow } = await import("@tauri-apps/api/window");
|
||||||
const win = getCurrentWindow();
|
const win = getCurrentWindow();
|
||||||
|
|
@ -587,6 +685,10 @@ async function boot(): Promise<void> {
|
||||||
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === "b") {
|
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === "b") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$("#toggle-sidebar").click();
|
$("#toggle-sidebar").click();
|
||||||
|
}
|
||||||
|
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === "f") {
|
||||||
|
e.preventDefault();
|
||||||
|
openSearch();
|
||||||
}
|
}
|
||||||
if (e.ctrlKey && e.shiftKey && e.code === "KeyC") {
|
if (e.ctrlKey && e.shiftKey && e.code === "KeyC") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -637,6 +739,7 @@ async function boot(): Promise<void> {
|
||||||
|
|
||||||
await registerHotkey(null, settings.hotkey);
|
await registerHotkey(null, settings.hotkey);
|
||||||
if (settings.autostart) await applyAutostart(true);
|
if (settings.autostart) await applyAutostart(true);
|
||||||
|
if (settings.alwaysOnTop) await applyAlwaysOnTop(true);
|
||||||
void onAppQuit(() => flushAppData());
|
void onAppQuit(() => flushAppData());
|
||||||
void refreshRates();
|
void refreshRates();
|
||||||
setInterval(() => void refreshRates(), 60 * 60 * 1000);
|
setInterval(() => void refreshRates(), 60 * 60 * 1000);
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ export interface SettingsData {
|
||||||
dataDir: string;
|
dataDir: string;
|
||||||
/** how long deleted sheets stay in backups/deleted */
|
/** how long deleted sheets stay in backups/deleted */
|
||||||
deletedRetentionDays: number;
|
deletedRetentionDays: number;
|
||||||
|
/** keep the window above others */
|
||||||
|
alwaysOnTop: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSettingsData: SettingsData = {
|
export const defaultSettingsData: SettingsData = {
|
||||||
|
|
@ -44,6 +46,7 @@ export const defaultSettingsData: SettingsData = {
|
||||||
resultsWidth: 42,
|
resultsWidth: 42,
|
||||||
dataDir: "",
|
dataDir: "",
|
||||||
deletedRetentionDays: 14,
|
deletedRetentionDays: 14,
|
||||||
|
alwaysOnTop: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** "" -> undefined for the Rust side (default app data dir) */
|
/** "" -> undefined for the Rust side (default app data dir) */
|
||||||
|
|
|
||||||
|
|
@ -433,6 +433,50 @@ body {
|
||||||
.modal-btns button:hover { background: var(--hover); }
|
.modal-btns button:hover { background: var(--hover); }
|
||||||
.modal-btns button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
.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 */
|
/* autocomplete */
|
||||||
.cm-tooltip.cm-tooltip-autocomplete {
|
.cm-tooltip.cm-tooltip-autocomplete {
|
||||||
background: var(--bg-soft);
|
background: var(--bg-soft);
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,17 @@ export class SumEditor {
|
||||||
this.view.focus();
|
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 {
|
private evaluate(text: string): void {
|
||||||
this.results = this.engine.evaluateDocument(text);
|
this.results = this.engine.evaluateDocument(text);
|
||||||
this.view.dispatch({ effects: setResults.of(this.results) });
|
this.view.dispatch({ effects: setResults.of(this.results) });
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue