From 04b52ecc19d44a8c92da52b566454bfe84889672 Mon Sep 17 00:00:00 2001 From: th Date: Sun, 12 Apr 2026 19:26:04 +0200 Subject: [PATCH] gitea: default repo setting; editor: asset picker with thumbnail grid Gitea plugin: default_repo field per account (stored in DB, pre-fills create issue form but stays editable). Editor toolbar: asset picker flyout with 4-column thumbnail grid and live search filter. Clicking a tile inserts the matching markdown. Co-Authored-By: Claude Opus 4.6 (1M context) --- config.php | 2 +- electron/package.json | 2 +- public/index.php | 1 + public/js/modules/editor.js | 105 ++++++++++++++++++++++ public/plugins/gitea/backend.php | 12 ++- public/plugins/gitea/gitea.js | 16 +++- public/plugins/tests/asset-picker.test.js | 63 +++++++++++++ public/plugins/tests/runner.js | 1 + public/plugins/tests/tests.js | 1 + 9 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 public/plugins/tests/asset-picker.test.js diff --git a/config.php b/config.php index c4b3949..efe9748 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ +
slot('md-toolbar') ?> diff --git a/public/js/modules/editor.js b/public/js/modules/editor.js index a0d32d4..9d91d57 100644 --- a/public/js/modules/editor.js +++ b/public/js/modules/editor.js @@ -363,6 +363,7 @@ export function closeModal({ animate = true } = {}) { hideTagAc(); hideSnippetAc(); closeFindBar(); + closeAssetPicker(); modal.classList.remove('open'); state.editingId = null; state.returnToGalleryAfterEdit = false; @@ -1184,6 +1185,110 @@ $('md-upload-pdf-file')?.addEventListener('change', e => { e.target.value = ''; }); +// ── Asset picker (insert from uploads) ── + +const ASSET_IMAGE_EXT = /\.(png|jpe?g|gif|webp)$/i; +const ASSET_AUDIO_EXT = /\.(mp3|ogg|wav|m4a)$/i; +const ASSET_PDF_EXT = /\.pdf$/i; + +function assetType(name) { + if (ASSET_IMAGE_EXT.test(name)) return 'image'; + if (ASSET_AUDIO_EXT.test(name)) return 'audio'; + if (ASSET_PDF_EXT.test(name)) return 'pdf'; + return 'other'; +} + +let assetPickerEl = null; + +function closeAssetPicker() { + if (assetPickerEl) { assetPickerEl.remove(); assetPickerEl = null; } +} + +$('md-asset-picker')?.addEventListener('click', async () => { + if (assetPickerEl) { closeAssetPicker(); return; } + + let files = []; + try { files = await api('api/uploads'); } catch {} + if (!Array.isArray(files)) files = []; + + const btn = $('md-asset-picker'); + const rect = btn.getBoundingClientRect(); + + const picker = document.createElement('div'); + picker.id = 'asset-picker-flyout'; + picker.style.cssText = `position:fixed;top:${rect.bottom + 4}px;right:${window.innerWidth - rect.right}px;width:340px;max-height:400px;background:var(--t-surface);border:1px solid var(--t-border);border-radius:6px;box-shadow:0 12px 40px rgba(0,0,0,0.5);z-index:200;display:flex;flex-direction:column;overflow:hidden;`; + + const searchHtml = `
+ +
`; + + const gridId = 'asset-picker-grid'; + picker.innerHTML = searchHtml + `
`; + document.body.appendChild(picker); + assetPickerEl = picker; + + const grid = picker.querySelector(`#${gridId}`); + const searchInput = picker.querySelector('#asset-picker-search'); + + function renderTiles(filter) { + const q = (filter || '').toLowerCase(); + const filtered = q ? files.filter(f => f.name.toLowerCase().includes(q)) : files; + if (!filtered.length) { + grid.innerHTML = `
${files.length ? 'no matches' : 'no assets'}
`; + return; + } + grid.innerHTML = filtered.map(f => { + const type = assetType(f.name); + const label = f.name.replace(/\.[^.]+$/, ''); + if (type === 'image') { + return `
+ +
`; + } + const icon = type === 'audio' ? 'ph-music-note' : type === 'pdf' ? 'ph-file-pdf' : 'ph-file'; + const iconColor = type === 'pdf' ? 'color:var(--t-danger)' : 'color:var(--t-dim)'; + return `
+ + ${esc(label)} +
`; + }).join(''); + + grid.querySelectorAll('.asset-pick-tile').forEach(tile => { + tile.addEventListener('click', () => { + const url = tile.dataset.url; + const name = tile.dataset.name; + const type = tile.dataset.type; + let md; + if (type === 'image') md = `![${name.replace(/\.[^.]+$/, '')}](${url})`; + else if (type === 'pdf') md = `[📄 ${name}](${url})`; + else if (type === 'audio') md = ``; + else md = `[${name}](${url})`; + insertMd(md); + closeAssetPicker(); + }); + tile.addEventListener('mouseenter', () => { tile.style.borderColor = 'var(--t-accent)'; }); + tile.addEventListener('mouseleave', () => { tile.style.borderColor = 'var(--t-border)'; }); + }); + } + + renderTiles(''); + setTimeout(() => searchInput.focus(), 0); + searchInput.addEventListener('input', () => renderTiles(searchInput.value)); + searchInput.addEventListener('keydown', e => { + if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeAssetPicker(); } + }); + + const onClickOutside = e => { + if (!picker.contains(e.target) && e.target !== btn && !btn.contains(e.target)) { + closeAssetPicker(); + document.removeEventListener('mousedown', onClickOutside); + } + }; + document.addEventListener('mousedown', onClickOutside); +}); + // ── Image resize ── document.addEventListener('mousedown', e => { diff --git a/public/plugins/gitea/backend.php b/public/plugins/gitea/backend.php index b1fcde3..7e26a8c 100644 --- a/public/plugins/gitea/backend.php +++ b/public/plugins/gitea/backend.php @@ -27,10 +27,16 @@ require_once __DIR__ . '/GiteaClient.php'; name TEXT NOT NULL, base_url TEXT NOT NULL, token TEXT NOT NULL, + default_repo TEXT DEFAULT NULL, position INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) "); + // Migration: add default_repo column if missing + $cols = $db->query("PRAGMA table_info(gitea_accounts)")->fetchAll(PDO::FETCH_COLUMN, 1); + if (!in_array('default_repo', $cols)) { + $db->exec("ALTER TABLE gitea_accounts ADD COLUMN default_repo TEXT DEFAULT NULL"); + } })(); // ── Encryption helpers (mirrors email plugin / CalDavSync) ──────── @@ -85,7 +91,7 @@ PluginManager::instance()->onApiRoute(function (string $route, string $method) { if ($route === '/api/gitea/accounts' && $method === 'GET') { $db = Database::connect(); - $rows = $db->query('SELECT id, name, base_url, position, created_at FROM gitea_accounts ORDER BY position ASC, id ASC')->fetchAll(); + $rows = $db->query('SELECT id, name, base_url, default_repo, position, created_at FROM gitea_accounts ORDER BY position ASC, id ASC')->fetchAll(); giteaJson($rows); } @@ -132,6 +138,10 @@ PluginManager::instance()->onApiRoute(function (string $route, string $method) { if (!empty($input['token'])) { $fields[] = 'token = :token'; $params['token'] = giteaEncrypt($input['token']); } + if (array_key_exists('default_repo', $input)) { + $val = trim((string)($input['default_repo'] ?? '')); + $fields[] = 'default_repo = :default_repo'; $params['default_repo'] = $val ?: null; + } if (!$fields) giteaJson(['ok' => true]); $db = Database::connect(); $db->prepare('UPDATE gitea_accounts SET ' . implode(', ', $fields) . ' WHERE id = :id')->execute($params); diff --git a/public/plugins/gitea/gitea.js b/public/plugins/gitea/gitea.js index 95d4e4a..460d66a 100644 --- a/public/plugins/gitea/gitea.js +++ b/public/plugins/gitea/gitea.js @@ -113,6 +113,10 @@ function buildSettingsPanel(panelEl) { access token +
@@ -150,7 +154,7 @@ function renderAccountsList() {