Let a sync window take a whole share in one click
The window editor is path-first, which is exact but slow for the case that is nearly all of them. The chips only offer whole shares; a subpath already in the window marks its share partial rather than offering to widen it, and the path box stays the only way to express a subpath or a profile.
This commit is contained in:
@@ -91,6 +91,38 @@ require_once dirname(__DIR__) . '/include/monitor.php';
|
||||
|
||||
// ── Live sync log ─────────────────────────────────────────────────────────────
|
||||
$action = $_GET['action'] ?? '';
|
||||
// ── Share list, for the quick-select strip on the window editor ──────────────────────────────
|
||||
// The editor is path-first — type or browse, then Add — which is exact but slow for the case that
|
||||
// is nearly all of them: add one whole share. This answers "what shares exist here", so the strip
|
||||
// can offer them as one click each.
|
||||
//
|
||||
// Read from /boot/config/shares/*.cfg rather than by listing /mnt/user, because a share is a
|
||||
// declared thing: the directory can be absent on a share that has never been written to, and
|
||||
// listing the mount would also invent entries for stray directories that are not shares at all.
|
||||
if ($action === 'shares') {
|
||||
$out = [];
|
||||
foreach (glob('/boot/config/shares/*.cfg') ?: [] as $cfg) {
|
||||
$name = basename($cfg, '.cfg');
|
||||
$raw = (string) @file_get_contents($cfg);
|
||||
$get = function (string $k) use ($raw): string {
|
||||
return preg_match('/^' . $k . '="([^"]*)"/m', $raw, $m) ? $m[1] : '';
|
||||
};
|
||||
$path = '/mnt/user/' . $name;
|
||||
$out[] = [
|
||||
'name' => $name,
|
||||
'path' => $path,
|
||||
'comment' => $get('shareComment'),
|
||||
'pool' => $get('shareCachePool'),
|
||||
'cache' => $get('shareUseCache'),
|
||||
// Present is not the same as declared — a share can exist in conf with no directory yet.
|
||||
'exists' => is_dir($path),
|
||||
];
|
||||
}
|
||||
usort($out, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok' => true, 'shares' => $out]);
|
||||
exit;
|
||||
}
|
||||
if ($action === 'rsync_log') {
|
||||
$lockDir = '/tmp/unraid_locks';
|
||||
$lines = [];
|
||||
|
||||
@@ -40,6 +40,19 @@ require_once dirname(__DIR__) . '/include/confui.php';
|
||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
?>
|
||||
<style>
|
||||
/* ── Quick-add share chips ───────────────────────────────────────────────────────────────────
|
||||
A chip is a share, not a path. Anything already covered by this window is shown dimmed and
|
||||
inert rather than hidden — a share missing from the strip would read as "not on this host",
|
||||
which is a different and wrong statement. */
|
||||
.vv-ry-chips { display:flex;flex-wrap:wrap;gap:3px;max-height:96px;overflow-y:auto;
|
||||
padding:4px;background:#0a0a0a;border:1px solid #161616;border-radius:3px; }
|
||||
.vv-ry-chip { font-size:10px;padding:2px 7px;border-radius:10px;cursor:pointer;
|
||||
background:#0e1a2a;color:#7ab;border:1px solid #1e3a5a;white-space:nowrap;
|
||||
user-select:none; }
|
||||
.vv-ry-chip:hover { background:#14263d; }
|
||||
.vv-ry-chip.in { background:#0d1a0d;color:#4a7a4a;border-color:#1f3a1f;cursor:default; }
|
||||
.vv-ry-chip.part { background:#1a1408;color:#a08a4a;border-color:#3a2f1a; }
|
||||
.vv-ry-chip.gone { opacity:.45; }
|
||||
.vv-ry-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; }
|
||||
.vv-ry-sec { font-size:10px;font-weight:bold;color:#444;letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px; }
|
||||
.vv-ry-row { display:flex;justify-content:space-between;align-items:baseline;gap:6px;margin:2px 0; }
|
||||
@@ -1237,6 +1250,15 @@ function _vvRyBuildEditPanel(key) {
|
||||
}
|
||||
// Share add form
|
||||
h += `<div style="margin-top:6px;" onclick="event.stopPropagation()">
|
||||
<!-- Quick select. The path box below stays the precise tool — subpaths like
|
||||
/mnt/user/Media_Server/Emby and per-entry profiles can only be expressed there. This
|
||||
strip only covers the common case: add one whole share, one click. -->
|
||||
<div style="margin-bottom:5px;">
|
||||
<div style="font-size:9px;color:#4a6a80;text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px;">
|
||||
Quick add a share <span style="color:#3a3a3a;text-transform:none;letter-spacing:0;">— uses the profile selected below</span>
|
||||
</div>
|
||||
<div id="vv-ry-echips-${key}" class="vv-ry-chips">loading…</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;margin-bottom:3px;">
|
||||
<input id="vv-ry-epath-${key}" type="text" placeholder="/mnt/user/…" autocomplete="off"
|
||||
style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#888;
|
||||
@@ -1272,6 +1294,9 @@ function _vvRyBuildEditPanel(key) {
|
||||
|
||||
panel.innerHTML = h;
|
||||
_vvRyLoadProfiles(key);
|
||||
// After innerHTML, not before — the chip host does not exist until the panel is written, and
|
||||
// the strip has to redraw on every rebuild so an entry added or removed re-marks its share.
|
||||
vvRyRenderChips(key);
|
||||
}
|
||||
|
||||
// ── Script mutations ──────────────────────────────────────────────────────────
|
||||
@@ -1302,6 +1327,64 @@ function vvRyERemShare(key, idx) {
|
||||
const s = _vvRyWinEState[key]?.shares; if (!s) return;
|
||||
s.splice(idx, 1); _vvRyBuildEditPanel(key);
|
||||
}
|
||||
// ── Quick-add share chips ────────────────────────────────────────────────────────────────────
|
||||
// Fetched once per page and reused by every window's editor; rebuilding 41 chips per panel render
|
||||
// is cheap, another HTTP round trip per open is not.
|
||||
let _vvRyShares = null;
|
||||
|
||||
async function _vvRyLoadShares() {
|
||||
if (_vvRyShares) return _vvRyShares;
|
||||
try {
|
||||
const d = await (await fetch('/plugins/varaverk/api/rsync.php?action=shares')).json();
|
||||
_vvRyShares = d.ok ? (d.shares || []) : [];
|
||||
} catch (e) { _vvRyShares = []; }
|
||||
return _vvRyShares;
|
||||
}
|
||||
|
||||
// A share is "in" only when this window holds its exact path. A window holding a SUBPATH of it —
|
||||
// /mnt/user/Media_Server/Emby against the Media_Server share — is marked partial instead, because
|
||||
// adding the parent would silently widen a deliberately narrow entry.
|
||||
function _vvRyChipState(share, entries) {
|
||||
const p = (share.path || '').replace(/\/+$/, '');
|
||||
for (const e of entries) {
|
||||
const ep = (e.path || '').replace(/\/+$/, '');
|
||||
if (ep === p) return 'in';
|
||||
if (ep.startsWith(p + '/')) return 'part';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function vvRyRenderChips(key) {
|
||||
const host = document.getElementById('vv-ry-echips-' + key);
|
||||
if (!host) return;
|
||||
const shares = await _vvRyLoadShares();
|
||||
const entries = (_vvRyWinEState[key] && _vvRyWinEState[key].shares) || [];
|
||||
if (!shares.length) { host.innerHTML = '<span style="font-size:10px;color:#444;">no shares found</span>'; return; }
|
||||
|
||||
host.innerHTML = shares.map(function (s) {
|
||||
const st = _vvRyChipState(s, entries);
|
||||
const cls = 'vv-ry-chip' + (st ? ' ' + st : '') + (s.exists ? '' : ' gone');
|
||||
const tip = st === 'in' ? 'already in this window'
|
||||
: st === 'part' ? 'a subpath of this share is already here — adding the whole share would widen it'
|
||||
: (s.comment || s.path) + (s.exists ? '' : ' (declared, but no directory yet)');
|
||||
return '<span class="' + cls + '" data-path="' + vvEscAttr(s.path) + '" data-state="' + st + '" '
|
||||
+ 'title="' + vvEscAttr(tip) + '">' + vvEscHtml(s.name) + '</span>';
|
||||
}).join('');
|
||||
|
||||
// Delegated, and bound once — the chips are rebuilt on every render.
|
||||
if (!host.dataset.bound) {
|
||||
host.dataset.bound = '1';
|
||||
host.addEventListener('click', function (ev) {
|
||||
const c = ev.target.closest('.vv-ry-chip');
|
||||
if (!c || c.dataset.state === 'in') return;
|
||||
const pathEl = document.getElementById('vv-ry-epath-' + key);
|
||||
if (pathEl) pathEl.value = c.dataset.path;
|
||||
// Routed through the existing add so profile handling, validation and the state shape stay
|
||||
// in exactly one place.
|
||||
vvRyEAddShare(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
function vvRyEAddShare(key) {
|
||||
const path = document.getElementById('vv-ry-epath-' + key)?.value.trim();
|
||||
const prof = document.getElementById('vv-ry-eprof-' + key)?.value.trim() || '';
|
||||
|
||||
Reference in New Issue
Block a user