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) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-12 19:26:04 +02:00
1 parent c5bf8b0407
commit 04b52ecc19
9 files changed
+197 -6

No files matched your search

+105
View File
@@ -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 = `<div style="padding:8px;border-bottom:1px solid var(--t-border);">
<input id="asset-picker-search" type="text" placeholder="search assets..." autocomplete="off"
style="width:100%;background:var(--t-bg);border:1px solid var(--t-border);color:var(--t-bright);padding:5px 8px;font-size:11px;font-family:var(--t-font-content,'JetBrains Mono',monospace);border-radius:4px;outline:none;"
class="focus:border-t-accent">
</div>`;
const gridId = 'asset-picker-grid';
picker.innerHTML = searchHtml + `<div id="${gridId}" style="flex:1;overflow-y:auto;padding:8px;display:grid;grid-template-columns:repeat(4,1fr);gap:6px;"></div>`;
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 = `<div style="grid-column:1/-1;text-align:center;padding:24px 0;color:var(--t-dim);font-size:10px;font-style:italic;">${files.length ? 'no matches' : 'no assets'}</div>`;
return;
}
grid.innerHTML = filtered.map(f => {
const type = assetType(f.name);
const label = f.name.replace(/\.[^.]+$/, '');
if (type === 'image') {
return `<div class="asset-pick-tile" data-url="${esc(f.url)}" data-name="${esc(f.name)}" data-type="image" title="${esc(f.name)}" style="cursor:pointer;border-radius:4px;overflow:hidden;aspect-ratio:1;background:var(--t-bg);border:1px solid var(--t-border);">
<img src="${esc(f.url)}" style="width:100%;height:100%;object-fit:cover;" loading="lazy">
</div>`;
}
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 `<div class="asset-pick-tile" data-url="${esc(f.url)}" data-name="${esc(f.name)}" data-type="${type}" title="${esc(f.name)}" style="cursor:pointer;border-radius:4px;overflow:hidden;aspect-ratio:1;background:var(--t-bg);border:1px solid var(--t-border);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:4px;">
<i class="ph ${icon}" style="font-size:20px;${iconColor}"></i>
<span style="font-size:7px;color:var(--t-muted);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--t-font-pixel,'Silkscreen',monospace);">${esc(label)}</span>
</div>`;
}).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 = `<audio controls src="${url}"></audio>`;
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 => {