new shortcuts, search this note feature
This commit is contained in:
parent
60339d8f5c
commit
f4d6b920ab
21
README.md
21
README.md
|
@ -5,3 +5,24 @@ This template should help get you started developing with Tauri in vanilla HTML,
|
|||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
|
||||
# Android Build
|
||||
https://v2.tauri.app/start/prerequisites/#android
|
||||
|
||||
Use the SDK Manager in Android Studio to install the following:
|
||||
|
||||
Android SDK Platform
|
||||
Android SDK Platform-Tools
|
||||
NDK (Side by side)
|
||||
Android SDK Build-Tools
|
||||
Android SDK Command-line Tools
|
||||
|
||||
|
||||
`export JAVA_HOME=/opt/android-studio/jbr`
|
||||
|
||||
```
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
export NDK_HOME="$ANDROID_HOME/ndk/$(ls -1 $ANDROID_HOME/ndk)"
|
||||
```
|
||||
|
||||
`rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android`
|
206
index.html
206
index.html
|
@ -1,97 +1,143 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Snotes</title>
|
||||
<script type="module" src="/src/main.ts" defer></script>
|
||||
<style>
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Snotes</title>
|
||||
<script type="module" src="/src/main.ts" defer></script>
|
||||
<style>
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
.logo.typescript:hover {
|
||||
filter: drop-shadow(0 0 2em #2d79c7);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
.logo.typescript:hover {
|
||||
filter: drop-shadow(0 0 2em #2d79c7);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="menu" id="contextMenu">
|
||||
<button id="deleteButton">Delete</button>
|
||||
</div>
|
||||
<div class="top-bar">
|
||||
<div id="button-row">
|
||||
<button class="row" id="show-notes-button">Refresh Notes</button>
|
||||
<button class="row" type="submit" id="save-button">Save</button>
|
||||
<button class="row" id="new-button">New</button>
|
||||
<button class="row" id="image-button">OCR</button>
|
||||
<button class="row" id="export-button">Export</button>
|
||||
<button class="row" id="settings-button">Settings</button>
|
||||
<input type="file" id="fileInput" accept="image/*" style="display: none;" />
|
||||
<body>
|
||||
<div class="menu" id="contextMenu">
|
||||
<button id="deleteButton">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="sidebar">
|
||||
<div class="resizable-handle"></div>
|
||||
<div class="searchbar-container">
|
||||
<img id="reverse-icon-asc" src="./src/assets/sort-from-bottom-to-top.svg">
|
||||
<img id="reverse-icon-desc" src="./src/assets/sort-from-top-to-bottom.svg">
|
||||
<input type="text" name="note-search" id="note-searchbar" placeholder="Search...">
|
||||
<div class="top-bar">
|
||||
<div id="button-row">
|
||||
<button class="row" id="show-notes-button">Refresh Notes</button>
|
||||
<button class="row" type="submit" id="save-button">Save</button>
|
||||
<button class="row" id="new-button">New</button>
|
||||
<button class="row" id="image-button">OCR</button>
|
||||
<button class="row" id="export-button">Export</button>
|
||||
<button class="row" id="settings-button">Settings</button>
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
<div class="note-sidebar-container unselectable" id="note-sidebar-container">
|
||||
<!-- This is how the generated notes will look like:
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="sidebar">
|
||||
<div class="resizable-handle"></div>
|
||||
<div class="searchbar-container">
|
||||
<img
|
||||
id="reverse-icon-asc"
|
||||
src="./src/assets/sort-from-bottom-to-top.svg"
|
||||
/>
|
||||
<img
|
||||
id="reverse-icon-desc"
|
||||
src="./src/assets/sort-from-top-to-bottom.svg"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="note-search"
|
||||
id="note-searchbar"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="note-sidebar-container unselectable"
|
||||
id="note-sidebar-container"
|
||||
>
|
||||
<!-- This is how the generated notes will look like:
|
||||
<div class="sidebar-note rightclick-element">
|
||||
<span class="sidebar-note-id">1</span>
|
||||
<span class="sidebar-note-content">Lorem ipsum dolor sit amet...</span>
|
||||
<span class="sidebar-note-tag">Tag</span>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor">
|
||||
<input autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" id="create-tag"
|
||||
placeholder="Tag..." />
|
||||
|
||||
<textarea autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" id="create-input"
|
||||
placeholder="Type your note here..."></textarea>
|
||||
|
||||
<p id="create-msg"></p>
|
||||
|
||||
<p style="white-space: pre-line" id="notes-list"></p>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="id-modal-bg"></div>
|
||||
<div id="id-modal-container">
|
||||
<div class="openbyid-bar-container">
|
||||
<input type="text" name="id-search" id="id-search" placeholder="Note ID...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-modal-container">
|
||||
<div class="settings-flex">
|
||||
<h2 class="row"><code>SNOTES</code></h2>
|
||||
<p><small id="app-version"></small></p>
|
||||
<div class="fontsize-setting">
|
||||
<h3>Font Size (in px)</h3>
|
||||
<input type="number" name="fontsize" id="fontsize-setting-input" placeholder="">
|
||||
</div>
|
||||
<div id="ocrlanguage-setting">
|
||||
<div>
|
||||
<h3>OCR Language</h3> <a style="display: inline;" href="https://tesseract-ocr.github.io/tessdoc/Data-Files"
|
||||
target="_blank">Language
|
||||
Codes</a>
|
||||
</div>
|
||||
<div class="editor">
|
||||
<input
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
id="create-tag"
|
||||
placeholder="Tag..."
|
||||
/>
|
||||
|
||||
<textarea
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
id="create-input"
|
||||
placeholder="Type your note here..."
|
||||
></textarea>
|
||||
|
||||
<p id="create-msg"></p>
|
||||
|
||||
<p style="white-space: pre-line" id="notes-list"></p>
|
||||
</div>
|
||||
|
||||
<div id="id-modal-bg"></div>
|
||||
<div id="id-modal-container">
|
||||
<div class="openbyid-bar-container">
|
||||
<input
|
||||
type="text"
|
||||
name="id-search"
|
||||
id="id-search"
|
||||
placeholder="Note ID..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-modal-container">
|
||||
<div class="settings-flex">
|
||||
<h2 class="row"><code>SNOTES</code></h2>
|
||||
<p><small id="app-version"></small></p>
|
||||
<div class="fontsize-setting">
|
||||
<h3>Font Size (in px)</h3>
|
||||
<input
|
||||
type="number"
|
||||
name="fontsize"
|
||||
id="fontsize-setting-input"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<input type="text" name="ocrlanguage" id="ocr-language-setting-input" placeholder="eng">
|
||||
<div id="ocrlanguage-setting">
|
||||
<div>
|
||||
<h3>OCR Language</h3>
|
||||
<a
|
||||
style="display: inline"
|
||||
href="https://tesseract-ocr.github.io/tessdoc/Data-Files"
|
||||
target="_blank"
|
||||
>Language Codes</a
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="ocrlanguage"
|
||||
id="ocr-language-setting-input"
|
||||
placeholder="eng"
|
||||
/>
|
||||
</div>
|
||||
<button class="save-button" id="save-settings-button">Save</button>
|
||||
</div>
|
||||
<button class="save-button" id="save-settings-button">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -3484,7 +3484,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
|||
|
||||
[[package]]
|
||||
name = "snotes-deck"
|
||||
version = "0.0.14"
|
||||
version = "0.0.15"
|
||||
dependencies = [
|
||||
"home",
|
||||
"libsnotes",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "snotes-deck"
|
||||
version = "0.0.14"
|
||||
version = "0.0.15"
|
||||
description = "A simple little Note App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"productName": "snotes-deck",
|
||||
"mainBinaryName": "snotes-deck",
|
||||
"version": "0.0.14",
|
||||
"version": "0.0.15",
|
||||
"identifier": "space.maidsin.snotes-deck",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
|
|
240
src/main.ts
240
src/main.ts
|
@ -25,6 +25,7 @@ let idModalActive = false;
|
|||
let typingTimer: number | null = null;
|
||||
const AUTOSAVE_DELAY = 5000;
|
||||
const BACKGROUND_COLOR = "#252525";
|
||||
const BACKGROUND_COLOR_HOVER = "#5C5C5C";
|
||||
|
||||
enum EditorState {
|
||||
NEW,
|
||||
|
@ -520,8 +521,24 @@ function handleKeyboardShortcuts(event: KeyboardEvent) {
|
|||
event.preventDefault();
|
||||
showNotes();
|
||||
}
|
||||
// focus editor
|
||||
if (event.ctrlKey && event.key === "h") {
|
||||
event.preventDefault();
|
||||
if (createNoteContentEl) {
|
||||
createNoteContentEl.focus();
|
||||
}
|
||||
}
|
||||
// focus tags
|
||||
if (event.ctrlKey && event.key === "j") {
|
||||
event.preventDefault();
|
||||
if (createNoteTagEl) {
|
||||
createNoteTagEl.focus();
|
||||
}
|
||||
}
|
||||
// TODO: if we're focused the searchbox, arrow down will focus on the first result in the list
|
||||
|
||||
// focus searchbox
|
||||
if (event.ctrlKey && event.key === "f") {
|
||||
if (event.ctrlKey && event.key === "p") {
|
||||
event.preventDefault();
|
||||
if (searchbarEl) {
|
||||
searchbarEl.focus();
|
||||
|
@ -573,8 +590,229 @@ function handleKeyboardShortcuts(event: KeyboardEvent) {
|
|||
}
|
||||
}
|
||||
// quick switch note 1-9
|
||||
if (event.ctrlKey && event.key === "f") {
|
||||
openSearchModal();
|
||||
}
|
||||
}
|
||||
interface SearchResult {
|
||||
text: string;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: when clicked should it close the search results?
|
||||
* @returns void
|
||||
*/
|
||||
function openSearchModal() {
|
||||
const createNoteContentEl = document.querySelector(
|
||||
"textarea"
|
||||
) as HTMLTextAreaElement;
|
||||
if (!createNoteContentEl) return;
|
||||
|
||||
const modalBg = document.createElement("div");
|
||||
modalBg.id = "search-modal-bg";
|
||||
modalBg.style.cssText = `
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "search-modal";
|
||||
modal.style.cssText = `
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: ${BACKGROUND_COLOR};
|
||||
padding: 1rem;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||
z-index: 1001;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const searchInput = document.createElement("input");
|
||||
searchInput.type = "text";
|
||||
searchInput.placeholder = "Search this note...";
|
||||
searchInput.style.cssText = `
|
||||
width: 100%;
|
||||
padding: .5rem;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: .25rem;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const resultContainer = document.createElement("div");
|
||||
resultContainer.id = "search-results";
|
||||
resultContainer.style.cssText = `
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid #ccc;
|
||||
background-color: ${BACKGROUND_COLOR};
|
||||
`;
|
||||
|
||||
// Close button
|
||||
const closeButton = document.createElement("button");
|
||||
closeButton.textContent = "×";
|
||||
closeButton.style.cssText = `
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
// Append elements to the modal
|
||||
modal.appendChild(closeButton);
|
||||
modal.appendChild(searchInput);
|
||||
modal.appendChild(resultContainer);
|
||||
|
||||
// Append the modal to the body
|
||||
document.body.appendChild(modalBg);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add focus
|
||||
searchInput.focus();
|
||||
|
||||
function findSearchResults(searchTerm: string): SearchResult[] {
|
||||
const content = createNoteContentEl.value;
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
if (!searchTerm) return results;
|
||||
|
||||
const lines = content.split("\n");
|
||||
let currentIndex = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const lowerLine = line.toLowerCase();
|
||||
const searchTermLower = searchTerm.toLowerCase();
|
||||
let position = lowerLine.indexOf(searchTermLower);
|
||||
|
||||
while (position !== -1) {
|
||||
results.push({
|
||||
text: line,
|
||||
startIndex: currentIndex + position,
|
||||
endIndex: currentIndex + position + searchTerm.length,
|
||||
});
|
||||
position = lowerLine.indexOf(searchTermLower, position + 1);
|
||||
}
|
||||
currentIndex += line.length + 1; // +1 for the newline character
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// filter content
|
||||
searchInput.addEventListener("input", () => {
|
||||
const searchTerm = searchInput.value;
|
||||
const results = findSearchResults(searchTerm);
|
||||
|
||||
// Display results
|
||||
resultContainer.innerHTML = "";
|
||||
results.forEach((result) => {
|
||||
const p = document.createElement("p");
|
||||
|
||||
// Highlight matches
|
||||
const beforeMatch = result.text.substring(
|
||||
0,
|
||||
result.text.toLowerCase().indexOf(searchTerm.toLowerCase())
|
||||
);
|
||||
const match = result.text.substring(
|
||||
result.text.toLowerCase().indexOf(searchTerm.toLowerCase()),
|
||||
result.text.toLowerCase().indexOf(searchTerm.toLowerCase()) +
|
||||
searchTerm.length
|
||||
);
|
||||
const afterMatch = result.text.substring(
|
||||
result.text.toLowerCase().indexOf(searchTerm.toLowerCase()) +
|
||||
searchTerm.length
|
||||
);
|
||||
|
||||
p.innerHTML = `${beforeMatch}<mark>${match}</mark>${afterMatch}`;
|
||||
p.style.cssText = `
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
`;
|
||||
p.addEventListener("mouseover", () => {
|
||||
p.style.backgroundColor = BACKGROUND_COLOR_HOVER;
|
||||
});
|
||||
p.addEventListener("mouseout", () => {
|
||||
p.style.backgroundColor = "transparent";
|
||||
});
|
||||
p.addEventListener("click", () => selectResult(result));
|
||||
resultContainer.appendChild(p);
|
||||
});
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
modal.remove();
|
||||
modalBg.remove();
|
||||
}
|
||||
|
||||
// Close modal
|
||||
modalBg.addEventListener("click", closeModal);
|
||||
closeButton.addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", (e) => e.stopPropagation());
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to and focus on the clicked result in the Editor
|
||||
*
|
||||
* @param result the search result
|
||||
* @returns void
|
||||
*/
|
||||
function selectResult(result: SearchResult) {
|
||||
const createNoteContentEl = document.querySelector(
|
||||
"textarea"
|
||||
) as HTMLTextAreaElement;
|
||||
if (!createNoteContentEl) return;
|
||||
|
||||
createNoteContentEl.focus();
|
||||
createNoteContentEl.setSelectionRange(result.startIndex, result.endIndex);
|
||||
|
||||
// Calculate the position of the selection
|
||||
const textBeforeSelection = createNoteContentEl.value.substring(
|
||||
0,
|
||||
result.startIndex
|
||||
);
|
||||
const lines = textBeforeSelection.split("\n");
|
||||
const lineNumber = lines.length;
|
||||
|
||||
// Get the line height (fallback to 20 if computation fails)
|
||||
const computedLineHeight =
|
||||
parseInt(getComputedStyle(createNoteContentEl).lineHeight) || 20;
|
||||
|
||||
// Calculate scroll position to center the selection in the viewport
|
||||
const targetPosition = (lineNumber - 1) * computedLineHeight;
|
||||
const textareaHeight = createNoteContentEl.clientHeight;
|
||||
const scrollPosition = Math.max(0, targetPosition - textareaHeight / 2);
|
||||
|
||||
createNoteContentEl.scrollTo({
|
||||
top: scrollPosition,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Searches for note and displays the results accordingly
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue