From fca54607cbcb8bfea45a30f2e06b007f0f1ff101 Mon Sep 17 00:00:00 2001 From: TutorialsGHG <65071223+TutorialsGHG@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:39:07 +0200 Subject: [PATCH] 3.3 --- notifier.py | 4 +- pwa/index.html | 398 +++++++++++++++++++++++++++++++++------------- pwa/manifest.json | 2 +- pwa/sw.js | 2 +- 4 files changed, 294 insertions(+), 112 deletions(-) diff --git a/notifier.py b/notifier.py index f842356..3724b9c 100644 --- a/notifier.py +++ b/notifier.py @@ -923,6 +923,7 @@ def api_create_usecase(): "id": uid, "name": name, "color": data.get("color", "#4f8ef7"), + "group": data.get("group", "").strip(), "blocks": data.get("blocks", ["notifications"]), "min_interval": int(data.get("min_interval", 10)), "max_interval": int(data.get("max_interval", 30)), @@ -1130,6 +1131,7 @@ def pwa_app_name(): with _usecases_lock: usecases_for_pwa = [ {"id": u["id"], "name": u["name"], "color": u.get("color","#4f8ef7"), + "group": u.get("group", ""), "blocks": u.get("blocks", []), "min_interval": u.get("min_interval",10), "max_interval": u.get("max_interval",30)} for u in _usecases @@ -1294,4 +1296,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/pwa/index.html b/pwa/index.html index 9aae987..398f2bf 100644 --- a/pwa/index.html +++ b/pwa/index.html @@ -28,6 +28,7 @@ --yellow: #f59e0b; --text: #e2e2e8; --muted: #6b6b7b; + --mono: 'Space Mono', monospace; --safe-t: env(safe-area-inset-top, 0px); --safe-b: env(safe-area-inset-bottom, 0px); } @@ -70,7 +71,7 @@ display: none; } - /* ── SHARED BACKGROUND (main.png, persists across screens) ── */ + /* ── SHARED BACKGROUND ── */ #shared-bg { position: fixed; inset: 0; @@ -84,8 +85,6 @@ #shared-bg.loaded { opacity: 1; } - - /* Overlay darkening — heavier on selector, lighter on splash */ #shared-bg-overlay { position: fixed; inset: 0; @@ -174,7 +173,7 @@ .sel-header { position: relative; z-index: 2; - padding: calc(var(--safe-t) + 22px) 20px 16px; + padding: calc(var(--safe-t) + 22px) 20px 14px; text-align: center; flex-shrink: 0; border-bottom: 1px solid rgba(255, 255, 255, 0.08); @@ -193,79 +192,196 @@ margin-top: 4px; } + /* ── TREE VIEW ── */ .uc-list { flex: 1; overflow-y: auto; position: relative; z-index: 2; - padding: 14px 16px calc(var(--safe-b) + 16px); + padding: 12px 14px calc(var(--safe-b) + 16px); display: flex; flex-direction: column; - gap: 10px; + gap: 2px; } - .uc-item { - background: rgba(17, 17, 21, 0.75); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 14px; - padding: 15px 17px; + + .tree-root { + width: 100%; + display: flex; + flex-direction: column; + gap: 2px; + } + + /* Folder row */ + .tree-folder { + display: flex; + flex-direction: column; + } + .tree-folder-hdr { display: flex; align-items: center; - gap: 13px; + gap: 0; + padding: 8px 10px 8px 6px; cursor: pointer; - transition: 0.2s; - position: relative; - overflow: hidden; - backdrop-filter: blur(6px); - -webkit-backdrop-filter: blur(6px); + border-radius: 10px; + user-select: none; + transition: background 0.12s; + background: rgba(17, 17, 21, 0.5); + border: 1px solid rgba(255, 255, 255, 0.07); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + margin-bottom: 2px; } - .uc-item::before { + .tree-folder-hdr:active { + background: rgba(255, 255, 255, 0.07); + } + + /* Indent guides */ + .tree-indent { + display: flex; + flex-shrink: 0; + } + .tree-guide { + width: 20px; + flex-shrink: 0; + position: relative; + display: flex; + justify-content: center; + } + .tree-guide::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 1px; + background: rgba(255, 255, 255, 0.1); + } + .tree-guide.last::before { + bottom: 50%; + } + + /* Arrow + icon */ + .tree-arrow { + width: 18px; + height: 18px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.6rem; + color: var(--muted); + transition: transform 0.18s; + } + .tree-arrow.open { + transform: rotate(90deg); + } + .tree-folder-icon { + font-size: 0.95rem; + margin-right: 8px; + flex-shrink: 0; + } + .tree-folder-label { + font-size: 0.85rem; + font-weight: 700; + color: rgba(255, 255, 255, 0.85); + flex: 1; + } + .tree-folder-count { + font-size: 0.6rem; + color: var(--muted); + font-family: var(--mono); + background: rgba(0,0,0,0.3); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 999px; + padding: 1px 7px; + margin-left: 8px; + } + .tree-children { + display: flex; + flex-direction: column; + gap: 2px; + padding-left: 10px; + margin-bottom: 4px; + } + .tree-children.collapsed { + display: none; + } + + /* Leaf = usecase item */ + .tree-leaf { + display: flex; + align-items: center; + gap: 0; + cursor: pointer; + border-radius: 10px; + transition: background 0.12s; + background: rgba(17, 17, 21, 0.6); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + overflow: hidden; + position: relative; + } + .tree-leaf::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; - width: 4px; + width: 3px; background: var(--uc-color, var(--accent)); - border-radius: 14px 0 0 14px; } - .uc-item:active { + .tree-leaf:active { transform: scale(0.98); + background: rgba(255, 255, 255, 0.07); } - .uc-item.active { - border-color: rgba(255, 255, 255, 0.2); - background: rgba(79, 142, 247, 0.12); + .tree-leaf.active-uc { + border-color: rgba(255, 255, 255, 0.18); + background: rgba(79, 142, 247, 0.1); } - .uc-dot { - width: 38px; - height: 38px; - border-radius: 11px; - flex-shrink: 0; - background: var(--uc-color, var(--accent)); - opacity: 0.25; + .tree-leaf-inner { + flex: 1; display: flex; align-items: center; - justify-content: center; - font-size: 1.1rem; - } - .uc-item.active .uc-dot { - opacity: 0.45; - } - .uc-info { - flex: 1; + gap: 10px; + padding: 10px 12px 10px 10px; min-width: 0; } - .uc-name-big { - font-size: 0.92rem; + .tree-leaf-color { + width: 9px; + height: 9px; + border-radius: 50%; + flex-shrink: 0; + background: var(--uc-color, var(--accent)); + box-shadow: 0 0 6px var(--uc-color, var(--accent)); + } + .tree-leaf-name { + font-size: 0.88rem; font-weight: 600; color: rgba(255, 255, 255, 0.9); + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } - .uc-blocks-sm { - font-size: 0.63rem; - color: rgba(255, 255, 255, 0.4); - margin-top: 3px; - font-family: 'Space Mono', monospace; + .tree-leaf-chips { + display: flex; + flex-wrap: wrap; + gap: 3px; + flex-shrink: 0; } - .uc-active-tag { + .tree-leaf-chip { + padding: 1px 6px; + border-radius: 4px; + font-size: 0.6rem; + font-family: var(--mono); + font-weight: 600; + background: rgba(79, 142, 247, 0.1); + color: var(--accent); + border: 1px solid rgba(79, 142, 247, 0.2); + } + .tree-leaf-active-tag { font-size: 0.58rem; font-family: 'Space Mono', monospace; color: var(--green); @@ -277,10 +393,11 @@ letter-spacing: 0.08em; flex-shrink: 0; } - .uc-arrow { - color: rgba(255, 255, 255, 0.25); - font-size: 0.9rem; + .tree-leaf-arrow { + color: rgba(255, 255, 255, 0.2); + font-size: 0.85rem; flex-shrink: 0; + padding-right: 2px; } /* ── APP ── */ @@ -378,15 +495,9 @@ font-size: 0.92rem; font-weight: 700; } - .sv.green { - color: var(--green); - } - .sv.yellow { - color: var(--yellow); - } - .sv.accent { - color: var(--accent); - } + .sv.green { color: var(--green); } + .sv.yellow { color: var(--yellow); } + .sv.accent { color: var(--accent); } /* Cards */ .c { @@ -476,25 +587,19 @@ background: var(--accent); color: #fff; } - .btn-accent:active { - filter: brightness(0.9); - } + .btn-accent:active { filter: brightness(0.9); } .btn-ghost { background: rgba(255, 255, 255, 0.07); color: var(--text); border: 1px solid rgba(255, 255, 255, 0.1); } - .btn-ghost:active { - background: rgba(255, 255, 255, 0.12); - } + .btn-ghost:active { background: rgba(255, 255, 255, 0.12); } .btn-green { background: rgba(34, 197, 94, 0.1); color: var(--green); border: 1px solid rgba(34, 197, 94, 0.2); } - .btn-green:active { - background: rgba(34, 197, 94, 0.2); - } + .btn-green:active { background: rgba(34, 197, 94, 0.2); } /* Usecase chip at top of app */ .uc-chip { @@ -837,7 +942,6 @@ // ── Screen transitions ───────────────────────────────────── function show(id) { - // Update background overlay style const overlay = document.getElementById('shared-bg-overlay'); overlay.className = 'for-' + @@ -866,7 +970,6 @@ // ── Init ────────────────────────────────────────────────── async function init() { - // Load splash image and keep it as shared background const splashR = await fetchJSON('/api/pwa/splash_image').catch( () => null, ); @@ -879,7 +982,6 @@ }); } - // Load app info const info = await fetchJSON('/api/pwa/app_name').catch(() => null); if (info) { appInfo = info; @@ -893,7 +995,6 @@ settingsName.toUpperCase(); document.getElementById('splashSub').textContent = `${(info.usecases || []).length} USECASE(S)`; - // Apply saved background appearance applyBgSettings( info.pwa_bg_blur !== undefined ? info.pwa_bg_blur : 18, info.pwa_bg_opacity !== undefined ? info.pwa_bg_opacity : 0.72, @@ -904,7 +1005,93 @@ startOverlayPoll(); } - // ── Selector list ───────────────────────────────────────── + // ── Tree builder ────────────────────────────────────────── + const SEP = /\s*(?:\/|>|\u203a)\s*/; + let _treeGid = 0; + + function insertIntoTree(node, parts, uc) { + if (!parts.length) { + node._items.push(uc); + return; + } + const k = parts[0]; + if (!node[k]) node[k] = { _items: [] }; + insertIntoTree(node[k], parts.slice(1), uc); + } + + function countLeaves(node) { + let n = (node._items || []).length; + Object.keys(node) + .filter((k) => k !== '_items') + .forEach((k) => (n += countLeaves(node[k]))); + return n; + } + + function leafHtml(uc, depth) { + const indent = Array.from( + { length: depth }, + () => '', + ).join(''); + const chips = (uc.blocks || []) + .slice(0, 3) + .map((bid) => { + const b = allBlocks.find((x) => x.id === bid); + return b ? `${b.icon} ${b.name}` : ''; + }) + .join(''); + const isActive = uc.id === activeUCId; + const rightEl = isActive + ? 'Active' + : ''; + return `
+
${indent}
+
+
+ ${escHtml(uc.name)} +
${chips}
+ ${rightEl} +
+
`; + } + + function renderNode(node, depth) { + let html = ''; + (node._items || []).forEach((uc) => { + html += leafHtml(uc, depth); + }); + Object.keys(node) + .filter((k) => k !== '_items') + .forEach((key) => { + const child = node[key]; + const id = 'tg-' + ++_treeGid; + const count = countLeaves(child); + const indent = Array.from( + { length: depth }, + () => '', + ).join(''); + html += `
+
+
${indent}
+ + 📁 + ${escHtml(key)} + ${count} +
+
${renderNode(child, depth + 1)}
+
`; + }); + return html; + } + + function toggleGroup(id) { + const body = document.getElementById(id); + const arr = document.getElementById('arr-' + id); + if (!body) return; + const collapsed = body.classList.toggle('collapsed'); + if (arr) arr.classList.toggle('open', !collapsed); + } + + // ── Selector list (tree view) ───────────────────────────── async function loadSelectorList() { const r = await fetchJSON('/api/pwa/app_name').catch(() => null); if (!r) { @@ -921,31 +1108,29 @@ allBlocks = r.blocks || []; const usecases = r.usecases || []; - document.getElementById('ucList').innerHTML = usecases.length - ? usecases - .map((uc) => { - const isActive = uc.id === activeUCId; - const blockIcons = (uc.blocks || []) - .map((bid) => { - const b = allBlocks.find((x) => x.id === bid); - return b ? b.icon : ''; - }) - .join(' '); - return `
-
${uc.name[0]}
-
-
${escHtml(uc.name)}
-
${blockIcons}
-
- ${isActive ? 'Active' : ''} -
`; - }) - .join('') - : '

No usecases yet.
Create them in the desktop UI.

'; + if (!usecases.length) { + document.getElementById('ucList').innerHTML = + '

No usecases yet.
Create them in the desktop UI.

'; + return; + } + + // Build tree + _treeGid = 0; + const tree = { _items: [] }; + usecases.forEach((uc) => { + const path = (uc.group || '').trim(); + if (!path) { + tree._items.push(uc); + return; + } + insertIntoTree(tree, path.split(SEP).filter(Boolean), uc); + }); + + document.getElementById('ucList').innerHTML = + `
${renderNode(tree, 0)}
`; } async function pickUC(id, color) { - // Activate on server await postJSON('/api/pwa/activate_usecase', { usecase_id: id }).catch( () => null, ); @@ -992,7 +1177,6 @@ : 0; document.getElementById('a-next').textContent = next > 0 ? `${Math.floor(next / 60)}m ${next % 60}s` : '—'; - // Overlay must vanish immediately when paused if (paused && _overlayActive) hideOverlay(); } @@ -1127,7 +1311,6 @@ el.classList.add('visible'); _overlayActive = true; } - // Auto-dismiss after remaining_ms (keep in sync with desktop) clearTimeout(_overlayDismissTimer); if (r.remaining_ms > 0) { _overlayDismissTimer = setTimeout(hideOverlay, r.remaining_ms); @@ -1142,7 +1325,6 @@ _overlayActive = false; const el = document.getElementById('pwa-overlay'); el.classList.remove('visible'); - // Clear src after transition so old image doesn't flash setTimeout(() => { if (!_overlayActive) document.getElementById('pwa-overlay-img').src = ''; @@ -1156,7 +1338,6 @@ function applyBgSettings(blur, opac) { _bgBlur = blur; _bgOpac = opac; - // Rebuild the dynamic CSS classes for selector and app let styleEl = document.getElementById('bg-dynamic-style'); if (!styleEl) { styleEl = document.createElement('style'); @@ -1164,16 +1345,15 @@ document.head.appendChild(styleEl); } styleEl.textContent = ` - #shared-bg-overlay.for-selector { - background: rgba(8,8,10,${opac}) !important; - backdrop-filter: blur(${blur}px) saturate(.7) !important; - -webkit-backdrop-filter: blur(${blur}px) saturate(.7) !important; - } - #shared-bg-overlay.for-app { - background: rgba(8,8,10,${Math.min(opac + 0.16, 0.97)}) !important; - } - `; - // Sync slider UI + #shared-bg-overlay.for-selector { + background: rgba(8,8,10,${opac}) !important; + backdrop-filter: blur(${blur}px) saturate(.7) !important; + -webkit-backdrop-filter: blur(${blur}px) saturate(.7) !important; + } + #shared-bg-overlay.for-app { + background: rgba(8,8,10,${Math.min(opac + 0.16, 0.97)}) !important; + } +`; document.getElementById('blurSlider').value = blur; document.getElementById('opacSlider').value = Math.round(opac * 100); document.getElementById('blurVal').textContent = blur + 'px'; diff --git a/pwa/manifest.json b/pwa/manifest.json index 0aa46c5..ae748f4 100644 --- a/pwa/manifest.json +++ b/pwa/manifest.json @@ -12,4 +12,4 @@ { "src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ] -} +} \ No newline at end of file diff --git a/pwa/sw.js b/pwa/sw.js index dd9a978..ba19947 100644 --- a/pwa/sw.js +++ b/pwa/sw.js @@ -23,4 +23,4 @@ self.addEventListener('fetch', e => { e.respondWith( caches.match(e.request).then(r => r || fetch(e.request)) ); -}); +}); \ No newline at end of file