const { ipcRenderer, shell } = require('electron'); const axios = require('axios'); const fs = require('fs'); const path = require('path'); const TurndownService = require('turndown'); const { marked } = require('marked'); // Глобальные сервисы const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' }); const rootPath = process.cwd(); const SETTINGS_PATH = path.join(rootPath, 'settings.json'); // Глобальное состояние let currentFilePath = ""; let settings = {}; let translations = {}; let allNotes = []; let isPreviewMode = false; // --- 1. СИСТЕМНЫЕ ФУНКЦИИ (ОКНО) --- window.closeApp = () => ipcRenderer.send('close-app'); window.minimizeApp = () => ipcRenderer.send('minimize-app'); // --- 2. ИНИЦИАЛИЗАЦИЯ --- window.onload = () => { loadSettingsFromFile(); loadLocalization(); applyTheme(settings.theme || 'dark'); loadSettingsIntoUI(); refreshFileList(); initDragAndDrop(); initContextMenu(); initHotkeys(); initScrollSync(); console.log("ObsidianPad Ready"); }; function loadSettingsFromFile() { try { if (fs.existsSync(SETTINGS_PATH)) { settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8')); } else { settings = { theme: "dark", lang: "ru", obsFolder: "", obsApiUrl: "http://127.0.0.1:27123", obsApiKey: "", titleUrl: "", titleKey: "", titleModel: "", transUrl: "", transKey: "", transModel: "" }; fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2)); } } catch (e) { console.error(e); } } function loadLocalization() { try { const langPath = path.join(rootPath, 'lang.json'); if (fs.existsSync(langPath)) { translations = JSON.parse(fs.readFileSync(langPath, 'utf8')); updateUIStrings(); } } catch (e) { console.error(e); } } function updateUIStrings() { const lang = settings.lang || 'ru'; const dict = translations[lang]; if (!dict) return; document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); if (dict[key]) el.innerText = dict[key]; }); } // --- 3. OBSIDIAN API --- async function obsReq(urlPath, method = 'GET', data = null, headers = {}) { if (!settings.obsApiUrl || !settings.obsApiKey) return null; const baseUrl = settings.obsApiUrl.replace(/\/+$/, ''); try { const config = { url: `${baseUrl}${urlPath}`, method, data, headers: { 'Authorization': `Bearer ${settings.obsApiKey.trim()}`, 'Content-Type': 'text/markdown', 'Accept': 'application/json', ...headers }, timeout: 5000 }; const res = await axios(config); return res.data; } catch (e) { console.error("Obsidian API Connection Failed"); return null; } } window.refreshFileList = async () => { let folder = (settings.obsFolder || "").trim().replace(/^\/+|\/+$/g, ''); const data = await obsReq(folder === "" ? "/vault/" : `/vault/${folder}/`); if (data && data.files) { allNotes = data.files.filter(f => f.endsWith('.md') && f !== 'temp.md'); window.filterNotes(document.getElementById('note-search').value || ""); } }; window.filterNotes = (query) => { const select = document.getElementById('file-list'); const folder = (settings.obsFolder || "").trim().replace(/^\/+|\/+$/g, ''); const q = query.toLowerCase(); const lang = settings.lang || 'ru'; select.innerHTML = ``; allNotes.forEach(f => { if (f.toLowerCase().includes(q)) { const opt = document.createElement('option'); opt.value = folder === "" ? f : `${folder}/${f}`; opt.textContent = f.replace('.md', ''); select.appendChild(opt); } }); if (currentFilePath) select.value = currentFilePath; }; window.loadFile = async (path) => { if (!path) return; const content = await obsReq(`/vault/${path}`); if (content !== null) { document.getElementById('editor').value = (typeof content === 'string') ? content : JSON.stringify(content); currentFilePath = path; if (isPreviewMode) window.updatePreview(); window.updateStats(); document.getElementById('status').innerText = "Loaded: " + path.split('/').pop(); } }; window.closeFile = () => { currentFilePath = ""; document.getElementById('editor').value = ""; document.getElementById('file-list').value = ""; if (isPreviewMode) window.togglePreview(); window.updateStats(); document.getElementById('status').innerText = "Ready"; }; // --- 4. СОХРАНЕНИЕ И НОВАЯ ЗАМЕТКА --- window.handleSaveAction = async () => { if (!currentFilePath) window.openNewNoteModal(); else window.saveCurrentFile(); }; window.saveCurrentFile = async () => { if (!currentFilePath) return; const content = document.getElementById('editor').value; const res = await obsReq(`/vault/${currentFilePath}`, 'PUT', content); if (res !== null) { document.getElementById('status').innerText = "Saved: " + new Date().toLocaleTimeString(); if (isPreviewMode) window.updatePreview(); } }; window.openNewNoteModal = () => { const content = document.getElementById('editor').value; if (!content.trim()) return; document.getElementById('new-note-modal').classList.remove('hidden'); document.getElementById('new-note-title').value = ""; document.getElementById('modal-status').innerText = ""; }; window.closeNewNoteModal = () => document.getElementById('new-note-modal').classList.add('hidden'); window.confirmCreateNote = async () => { const title = document.getElementById('new-note-title').value.trim(); if (!title) return; const content = document.getElementById('editor').value; let folder = (settings.obsFolder || "").trim().replace(/^\/+|\/+$/g, ''); const fullPath = (folder ? folder + "/" : "") + title + ".md"; const res = await obsReq(`/vault/${fullPath}`, 'PUT', content); if (res !== null) { currentFilePath = fullPath; await obsReq(`/vault/${folder ? folder + "/" : ""}temp.md`, 'PUT', ""); window.closeNewNoteModal(); await window.refreshFileList(); window.loadFile(fullPath); } }; // --- 5. ИИ ФУНКЦИИ --- async function fetchAIChat(type, system, user) { const url = settings[`${type}Url`]; const key = settings[`${type}Key`]; const model = settings[`${type}Model`]; if (!url || !model) return null; try { const res = await axios.post(`${url.replace(/\/models$/, '').replace(/\/+$/, '')}/chat/completions`, { model, messages: [{role: "system", content: system}, {role: "user", content: user.substring(0, 2000)}] }, { headers: { 'Authorization': `Bearer ${key}` } }); return res.data.choices[0].message.content.trim(); } catch (e) { return null; } } window.generateTitleForModal = async () => { const content = document.getElementById('editor').value; const status = document.getElementById('modal-status'); status.innerText = "AI Generation..."; const title = await fetchAIChat('title', "Create a 2-word title. Return ONLY text.", content); if (title) { document.getElementById('new-note-title').value = title.replace(/[\\/:"*?<>|]/g, "").trim(); status.innerText = ""; } else { status.innerText = "AI Error"; } }; window.generateTitleDirect = async () => { if (!currentFilePath) return; const title = await fetchAIChat('title', "Create 2-word title. Text only.", document.getElementById('editor').value); if (title) { const clean = title.replace(/[\\/:"*?<>|]/g, "").trim(); const folder = currentFilePath.includes('/') ? currentFilePath.substring(0, currentFilePath.lastIndexOf('/') + 1) : ''; const newPath = `${folder}${clean}.md`; if (await obsReq(`/vault/${currentFilePath}`, 'PATCH', null, { 'X-File-Name': newPath })) { currentFilePath = newPath; window.refreshFileList(); } } }; window.translateText = async () => { const content = document.getElementById('editor').value; if (!content) return; document.getElementById('status').innerText = "Translating..."; const res = await fetchAIChat('trans', "Translate to Russian. Keep markdown.", content); if (res) { document.getElementById('editor').value = res; window.saveCurrentFile(); } }; // --- 6. РЕДАКТОР --- window.insertText = (type) => { const el = document.getElementById('editor'); const s = el.selectionStart, e = el.selectionEnd, sel = el.value.substring(s, e); let r = ""; switch(type) { case 'H1': r = `# ${sel}`; break; case 'H2': r = `## ${sel}`; break; case 'H3': r = `### ${sel}`; break; case 'B': r = `**${sel}**`; break; case 'I': r = `*${sel}*`; break; case 'CODE': r = `\`\`\`\n${sel}\n\`\`\``; break; case 'LINK': r = `[${sel}](url)`; break; case 'UL': r = `- ${sel}`; break; } el.setRangeText(r, s, e, 'select'); el.focus(); window.updateStats(); }; window.togglePreview = () => { isPreviewMode = !isPreviewMode; const editor = document.getElementById('editor'); const preview = document.getElementById('preview'); const btn = document.getElementById('preview-toggle'); if (isPreviewMode) { window.updatePreview(); editor.classList.add('hidden'); preview.classList.remove('hidden'); btn.classList.add('active'); } else { editor.classList.remove('hidden'); preview.classList.add('hidden'); btn.classList.remove('active'); } }; window.updatePreview = () => { document.getElementById('preview').innerHTML = marked.parse(document.getElementById('editor').value); }; window.updateStats = () => { const val = document.getElementById('editor').value; const lines = val.split('\n').length; const words = val.trim() ? val.trim().split(/\s+/).length : 0; document.getElementById('stats').innerHTML = `стр: ${lines}   слов: ${words}`; }; // --- 7. НАСТРОЙКИ --- window.toggleSettings = () => document.getElementById('settings').classList.toggle('hidden'); window.applyTheme = (t) => { document.body.className = t === 'light' ? 'light-theme' : 'dark-theme'; }; window.previewTheme = (t) => window.applyTheme(t); window.changeLang = (l) => { settings.lang = l; updateUIStrings(); }; window.saveSettings = () => { settings = { theme: document.getElementById('app-theme').value, lang: document.getElementById('app-lang').value, obsFolder: document.getElementById('obs-folder').value, obsApiUrl: document.getElementById('obs-api-url').value, obsApiKey: document.getElementById('obs-api-key').value, titleUrl: document.getElementById('ai-title-url').value, titleKey: document.getElementById('ai-title-key').value, titleModel: document.getElementById('ai-title-model').value, transUrl: document.getElementById('ai-trans-url').value, transKey: document.getElementById('ai-trans-key').value, transModel: document.getElementById('ai-trans-model').value }; fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2)); window.applyTheme(settings.theme); window.toggleSettings(); window.refreshFileList(); }; function loadSettingsIntoUI() { const s = settings; document.getElementById('app-theme').value = s.theme || "dark"; document.getElementById('app-lang').value = s.lang || "ru"; document.getElementById('obs-folder').value = s.obsFolder || ""; document.getElementById('obs-api-url').value = s.obsApiUrl || ""; document.getElementById('obs-api-key').value = s.obsApiKey || ""; document.getElementById('ai-title-url').value = s.titleUrl || ""; document.getElementById('ai-title-key').value = s.titleKey || ""; document.getElementById('ai-trans-url').value = s.transUrl || ""; document.getElementById('ai-trans-key').value = s.transKey || ""; if (s.titleModel) addOption('ai-title-model', s.titleModel); if (s.transModel) addOption('ai-trans-model', s.transModel); } function addOption(id, val) { const sel = document.getElementById(id); if(sel) sel.innerHTML = ``; } window.fetchModels = async (type) => { let url = document.getElementById(`ai-${type}-url`).value.trim(); const key = document.getElementById(`ai-${type}-key`).value.trim(); if (!url) return; if (!url.endsWith('/models')) url = url.replace(/\/+$/, '') + '/models'; try { const res = await axios.get(url, { headers: { 'Authorization': `Bearer ${key}` } }); const select = document.getElementById(`ai-${type}-model`); select.innerHTML = ""; const models = res.data.data || res.data; if (Array.isArray(models)) { models.forEach(m => { const id = m.id || m.name || m; const opt = document.createElement('option'); opt.value = id; opt.textContent = id; select.appendChild(opt); }); } } catch (e) { alert("AI Models Error"); } }; // --- 8. ВСПОМОГАТЕЛЬНЫЕ --- window.openAbout = () => { try { const aboutPath = path.join(rootPath, 'about.json'); const about = JSON.parse(fs.readFileSync(aboutPath, 'utf8')); document.getElementById('about-name').innerText = about.appName; document.getElementById('about-desc').innerText = about.description; document.getElementById('about-ver').innerText = about.version; document.getElementById('about-auth').innerText = about.author; const link = document.getElementById('about-link'); link.onclick = (e) => { e.preventDefault(); shell.openExternal(about.github); }; document.getElementById('about-modal').classList.remove('hidden'); } catch(e) { alert("about.json error"); } }; window.closeAbout = () => document.getElementById('about-modal').classList.add('hidden'); function initContextMenu() { const menu = document.getElementById('context-menu'); if(!menu) return; window.oncontextmenu = (e) => { e.preventDefault(); menu.style.top = e.pageY+'px'; menu.style.left = e.pageX+'px'; menu.classList.remove('hidden'); }; window.onclick = () => menu.classList.add('hidden'); } function initDragAndDrop() { const ed = document.getElementById('editor'); ed.ondrop = async (e) => { e.preventDefault(); const html = e.dataTransfer.getData('text/html'); if (html) { ed.setRangeText(turndownService.turndown(html), ed.selectionStart, ed.selectionEnd, 'end'); } }; } function initHotkeys() { window.onkeydown = (e) => { if (e.ctrlKey) { if (e.key === 's') { e.preventDefault(); window.handleSaveAction(); } if (e.key === 'n') { e.preventDefault(); window.openNewNoteModal(); } if (e.key === 'w') { e.preventDefault(); window.closeFile(); } if (e.key === 'p') { e.preventDefault(); window.togglePreview(); } } }; } function initScrollSync() { const ed = document.getElementById('editor'); const pr = document.getElementById('preview'); ed.onscroll = () => { if (!isPreviewMode) pr.scrollTop = (ed.scrollTop / (ed.scrollHeight - ed.clientHeight)) * (pr.scrollHeight - pr.clientHeight); }; } window.insertImageClick = () => document.getElementById('image-input').click();