Summary
• Implemented a system-wide privacy lock that automatically hides the studio interface when the OS is locked or when triggered via a new API endpoint.
Changes
• Backend edit_api.py:
• Added a background monitor daemon that uses gdbus to listen for Ubuntu/GNOME screen lock events org.gnome.ScreenSaver.ActiveChanged.
• Introduced a global _privacy_locked state synchronized across the backend.
• Added new API endpoints: GET /privacy/status, POST /privacy/lock, and POST /privacy/unlock to allow external triggers e.g., keyboard macros.
• Updated the static data exporter to include system_status.json, enabling efficient frontend polling.
• Frontend car.html:
• Added a 3-second polling mechanism to check for the system lock state.
• Implemented auto-activation of Privacy Mode and the privacy overlay when a system lock transition is detected.
• Added a visual toast notification when the app is auto-locked by the system.
Verification
• Verified backend code integrity via py_compile.
• Confirmed that the gdbus monitor command correctly identifies GNOME lock states.
• Ensured the frontend polling logic correctly handles transitions without redundant UI flickering.
Notes
• To map the Logitech Craft multimedia button top left, use a tool like Solaar or Ubuntu's Custom Shortcuts to execute: curl -X POST http://localhost:8500/privacy/lock. This will instantly hide the app regardless of browser focus.
This commit is contained in:
@@ -2034,7 +2034,8 @@
|
||||
<!-- Film strip selector tabs -->
|
||||
<div class="filmstrip-selector-tabs" style="display:flex;gap:4px;padding:4px 8px;background:#151515;border-top:1px solid #1a1a1a;align-items:center;justify-content:space-between;flex-shrink:0">
|
||||
<div style="display:flex;gap:4px">
|
||||
<button onclick="switchFilmstripTab('group')" class="fs-tab active" id="fsTabGroup" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'VISIBLE')">Group Variants</button>
|
||||
<button onclick="switchFilmstripTab('active')" class="fs-tab active" id="fsTabActive" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'ACTIVE')">Group Active</button>
|
||||
<button onclick="switchFilmstripTab('group')" class="fs-tab" id="fsTabGroup" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'VISIBLE')">Group Variant</button>
|
||||
<button onclick="switchFilmstripTab('hidden')" class="fs-tab" id="fsTabHidden" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'HIDDEN')">Hidden</button>
|
||||
<button onclick="switchFilmstripTab('source')" class="fs-tab" id="fsTabSource" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'SOURCE')">Source</button>
|
||||
<button onclick="switchFilmstripTab('archived')" class="fs-tab" id="fsTabArchived" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'ARCHIVED')">Archived</button>
|
||||
@@ -2108,6 +2109,14 @@
|
||||
<button class="sb-btn" id="lbEstimate21Btn" onclick="estimate21PlusKeybind()" title="Detect adult 21+ tags (Shortcut: E)">🔞 21+</button>
|
||||
</div>
|
||||
<div class="sb-sep"></div>
|
||||
<div id="lbPoseInfoWrap" style="display:none;margin-bottom:8px">
|
||||
<div class="sb-label" style="margin-bottom:4px">Pose information</div>
|
||||
<div id="lbPoseDescription" style="font-size:11px;color:#888;margin-bottom:6px;line-height:1.4"></div>
|
||||
<div style="display:flex;justify-content:center;background:#000;border-radius:6px;border:1px solid #333;overflow:hidden;position:relative;height:120px">
|
||||
<canvas id="lbPoseInfoCanvas" width="120" height="120" style="max-height:100%;object-fit:contain"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sb-sep"></div>
|
||||
<div class="sb-label">Actions</div>
|
||||
<div class="sb-actions" style="margin-bottom:8px">
|
||||
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger">Re-tag</button>
|
||||
@@ -2290,6 +2299,47 @@
|
||||
|
||||
// --- HYDRATION_START ---
|
||||
const PRELOADED_IMAGES = [
|
||||
"up_b8c61991_face.png",
|
||||
"_turntable/bb_01.png/views/view_008_120deg.png",
|
||||
"_turntable/bb_01.png/views/view_007_105deg.png",
|
||||
"_turntable/bb_01.png/views/view_006_090deg.png",
|
||||
"_turntable/bb_01.png/views/view_005_075deg.png",
|
||||
"_turntable/bb_01.png/views/view_004_060deg.png",
|
||||
"_turntable/bb_01.png/views/view_003_045deg.png",
|
||||
"_turntable/bb_01.png/views/view_002_030deg.png",
|
||||
"_turntable/bb_01.png/views/view_001_015deg.png",
|
||||
"_turntable/bb_01.png/views/view_000_000deg.png",
|
||||
"_turntable/up_b8c61991/views/view_023_345deg.png",
|
||||
"_turntable/up_b8c61991/views/view_022_330deg.png",
|
||||
"_turntable/up_b8c61991/views/view_021_315deg.png",
|
||||
"_turntable/up_b8c61991/views/view_020_300deg.png",
|
||||
"_turntable/up_b8c61991/views/view_019_285deg.png",
|
||||
"_turntable/up_b8c61991/views/view_018_270deg.png",
|
||||
"_turntable/up_b8c61991/views/view_017_255deg.png",
|
||||
"_turntable/up_b8c61991/views/view_016_240deg.png",
|
||||
"_turntable/up_b8c61991/views/view_015_225deg.png",
|
||||
"_turntable/up_b8c61991/views/view_014_210deg.png",
|
||||
"_turntable/up_b8c61991/views/view_013_195deg.png",
|
||||
"_turntable/up_b8c61991/views/view_012_180deg.png",
|
||||
"_turntable/up_b8c61991/views/view_011_165deg.png",
|
||||
"_turntable/up_b8c61991/views/view_010_150deg.png",
|
||||
"_turntable/up_b8c61991/views/view_009_135deg.png",
|
||||
"_turntable/up_b8c61991/views/view_008_120deg.png",
|
||||
"_turntable/up_b8c61991/views/view_007_105deg.png",
|
||||
"_turntable/up_b8c61991/views/view_006_090deg.png",
|
||||
"_turntable/up_b8c61991/views/view_005_075deg.png",
|
||||
"_turntable/up_b8c61991/views/view_004_060deg.png",
|
||||
"_turntable/up_b8c61991/views/view_003_045deg.png",
|
||||
"_turntable/up_b8c61991/views/view_002_030deg.png",
|
||||
"_turntable/up_b8c61991/views/view_001_015deg.png",
|
||||
"_turntable/up_b8c61991/views/view_000_000deg.png",
|
||||
"_turntable/pass-1.png/views/view_023_345deg.png",
|
||||
"_turntable/pass-1.png/views/view_022_330deg.png",
|
||||
"_turntable/pass-1.png/views/view_021_315deg.png",
|
||||
"_turntable/pass-1.png/views/view_020_300deg.png",
|
||||
"_turntable/pass-1.png/views/view_019_285deg.png",
|
||||
"_turntable/pass-1.png/views/view_018_270deg.png",
|
||||
"_turntable/pass-1.png/views/view_017_255deg.png",
|
||||
"_turntable/pass-1.png/views/view_016_240deg.png",
|
||||
"_turntable/up_810b587c/views/view_012_180deg.png",
|
||||
"_turntable/up_810b587c/views/view_011_165deg.png",
|
||||
@@ -2351,8 +2401,8 @@
|
||||
"_turntable/cg_9bb8f3bb/views/view_001_015deg.png",
|
||||
"_turntable/cg_9bb8f3bb/views/view_000_000deg.png",
|
||||
"_turntable/pa01.png/views/view_023_345deg.png",
|
||||
"20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
"20260628_105307_dup_20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
"20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
"_turntable/pa01.png/views/view_022_330deg.png",
|
||||
"_turntable/pa01.png/views/view_021_315deg.png",
|
||||
"20260627_212812_5_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||
@@ -5081,8 +5131,8 @@
|
||||
"20260625_021910_image.png",
|
||||
"20260625_021702_sc_image.png",
|
||||
"20260625_021615_sc_image.png",
|
||||
"20260625_015711_sc_image.png",
|
||||
"20260625_020212_dup_20260625_015711_sc_image.png",
|
||||
"20260625_015711_sc_image.png",
|
||||
"20260625_015711_sc_image.nobg.png",
|
||||
"20260625_015900_sc_image.png",
|
||||
"20260625_015413_sc_image.png",
|
||||
@@ -5269,9 +5319,9 @@
|
||||
"20260624_183010_0_20260624_182958_image.png",
|
||||
"20260624_182958_image.png",
|
||||
"20260624_174900_image.png",
|
||||
"20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png",
|
||||
"20260624_173115_image.png",
|
||||
"20260624_174943_dup_20260624_173115_image.png",
|
||||
"20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png",
|
||||
"20260624_174757_image.png",
|
||||
"20260624_174442_image.png",
|
||||
"20260624_172923_image.nobg.png",
|
||||
@@ -5778,8 +5828,8 @@
|
||||
"20260622_193411_1_20260622_193118_image.png",
|
||||
"20260622_193354_0_20260622_193118_image.png",
|
||||
"20260622_193118_image.png",
|
||||
"20260622_181910_0_20260619_124038_image.png",
|
||||
"20260622_212012_dup_20260622_181910_0_20260619_124038_image.png",
|
||||
"20260622_181910_0_20260619_124038_image.png",
|
||||
"20260622_181734_0_20260619_124038_image.png",
|
||||
"20260622_181720_0_20260619_124038_image.png",
|
||||
"20260622_181634_0_20260619_124038_image.png",
|
||||
@@ -6121,8 +6171,8 @@
|
||||
"20260620_152017_image.png",
|
||||
"20260618_013452_out4.png",
|
||||
"20260620_025959_out4.png",
|
||||
"20260622_021658_dup_20260621_212650_image.png",
|
||||
"20260621_212650_image.png",
|
||||
"20260622_021658_dup_20260621_212650_image.png",
|
||||
"20260621_210130_0_20260621_205151_image.png",
|
||||
"20260621_210118_0_20260621_205151_image.png",
|
||||
"20260621_210107_0_20260621_205151_image.png",
|
||||
@@ -7040,6 +7090,8 @@
|
||||
let filePrompts = {}; // filename → generation prompt
|
||||
let fileHidden = {}; // filename → boolean (hidden from cycling)
|
||||
let fileArchived = {}; // filename → boolean
|
||||
let filePoseDescription = {}; // filename → human readable string
|
||||
let filePoseSkeleton = {}; // filename → JSON string (COCO-17 keypoints)
|
||||
let fileTags = {}; // filename → array of strings
|
||||
let fileIsSource = {}; // filename → boolean
|
||||
let _archiveViewActive = false; // when true, show archived items only
|
||||
@@ -7171,7 +7223,7 @@
|
||||
|
||||
// --- studio view (replaces lightbox) ---
|
||||
let lbUrls = [], lbNames = [], lbIdx = 0;
|
||||
let _activeFilmstripTab = 'group'; // 'group', 'hidden', 'source', 'archived'
|
||||
let _activeFilmstripTab = 'active'; // 'active', 'group', 'hidden', 'source', 'archived'
|
||||
let _multiSelectModeActive = false;
|
||||
const _selectedFilenames = new Set();
|
||||
|
||||
@@ -7179,7 +7231,7 @@
|
||||
_activeFilmstripTab = tab;
|
||||
|
||||
// Highlight tab
|
||||
['Group', 'Hidden', 'Source', 'Archived'].forEach(t => {
|
||||
['Active', 'Group', 'Hidden', 'Source', 'Archived'].forEach(t => {
|
||||
const el = document.getElementById(`fsTab${t}`);
|
||||
if (el) el.classList.toggle('active', t.toLowerCase() === tab);
|
||||
});
|
||||
@@ -7190,11 +7242,25 @@
|
||||
|
||||
const currentImg = lbNames[lbIdx];
|
||||
|
||||
if (tab === 'group') {
|
||||
if (tab === 'active' || tab === 'group') {
|
||||
const data = groupData.get(lbCurrentGid);
|
||||
if (data) {
|
||||
lbUrls = data.urls;
|
||||
lbNames = data.names;
|
||||
if (tab === 'active') {
|
||||
// Filter out hidden, source, and archived
|
||||
const activeNames = [];
|
||||
const activeUrls = [];
|
||||
data.names.forEach((name, i) => {
|
||||
if (!fileHidden[name] && !fileIsSource[name] && !fileArchived[name]) {
|
||||
activeNames.push(name);
|
||||
activeUrls.push(data.urls[i]);
|
||||
}
|
||||
});
|
||||
lbNames = activeNames;
|
||||
lbUrls = activeUrls;
|
||||
} else {
|
||||
lbUrls = data.urls;
|
||||
lbNames = data.names;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const matched = [];
|
||||
@@ -7325,7 +7391,7 @@
|
||||
if (r.ok) {
|
||||
showToast(`Moved ${fname} to ${targetStrip}`);
|
||||
|
||||
if (targetStrip === 'VISIBLE') {
|
||||
if (targetStrip === 'VISIBLE' || targetStrip === 'ACTIVE') {
|
||||
fileArchived[fname] = false;
|
||||
fileHidden[fname] = false;
|
||||
if (fileTags[fname]) {
|
||||
@@ -7945,6 +8011,8 @@
|
||||
|
||||
const faceBook = document.getElementById('lbFaceBook');
|
||||
if (faceBook) faceBook.style.display = 'none';
|
||||
const poseInfoWrap = document.getElementById('lbPoseInfoWrap');
|
||||
if (poseInfoWrap) poseInfoWrap.style.display = 'none';
|
||||
|
||||
updatePadPreview();
|
||||
toggleCheckerboard(false);
|
||||
@@ -8134,6 +8202,21 @@
|
||||
} else if (faceBook) {
|
||||
faceBook.style.display = 'none';
|
||||
}
|
||||
|
||||
const poseInfoWrap = document.getElementById('lbPoseInfoWrap');
|
||||
const poseDescEl = document.getElementById('lbPoseDescription');
|
||||
const poseCanvas = document.getElementById('lbPoseInfoCanvas');
|
||||
if (poseInfoWrap && poseDescEl && poseCanvas) {
|
||||
const desc = filePoseDescription[fname];
|
||||
const kpts = filePoseSkeleton[fname];
|
||||
if (desc || kpts) {
|
||||
poseInfoWrap.style.display = 'block';
|
||||
poseDescEl.textContent = desc || 'Pose detected';
|
||||
_renderPoseToCanvas(poseCanvas, kpts);
|
||||
} else {
|
||||
poseInfoWrap.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alias for old callsites
|
||||
@@ -8622,6 +8705,40 @@
|
||||
});
|
||||
}
|
||||
|
||||
const POSE_SKELETON = [[5,7],[7,9],[6,8],[8,10],[11,13],[13,15],[12,14],[14,16],[5,6],[11,12],[5,11],[6,12],[0,1],[0,2],[1,3],[2,4],[0,5],[0,6]];
|
||||
function _renderPoseToCanvas(canvas, kptsJson) {
|
||||
if (!canvas || !kptsJson) return;
|
||||
let kpts;
|
||||
try { kpts = typeof kptsJson === 'string' ? JSON.parse(kptsJson) : kptsJson; } catch(e) { return; }
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Auto-scale to fit
|
||||
const xs = kpts.filter(k => k[2] >= POSE_MIN_SCORE).map(k => k[0]);
|
||||
const ys = kpts.filter(k => k[2] >= POSE_MIN_SCORE).map(k => k[1]);
|
||||
if (!xs.length) return;
|
||||
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
||||
const minY = Math.min(...ys), maxY = Math.max(...ys);
|
||||
const w = maxX - minX, h = maxY - minY;
|
||||
const margin = 10;
|
||||
const scale = Math.min((canvas.width - margin*2) / (w || 1), (canvas.height - margin*2) / (h || 1));
|
||||
const offX = margin - minX * scale + (canvas.width - margin*2 - w * scale) / 2;
|
||||
const offY = margin - minY * scale + (canvas.height - margin*2 - h * scale) / 2;
|
||||
const cx = x => offX + x * scale, cy = y => offY + y * scale;
|
||||
|
||||
ctx.lineWidth = 2; ctx.strokeStyle = '#3b82f6';
|
||||
POSE_SKELETON.forEach(([a, b]) => {
|
||||
const pa = kpts[a], pb = kpts[b];
|
||||
if (!pa || !pb || pa[2] < POSE_MIN_SCORE || pb[2] < POSE_MIN_SCORE) return;
|
||||
ctx.beginPath(); ctx.moveTo(cx(pa[0]), cy(pa[1])); ctx.lineTo(cx(pb[0]), cy(pb[1])); ctx.stroke();
|
||||
});
|
||||
kpts.forEach((p, i) => {
|
||||
if (!p || p[2] < POSE_MIN_SCORE) return;
|
||||
ctx.beginPath(); ctx.arc(cx(p[0]), cy(p[1]), 2, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#fff'; ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
function _poseHitTest(mx, my) {
|
||||
const st = window._poseState; if (!st) return null;
|
||||
const cx = x => st.offX + x * st.scale, cy = y => st.offY + y * st.scale;
|
||||
@@ -10073,6 +10190,8 @@
|
||||
fileSortOrders[img.filename] = img.sort_order ?? null;
|
||||
fileHidden[img.filename] = !!img.hidden;
|
||||
fileArchived[img.filename] = !!img.archived;
|
||||
filePoseDescription[img.filename] = img.pose_description || null;
|
||||
filePoseSkeleton[img.filename] = img.pose_skeleton || null;
|
||||
fileTags[img.filename] = img.tags || [];
|
||||
fileIsSource[img.filename] = !!img.is_source;
|
||||
fileHasBg[img.filename] = img.has_background !== false;
|
||||
|
||||
Reference in New Issue
Block a user