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 ─────────────────────────────────────────────────────────────
|
// ── Live sync log ─────────────────────────────────────────────────────────────
|
||||||
$action = $_GET['action'] ?? '';
|
$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') {
|
if ($action === 'rsync_log') {
|
||||||
$lockDir = '/tmp/unraid_locks';
|
$lockDir = '/tmp/unraid_locks';
|
||||||
$lines = [];
|
$lines = [];
|
||||||
|
|||||||
@@ -40,6 +40,19 @@ require_once dirname(__DIR__) . '/include/confui.php';
|
|||||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||||
?>
|
?>
|
||||||
<style>
|
<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-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-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; }
|
.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
|
// Share add form
|
||||||
h += `<div style="margin-top:6px;" onclick="event.stopPropagation()">
|
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;">
|
<div style="display:flex;gap:4px;margin-bottom:3px;">
|
||||||
<input id="vv-ry-epath-${key}" type="text" placeholder="/mnt/user/…" autocomplete="off"
|
<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;
|
style="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:3px;color:#888;
|
||||||
@@ -1272,6 +1294,9 @@ function _vvRyBuildEditPanel(key) {
|
|||||||
|
|
||||||
panel.innerHTML = h;
|
panel.innerHTML = h;
|
||||||
_vvRyLoadProfiles(key);
|
_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 ──────────────────────────────────────────────────────────
|
// ── Script mutations ──────────────────────────────────────────────────────────
|
||||||
@@ -1302,6 +1327,64 @@ function vvRyERemShare(key, idx) {
|
|||||||
const s = _vvRyWinEState[key]?.shares; if (!s) return;
|
const s = _vvRyWinEState[key]?.shares; if (!s) return;
|
||||||
s.splice(idx, 1); _vvRyBuildEditPanel(key);
|
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) {
|
function vvRyEAddShare(key) {
|
||||||
const path = document.getElementById('vv-ry-epath-' + key)?.value.trim();
|
const path = document.getElementById('vv-ry-epath-' + key)?.value.trim();
|
||||||
const prof = document.getElementById('vv-ry-eprof-' + key)?.value.trim() || '';
|
const prof = document.getElementById('vv-ry-eprof-' + key)?.value.trim() || '';
|
||||||
|
|||||||
Reference in New Issue
Block a user