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:
1 parent
c5bf8b0407
commit
04b52ecc19
9 files changed
+197
-6
No files matched your search
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user