| … | … | @@ -127,8 +127,6 @@ |
| 127 | 127 | currentNotebook: null, |
| 128 | 128 | notes: [], |
| 129 | 129 | currentNote: null, |
| 130 | | readme: null, |
| 131 | | readmeCollapsed: false, |
| 132 | 130 | editing: false, |
| 133 | 131 | editingId: null, // null = new note |
| 134 | 132 | canWrite: false, |
| … | … | @@ -165,11 +163,8 @@ |
| 165 | 163 | // Editor |
| 166 | 164 | var $editorTitle = document.getElementById('editor-title') |
| 167 | 165 | var $editorTags = document.getElementById('editor-tags') |
| 168 | | var $editorType = document.getElementById('editor-type') |
| 169 | 166 | var $editorContainer = document.getElementById('editor-container') |
| 170 | | var $spreadsheetContainer = document.getElementById('spreadsheet-container') |
| 171 | 167 | var tuiEditor = null |
| 172 | | var spreadsheetInstance = null |
| 173 | 168 | |
| 174 | 169 | // Notebook view |
| 175 | 170 | var $notebookTitle = document.getElementById('notebook-title') |
| … | … | @@ -233,9 +228,9 @@ |
| 233 | 228 | |
| 234 | 229 | var parts = path.split('/') |
| 235 | 230 | |
| 236 | | // #/:notebook/:noteSlug/edit |
| 231 | // #/:notebook/:noteId/edit |
| 237 | 232 | if (parts.length >= 3 && parts[2] === 'edit') { |
| 238 | | return { view: 'editor', notebook: decodeURIComponent(parts[0]), noteId: decodeURIComponent(parts[1]) } |
| 233 | return { view: 'editor', notebook: decodeURIComponent(parts[0]), noteId: parts[1] } |
| 239 | 234 | } |
| 240 | 235 | |
| 241 | 236 | // #/:notebook/new |
| … | … | @@ -243,9 +238,9 @@ |
| 243 | 238 | return { view: 'editor', notebook: decodeURIComponent(parts[0]), noteId: null } |
| 244 | 239 | } |
| 245 | 240 | |
| 246 | | // #/:notebook/:noteSlug |
| 241 | // #/:notebook/:noteId |
| 247 | 242 | if (parts.length >= 2) { |
| 248 | | return { view: 'note', notebook: decodeURIComponent(parts[0]), noteId: decodeURIComponent(parts[1]) } |
| 243 | return { view: 'note', notebook: decodeURIComponent(parts[0]), noteId: parts[1] } |
| 249 | 244 | } |
| 250 | 245 | |
| 251 | 246 | // #/:notebook — check for ?link= param |
| … | … | @@ -438,12 +433,12 @@ |
| 438 | 433 | noteLi.className += ' active' |
| 439 | 434 | } |
| 440 | 435 | noteLi.textContent = note.title || 'Untitled' |
| 441 | | noteLi.addEventListener('click', (function (slug, noteSlug) { |
| 436 | noteLi.addEventListener('click', (function (slug, id) { |
| 442 | 437 | return function (e) { |
| 443 | 438 | e.stopPropagation() |
| 444 | | navigate('#/' + encodeURIComponent(slug) + '/' + encodeURIComponent(noteSlug)) |
| 439 | navigate('#/' + encodeURIComponent(slug) + '/' + id) |
| 445 | 440 | } |
| 446 | | })(nb.slug, note.slug || note.id)) |
| 441 | })(nb.slug, note.id)) |
| 447 | 442 | noteUl.appendChild(noteLi) |
| 448 | 443 | } |
| 449 | 444 | |
| … | … | @@ -529,7 +524,7 @@ |
| 529 | 524 | var html = '<h1>Recent Notes</h1><div class="recent-feed">' |
| 530 | 525 | for (var i = 0; i < notes.length; i++) { |
| 531 | 526 | var n = notes[i] |
| 532 | | var href = '#/' + encodeURIComponent(n.notebook) + '/' + encodeURIComponent(n.slug || n.id) |
| 527 | var href = '#/' + encodeURIComponent(n.notebook) + '/' + n.id |
| 533 | 528 | var date = new Date(n.updatedAt) |
| 534 | 529 | var timeStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) |
| 535 | 530 | var preview = (n.content || '').replace(/[#*`\[\]!<>]/g, '').substring(0, 200) |
| … | … | @@ -624,15 +619,6 @@ |
| 624 | 619 | } |
| 625 | 620 | |
| 626 | 621 | await loadNoteList(slug) |
| 627 | | |
| 628 | | // Fetch README for this notebook |
| 629 | | try { |
| 630 | | var readmeData = await HnApi.getReadme(slug) |
| 631 | | state.readme = readmeData.note || readmeData |
| 632 | | } catch (err) { |
| 633 | | state.readme = null |
| 634 | | } |
| 635 | | |
| 636 | 622 | renderNotebookCards(slug) |
| 637 | 623 | |
| 638 | 624 | // Handle wiki-link redirect |
| … | … | @@ -641,7 +627,7 @@ |
| 641 | 627 | return n.title && n.title.toLowerCase() === linkTitle.toLowerCase() |
| 642 | 628 | }) |
| 643 | 629 | if (match) { |
| 644 | | navigate('#/' + encodeURIComponent(slug) + '/' + encodeURIComponent(match.slug || match.id)) |
| 630 | navigate('#/' + encodeURIComponent(slug) + '/' + match.id) |
| 645 | 631 | } |
| 646 | 632 | } |
| 647 | 633 | } catch (err) { |
| … | … | @@ -663,77 +649,8 @@ |
| 663 | 649 | |
| 664 | 650 | function renderNotebookCards (notebook) { |
| 665 | 651 | $notebookNotes.innerHTML = '' |
| 666 | | |
| 667 | | // Render README panel at top if available |
| 668 | | if (state.readme) { |
| 669 | | var readmePanel = document.createElement('div') |
| 670 | | readmePanel.className = 'notebook-readme' |
| 671 | | |
| 672 | | var readmeHeader = document.createElement('div') |
| 673 | | readmeHeader.className = 'readme-header' |
| 674 | | readmeHeader.innerHTML = '<span class="readme-label">README</span>' |
| 675 | | |
| 676 | | var readmeActions = document.createElement('div') |
| 677 | | readmeActions.className = 'readme-actions' |
| 678 | | |
| 679 | | var toggleBtn = document.createElement('button') |
| 680 | | toggleBtn.className = 'btn-sm readme-toggle' |
| 681 | | toggleBtn.textContent = state.readmeCollapsed ? 'Show' : 'Hide' |
| 682 | | toggleBtn.addEventListener('click', function () { |
| 683 | | state.readmeCollapsed = !state.readmeCollapsed |
| 684 | | toggleBtn.textContent = state.readmeCollapsed ? 'Show' : 'Hide' |
| 685 | | readmeBody.style.display = state.readmeCollapsed ? 'none' : 'block' |
| 686 | | }) |
| 687 | | readmeActions.appendChild(toggleBtn) |
| 688 | | |
| 689 | | if (state.canWrite) { |
| 690 | | var editReadmeBtn = document.createElement('button') |
| 691 | | editReadmeBtn.className = 'btn-sm' |
| 692 | | editReadmeBtn.textContent = 'Edit' |
| 693 | | editReadmeBtn.addEventListener('click', function () { |
| 694 | | navigate('#/' + encodeURIComponent(notebook) + '/' + encodeURIComponent(state.readme.slug || state.readme.id) + '/edit') |
| 695 | | }) |
| 696 | | readmeActions.appendChild(editReadmeBtn) |
| 697 | | } |
| 698 | | readmeHeader.appendChild(readmeActions) |
| 699 | | readmePanel.appendChild(readmeHeader) |
| 700 | | |
| 701 | | var readmeBody = document.createElement('div') |
| 702 | | readmeBody.className = 'readme-body markdown-body' |
| 703 | | // Render markdown, resolve TOC slug links relative to current notebook |
| 704 | | var readmeHtml = window.renderMarkdown(rewriteMediaUrls(state.readme.content || '')) |
| 705 | | readmeBody.innerHTML = readmeHtml |
| 706 | | if (state.readmeCollapsed) readmeBody.style.display = 'none' |
| 707 | | readmePanel.appendChild(readmeBody) |
| 708 | | |
| 709 | | // Make TOC links navigate within the notebook |
| 710 | | var tocLinks = readmeBody.querySelectorAll('a[href]') |
| 711 | | for (var li = 0; li < tocLinks.length; li++) { |
| 712 | | ;(function (link) { |
| 713 | | var href = link.getAttribute('href') |
| 714 | | // Relative slug links (no protocol, no /, no #) |
| 715 | | if (href && !href.startsWith('http') && !href.startsWith('/') && !href.startsWith('#') && !href.startsWith('mailto:')) { |
| 716 | | link.setAttribute('href', '#/' + encodeURIComponent(notebook) + '/' + encodeURIComponent(href)) |
| 717 | | link.addEventListener('click', function (e) { |
| 718 | | e.preventDefault() |
| 719 | | navigate('#/' + encodeURIComponent(notebook) + '/' + encodeURIComponent(href)) |
| 720 | | }) |
| 721 | | } |
| 722 | | })(tocLinks[li]) |
| 723 | | } |
| 724 | | |
| 725 | | $notebookNotes.appendChild(readmePanel) |
| 726 | | } |
| 727 | | |
| 728 | | // Get the readme ID to filter it from card list |
| 729 | | var readmeId = state.readme ? state.readme.id : null |
| 730 | | |
| 731 | 652 | for (var i = 0; i < state.notes.length; i++) { |
| 732 | 653 | var note = state.notes[i] |
| 733 | | // Skip README from card list |
| 734 | | if (readmeId && note.id === readmeId) continue |
| 735 | | |
| 736 | | var noteSlug = note.slug || note.id |
| 737 | 654 | var card = document.createElement('div') |
| 738 | 655 | card.className = 'note-card' |
| 739 | 656 | |
| … | … | @@ -745,12 +662,12 @@ |
| 745 | 662 | editBtn.className = 'note-card-btn note-card-edit' |
| 746 | 663 | editBtn.textContent = 'Edit' |
| 747 | 664 | editBtn.title = 'Edit note' |
| 748 | | editBtn.addEventListener('click', (function (ns) { |
| 665 | editBtn.addEventListener('click', (function (id) { |
| 749 | 666 | return function (e) { |
| 750 | 667 | e.stopPropagation() |
| 751 | | navigate('#/' + encodeURIComponent(notebook) + '/' + encodeURIComponent(ns) + '/edit') |
| 668 | navigate('#/' + encodeURIComponent(notebook) + '/' + id + '/edit') |
| 752 | 669 | } |
| 753 | | })(noteSlug)) |
| 670 | })(note.id)) |
| 754 | 671 | var delBtn = document.createElement('button') |
| 755 | 672 | delBtn.className = 'note-card-btn note-card-delete' |
| 756 | 673 | delBtn.textContent = 'Delete' |
| … | … | @@ -771,23 +688,12 @@ |
| 771 | 688 | |
| 772 | 689 | var h3 = document.createElement('h3') |
| 773 | 690 | h3.textContent = note.title || 'Untitled' |
| 774 | | if (note.type === 'spreadsheet') { |
| 775 | | var typeLabel = document.createElement('span') |
| 776 | | typeLabel.className = 'note-type-label' |
| 777 | | typeLabel.textContent = 'spreadsheet' |
| 778 | | typeLabel.style.cssText = 'font-size:0.7em;background:var(--color-primary,#6c9eff);color:#fff;padding:2px 6px;border-radius:3px;margin-left:8px;vertical-align:middle' |
| 779 | | h3.appendChild(typeLabel) |
| 780 | | } |
| 781 | 691 | card.appendChild(h3) |
| 782 | 692 | |
| 783 | 693 | if (note.content) { |
| 784 | 694 | var snippet = document.createElement('div') |
| 785 | 695 | snippet.className = 'snippet' |
| 786 | | if (note.type === 'spreadsheet') { |
| 787 | | snippet.textContent = '(spreadsheet data)' |
| 788 | | } else { |
| 789 | | snippet.textContent = note.content.substring(0, 120) + (note.content.length > 120 ? '...' : '') |
| 790 | | } |
| 696 | snippet.textContent = note.content.substring(0, 120) + (note.content.length > 120 ? '...' : '') |
| 791 | 697 | card.appendChild(snippet) |
| 792 | 698 | } |
| 793 | 699 | |
| … | … | @@ -808,9 +714,9 @@ |
| 808 | 714 | meta.textContent = new Date(note.updatedAt || note.createdAt).toLocaleDateString() |
| 809 | 715 | card.appendChild(meta) |
| 810 | 716 | |
| 811 | | card.addEventListener('click', (function (ns) { |
| 812 | | return function () { navigate('#/' + encodeURIComponent(notebook) + '/' + encodeURIComponent(ns)) } |
| 813 | | })(noteSlug)) |
| 717 | card.addEventListener('click', (function (id) { |
| 718 | return function () { navigate('#/' + encodeURIComponent(notebook) + '/' + id) } |
| 719 | })(note.id)) |
| 814 | 720 | |
| 815 | 721 | $notebookNotes.appendChild(card) |
| 816 | 722 | } |
| … | … | @@ -827,14 +733,13 @@ |
| 827 | 733 | await loadNoteList(notebook) |
| 828 | 734 | } |
| 829 | 735 | |
| 830 | | // noteId may be a slug or hex ID — API resolves both |
| 831 | 736 | var data = await HnApi.getNote(notebook, noteId) |
| 832 | 737 | state.currentNote = data.note || data |
| 833 | 738 | showView('note') |
| 834 | 739 | updateBreadcrumb() |
| 835 | 740 | renderNotebookList() |
| 836 | 741 | |
| 837 | | // Render note content (markdown or spreadsheet) |
| 742 | // Render markdown content |
| 838 | 743 | $noteContent.className = 'markdown-body' |
| 839 | 744 | $noteContent.innerHTML = '<h1>' + esc(state.currentNote.title) + '</h1>' |
| 840 | 745 | if (state.currentNote.tags && state.currentNote.tags.length > 0) { |
| … | … | @@ -845,35 +750,7 @@ |
| 845 | 750 | tagsHtml += '</div>' |
| 846 | 751 | $noteContent.innerHTML += tagsHtml |
| 847 | 752 | } |
| 848 | | |
| 849 | | if (state.currentNote.type === 'spreadsheet') { |
| 850 | | // Render read-only spreadsheet |
| 851 | | var ssContainer = document.createElement('div') |
| 852 | | ssContainer.id = 'note-spreadsheet-view' |
| 853 | | $noteContent.appendChild(ssContainer) |
| 854 | | var ssData |
| 855 | | try { |
| 856 | | ssData = state.currentNote.content ? JSON.parse(state.currentNote.content) : {} |
| 857 | | } catch (e) { |
| 858 | | ssData = {} |
| 859 | | } |
| 860 | | var cellData = ssData.data && ssData.data.length ? ssData.data : [['']] |
| 861 | | var wsViewConfig = { |
| 862 | | data: cellData, |
| 863 | | minDimensions: [4, 4], |
| 864 | | tableOverflow: true, |
| 865 | | tableWidth: '100%', |
| 866 | | tableHeight: '400px', |
| 867 | | defaultColWidth: 120, |
| 868 | | editable: false |
| 869 | | } |
| 870 | | if (ssData.columns) wsViewConfig.columns = ssData.columns |
| 871 | | jspreadsheet(ssContainer, { |
| 872 | | worksheets: [wsViewConfig] |
| 873 | | }) |
| 874 | | } else { |
| 875 | | $noteContent.innerHTML += window.renderMarkdown(rewriteMediaUrls(state.currentNote.content)) |
| 876 | | } |
| 753 | $noteContent.innerHTML += window.renderMarkdown(rewriteMediaUrls(state.currentNote.content)) |
| 877 | 754 | |
| 878 | 755 | // Add media hover overlays for writers |
| 879 | 756 | if (state.canWrite) addMediaOverlays() |
| … | … | @@ -1253,10 +1130,7 @@ |
| 1253 | 1130 | var pubKey = HnIdentity.getPublicKey() |
| 1254 | 1131 | if (pubKey) { |
| 1255 | 1132 | try { |
| 1256 | | // Use getFreshChallenge to get a dedicated challenge for this upload. |
| 1257 | | // Prevents race condition where fetch() interceptor consumes the shared |
| 1258 | | // cached challenge before the XHR upload reaches the server. |
| 1259 | | var ch = await HnIdentity.getFreshChallenge() |
| 1133 | var ch = await HnIdentity.getChallenge() |
| 1260 | 1134 | var sig = await HnIdentity.signChallenge(ch.challenge) |
| 1261 | 1135 | headers['X-Wirecore-PublicKey'] = pubKey |
| 1262 | 1136 | headers['X-Wirecore-Signature'] = sig |
| … | … | @@ -1303,7 +1177,7 @@ |
| 1303 | 1177 | if (!state.canWrite) { |
| 1304 | 1178 | alert('You do not have write permissions.') |
| 1305 | 1179 | if (noteId) { |
| 1306 | | navigate('#/' + encodeURIComponent(notebook) + '/' + encodeURIComponent(noteId)) |
| 1180 | navigate('#/' + encodeURIComponent(notebook) + '/' + noteId) |
| 1307 | 1181 | } else { |
| 1308 | 1182 | navigate('#/' + encodeURIComponent(notebook)) |
| 1309 | 1183 | } |
| … | … | @@ -1325,20 +1199,18 @@ |
| 1325 | 1199 | } |
| 1326 | 1200 | |
| 1327 | 1201 | state.editing = true |
| 1202 | state.editingId = noteId |
| 1328 | 1203 | |
| 1329 | 1204 | var initialContent = '' |
| 1330 | | var noteType = 'markdown' |
| 1331 | 1205 | |
| 1332 | 1206 | if (noteId) { |
| 1333 | | // Edit existing — noteId may be a slug, resolve to hex ID |
| 1207 | // Edit existing |
| 1334 | 1208 | try { |
| 1335 | 1209 | var data = await HnApi.getNote(notebook, noteId) |
| 1336 | 1210 | var note = data.note || data |
| 1337 | | state.editingId = note.slug || note.id // Use slug for API calls (server resolves both) |
| 1338 | 1211 | $editorTitle.value = note.title || '' |
| 1339 | 1212 | $editorTags.value = (note.tags || []).join(', ') |
| 1340 | 1213 | initialContent = rewriteMediaUrls(note.content) |
| 1341 | | noteType = note.type || 'markdown' |
| 1342 | 1214 | state.currentNote = note |
| 1343 | 1215 | } catch (err) { |
| 1344 | 1216 | console.error('Failed to load note for editing:', err) |
| … | … | @@ -1346,85 +1218,34 @@ |
| 1346 | 1218 | return |
| 1347 | 1219 | } |
| 1348 | 1220 | } else { |
| 1349 | | // New note — preserve type selector if already set (user may have toggled it) |
| 1350 | | state.editingId = null |
| 1351 | | if (!$editorTitle.value) $editorTitle.value = '' |
| 1352 | | $editorTags.value = $editorTags.value || '' |
| 1221 | // New note |
| 1222 | $editorTitle.value = '' |
| 1223 | $editorTags.value = '' |
| 1353 | 1224 | initialContent = '' |
| 1354 | | noteType = $editorType.value || 'markdown' |
| 1355 | 1225 | state.currentNote = null |
| 1356 | 1226 | } |
| 1357 | 1227 | |
| 1358 | | // Set type selector |
| 1359 | | $editorType.value = noteType |
| 1360 | | // Lock type for existing notes (changing type after creation is not supported) |
| 1361 | | $editorType.disabled = !!noteId |
| 1362 | | |
| 1363 | 1228 | showView('editor') |
| 1364 | 1229 | updateBreadcrumb() |
| 1365 | 1230 | renderNotebookList() |
| 1366 | 1231 | |
| 1367 | | // Destroy previous editor instances |
| 1232 | // Destroy previous editor instance if any |
| 1368 | 1233 | if (tuiEditor) { |
| 1369 | 1234 | tuiEditor.destroy() |
| 1370 | 1235 | tuiEditor = null |
| 1371 | 1236 | } |
| 1372 | | if (spreadsheetInstance) { |
| 1373 | | try { |
| 1374 | | if (spreadsheetInstance[0] && spreadsheetInstance[0].parent) { |
| 1375 | | jspreadsheet.destroy($spreadsheetContainer) |
| 1376 | | } |
| 1377 | | } catch (e) {} |
| 1378 | | spreadsheetInstance = null |
| 1379 | | } |
| 1380 | | $spreadsheetContainer.innerHTML = '' |
| 1381 | | |
| 1382 | | // Branch: spreadsheet or markdown editor |
| 1383 | | if (noteType === 'spreadsheet') { |
| 1384 | | $editorContainer.style.display = 'none' |
| 1385 | | $spreadsheetContainer.style.display = '' |
| 1386 | | var ssData |
| 1387 | | try { |
| 1388 | | ssData = initialContent ? JSON.parse(initialContent) : {} |
| 1389 | | } catch (e) { |
| 1390 | | ssData = {} |
| 1391 | | } |
| 1392 | | var cellData = ssData.data && ssData.data.length ? ssData.data : [['', '', '', ''], ['', '', '', ''], ['', '', '', ''], ['', '', '', '']] |
| 1393 | | var wsConfig = { |
| 1394 | | data: cellData, |
| 1395 | | minDimensions: [4, 4], |
| 1396 | | tableOverflow: true, |
| 1397 | | tableWidth: '100%', |
| 1398 | | tableHeight: 'calc(100vh - 240px)', |
| 1399 | | defaultColWidth: 120 |
| 1400 | | } |
| 1401 | | if (ssData.columns) wsConfig.columns = ssData.columns |
| 1402 | | spreadsheetInstance = jspreadsheet($spreadsheetContainer, { |
| 1403 | | worksheets: [wsConfig] |
| 1404 | | }) |
| 1405 | | return // Skip Toast UI setup |
| 1406 | | } |
| 1407 | | |
| 1408 | | // Markdown editor path |
| 1409 | | $editorContainer.style.display = '' |
| 1410 | | $spreadsheetContainer.style.display = 'none' |
| 1411 | 1237 | |
| 1412 | 1238 | // Helper: insert raw markdown without WYSIWYG escaping |
| 1413 | 1239 | // Switches to markdown mode, inserts at end, adds trailing newline for cursor escape |
| 1414 | 1240 | function insertRawMarkdown (md) { |
| 1415 | 1241 | if (!tuiEditor) return |
| 1416 | | // Switch to markdown mode to avoid WYSIWYG mangling HTML/fenced blocks |
| 1242 | // Switch to markdown mode to insert at cursor position |
| 1417 | 1243 | var wasWysiwyg = tuiEditor.isWysiwygMode() |
| 1418 | 1244 | if (wasWysiwyg) tuiEditor.changeMode('markdown') |
| 1419 | | var current = tuiEditor.getMarkdown() |
| 1420 | | // Ensure trailing newlines so cursor can escape the block |
| 1421 | | var combined = current.replace(/\n+$/, '') + '\n\n' + md.replace(/^\n+/, '') + '\n\n' |
| 1422 | | tuiEditor.setMarkdown(combined) |
| 1423 | | // Don't switch back if content has gallery blocks — wysiwyg mangles them |
| 1424 | | if (wasWysiwyg && combined.indexOf('```gallery') === -1) { |
| 1425 | | tuiEditor.changeMode('wysiwyg') |
| 1426 | | } |
| 1427 | | tuiEditor.moveCursorToEnd() |
| 1245 | // Insert at cursor position using replaceSelection |
| 1246 | var insertion = '\n' + md.replace(/^\n+/, '').replace(/\n+$/, '') + '\n' |
| 1247 | tuiEditor.replaceSelection(insertion) |
| 1248 | if (wasWysiwyg) tuiEditor.changeMode('wysiwyg') |
| 1428 | 1249 | } |
| 1429 | 1250 | |
| 1430 | 1251 | // Custom toolbar button elements (created before editor so they can be referenced) |
| … | … | @@ -1708,23 +1529,12 @@ |
| 1708 | 1529 | if (!await ensureIdentity()) return |
| 1709 | 1530 | var notebook = state.currentNotebook.slug |
| 1710 | 1531 | var title = $editorTitle.value.trim() |
| 1711 | | var noteType = $editorType.value |
| 1712 | | var content |
| 1532 | var content = tuiEditor ? tuiEditor.getMarkdown() : '' |
| 1713 | 1533 | var tags = $editorTags.value |
| 1714 | 1534 | .split(',') |
| 1715 | 1535 | .map(function (t) { return t.trim() }) |
| 1716 | 1536 | .filter(function (t) { return t.length > 0 }) |
| 1717 | 1537 | |
| 1718 | | if (noteType === 'spreadsheet' && spreadsheetInstance) { |
| 1719 | | var ssData = spreadsheetInstance[0].getData() |
| 1720 | | content = JSON.stringify({ data: ssData }) |
| 1721 | | } else { |
| 1722 | | content = tuiEditor ? tuiEditor.getMarkdown() : '' |
| 1723 | | // Normalize gallery blocks — ensure ```gallery is on its own line with no trailing whitespace |
| 1724 | | content = content.replace(/[ \t]*```gallery[ \t]*/g, '```gallery') |
| 1725 | | content = content.replace(/([^\n])```gallery/g, '$1\n```gallery') |
| 1726 | | } |
| 1727 | | |
| 1728 | 1538 | if (!title) { |
| 1729 | 1539 | alert('Title is required') |
| 1730 | 1540 | return |
| … | … | @@ -1733,14 +1543,13 @@ |
| 1733 | 1543 | try { |
| 1734 | 1544 | if (state.editingId) { |
| 1735 | 1545 | // Update existing note |
| 1736 | | var updated = await HnApi.updateNote(notebook, state.editingId, { title: title, content: content, tags: tags }) |
| 1737 | | var updatedNote = updated.note || updated |
| 1738 | | navigate('#/' + encodeURIComponent(notebook) + '/' + encodeURIComponent(updatedNote.slug || state.editingId)) |
| 1546 | await HnApi.updateNote(notebook, state.editingId, { title: title, content: content, tags: tags }) |
| 1547 | navigate('#/' + encodeURIComponent(notebook) + '/' + state.editingId) |
| 1739 | 1548 | } else { |
| 1740 | 1549 | // Create new note |
| 1741 | | var data = await HnApi.createNote(notebook, { title: title, content: content, tags: tags, type: noteType }) |
| 1550 | var data = await HnApi.createNote(notebook, { title: title, content: content, tags: tags }) |
| 1742 | 1551 | var note = data.note || data |
| 1743 | | navigate('#/' + encodeURIComponent(notebook) + '/' + encodeURIComponent(note.slug || note.id)) |
| 1552 | navigate('#/' + encodeURIComponent(notebook) + '/' + note.id) |
| 1744 | 1553 | } |
| 1745 | 1554 | await loadNotebooks() |
| 1746 | 1555 | } catch (err) { |
| … | … | @@ -1783,7 +1592,7 @@ |
| 1783 | 1592 | |
| 1784 | 1593 | var pathDiv = document.createElement('div') |
| 1785 | 1594 | pathDiv.className = 'path' |
| 1786 | | pathDiv.textContent = r.notebook + '/' + (r.slug || r.id) |
| 1595 | pathDiv.textContent = r.notebook + '/' + r.id |
| 1787 | 1596 | item.appendChild(pathDiv) |
| 1788 | 1597 | |
| 1789 | 1598 | if (r.snippet) { |
| … | … | @@ -1793,9 +1602,9 @@ |
| 1793 | 1602 | item.appendChild(snippet) |
| 1794 | 1603 | } |
| 1795 | 1604 | |
| 1796 | | item.addEventListener('click', (function (nb, noteSlug) { |
| 1797 | | return function () { navigate('#/' + encodeURIComponent(nb) + '/' + encodeURIComponent(noteSlug)) } |
| 1798 | | })(r.notebook, r.slug || r.id)) |
| 1605 | item.addEventListener('click', (function (nb, id) { |
| 1606 | return function () { navigate('#/' + encodeURIComponent(nb) + '/' + id) } |
| 1607 | })(r.notebook, r.id)) |
| 1799 | 1608 | |
| 1800 | 1609 | $searchResults.appendChild(item) |
| 1801 | 1610 | } |
| … | … | @@ -2536,10 +2345,7 @@ |
| 2536 | 2345 | if (mwStatus) { |
| 2537 | 2346 | if (mwStatus.linked) { |
| 2538 | 2347 | html += '<span class="p2p-badge p2p-badge-on">Linked</span> ' |
| 2539 | | html += '<span class="text-muted">' + (mwStatus.linkedPeers || 0) + ' peer' + ((mwStatus.linkedPeers || 0) !== 1 ? 's' : '') |
| 2540 | | html += ', ' + (mwStatus.mirroredDrives || 0) + ' mirrored drive' + ((mwStatus.mirroredDrives || 0) !== 1 ? 's' : '') |
| 2541 | | html += ', ' + (mwStatus.sharedUsers || 0) + ' shared user' + ((mwStatus.sharedUsers || 0) !== 1 ? 's' : '') |
| 2542 | | html += '</span>' |
| 2348 | html += '<span class="text-muted">' + (mwStatus.sharedNotebooks || 0) + ' shared notebook' + ((mwStatus.sharedNotebooks || 0) !== 1 ? 's' : '') + '</span>' |
| 2543 | 2349 | } else { |
| 2544 | 2350 | html += '<span class="p2p-badge p2p-badge-off">Not linked</span> ' |
| 2545 | 2351 | html += '<span class="text-muted">No peer instances linked yet</span>' |
| … | … | @@ -2549,24 +2355,10 @@ |
| 2549 | 2355 | } |
| 2550 | 2356 | html += '</div>' |
| 2551 | 2357 | |
| 2552 | | // Mirrored drives list |
| 2553 | | if (mwStatus && mwStatus.registeredDrives && mwStatus.registeredDrives.length > 0) { |
| 2554 | | html += '<div style="margin:0.5rem 0;font-size:0.85rem">' |
| 2555 | | html += '<strong>Mirrored drives:</strong> ' |
| 2556 | | for (var di = 0; di < mwStatus.registeredDrives.length; di++) { |
| 2557 | | var rd = mwStatus.registeredDrives[di] |
| 2558 | | html += '<span class="p2p-badge p2p-badge-on" style="margin:2px">' + esc(rd.slug) + '</span> ' |
| 2559 | | } |
| 2560 | | html += '</div>' |
| 2561 | | } |
| 2562 | | |
| 2563 | 2358 | // Actions |
| 2564 | | html += '<div style="display:flex;gap:8px;margin-top:0.75rem;flex-wrap:wrap">' |
| 2359 | html += '<div style="display:flex;gap:8px;margin-top:0.75rem">' |
| 2565 | 2360 | html += '<button class="btn-sm btn-primary" id="mw-link-btn">Create Link Invite</button>' |
| 2566 | 2361 | html += '<button class="btn-sm" id="mw-join-btn">Join with Invite</button>' |
| 2567 | | if (mwStatus && mwStatus.linked) { |
| 2568 | | html += '<button class="btn-sm" id="mw-sync-btn">Sync Now</button>' |
| 2569 | | } |
| 2570 | 2362 | html += '</div>' |
| 2571 | 2363 | |
| 2572 | 2364 | // Invite display (hidden by default) |
| … | … | @@ -2576,9 +2368,6 @@ |
| 2576 | 2368 | html += '<button class="btn-sm" id="mw-copy-invite">Copy</button>' |
| 2577 | 2369 | html += '</div>' |
| 2578 | 2370 | |
| 2579 | | // Sync result display (hidden by default) |
| 2580 | | html += '<div id="mw-sync-result" style="display:none;margin-top:0.5rem;padding:0.5rem;background:var(--bg-tertiary);border-radius:4px;font-size:0.85rem"></div>' |
| 2581 | | |
| 2582 | 2371 | html += '</div>' |
| 2583 | 2372 | |
| 2584 | 2373 | // Stats cards |
| … | … | @@ -2808,26 +2597,9 @@ |
| 2808 | 2597 | mwJoinBtn.disabled = true |
| 2809 | 2598 | mwJoinBtn.textContent = 'Joining...' |
| 2810 | 2599 | try { |
| 2811 | | var result = await HnApi.multiwriterJoin(invite.trim()) |
| 2812 | | var syncResult = document.getElementById('mw-sync-result') |
| 2813 | | if (syncResult) { |
| 2814 | | syncResult.style.display = 'block' |
| 2815 | | syncResult.textContent = 'Successfully linked! Both devices are now syncing.' |
| 2816 | | } |
| 2817 | | // Poll status briefly to show sync progress |
| 2818 | | var pollCount = 0 |
| 2819 | | var pollId = setInterval(async function () { |
| 2820 | | pollCount++ |
| 2821 | | if (pollCount > 6) { clearInterval(pollId); return } |
| 2822 | | try { |
| 2823 | | var st = await HnApi.multiwriterStatus() |
| 2824 | | if (syncResult) { |
| 2825 | | syncResult.textContent = 'Linked! Peers: ' + (st.linkedPeers || 0) + ', Drives: ' + (st.mirroredDrives || 0) + ', Users: ' + (st.sharedUsers || 0) |
| 2826 | | } |
| 2827 | | } catch (e) {} |
| 2828 | | }, 5000) |
| 2829 | | // Refresh panel after short delay |
| 2830 | | setTimeout(function () { loadAdminPanel() }, 2000) |
| 2600 | await HnApi.multiwriterJoin(invite.trim()) |
| 2601 | alert('Successfully joined! Instances are now linked.') |
| 2602 | loadAdminPanel() |
| 2831 | 2603 | } catch (err) { |
| 2832 | 2604 | alert('Error: ' + (err.message || 'Failed to join')) |
| 2833 | 2605 | } |
| … | … | @@ -2850,30 +2622,6 @@ |
| 2850 | 2622 | }) |
| 2851 | 2623 | } |
| 2852 | 2624 | |
| 2853 | | // Multi-writer: Manual sync |
| 2854 | | var mwSyncBtn = $adminContent.querySelector('#mw-sync-btn') |
| 2855 | | if (mwSyncBtn) { |
| 2856 | | mwSyncBtn.addEventListener('click', async function () { |
| 2857 | | mwSyncBtn.disabled = true |
| 2858 | | mwSyncBtn.textContent = 'Syncing...' |
| 2859 | | try { |
| 2860 | | var result = await HnApi.multiwriterSync() |
| 2861 | | var syncResult = document.getElementById('mw-sync-result') |
| 2862 | | if (syncResult) { |
| 2863 | | syncResult.style.display = 'block' |
| 2864 | | syncResult.textContent = 'Synced: ' + (result.mirroredDrives || 0) + ' drives, ' + (result.sharedUsers || 0) + ' users, ' + (result.linkedPeers || 0) + ' peers' |
| 2865 | | } |
| 2866 | | loadAdminPanel() |
| 2867 | | } catch (err) { |
| 2868 | | alert('Sync failed: ' + (err.message || 'Unknown error')) |
| 2869 | | } |
| 2870 | | mwSyncBtn.disabled = false |
| 2871 | | mwSyncBtn.textContent = 'Sync Now' |
| 2872 | | }) |
| 2873 | | } |
| 2874 | | |
| 2875 | | // (Add Writer Key removed — HTTP callback handles this automatically) |
| 2876 | | |
| 2877 | 2625 | } catch (err) { |
| 2878 | 2626 | if (err.status === 403) { |
| 2879 | 2627 | $adminContent.innerHTML = '<p class="text-muted">You do not have admin access.</p>' |
| … | … | @@ -3066,7 +2814,7 @@ |
| 3066 | 2814 | // Note toolbar buttons |
| 3067 | 2815 | document.getElementById('btn-edit-note').addEventListener('click', function () { |
| 3068 | 2816 | if (state.currentNotebook && state.currentNote) { |
| 3069 | | navigate('#/' + encodeURIComponent(state.currentNotebook.slug) + '/' + encodeURIComponent(state.currentNote.slug || state.currentNote.id) + '/edit') |
| 2817 | navigate('#/' + encodeURIComponent(state.currentNotebook.slug) + '/' + state.currentNote.id + '/edit') |
| 3070 | 2818 | } |
| 3071 | 2819 | }) |
| 3072 | 2820 | |
| … | … | @@ -3075,30 +2823,12 @@ |
| 3075 | 2823 | document.getElementById('btn-note-links').addEventListener('click', showLinks) |
| 3076 | 2824 | document.getElementById('btn-note-backlinks').addEventListener('click', showBacklinks) |
| 3077 | 2825 | |
| 3078 | | // Editor type selector — swap editor when creating new notes |
| 3079 | | $editorType.addEventListener('change', function () { |
| 3080 | | if (state.editingId) return // Can't change type of existing notes |
| 3081 | | var notebook = state.currentNotebook ? state.currentNotebook.slug : null |
| 3082 | | if (!notebook) return |
| 3083 | | // Warn if switching would discard content |
| 3084 | | var hasContent = (tuiEditor && tuiEditor.getMarkdown().trim()) || |
| 3085 | | (spreadsheetInstance && spreadsheetInstance[0] && spreadsheetInstance[0].getData().some(function (row) { return row.some(function (c) { return c !== '' }) })) |
| 3086 | | if (hasContent && !confirm('Switching note type will discard current content. Continue?')) { |
| 3087 | | // Revert the selector |
| 3088 | | $editorType.value = spreadsheetInstance ? 'spreadsheet' : 'markdown' |
| 3089 | | return |
| 3090 | | } |
| 3091 | | // Re-run loadEditor which will pick up the new type value |
| 3092 | | loadEditor(notebook, null) |
| 3093 | | }) |
| 3094 | | |
| 3095 | 2826 | // Editor save/cancel |
| 3096 | 2827 | document.getElementById('btn-save-note').addEventListener('click', saveNote) |
| 3097 | 2828 | document.getElementById('btn-cancel-edit').addEventListener('click', function () { |
| 3098 | 2829 | if (state.currentNotebook) { |
| 3099 | 2830 | if (state.editingId) { |
| 3100 | | var cancelSlug = (state.currentNote && state.currentNote.slug) || state.editingId |
| 3101 | | navigate('#/' + encodeURIComponent(state.currentNotebook.slug) + '/' + encodeURIComponent(cancelSlug)) |
| 2831 | navigate('#/' + encodeURIComponent(state.currentNotebook.slug) + '/' + state.editingId) |
| 3102 | 2832 | } else { |
| 3103 | 2833 | navigate('#/' + encodeURIComponent(state.currentNotebook.slug)) |
| 3104 | 2834 | } |
| … | … | @@ -3169,12 +2899,6 @@ |
| 3169 | 2899 | } |
| 3170 | 2900 | updateProfileLink() |
| 3171 | 2901 | await loadNotebooks() |
| 3172 | | |
| 3173 | | // First visit — no identity: show welcome modal |
| 3174 | | if (!HnIdentity.hasIdentity() && !HnApi.getToken()) { |
| 3175 | | showIdentityChoiceModal() |
| 3176 | | } |
| 3177 | | |
| 3178 | 2902 | await refreshPermissions() |
| 3179 | 2903 | onRoute() |
| 3180 | 2904 | } |