Merge pull request #139 from gamosoft/features/code-blocks
Features/code blocks
This commit is contained in:
commit
9c02ad0e3e
|
|
@ -269,7 +269,9 @@ async def login_page(request: Request, error: str = None):
|
||||||
async with aiofiles.open(login_path, 'r', encoding='utf-8') as f:
|
async with aiofiles.open(login_path, 'r', encoding='utf-8') as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
|
|
||||||
# No server-side manipulation needed - frontend handles error display via URL params
|
# Inject app name throughout the login page
|
||||||
|
app_name = config['app']['name']
|
||||||
|
content = content.replace('NoteDiscovery', app_name)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
@ -320,15 +322,6 @@ pages_router = APIRouter(
|
||||||
# Application Routes (with auth via router dependencies)
|
# Application Routes (with auth via router dependencies)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
@pages_router.get("/", response_class=HTMLResponse)
|
|
||||||
async def root(request: Request):
|
|
||||||
"""Serve the main application page"""
|
|
||||||
index_path = static_path / "index.html"
|
|
||||||
async with aiofiles.open(index_path, 'r', encoding='utf-8') as f:
|
|
||||||
content = await f.read()
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("")
|
@api_router.get("")
|
||||||
async def api_documentation():
|
async def api_documentation():
|
||||||
"""API Documentation - List all available endpoints"""
|
"""API Documentation - List all available endpoints"""
|
||||||
|
|
@ -1252,20 +1245,22 @@ async def health_check():
|
||||||
# Catch-all route for SPA (Single Page Application) routing
|
# Catch-all route for SPA (Single Page Application) routing
|
||||||
# This allows URLs like /folder/note to work for direct navigation
|
# This allows URLs like /folder/note to work for direct navigation
|
||||||
@pages_router.get("/{full_path:path}", response_class=HTMLResponse)
|
@pages_router.get("/{full_path:path}", response_class=HTMLResponse)
|
||||||
|
@limiter.limit("120/minute")
|
||||||
async def catch_all(full_path: str, request: Request):
|
async def catch_all(full_path: str, request: Request):
|
||||||
"""
|
"""
|
||||||
Serve index.html for all non-API routes.
|
Serve index.html for all non-API routes (including root /).
|
||||||
This enables client-side routing (e.g., /folder/note)
|
This enables client-side routing (e.g., /folder/note)
|
||||||
"""
|
"""
|
||||||
# Skip if it's an API route or static file (shouldn't reach here, but just in case)
|
# Skip if it's an API route or static file (shouldn't reach here, but just in case)
|
||||||
if full_path.startswith('api/') or full_path.startswith('static/'):
|
if full_path.startswith('api/') or full_path.startswith('static/'):
|
||||||
raise HTTPException(status_code=404, detail="Not found")
|
raise HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
# Serve index.html for all other routes
|
# Serve index.html with app name injected
|
||||||
index_path = static_path / "index.html"
|
index_path = static_path / "index.html"
|
||||||
async with aiofiles.open(index_path, 'r', encoding='utf-8') as f:
|
async with aiofiles.open(index_path, 'r', encoding='utf-8') as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
return content
|
app_name = config['app']['name']
|
||||||
|
return content.replace('<title>NoteDiscovery</title>', f'<title>{app_name}</title>')
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
192
frontend/app.js
192
frontend/app.js
|
|
@ -164,6 +164,10 @@ function noteApp() {
|
||||||
byEndPath: new Map(), // '/filename' and '/filename.md' -> true
|
byEndPath: new Map(), // '/filename' and '/filename.md' -> true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Image lookup map for O(1) image wikilink resolution (built on loadNotes)
|
||||||
|
// Maps image filename (case-insensitive) -> full path
|
||||||
|
_imageLookup: new Map(),
|
||||||
|
|
||||||
// Preview rendering debounce
|
// Preview rendering debounce
|
||||||
_previewDebounceTimeout: null,
|
_previewDebounceTimeout: null,
|
||||||
_lastRenderedContent: '',
|
_lastRenderedContent: '',
|
||||||
|
|
@ -454,6 +458,7 @@ function noteApp() {
|
||||||
// Set initial homepage state ONLY if we're actually on the homepage
|
// Set initial homepage state ONLY if we're actually on the homepage
|
||||||
if (window.location.pathname === '/') {
|
if (window.location.pathname === '/') {
|
||||||
window.history.replaceState({ homepageFolder: '' }, '', '/');
|
window.history.replaceState({ homepageFolder: '' }, '', '/');
|
||||||
|
document.title = this.appName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for browser back/forward navigation
|
// Listen for browser back/forward navigation
|
||||||
|
|
@ -478,7 +483,7 @@ function noteApp() {
|
||||||
this.noteContent = '';
|
this.noteContent = '';
|
||||||
this.currentNoteName = '';
|
this.currentNoteName = '';
|
||||||
this.outline = [];
|
this.outline = [];
|
||||||
document.title = 'NoteDiscovery';
|
document.title = this.appName;
|
||||||
|
|
||||||
// Restore homepage folder state if it was saved
|
// Restore homepage folder state if it was saved
|
||||||
if (e.state && e.state.homepageFolder !== undefined) {
|
if (e.state && e.state.homepageFolder !== undefined) {
|
||||||
|
|
@ -1003,12 +1008,25 @@ function noteApp() {
|
||||||
this._noteLookup.byName.clear();
|
this._noteLookup.byName.clear();
|
||||||
this._noteLookup.byNameLower.clear();
|
this._noteLookup.byNameLower.clear();
|
||||||
this._noteLookup.byEndPath.clear();
|
this._noteLookup.byEndPath.clear();
|
||||||
|
this._imageLookup.clear();
|
||||||
|
|
||||||
for (const note of this.notes) {
|
for (const note of this.notes) {
|
||||||
const path = note.path;
|
const path = note.path;
|
||||||
const pathLower = path.toLowerCase();
|
const pathLower = path.toLowerCase();
|
||||||
const name = note.name;
|
const name = note.name;
|
||||||
const nameLower = name.toLowerCase();
|
const nameLower = name.toLowerCase();
|
||||||
|
|
||||||
|
// Handle images separately - build image lookup map
|
||||||
|
if (note.type === 'image') {
|
||||||
|
// Map filename (case-insensitive) to full path
|
||||||
|
// First match wins if there are duplicates
|
||||||
|
if (!this._imageLookup.has(nameLower)) {
|
||||||
|
this._imageLookup.set(nameLower, path);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notes only from here
|
||||||
const nameWithoutMd = name.replace(/\.md$/i, '');
|
const nameWithoutMd = name.replace(/\.md$/i, '');
|
||||||
const nameWithoutMdLower = nameWithoutMd.toLowerCase();
|
const nameWithoutMdLower = nameWithoutMd.toLowerCase();
|
||||||
|
|
||||||
|
|
@ -1045,6 +1063,13 @@ function noteApp() {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Resolve image wikilink to full path (O(1) lookup)
|
||||||
|
// Returns the full path if found, null otherwise
|
||||||
|
resolveImageWikilink(imageName) {
|
||||||
|
const nameLower = imageName.toLowerCase();
|
||||||
|
return this._imageLookup.get(nameLower) || null;
|
||||||
|
},
|
||||||
|
|
||||||
// Load all tags
|
// Load all tags
|
||||||
async loadTags() {
|
async loadTags() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1993,10 +2018,9 @@ function noteApp() {
|
||||||
|
|
||||||
let link;
|
let link;
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
// For images, insert image markdown
|
// For images, use wiki-style link (resolves by filename, never breaks)
|
||||||
const filename = notePath.split('/').pop().replace(/\.[^/.]+$/, ''); // Remove extension
|
const filename = notePath.split('/').pop();
|
||||||
// Use relative path (not /api/images/) for portability
|
link = `![[${filename}]]`;
|
||||||
link = ``;
|
|
||||||
} else {
|
} else {
|
||||||
// For notes, insert note link
|
// For notes, insert note link
|
||||||
const noteName = notePath.split('/').pop().replace('.md', '');
|
const noteName = notePath.split('/').pop().replace('.md', '');
|
||||||
|
|
@ -2083,11 +2107,21 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Insert image markdown at cursor position
|
// Insert image markdown at cursor position using wiki-style syntax
|
||||||
|
// This ensures image links don't break when notes are moved
|
||||||
insertImageMarkdown(imagePath, altText, cursorPos) {
|
insertImageMarkdown(imagePath, altText, cursorPos) {
|
||||||
const filename = altText.replace(/\.[^/.]+$/, ''); // Remove extension
|
// Extract just the filename from the path (e.g., "folder/_attachments/image.png" -> "image.png")
|
||||||
// Use relative path (not /api/images/) for portability
|
const filename = imagePath.split('/').pop();
|
||||||
const markdown = ``;
|
|
||||||
|
// Use wiki-style image link: ![[filename.png]] or ![[filename.png|alt text]]
|
||||||
|
// The alt text is optional - only add if different from filename
|
||||||
|
const filenameWithoutExt = filename.replace(/\.[^/.]+$/, '');
|
||||||
|
const altWithoutExt = altText.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
// If alt text is meaningful (not just "pasted-image"), include it
|
||||||
|
const markdown = (altWithoutExt && altWithoutExt !== filenameWithoutExt && !altWithoutExt.startsWith('pasted-image'))
|
||||||
|
? `![[${filename}|${altWithoutExt}]]`
|
||||||
|
: `![[${filename}]]`;
|
||||||
|
|
||||||
const textBefore = this.noteContent.substring(0, cursorPos);
|
const textBefore = this.noteContent.substring(0, cursorPos);
|
||||||
const textAfter = this.noteContent.substring(cursorPos);
|
const textAfter = this.noteContent.substring(cursorPos);
|
||||||
|
|
@ -2097,7 +2131,7 @@ function noteApp() {
|
||||||
// Trigger autosave
|
// Trigger autosave
|
||||||
this.autoSave();
|
this.autoSave();
|
||||||
|
|
||||||
// Reload notes to show the new image in sidebar
|
// Reload notes to show the new image in sidebar and update lookup maps
|
||||||
this.loadNotes();
|
this.loadNotes();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -2159,7 +2193,7 @@ function noteApp() {
|
||||||
|
|
||||||
// Update browser tab title for image
|
// Update browser tab title for image
|
||||||
const imageName = imagePath.split('/').pop();
|
const imageName = imagePath.split('/').pop();
|
||||||
document.title = `${imageName} - NoteDiscovery`;
|
document.title = `${imageName} - ${this.appName}`;
|
||||||
|
|
||||||
// Update browser URL
|
// Update browser URL
|
||||||
if (updateHistory) {
|
if (updateHistory) {
|
||||||
|
|
@ -2475,7 +2509,7 @@ function noteApp() {
|
||||||
this.currentNote = '';
|
this.currentNote = '';
|
||||||
this.noteContent = '';
|
this.noteContent = '';
|
||||||
this.currentImage = '';
|
this.currentImage = '';
|
||||||
document.title = 'NoteDiscovery';
|
document.title = this.appName;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
|
@ -2491,7 +2525,7 @@ function noteApp() {
|
||||||
this.currentImage = ''; // Clear image viewer when loading a note
|
this.currentImage = ''; // Clear image viewer when loading a note
|
||||||
|
|
||||||
// Update browser tab title
|
// Update browser tab title
|
||||||
document.title = `${this.currentNoteName} - NoteDiscovery`;
|
document.title = `${this.currentNoteName} - ${this.appName}`;
|
||||||
this.lastSaved = false;
|
this.lastSaved = false;
|
||||||
|
|
||||||
// Extract outline for TOC panel
|
// Extract outline for TOC panel
|
||||||
|
|
@ -3076,7 +3110,7 @@ function noteApp() {
|
||||||
if (this.currentNote && this.currentNote.startsWith(folderPrefix)) {
|
if (this.currentNote && this.currentNote.startsWith(folderPrefix)) {
|
||||||
this.currentNote = '';
|
this.currentNote = '';
|
||||||
this.noteContent = '';
|
this.noteContent = '';
|
||||||
document.title = 'NoteDiscovery';
|
document.title = this.appName;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadNotes();
|
await this.loadNotes();
|
||||||
|
|
@ -3570,7 +3604,7 @@ function noteApp() {
|
||||||
this.currentNoteName = '';
|
this.currentNoteName = '';
|
||||||
this._lastRenderedContent = ''; // Clear render cache
|
this._lastRenderedContent = ''; // Clear render cache
|
||||||
this._cachedRenderedHTML = '';
|
this._cachedRenderedHTML = '';
|
||||||
document.title = 'NoteDiscovery';
|
document.title = this.appName;
|
||||||
// Redirect to root
|
// Redirect to root
|
||||||
window.history.replaceState({}, '', '/');
|
window.history.replaceState({}, '', '/');
|
||||||
}
|
}
|
||||||
|
|
@ -3746,7 +3780,54 @@ function noteApp() {
|
||||||
|
|
||||||
// Convert Obsidian-style wikilinks: [[note]] or [[note|display text]]
|
// Convert Obsidian-style wikilinks: [[note]] or [[note|display text]]
|
||||||
// Must be done before marked.parse() to avoid conflicts with markdown syntax
|
// Must be done before marked.parse() to avoid conflicts with markdown syntax
|
||||||
|
// BUT we need to protect code blocks first to avoid converting [[text]] inside code
|
||||||
const self = this; // Reference for closure
|
const self = this; // Reference for closure
|
||||||
|
|
||||||
|
// Step 1: Temporarily replace code blocks and inline code with placeholders
|
||||||
|
const codeBlocks = [];
|
||||||
|
// Protect fenced code blocks (```...```)
|
||||||
|
contentToRender = contentToRender.replace(/```[\s\S]*?```/g, (match) => {
|
||||||
|
codeBlocks.push(match);
|
||||||
|
return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`;
|
||||||
|
});
|
||||||
|
// Protect inline code (`...`)
|
||||||
|
contentToRender = contentToRender.replace(/`[^`]+`/g, (match) => {
|
||||||
|
codeBlocks.push(match);
|
||||||
|
return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Convert image wikilinks FIRST: ![[image.png]] or ![[image.png|alt text]]
|
||||||
|
// Must be before note wikilinks to prevent [[image.png]] from being matched first
|
||||||
|
contentToRender = contentToRender.replace(
|
||||||
|
/!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
||||||
|
(match, imageName, altText) => {
|
||||||
|
const filename = imageName.trim();
|
||||||
|
const alt = altText ? altText.trim() : filename.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
// Resolve image path using O(1) lookup
|
||||||
|
const imagePath = self.resolveImageWikilink(filename);
|
||||||
|
|
||||||
|
if (imagePath) {
|
||||||
|
// URL-encode path segments for the API
|
||||||
|
const encodedPath = imagePath.split('/').map(segment => {
|
||||||
|
try {
|
||||||
|
return encodeURIComponent(decodeURIComponent(segment));
|
||||||
|
} catch (e) {
|
||||||
|
return encodeURIComponent(segment);
|
||||||
|
}
|
||||||
|
}).join('/');
|
||||||
|
|
||||||
|
const safeAlt = alt.replace(/"/g, '"');
|
||||||
|
return `<img src="/api/images/${encodedPath}" alt="${safeAlt}" title="${safeAlt}">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image not found - return broken image indicator
|
||||||
|
const safeFilename = filename.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
return `<span class="wikilink-broken" title="Image not found">🖼️ ${safeFilename}</span>`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 2b: Convert note wikilinks: [[note]] or [[note|display text]]
|
||||||
contentToRender = contentToRender.replace(
|
contentToRender = contentToRender.replace(
|
||||||
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
||||||
(match, target, displayText) => {
|
(match, target, displayText) => {
|
||||||
|
|
@ -3769,6 +3850,11 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Step 3: Restore code blocks
|
||||||
|
contentToRender = contentToRender.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
|
||||||
|
return codeBlocks[parseInt(index)];
|
||||||
|
});
|
||||||
|
|
||||||
// Configure marked with syntax highlighting
|
// Configure marked with syntax highlighting
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
|
|
@ -3888,22 +3974,35 @@ function noteApp() {
|
||||||
|
|
||||||
// Add copy button to code block
|
// Add copy button to code block
|
||||||
addCopyButtonToCodeBlock(preElement) {
|
addCopyButtonToCodeBlock(preElement) {
|
||||||
// Create copy button
|
// Extract language from code element class (e.g., "language-toml" -> "TOML")
|
||||||
|
const codeElement = preElement.querySelector('code');
|
||||||
|
let language = '';
|
||||||
|
if (codeElement && codeElement.className) {
|
||||||
|
const match = codeElement.className.match(/language-(\w+)/);
|
||||||
|
if (match) {
|
||||||
|
const langMap = {
|
||||||
|
'js': 'JavaScript', 'ts': 'TypeScript', 'py': 'Python',
|
||||||
|
'rb': 'Ruby', 'cs': 'C#', 'cpp': 'C++', 'sh': 'Shell',
|
||||||
|
'bash': 'Bash', 'zsh': 'Zsh', 'yml': 'YAML', 'md': 'Markdown'
|
||||||
|
};
|
||||||
|
const rawLang = match[1].toLowerCase();
|
||||||
|
language = langMap[rawLang] || match[1].toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create copy button with language label
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.className = 'copy-code-button';
|
button.className = 'copy-code-button';
|
||||||
button.innerHTML = `
|
const displayText = language || this.t('common.copy_to_clipboard').split(' ')[0]; // Use first word as fallback
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
button.innerHTML = `<span>${displayText}</span>`;
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
button.dataset.originalText = displayText; // Store for restore after copy
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
button.title = this.t('common.copy_to_clipboard');
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
button.title = 'Copy to clipboard';
|
|
||||||
|
|
||||||
// Style the button
|
// Style the button
|
||||||
button.style.position = 'absolute';
|
button.style.position = 'absolute';
|
||||||
button.style.top = '8px';
|
button.style.top = '8px';
|
||||||
button.style.right = '8px';
|
button.style.right = '8px';
|
||||||
button.style.padding = '6px';
|
button.style.padding = '4px 10px';
|
||||||
button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
||||||
button.style.border = 'none';
|
button.style.border = 'none';
|
||||||
button.style.borderRadius = '4px';
|
button.style.borderRadius = '4px';
|
||||||
|
|
@ -3915,6 +4014,11 @@ function noteApp() {
|
||||||
button.style.alignItems = 'center';
|
button.style.alignItems = 'center';
|
||||||
button.style.justifyContent = 'center';
|
button.style.justifyContent = 'center';
|
||||||
button.style.zIndex = '10';
|
button.style.zIndex = '10';
|
||||||
|
button.style.fontSize = '11px';
|
||||||
|
button.style.fontWeight = '600';
|
||||||
|
button.style.fontFamily = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
|
||||||
|
button.style.textTransform = 'uppercase';
|
||||||
|
button.style.letterSpacing = '0.5px';
|
||||||
|
|
||||||
// Style the pre element to be relative
|
// Style the pre element to be relative
|
||||||
preElement.style.position = 'relative';
|
preElement.style.position = 'relative';
|
||||||
|
|
@ -3938,28 +4042,23 @@ function noteApp() {
|
||||||
|
|
||||||
const code = codeElement.textContent;
|
const code = codeElement.textContent;
|
||||||
|
|
||||||
|
const originalText = button.dataset.originalText;
|
||||||
|
const copiedText = this.t('common.copied');
|
||||||
|
const copyTitle = this.t('common.copy_to_clipboard');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(code);
|
await navigator.clipboard.writeText(code);
|
||||||
|
|
||||||
// Visual feedback - change icon to checkmark
|
// Visual feedback - show localized "Copied!"
|
||||||
button.innerHTML = `
|
button.innerHTML = `<span>${copiedText}</span>`;
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
button.style.backgroundColor = 'rgba(34, 197, 94, 0.8)';
|
button.style.backgroundColor = 'rgba(34, 197, 94, 0.8)';
|
||||||
button.title = 'Copied!';
|
button.title = copiedText;
|
||||||
|
|
||||||
// Reset after 2 seconds
|
// Reset after 2 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.innerHTML = `
|
button.innerHTML = `<span>${originalText}</span>`;
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
||||||
button.title = 'Copy to clipboard';
|
button.title = copyTitle;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy code:', err);
|
console.error('Failed to copy code:', err);
|
||||||
|
|
@ -3974,19 +4073,10 @@ function noteApp() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
button.innerHTML = `
|
button.innerHTML = `<span>${copiedText}</span>`;
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
button.style.backgroundColor = 'rgba(34, 197, 94, 0.8)';
|
button.style.backgroundColor = 'rgba(34, 197, 94, 0.8)';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.innerHTML = `
|
button.innerHTML = `<span>${originalText}</span>`;
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (fallbackErr) {
|
} catch (fallbackErr) {
|
||||||
|
|
@ -4966,7 +5056,7 @@ function noteApp() {
|
||||||
this.noteContent = '';
|
this.noteContent = '';
|
||||||
this.currentImage = '';
|
this.currentImage = '';
|
||||||
this.outline = [];
|
this.outline = [];
|
||||||
document.title = 'NoteDiscovery';
|
document.title = this.appName;
|
||||||
|
|
||||||
// Invalidate cache to force recalculation
|
// Invalidate cache to force recalculation
|
||||||
this._homepageCache = {
|
this._homepageCache = {
|
||||||
|
|
@ -4989,7 +5079,7 @@ function noteApp() {
|
||||||
this.currentImage = '';
|
this.currentImage = '';
|
||||||
this.outline = [];
|
this.outline = [];
|
||||||
this.mobileSidebarOpen = false;
|
this.mobileSidebarOpen = false;
|
||||||
document.title = 'NoteDiscovery';
|
document.title = this.appName;
|
||||||
|
|
||||||
// Clear undo/redo history
|
// Clear undo/redo history
|
||||||
this.undoHistory = [];
|
this.undoHistory = [];
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
"loading": "Laden...",
|
"loading": "Laden...",
|
||||||
"saved": "✓ Gespeichert",
|
"saved": "✓ Gespeichert",
|
||||||
"saving": "Speichern...",
|
"saving": "Speichern...",
|
||||||
"copied": "✓ Kopiert!",
|
"copied": "Kopiert!",
|
||||||
|
"copy_to_clipboard": "In Zwischenablage kopieren",
|
||||||
"failed": "{{action}} fehlgeschlagen. Bitte versuche es erneut."
|
"failed": "{{action}} fehlgeschlagen. Bitte versuche es erneut."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"saved": "✓ Saved",
|
"saved": "✓ Saved",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"copied": "✓ Copied!",
|
"copied": "Copied!",
|
||||||
|
"copy_to_clipboard": "Copy to clipboard",
|
||||||
"failed": "Failed to {{action}}. Please try again."
|
"failed": "Failed to {{action}}. Please try again."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"saved": "✓ Saved",
|
"saved": "✓ Saved",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"copied": "✓ Copied!",
|
"copied": "Copied!",
|
||||||
|
"copy_to_clipboard": "Copy to clipboard",
|
||||||
"failed": "Failed to {{action}}. Please try again."
|
"failed": "Failed to {{action}}. Please try again."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
"saved": "✓ Guardado",
|
"saved": "✓ Guardado",
|
||||||
"saving": "Guardando...",
|
"saving": "Guardando...",
|
||||||
"copied": "✓ ¡Copiado!",
|
"copied": "¡Copiado!",
|
||||||
|
"copy_to_clipboard": "Copiar al portapapeles",
|
||||||
"failed": "Error al {{action}}. Por favor, inténtalo de nuevo."
|
"failed": "Error al {{action}}. Por favor, inténtalo de nuevo."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
"saved": "✓ Enregistré",
|
"saved": "✓ Enregistré",
|
||||||
"saving": "Enregistrement...",
|
"saving": "Enregistrement...",
|
||||||
"copied": "✓ Copié !",
|
"copied": "Copié !",
|
||||||
|
"copy_to_clipboard": "Copier dans le presse-papiers",
|
||||||
"failed": "Échec de {{action}}. Veuillez réessayer."
|
"failed": "Échec de {{action}}. Veuillez réessayer."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
"loading": "Nalaganje...",
|
"loading": "Nalaganje...",
|
||||||
"saved": "✓ Shranjeno",
|
"saved": "✓ Shranjeno",
|
||||||
"saving": "Shranjevanje...",
|
"saving": "Shranjevanje...",
|
||||||
"copied": "✓ Kopirano!",
|
"copied": "Kopirano!",
|
||||||
|
"copy_to_clipboard": "Kopiraj v odložišče",
|
||||||
"failed": "Napaka pri {{action}}. Prosimo, poskusite znova."
|
"failed": "Napaka pri {{action}}. Prosimo, poskusite znova."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"saved": "✓ 已保存",
|
"saved": "✓ 已保存",
|
||||||
"saving": "保存中...",
|
"saving": "保存中...",
|
||||||
"copied": "✓ 已复制!",
|
"copied": "已复制!",
|
||||||
|
"copy_to_clipboard": "复制到剪贴板",
|
||||||
"failed": "{{action}} 失败。请重试。"
|
"failed": "{{action}} 失败。请重试。"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue