script.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. const API_BASE = '/api/notes';
  2. let currentNoteId = null;
  3. let autoSaveTimeout = null;
  4. let lastSavedContent = '';
  5. let currentNoteModifiedAt = null;
  6. let versionCheckInterval = null;
  7. const VERSION_CHECK_INTERVAL_MS = 5000;
  8. function initializeTheme() {
  9. const savedTheme = localStorage.getItem('theme') || 'dark';
  10. const body = document.body;
  11. if (savedTheme === 'light') {
  12. body.setAttribute('data-theme', 'light');
  13. } else {
  14. body.removeAttribute('data-theme');
  15. }
  16. const themeSwitch = document.getElementById('themeSwitch');
  17. if (themeSwitch) {
  18. if (savedTheme === 'light') {
  19. themeSwitch.classList.remove('dark');
  20. } else {
  21. themeSwitch.classList.add('dark');
  22. }
  23. }
  24. updateThemeColor();
  25. }
  26. function updateThemeSwitchState() {
  27. const savedTheme = localStorage.getItem('theme') || 'dark';
  28. const themeSwitch = document.getElementById('themeSwitch');
  29. if (themeSwitch) {
  30. if (savedTheme === 'light') {
  31. themeSwitch.classList.remove('dark');
  32. } else {
  33. themeSwitch.classList.add('dark');
  34. }
  35. }
  36. }
  37. function toggleTheme() {
  38. const body = document.body;
  39. const themeSwitch = document.getElementById('themeSwitch');
  40. if (!themeSwitch) return;
  41. const currentTheme = body.getAttribute('data-theme');
  42. if (currentTheme === 'light') {
  43. body.removeAttribute('data-theme');
  44. themeSwitch.classList.add('dark');
  45. localStorage.setItem('theme', 'dark');
  46. } else {
  47. body.setAttribute('data-theme', 'light');
  48. themeSwitch.classList.remove('dark');
  49. localStorage.setItem('theme', 'light');
  50. }
  51. updateThemeColor();
  52. }
  53. function init() {
  54. initializeTheme();
  55. setupMobileOptimizations();
  56. const pathname = window.location.pathname;
  57. const pathParts = pathname.split('/');
  58. let idFromUrl = null;
  59. for (const part of pathParts) {
  60. if (part && /^[A-Za-z0-9]{26}$/.test(part)) {
  61. idFromUrl = part;
  62. break;
  63. }
  64. }
  65. const noteContent = document.getElementById('noteContent');
  66. if (noteContent) {
  67. if (idFromUrl) {
  68. loadNoteById(idFromUrl);
  69. } else {
  70. newNote();
  71. }
  72. noteContent.addEventListener('input', handleContentChange);
  73. }
  74. const noteIdInput = document.getElementById('noteIdInput');
  75. if (noteIdInput) {
  76. noteIdInput.addEventListener('keypress', function(e) {
  77. if (e.key === 'Enter') {
  78. loadNoteFromInput();
  79. }
  80. });
  81. }
  82. }
  83. function setupMobileOptimizations() {
  84. const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  85. if (isMobile) {
  86. const noteContent = document.getElementById('noteContent');
  87. if (noteContent) {
  88. noteContent.addEventListener('focus', function() {
  89. setTimeout(() => {
  90. this.scrollIntoView({ behavior: 'smooth', block: 'center' });
  91. }, 300);
  92. });
  93. noteContent.addEventListener('touchmove', function(e) {
  94. e.stopPropagation();
  95. }, { passive: true });
  96. }
  97. updateThemeColor();
  98. }
  99. window.addEventListener('orientationchange', function() {
  100. setTimeout(() => {
  101. const vh = window.innerHeight * 0.01;
  102. document.documentElement.style.setProperty('--vh', `${vh}px`);
  103. }, 100);
  104. });
  105. const vh = window.innerHeight * 0.01;
  106. document.documentElement.style.setProperty('--vh', `${vh}px`);
  107. }
  108. function updateThemeColor() {
  109. const themeColorMeta = document.querySelector('meta[name="theme-color"]');
  110. const currentTheme = document.body.getAttribute('data-theme');
  111. if (themeColorMeta) {
  112. if (currentTheme === 'light') {
  113. themeColorMeta.setAttribute('content', '#007bff');
  114. } else {
  115. themeColorMeta.setAttribute('content', '#2d2d2d');
  116. }
  117. }
  118. }
  119. function handleContentChange() {
  120. const noteContent = document.getElementById('noteContent');
  121. if (!noteContent) {
  122. console.error('Element with ID "noteContent" not found');
  123. return;
  124. }
  125. const content = noteContent.value;
  126. if (autoSaveTimeout) {
  127. clearTimeout(autoSaveTimeout);
  128. }
  129. autoSaveTimeout = setTimeout(() => {
  130. autoSave(content);
  131. }, 1000);
  132. }
  133. async function autoSave(content) {
  134. if (content === lastSavedContent) {
  135. return;
  136. }
  137. try {
  138. if (currentNoteId) {
  139. const response = await fetch(`${API_BASE}/${currentNoteId}`, {
  140. method: 'PUT',
  141. headers: {
  142. 'Content-Type': 'application/json',
  143. },
  144. body: JSON.stringify({
  145. content: content
  146. })
  147. });
  148. if (response.ok) {
  149. const updatedNote = await response.json();
  150. setCurrentNoteVersion(updatedNote.modifiedAt);
  151. }
  152. } else {
  153. const response = await fetch(API_BASE, {
  154. method: 'POST',
  155. headers: {
  156. 'Content-Type': 'application/json',
  157. },
  158. body: JSON.stringify({
  159. note: content
  160. })
  161. });
  162. if (response.ok) {
  163. const note = await response.json();
  164. currentNoteId = note.id;
  165. const newUrl = `/${note.id}`;
  166. window.history.replaceState({}, '', newUrl);
  167. showNoteId(note.id);
  168. setCurrentNoteVersion(note.modifiedAt);
  169. }
  170. }
  171. lastSavedContent = content;
  172. } catch (error) {
  173. console.error('Auto-save failed:', error);
  174. }
  175. }
  176. async function loadNoteById(id) {
  177. stopVersionPolling();
  178. try {
  179. const response = await fetch(`${API_BASE}/${id}`);
  180. if (response.ok) {
  181. const note = await response.json();
  182. currentNoteId = note.id;
  183. const noteContent = document.getElementById('noteContent');
  184. if (noteContent) {
  185. noteContent.value = note.content;
  186. lastSavedContent = note.content;
  187. } else {
  188. console.error('Element with ID "noteContent" not found');
  189. }
  190. const newUrl = `/${note.id}`;
  191. window.history.replaceState({}, '', newUrl);
  192. showNoteId(note.id);
  193. setCurrentNoteVersion(note.modifiedAt);
  194. } else {
  195. console.warn(`Note with ID ${id} not found (${response.status}), creating new note`);
  196. await newNote();
  197. }
  198. } catch (error) {
  199. console.error('Failed to load note:', error, 'creating new note instead');
  200. await newNote();
  201. }
  202. }
  203. function showNoteId(id) {
  204. const noteIdDisplay = document.getElementById('noteIdDisplay');
  205. if (noteIdDisplay) {
  206. noteIdDisplay.textContent = id;
  207. noteIdDisplay.style.display = 'inline-block';
  208. } else {
  209. console.error('Element with ID "noteIdDisplay" not found');
  210. }
  211. }
  212. async function copyNoteLink() {
  213. try {
  214. const noteIdDisplay = document.getElementById('noteIdDisplay');
  215. if (!noteIdDisplay) {
  216. console.error('Element with ID "noteIdDisplay" not found');
  217. return;
  218. }
  219. const currentUrl = window.location.href;
  220. await navigator.clipboard.writeText(currentUrl);
  221. const originalText = noteIdDisplay.textContent;
  222. noteIdDisplay.textContent = 'Copied!';
  223. noteIdDisplay.style.color = 'var(--accent-primary)';
  224. setTimeout(() => {
  225. noteIdDisplay.textContent = originalText;
  226. noteIdDisplay.style.color = '';
  227. }, 2000);
  228. } catch (error) {
  229. console.error('Failed to copy note link:', error);
  230. const noteIdDisplay = document.getElementById('noteIdDisplay');
  231. if (noteIdDisplay) {
  232. const originalText = noteIdDisplay.textContent;
  233. noteIdDisplay.textContent = 'Copy failed';
  234. setTimeout(() => {
  235. noteIdDisplay.textContent = originalText;
  236. }, 2000);
  237. }
  238. }
  239. }
  240. async function newNote() {
  241. stopVersionPolling();
  242. try {
  243. const response = await fetch(API_BASE, {
  244. method: 'POST',
  245. headers: {
  246. 'Content-Type': 'application/json',
  247. },
  248. body: JSON.stringify({
  249. note: ''
  250. })
  251. });
  252. if (response.ok) {
  253. const note = await response.json();
  254. currentNoteId = note.id;
  255. lastSavedContent = '';
  256. const noteContent = document.getElementById('noteContent');
  257. if (noteContent) {
  258. noteContent.value = '';
  259. noteContent.focus();
  260. } else {
  261. console.error('Element with ID "noteContent" not found');
  262. }
  263. const newUrl = `/${note.id}`;
  264. window.history.replaceState({}, '', newUrl);
  265. showNoteId(note.id);
  266. setCurrentNoteVersion(note.modifiedAt);
  267. }
  268. } catch (error) {
  269. console.error('Failed to create new note:', error);
  270. }
  271. }
  272. function showIdInput() {
  273. const idInputOverlay = document.getElementById('idInputOverlay');
  274. const noteIdInput = document.getElementById('noteIdInput');
  275. if (idInputOverlay) {
  276. idInputOverlay.classList.remove('hidden');
  277. }
  278. if (noteIdInput) {
  279. noteIdInput.focus();
  280. }
  281. }
  282. function hideIdInput() {
  283. const idInputOverlay = document.getElementById('idInputOverlay');
  284. const noteIdInput = document.getElementById('noteIdInput');
  285. if (idInputOverlay) {
  286. idInputOverlay.classList.add('hidden');
  287. }
  288. if (noteIdInput) {
  289. noteIdInput.value = '';
  290. }
  291. }
  292. function loadNoteFromInput() {
  293. const noteIdInput = document.getElementById('noteIdInput');
  294. if (!noteIdInput) return;
  295. const id = noteIdInput.value.trim();
  296. if (id) {
  297. hideIdInput();
  298. window.location.href = `/${id}`;
  299. }
  300. }
  301. async function checkForNoteUpdates() {
  302. if (!currentNoteId || !currentNoteModifiedAt) {
  303. return;
  304. }
  305. try {
  306. const response = await fetch(`${API_BASE}/${currentNoteId}/metadata`);
  307. if (response.ok) {
  308. const metadata = await response.json();
  309. const serverModifiedAt = new Date(metadata.modifiedAt).getTime();
  310. const localModifiedAt = new Date(currentNoteModifiedAt).getTime();
  311. if (serverModifiedAt > localModifiedAt) {
  312. await autoReloadNote();
  313. }
  314. }
  315. } catch (error) {
  316. console.error('Failed to check for note updates:', error);
  317. }
  318. }
  319. function startVersionPolling() {
  320. stopVersionPolling();
  321. if (currentNoteId) {
  322. versionCheckInterval = setInterval(checkForNoteUpdates, VERSION_CHECK_INTERVAL_MS);
  323. }
  324. }
  325. function stopVersionPolling() {
  326. if (versionCheckInterval) {
  327. clearInterval(versionCheckInterval);
  328. versionCheckInterval = null;
  329. }
  330. }
  331. function setCurrentNoteVersion(modifiedAt) {
  332. currentNoteModifiedAt = modifiedAt;
  333. startVersionPolling();
  334. }
  335. function showUpdateToast() {
  336. const toast = document.getElementById('updateToast');
  337. if (toast) {
  338. toast.classList.remove('hidden');
  339. setTimeout(() => {
  340. toast.classList.add('hidden');
  341. }, 2000);
  342. }
  343. }
  344. async function autoReloadNote() {
  345. if (currentNoteId) {
  346. stopVersionPolling();
  347. await loadNoteById(currentNoteId);
  348. showUpdateToast();
  349. }
  350. }
  351. window.addEventListener('load', init);