diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..90e4652 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ahmed Moussa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c12a511..1861acd 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,166 @@ # Obsidian Toolkit -Obsidian Toolkit is a collection of practical tools designed to improve the writing and editing experience inside Obsidian. It helps you format documents more efficiently, manage images with ease, and stay focused while working, all without requiring any CSS knowledge. +An all-in-one productivity suite for Obsidian. Seven integrated modules in a single plugin โ€” no configuration required to get started. --- ## Features -### Advanced Formatting +### ๐ŸŽจ Formatting Palette โ€” `Ctrl+Shift+F` +A searchable command palette with 60+ formatting elements organised by category. -Create cleaner and more professional-looking notes with improved styling for headings, tables, code blocks, and other document elements. +- **Insert**: Dates, times, progress bars, AI smart suggestions, AI template generator +- **Callouts**: All 12 built-in Obsidian callouts (note, tip, warning, dangerโ€ฆ) +- **Custom callouts**: Goal, Idea, Task, Formula, Remember, Blocker, Command, Configโ€ฆ +- **Islamic**: Ayah, Hadith, Dua โ€” with RTL and Arabic typography +- **Headings, Text, Lists, Code, Dividers**: Bold, italic, highlight, strikethrough, subscript, superscript, bullet/numbered/task lists, code blocks, math, decorative dividers -3 +Keyboard navigation: `โ†‘โ†“` to move, `Enter` to insert, `Esc` to close. -3 2 +--- -### Image Management +### ๐Ÿค– AI Smart Templates (requires AI Provider) +Two AI-powered features accessible from the Formatting Palette or via commands: -Quickly align images to the left, right, or center and control how they appear within your notes. +**AI Smart Suggestion โ€” `Ctrl+Shift+G`** +Reads the last 12 lines of context (or your selection) and suggests a single continuation โ€” a callout, heading, checklist, or paragraph. -55 +**AI Template Generator โ€” `Ctrl+Shift+T`** +Describe what you want to create (e.g. "weekly meeting notes", "book review", "lesson plan") with optional extra instructions, and the AI generates a complete structured markdown template. +**Compatible providers:** +- **Ollama** (local, free) โ€” auto-fetches your installed models from `/api/tags` +- **OpenAI** (gpt-4o-mini, gpt-4oโ€ฆ) +- **Custom** (any OpenAI-compatible endpoint: OpenRouter, Groq, Anthropic proxyโ€ฆ) -### Focus Mode +Configure in **Settings โ†’ Obsidian Toolkit โ†’ AI Provider**. -Work in a distraction-free environment with a dedicated fullscreen writing mode designed to keep your attention on the content. +--- -1 +### ๐Ÿ–ผ Image Control *(Reading View)* +Click any image to reveal a floating toolbar: +| Button | Action | +|--------|--------| +| `โˆ’` / `+` | Resize by 30px steps | +| `โฌคโ† โฌค โ†’โฌค` | Float left / center / float right | +| `โ†ป` | Rotate 90ยฐ | +| `โœŽ` | Add a caption | +| `โŸฒ` | Reset to original size and position | +| `โŽ˜` | Copy Obsidian markdown link with current width | +| `โคข` | Open in fullscreen lightbox | -### Command Palette Integration +The lightbox supports pan (drag), zoom (scroll wheel), and rotation. -Access all toolkit features directly from Obsidian's Command Palette and assign your preferred keyboard shortcuts. -image +**Special layouts** (add CSS classes to a div wrapping images in markdown): +- `.img-grid-2` / `.img-grid-3` / `.img-grid-4` โ€” photo grid +- `.img-col` / `.img-col-right` โ€” text + image column layout +- `.img-caption` โ€” image with caption below -### Premium File Tree -Apple-style icons, image thumbnails & refined spacing in the file explorer +--- -2 +### โ›ถ Fullscreen Note โ€” `Ctrl+Shift+Z` +Floating fullscreen button on every note. Optionally enters fullscreen automatically after a configurable inactivity period (5โ€“120 seconds), with a 3-second soft-hint and cancel button before switching. -### settings -4 +--- -5 +### ๐ŸŒณ Premium File Tree +Apple-style SF Symbols-inspired icons for every file type in the file explorer. + +| Type | Icon | +|------|------| +| Folder (open/closed) | Custom SVG folder | +| Markdown | Document with M symbol | +| Canvas | Four-grid layout | +| PDF | Document with lines | +| Image | Photo frame with mountain | +| Video | Film frame | +| Audio | Waveform | +| Code | `` | +| Archive | Box | +| Link | Chain link | + +Image files show a real 18ร—18px thumbnail in the tree. Row height is configurable: Compact / Default / Spacious. + +--- + +### โšก Quick Access Bar +A persistent bar of pinned buttons above the file tree. Each button opens a specific note or folder with one click. + +Add items from Settings โ†’ add the current file, the current folder, or any manual path. + +--- + +### ๐Ÿ“ Folder Focus Mode +Right-click any folder โ†’ **"๐Ÿ“ Focus on this folder"** to make it the visual root of the sidebar โ€” all other folders and files disappear, sub-folders start collapsed. + +**Main Folders** (Settings โ†’ Folder Focus Mode): define a list of top-level folders that appear as tabs in a persistent bar above the tree. Click a tab โ†’ instant focus. Click again โ†’ return to full vault view. + +> Note: Search, Graph View, and Quick Switcher (`Ctrl+O`) always cover the entire vault regardless of focus state. --- ## Installation -### Option 1: Install via BRAT (Recommended) +### From Obsidian Community Plugins *(recommended)* +1. Open Obsidian โ†’ Settings โ†’ Community Plugins โ†’ Browse +2. Search for **Obsidian Toolkit** +3. Install โ†’ Enable -1. Install the BRAT plugin in Obsidian. -2. Open BRAT and select **Add Beta Plugin**. -3. Enter the repository: - -```text -ahmdmusa/obsidian-toolkit -``` - -4. Install and enable the plugin. +### Manual installation +1. Download `main.js`, `styles.css`, and `manifest.json` from the [latest release](https://github.com/ahmdmusa/obsidian-toolkit/releases/latest) +2. Copy them to `/.obsidian/plugins/obsidian-toolkit/` +3. Reload Obsidian โ†’ Settings โ†’ Community Plugins โ†’ enable **Obsidian Toolkit** --- -### Option 2: Manual Installation +## Settings overview -1. Download the latest release from the Releases page. -2. Extract the downloaded folder into: +| Section | Key options | +|---------|-------------| +| ๐ŸŽจ Formatting Palette | Enable / disable | +| ๐Ÿ–ผ Image Control | Enable / disable (Reading View only) | +| โ›ถ Fullscreen Note | Enable, auto-fullscreen toggle, inactivity timer | +| ๐Ÿค– AI Provider | Provider dropdown, model selector, API key, response language, system prompt | +| ๐ŸŒณ Premium File Tree | Enable, image thumbnails toggle, row height | +| โšก Quick Access Bar | Enable, manage pinned items | +| ๐Ÿ“ Folder Focus Mode | Enable, manage main folder tabs | -```text -.obsidian/plugins/ +--- + +## Keyboard shortcuts + +| Shortcut | Action | +|----------|--------| +| `Ctrl+Shift+F` | Open Formatting Palette | +| `Ctrl+Shift+Z` | Toggle Fullscreen | +| `Ctrl+Shift+G` | AI Smart Suggestion | +| `Ctrl+Shift+T` | AI Template Generator | + +--- + +## AI Provider setup (Ollama) + +1. Install Ollama: [ollama.com](https://ollama.com) +2. Pull a model: `ollama pull qwen2.5:3b` +3. Run: `ollama serve` +4. In Obsidian settings: Provider = Ollama โ†’ click **Fetch models** โ†’ select your model โ†’ **Test Connection** + +If you get a CORS error, start Ollama with: +```bash +OLLAMA_ORIGINS="app://obsidian.md*" ollama serve ``` -3. Restart Obsidian. -4. Enable **Obsidian Toolkit** from **Settings โ†’ Community Plugins**. +--- + +## Compatibility + +- Obsidian `0.15.0` or later +- Desktop and mobile (Image Control and Fullscreen are desktop-optimised) +- Works with any community theme --- ## License -This project is licensed under the MIT License. - ---- - -# Obsidian Toolkit / ุน - -ู…ุฌู…ูˆุนุฉ ุฃุฏูˆุงุช ุนู…ู„ูŠุฉ ุชู‡ุฏู ุฅู„ู‰ ุชุญุณูŠู† ุชุฌุฑุจุฉ ุงู„ูƒุชุงุจุฉ ูˆุงู„ุชุญุฑูŠุฑ ุฏุงุฎู„ Obsidian. ุชุณุงุนุฏูƒ ุงู„ุฅุถุงูุฉ ุนู„ู‰ ุชู†ุณูŠู‚ ุงู„ู…ุณุชู†ุฏุงุช ุจุดูƒู„ ุฃูุถู„ุŒ ูˆุฅุฏุงุฑุฉ ุงู„ุตูˆุฑ ุจุณู‡ูˆู„ุฉุŒ ูˆุงู„ุญูุงุธ ุนู„ู‰ ุงู„ุชุฑูƒูŠุฒ ุฃุซู†ุงุก ุงู„ุนู…ู„ุŒ ุฏูˆู† ุงู„ุญุงุฌุฉ ุฅู„ู‰ ุฃูŠ ู…ุนุฑูุฉ ู…ุณุจู‚ุฉ ุจู€ CSS. - ---- - -## ุงู„ู…ู…ูŠุฒุงุช - -### ุชู†ุณูŠู‚ ู…ุชู‚ุฏู… ู„ู„ู…ุณุชู†ุฏุงุช - -ุงุญุตู„ ุนู„ู‰ ู…ู„ุงุญุธุงุช ุฃูƒุซุฑ ุงุญุชุฑุงููŠุฉ ูˆุชู†ุธูŠู…ุงู‹ ู…ู† ุฎู„ุงู„ ุชุญุณูŠู† ู…ุธู‡ุฑ ุงู„ุนู†ุงูˆูŠู† ูˆุงู„ุฌุฏุงูˆู„ ูˆูƒุชู„ ุงู„ุฃูƒูˆุงุฏ ูˆุงู„ุนู†ุงุตุฑ ุงู„ู…ุฎุชู„ูุฉ ุฏุงุฎู„ ุงู„ู…ุณุชู†ุฏ. - -### ุฅุฏุงุฑุฉ ุงู„ุตูˆุฑ - -ู‚ู… ุจู…ุญุงุฐุงุฉ ุงู„ุตูˆุฑ ุฅู„ู‰ ุงู„ูŠู…ูŠู† ุฃูˆ ุงู„ูŠุณุงุฑ ุฃูˆ ุงู„ูˆุณุท ุจุณู‡ูˆู„ุฉุŒ ู…ุน ุชุญูƒู… ุฃูุถู„ ููŠ ุทุฑูŠู‚ุฉ ุนุฑุถู‡ุง ุฏุงุฎู„ ุงู„ู…ู„ุงุญุธุงุช. - -### ูˆุถุน ุงู„ุชุฑูƒูŠุฒ - -ุงุณุชู…ุชุน ุจุจูŠุฆุฉ ูƒุชุงุจุฉ ุฎุงู„ูŠุฉ ู…ู† ุงู„ู…ุดุชุชุงุช ุนุจุฑ ูˆุถุน ู…ู„ุก ุงู„ุดุงุดุฉ ุงู„ู…ุตู…ู… ู„ู…ุณุงุนุฏุชูƒ ุนู„ู‰ ุงู„ุชุฑูƒูŠุฒ ุนู„ู‰ ุงู„ู…ุญุชูˆู‰. - -### ุงู„ุชูƒุงู…ู„ ู…ุน ู„ูˆุญุฉ ุงู„ุฃูˆุงู…ุฑ - -ูŠู…ูƒู†ูƒ ุงู„ูˆุตูˆู„ ุฅู„ู‰ ุฌู…ูŠุน ุงู„ุฃุฏูˆุงุช ู…ุจุงุดุฑุฉ ู…ู† Command Palette ูˆุชุฎุตูŠุต ุงุฎุชุตุงุฑุงุช ู„ูˆุญุฉ ุงู„ู…ูุงุชูŠุญ ุญุณุจ ุงุญุชูŠุงุฌุงุชูƒ. - ---- - -## ุงู„ุชุซุจูŠุช - -### ุงู„ุทุฑูŠู‚ุฉ ุงู„ุชุซุจูŠุช ุงู„ูŠุฏูˆูŠ - -1. ู‚ู… ุจุชู†ุฒูŠู„ ุฃุญุฏุซ ุฅุตุฏุงุฑ ู…ู† ุตูุญุฉ Releases. -2. ููƒ ุถุบุท ุงู„ู…ุฌู„ุฏ ุฏุงุฎู„ ุงู„ู…ุณุงุฑ: - -```text -.obsidian/plugins/ -``` - -3. ุฃุนุฏ ุชุดุบูŠู„ Obsidian. -4. ูุนู‘ู„ ุฅุถุงูุฉ **Obsidian Toolkit** ู…ู† **Settings โ†’ Community Plugins**. - ---- - -## ุงู„ุชุฑุฎูŠุต - -ู‡ุฐุง ุงู„ู…ุดุฑูˆุน ูŠุญู…ู„ ุฑุฎุตุฉ MIT. -#### by : ahmedmousa -##### thanks โค๏ธ +MIT โ€” see [LICENSE](LICENSE) diff --git a/main.js b/main.js index 953e4e3..e95a3a8 100644 --- a/main.js +++ b/main.js @@ -1,5 +1,5 @@ 'use strict'; -const { Plugin, PluginSettingTab, Setting, Modal, MarkdownView, Notice } = require('obsidian'); +const { Plugin, PluginSettingTab, Setting, Modal, MarkdownView, Notice, TFile, TFolder } = require('obsidian'); /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• LANGUAGE DETECTION @@ -44,6 +44,52 @@ const TR = { lightbox:'Open in lightbox', copyMd:'Copy Markdown', copied:'โœ… Copied!', progressTitle:'Insert Progress Bar', progressValue:'Value', progressPreview:'Preview', progressInsert:'Insert', progressCancel:'Cancel', + reset:'Reset to original', + aiName:'๐Ÿค– AI Provider (Custom / Ollama)', aiDesc:'Connect any OpenAI-compatible API (OpenRouter, Groq, Ollama, Anthropic proxyโ€ฆ) to power Smart Template suggestions.', + aiUrlName:'API Base URL', aiUrlDesc:'e.g. https://openrouter.ai/api/v1 or http://localhost:11434/v1 for Ollama', + aiKeyName:'API Key', aiKeyDesc:'Leave empty for providers that don\'t require a key (e.g. local Ollama)', + aiModelName:'Model name', aiModelDesc:'e.g. gpt-4o-mini, llama3, anthropic/claude-3.5-sonnet', + aiTest:'Test Connection', aiTestOk:'โœ… Connection successful!', aiTestFail:'โŒ Connection failed: ', + aiOn:'โœ… AI Provider enabled', aiOff:'โ›” AI Provider disabled', + iSmartTemplate:'AI Smart Suggestion', + aiSuggestTitle:'โœจ Smart Template Suggestion', aiThinking:'Thinkingโ€ฆ', + aiNoContext:'Write some text first so the AI has context to work with.', + aiDisabled:'Enable the AI Provider in Settings first.', + aiRegenerate:'Regenerate', aiInsertSuggestion:'Insert', aiCloseModal:'Close', + aiError:'โš ๏ธ Request failed: ', + aiProviderName:'AI Provider', aiProviderDesc:'Choose which AI provider to use', + aiProviderOllama:'Ollama (local)', aiProviderOpenAI:'OpenAI', aiProviderCustom:'Custom (OpenAI-compatible)', + aiModelDropdownName:'Ollama Model', aiModelDropdownDesc:'Select an installed Ollama model. Make sure Ollama is running (ollama serve).', + aiFetchModels:'Fetch models', aiFetchingModels:'Fetchingโ€ฆ', + aiModelsFetchOk:'โœ… Found {n} model(s)', aiModelsFetchFail:'โŒ Could not reach Ollama: ', + aiNoModelsYet:'Click "Fetch models" to load your installed Ollama models', + aiLangName:'Response Language', aiLangDesc:'Language used for AI Smart Template suggestions', + aiLangAuto:'Auto (match your text)', aiLangAr:'Arabic', aiLangEn:'English', + aiPromptName:'Edit System Prompt', aiPromptDesc:'Customize how the AI generates Smart Template suggestions', + aiPromptReset:'Reset to default', + iTemplateGen:'AI Template Generator', + tgTitle:'๐Ÿ“ AI Template Generator', + tgDescLabel:'What do you want to create?', tgDescPlaceholder:'e.g. weekly meeting notes, book review, daily planner, lesson planโ€ฆ', + tgPromptLabel:'Extra instructions (optional)', tgPromptPlaceholder:'e.g. include a callout for action items, keep it short, use Arabic headingsโ€ฆ', + tgGenerate:'Generate', tgRegenerate:'โ†ป Regenerate', tgInsert:'Insert', tgCancel:'Cancel', + tgEmptyDesc:'Describe what kind of template you want first.', + tgThinking:'Generating templateโ€ฆ', + qbName:'โšก Quick Access Bar', qbDesc:'A pinned row of buttons above your file explorer for one-click access to favorite notes and folders.', + qbItemsLabel:'Pinned items', + qbPathPlaceholder:'folder/note.md or folder/subfolder', + qbLabelPlaceholder:'Display name (optional)', + qbAdd:'Add', qbAddCurrent:'+ Add current file', qbAddCurrentFolder:'+ Add current folder', + qbRemove:'Remove', qbEmpty:'No pinned items yet โ€” add some below.', + qbNotFound:'โš ๏ธ Not found in vault: ', qbNoActiveFile:'No active file to add.', + qbInvalidPath:'Enter a path first.', qbDuplicate:'Already pinned.', + ffFocusMenuItem:'๐Ÿ“ Focus on this folder', ffUnfocusMenuItem:'โœ• Exit folder focus', + ffBack:'All files', ffFocusOn:'๐Ÿ“ Focused on: ', ffFocusOff:'Folder focus cleared', + ffName:'๐Ÿ“ Folder Focus Mode', ffDesc:'Right-click any folder โ†’ "Focus on this folder" to make it look like the vault root in the sidebar. (Search, graph, and Quick Switcher still cover the whole vault.)', + mfName:'๐Ÿ“‚ Main Folders (auto-focus)', mfDesc:'Folders in this list jump straight into Focus Mode when clicked โ€” their sub-folders stay collapsed.', + mfPathPlaceholder:'folder/subfolder', mfAddCurrent:'+ Add current folder', + mfAdd:'Add', mfRemove:'Remove', mfEmpty:'No main folders yet โ€” add some below.', + mfDuplicate:'Already a main folder.', mfNoActiveFile:'No active file to detect its folder.', + mfInvalidPath:'Enter a path first.', }, ar: { paletteTitle:'ู„ูˆุญุฉ ุงู„ุชู†ุณูŠู‚', searchPlaceholder:'ุงุจุญุซ ุนู† ุชู†ุณูŠู‚โ€ฆ', noResults:'ู„ุง ุชูˆุฌุฏ ู†ุชุงุฆุฌ', @@ -75,6 +121,52 @@ const TR = { lightbox:'ูุชุญ ููŠ lightbox', copyMd:'ู†ุณุฎ Markdown', copied:'โœ… ุชู… ุงู„ู†ุณุฎ!', progressTitle:'ุฅุฏุฑุงุฌ ุดุฑูŠุท ุชู‚ุฏู…', progressValue:'ุงู„ู†ุณุจุฉ', progressPreview:'ู…ุนุงูŠู†ุฉ', progressInsert:'ุฅุฏุฑุงุฌ', progressCancel:'ุฅู„ุบุงุก', + reset:'ุฅุนุงุฏุฉ ุงู„ุถุจุท ู„ู„ูˆุถุน ุงู„ุฃุตู„ูŠ', + aiName:'๐Ÿค– ู…ุฒูˆู‘ุฏ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ (ู…ุฎุตุต / Ollama)', aiDesc:'ุงุฑุจุท ุฃูŠ API ู…ุชูˆุงูู‚ ู…ุน OpenAI (OpenRouter, Groq, Ollama, Anthropic proxyโ€ฆ) ู„ุชุดุบูŠู„ ุงู‚ุชุฑุงุญุงุช Smart Template.', + aiUrlName:'ุฑุงุจุท API ุงู„ุฃุณุงุณูŠ', aiUrlDesc:'ู…ุซุงู„: https://openrouter.ai/api/v1 ุฃูˆ http://localhost:11434/v1 ู„ู€ Ollama', + aiKeyName:'ู…ูุชุงุญ API', aiKeyDesc:'ุงุชุฑูƒู‡ ูุงุถูŠ ู„ู„ู…ุฒูˆู‘ุฏุงุช ุงู„ุชูŠ ู„ุง ุชุชุทู„ุจ ู…ูุชุงุญ (ู…ุซู„ Ollama ุงู„ู…ุญู„ูŠ)', + aiModelName:'ุงุณู… ุงู„ู…ูˆุฏูŠู„', aiModelDesc:'ู…ุซุงู„: gpt-4o-mini, llama3, anthropic/claude-3.5-sonnet', + aiTest:'ุงุฎุชุจุงุฑ ุงู„ุงุชุตุงู„', aiTestOk:'โœ… ุงู„ุงุชุตุงู„ ู†ุงุฌุญ!', aiTestFail:'โŒ ูุดู„ ุงู„ุงุชุตุงู„: ', + aiOn:'โœ… ู…ุฒูˆู‘ุฏ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ู…ูุนู‘ู„', aiOff:'โ›” ู…ุฒูˆู‘ุฏ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ู…ุนุทู‘ู„', + iSmartTemplate:'ุงู‚ุชุฑุงุญ ุฐูƒูŠ (AI)', + aiSuggestTitle:'โœจ ุงู‚ุชุฑุงุญ Smart Template', aiThinking:'ุฌุงุฑูŠ ุงู„ุชููƒูŠุฑโ€ฆ', + aiNoContext:'ุงูƒุชุจ ุจุนุถ ุงู„ู†ุต ุฃูˆู„ุงู‹ ุญุชู‰ ูŠูƒูˆู† ู„ู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ุณูŠุงู‚ ู„ู„ุนู…ู„ ุนู„ูŠู‡.', + aiDisabled:'ูุนู‘ู„ ู…ุฒูˆู‘ุฏ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ู…ู† ุงู„ุฅุนุฏุงุฏุงุช ุฃูˆู„ุงู‹.', + aiRegenerate:'ุฅุนุงุฏุฉ ุงู„ู…ุญุงูˆู„ุฉ', aiInsertSuggestion:'ุฅุฏุฑุงุฌ', aiCloseModal:'ุฅุบู„ุงู‚', + aiError:'โš ๏ธ ูุดู„ ุงู„ุทู„ุจ: ', + aiProviderName:'ู…ุฒูˆู‘ุฏ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ', aiProviderDesc:'ุงุฎุชุฑ ู…ุฒูˆู‘ุฏ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ุงู„ู…ุณุชุฎุฏู…', + aiProviderOllama:'Ollama (ู…ุญู„ูŠ)', aiProviderOpenAI:'OpenAI', aiProviderCustom:'ู…ุฎุตุต (ู…ุชูˆุงูู‚ ู…ุน OpenAI)', + aiModelDropdownName:'ู…ูˆุฏูŠู„ Ollama', aiModelDropdownDesc:'ุงุฎุชุฑ ู…ูˆุฏูŠู„ ู…ุซุจู‘ุช ุนู„ู‰ Ollama. ุชุฃูƒุฏ ุฅู† Ollama ุดุบู‘ุงู„ (ollama serve).', + aiFetchModels:'ุฌู„ุจ ุงู„ู…ูˆุฏูŠู„ุงุช', aiFetchingModels:'ุฌุงุฑูŠ ุงู„ุฌู„ุจโ€ฆ', + aiModelsFetchOk:'โœ… ุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ {n} ู…ูˆุฏูŠู„', aiModelsFetchFail:'โŒ ุชุนุฐู‘ุฑ ุงู„ูˆุตูˆู„ ู„ู€ Ollama: ', + aiNoModelsYet:'ุงุถุบุท "ุฌู„ุจ ุงู„ู…ูˆุฏูŠู„ุงุช" ู„ุชุญู…ูŠู„ ุงู„ู…ูˆุฏูŠู„ุงุช ุงู„ู…ุซุจุชุฉ ุนู„ู‰ Ollama', + aiLangName:'ู„ุบุฉ ุงู„ุฑุฏ', aiLangDesc:'ุงู„ู„ุบุฉ ุงู„ู…ุณุชุฎุฏู…ุฉ ููŠ ุงู‚ุชุฑุงุญุงุช Smart Template', + aiLangAuto:'ุชู„ู‚ุงุฆูŠ (ุญุณุจ ู†ุตูƒ)', aiLangAr:'ุงู„ุนุฑุจูŠุฉ', aiLangEn:'ุงู„ุฅู†ุฌู„ูŠุฒูŠุฉ', + aiPromptName:'ุชุนุฏูŠู„ System Prompt', aiPromptDesc:'ุชุญูƒู… ููŠ ุทุฑูŠู‚ุฉ ุชูˆู„ูŠุฏ ุงู‚ุชุฑุงุญุงุช Smart Template', + aiPromptReset:'ุฅุนุงุฏุฉ ุงู„ุถุจุท ู„ู„ูˆุถุน ุงู„ุงูุชุฑุงุถูŠ', + iTemplateGen:'ู…ูˆู„ู‘ุฏ ู‚ูˆุงู„ุจ (AI)', + tgTitle:'๐Ÿ“ ู…ูˆู„ู‘ุฏ ุงู„ู‚ูˆุงู„ุจ ุจุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ', + tgDescLabel:'ุนุงูŠุฒ ุชุนู…ู„ ุฅูŠู‡ุŸ', tgDescPlaceholder:'ู…ุซุงู„: ู…ุญุถุฑ ุงุฌุชู…ุงุน ุฃุณุจูˆุนูŠุŒ ู…ุฑุงุฌุนุฉ ูƒุชุงุจุŒ ุฎุทุฉ ูŠูˆู…ูŠุฉุŒ ุฎุทุฉ ุฏุฑุณโ€ฆ', + tgPromptLabel:'ุชุนู„ูŠู…ุงุช ุฅุถุงููŠุฉ (ุงุฎุชูŠุงุฑูŠ)', tgPromptPlaceholder:'ู…ุซุงู„: ุถูŠู callout ู„ู€ action itemsุŒ ุฎู„ูŠู‡ ู…ุฎุชุตุฑุŒ ุงุณุชุฎุฏู… ุนู†ุงูˆูŠู† ุนุฑุจูŠุฉโ€ฆ', + tgGenerate:'ุชูˆู„ูŠุฏ', tgRegenerate:'โ†ป ุฅุนุงุฏุฉ ุงู„ู…ุญุงูˆู„ุฉ', tgInsert:'ุฅุฏุฑุงุฌ', tgCancel:'ุฅู„ุบุงุก', + tgEmptyDesc:'ุงูƒุชุจ ูˆุตู ู„ู†ูˆุน ุงู„ู‚ุงู„ุจ ุงู„ู„ูŠ ุนุงูŠุฒู‡ ุฃูˆู„ุงู‹.', + tgThinking:'ุฌุงุฑูŠ ุชูˆู„ูŠุฏ ุงู„ู‚ุงู„ุจโ€ฆ', + qbName:'โšก ุดุฑูŠุท ุงู„ูˆุตูˆู„ ุงู„ุณุฑูŠุน', qbDesc:'ุดุฑูŠุท ุฃุฒุฑุงุฑ ุซุงุจุช ููˆู‚ ุดุฌุฑุฉ ุงู„ู…ู„ูุงุช ู„ู„ูˆุตูˆู„ ุจุถุบุทุฉ ูˆุงุญุฏุฉ ู„ู…ู„ูุงุชูƒ ูˆููˆู„ุฏุฑุงุชูƒ ุงู„ู…ูุถู„ุฉ.', + qbItemsLabel:'ุงู„ุนู†ุงุตุฑ ุงู„ู…ุซุจู‘ุชุฉ', + qbPathPlaceholder:'folder/note.md ุฃูˆ folder/subfolder', + qbLabelPlaceholder:'ุงู„ุงุณู… ุงู„ู…ุนุฑูˆุถ (ุงุฎุชูŠุงุฑูŠ)', + qbAdd:'ุฅุถุงูุฉ', qbAddCurrent:'+ ุฅุถุงูุฉ ุงู„ู…ู„ู ุงู„ุญุงู„ูŠ', qbAddCurrentFolder:'+ ุฅุถุงูุฉ ุงู„ููˆู„ุฏุฑ ุงู„ุญุงู„ูŠ', + qbRemove:'ุญุฐู', qbEmpty:'ู„ุง ุชูˆุฌุฏ ุนู†ุงุตุฑ ู…ุซุจู‘ุชุฉ โ€” ุฃุถู ุนู†ุงุตุฑ ู…ู† ุงู„ุฃุณูู„.', + qbNotFound:'โš ๏ธ ุบูŠุฑ ู…ูˆุฌูˆุฏ ููŠ ุงู„ู€ vault: ', qbNoActiveFile:'ู„ุง ูŠูˆุฌุฏ ู…ู„ู ู…ูุชูˆุญ ู„ุฅุถุงูุชู‡.', + qbInvalidPath:'ุงูƒุชุจ ุงู„ู…ุณุงุฑ ุฃูˆู„ุงู‹.', qbDuplicate:'ุงู„ุนู†ุตุฑ ู…ุซุจู‘ุช ู…ุณุจู‚ุงู‹.', + ffFocusMenuItem:'๐Ÿ“ ุฑูƒู‘ุฒ ุนู„ู‰ ู‡ุฐุง ุงู„ููˆู„ุฏุฑ', ffUnfocusMenuItem:'โœ• ุฅู„ุบุงุก ุงู„ุชุฑูƒูŠุฒ', + ffBack:'ูƒู„ ุงู„ู…ู„ูุงุช', ffFocusOn:'๐Ÿ“ ู…ูุฑูƒูŽู‘ุฒ ุนู„ู‰: ', ffFocusOff:'ุชู… ุฅู„ุบุงุก ุงู„ุชุฑูƒูŠุฒ', + ffName:'๐Ÿ“ ูˆุถุน ุงู„ุชุฑูƒูŠุฒ ุนู„ู‰ ููˆู„ุฏุฑ', ffDesc:'ูƒู„ูŠูƒ ูŠู…ูŠู† ุนู„ู‰ ุฃูŠ ููˆู„ุฏุฑ โ†’ "ุฑูƒู‘ุฒ ุนู„ู‰ ู‡ุฐุง ุงู„ููˆู„ุฏุฑ" ูŠุฎู„ูŠู‡ ูŠุจุงู† ูƒุฃู†ู‡ ุฌุฐุฑ ุงู„ู€ vault ููŠ ุงู„ุดุฑูŠุท ุงู„ุฌุงู†ุจูŠ. (ุงู„ุจุญุซุŒ ุงู„ู€ graphุŒ ูˆ Quick Switcher ุจูŠูุถู„ูˆุง ูŠุดู…ู„ูˆุง ุงู„ู€ vault ูƒุงู…ู„.)', + mfName:'๐Ÿ“‚ ุงู„ููˆู„ุฏุฑุงุช ุงู„ุฑุฆูŠุณูŠุฉ (ุชุฑูƒูŠุฒ ุชู„ู‚ุงุฆูŠ)', mfDesc:'ุงู„ููˆู„ุฏุฑุงุช ููŠ ู‡ุฐู‡ ุงู„ู‚ุงุฆู…ุฉ ุชุฏุฎู„ ููŠ Focus Mode ููˆุฑุงู‹ ุนู†ุฏ ุงู„ุถุบุท ุนู„ูŠู‡ุง โ€” ูˆุงู„ููˆู„ุฏุฑุงุช ุงู„ูุฑุนูŠุฉ ุจุฏุงุฎู„ู‡ุง ุชูุถู„ ู…ู‚ููˆู„ุฉ.', + mfPathPlaceholder:'folder/subfolder', mfAddCurrent:'+ ุฅุถุงูุฉ ุงู„ููˆู„ุฏุฑ ุงู„ุญุงู„ูŠ', + mfAdd:'ุฅุถุงูุฉ', mfRemove:'ุญุฐู', mfEmpty:'ู„ุง ุชูˆุฌุฏ ููˆู„ุฏุฑุงุช ุฑุฆูŠุณูŠุฉ โ€” ุฃุถู ุนู†ุงุตุฑ ู…ู† ุงู„ุฃุณูู„.', + mfDuplicate:'ู…ูˆุฌูˆุฏ ู…ุณุจู‚ุงู‹ ููŠ ุงู„ู‚ุงุฆู…ุฉ.', mfNoActiveFile:'ู„ุง ูŠูˆุฌุฏ ู…ู„ู ู…ูุชูˆุญ ู„ู…ุนุฑูุฉ ููˆู„ุฏุฑู‡.', + mfInvalidPath:'ุงูƒุชุจ ุงู„ู…ุณุงุฑ ุฃูˆู„ุงู‹.', } }; const t = (k) => { const l = getAppLang(); return TR[l]?.[k] ?? TR.en[k] ?? k; }; @@ -97,6 +189,22 @@ const DEFAULT_SETTINGS = { enableFileTree: true, fileTreeThumbnails: true, fileTreeRowHeight: 'default', + /* โ”€โ”€ AI / Smart Template Engine โ”€โ”€ */ + enableAI: false, + aiProvider: 'ollama', // 'ollama' | 'openai' | 'custom' + aiApiUrl: 'http://localhost:11434/v1', + aiApiKey: '', + aiModel: 'qwen2.5:3b', + aiResponseLang: 'auto', // 'auto' | 'ar' | 'en' + aiSystemPrompt: '', // empty = use built-in default + aiOllamaModels: [], // cached list from /api/tags + /* โ”€โ”€ Quick Access Bar โ”€โ”€ */ + enableQuickBar: true, + quickBarItems: [], // [{ id, path, label }] + /* โ”€โ”€ Folder Focus Mode โ”€โ”€ */ + enableFolderFocus: true, + focusedFolder: null, // path of folder currently treated as vault root + mainFolders: [], // paths that auto-enter Focus Mode on click }; /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -137,8 +245,263 @@ function getFileType(name) { } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - PROGRESS BAR MODAL + AI PROVIDER PRESETS โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +const AI_PROVIDER_PRESETS = { + ollama: { url: 'http://localhost:11434/v1', model: 'qwen2.5:3b' }, + openai: { url: 'https://api.openai.com/v1', model: 'gpt-4o-mini' }, + custom: { url: '', model: '' }, +}; + +/* Convert an OpenAI-compatible base URL (โ€ฆ/v1) to the Ollama root for /api/tags */ +function ollamaTagsUrl(aiApiUrl) { + const base = (aiApiUrl || AI_PROVIDER_PRESETS.ollama.url).replace(/\/+$/, ''); + const root = base.replace(/\/v1$/, ''); + return `${root}/api/tags`; +} + +async function fetchOllamaModels(aiApiUrl) { + const res = await fetch(ollamaTagsUrl(aiApiUrl)); + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + const data = await res.json(); + return (data?.models || []).map(m => m.name).filter(Boolean); +} + +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + AI CLIENT (OpenAI-compatible Custom API / Ollama) +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +async function callAI(plugin, messages, opts = {}) { + const { aiApiUrl, aiApiKey, aiModel } = plugin.settings; + const base = (aiApiUrl || '').replace(/\/+$/, ''); + const url = `${base}/chat/completions`; + + const headers = { 'Content-Type': 'application/json' }; + if (aiApiKey) headers['Authorization'] = `Bearer ${aiApiKey}`; + + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ + model: aiModel || 'gpt-4o-mini', + messages, + temperature: opts.temperature ?? 0.6, + max_tokens: opts.maxTokens ?? 400, + }), + }); + + if (!res.ok) { + const body = await res.text().catch(() => ''); + throw new Error(`HTTP ${res.status} ${res.statusText} ${body.slice(0, 180)}`); + } + + const data = await res.json(); + const content = data?.choices?.[0]?.message?.content; + if (!content) throw new Error('Empty response from model'); + return content.trim(); +} + +const DEFAULT_SMART_TEMPLATE_PROMPT = + 'You are a writing assistant embedded in Obsidian (a markdown note-taking app). ' + + 'Given the context the user is currently writing, suggest ONE short, useful continuation: ' + + 'this can be a relevant Obsidian callout block (e.g. "> [!tip]"), a heading, a checklist, ' + + 'a table skeleton, or a short paragraph continuation โ€” whatever best fits the context. ' + + 'Reply with ONLY the raw markdown snippet to insert, no explanations, no code fences, no extra commentary. ' + + 'Match the language of the provided context (Arabic or English).'; + +function buildSystemPrompt(plugin) { + let prompt = (plugin.settings.aiSystemPrompt || '').trim() || DEFAULT_SMART_TEMPLATE_PROMPT; + const lang = plugin.settings.aiResponseLang; + if (lang === 'ar') prompt += ' Always reply in Arabic, regardless of the input language.'; + else if (lang === 'en') prompt += ' Always reply in English, regardless of the input language.'; + return prompt; +} + +/* strip accidental ```fences``` some local models add despite instructions */ +function stripCodeFences(text) { + const trimmed = text.trim(); + const fenceMatch = trimmed.match(/^```[a-zA-Z]*\n([\s\S]*?)\n```$/); + return fenceMatch ? fenceMatch[1].trim() : trimmed; +} + +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + SMART TEMPLATE SUGGESTION MODAL +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +class AISuggestModal extends Modal { + constructor(app, plugin, editor, context) { + super(app); + this.plugin = plugin; this.editor = editor; this.context = context; + this.suggestion = ''; + } + + onOpen() { + const { contentEl, modalEl } = this; + contentEl.empty(); + modalEl.addClass('ai-modal-root'); + contentEl.addClass('ai-modal'); + + contentEl.createEl('div', { text: t('aiSuggestTitle'), cls: 'ai-modal-title' }); + this.bodyEl = contentEl.createDiv('ai-modal-body'); + + this.actionsEl = contentEl.createDiv('ai-modal-actions'); + this.regenBtn = this.actionsEl.createEl('button', { text: 'โ†ป ' + t('aiRegenerate'), cls:'progress-btn progress-btn-cancel' }); + this.insertBtn = this.actionsEl.createEl('button', { text: t('aiInsertSuggestion'), cls:'progress-btn progress-btn-insert' }); + this.closeBtn = this.actionsEl.createEl('button', { text: t('aiCloseModal'), cls:'progress-btn progress-btn-cancel' }); + + this.regenBtn.style.display = 'none'; + this.insertBtn.style.display = 'none'; + + this.regenBtn.addEventListener('click', () => this.fetchSuggestion()); + this.insertBtn.addEventListener('click', () => { + if (this.suggestion) this.editor.replaceSelection(this.suggestion); + this.close(); + }); + this.closeBtn.addEventListener('click', () => this.close()); + + this.fetchSuggestion(); + } + + setLoading() { + this.bodyEl.empty(); + const loading = this.bodyEl.createDiv('ai-loading'); + loading.createDiv('ai-spinner'); + loading.createEl('span', { text: t('aiThinking') }); + this.regenBtn.style.display = 'none'; + this.insertBtn.style.display = 'none'; + } + + async fetchSuggestion() { + this.setLoading(); + try { + const messages = [ + { role: 'system', content: buildSystemPrompt(this.plugin) }, + { role: 'user', content: this.context }, + ]; + const result = stripCodeFences(await callAI(this.plugin, messages)); + this.suggestion = result; + this.bodyEl.empty(); + const pre = this.bodyEl.createEl('pre', { cls: 'ai-suggestion-box' }); + pre.textContent = result; + this.regenBtn.style.display = ''; + this.insertBtn.style.display = ''; + } catch (err) { + this.bodyEl.empty(); + this.bodyEl.createEl('div', { text: t('aiError') + err.message, cls: 'ai-error-box' }); + this.regenBtn.style.display = ''; + } + } + + onClose() { this.contentEl.empty(); } +} + +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + TEMPLATE GENERATOR (full multi-section templates) +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +const DEFAULT_TEMPLATE_GEN_PROMPT = + 'You are a template-generation assistant for Obsidian (a markdown note-taking app). ' + + 'The user will describe what kind of note or template they want to create. ' + + 'Generate a COMPLETE, well-structured markdown template: use headings (##, ###), ' + + 'Obsidian callouts ("> [!type]") where relevant, checklists, tables, and clear ' + + 'placeholders (e.g. "...") for the user to fill in. ' + + 'Reply with ONLY the raw markdown template โ€” no explanations, no code fences, no extra commentary. ' + + 'Match the language of the user\'s description (Arabic or English) unless told otherwise.'; + +function buildTemplateSystemPrompt(plugin) { + let prompt = DEFAULT_TEMPLATE_GEN_PROMPT; + const lang = plugin.settings.aiResponseLang; + if (lang === 'ar') prompt += ' Always write the template in Arabic, regardless of the input language.'; + else if (lang === 'en') prompt += ' Always write the template in English, regardless of the input language.'; + return prompt; +} + +class TemplateGeneratorModal extends Modal { + constructor(app, plugin, editor) { + super(app); + this.plugin = plugin; this.editor = editor; + this.result = ''; + } + + onOpen() { + const { contentEl, modalEl } = this; + contentEl.empty(); + modalEl.addClass('ai-modal-root'); + contentEl.addClass('ai-modal'); + + contentEl.createEl('div', { text: t('tgTitle'), cls: 'ai-modal-title' }); + + contentEl.createEl('div', { text: t('tgDescLabel'), cls: 'progress-label' }); + this.descEl = contentEl.createEl('textarea', { cls: 'ai-textarea', placeholder: t('tgDescPlaceholder') }); + this.descEl.rows = 2; + + contentEl.createEl('div', { text: t('tgPromptLabel'), cls: 'progress-label ai-label-spaced' }); + this.extraEl = contentEl.createEl('textarea', { cls: 'ai-textarea', placeholder: t('tgPromptPlaceholder') }); + this.extraEl.rows = 2; + + this.bodyEl = contentEl.createDiv('ai-modal-body'); + + this.actionsEl = contentEl.createDiv('ai-modal-actions'); + this.generateBtn = this.actionsEl.createEl('button', { text: t('tgGenerate'), cls:'progress-btn progress-btn-insert' }); + this.regenBtn = this.actionsEl.createEl('button', { text: t('tgRegenerate'), cls:'progress-btn progress-btn-cancel' }); + this.insertBtn = this.actionsEl.createEl('button', { text: t('tgInsert'), cls:'progress-btn progress-btn-insert' }); + this.cancelBtn = this.actionsEl.createEl('button', { text: t('tgCancel'), cls:'progress-btn progress-btn-cancel' }); + + this.regenBtn.style.display = 'none'; + this.insertBtn.style.display = 'none'; + + this.generateBtn.addEventListener('click', () => this.generate()); + this.regenBtn.addEventListener('click', () => this.generate()); + this.insertBtn.addEventListener('click', () => { + if (this.result) this.editor.replaceSelection(this.result); + this.close(); + }); + this.cancelBtn.addEventListener('click', () => this.close()); + + setTimeout(() => this.descEl.focus(), 30); + } + + setLoading() { + this.bodyEl.empty(); + const loading = this.bodyEl.createDiv('ai-loading'); + loading.createDiv('ai-spinner'); + loading.createEl('span', { text: t('tgThinking') }); + this.generateBtn.style.display = 'none'; + this.regenBtn.style.display = 'none'; + this.insertBtn.style.display = 'none'; + } + + async generate() { + const description = this.descEl.value.trim(); + if (!description) { new Notice(t('tgEmptyDesc')); return; } + const extra = this.extraEl.value.trim(); + + this.setLoading(); + try { + let userMsg = description; + if (extra) userMsg += `\n\nAdditional instructions: ${extra}`; + + const messages = [ + { role: 'system', content: buildTemplateSystemPrompt(this.plugin) }, + { role: 'user', content: userMsg }, + ]; + const result = stripCodeFences(await callAI(this.plugin, messages, { maxTokens: 900 })); + this.result = result; + this.bodyEl.empty(); + const pre = this.bodyEl.createEl('pre', { cls: 'ai-suggestion-box' }); + pre.textContent = result; + this.regenBtn.style.display = ''; + this.insertBtn.style.display = ''; + } catch (err) { + this.bodyEl.empty(); + this.bodyEl.createEl('div', { text: t('aiError') + err.message, cls: 'ai-error-box' }); + this.regenBtn.style.display = ''; + } finally { + this.generateBtn.style.display = 'none'; + } + } + + onClose() { this.contentEl.empty(); } +} + + function makeProgressBar(pct) { const filled = Math.round(pct / 10); return `[${'โ–ˆ'.repeat(filled)}${'โ–‘'.repeat(10 - filled)}] ${pct}%`; @@ -199,7 +562,7 @@ class ProgressInputModal extends Modal { /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• FORMATTING ITEMS โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ -function buildItems(editor, app) { +function buildItems(editor, app, plugin) { const insertCallout = (type) => { const body = editor.getSelection() || 'โ€ฆ'; const lines = body.split('\n').map(l => `> ${l}`).join('\n'); @@ -221,8 +584,28 @@ function buildItems(editor, app) { editor.replaceSelection(text); }; + const openSmartSuggestion = () => { + if (!plugin?.settings?.enableAI) { new Notice(t('aiDisabled')); return; } + const cursor = editor.getCursor(); + const selection = editor.getSelection(); + let context = selection; + if (!context) { + const fromLine = Math.max(0, cursor.line - 12); + context = editor.getRange({ line: fromLine, ch: 0 }, cursor); + } + if (!context || !context.trim()) { new Notice(t('aiNoContext')); return; } + new AISuggestModal(app, plugin, editor, context).open(); + }; + + const openTemplateGenerator = () => { + if (!plugin?.settings?.enableAI) { new Notice(t('aiDisabled')); return; } + new TemplateGeneratorModal(app, plugin, editor).open(); + }; + return [ /* โ”€โ”€ INSERT โ”€โ”€ */ + { id:'i-template', label:t('iTemplateGen'), icon:'๐Ÿ“', cat:'Insert', color:'#0ea5e9', mdpreview:'AI: ## โ€ฆ > [!tip] โ€ฆ', action:openTemplateGenerator }, + { id:'i-smart', label:t('iSmartTemplate'), icon:'โœจ', cat:'Insert', color:'#9333ea', mdpreview:'AI: > [!tip] โ€ฆ', action:openSmartSuggestion }, { id:'i-date', label:t('iDateLocale'), icon:'๐Ÿ“…', cat:'Insert', color:'#0f766e', mdpreview:'June 11, 2025', action:()=>insertDateTime('date') }, { id:'i-date-iso', label:t('iDateIso'), icon:'๐Ÿ—“๏ธ', cat:'Insert', color:'#0891b2', mdpreview:'2025-06-11', action:()=>insertDateTime('date-iso') }, { id:'i-time', label:t('iTimeLocale'), icon:'๐Ÿ•', cat:'Insert', color:'#7c3aed', mdpreview:'2:30 PM', action:()=>insertDateTime('time') }, @@ -303,10 +686,10 @@ function buildItems(editor, app) { FORMATTING PALETTE MODAL โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ class FormattingPaletteModal extends Modal { - constructor(app, editor) { super(app); this.editor = editor; this.allItems = []; this.filtered = []; this.activeIdx = 0; this.cardEls = []; } + constructor(app, editor, plugin) { super(app); this.editor = editor; this.plugin = plugin; this.allItems = []; this.filtered = []; this.activeIdx = 0; this.cardEls = []; } onOpen() { - this.allItems = buildItems(this.editor, this.app); + this.allItems = buildItems(this.editor, this.app, this.plugin); this.filtered = [...this.allItems]; const { contentEl, modalEl } = this; contentEl.empty(); modalEl.addClass('fp-modal-root'); contentEl.addClass('fp-palette'); @@ -441,7 +824,8 @@ class ImageControlManager { wrapImage(img) { const src = img.src; - let width = img.naturalWidth > 0 ? Math.min(img.naturalWidth, 400) : 360; + const originalWidth = img.naturalWidth > 0 ? Math.min(img.naturalWidth, 400) : 360; + let width = originalWidth; let align = 'center', rotate = 0; const outer = document.createElement('div'); outer.className = 'ic-block-center'; @@ -484,6 +868,12 @@ class ImageControlManager { let captionVisible = false; tb.appendChild(mkBtn('โœŽ', t('caption'), () => { captionVisible=!captionVisible; captIn.classList.toggle('visible',captionVisible); if(captionVisible) setTimeout(()=>captIn.focus(),50); })); tb.appendChild(sep()); + tb.appendChild(mkBtn('โŸฒ', t('reset'), () => { + width = originalWidth; align = 'center'; rotate = 0; + applySize(); applyAlign(); + img.style.transform = ''; + })); + tb.appendChild(sep()); tb.appendChild(mkBtn('โŽ˜', t('copyMd'), () => { navigator.clipboard.writeText(`![[${img.src}|${width}]]`).then(()=>new Notice(t('copied'))); })); tb.appendChild(mkBtn('โคข', t('lightbox'), () => this.openLightbox(src))); @@ -592,12 +982,17 @@ class FullscreenManager { PREMIUM FILE TREE MANAGER โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ class FileTreeManager { - constructor(plugin) { this.plugin=plugin; this._obs=null; } + constructor(plugin) { this.plugin=plugin; this._obs=null; this._rafId=null; this._imageMap=null; } register() { document.body.classList.add('ft-active', `ft-height-${this.plugin.settings.fileTreeRowHeight}`); - this._obs = new MutationObserver(()=>this.processTree()); + this._buildImageMap(); + this.plugin.registerEvent(this.plugin.app.vault.on('create', () => this._buildImageMap())); + this.plugin.registerEvent(this.plugin.app.vault.on('delete', () => this._buildImageMap())); + this.plugin.registerEvent(this.plugin.app.vault.on('rename', () => this._buildImageMap())); + + this._obs = new MutationObserver(()=>this.scheduleProcessTree()); const attach = () => { const nav = document.querySelector('.nav-files-container'); @@ -609,18 +1004,39 @@ class FileTreeManager { }; this.plugin.registerEvent(this.plugin.app.workspace.on('layout-change', attach)); - this.plugin.registerEvent(this.plugin.app.workspace.on('file-open', ()=>this.processTree())); + this.plugin.registerEvent(this.plugin.app.workspace.on('file-open', ()=>this.scheduleProcessTree())); attach(); } + /* Build a cached filename โ†’ resource-path lookup once, instead of + calling vault.getFiles() on every single row render. */ + _buildImageMap() { + this._imageMap = new Map(); + for (const file of this.plugin.app.vault.getFiles()) { + if (getFileType(file.name) === 'image') this._imageMap.set(file.name, file.path); + } + } + + /* Coalesce bursts of MutationObserver events (e.g. during file-tree + scroll/virtualization) into a single pass per animation frame. */ + scheduleProcessTree() { + if (this._rafId != null) return; + this._rafId = requestAnimationFrame(() => { + this._rafId = null; + this.processTree(); + }); + } + processTree() { + const nav = document.querySelector('.nav-files-container'); + if (!nav) return; /* folders */ - document.querySelectorAll('.nav-folder-title:not([data-ft])').forEach(el=>{ + nav.querySelectorAll('.nav-folder-title:not([data-ft])').forEach(el=>{ el.setAttribute('data-ft','1'); this._enhanceFolder(el); }); /* files */ - document.querySelectorAll('.nav-file-title:not([data-ft])').forEach(el=>{ + nav.querySelectorAll('.nav-file-title:not([data-ft])').forEach(el=>{ el.setAttribute('data-ft','1'); this._enhanceFile(el); }); @@ -652,15 +1068,16 @@ class FileTreeManager { icon.innerHTML = FT_ICONS[type] || FT_ICONS.text; titleEl.prepend(icon); - /* image thumbnail */ + /* image thumbnail โ€” uses the cached map, no full vault scan */ if (type === 'image' && this.plugin.settings.fileTreeThumbnails) { - const file = this.plugin.app.vault.getFiles().find(f => f.name === filename); - if (file) { - const src = this.plugin.app.vault.adapter.getResourcePath(file.path); - const img = document.createElement('img'); + const path = this._imageMap?.get(filename); + if (path) { + const src = this.plugin.app.vault.adapter.getResourcePath(path); + const img = document.createElement('img'); img.className = 'ft-thumb'; img.src = src; img.alt = ''; + img.loading = 'lazy'; img.onload = () => { icon.innerHTML=''; icon.appendChild(img); icon.classList.add('ft-has-thumb'); }; } } @@ -673,6 +1090,7 @@ class FileTreeManager { destroy() { this._obs?.disconnect(); + if (this._rafId != null) { cancelAnimationFrame(this._rafId); this._rafId = null; } document.body.classList.remove('ft-active','ft-height-compact','ft-height-default','ft-height-spacious'); document.querySelectorAll('[data-ft]').forEach(el=>{ el.removeAttribute('data-ft'); el.querySelectorAll('.ft-icon').forEach(i=>i.remove()); }); delete document.querySelector('.nav-files-container')?.dataset.ftObserved; @@ -680,8 +1098,259 @@ class FileTreeManager { } /* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - SETTINGS TAB + QUICK ACCESS BAR โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +class QuickBarManager { + constructor(plugin) { this.plugin = plugin; this.barEl = null; } + + register() { + const attach = () => this.injectBar(); + this.plugin.registerEvent(this.plugin.app.workspace.on('layout-change', attach)); + attach(); + } + + injectBar() { + const nav = document.querySelector('.nav-files-container'); + if (!nav || !nav.parentElement) return; + const parent = nav.parentElement; + + if (this.barEl && this.barEl.isConnected) { this.render(); return; } + + this.barEl = document.createElement('div'); + this.barEl.className = 'qb-bar'; + parent.insertBefore(this.barEl, nav); + this.render(); + } + + render() { + if (!this.barEl) return; + this.barEl.empty ? this.barEl.empty() : (this.barEl.innerHTML = ''); + + const items = this.plugin.settings.quickBarItems || []; + if (!items.length) { + this.barEl.style.display = 'none'; + return; + } + this.barEl.style.display = 'flex'; + + items.forEach(item => { + const btn = document.createElement('button'); + btn.className = 'qb-item'; + btn.title = item.path; + + const af = this.plugin.app.vault.getAbstractFileByPath(item.path); + const isFolder = af instanceof TFolder; + const iconKey = isFolder ? 'folder' : (FT_ICONS[getFileType(item.path)] ? getFileType(item.path) : 'text'); + + const icon = document.createElement('span'); + icon.className = `qb-icon ft-icon ft-${iconKey}`; + icon.innerHTML = FT_ICONS[iconKey] || FT_ICONS.text; + + const label = document.createElement('span'); + label.className = 'qb-label'; + label.textContent = item.label || item.path.split('/').pop(); + + btn.append(icon, label); + btn.addEventListener('click', () => this.openItem(item)); + this.barEl.appendChild(btn); + }); + } + + openItem(item) { + const af = this.plugin.app.vault.getAbstractFileByPath(item.path); + if (!af) { new Notice(t('qbNotFound') + item.path); return; } + + if (af instanceof TFolder) { + const explorerLeaf = this.plugin.app.workspace.getLeavesOfType('file-explorer')[0]; + explorerLeaf?.view?.revealInFolder?.(af); + } else if (af instanceof TFile) { + this.plugin.app.workspace.getLeaf(false).openFile(af); + } + } + + destroy() { + this.barEl?.remove(); + this.barEl = null; + } +} + +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + FOLDER FOCUS MODE +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +class FolderFocusManager { + constructor(plugin) { this.plugin = plugin; this._obs = null; this._rafId = null; this.breadcrumbEl = null; this._navEl = null; this._clickHandler = null; } + + register() { + this.plugin.registerEvent(this.plugin.app.workspace.on('file-menu', (menu, file) => { + if (!(file instanceof TFolder)) return; + if (this.plugin.settings.focusedFolder === file.path) { + menu.addItem(item => item.setTitle(t('ffUnfocusMenuItem')).setIcon('x-circle').onClick(() => this.clearFocus())); + } else { + menu.addItem(item => item.setTitle(t('ffFocusMenuItem')).setIcon('focus').onClick(() => this.setFocus(file.path))); + } + })); + + this._obs = new MutationObserver(() => this.schedule()); + + this._clickHandler = (e) => { + const titleEl = e.target.closest('.nav-folder-title'); + if (!titleEl) return; + const path = titleEl.dataset.path; + if (!path) return; + if (!(this.plugin.settings.mainFolders || []).includes(path)) return; + if (this.plugin.settings.focusedFolder === path) return; + e.preventDefault(); + e.stopPropagation(); + this.setFocus(path, { collapseChildren: true }); + }; + + const attach = () => { + const nav = document.querySelector('.nav-files-container'); + if (nav && !nav.dataset.ffObserved) { + nav.dataset.ffObserved = '1'; + this._obs.observe(nav, { childList:true, subtree:true }); + nav.addEventListener('click', this._clickHandler, true); + this._navEl = nav; + this.applyFocus(); + } + }; + + this.plugin.registerEvent(this.plugin.app.workspace.on('layout-change', attach)); + attach(); + } + + schedule() { + if (this._rafId != null) return; + this._rafId = requestAnimationFrame(() => { + this._rafId = null; + this.applyFocus(); + }); + } + + setFocus(path, opts = {}) { + this.plugin.settings.focusedFolder = path; + this.plugin.saveSettings(); + this.applyFocus({ collapseChildren: opts.collapseChildren }); + } + + clearFocus() { + this.plugin.settings.focusedFolder = null; + this.plugin.saveSettings(); + new Notice(t('ffFocusOff')); + this.applyFocus(); + } + + applyFocus(opts = {}) { + const nav = document.querySelector('.nav-files-container'); + if (!nav) return; + const focus = this.plugin.settings.focusedFolder; + const mainFolders = this.plugin.settings.mainFolders || []; + + /* reset previous state */ + nav.querySelectorAll('.ff-hidden').forEach(el => el.classList.remove('ff-hidden')); + nav.querySelectorAll('.ff-self').forEach(el => el.classList.remove('ff-self')); + nav.querySelectorAll('.ff-main-badge').forEach(el => el.remove()); + document.body.classList.toggle('ff-active', !!focus); + this.renderBreadcrumb(); + + /* decorate main-folder titles with a small indicator */ + mainFolders.forEach(mfPath => { + const mfTitle = nav.querySelector(`.nav-folder-title[data-path="${CSS.escape(mfPath)}"]`); + if (!mfTitle || mfTitle.querySelector('.ff-main-badge')) return; + const badge = document.createElement('span'); + badge.className = 'ff-main-badge'; + badge.title = 'Main folder โ€” click to focus'; + mfTitle.appendChild(badge); + }); + + if (!focus) return; + + /* data-path lives on .nav-folder-title, not .nav-folder */ + const titleEl = nav.querySelector(`.nav-folder-title[data-path="${CSS.escape(focus)}"]`); + const focusEl = titleEl?.closest('.nav-folder'); + if (!focusEl) return; + + focusEl.classList.add('ff-self'); + focusEl.classList.remove('is-collapsed'); + + /* optionally collapse all direct sub-folders */ + if (opts.collapseChildren) { + focusEl.querySelectorAll('.nav-folder').forEach(sub => { + sub.classList.add('is-collapsed'); + }); + } + + nav.querySelectorAll('.nav-folder, .nav-file').forEach(el => { + if (el === focusEl || focusEl.contains(el)) return; + el.classList.add('ff-hidden'); + }); + } + + renderBreadcrumb() { + const nav = document.querySelector('.nav-files-container'); + if (!nav || !nav.parentElement) return; + const parent = nav.parentElement; + const focus = this.plugin.settings.focusedFolder; + const mainFolders = this.plugin.settings.mainFolders || []; + + /* always show the bar if we have main folders or an active focus */ + const shouldShow = mainFolders.length > 0 || !!focus; + + if (!shouldShow) { this.breadcrumbEl?.remove(); this.breadcrumbEl = null; return; } + + if (!this.breadcrumbEl || !this.breadcrumbEl.isConnected) { + this.breadcrumbEl = document.createElement('div'); + this.breadcrumbEl.className = 'ff-breadcrumb'; + parent.insertBefore(this.breadcrumbEl, nav); + } + this.breadcrumbEl.innerHTML = ''; + + /* โ† back button */ + if (focus) { + const back = document.createElement('button'); + back.className = 'ff-back-btn'; + back.textContent = 'โ† ' + t('ffBack'); + back.addEventListener('click', () => this.clearFocus()); + this.breadcrumbEl.appendChild(back); + } + + /* main-folder tabs */ + mainFolders.forEach(mfPath => { + const name = mfPath.split('/').pop(); + const btn = document.createElement('button'); + btn.className = 'ff-tab' + (mfPath === focus ? ' ff-tab-active' : ''); + btn.textContent = name; + btn.title = mfPath; + btn.addEventListener('click', () => { + if (mfPath === focus) this.clearFocus(); + else this.setFocus(mfPath, { collapseChildren: true }); + }); + this.breadcrumbEl.appendChild(btn); + }); + + /* current-folder label (if focused on a non-main folder) */ + if (focus && !mainFolders.includes(focus)) { + const label = document.createElement('span'); + label.className = 'ff-label'; + label.textContent = focus.split('/').pop(); + this.breadcrumbEl.appendChild(label); + } + } + + destroy() { + this._obs?.disconnect(); + if (this._rafId != null) { cancelAnimationFrame(this._rafId); this._rafId = null; } + if (this._navEl && this._clickHandler) this._navEl.removeEventListener('click', this._clickHandler, true); + document.body.classList.remove('ff-active'); + document.querySelectorAll('.ff-hidden,.ff-self').forEach(el=>el.classList.remove('ff-hidden','ff-self')); + document.querySelectorAll('.ff-main-badge').forEach(el=>el.remove()); + this.breadcrumbEl?.remove(); + this.breadcrumbEl = null; + delete document.querySelector('.nav-files-container')?.dataset.ffObserved; + } +} + + class ToolkitSettingsTab extends PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin=plugin; } @@ -725,6 +1394,132 @@ class ToolkitSettingsTab extends PluginSettingTab { containerEl.createEl('hr'); + new Setting(containerEl).setName(t('aiName')).setDesc(t('aiDesc')) + .addToggle(tog=>tog.setValue(this.plugin.settings.enableAI).onChange(async val=>{ + this.plugin.settings.enableAI=val; await this.plugin.saveSettings(); + new Notice(val?t('aiOn'):t('aiOff')); + this.display(); + })); + + if (this.plugin.settings.enableAI) { + const isOllama = this.plugin.settings.aiProvider === 'ollama'; + + /* โ”€โ”€ Provider โ”€โ”€ */ + new Setting(containerEl).setName(t('aiProviderName')).setDesc(t('aiProviderDesc')) + .addDropdown(dd=>dd + .addOption('ollama', t('aiProviderOllama')) + .addOption('openai', t('aiProviderOpenAI')) + .addOption('custom', t('aiProviderCustom')) + .setValue(this.plugin.settings.aiProvider) + .onChange(async val=>{ + this.plugin.settings.aiProvider = val; + const preset = AI_PROVIDER_PRESETS[val] || AI_PROVIDER_PRESETS.custom; + this.plugin.settings.aiApiUrl = preset.url; + if (preset.model) this.plugin.settings.aiModel = preset.model; + await this.plugin.saveSettings(); + this.display(); + })); + + /* โ”€โ”€ Base URL โ”€โ”€ */ + new Setting(containerEl).setName(t('aiUrlName')).setDesc(t('aiUrlDesc')) + .addText(tx=>tx.setPlaceholder('http://localhost:11434/v1') + .setValue(this.plugin.settings.aiApiUrl) + .onChange(async val=>{ this.plugin.settings.aiApiUrl = val.trim(); await this.plugin.saveSettings(); })); + + /* โ”€โ”€ API Key (hidden-but-optional for Ollama) โ”€โ”€ */ + new Setting(containerEl).setName(t('aiKeyName')).setDesc(t('aiKeyDesc')) + .addText(tx=>{ + tx.inputEl.type = 'password'; + tx.setPlaceholder(isOllama ? '(not required)' : 'sk-โ€ฆ') + .setValue(this.plugin.settings.aiApiKey) + .onChange(async val=>{ this.plugin.settings.aiApiKey = val.trim(); await this.plugin.saveSettings(); }); + }); + + /* โ”€โ”€ Model โ”€โ”€ */ + if (isOllama) { + const models = this.plugin.settings.aiOllamaModels || []; + const modelSetting = new Setting(containerEl).setName(t('aiModelDropdownName')).setDesc(t('aiModelDropdownDesc')); + + if (models.length) { + modelSetting.addDropdown(dd=>{ + models.forEach(m => dd.addOption(m, m)); + if (!models.includes(this.plugin.settings.aiModel)) dd.addOption(this.plugin.settings.aiModel, this.plugin.settings.aiModel); + dd.setValue(this.plugin.settings.aiModel) + .onChange(async val=>{ this.plugin.settings.aiModel = val; await this.plugin.saveSettings(); }); + }); + } else { + modelSetting.setDesc(t('aiModelDropdownDesc') + ' โ€” ' + t('aiNoModelsYet')); + modelSetting.addText(tx=>tx.setPlaceholder('qwen2.5:3b') + .setValue(this.plugin.settings.aiModel) + .onChange(async val=>{ this.plugin.settings.aiModel = val.trim(); await this.plugin.saveSettings(); })); + } + + modelSetting.addButton(btn=>btn.setButtonText(t('aiFetchModels')).onClick(async ()=>{ + btn.setButtonText(t('aiFetchingModels')).setDisabled(true); + try { + const fetched = await fetchOllamaModels(this.plugin.settings.aiApiUrl); + this.plugin.settings.aiOllamaModels = fetched; + if (fetched.length && !fetched.includes(this.plugin.settings.aiModel)) { + this.plugin.settings.aiModel = fetched[0]; + } + await this.plugin.saveSettings(); + new Notice(t('aiModelsFetchOk').replace('{n}', fetched.length)); + } catch (err) { + new Notice(t('aiModelsFetchFail') + err.message); + } finally { + this.display(); + } + })); + } else { + new Setting(containerEl).setName(t('aiModelName')).setDesc(t('aiModelDesc')) + .addText(tx=>tx.setPlaceholder('gpt-4o-mini') + .setValue(this.plugin.settings.aiModel) + .onChange(async val=>{ this.plugin.settings.aiModel = val.trim(); await this.plugin.saveSettings(); })); + } + + /* โ”€โ”€ Test connection โ”€โ”€ */ + new Setting(containerEl).setName(t('aiTest')).setDesc('') + .addButton(btn=>btn.setButtonText(t('aiTest')).onClick(async ()=>{ + btn.setDisabled(true); + try { + await callAI(this.plugin, [{ role:'user', content:'Reply with the single word: OK' }]); + new Notice(t('aiTestOk')); + } catch (err) { + new Notice(t('aiTestFail') + err.message); + } finally { + btn.setDisabled(false); + } + })); + + /* โ”€โ”€ Response language โ”€โ”€ */ + new Setting(containerEl).setName(t('aiLangName')).setDesc(t('aiLangDesc')) + .addDropdown(dd=>dd + .addOption('auto', t('aiLangAuto')) + .addOption('ar', t('aiLangAr')) + .addOption('en', t('aiLangEn')) + .setValue(this.plugin.settings.aiResponseLang) + .onChange(async val=>{ this.plugin.settings.aiResponseLang = val; await this.plugin.saveSettings(); })); + + /* โ”€โ”€ System prompt โ”€โ”€ */ + const promptSetting = new Setting(containerEl).setName(t('aiPromptName')).setDesc(t('aiPromptDesc')); + promptSetting.settingEl.style.flexDirection = 'column'; + promptSetting.settingEl.style.alignItems = 'stretch'; + promptSetting.addTextArea(ta=>{ + ta.inputEl.rows = 5; + ta.inputEl.style.width = '100%'; + ta.setPlaceholder(DEFAULT_SMART_TEMPLATE_PROMPT) + .setValue(this.plugin.settings.aiSystemPrompt) + .onChange(async val=>{ this.plugin.settings.aiSystemPrompt = val; await this.plugin.saveSettings(); }); + }); + promptSetting.addButton(btn=>btn.setButtonText(t('aiPromptReset')).onClick(async ()=>{ + this.plugin.settings.aiSystemPrompt = ''; + await this.plugin.saveSettings(); + this.display(); + })); + } + + containerEl.createEl('hr'); + new Setting(containerEl).setName(t('ftName')).setDesc(t('ftDesc')) .addToggle(tog=>tog.setValue(this.plugin.settings.enableFileTree).onChange(async val=>{ this.plugin.settings.enableFileTree=val; await this.plugin.saveSettings(); @@ -753,7 +1548,142 @@ class ToolkitSettingsTab extends PluginSettingTab { ); } - containerEl.createEl('p', { text:'Obsidian Toolkit v2.2 โ€” by Ahmed', cls:'setting-item-description' }); + containerEl.createEl('hr'); + + new Setting(containerEl).setName(t('qbName')).setDesc(t('qbDesc')) + .addToggle(tog=>tog.setValue(this.plugin.settings.enableQuickBar).onChange(async val=>{ + this.plugin.settings.enableQuickBar=val; await this.plugin.saveSettings(); + val ? this.plugin.quickBarMgr.injectBar() : this.plugin.quickBarMgr.destroy(); + this.display(); + })); + + if (this.plugin.settings.enableQuickBar) { + containerEl.createEl('div', { text: t('qbItemsLabel'), cls: 'setting-item-name', attr: { style: 'margin: 8px 0 4px;' } }); + + const items = this.plugin.settings.quickBarItems || []; + if (!items.length) { + containerEl.createEl('p', { text: t('qbEmpty'), cls: 'setting-item-description' }); + } else { + items.forEach((item, idx) => { + new Setting(containerEl) + .setName(item.label || item.path.split('/').pop()) + .setDesc(item.path) + .addExtraButton(btn=>btn.setIcon('x').setTooltip(t('qbRemove')).onClick(async ()=>{ + this.plugin.settings.quickBarItems.splice(idx, 1); + await this.plugin.saveSettings(); + this.plugin.quickBarMgr.render(); + this.display(); + })); + }); + } + + /* quick-add buttons */ + new Setting(containerEl) + .addButton(btn=>btn.setButtonText(t('qbAddCurrent')).onClick(async ()=>{ + const file = this.plugin.app.workspace.getActiveFile(); + if (!file) { new Notice(t('qbNoActiveFile')); return; } + await this.addQuickItem(file.path, file.name); + })) + .addButton(btn=>btn.setButtonText(t('qbAddCurrentFolder')).onClick(async ()=>{ + const file = this.plugin.app.workspace.getActiveFile(); + if (!file || !file.parent) { new Notice(t('qbNoActiveFile')); return; } + const folder = file.parent; + await this.addQuickItem(folder.path, folder.name || folder.path); + })); + + /* manual add */ + let manualPath = '', manualLabel = ''; + new Setting(containerEl) + .addText(tx=>tx.setPlaceholder(t('qbPathPlaceholder')).onChange(val=>{ manualPath = val.trim(); })) + .addText(tx=>tx.setPlaceholder(t('qbLabelPlaceholder')).onChange(val=>{ manualLabel = val.trim(); })) + .addButton(btn=>btn.setButtonText(t('qbAdd')).setCta().onClick(async ()=>{ + if (!manualPath) { new Notice(t('qbInvalidPath')); return; } + await this.addQuickItem(manualPath, manualLabel); + })); + } + + containerEl.createEl('hr'); + + new Setting(containerEl).setName(t('ffName')).setDesc(t('ffDesc')) + .addToggle(tog=>tog.setValue(this.plugin.settings.enableFolderFocus).onChange(async val=>{ + this.plugin.settings.enableFolderFocus=val; await this.plugin.saveSettings(); + if (val) { this.plugin.folderFocusMgr.register(); } + else { this.plugin.settings.focusedFolder = null; await this.plugin.saveSettings(); this.plugin.folderFocusMgr.destroy(); } + this.display(); + })); + + if (this.plugin.settings.enableFolderFocus && this.plugin.settings.focusedFolder) { + new Setting(containerEl) + .setName(t('ffFocusOn') + this.plugin.settings.focusedFolder) + .addButton(btn=>btn.setButtonText(t('ffUnfocusMenuItem')).onClick(async ()=>{ + this.plugin.folderFocusMgr.clearFocus(); + this.display(); + })); + } + + if (this.plugin.settings.enableFolderFocus) { + containerEl.createEl('div', { text: t('mfName'), cls: 'setting-item-name', attr:{ style:'margin:10px 0 2px;' } }); + containerEl.createEl('div', { text: t('mfDesc'), cls: 'setting-item-description', attr:{ style:'margin-bottom:8px;' } }); + + const mf = this.plugin.settings.mainFolders || []; + if (!mf.length) { + containerEl.createEl('p', { text: t('mfEmpty'), cls: 'setting-item-description' }); + } else { + mf.forEach((path, idx) => { + new Setting(containerEl) + .setName(path.split('/').pop()) + .setDesc(path) + .addExtraButton(btn=>btn.setIcon('arrow-up').onClick(async ()=>{ + if (idx === 0) return; + [mf[idx-1], mf[idx]] = [mf[idx], mf[idx-1]]; + await this.plugin.saveSettings(); this.plugin.folderFocusMgr.applyFocus(); this.display(); + })) + .addExtraButton(btn=>btn.setIcon('arrow-down').onClick(async ()=>{ + if (idx === mf.length-1) return; + [mf[idx], mf[idx+1]] = [mf[idx+1], mf[idx]]; + await this.plugin.saveSettings(); this.plugin.folderFocusMgr.applyFocus(); this.display(); + })) + .addExtraButton(btn=>btn.setIcon('x').setTooltip(t('mfRemove')).onClick(async ()=>{ + mf.splice(idx, 1); + await this.plugin.saveSettings(); this.plugin.folderFocusMgr.applyFocus(); this.display(); + })); + }); + } + + new Setting(containerEl) + .addButton(btn=>btn.setButtonText(t('mfAddCurrent')).onClick(async ()=>{ + const file = this.plugin.app.workspace.getActiveFile(); + if (!file?.parent) { new Notice(t('mfNoActiveFile')); return; } + const path = file.parent.path; + if (!path || path === '/') { new Notice(t('mfInvalidPath')); return; } + const mf = this.plugin.settings.mainFolders || (this.plugin.settings.mainFolders = []); + if (mf.includes(path)) { new Notice(t('mfDuplicate')); return; } + mf.push(path); + await this.plugin.saveSettings(); this.plugin.folderFocusMgr.applyFocus(); this.display(); + })); + + let mfManual = ''; + new Setting(containerEl) + .addText(tx=>tx.setPlaceholder(t('mfPathPlaceholder')).onChange(val=>{ mfManual = val.trim(); })) + .addButton(btn=>btn.setButtonText(t('mfAdd')).setCta().onClick(async ()=>{ + if (!mfManual) { new Notice(t('mfInvalidPath')); return; } + const mf = this.plugin.settings.mainFolders || (this.plugin.settings.mainFolders = []); + if (mf.includes(mfManual)) { new Notice(t('mfDuplicate')); return; } + mf.push(mfManual); + await this.plugin.saveSettings(); this.plugin.folderFocusMgr.applyFocus(); this.display(); + })); + } + + containerEl.createEl('p', { text:'Obsidian Toolkit v2.6 โ€” by Ahmed', cls:'setting-item-description' }); + } + + async addQuickItem(path, label) { + const items = this.plugin.settings.quickBarItems || (this.plugin.settings.quickBarItems = []); + if (items.some(i => i.path === path)) { new Notice(t('qbDuplicate')); return; } + items.push({ id: `${Date.now()}`, path, label: label || path.split('/').pop() }); + await this.plugin.saveSettings(); + this.plugin.quickBarMgr.render(); + this.display(); } } @@ -764,19 +1694,17 @@ class ObsidianToolkitPlugin extends Plugin { async onload() { await this.loadSettings(); - this._styleEl = document.createElement('link'); this._styleEl.rel='stylesheet'; - this._styleEl.href = this.app.vault.adapter.getResourcePath(this.app.vault.configDir+'/plugins/obsidian-toolkit/styles.css'); - document.head.appendChild(this._styleEl); + /* CSS auto-loaded by Obsidian from styles.css */ if (this.settings.enableFormatting) { this.addRibbonIcon('wand-2', 'Formatting Palette (Ctrl+Shift+F)', ()=>{ const view = this.app.workspace.getActiveViewOfType(MarkdownView); - if (view) new FormattingPaletteModal(this.app, view.editor).open(); + if (view) new FormattingPaletteModal(this.app, view.editor, this).open(); else new Notice(t('openMd')); }); this.addCommand({ id:'open-formatting-palette', name:'Open Formatting Palette', hotkeys:[{modifiers:['Ctrl','Shift'],key:'f'}], - editorCallback:(editor)=>new FormattingPaletteModal(this.app, editor).open() }); + editorCallback:(editor)=>new FormattingPaletteModal(this.app, editor, this).open() }); } if (this.settings.enableImageControl) { @@ -794,15 +1722,43 @@ class ObsidianToolkitPlugin extends Plugin { this.fileTreeMgr = new FileTreeManager(this); if (this.settings.enableFileTree) this.fileTreeMgr.register(); + this.quickBarMgr = new QuickBarManager(this); + if (this.settings.enableQuickBar) this.quickBarMgr.register(); + + this.folderFocusMgr = new FolderFocusManager(this); + if (this.settings.enableFolderFocus) this.folderFocusMgr.register(); + + this.addCommand({ id:'ai-smart-suggestion', name:'AI Smart Template Suggestion', + hotkeys:[{modifiers:['Ctrl','Shift'],key:'g'}], + editorCallback:(editor)=>{ + if (!this.settings.enableAI) { new Notice(t('aiDisabled')); return; } + const cursor = editor.getCursor(); + const selection = editor.getSelection(); + let context = selection; + if (!context) { + const fromLine = Math.max(0, cursor.line - 12); + context = editor.getRange({ line: fromLine, ch: 0 }, cursor); + } + if (!context || !context.trim()) { new Notice(t('aiNoContext')); return; } + new AISuggestModal(this.app, this, editor, context).open(); + } }); + + this.addCommand({ id:'ai-template-generator', name:'AI Template Generator', + hotkeys:[{modifiers:['Ctrl','Shift'],key:'t'}], + editorCallback:(editor)=>{ + if (!this.settings.enableAI) { new Notice(t('aiDisabled')); return; } + new TemplateGeneratorModal(this.app, this, editor).open(); + } }); + this.addSettingTab(new ToolkitSettingsTab(this.app, this)); - console.log('โœ… Obsidian Toolkit v2.2 โ€” lang:', getAppLang()); } onunload() { - this._styleEl?.remove(); this.imgMgr?.destroy(); this.fullscreenMgr?.destroy(); this.fileTreeMgr?.destroy(); + this.quickBarMgr?.destroy(); + this.folderFocusMgr?.destroy(); document.querySelectorAll('.ic-lightbox-overlay').forEach(e=>e.remove()); } diff --git a/manifest.json b/manifest.json index d298896..af26088 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,11 @@ { "id": "obsidian-toolkit", "name": "Obsidian Toolkit", - "version": "2.3", + "version": "2.6.0", "minAppVersion": "0.15.0", - "description": "Enhanced formatting and image control toolkit for Obsidian with security and performance fixes.", - "author": "Ahmed-Moussa -EG", - "authorUrl": "https://github.com/ahmed", + "description": "All-in-one productivity suite: Formatting Palette, AI Smart Templates, Image Control, Fullscreen Note, Premium File Tree, Quick Access Bar, and Folder Focus Mode.", + "author": "Ahmed Moussa", + "authorUrl": "https://github.com/ahmdmusa", + "fundingUrl": "", "isDesktopOnly": false } diff --git a/styles.css b/styles.css index 64165c3..f483f8c 100644 --- a/styles.css +++ b/styles.css @@ -364,4 +364,190 @@ body .markdown-preview-view ol > li::marker { color: var(--text-accent); font-we border-left: 1px solid var(--background-modifier-border); margin-left: 10px; padding-left: 4px; -} \ No newline at end of file +} +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + 7. AI SMART TEMPLATE MODAL + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +.ai-modal-root { width: 480px !important; max-width: 94vw !important; } +.ai-modal { padding: 18px 20px 16px; } +.ai-modal-title { font-size: 0.95em; font-weight: 700; margin-bottom: 14px; color: var(--text-normal); } +.ai-modal-body { min-height: 60px; margin-bottom: 14px; } + +.ai-loading { display: flex; align-items: center; gap: 10px; color: var(--text-muted); font-size: 0.9em; padding: 8px 2px; } +.ai-spinner { + width: 16px; height: 16px; + border: 2px solid var(--background-modifier-border); + border-top-color: var(--interactive-accent); + border-radius: 50%; + animation: ai-spin 0.7s linear infinite; +} +@keyframes ai-spin { to { transform: rotate(360deg); } } + +.ai-suggestion-box { + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: 9px; + padding: 12px 14px; + font-family: var(--fs-mono); + font-size: 0.85em; + white-space: pre-wrap; + word-break: break-word; + max-height: 320px; + overflow-y: auto; + color: var(--text-normal); + margin: 0; +} +.ai-error-box { + color: var(--text-error, #dc2626); + font-size: 0.85em; + padding: 8px 2px; + word-break: break-word; +} +.ai-modal-actions { display: flex; gap: 8px; justify-content: flex-end; } + +/* โ”€โ”€ Reset button in image toolbar โ”€โ”€ */ +.ic-btn[title] { white-space: nowrap; } + +/* โ”€โ”€ Template Generator inputs โ”€โ”€ */ +.ai-textarea { + width: 100%; + resize: vertical; + font-family: inherit; + font-size: 0.85em; + padding: 8px 10px; + border: 1.5px solid var(--background-modifier-border); + border-radius: 7px; + background: var(--background-secondary); + color: var(--text-normal); + outline: none; + margin-bottom: 10px; + box-sizing: border-box; +} +.ai-textarea:focus { border-color: var(--interactive-accent); } +.ai-label-spaced { margin-top: 6px; } + +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + 8. QUICK ACCESS BAR + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +.qb-bar { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 8px 10px; + border-bottom: 1px solid var(--background-modifier-border); + flex-shrink: 0; +} +.qb-item { + display: flex; + align-items: center; + gap: 5px; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: 7px; + padding: 4px 9px; + font-size: 12px; + color: var(--text-normal); + cursor: pointer; + transition: background 0.12s, border-color 0.12s; + max-width: 160px; +} +.qb-item:hover { + background: var(--background-modifier-hover); + border-color: var(--interactive-accent); +} +.qb-icon { + margin-right: 0; + flex-shrink: 0; +} +.qb-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + 9. FOLDER FOCUS MODE + โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ +body.ff-active .nav-folder.ff-hidden, +body.ff-active .nav-file.ff-hidden { + display: none !important; +} +body.ff-active .nav-folder.ff-self > .nav-folder-title { + display: none !important; +} +body.ff-active .nav-folder.ff-self > .nav-folder-children { + margin-left: 0 !important; + padding-left: 0 !important; + border-left: none !important; +} + +.ff-breadcrumb { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-bottom: 1px solid var(--background-modifier-border); + flex-shrink: 0; +} +.ff-back-btn { + display: flex; + align-items: center; + gap: 5px; + background: var(--interactive-accent); + color: var(--text-on-accent); + border: none; + border-radius: 6px; + padding: 4px 10px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.15s; +} +.ff-back-btn:hover { opacity: 0.85; } +.ff-label { + font-size: 12px; + font-weight: 600; + color: var(--text-normal); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* โ”€โ”€ Main folder tabs in breadcrumb โ”€โ”€ */ +.ff-breadcrumb { + flex-wrap: wrap; +} +.ff-tab { + display: flex; + align-items: center; + background: var(--background-secondary); + border: 1px solid var(--background-modifier-border); + border-radius: 7px; + padding: 4px 10px; + font-size: 12px; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + transition: background 0.12s, border-color 0.12s, color 0.12s; +} +.ff-tab:hover { + background: var(--background-modifier-hover); + color: var(--text-normal); +} +.ff-tab.ff-tab-active { + background: var(--interactive-accent); + color: var(--text-on-accent); + border-color: var(--interactive-accent); +} + +/* โ”€โ”€ Main folder badge (dot indicator) โ”€โ”€ */ +.ff-main-badge { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--interactive-accent); + margin-left: 5px; + opacity: 0.7; + flex-shrink: 0; +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..446a4ac --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "2.6.0": "0.15.0" +}