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 `
No usecases yet.
Create them in the desktop UI.
No usecases yet.
Create them in the desktop UI.