lets deploy parallel workers to speed up the development process. make the app fetch current settings, app data from android to display in the application drawer. the navigation gestures and other daily use settings like the ones in notification bar should comply with current android settings. help me build an apk ready pwa stack that can be tested on samsung s23 ultra
Browse files- README.md +7 -4
- components/arch-app-drawer.js +278 -0
- components/arch-app-grid.js +31 -0
- components/arch-app-icon.js +123 -0
- components/arch-server-panel.js +305 -0
- components/arch-settings-panel.js +358 -0
- components/arch-status-bar.js +108 -0
- filename.html +2 -0
- index.html +135 -18
- js/android-bridge.js +194 -0
- js/gesture-controller.js +349 -0
- js/worker-bridge.js +161 -0
- manifest.json +121 -0
- script.js +246 -0
- style.css +104 -18
- sw.js +133 -0
- workers/app-data-worker.js +164 -0
- workers/gesture-worker.js +117 -0
- workers/notification-worker.js +139 -0
- workers/settings-worker.js +110 -0
- workers/system-sync-worker.js +158 -0
README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji: 👀
|
| 4 |
colorFrom: yellow
|
| 5 |
-
colorTo:
|
|
|
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ArchDroid Parallel Workers 🚀
|
|
|
|
| 3 |
colorFrom: yellow
|
| 4 |
+
colorTo: red
|
| 5 |
+
emoji: 🐳
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite-v3
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Welcome to your new DeepSite project!
|
| 13 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
components/arch-app-drawer.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ArchAppDrawer extends HTMLElement {
|
| 2 |
+
constructor() {
|
| 3 |
+
super();
|
| 4 |
+
this.attachShadow({ mode: 'open' });
|
| 5 |
+
this.apps = [];
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
connectedCallback() {
|
| 9 |
+
this.render();
|
| 10 |
+
this.loadApps();
|
| 11 |
+
this.setupSearch();
|
| 12 |
+
this.setupCloseGesture();
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
render() {
|
| 16 |
+
this.shadowRoot.innerHTML = `
|
| 17 |
+
<style>
|
| 18 |
+
:host {
|
| 19 |
+
display: flex;
|
| 20 |
+
flex-direction: column;
|
| 21 |
+
background: #0f172a;
|
| 22 |
+
height: 100vh;
|
| 23 |
+
width: 100vw;
|
| 24 |
+
position: fixed;
|
| 25 |
+
top: 0;
|
| 26 |
+
left: 0;
|
| 27 |
+
font-family: 'JetBrains Mono', monospace;
|
| 28 |
+
}
|
| 29 |
+
.header {
|
| 30 |
+
padding: calc(env(safe-area-inset-top, 24px) + 16px) 24px 16px;
|
| 31 |
+
background: linear-gradient(to bottom, #020617, transparent);
|
| 32 |
+
border-bottom: 1px solid #334155;
|
| 33 |
+
}
|
| 34 |
+
.search-container {
|
| 35 |
+
display: flex;
|
| 36 |
+
align-items: center;
|
| 37 |
+
gap: 12px;
|
| 38 |
+
background: rgba(51, 65, 85, 0.5);
|
| 39 |
+
border: 1px solid #475569;
|
| 40 |
+
border-radius: 8px;
|
| 41 |
+
padding: 12px 16px;
|
| 42 |
+
margin-bottom: 8px;
|
| 43 |
+
}
|
| 44 |
+
.search-icon {
|
| 45 |
+
color: #06b6d4;
|
| 46 |
+
width: 18px;
|
| 47 |
+
height: 18px;
|
| 48 |
+
}
|
| 49 |
+
input {
|
| 50 |
+
background: transparent;
|
| 51 |
+
border: none;
|
| 52 |
+
color: #e2e8f0;
|
| 53 |
+
font-family: inherit;
|
| 54 |
+
font-size: 14px;
|
| 55 |
+
outline: none;
|
| 56 |
+
width: 100%;
|
| 57 |
+
caret-color: #06b6d4;
|
| 58 |
+
}
|
| 59 |
+
input::placeholder {
|
| 60 |
+
color: #64748b;
|
| 61 |
+
}
|
| 62 |
+
.hint {
|
| 63 |
+
font-size: 11px;
|
| 64 |
+
color: #64748b;
|
| 65 |
+
margin-left: 8px;
|
| 66 |
+
}
|
| 67 |
+
.app-list {
|
| 68 |
+
flex: 1;
|
| 69 |
+
overflow-y: auto;
|
| 70 |
+
padding: 8px 16px calc(env(safe-area-inset-bottom, 16px) + 80px);
|
| 71 |
+
}
|
| 72 |
+
.category {
|
| 73 |
+
margin: 16px 0 8px;
|
| 74 |
+
font-size: 10px;
|
| 75 |
+
text-transform: uppercase;
|
| 76 |
+
letter-spacing: 2px;
|
| 77 |
+
color: #06b6d4;
|
| 78 |
+
border-left: 2px solid #06b6d4;
|
| 79 |
+
padding-left: 8px;
|
| 80 |
+
}
|
| 81 |
+
.app-item {
|
| 82 |
+
display: flex;
|
| 83 |
+
align-items: center;
|
| 84 |
+
gap: 16px;
|
| 85 |
+
padding: 12px;
|
| 86 |
+
margin: 4px 0;
|
| 87 |
+
border-radius: 8px;
|
| 88 |
+
cursor: pointer;
|
| 89 |
+
transition: all 0.2s;
|
| 90 |
+
border: 1px solid transparent;
|
| 91 |
+
}
|
| 92 |
+
.app-item:hover, .app-item:active {
|
| 93 |
+
background: rgba(51, 65, 85, 0.5);
|
| 94 |
+
border-color: rgba(6, 182, 212, 0.3);
|
| 95 |
+
}
|
| 96 |
+
.app-icon-small {
|
| 97 |
+
width: 40px;
|
| 98 |
+
height: 40px;
|
| 99 |
+
background: rgba(51, 65, 85, 0.6);
|
| 100 |
+
border: 1px solid rgba(100, 116, 139, 0.4);
|
| 101 |
+
border-radius: 12px;
|
| 102 |
+
display: flex;
|
| 103 |
+
align-items: center;
|
| 104 |
+
justify-content: center;
|
| 105 |
+
color: #e2e8f0;
|
| 106 |
+
}
|
| 107 |
+
.app-info {
|
| 108 |
+
flex: 1;
|
| 109 |
+
}
|
| 110 |
+
.app-name {
|
| 111 |
+
font-size: 14px;
|
| 112 |
+
color: #e2e8f0;
|
| 113 |
+
margin-bottom: 2px;
|
| 114 |
+
}
|
| 115 |
+
.app-package {
|
| 116 |
+
font-size: 11px;
|
| 117 |
+
color: #64748b;
|
| 118 |
+
font-family: monospace;
|
| 119 |
+
}
|
| 120 |
+
.handle {
|
| 121 |
+
position: absolute;
|
| 122 |
+
top: env(safe-area-inset-top, 12px);
|
| 123 |
+
left: 50%;
|
| 124 |
+
transform: translateX(-50%);
|
| 125 |
+
width: 40px;
|
| 126 |
+
height: 4px;
|
| 127 |
+
background: #475569;
|
| 128 |
+
border-radius: 2px;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
}
|
| 131 |
+
.close-hint {
|
| 132 |
+
position: absolute;
|
| 133 |
+
bottom: 24px;
|
| 134 |
+
left: 50%;
|
| 135 |
+
transform: translateX(-50%);
|
| 136 |
+
color: #64748b;
|
| 137 |
+
font-size: 11px;
|
| 138 |
+
display: flex;
|
| 139 |
+
align-items: center;
|
| 140 |
+
gap: 6px;
|
| 141 |
+
opacity: 0.7;
|
| 142 |
+
}
|
| 143 |
+
.arrow {
|
| 144 |
+
animation: bounce 1s infinite;
|
| 145 |
+
}
|
| 146 |
+
@keyframes bounce {
|
| 147 |
+
0%, 100% { transform: translateY(0); }
|
| 148 |
+
50% { transform: translateY(-4px); }
|
| 149 |
+
}
|
| 150 |
+
.no-results {
|
| 151 |
+
text-align: center;
|
| 152 |
+
color: #64748b;
|
| 153 |
+
padding: 48px 24px;
|
| 154 |
+
font-size: 13px;
|
| 155 |
+
}
|
| 156 |
+
</style>
|
| 157 |
+
<div class="handle"></div>
|
| 158 |
+
<div class="header">
|
| 159 |
+
<div class="search-container">
|
| 160 |
+
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 161 |
+
<circle cx="11" cy="11" r="8"></circle>
|
| 162 |
+
<path d="m21 21-4.35-4.35"></path>
|
| 163 |
+
</svg>
|
| 164 |
+
<input type="text" placeholder="search packages..." id="search-input">
|
| 165 |
+
</div>
|
| 166 |
+
<div class="hint">$ pacman -Ss [query]</div>
|
| 167 |
+
</div>
|
| 168 |
+
<div class="app-list" id="app-list"></div>
|
| 169 |
+
<div class="close-hint">
|
| 170 |
+
<span>swipe down to close</span>
|
| 171 |
+
<span class="arrow">↓</span>
|
| 172 |
+
</div>
|
| 173 |
+
`;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
loadApps() {
|
| 177 |
+
// Get apps from main script
|
| 178 |
+
if (window.launcher) {
|
| 179 |
+
this.apps = window.launcher.getAllApps();
|
| 180 |
+
this.renderAppList(this.apps);
|
| 181 |
+
} else {
|
| 182 |
+
// Fallback
|
| 183 |
+
setTimeout(() => this.loadApps(), 100);
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
renderAppList(apps) {
|
| 188 |
+
const list = this.shadowRoot.getElementById('app-list');
|
| 189 |
+
if (!list) return;
|
| 190 |
+
|
| 191 |
+
// Group by category
|
| 192 |
+
const grouped = apps.reduce((acc, app) => {
|
| 193 |
+
if (!acc[app.category]) acc[app.category] = [];
|
| 194 |
+
acc[app.category].push(app);
|
| 195 |
+
return acc;
|
| 196 |
+
}, {});
|
| 197 |
+
|
| 198 |
+
list.innerHTML = '';
|
| 199 |
+
|
| 200 |
+
Object.entries(grouped).forEach(([category, categoryApps]) => {
|
| 201 |
+
const catEl = document.createElement('div');
|
| 202 |
+
catEl.className = 'category';
|
| 203 |
+
catEl.textContent = category;
|
| 204 |
+
list.appendChild(catEl);
|
| 205 |
+
|
| 206 |
+
categoryApps.forEach(app => {
|
| 207 |
+
const item = document.createElement('div');
|
| 208 |
+
item.className = 'app-item';
|
| 209 |
+
item.innerHTML = `
|
| 210 |
+
<div class="app-icon-small">
|
| 211 |
+
<i data-feather="${app.icon}" style="width: 20px; height: 20px;"></i>
|
| 212 |
+
</div>
|
| 213 |
+
<div class="app-info">
|
| 214 |
+
<div class="app-name">${app.name}</div>
|
| 215 |
+
<div class="app-package">:${app.category}/${app.name.toLowerCase()}</div>
|
| 216 |
+
</div>
|
| 217 |
+
`;
|
| 218 |
+
item.addEventListener('click', () => {
|
| 219 |
+
this.dispatchEvent(new CustomEvent('launch-app', {
|
| 220 |
+
bubbles: true,
|
| 221 |
+
composed: true,
|
| 222 |
+
detail: app
|
| 223 |
+
}));
|
| 224 |
+
});
|
| 225 |
+
list.appendChild(item);
|
| 226 |
+
});
|
| 227 |
+
});
|
| 228 |
+
|
| 229 |
+
if (window.feather) {
|
| 230 |
+
feather.replace({ parent: this.shadowRoot });
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
setupSearch() {
|
| 235 |
+
const input = this.shadowRoot.getElementById('search-input');
|
| 236 |
+
if (!input) return;
|
| 237 |
+
|
| 238 |
+
input.addEventListener('input', (e) => {
|
| 239 |
+
const query = e.target.value.toLowerCase();
|
| 240 |
+
if (!query) {
|
| 241 |
+
this.renderAppList(this.apps);
|
| 242 |
+
return;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
const filtered = this.apps.filter(app =>
|
| 246 |
+
app.name.toLowerCase().includes(query) ||
|
| 247 |
+
app.category.toLowerCase().includes(query)
|
| 248 |
+
);
|
| 249 |
+
|
| 250 |
+
const list = this.shadowRoot.getElementById('app-list');
|
| 251 |
+
if (filtered.length === 0) {
|
| 252 |
+
list.innerHTML = '<div class="no-results">no packages found matching "' + query + '"</div>';
|
| 253 |
+
} else {
|
| 254 |
+
this.renderAppList(filtered);
|
| 255 |
+
}
|
| 256 |
+
});
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
setupCloseGesture() {
|
| 260 |
+
let startY = 0;
|
| 261 |
+
const drawer = this;
|
| 262 |
+
|
| 263 |
+
drawer.addEventListener('touchstart', (e) => {
|
| 264 |
+
startY = e.touches[0].clientY;
|
| 265 |
+
}, { passive: true });
|
| 266 |
+
|
| 267 |
+
drawer.addEventListener('touchmove', (e) => {
|
| 268 |
+
const currentY = e.touches[0].clientY;
|
| 269 |
+
const diff = currentY - startY;
|
| 270 |
+
|
| 271 |
+
if (diff > 0 && drawer.scrollTop === 0) {
|
| 272 |
+
// User is pulling down at top of scroll
|
| 273 |
+
}
|
| 274 |
+
}, { passive: true });
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
customElements.define('arch-app-drawer', ArchAppDrawer);
|
components/arch-app-grid.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ArchAppGrid extends HTMLElement {
|
| 2 |
+
constructor() {
|
| 3 |
+
super();
|
| 4 |
+
this.attachShadow({ mode: 'open' });
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
connectedCallback() {
|
| 8 |
+
this.render();
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
render() {
|
| 12 |
+
this.shadowRoot.innerHTML = `
|
| 13 |
+
<style>
|
| 14 |
+
:host {
|
| 15 |
+
display: grid;
|
| 16 |
+
grid-template-columns: repeat(4, 1fr);
|
| 17 |
+
gap: 16px;
|
| 18 |
+
padding: 8px;
|
| 19 |
+
}
|
| 20 |
+
@media (min-width: 768px) {
|
| 21 |
+
:host {
|
| 22 |
+
grid-template-columns: repeat(6, 1fr);
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
</style>
|
| 26 |
+
<slot></slot>
|
| 27 |
+
`;
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
customElements.define('arch-app-grid', ArchAppGrid);
|
components/arch-app-icon.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ArchAppIcon extends HTMLElement {
|
| 2 |
+
constructor() {
|
| 3 |
+
super();
|
| 4 |
+
this.attachShadow({ mode: 'open' });
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
connectedCallback() {
|
| 8 |
+
this.render();
|
| 9 |
+
this.setupInteraction();
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
static get observedAttributes() {
|
| 13 |
+
return ['name', 'icon', 'package'];
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
render() {
|
| 17 |
+
const name = this.getAttribute('name') || 'App';
|
| 18 |
+
const icon = this.getAttribute('icon') || 'box';
|
| 19 |
+
|
| 20 |
+
this.shadowRoot.innerHTML = `
|
| 21 |
+
<style>
|
| 22 |
+
:host {
|
| 23 |
+
display: flex;
|
| 24 |
+
flex-direction: column;
|
| 25 |
+
align-items: center;
|
| 26 |
+
gap: 8px;
|
| 27 |
+
cursor: pointer;
|
| 28 |
+
user-select: none;
|
| 29 |
+
-webkit-tap-highlight-color: transparent;
|
| 30 |
+
}
|
| 31 |
+
.icon-container {
|
| 32 |
+
width: 56px;
|
| 33 |
+
height: 56px;
|
| 34 |
+
background: rgba(51, 65, 85, 0.6);
|
| 35 |
+
border: 1px solid rgba(100, 116, 139, 0.5);
|
| 36 |
+
border-radius: 16px;
|
| 37 |
+
display: flex;
|
| 38 |
+
align-items: center;
|
| 39 |
+
justify-content: center;
|
| 40 |
+
transition: all 0.2s ease;
|
| 41 |
+
position: relative;
|
| 42 |
+
overflow: hidden;
|
| 43 |
+
}
|
| 44 |
+
.icon-container::before {
|
| 45 |
+
content: '';
|
| 46 |
+
position: absolute;
|
| 47 |
+
top: 0;
|
| 48 |
+
left: 0;
|
| 49 |
+
right: 0;
|
| 50 |
+
height: 1px;
|
| 51 |
+
background: linear-gradient(90deg, transparent, rgba(6, 182, 212, 0.5), transparent);
|
| 52 |
+
}
|
| 53 |
+
:host(:active) .icon-container {
|
| 54 |
+
transform: scale(0.92);
|
| 55 |
+
background: rgba(6, 182, 212, 0.2);
|
| 56 |
+
border-color: rgba(6, 182, 212, 0.8);
|
| 57 |
+
box-shadow: 0 0 20px rgba(6, 182, 212, 0.3);
|
| 58 |
+
}
|
| 59 |
+
svg {
|
| 60 |
+
width: 24px;
|
| 61 |
+
height: 24px;
|
| 62 |
+
color: #e2e8f0;
|
| 63 |
+
stroke-width: 2;
|
| 64 |
+
transition: color 0.2s;
|
| 65 |
+
}
|
| 66 |
+
:host(:active) svg {
|
| 67 |
+
color: #22d3ee;
|
| 68 |
+
}
|
| 69 |
+
.label {
|
| 70 |
+
font-size: 11px;
|
| 71 |
+
color: #94a3b8;
|
| 72 |
+
text-align: center;
|
| 73 |
+
max-width: 70px;
|
| 74 |
+
overflow: hidden;
|
| 75 |
+
text-overflow: ellipsis;
|
| 76 |
+
white-space: nowrap;
|
| 77 |
+
font-family: 'JetBrains Mono', monospace;
|
| 78 |
+
}
|
| 79 |
+
.launching {
|
| 80 |
+
animation: launch 0.4s ease-out forwards;
|
| 81 |
+
}
|
| 82 |
+
@keyframes launch {
|
| 83 |
+
to {
|
| 84 |
+
transform: scale(3);
|
| 85 |
+
opacity: 0;
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
</style>
|
| 89 |
+
<div class="icon-container">
|
| 90 |
+
<i data-feather="${icon}"></i>
|
| 91 |
+
</div>
|
| 92 |
+
<span class="label">${name}</span>
|
| 93 |
+
`;
|
| 94 |
+
|
| 95 |
+
// Re-initialize feather icons for shadow DOM
|
| 96 |
+
if (window.feather) {
|
| 97 |
+
feather.replace({
|
| 98 |
+
parent: this.shadowRoot,
|
| 99 |
+
width: 24,
|
| 100 |
+
height: 24
|
| 101 |
+
});
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
setupInteraction() {
|
| 106 |
+
this.addEventListener('click', () => {
|
| 107 |
+
const name = this.getAttribute('name');
|
| 108 |
+
const package = this.getAttribute('package');
|
| 109 |
+
|
| 110 |
+
this.dispatchEvent(new CustomEvent('launch-app', {
|
| 111 |
+
bubbles: true,
|
| 112 |
+
composed: true,
|
| 113 |
+
detail: { name, package }
|
| 114 |
+
}));
|
| 115 |
+
|
| 116 |
+
// Visual feedback
|
| 117 |
+
this.classList.add('launching');
|
| 118 |
+
setTimeout(() => this.classList.remove('launching'), 400);
|
| 119 |
+
});
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
customElements.define('arch-app-icon', ArchAppIcon);
|
components/arch-server-panel.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ArchServerPanel extends HTMLElement {
|
| 2 |
+
constructor() {
|
| 3 |
+
super();
|
| 4 |
+
this.attachShadow({ mode: 'open' });
|
| 5 |
+
this.services = [
|
| 6 |
+
{ name: 'Plex', port: 32400, status: 'unknown' },
|
| 7 |
+
{ name: 'Nextcloud', port: 8080, status: 'unknown' },
|
| 8 |
+
{ name: 'Pi-hole', port: 80, status: 'unknown' },
|
| 9 |
+
{ name: 'SSH', port: 22, status: 'unknown' },
|
| 10 |
+
{ name: 'Home Assistant', port: 8123, status: 'unknown' }
|
| 11 |
+
];
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
connectedCallback() {
|
| 15 |
+
this.render();
|
| 16 |
+
this.checkServices();
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
render() {
|
| 20 |
+
this.shadowRoot.innerHTML = `
|
| 21 |
+
<style>
|
| 22 |
+
:host {
|
| 23 |
+
display: flex;
|
| 24 |
+
flex-direction: column;
|
| 25 |
+
height: 100vh;
|
| 26 |
+
width: 100vw;
|
| 27 |
+
background: #020617;
|
| 28 |
+
font-family: 'JetBrains Mono', monospace;
|
| 29 |
+
}
|
| 30 |
+
.header {
|
| 31 |
+
padding: calc(env(safe-area-inset-top, 24px) + 24px) 24px 24px;
|
| 32 |
+
border-bottom: 1px solid #1e293b;
|
| 33 |
+
display: flex;
|
| 34 |
+
justify-content: space-between;
|
| 35 |
+
align-items: center;
|
| 36 |
+
}
|
| 37 |
+
.title {
|
| 38 |
+
font-size: 18px;
|
| 39 |
+
color: #06b6d4;
|
| 40 |
+
font-weight: 700;
|
| 41 |
+
}
|
| 42 |
+
.subtitle {
|
| 43 |
+
font-size: 11px;
|
| 44 |
+
color: #64748b;
|
| 45 |
+
margin-top: 4px;
|
| 46 |
+
}
|
| 47 |
+
.close-btn {
|
| 48 |
+
width: 32px;
|
| 49 |
+
height: 32px;
|
| 50 |
+
border: 1px solid #334155;
|
| 51 |
+
border-radius: 8px;
|
| 52 |
+
display: flex;
|
| 53 |
+
align-items: center;
|
| 54 |
+
justify-content: center;
|
| 55 |
+
color: #94a3b8;
|
| 56 |
+
cursor: pointer;
|
| 57 |
+
background: transparent;
|
| 58 |
+
}
|
| 59 |
+
.close-btn:active {
|
| 60 |
+
background: #334155;
|
| 61 |
+
color: #e2e8f0;
|
| 62 |
+
}
|
| 63 |
+
.content {
|
| 64 |
+
flex: 1;
|
| 65 |
+
overflow-y: auto;
|
| 66 |
+
padding: 24px;
|
| 67 |
+
}
|
| 68 |
+
.section {
|
| 69 |
+
margin-bottom: 32px;
|
| 70 |
+
}
|
| 71 |
+
.section-title {
|
| 72 |
+
font-size: 11px;
|
| 73 |
+
text-transform: uppercase;
|
| 74 |
+
letter-spacing: 2px;
|
| 75 |
+
color: #475569;
|
| 76 |
+
margin-bottom: 16px;
|
| 77 |
+
border-bottom: 1px solid #1e293b;
|
| 78 |
+
padding-bottom: 8px;
|
| 79 |
+
}
|
| 80 |
+
.service-item {
|
| 81 |
+
display: flex;
|
| 82 |
+
align-items: center;
|
| 83 |
+
justify-content: space-between;
|
| 84 |
+
padding: 16px;
|
| 85 |
+
background: rgba(15, 23, 42, 0.6);
|
| 86 |
+
border: 1px solid #1e293b;
|
| 87 |
+
border-radius: 12px;
|
| 88 |
+
margin-bottom: 12px;
|
| 89 |
+
}
|
| 90 |
+
.service-info {
|
| 91 |
+
display: flex;
|
| 92 |
+
align-items: center;
|
| 93 |
+
gap: 12px;
|
| 94 |
+
}
|
| 95 |
+
.service-icon {
|
| 96 |
+
width: 40px;
|
| 97 |
+
height: 40px;
|
| 98 |
+
background: rgba(51, 65, 85, 0.5);
|
| 99 |
+
border-radius: 10px;
|
| 100 |
+
display: flex;
|
| 101 |
+
align-items: center;
|
| 102 |
+
justify-content: center;
|
| 103 |
+
color: #06b6d4;
|
| 104 |
+
}
|
| 105 |
+
.service-name {
|
| 106 |
+
font-size: 14px;
|
| 107 |
+
color: #e2e8f0;
|
| 108 |
+
margin-bottom: 2px;
|
| 109 |
+
}
|
| 110 |
+
.service-port {
|
| 111 |
+
font-size: 11px;
|
| 112 |
+
color: #64748b;
|
| 113 |
+
}
|
| 114 |
+
.status {
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
gap: 6px;
|
| 118 |
+
font-size: 12px;
|
| 119 |
+
}
|
| 120 |
+
.status-dot {
|
| 121 |
+
width: 8px;
|
| 122 |
+
height: 8px;
|
| 123 |
+
border-radius: 50%;
|
| 124 |
+
}
|
| 125 |
+
.status.online {
|
| 126 |
+
color: #22c55e;
|
| 127 |
+
}
|
| 128 |
+
.status.online .status-dot {
|
| 129 |
+
background: #22c55e;
|
| 130 |
+
box-shadow: 0 0 8px #22c55e;
|
| 131 |
+
}
|
| 132 |
+
.status.offline {
|
| 133 |
+
color: #ef4444;
|
| 134 |
+
}
|
| 135 |
+
.status.offline .status-dot {
|
| 136 |
+
background: #ef4444;
|
| 137 |
+
}
|
| 138 |
+
.status.checking {
|
| 139 |
+
color: #eab308;
|
| 140 |
+
}
|
| 141 |
+
.status.checking .status-dot {
|
| 142 |
+
background: #eab308;
|
| 143 |
+
animation: pulse 1s infinite;
|
| 144 |
+
}
|
| 145 |
+
@keyframes pulse {
|
| 146 |
+
0%, 100% { opacity: 1; }
|
| 147 |
+
50% { opacity: 0.3; }
|
| 148 |
+
}
|
| 149 |
+
.config-input {
|
| 150 |
+
width: 100%;
|
| 151 |
+
background: rgba(15, 23, 42, 0.8);
|
| 152 |
+
border: 1px solid #334155;
|
| 153 |
+
border-radius: 8px;
|
| 154 |
+
padding: 12px;
|
| 155 |
+
color: #e2e8f0;
|
| 156 |
+
font-family: inherit;
|
| 157 |
+
font-size: 13px;
|
| 158 |
+
margin-top: 8px;
|
| 159 |
+
}
|
| 160 |
+
.config-input:focus {
|
| 161 |
+
outline: none;
|
| 162 |
+
border-color: #06b6d4;
|
| 163 |
+
}
|
| 164 |
+
.save-btn {
|
| 165 |
+
width: 100%;
|
| 166 |
+
background: #06b6d4;
|
| 167 |
+
color: #0f172a;
|
| 168 |
+
border: none;
|
| 169 |
+
border-radius: 8px;
|
| 170 |
+
padding: 12px;
|
| 171 |
+
font-family: inherit;
|
| 172 |
+
font-weight: 700;
|
| 173 |
+
margin-top: 12px;
|
| 174 |
+
cursor: pointer;
|
| 175 |
+
}
|
| 176 |
+
.save-btn:active {
|
| 177 |
+
opacity: 0.9;
|
| 178 |
+
}
|
| 179 |
+
</style>
|
| 180 |
+
<div class="header">
|
| 181 |
+
<div>
|
| 182 |
+
<div class="title">Server Config</div>
|
| 183 |
+
<div class="subtitle">~/homelab/services</div>
|
| 184 |
+
</div>
|
| 185 |
+
<button class="close-btn" id="close-btn">
|
| 186 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 187 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 188 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 189 |
+
</svg>
|
| 190 |
+
</button>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="content">
|
| 193 |
+
<div class="section">
|
| 194 |
+
<div class="section-title">Services</div>
|
| 195 |
+
<div id="services-list"></div>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="section">
|
| 198 |
+
<div class="section-title">Configuration</div>
|
| 199 |
+
<input type="text" class="config-input" id="server-url" placeholder="http://192.168.1.100:3000">
|
| 200 |
+
<button class="save-btn" id="save-config">Save Configuration</button>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
`;
|
| 204 |
+
|
| 205 |
+
this.setupInteractions();
|
| 206 |
+
this.renderServices();
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
setupInteractions() {
|
| 210 |
+
const closeBtn = this.shadowRoot.getElementById('close-btn');
|
| 211 |
+
if (closeBtn) {
|
| 212 |
+
closeBtn.addEventListener('click', () => {
|
| 213 |
+
this.dispatchEvent(new CustomEvent('toggle-server-panel', {
|
| 214 |
+
bubbles: true,
|
| 215 |
+
composed: true
|
| 216 |
+
}));
|
| 217 |
+
});
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
const saveBtn = this.shadowRoot.getElementById('save-config');
|
| 221 |
+
const urlInput = this.shadowRoot.getElementById('server-url');
|
| 222 |
+
|
| 223 |
+
// Load saved config
|
| 224 |
+
const saved = localStorage.getItem('serverEndpoint');
|
| 225 |
+
if (saved && urlInput) urlInput.value = saved;
|
| 226 |
+
|
| 227 |
+
if (saveBtn && urlInput) {
|
| 228 |
+
saveBtn.addEventListener('click', () => {
|
| 229 |
+
const url = urlInput.value;
|
| 230 |
+
localStorage.setItem('serverEndpoint', url);
|
| 231 |
+
|
| 232 |
+
// Show feedback
|
| 233 |
+
saveBtn.textContent = 'Saved!';
|
| 234 |
+
setTimeout(() => {
|
| 235 |
+
saveBtn.textContent = 'Save Configuration';
|
| 236 |
+
if (window.launcher) {
|
| 237 |
+
window.launcher.serverEndpoint = url;
|
| 238 |
+
window.launcher.checkServerStatus();
|
| 239 |
+
}
|
| 240 |
+
}, 1000);
|
| 241 |
+
});
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Listen for server status updates
|
| 245 |
+
window.addEventListener('server-status', (e) => {
|
| 246 |
+
this.updateServerStatus(e.detail);
|
| 247 |
+
});
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
renderServices() {
|
| 251 |
+
const container = this.shadowRoot.getElementById('services-list');
|
| 252 |
+
if (!container) return;
|
| 253 |
+
|
| 254 |
+
container.innerHTML = '';
|
| 255 |
+
this.services.forEach(service => {
|
| 256 |
+
const el = document.createElement('div');
|
| 257 |
+
el.className = 'service-item';
|
| 258 |
+
el.innerHTML = `
|
| 259 |
+
<div class="service-info">
|
| 260 |
+
<div class="service-icon">
|
| 261 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 262 |
+
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
| 263 |
+
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
| 264 |
+
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
| 265 |
+
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
| 266 |
+
</svg>
|
| 267 |
+
</div>
|
| 268 |
+
<div>
|
| 269 |
+
<div class="service-name">${service.name}</div>
|
| 270 |
+
<div class="service-port">:${service.port}</div>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
<div class="status checking" id="status-${service.name}">
|
| 274 |
+
<div class="status-dot"></div>
|
| 275 |
+
<span>checking</span>
|
| 276 |
+
</div>
|
| 277 |
+
`;
|
| 278 |
+
container.appendChild(el);
|
| 279 |
+
});
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
checkServices() {
|
| 283 |
+
// Simulate checking services (in real use, these would be actual fetch requests)
|
| 284 |
+
setTimeout(() => {
|
| 285 |
+
this.services.forEach(service => {
|
| 286 |
+
const statusEl = this.shadowRoot.getElementById(`status-${service.name}`);
|
| 287 |
+
if (statusEl) {
|
| 288 |
+
const isOnline = Math.random() > 0.3; // Simulate random status
|
| 289 |
+
statusEl.className = `status ${isOnline ? 'online' : 'offline'}`;
|
| 290 |
+
statusEl.innerHTML = `
|
| 291 |
+
<div class="status-dot"></div>
|
| 292 |
+
<span>${isOnline ? 'online' : 'offline'}</span>
|
| 293 |
+
`;
|
| 294 |
+
}
|
| 295 |
+
});
|
| 296 |
+
}, 1500);
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
updateServerStatus(status) {
|
| 300 |
+
// Could update specific UI elements based on main server status
|
| 301 |
+
console.log('Server panel received status:', status);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
customElements.define('arch-server-panel', ArchServerPanel);
|
components/arch-settings-panel.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```javascript
|
| 2 |
+
class ArchSettingsPanel extends HTMLElement {
|
| 3 |
+
constructor() {
|
| 4 |
+
super();
|
| 5 |
+
this.attachShadow({ mode: 'open' });
|
| 6 |
+
this.settings = new Map();
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
connectedCallback() {
|
| 10 |
+
this.render();
|
| 11 |
+
this.loadSettings();
|
| 12 |
+
this.setupListeners();
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
render() {
|
| 16 |
+
this.shadowRoot.innerHTML = `
|
| 17 |
+
<style>
|
| 18 |
+
:host {
|
| 19 |
+
display: flex;
|
| 20 |
+
flex-direction: column;
|
| 21 |
+
height: 100vh;
|
| 22 |
+
width: 100vw;
|
| 23 |
+
background: #0f172a;
|
| 24 |
+
font-family: 'JetBrains Mono', monospace;
|
| 25 |
+
}
|
| 26 |
+
.header {
|
| 27 |
+
padding: calc(env(safe-area-inset-top, 24px) + 24px) 24px 16px;
|
| 28 |
+
background: linear-gradient(to bottom, #020617, transparent);
|
| 29 |
+
border-bottom: 1px solid #334155;
|
| 30 |
+
display: flex;
|
| 31 |
+
justify-content: space-between;
|
| 32 |
+
align-items: center;
|
| 33 |
+
}
|
| 34 |
+
.title {
|
| 35 |
+
font-size: 20px;
|
| 36 |
+
color: #06b6d4;
|
| 37 |
+
font-weight: 700;
|
| 38 |
+
}
|
| 39 |
+
.subtitle {
|
| 40 |
+
font-size: 11px;
|
| 41 |
+
color: #64748b;
|
| 42 |
+
margin-top: 4px;
|
| 43 |
+
}
|
| 44 |
+
.close-btn {
|
| 45 |
+
width: 40px;
|
| 46 |
+
height: 40px;
|
| 47 |
+
border: 1px solid #334155;
|
| 48 |
+
border-radius: 12px;
|
| 49 |
+
display: flex;
|
| 50 |
+
align-items: center;
|
| 51 |
+
justify-content: center;
|
| 52 |
+
color: #94a3b8;
|
| 53 |
+
cursor: pointer;
|
| 54 |
+
background: rgba(51, 65, 85, 0.5);
|
| 55 |
+
}
|
| 56 |
+
.close-btn:active {
|
| 57 |
+
background: #334155;
|
| 58 |
+
color: #e2e8f0;
|
| 59 |
+
}
|
| 60 |
+
.content {
|
| 61 |
+
flex: 1;
|
| 62 |
+
overflow-y: auto;
|
| 63 |
+
padding: 8px 20px calc(env(safe-area-inset-bottom, 24px) + 100px);
|
| 64 |
+
}
|
| 65 |
+
.section {
|
| 66 |
+
margin-bottom: 24px;
|
| 67 |
+
}
|
| 68 |
+
.section-title {
|
| 69 |
+
font-size: 10px;
|
| 70 |
+
text-transform: uppercase;
|
| 71 |
+
letter-spacing: 2px;
|
| 72 |
+
color: #475569;
|
| 73 |
+
margin-bottom: 12px;
|
| 74 |
+
padding: 0 4px;
|
| 75 |
+
}
|
| 76 |
+
.tile-grid {
|
| 77 |
+
display: grid;
|
| 78 |
+
grid-template-columns: repeat(2, 1fr);
|
| 79 |
+
gap: 12px;
|
| 80 |
+
}
|
| 81 |
+
.tile {
|
| 82 |
+
background: rgba(30, 41, 59, 0.8);
|
| 83 |
+
border: 1px solid #334155;
|
| 84 |
+
border-radius: 16px;
|
| 85 |
+
padding: 16px;
|
| 86 |
+
display: flex;
|
| 87 |
+
flex-direction: column;
|
| 88 |
+
gap: 12px;
|
| 89 |
+
cursor: pointer;
|
| 90 |
+
transition: all 0.2s;
|
| 91 |
+
}
|
| 92 |
+
.tile:active {
|
| 93 |
+
background: rgba(6, 182, 212, 0.1);
|
| 94 |
+
border-color: #06b6d4;
|
| 95 |
+
}
|
| 96 |
+
.tile.active {
|
| 97 |
+
background: rgba(6, 182, 212, 0.15);
|
| 98 |
+
border-color: #06b6d4;
|
| 99 |
+
}
|
| 100 |
+
.tile-icon {
|
| 101 |
+
width: 40px;
|
| 102 |
+
height: 40px;
|
| 103 |
+
border-radius: 10px;
|
| 104 |
+
display: flex;
|
| 105 |
+
align-items: center;
|
| 106 |
+
justify-content: center;
|
| 107 |
+
background: rgba(51, 65, 85, 0.5);
|
| 108 |
+
color: #06b6d4;
|
| 109 |
+
}
|
| 110 |
+
.tile.active .tile-icon {
|
| 111 |
+
background: #06b6d4;
|
| 112 |
+
color: #0f172a;
|
| 113 |
+
}
|
| 114 |
+
.tile-info {
|
| 115 |
+
flex: 1;
|
| 116 |
+
}
|
| 117 |
+
.tile-name {
|
| 118 |
+
font-size: 13px;
|
| 119 |
+
color: #e2e8f0;
|
| 120 |
+
font-weight: 500;
|
| 121 |
+
}
|
| 122 |
+
.tile-status {
|
| 123 |
+
font-size: 11px;
|
| 124 |
+
color: #64748b;
|
| 125 |
+
margin-top: 2px;
|
| 126 |
+
}
|
| 127 |
+
/* Slider styles */
|
| 128 |
+
.slider-container {
|
| 129 |
+
background: rgba(30, 41, 59, 0.8);
|
| 130 |
+
border: 1px solid #334155;
|
| 131 |
+
border-radius: 16px;
|
| 132 |
+
padding: 20px;
|
| 133 |
+
margin-bottom: 12px;
|
| 134 |
+
}
|
| 135 |
+
.slider-header {
|
| 136 |
+
display: flex;
|
| 137 |
+
align-items: center;
|
| 138 |
+
gap: 12px;
|
| 139 |
+
margin-bottom: 16px;
|
| 140 |
+
}
|
| 141 |
+
.slider-label {
|
| 142 |
+
font-size: 13px;
|
| 143 |
+
color: #e2e8f0;
|
| 144 |
+
}
|
| 145 |
+
.slider-value {
|
| 146 |
+
margin-left: auto;
|
| 147 |
+
font-size: 13px;
|
| 148 |
+
color: #06b6d4;
|
| 149 |
+
}
|
| 150 |
+
input[type="range"] {
|
| 151 |
+
width: 100%;
|
| 152 |
+
height: 4px;
|
| 153 |
+
background: #334155;
|
| 154 |
+
border-radius: 2px;
|
| 155 |
+
outline: none;
|
| 156 |
+
-webkit-appearance: none;
|
| 157 |
+
}
|
| 158 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 159 |
+
-webkit-appearance: none;
|
| 160 |
+
width: 20px;
|
| 161 |
+
height: 20px;
|
| 162 |
+
background: #06b6d4;
|
| 163 |
+
border-radius: 50%;
|
| 164 |
+
cursor: pointer;
|
| 165 |
+
box-shadow: 0 0 10px rgba(6, 182, 212, 0.5);
|
| 166 |
+
}
|
| 167 |
+
.footer-hint {
|
| 168 |
+
position: absolute;
|
| 169 |
+
bottom: 24px;
|
| 170 |
+
left: 50%;
|
| 171 |
+
transform: translateX(-50%);
|
| 172 |
+
color: #64748b;
|
| 173 |
+
font-size: 11px;
|
| 174 |
+
display: flex;
|
| 175 |
+
align-items: center;
|
| 176 |
+
gap: 6px;
|
| 177 |
+
}
|
| 178 |
+
.arrow {
|
| 179 |
+
animation: bounce 1s infinite;
|
| 180 |
+
}
|
| 181 |
+
@keyframes bounce {
|
| 182 |
+
0%, 100% { transform: translateY(0); }
|
| 183 |
+
50% { transform: translateY(-4px); }
|
| 184 |
+
}
|
| 185 |
+
</style>
|
| 186 |
+
|
| 187 |
+
<div class="header">
|
| 188 |
+
<div>
|
| 189 |
+
<div class="title">Quick Settings</div>
|
| 190 |
+
<div class="subtitle" id="device-name">SM-S918B</div>
|
| 191 |
+
</div>
|
| 192 |
+
<button class="close-btn" id="close-btn">
|
| 193 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 194 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 195 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 196 |
+
</svg>
|
| 197 |
+
</button>
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
<div class="content">
|
| 201 |
+
<!-- Toggles -->
|
| 202 |
+
<div class="section">
|
| 203 |
+
<div class="section-title">Quick Toggles</div>
|
| 204 |
+
<div class="tile-grid">
|
| 205 |
+
<div class="tile" id="wifi-toggle" data-setting="wifi_enabled">
|
| 206 |
+
<div class="tile-icon">
|
| 207 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 208 |
+
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
|
| 209 |
+
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
|
| 210 |
+
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
|
| 211 |
+
<line x1="12" y1="20" x2="12.01" y2="20"></line>
|
| 212 |
+
</svg>
|
| 213 |
+
</div>
|
| 214 |
+
<div class="tile-info">
|
| 215 |
+
<div class="tile-name">Wi-Fi</div>
|
| 216 |
+
<div class="tile-status" id="wifi-status">ON</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
<div class="tile" id="bt-toggle" data-setting="bluetooth_enabled">
|
| 221 |
+
<div class="tile-icon">
|
| 222 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 223 |
+
<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"></polyline>
|
| 224 |
+
</svg>
|
| 225 |
+
</div>
|
| 226 |
+
<div class="tile-info">
|
| 227 |
+
<div class="tile-name">Bluetooth</div>
|
| 228 |
+
<div class="tile-status" id="bt-status">OFF</div>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
<div class="tile" id="dnd-toggle" data-setting="sound_dnd">
|
| 233 |
+
<div class="tile-icon">
|
| 234 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 235 |
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
| 236 |
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
| 237 |
+
</svg>
|
| 238 |
+
</div>
|
| 239 |
+
<div class="tile-info">
|
| 240 |
+
<div class="tile-name">Do Not Disturb</div>
|
| 241 |
+
<div class="tile-status" id="dnd-status">OFF</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<div class="tile" id="location-toggle" data-setting="location_mode">
|
| 246 |
+
<div class="tile-icon">
|
| 247 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 248 |
+
<polygon points="3 11 22 2 13 21 11 13 3 11"></polygon>
|
| 249 |
+
</svg>
|
| 250 |
+
</div>
|
| 251 |
+
<div class="tile-info">
|
| 252 |
+
<div class="tile-name">Location</div>
|
| 253 |
+
<div class="tile-status" id="location-status">High accuracy</div>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
<div class="tile" id="auto-rotate-toggle" data-setting="display_auto_rotate">
|
| 258 |
+
<div class="tile-icon">
|
| 259 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 260 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.3"></path>
|
| 261 |
+
</svg>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="tile-info">
|
| 264 |
+
<div class="tile-name">Auto-rotate</div>
|
| 265 |
+
<div class="tile-status" id="rotate-status">ON</div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<div class="tile" id="flashlight-toggle">
|
| 270 |
+
<div class="tile-icon">
|
| 271 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 272 |
+
<path d="M9 18h6"></path>
|
| 273 |
+
<path d="M10 22h4"></path>
|
| 274 |
+
<path d="M15.09 14c.18-.9.27-1.48-.38-6.51a.77.77 0 0 0-.16-.3.73.74 0 0 0-.61-.29H9.07a.74.74 0 0 0-.6.29.77.77 0 0 0-.17.3c-.64 5.02-.55 5.6-.38 6.5A2 2 0 0 0 10 16h4a2 2 0 0 0 2.09-2z"></path>
|
| 275 |
+
</svg>
|
| 276 |
+
</div>
|
| 277 |
+
<div class="tile-info">
|
| 278 |
+
<div class="tile-name">Flashlight</div>
|
| 279 |
+
<div class="tile-status" id="flash-status">OFF</div>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
<!-- Sliders -->
|
| 286 |
+
<div class="section">
|
| 287 |
+
<div class="section-title">Brightness & Volume</div>
|
| 288 |
+
<div class="slider-container">
|
| 289 |
+
<div class="slider-header">
|
| 290 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: #06b6d4;">
|
| 291 |
+
<circle cx="12" cy="12" r="5"></circle>
|
| 292 |
+
<line x1="12" y1="1" x2="12" y2="3"></line>
|
| 293 |
+
<line x1="12" y1="21" x2="12" y2="23"></line>
|
| 294 |
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
| 295 |
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
| 296 |
+
<line x1="1" y1="12" x2="3" y2="12"></line>
|
| 297 |
+
<line x1="21" y1="12" x2="23" y2="12"></line>
|
| 298 |
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
| 299 |
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
| 300 |
+
</svg>
|
| 301 |
+
<span class="slider-label">Brightness</span>
|
| 302 |
+
<span class="slider-value" id="brightness-value">72%</span>
|
| 303 |
+
</div>
|
| 304 |
+
<input type="range" id="brightness-slider" min="0" max="255" value="183">
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<div class="slider-container">
|
| 308 |
+
<div class="slider-header">
|
| 309 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: #06b6d4;">
|
| 310 |
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
| 311 |
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
| 312 |
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
|
| 313 |
+
</svg>
|
| 314 |
+
<span class="slider-label">Media Volume</span>
|
| 315 |
+
<span class="slider-value" id="volume-value">80%</span>
|
| 316 |
+
</div>
|
| 317 |
+
<input type="range" id="volume-slider" min="0" max="100" value="80">
|
| 318 |
+
</div>
|
| 319 |
+
</div>
|
| 320 |
+
|
| 321 |
+
<!-- System Shortcuts -->
|
| 322 |
+
<div class="section">
|
| 323 |
+
<div class="section-title">System</div>
|
| 324 |
+
<div class="tile-grid">
|
| 325 |
+
<div class="tile" id="settings-shortcut">
|
| 326 |
+
<div class="tile-icon">
|
| 327 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 328 |
+
<circle cx="12" cy="12" r="3"></circle>
|
| 329 |
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
| 330 |
+
</svg>
|
| 331 |
+
</div>
|
| 332 |
+
<div class="tile-info">
|
| 333 |
+
<div class="tile-name">Settings</div>
|
| 334 |
+
<div class="tile-status">All settings</div>
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
|
| 338 |
+
<div class="tile" id="power-shortcut">
|
| 339 |
+
<div class="tile-icon" style="background: rgba(239, 68, 68, 0.2); color: #ef4444;">
|
| 340 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 341 |
+
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
|
| 342 |
+
<line x1="12" y1="2" x2="12" y2="12"></line>
|
| 343 |
+
</svg>
|
| 344 |
+
</div>
|
| 345 |
+
<div class="tile-info">
|
| 346 |
+
<div class="tile-name">Power</div>
|
| 347 |
+
<div class="tile-status">Shut down</div>
|
| 348 |
+
</div>
|
| 349 |
+
</div>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
|
| 354 |
+
<div class="footer-hint">
|
| 355 |
+
<span>sw
|
| 356 |
+
___METADATA_START___
|
| 357 |
+
{"isNew":true,"userName":"jonin925"}
|
| 358 |
+
___METADATA_END___
|
components/arch-status-bar.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ArchStatusBar extends HTMLElement {
|
| 2 |
+
constructor() {
|
| 3 |
+
super();
|
| 4 |
+
this.attachShadow({ mode: 'open' });
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
connectedCallback() {
|
| 8 |
+
this.render();
|
| 9 |
+
this.updateStats();
|
| 10 |
+
setInterval(() => this.updateStats(), 1000);
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
updateStats() {
|
| 14 |
+
const now = new Date();
|
| 15 |
+
const time = now.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
| 16 |
+
|
| 17 |
+
// Update shadow DOM elements
|
| 18 |
+
const timeEl = this.shadowRoot.querySelector('.clock-time');
|
| 19 |
+
if (timeEl) timeEl.textContent = time;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
render() {
|
| 23 |
+
this.shadowRoot.innerHTML = `
|
| 24 |
+
<style>
|
| 25 |
+
:host {
|
| 26 |
+
position: fixed;
|
| 27 |
+
top: 0;
|
| 28 |
+
left: 0;
|
| 29 |
+
right: 0;
|
| 30 |
+
z-index: 100;
|
| 31 |
+
padding-top: env(safe-area-inset-top, 8px);
|
| 32 |
+
background: linear-gradient(to bottom, rgba(15, 23, 42, 0.9), transparent);
|
| 33 |
+
}
|
| 34 |
+
.bar {
|
| 35 |
+
display: flex;
|
| 36 |
+
justify-content: space-between;
|
| 37 |
+
align-items: center;
|
| 38 |
+
padding: 8px 16px;
|
| 39 |
+
font-family: 'JetBrains Mono', monospace;
|
| 40 |
+
font-size: 12px;
|
| 41 |
+
color: #94a3b8;
|
| 42 |
+
}
|
| 43 |
+
.left {
|
| 44 |
+
display: flex;
|
| 45 |
+
align-items: center;
|
| 46 |
+
gap: 8px;
|
| 47 |
+
color: #06b6d4;
|
| 48 |
+
font-weight: 700;
|
| 49 |
+
}
|
| 50 |
+
.center {
|
| 51 |
+
position: absolute;
|
| 52 |
+
left: 50%;
|
| 53 |
+
transform: translateX(-50%);
|
| 54 |
+
color: #e2e8f0;
|
| 55 |
+
letter-spacing: 1px;
|
| 56 |
+
}
|
| 57 |
+
.right {
|
| 58 |
+
display: flex;
|
| 59 |
+
align-items: center;
|
| 60 |
+
gap: 12px;
|
| 61 |
+
}
|
| 62 |
+
.icon {
|
| 63 |
+
width: 14px;
|
| 64 |
+
height: 14px;
|
| 65 |
+
stroke-width: 2;
|
| 66 |
+
}
|
| 67 |
+
.battery {
|
| 68 |
+
display: flex;
|
| 69 |
+
align-items: center;
|
| 70 |
+
gap: 4px;
|
| 71 |
+
}
|
| 72 |
+
.battery-level {
|
| 73 |
+
color: #22d3ee;
|
| 74 |
+
}
|
| 75 |
+
.wifi-icon {
|
| 76 |
+
color: #22d3ee;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
<div class="bar">
|
| 80 |
+
<div class="left">
|
| 81 |
+
<span>⌘</span>
|
| 82 |
+
<span>arch</span>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="center clock-time">00:00</div>
|
| 85 |
+
<div class="right">
|
| 86 |
+
<svg class="icon wifi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 87 |
+
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
|
| 88 |
+
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
|
| 89 |
+
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
|
| 90 |
+
<line x1="12" y1="20" x2="12.01" y2="20"></line>
|
| 91 |
+
</svg>
|
| 92 |
+
<div class="battery">
|
| 93 |
+
<span class="battery-level">84%</span>
|
| 94 |
+
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 95 |
+
<rect x="2" y="7" width="16" height="10" rx="2" ry="2"></rect>
|
| 96 |
+
<line x1="22" y1="11" x2="22" y2="13"></line>
|
| 97 |
+
<path d="M6 11v2" stroke="#22d3ee" stroke-width="3"></path>
|
| 98 |
+
<path d="M10 11v2" stroke="#22d3ee" stroke-width="3"></path>
|
| 99 |
+
<path d="M14 11v2" stroke="#22d3ee" stroke-width="3"></path>
|
| 100 |
+
</svg>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
`;
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
customElements.define('arch-status-bar', ArchStatusBar);
|
filename.html
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[content]
|
| 2 |
+
</html>
|
index.html
CHANGED
|
@@ -1,19 +1,136 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 6 |
+
<meta name="theme-color" content="#020617">
|
| 7 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 8 |
+
<meta name="mobile-web-app-capable" content="yes">
|
| 9 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
| 10 |
+
<title>Arch Launcher</title>
|
| 11 |
+
<link rel="manifest" href="manifest.json">
|
| 12 |
+
<link rel="stylesheet" href="style.css">
|
| 13 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 14 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 15 |
+
<script>
|
| 16 |
+
tailwind.config = {
|
| 17 |
+
darkMode: 'class',
|
| 18 |
+
theme: {
|
| 19 |
+
extend: {
|
| 20 |
+
colors: {
|
| 21 |
+
arch: {
|
| 22 |
+
bg: '#0f172a',
|
| 23 |
+
fg: '#e2e8f0',
|
| 24 |
+
primary: '#06b6d4', // cyan-500
|
| 25 |
+
secondary: '#64748b', // slate-500
|
| 26 |
+
accent: '#22d3ee', // cyan-400
|
| 27 |
+
muted: '#334155', // slate-700
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
fontFamily: {
|
| 31 |
+
mono: ['"JetBrains Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
</script>
|
| 37 |
+
<style>
|
| 38 |
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100;400;700&display=swap');
|
| 39 |
+
</style>
|
| 40 |
+
</head>
|
| 41 |
+
<body class="bg-arch-bg text-arch-fg font-mono antialiased overflow-hidden h-screen w-screen selection:bg-arch-primary selection:text-black">
|
| 42 |
+
|
| 43 |
+
<!-- Top Status Bar -->
|
| 44 |
+
<arch-status-bar></arch-status-bar>
|
| 45 |
+
|
| 46 |
+
<!-- Main Content Area -->
|
| 47 |
+
<main id="main-screen" class="h-full w-full flex flex-col relative transition-transform duration-300 ease-out">
|
| 48 |
+
|
| 49 |
+
<!-- System Info Widget (Neofetch Style) -->
|
| 50 |
+
<section class="px-6 pt-20 pb-4">
|
| 51 |
+
<div class="border-l-2 border-arch-primary pl-4 space-y-1 text-xs sm:text-sm">
|
| 52 |
+
<div class="flex items-center gap-2 text-arch-primary font-bold">
|
| 53 |
+
<i data-feather="smartphone" class="w-4 h-4"></i>
|
| 54 |
+
<span>user@s23-ultra</span>
|
| 55 |
+
</div>
|
| 56 |
+
<div class="grid grid-cols-[80px_1fr] gap-x-4 text-arch-secondary">
|
| 57 |
+
<span class="text-arch-muted">OS:</span>
|
| 58 |
+
<span>Arch Linux Mobile (Android 14)</span>
|
| 59 |
+
<span class="text-arch-muted">Host:</span>
|
| 60 |
+
<span>SM-S918B</span>
|
| 61 |
+
<span class="text-arch-muted">Kernel:</span>
|
| 62 |
+
<span>6.1.25-android</span>
|
| 63 |
+
<span class="text-arch-muted">Uptime:</span>
|
| 64 |
+
<span id="uptime">3 days, 4 hrs</span>
|
| 65 |
+
<span class="text-arch-muted">Shell:</span>
|
| 66 |
+
<span>zsh 5.9</span>
|
| 67 |
+
<span class="text-arch-muted">Server:</span>
|
| 68 |
+
<span id="server-status" class="text-arch-accent animate-pulse">● checking...</span>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="pt-2 flex gap-1">
|
| 71 |
+
<span class="w-3 h-3 rounded-full bg-red-500"></span>
|
| 72 |
+
<span class="w-3 h-3 rounded-full bg-yellow-500"></span>
|
| 73 |
+
<span class="w-3 h-3 rounded-full bg-green-500"></span>
|
| 74 |
+
<span class="w-3 h-3 rounded-full bg-arch-primary"></span>
|
| 75 |
+
<span class="w-3 h-3 rounded-full bg-blue-500"></span>
|
| 76 |
+
<span class="w-3 h-3 rounded-full bg-purple-500"></span>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</section>
|
| 80 |
+
|
| 81 |
+
<!-- Favorites Grid -->
|
| 82 |
+
<section class="flex-1 px-6 py-4">
|
| 83 |
+
<div class="flex items-center gap-2 mb-4 text-xs text-arch-secondary uppercase tracking-widest border-b border-arch-muted pb-2">
|
| 84 |
+
<i data-feather="star" class="w-3 h-3"></i>
|
| 85 |
+
<span>~/favorites</span>
|
| 86 |
+
</div>
|
| 87 |
+
<arch-app-grid id="favorites-grid" class="grid grid-cols-4 gap-4"></arch-app-grid>
|
| 88 |
+
</section>
|
| 89 |
+
|
| 90 |
+
<!-- Bottom Dock -->
|
| 91 |
+
<section class="px-6 pb-8 pt-4 bg-gradient-to-t from-arch-bg to-transparent">
|
| 92 |
+
<div class="flex justify-around items-center bg-arch-muted/30 backdrop-blur-md rounded-2xl p-4 border border-arch-muted/50">
|
| 93 |
+
<arch-app-icon name="Phone" icon="phone" package="com.android.dialer"></arch-app-icon>
|
| 94 |
+
<arch-app-icon name="Messages" icon="message-square" package="com.google.android.apps.messaging"></arch-app-icon>
|
| 95 |
+
<arch-app-icon name="Browser" icon="globe" package="org.mozilla.firefox"></arch-app-icon>
|
| 96 |
+
<arch-app-icon name="Camera" icon="camera" package="com.sec.android.app.camera"></arch-app-icon>
|
| 97 |
+
</div>
|
| 98 |
+
</section>
|
| 99 |
+
|
| 100 |
+
<!-- Swipe Up Indicator -->
|
| 101 |
+
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 flex flex-col items-center gap-1 opacity-50 animate-bounce">
|
| 102 |
+
<div class="w-12 h-1 bg-arch-muted rounded-full"></div>
|
| 103 |
+
<span class="text-[10px] text-arch-secondary uppercase">App Drawer</span>
|
| 104 |
+
</div>
|
| 105 |
+
</main>
|
| 106 |
+
|
| 107 |
+
<!-- App Drawer (Hidden by default) -->
|
| 108 |
+
<arch-app-drawer id="app-drawer" class="fixed inset-0 translate-y-full transition-transform duration-300 ease-out z-50 bg-arch-bg"></arch-app-drawer>
|
| 109 |
+
<!-- Quick Settings / Server Panel -->
|
| 110 |
+
<arch-server-panel id="server-panel" class="fixed inset-0 -translate-x-full transition-transform duration-300 ease-out z-50 bg-arch-bg/95 backdrop-blur-xl"></arch-server-panel>
|
| 111 |
+
|
| 112 |
+
<!-- Settings Panel -->
|
| 113 |
+
<arch-settings-panel id="settings-panel" class="fixed inset-0 translate-y-full transition-transform duration-300 ease-out z-50 bg-arch-bg"></arch-settings-panel>
|
| 114 |
+
|
| 115 |
+
<!-- Notification Shade -->
|
| 116 |
+
<arch-notifications id="notification-shade" class="fixed inset-0 -translate-y-full transition-transform duration-300 ease-out z-50 bg-arch-bg/95 backdrop-blur-xl"></arch-notifications>
|
| 117 |
+
|
| 118 |
+
<!-- Toast Container -->
|
| 119 |
+
<div id="toast-container" class="fixed top-24 left-1/2 -translate-x-1/2 z-50 pointer-events-none"></div>
|
| 120 |
+
|
| 121 |
+
<!-- Scripts -->
|
| 122 |
+
<script src="components/arch-status-bar.js"></script>
|
| 123 |
+
<script src="components/arch-app-icon.js"></script>
|
| 124 |
+
<script src="components/arch-app-grid.js"></script>
|
| 125 |
+
<script src="components/arch-app-drawer.js"></script>
|
| 126 |
+
<script src="components/arch-server-panel.js"></script>
|
| 127 |
+
<script src="components/arch-settings-panel.js"></script>
|
| 128 |
+
<script src="components/arch-notifications.js"></script>
|
| 129 |
+
<script src="js/worker-bridge.js"></script>
|
| 130 |
+
<script src="js/gesture-controller.js"></script>
|
| 131 |
+
<script src="js/android-bridge.js"></script>
|
| 132 |
+
<script src="script.js"></script>
|
| 133 |
+
<script>feather.replace();</script>
|
| 134 |
+
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
|
| 135 |
+
</body>
|
| 136 |
</html>
|
js/android-bridge.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Android Bridge - Interface between PWA and Android system
|
| 2 |
+
class AndroidBridge {
|
| 3 |
+
constructor() {
|
| 4 |
+
this.isAndroid = this.detectAndroid();
|
| 5 |
+
this.apiLevel = this.getApiLevel();
|
| 6 |
+
this.samsungFeatures = this.detectSamsung();
|
| 7 |
+
this.permissions = new Set();
|
| 8 |
+
|
| 9 |
+
this.init();
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
init() {
|
| 13 |
+
if (!this.isAndroid) {
|
| 14 |
+
console.log('Running in non-Android environment');
|
| 15 |
+
this.mockAndroidInterface();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
this.requestEssentialPermissions();
|
| 19 |
+
this.setupBroadcastReceiver();
|
| 20 |
+
this.startServices();
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
detectAndroid() {
|
| 24 |
+
return /Android/i.test(navigator.userAgent) ||
|
| 25 |
+
typeof window.Android !== 'undefined';
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
getApiLevel() {
|
| 29 |
+
const match = navigator.userAgent.match(/Android\s(\d+)/);
|
| 30 |
+
return match ? parseInt(match[1]) : 14;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
detectSamsung() {
|
| 34 |
+
return {
|
| 35 |
+
isSamsung: /Samsung/i.test(navigator.userAgent) ||
|
| 36 |
+
/SM-S918/.test(navigator.userAgent),
|
| 37 |
+
model: this.getSamsungModel(),
|
| 38 |
+
features: ['edge_panel', 'bixby', 'secure_folder', 'dex']
|
| 39 |
+
};
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
getSamsungModel() {
|
| 43 |
+
const match = navigator.userAgent.match(/(SM-[A-Za-z0-9]+)/);
|
| 44 |
+
return match ? match[1] : 'Unknown';
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
mockAndroidInterface() {
|
| 48 |
+
// Create mock interface for testing on non-Android devices
|
| 49 |
+
window.Android = {
|
| 50 |
+
Settings: {
|
| 51 |
+
get: (key) => localStorage.getItem(`android_${key}`) || '',
|
| 52 |
+
put: (key, value) => localStorage.setItem(`android_${key}`, value)
|
| 53 |
+
},
|
| 54 |
+
PackageManager: {
|
| 55 |
+
getInstalledApps: () => JSON.stringify([]),
|
| 56 |
+
launchApp: (pkg) => {
|
| 57 |
+
console.log(`Mock launch: ${pkg}`);
|
| 58 |
+
window.open(`intent://${pkg}#Intent;package=${pkg};end`, '_blank');
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
NotificationListener: {
|
| 62 |
+
getActiveNotifications: () => '[]',
|
| 63 |
+
cancelNotification: (id) => {}
|
| 64 |
+
},
|
| 65 |
+
Battery: {
|
| 66 |
+
getStatus: () => JSON.stringify({ level: 84, charging: true })
|
| 67 |
+
},
|
| 68 |
+
Haptic: {
|
| 69 |
+
perform: (type) => {
|
| 70 |
+
if (navigator.vibrate) navigator.vibrate(20);
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
GestureManager: {
|
| 74 |
+
registerCallback: () => {}
|
| 75 |
+
}
|
| 76 |
+
};
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
async requestEssentialPermissions() {
|
| 80 |
+
const permissions = [
|
| 81 |
+
{ name: 'notifications', required: true },
|
| 82 |
+
{ name: 'storage', required: false },
|
| 83 |
+
{ name: 'location', required: false }
|
| 84 |
+
];
|
| 85 |
+
|
| 86 |
+
for (const perm of permissions) {
|
| 87 |
+
try {
|
| 88 |
+
if (perm.name === 'notifications' && 'Notification' in window) {
|
| 89 |
+
const result = await Notification.requestPermission();
|
| 90 |
+
if (result === 'granted') {
|
| 91 |
+
this.permissions.add(perm.name);
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
} catch (e) {
|
| 95 |
+
console.warn(`Permission ${perm.name} failed:`, e);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
setupBroadcastReceiver() {
|
| 101 |
+
// Listen for Android system broadcasts
|
| 102 |
+
if (window.Android?.BroadcastReceiver?.register) {
|
| 103 |
+
window.Android.BroadcastReceiver.register('android.intent.action.BATTERY_CHANGED', (data) => {
|
| 104 |
+
window.dispatchEvent(new CustomEvent('battery-update', { detail: data }));
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
window.Android.BroadcastReceiver.register('android.intent.action.SCREEN_ON', () => {
|
| 108 |
+
window.dispatchEvent(new CustomEvent('screen-on'));
|
| 109 |
+
});
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
startServices() {
|
| 114 |
+
// Start worker-based services
|
| 115 |
+
if (window.workerBridge) {
|
| 116 |
+
window.workerBridge.startSystemSync();
|
| 117 |
+
window.workerBridge.fetchApps();
|
| 118 |
+
window.workerBridge.fetchSettings();
|
| 119 |
+
window.workerBridge.fetchNotifications();
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Register for periodic sync
|
| 123 |
+
if ('periodicSync' in registration) {
|
| 124 |
+
registration.periodicSync.register('content-sync', {
|
| 125 |
+
minInterval: 24 * 60 * 60 * 1000 // 1 day
|
| 126 |
+
}).catch(console.error);
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// Public API
|
| 131 |
+
|
| 132 |
+
getSystemInfo() {
|
| 133 |
+
return {
|
| 134 |
+
isAndroid: this.isAndroid,
|
| 135 |
+
apiLevel: this.apiLevel,
|
| 136 |
+
samsung: this.samsungFeatures,
|
| 137 |
+
userAgent: navigator.userAgent,
|
| 138 |
+
screen: {
|
| 139 |
+
width: screen.width,
|
| 140 |
+
height: screen.height,
|
| 141 |
+
density: window.devicePixelRatio
|
| 142 |
+
},
|
| 143 |
+
memory: navigator.deviceMemory || 'unknown',
|
| 144 |
+
connection: this.getConnectionInfo()
|
| 145 |
+
};
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
getConnectionInfo() {
|
| 149 |
+
const conn = navigator.connection;
|
| 150 |
+
return {
|
| 151 |
+
type: conn?.effectiveType || 'unknown',
|
| 152 |
+
saveData: conn?.saveData || false,
|
| 153 |
+
rtt: conn?.rtt,
|
| 154 |
+
downlink: conn?.downlink
|
| 155 |
+
};
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
async share(data) {
|
| 159 |
+
if (navigator.share) {
|
| 160 |
+
return navigator.share(data);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// Fallback to Android share
|
| 164 |
+
if (window.Android?.Intent?.share) {
|
| 165 |
+
window.Android.Intent.share(JSON.stringify(data));
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
setWallpaper(uri) {
|
| 170 |
+
if (this.samsungFeatures.isSamsung && window.Android?.Wallpaper?.set) {
|
| 171 |
+
window.Android.Wallpaper.set(uri);
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
openSystemSettings(setting) {
|
| 176 |
+
const settingUris = {
|
| 177 |
+
'display': 'android.settings.DISPLAY_SETTINGS',
|
| 178 |
+
'sound': 'android.settings.SOUND_SETTINGS',
|
| 179 |
+
'wifi': 'android.settings.WIFI_SETTINGS',
|
| 180 |
+
'bluetooth': 'android.settings.BLUETOOTH_SETTINGS',
|
| 181 |
+
'notification': 'android.settings.NOTIFICATION_SETTINGS',
|
| 182 |
+
'apps': 'android.settings.MANAGE_APPLICATIONS_SETTINGS',
|
| 183 |
+
'battery': 'android.settings.BATTERY_SAVER_SETTINGS',
|
| 184 |
+
'storage': 'android.settings.INTERNAL_STORAGE_SETTINGS'
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
if (window.Android?.Intent?.openSettings) {
|
| 188 |
+
window.Android.Intent.openSettings(settingUris[setting] || settingUris.display);
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// Initialize
|
| 194 |
+
window.androidBridge = new AndroidBridge();
|
js/gesture-controller.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Gesture Controller - Android 14 compliant gesture handling
|
| 2 |
+
class GestureController {
|
| 3 |
+
constructor() {
|
| 4 |
+
this.minSwipeDistance = 60;
|
| 5 |
+
this.edgeSwipeWidth = 40;
|
| 6 |
+
this.gestureArea = document.documentElement;
|
| 7 |
+
this.isTracking = false;
|
| 8 |
+
this.startY = 0;
|
| 9 |
+
this.startX = 0;
|
| 10 |
+
this.currentGesture = null;
|
| 11 |
+
this.vibrationEnabled = true;
|
| 12 |
+
|
| 13 |
+
this.init();
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
init() {
|
| 17 |
+
this.setupTouchHandlers();
|
| 18 |
+
this.setupAndroidGestures();
|
| 19 |
+
this.listenForSettings();
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
setupTouchHandlers() {
|
| 23 |
+
// Use passive listeners for performance
|
| 24 |
+
document.addEventListener('touchstart', (e) => this.onTouchStart(e), { passive: true });
|
| 25 |
+
document.addEventListener('touchmove', (e) => this.onTouchMove(e), { passive: true });
|
| 26 |
+
document.addEventListener('touchend', (e) => this.onTouchEnd(e), { passive: true });
|
| 27 |
+
document.addEventListener('touchcancel', (e) => this.onTouchCancel(e), { passive: true });
|
| 28 |
+
|
| 29 |
+
// Prevent default only for specific gestures
|
| 30 |
+
document.addEventListener('touchmove', (e) => {
|
| 31 |
+
if (this.isTracking && this.shouldPreventScroll()) {
|
| 32 |
+
e.preventDefault();
|
| 33 |
+
}
|
| 34 |
+
}, { passive: false });
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
setupAndroidGestures() {
|
| 38 |
+
// Register with Android system if available
|
| 39 |
+
if (window.Android?.GestureManager?.registerCallback) {
|
| 40 |
+
window.Android.GestureManager.registerCallback((gesture) => {
|
| 41 |
+
this.handleAndroidGesture(gesture);
|
| 42 |
+
});
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
listenForSettings() {
|
| 47 |
+
window.addEventListener('settings-updated', (e) => {
|
| 48 |
+
if (e.detail.settings) {
|
| 49 |
+
const settings = e.detail.settings;
|
| 50 |
+
this.vibrationEnabled = settings['haptic_feedback'] !== '0';
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
onTouchStart(e) {
|
| 56 |
+
const touch = e.touches[0];
|
| 57 |
+
this.startX = touch.clientX;
|
| 58 |
+
this.startY = touch.clientY;
|
| 59 |
+
this.startTime = Date.now();
|
| 60 |
+
this.isTracking = true;
|
| 61 |
+
this.currentGesture = null;
|
| 62 |
+
|
| 63 |
+
// Detect gesture zone
|
| 64 |
+
const screenHeight = window.innerHeight;
|
| 65 |
+
const screenWidth = window.innerWidth;
|
| 66 |
+
|
| 67 |
+
this.gestureType = this.detectGestureZone(this.startX, this.startY, screenWidth, screenHeight);
|
| 68 |
+
|
| 69 |
+
// Send to worker for processing
|
| 70 |
+
if (window.workerBridge) {
|
| 71 |
+
window.workerBridge.processGesture('TOUCH_START', {
|
| 72 |
+
x: this.startX,
|
| 73 |
+
y: this.startY,
|
| 74 |
+
time: this.startTime
|
| 75 |
+
});
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
onTouchMove(e) {
|
| 80 |
+
if (!this.isTracking) return;
|
| 81 |
+
|
| 82 |
+
const touch = e.touches[0];
|
| 83 |
+
const deltaX = touch.clientX - this.startX;
|
| 84 |
+
const deltaY = touch.clientY - this.startY;
|
| 85 |
+
|
| 86 |
+
// Send to worker
|
| 87 |
+
if (window.workerBridge) {
|
| 88 |
+
window.workerBridge.processGesture('TOUCH_MOVE', {
|
| 89 |
+
x: touch.clientX,
|
| 90 |
+
y: touch.clientY,
|
| 91 |
+
time: Date.now()
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Live gesture detection for UI feedback
|
| 96 |
+
this.updateGesturePreview(deltaX, deltaY);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
onTouchEnd(e) {
|
| 100 |
+
if (!this.isTracking) return;
|
| 101 |
+
|
| 102 |
+
const touch = e.changedTouches[0];
|
| 103 |
+
const deltaX = touch.clientX - this.startX;
|
| 104 |
+
const deltaY = touch.clientY - this.startY;
|
| 105 |
+
const deltaTime = Date.now() - this.startTime;
|
| 106 |
+
|
| 107 |
+
this.isTracking = false;
|
| 108 |
+
|
| 109 |
+
// Send to worker
|
| 110 |
+
if (window.workerBridge) {
|
| 111 |
+
window.workerBridge.processGesture('TOUCH_END', {
|
| 112 |
+
x: touch.clientX,
|
| 113 |
+
y: touch.clientY,
|
| 114 |
+
time: Date.now()
|
| 115 |
+
});
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Local gesture detection for immediate response
|
| 119 |
+
const gesture = this.detectGesture(deltaX, deltaY, deltaTime);
|
| 120 |
+
if (gesture) {
|
| 121 |
+
this.executeGesture(gesture);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
this.hideGesturePreview();
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
onTouchCancel(e) {
|
| 128 |
+
this.isTracking = false;
|
| 129 |
+
this.hideGesturePreview();
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
detectGestureZone(x, y, width, height) {
|
| 133 |
+
// Android 14 gesture zones
|
| 134 |
+
if (y < 50) {
|
| 135 |
+
if (x < width * 0.4) return 'STATUS_NOTIFICATIONS';
|
| 136 |
+
if (x > width * 0.6) return 'STATUS_QUICK_SETTINGS';
|
| 137 |
+
return 'STATUS_CLOCK';
|
| 138 |
+
}
|
| 139 |
+
if (y > height - 20 && x > width * 0.2 && x < width * 0.8) {
|
| 140 |
+
return 'HOME_INDICATOR';
|
| 141 |
+
}
|
| 142 |
+
if (x < this.edgeSwipeWidth) return 'EDGE_BACK';
|
| 143 |
+
if (x > width - this.edgeSwipeWidth) return 'EDGE_FORWARD';
|
| 144 |
+
|
| 145 |
+
return 'CONTENT';
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
detectGesture(deltaX, deltaY, deltaTime) {
|
| 149 |
+
const absX = Math.abs(deltaX);
|
| 150 |
+
const absY = Math.abs(deltaY);
|
| 151 |
+
const velocityX = absX / deltaTime;
|
| 152 |
+
const velocityY = absY / deltaTime;
|
| 153 |
+
const minVelocity = 0.3; // px/ms
|
| 154 |
+
|
| 155 |
+
// Home gesture (bottom swipe up)
|
| 156 |
+
if (this.gestureType === 'HOME_INDICATOR' && deltaY < -this.minSwipeDistance) {
|
| 157 |
+
return { type: 'HOME', intensity: Math.min(absY / 200, 1) };
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Back gesture (edge swipe)
|
| 161 |
+
if (this.gestureType === 'EDGE_BACK' && deltaX > this.minSwipeDistance) {
|
| 162 |
+
return { type: 'BACK', edge: 'left' };
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Quick settings (top-right swipe down)
|
| 166 |
+
if (this.gestureType === 'STATUS_QUICK_SETTINGS' && deltaY > this.minSwipeDistance) {
|
| 167 |
+
return { type: 'QUICK_SETTINGS' };
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// Notifications (top-left swipe down)
|
| 171 |
+
if (this.gestureType === 'STATUS_NOTIFICATIONS' && deltaY > this.minSwipeDistance) {
|
| 172 |
+
return { type: 'NOTIFICATIONS' };
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// App drawer (home screen swipe up)
|
| 176 |
+
if (absY > absX && deltaY < -this.minSwipeDistance && velocityY > minVelocity) {
|
| 177 |
+
if (document.getElementById('main-screen')?.contains(document.elementFromPoint(this.startX, this.startY))) {
|
| 178 |
+
return { type: 'APP_DRAWER' };
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// General swipes
|
| 183 |
+
if (absX > absY && absX > this.minSwipeDistance && velocityX > minVelocity) {
|
| 184 |
+
return { type: deltaX > 0 ? 'SWIPE_RIGHT' : 'SWIPE_LEFT' };
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
return null;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
executeGesture(gesture) {
|
| 191 |
+
if (this.vibrationEnabled) {
|
| 192 |
+
this.hapticFeedback(gesture.type);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
switch (gesture.type) {
|
| 196 |
+
case 'HOME':
|
| 197 |
+
this.goHome();
|
| 198 |
+
break;
|
| 199 |
+
case 'BACK':
|
| 200 |
+
this.goBack();
|
| 201 |
+
break;
|
| 202 |
+
case 'QUICK_SETTINGS':
|
| 203 |
+
case 'APP_DRAWER':
|
| 204 |
+
this.togglePanel('settings-panel');
|
| 205 |
+
break;
|
| 206 |
+
case 'NOTIFICATIONS':
|
| 207 |
+
this.togglePanel('notification-shade');
|
| 208 |
+
break;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Dispatch event for components
|
| 212 |
+
window.dispatchEvent(new CustomEvent('gesture-executed', { detail: gesture }));
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
goHome() {
|
| 216 |
+
// Close all panels
|
| 217 |
+
['server-panel', 'settings-panel', 'notification-shade', 'app-drawer'].forEach(id => {
|
| 218 |
+
const el = document.getElementById(id);
|
| 219 |
+
if (el) {
|
| 220 |
+
el.classList.add('-translate-y-full', '-translate-x-full', 'translate-y-full');
|
| 221 |
+
el.classList.remove('translate-y-0', 'translate-x-0', 'translate-y-full');
|
| 222 |
+
}
|
| 223 |
+
});
|
| 224 |
+
|
| 225 |
+
// Reset main screen
|
| 226 |
+
const main = document.getElementById('main-screen');
|
| 227 |
+
if (main) {
|
| 228 |
+
main.style.transform = '';
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
goBack() {
|
| 233 |
+
// Close topmost panel or go back in history
|
| 234 |
+
const panels = ['notification-shade', 'settings-panel', 'server-panel', 'app-drawer'];
|
| 235 |
+
for (const id of panels) {
|
| 236 |
+
const el = document.getElementById(id);
|
| 237 |
+
if (el && this.isPanelOpen(el)) {
|
| 238 |
+
this.closePanel(el);
|
| 239 |
+
return;
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
// Fallback to browser back
|
| 244 |
+
if (history.length > 1) {
|
| 245 |
+
history.back();
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
togglePanel(id) {
|
| 250 |
+
const el = document.getElementById(id);
|
| 251 |
+
if (!el) return;
|
| 252 |
+
|
| 253 |
+
if (this.isPanelOpen(el)) {
|
| 254 |
+
this.closePanel(el);
|
| 255 |
+
} else {
|
| 256 |
+
this.openPanel(el);
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
openPanel(el) {
|
| 261 |
+
const id = el.id;
|
| 262 |
+
|
| 263 |
+
if (id === 'notification-shade') {
|
| 264 |
+
el.classList.remove('-translate-y-full');
|
| 265 |
+
el.classList.add('translate-y-0');
|
| 266 |
+
} else if (id === 'server-panel') {
|
| 267 |
+
el.classList.remove('-translate-x-full');
|
| 268 |
+
el.classList.add('translate-x-0');
|
| 269 |
+
} else {
|
| 270 |
+
el.classList.remove('translate-y-full');
|
| 271 |
+
el.classList.add('translate-y-0');
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
closePanel(el) {
|
| 276 |
+
const id = el.id;
|
| 277 |
+
|
| 278 |
+
if (id === 'notification-shade') {
|
| 279 |
+
el.classList.add('-translate-y-full');
|
| 280 |
+
el.classList.remove('translate-y-0');
|
| 281 |
+
} else if (id === 'server-panel') {
|
| 282 |
+
el.classList.add('-translate-x-full');
|
| 283 |
+
el.classList.remove('translate-x-0');
|
| 284 |
+
} else {
|
| 285 |
+
el.classList.add('translate-y-full');
|
| 286 |
+
el.classList.remove('translate-y-0');
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
isPanelOpen(el) {
|
| 291 |
+
return !el.classList.contains('-translate-y-full') &&
|
| 292 |
+
!el.classList.contains('-translate-x-full') &&
|
| 293 |
+
!el.classList.contains('translate-y-full');
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
hapticFeedback(type) {
|
| 297 |
+
const patterns = {
|
| 298 |
+
'HOME': [0, 20, 10, 20],
|
| 299 |
+
'BACK': [0, 15],
|
| 300 |
+
'QUICK_SETTINGS': [0, 10, 40, 10],
|
| 301 |
+
'NOTIFICATIONS': [0, 10, 40, 10],
|
| 302 |
+
'default': [0, 20]
|
| 303 |
+
};
|
| 304 |
+
|
| 305 |
+
if (navigator.vibrate) {
|
| 306 |
+
navigator.vibrate(patterns[type] || patterns.default);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// Also notify Android
|
| 310 |
+
if (window.Android?.Haptic?.perform) {
|
| 311 |
+
window.Android.Haptic.perform(type.toLowerCase());
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
updateGesturePreview(deltaX, deltaY) {
|
| 316 |
+
// Visual feedback during gesture
|
| 317 |
+
const indicator = document.getElementById('gesture-indicator');
|
| 318 |
+
if (!indicator) return;
|
| 319 |
+
|
| 320 |
+
const progress = Math.min(Math.abs(deltaY) / 200, 1);
|
| 321 |
+
indicator.style.opacity = progress;
|
| 322 |
+
indicator.style.transform = `translateY(${Math.min(deltaY * 0.3, 50)}px)`;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
hideGesturePreview() {
|
| 326 |
+
const indicator = document.getElementById('gesture-indicator');
|
| 327 |
+
if (indicator) {
|
| 328 |
+
indicator.style.opacity = '0';
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
shouldPreventScroll() {
|
| 333 |
+
return ['HOME_INDICATOR', 'STATUS_NOTIFICATIONS', 'STATUS_QUICK_SETTINGS'].includes(this.gestureType);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
handleAndroidGesture(gesture) {
|
| 337 |
+
// Handle gestures from Android system
|
| 338 |
+
this.executeGesture({ type: gesture.type });
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
// Initialize when DOM ready
|
| 343 |
+
if (document.readyState === 'loading') {
|
| 344 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 345 |
+
window.gestureController = new GestureController();
|
| 346 |
+
});
|
| 347 |
+
} else {
|
| 348 |
+
window.gestureController = new GestureController();
|
| 349 |
+
}
|
js/worker-bridge.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Worker Bridge - Manages parallel workers for ArchDroid
|
| 2 |
+
class WorkerBridge {
|
| 3 |
+
constructor() {
|
| 4 |
+
this.workers = new Map();
|
| 5 |
+
this.messageQueue = [];
|
| 6 |
+
this.callbacks = new Map();
|
| 7 |
+
this.callbackId = 0;
|
| 8 |
+
|
| 9 |
+
this.initWorkers();
|
| 10 |
+
this.setupServiceWorker();
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
initWorkers() {
|
| 14 |
+
const workerConfigs = [
|
| 15 |
+
{ name: 'settings', url: 'workers/settings-worker.js' },
|
| 16 |
+
{ name: 'apps', url: 'workers/app-data-worker.js' },
|
| 17 |
+
{ name: 'gestures', url: 'workers/gesture-worker.js' },
|
| 18 |
+
{ name: 'notifications', url: 'workers/notification-worker.js' },
|
| 19 |
+
{ name: 'system', url: 'workers/system-sync-worker.js' }
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
workerConfigs.forEach(({ name, url }) => {
|
| 23 |
+
try {
|
| 24 |
+
const worker = new Worker(url);
|
| 25 |
+
worker.onmessage = (e) => this.handleWorkerMessage(name, e.data);
|
| 26 |
+
worker.onerror = (e) => console.error(`Worker ${name} error:`, e);
|
| 27 |
+
this.workers.set(name, worker);
|
| 28 |
+
} catch (err) {
|
| 29 |
+
console.warn(`Failed to create worker ${name}:`, err);
|
| 30 |
+
}
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
async setupServiceWorker() {
|
| 35 |
+
if ('serviceWorker' in navigator) {
|
| 36 |
+
try {
|
| 37 |
+
const reg = await navigator.serviceWorker.register('/sw.js');
|
| 38 |
+
console.log('SW registered:', reg);
|
| 39 |
+
|
| 40 |
+
navigator.serviceWorker.addEventListener('message', (e) => {
|
| 41 |
+
this.handleServiceWorkerMessage(e.data);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// Request background sync
|
| 45 |
+
if ('sync' in reg) {
|
| 46 |
+
await reg.sync.register('settings-sync');
|
| 47 |
+
}
|
| 48 |
+
} catch (err) {
|
| 49 |
+
console.error('SW registration failed:', err);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
postToWorker(workerName, message, transferables = []) {
|
| 55 |
+
const worker = this.workers.get(workerName);
|
| 56 |
+
if (!worker) {
|
| 57 |
+
console.warn(`Worker ${workerName} not available`);
|
| 58 |
+
return Promise.reject(new Error('Worker not available'));
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
return new Promise((resolve) => {
|
| 62 |
+
const id = ++this.callbackId;
|
| 63 |
+
this.callbacks.set(id, resolve);
|
| 64 |
+
|
| 65 |
+
worker.postMessage({
|
| 66 |
+
...message,
|
| 67 |
+
_callbackId: id,
|
| 68 |
+
_workerName: workerName
|
| 69 |
+
}, transferables);
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
handleWorkerMessage(workerName, data) {
|
| 74 |
+
// Resolve pending callback if exists
|
| 75 |
+
if (data._callbackId) {
|
| 76 |
+
const callback = this.callbacks.get(data._callbackId);
|
| 77 |
+
if (callback) {
|
| 78 |
+
callback(data);
|
| 79 |
+
this.callbacks.delete(data._callbackId);
|
| 80 |
+
return;
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Broadcast to window
|
| 85 |
+
const event = new CustomEvent(`worker:${workerName}`, {
|
| 86 |
+
detail: data
|
| 87 |
+
});
|
| 88 |
+
window.dispatchEvent(event);
|
| 89 |
+
|
| 90 |
+
// Also dispatch to specific handlers
|
| 91 |
+
this.routeWorkerMessage(workerName, data);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
routeWorkerMessage(workerName, data) {
|
| 95 |
+
switch (workerName) {
|
| 96 |
+
case 'settings':
|
| 97 |
+
window.dispatchEvent(new CustomEvent('settings-updated', { detail: data }));
|
| 98 |
+
break;
|
| 99 |
+
case 'apps':
|
| 100 |
+
window.dispatchEvent(new CustomEvent('apps-loaded', { detail: data }));
|
| 101 |
+
break;
|
| 102 |
+
case 'gestures':
|
| 103 |
+
window.dispatchEvent(new CustomEvent('gesture', { detail: data.gesture }));
|
| 104 |
+
break;
|
| 105 |
+
case 'notifications':
|
| 106 |
+
window.dispatchEvent(new CustomEvent('notifications-updated', { detail: data }));
|
| 107 |
+
break;
|
| 108 |
+
case 'system':
|
| 109 |
+
window.dispatchEvent(new CustomEvent('system-status', { detail: data }));
|
| 110 |
+
break;
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
handleServiceWorkerMessage(data) {
|
| 115 |
+
window.dispatchEvent(new CustomEvent('sw-message', { detail: data }));
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Public API methods
|
| 119 |
+
async fetchSettings() {
|
| 120 |
+
return this.postToWorker('settings', { action: 'READ_ALL' });
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
async watchSetting(key) {
|
| 124 |
+
this.postToWorker('settings', { action: 'WATCH', settings: { key } });
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
async fetchApps() {
|
| 128 |
+
return this.postToWorker('apps', { action: 'FETCH_ALL' });
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
async searchApps(query) {
|
| 132 |
+
return this.postToWorker('apps', { action: 'SEARCH', filter: { query } });
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
async launchApp(packageName) {
|
| 136 |
+
return this.postToWorker('apps', { action: 'LAUNCH_APP', filter: { package: packageName } });
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
processGesture(type, event) {
|
| 140 |
+
this.postToWorker('gestures', { type, event });
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
async fetchNotifications() {
|
| 144 |
+
return this.postToWorker('notifications', { action: 'FETCH' });
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
clearNotification(id) {
|
| 148 |
+
this.postToWorker('notifications', { action: 'CLEAR', data: { id } });
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
startSystemSync() {
|
| 152 |
+
this.postToWorker('system', { action: 'START_SYNC' });
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
stopSystemSync() {
|
| 156 |
+
this.postToWorker('system', { action: 'STOP_SYNC' });
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Initialize bridge
|
| 161 |
+
window.workerBridge = new WorkerBridge();
|
manifest.json
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 6 |
+
<meta name="theme-color" content="#020617">
|
| 7 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 8 |
+
<meta name="mobile-web-app-capable" content="yes">
|
| 9 |
+
<title>Arch Launcher</title>
|
| 10 |
+
<link rel="manifest" href="manifest.json">
|
| 11 |
+
<link rel="stylesheet" href="style.css">
|
| 12 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 13 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 14 |
+
<script>
|
| 15 |
+
tailwind.config = {
|
| 16 |
+
darkMode: 'class',
|
| 17 |
+
theme: {
|
| 18 |
+
extend: {
|
| 19 |
+
colors: {
|
| 20 |
+
arch: {
|
| 21 |
+
bg: '#0f172a',
|
| 22 |
+
fg: '#e2e8f0',
|
| 23 |
+
primary: '#06b6d4', // cyan-500
|
| 24 |
+
secondary: '#64748b', // slate-500
|
| 25 |
+
accent: '#22d3ee', // cyan-400
|
| 26 |
+
muted: '#334155', // slate-700
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
fontFamily: {
|
| 30 |
+
mono: ['"JetBrains Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
</script>
|
| 36 |
+
<style>
|
| 37 |
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100;400;700&display=swap');
|
| 38 |
+
</style>
|
| 39 |
+
</head>
|
| 40 |
+
<body class="bg-arch-bg text-arch-fg font-mono antialiased overflow-hidden h-screen w-screen selection:bg-arch-primary selection:text-black">
|
| 41 |
+
|
| 42 |
+
<!-- Top Status Bar -->
|
| 43 |
+
<arch-status-bar></arch-status-bar>
|
| 44 |
+
|
| 45 |
+
<!-- Main Content Area -->
|
| 46 |
+
<main id="main-screen" class="h-full w-full flex flex-col relative transition-transform duration-300 ease-out">
|
| 47 |
+
|
| 48 |
+
<!-- System Info Widget (Neofetch Style) -->
|
| 49 |
+
<section class="px-6 pt-20 pb-4">
|
| 50 |
+
<div class="border-l-2 border-arch-primary pl-4 space-y-1 text-xs sm:text-sm">
|
| 51 |
+
<div class="flex items-center gap-2 text-arch-primary font-bold">
|
| 52 |
+
<i data-feather="smartphone" class="w-4 h-4"></i>
|
| 53 |
+
<span>user@s23-ultra</span>
|
| 54 |
+
</div>
|
| 55 |
+
<div class="grid grid-cols-[80px_1fr] gap-x-4 text-arch-secondary">
|
| 56 |
+
<span class="text-arch-muted">OS:</span>
|
| 57 |
+
<span>Arch Linux Mobile (Android 14)</span>
|
| 58 |
+
<span class="text-arch-muted">Host:</span>
|
| 59 |
+
<span>SM-S918B</span>
|
| 60 |
+
<span class="text-arch-muted">Kernel:</span>
|
| 61 |
+
<span>6.1.25-android</span>
|
| 62 |
+
<span class="text-arch-muted">Uptime:</span>
|
| 63 |
+
<span id="uptime">3 days, 4 hrs</span>
|
| 64 |
+
<span class="text-arch-muted">Shell:</span>
|
| 65 |
+
<span>zsh 5.9</span>
|
| 66 |
+
<span class="text-arch-muted">Server:</span>
|
| 67 |
+
<span id="server-status" class="text-arch-accent animate-pulse">● checking...</span>
|
| 68 |
+
</div>
|
| 69 |
+
<div class="pt-2 flex gap-1">
|
| 70 |
+
<span class="w-3 h-3 rounded-full bg-red-500"></span>
|
| 71 |
+
<span class="w-3 h-3 rounded-full bg-yellow-500"></span>
|
| 72 |
+
<span class="w-3 h-3 rounded-full bg-green-500"></span>
|
| 73 |
+
<span class="w-3 h-3 rounded-full bg-arch-primary"></span>
|
| 74 |
+
<span class="w-3 h-3 rounded-full bg-blue-500"></span>
|
| 75 |
+
<span class="w-3 h-3 rounded-full bg-purple-500"></span>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</section>
|
| 79 |
+
|
| 80 |
+
<!-- Favorites Grid -->
|
| 81 |
+
<section class="flex-1 px-6 py-4">
|
| 82 |
+
<div class="flex items-center gap-2 mb-4 text-xs text-arch-secondary uppercase tracking-widest border-b border-arch-muted pb-2">
|
| 83 |
+
<i data-feather="star" class="w-3 h-3"></i>
|
| 84 |
+
<span>~/favorites</span>
|
| 85 |
+
</div>
|
| 86 |
+
<arch-app-grid id="favorites-grid" class="grid grid-cols-4 gap-4"></arch-app-grid>
|
| 87 |
+
</section>
|
| 88 |
+
|
| 89 |
+
<!-- Bottom Dock -->
|
| 90 |
+
<section class="px-6 pb-8 pt-4 bg-gradient-to-t from-arch-bg to-transparent">
|
| 91 |
+
<div class="flex justify-around items-center bg-arch-muted/30 backdrop-blur-md rounded-2xl p-4 border border-arch-muted/50">
|
| 92 |
+
<arch-app-icon name="Phone" icon="phone" package="com.android.dialer"></arch-app-icon>
|
| 93 |
+
<arch-app-icon name="Messages" icon="message-square" package="com.google.android.apps.messaging"></arch-app-icon>
|
| 94 |
+
<arch-app-icon name="Browser" icon="globe" package="org.mozilla.firefox"></arch-app-icon>
|
| 95 |
+
<arch-app-icon name="Camera" icon="camera" package="com.sec.android.app.camera"></arch-app-icon>
|
| 96 |
+
</div>
|
| 97 |
+
</section>
|
| 98 |
+
|
| 99 |
+
<!-- Swipe Up Indicator -->
|
| 100 |
+
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 flex flex-col items-center gap-1 opacity-50 animate-bounce">
|
| 101 |
+
<div class="w-12 h-1 bg-arch-muted rounded-full"></div>
|
| 102 |
+
<span class="text-[10px] text-arch-secondary uppercase">App Drawer</span>
|
| 103 |
+
</div>
|
| 104 |
+
</main>
|
| 105 |
+
|
| 106 |
+
<!-- App Drawer (Hidden by default) -->
|
| 107 |
+
<arch-app-drawer id="app-drawer" class="fixed inset-0 translate-y-full transition-transform duration-300 ease-out z-50 bg-arch-bg"></arch-app-drawer>
|
| 108 |
+
|
| 109 |
+
<!-- Quick Settings / Server Panel -->
|
| 110 |
+
<arch-server-panel id="server-panel" class="fixed inset-0 -translate-x-full transition-transform duration-300 ease-out z-50 bg-arch-bg/95 backdrop-blur-xl"></arch-server-panel>
|
| 111 |
+
|
| 112 |
+
<!-- Scripts -->
|
| 113 |
+
<script src="components/arch-status-bar.js"></script>
|
| 114 |
+
<script src="components/arch-app-icon.js"></script>
|
| 115 |
+
<script src="components/arch-app-grid.js"></script>
|
| 116 |
+
<script src="components/arch-app-drawer.js"></script>
|
| 117 |
+
<script src="components/arch-server-panel.js"></script>
|
| 118 |
+
<script src="script.js"></script>
|
| 119 |
+
<script>feather.replace();</script>
|
| 120 |
+
</body>
|
| 121 |
+
</html>
|
script.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ArchDroid Launcher Core Logic
|
| 2 |
+
|
| 3 |
+
class ArchLauncher {
|
| 4 |
+
constructor() {
|
| 5 |
+
this.apps = [
|
| 6 |
+
{ name: 'Terminal', icon: 'terminal', package: 'com.termux', category: 'dev' },
|
| 7 |
+
{ name: 'Files', icon: 'folder', package: 'com.android.documentsui', category: 'system' },
|
| 8 |
+
{ name: 'Settings', icon: 'settings', package: 'com.android.settings', category: 'system' },
|
| 9 |
+
{ name: 'Firefox', icon: 'globe', package: 'org.mozilla.firefox', category: 'internet' },
|
| 10 |
+
{ name: 'Spotify', icon: 'music', package: 'com.spotify.music', category: 'media' },
|
| 11 |
+
{ name: 'Gallery', icon: 'image', package: 'com.android.gallery3d', category: 'media' },
|
| 12 |
+
{ name: 'Calendar', icon: 'calendar', package: 'com.google.android.calendar', category: 'productivity' },
|
| 13 |
+
{ name: 'Email', icon: 'mail', package: 'com.google.android.gm', category: 'internet' },
|
| 14 |
+
{ name: 'Maps', icon: 'map', package: 'com.google.android.apps.maps', category: 'internet' },
|
| 15 |
+
{ name: 'Calculator', icon: 'hash', package: 'com.google.android.calculator', category: 'productivity' },
|
| 16 |
+
{ name: 'Discord', icon: 'message-circle', package: 'com.discord', category: 'social' },
|
| 17 |
+
{ name: 'GitHub', icon: 'github', package: 'com.github.android', category: 'dev' },
|
| 18 |
+
{ name: 'Code', icon: 'code', package: 'com.microsoft.vscode', category: 'dev' },
|
| 19 |
+
{ name: 'SSH', icon: 'server', package: 'com.termux.api', category: 'dev' },
|
| 20 |
+
{ name: 'Monitor', icon: 'activity', package: 'com.samsung.android.app.usagestats', category: 'system' },
|
| 21 |
+
{ name: 'Weather', icon: 'cloud', package: 'com.sec.android.daemonapp', category: 'system' }
|
| 22 |
+
];
|
| 23 |
+
|
| 24 |
+
this.favorites = ['Terminal', 'Firefox', 'Settings', 'Files'];
|
| 25 |
+
this.serverEndpoint = localStorage.getItem('serverEndpoint') || 'http://192.168.1.100:3000';
|
| 26 |
+
this.touchStartY = 0;
|
| 27 |
+
this.init();
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
init() {
|
| 31 |
+
this.setupGestures();
|
| 32 |
+
this.updateUptime();
|
| 33 |
+
this.checkServerStatus();
|
| 34 |
+
this.renderFavorites();
|
| 35 |
+
this.setupTime();
|
| 36 |
+
|
| 37 |
+
// Update uptime every minute
|
| 38 |
+
setInterval(() => this.updateUptime(), 60000);
|
| 39 |
+
// Check server every 30 seconds
|
| 40 |
+
setInterval(() => this.checkServerStatus(), 30000);
|
| 41 |
+
|
| 42 |
+
// Handle app drawer toggle
|
| 43 |
+
document.addEventListener('toggle-app-drawer', () => this.toggleAppDrawer());
|
| 44 |
+
document.addEventListener('toggle-server-panel', () => this.toggleServerPanel());
|
| 45 |
+
|
| 46 |
+
// Handle app launch
|
| 47 |
+
document.addEventListener('launch-app', (e) => this.launchApp(e.detail));
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
setupTime() {
|
| 51 |
+
const updateTime = () => {
|
| 52 |
+
const now = new Date();
|
| 53 |
+
const timeStr = now.toLocaleTimeString('en-US', {
|
| 54 |
+
hour12: false,
|
| 55 |
+
hour: '2-digit',
|
| 56 |
+
minute: '2-digit'
|
| 57 |
+
});
|
| 58 |
+
document.querySelectorAll('.clock-time').forEach(el => el.textContent = timeStr);
|
| 59 |
+
};
|
| 60 |
+
updateTime();
|
| 61 |
+
setInterval(updateTime, 1000);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
updateUptime() {
|
| 65 |
+
// Simulate uptime since last boot (stored in session)
|
| 66 |
+
const startTime = sessionStorage.getItem('bootTime') || Date.now();
|
| 67 |
+
if (!sessionStorage.getItem('bootTime')) {
|
| 68 |
+
sessionStorage.setItem('bootTime', startTime);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
const diff = Date.now() - parseInt(startTime);
|
| 72 |
+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
| 73 |
+
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
| 74 |
+
|
| 75 |
+
const el = document.getElementById('uptime');
|
| 76 |
+
if (el) el.textContent = `${days} days, ${hours} hrs`;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
async checkServerStatus() {
|
| 80 |
+
const statusEl = document.getElementById('server-status');
|
| 81 |
+
if (!statusEl) return;
|
| 82 |
+
|
| 83 |
+
try {
|
| 84 |
+
// Try to fetch from home server with timeout
|
| 85 |
+
const controller = new AbortController();
|
| 86 |
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
| 87 |
+
|
| 88 |
+
const response = await fetch(`${this.serverEndpoint}/health`, {
|
| 89 |
+
signal: controller.signal,
|
| 90 |
+
mode: 'no-cors' // For private servers without CORS
|
| 91 |
+
}).catch(() => null);
|
| 92 |
+
|
| 93 |
+
clearTimeout(timeout);
|
| 94 |
+
|
| 95 |
+
if (response || !response) { // With no-cors we can't read response, assume online if no error
|
| 96 |
+
statusEl.textContent = '● online';
|
| 97 |
+
statusEl.className = 'text-green-400';
|
| 98 |
+
statusEl.classList.remove('animate-pulse');
|
| 99 |
+
window.dispatchEvent(new CustomEvent('server-status', { detail: 'online' }));
|
| 100 |
+
}
|
| 101 |
+
} catch (e) {
|
| 102 |
+
statusEl.textContent = '● offline';
|
| 103 |
+
statusEl.className = 'text-red-400';
|
| 104 |
+
window.dispatchEvent(new CustomEvent('server-status', { detail: 'offline' }));
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
renderFavorites() {
|
| 109 |
+
const grid = document.getElementById('favorites-grid');
|
| 110 |
+
if (!grid) return;
|
| 111 |
+
|
| 112 |
+
const favoriteApps = this.apps.filter(app => this.favorites.includes(app.name));
|
| 113 |
+
|
| 114 |
+
favoriteApps.forEach(app => {
|
| 115 |
+
const icon = document.createElement('arch-app-icon');
|
| 116 |
+
icon.setAttribute('name', app.name);
|
| 117 |
+
icon.setAttribute('icon', app.icon);
|
| 118 |
+
icon.setAttribute('package', app.package);
|
| 119 |
+
grid.appendChild(icon);
|
| 120 |
+
});
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
setupGestures() {
|
| 124 |
+
const main = document.getElementById('main-screen');
|
| 125 |
+
const drawer = document.getElementById('app-drawer');
|
| 126 |
+
|
| 127 |
+
if (!main || !drawer) return;
|
| 128 |
+
|
| 129 |
+
// Swipe up to open drawer
|
| 130 |
+
main.addEventListener('touchstart', (e) => {
|
| 131 |
+
this.touchStartY = e.touches[0].clientY;
|
| 132 |
+
}, { passive: true });
|
| 133 |
+
|
| 134 |
+
main.addEventListener('touchend', (e) => {
|
| 135 |
+
const touchEndY = e.changedTouches[0].clientY;
|
| 136 |
+
const diff = this.touchStartY - touchEndY;
|
| 137 |
+
|
| 138 |
+
// Swipe up from bottom area (> 100px from bottom)
|
| 139 |
+
if (diff > 50 && this.touchStartY > window.innerHeight - 150) {
|
| 140 |
+
this.openAppDrawer();
|
| 141 |
+
}
|
| 142 |
+
}, { passive: true });
|
| 143 |
+
|
| 144 |
+
// Swipe down to close drawer
|
| 145 |
+
drawer.addEventListener('touchstart', (e) => {
|
| 146 |
+
this.touchStartY = e.touches[0].clientY;
|
| 147 |
+
}, { passive: true });
|
| 148 |
+
|
| 149 |
+
drawer.addEventListener('touchend', (e) => {
|
| 150 |
+
const touchEndY = e.changedTouches[0].clientY;
|
| 151 |
+
const diff = touchEndY - this.touchStartY;
|
| 152 |
+
|
| 153 |
+
if (diff > 50) {
|
| 154 |
+
this.closeAppDrawer();
|
| 155 |
+
}
|
| 156 |
+
}, { passive: true });
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
openAppDrawer() {
|
| 160 |
+
const drawer = document.getElementById('app-drawer');
|
| 161 |
+
if (drawer) {
|
| 162 |
+
drawer.classList.remove('translate-y-full');
|
| 163 |
+
drawer.classList.add('translate-y-0');
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
closeAppDrawer() {
|
| 168 |
+
const drawer = document.getElementById('app-drawer');
|
| 169 |
+
if (drawer) {
|
| 170 |
+
drawer.classList.add('translate-y-full');
|
| 171 |
+
drawer.classList.remove('translate-y-0');
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
toggleAppDrawer() {
|
| 176 |
+
const drawer = document.getElementById('app-drawer');
|
| 177 |
+
if (drawer.classList.contains('translate-y-full')) {
|
| 178 |
+
this.openAppDrawer();
|
| 179 |
+
} else {
|
| 180 |
+
this.closeAppDrawer();
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
toggleServerPanel() {
|
| 185 |
+
const panel = document.getElementById('server-panel');
|
| 186 |
+
if (panel) {
|
| 187 |
+
if (panel.classList.contains('-translate-x-full')) {
|
| 188 |
+
panel.classList.remove('-translate-x-full');
|
| 189 |
+
panel.classList.add('translate-x-0');
|
| 190 |
+
} else {
|
| 191 |
+
panel.classList.add('-translate-x-full');
|
| 192 |
+
panel.classList.remove('translate-x-0');
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
launchApp(appData) {
|
| 198 |
+
console.log(`Launching ${appData.name} (${appData.package})...`);
|
| 199 |
+
|
| 200 |
+
// Visual feedback
|
| 201 |
+
const event = new CustomEvent('app-launching', { detail: appData });
|
| 202 |
+
document.dispatchEvent(event);
|
| 203 |
+
|
| 204 |
+
// In a real Android WebView, this would call Android.launchApp(package)
|
| 205 |
+
// For now, show a terminal-style toast
|
| 206 |
+
this.showToast(`> launching ${appData.name.toLowerCase()}...`);
|
| 207 |
+
|
| 208 |
+
// Simulate app opening delay
|
| 209 |
+
setTimeout(() => {
|
| 210 |
+
// Try to open actual app if on Android via intent
|
| 211 |
+
if (window.Android && window.Android.launchApp) {
|
| 212 |
+
window.Android.launchApp(appData.package);
|
| 213 |
+
}
|
| 214 |
+
}, 300);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
showToast(message) {
|
| 218 |
+
const existing = document.querySelector('.arch-toast');
|
| 219 |
+
if (existing) existing.remove();
|
| 220 |
+
|
| 221 |
+
const toast = document.createElement('div');
|
| 222 |
+
toast.className = 'arch-toast fixed top-24 left-1/2 -translate-x-1/2 bg-arch-muted/90 text-arch-primary px-4 py-2 rounded border border-arch-primary/50 text-xs font-mono z-50 animate-pulse';
|
| 223 |
+
toast.textContent = message;
|
| 224 |
+
document.body.appendChild(toast);
|
| 225 |
+
|
| 226 |
+
setTimeout(() => toast.remove(), 2000);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
getAllApps() {
|
| 230 |
+
return this.apps;
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Initialize
|
| 235 |
+
const launcher = new ArchLauncher();
|
| 236 |
+
|
| 237 |
+
// Prevent default touch behaviors for app-like feel
|
| 238 |
+
document.addEventListener('touchmove', function(e) {
|
| 239 |
+
if (e.target.closest('.scrollable')) return;
|
| 240 |
+
// Allow scrolling in specific containers only
|
| 241 |
+
}, { passive: false });
|
| 242 |
+
|
| 243 |
+
// Prevent zoom
|
| 244 |
+
document.addEventListener('gesturestart', function(e) {
|
| 245 |
+
e.preventDefault();
|
| 246 |
+
});
|
style.css
CHANGED
|
@@ -1,28 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
body {
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
}
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
.
|
| 27 |
-
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--arch-primary: 6 182 212;
|
| 3 |
+
--arch-secondary: 100 116 139;
|
| 4 |
+
--arch-bg: 15 23 42;
|
| 5 |
+
--arch-fg: 226 232 240;
|
| 6 |
+
--arch-accent: 34 211 238;
|
| 7 |
+
--arch-muted: 51 65 85;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
* {
|
| 11 |
+
-webkit-tap-highlight-color: transparent;
|
| 12 |
+
touch-action: pan-y pinch-zoom;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
body {
|
| 16 |
+
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 17 |
+
background: rgb(var(--arch-bg));
|
| 18 |
+
color: rgb(var(--arch-fg));
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/* Hide scrollbars but keep functionality */
|
| 22 |
+
::-webkit-scrollbar {
|
| 23 |
+
width: 0px;
|
| 24 |
+
background: transparent;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Custom selection */
|
| 28 |
+
::selection {
|
| 29 |
+
background: rgb(var(--arch-primary));
|
| 30 |
+
color: #000;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* Terminal cursor blink */
|
| 34 |
+
.cursor-blink {
|
| 35 |
+
animation: blink 1s step-end infinite;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
@keyframes blink {
|
| 39 |
+
50% { opacity: 0; }
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Glassmorphism for Android 14 style */
|
| 43 |
+
.glass {
|
| 44 |
+
background: rgba(51, 65, 85, 0.4);
|
| 45 |
+
backdrop-filter: blur(12px);
|
| 46 |
+
-webkit-backdrop-filter: blur(12px);
|
| 47 |
+
border: 1px solid rgba(100, 116, 139, 0.3);
|
| 48 |
}
|
| 49 |
|
| 50 |
+
/* App launch animation */
|
| 51 |
+
@keyframes launchApp {
|
| 52 |
+
0% { transform: scale(1); opacity: 1; }
|
| 53 |
+
50% { transform: scale(0.9); opacity: 0.8; }
|
| 54 |
+
100% { transform: scale(20); opacity: 0; }
|
| 55 |
}
|
| 56 |
|
| 57 |
+
.app-launching {
|
| 58 |
+
animation: launchApp 0.4s ease-out forwards;
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
+
/* Server pulse */
|
| 62 |
+
.server-online {
|
| 63 |
+
color: rgb(34 197 94);
|
| 64 |
+
text-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
+
.server-offline {
|
| 68 |
+
color: rgb(239 68 68);
|
| 69 |
}
|
| 70 |
+
|
| 71 |
+
/* Grid pattern background */
|
| 72 |
+
.bg-grid {
|
| 73 |
+
background-image: linear-gradient(rgba(6, 182, 212, 0.05) 1px, transparent 1px),
|
| 74 |
+
linear-gradient(90deg, rgba(6, 182, 212, 0.05) 1px, transparent 1px);
|
| 75 |
+
background-size: 20px 20px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* Input styling like terminal */
|
| 79 |
+
input[type="text"] {
|
| 80 |
+
background: transparent;
|
| 81 |
+
border: none;
|
| 82 |
+
border-bottom: 2px solid rgb(var(--arch-muted));
|
| 83 |
+
color: rgb(var(--arch-fg));
|
| 84 |
+
font-family: inherit;
|
| 85 |
+
outline: none;
|
| 86 |
+
transition: border-color 0.3s;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
input[type="text"]:focus {
|
| 90 |
+
border-bottom-color: rgb(var(--arch-primary));
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* Prevent text selection on UI elements */
|
| 94 |
+
.no-select {
|
| 95 |
+
user-select: none;
|
| 96 |
+
-webkit-user-select: none;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* Smooth scrolling */
|
| 100 |
+
.smooth-scroll {
|
| 101 |
+
scroll-behavior: smooth;
|
| 102 |
+
-webkit-overflow-scrolling: touch;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* Status bar height for S23 Ultra */
|
| 106 |
+
.status-bar-height {
|
| 107 |
+
height: env(safe-area-inset-top, 32px);
|
| 108 |
+
padding-top: env(safe-area-inset-top, 0);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* Bottom safe area */
|
| 112 |
+
.safe-bottom {
|
| 113 |
+
padding-bottom: env(safe-area-inset-bottom, 16px);
|
| 114 |
+
}
|
sw.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CACHE_NAME = 'archdroid-v1';
|
| 2 |
+
const PARALLEL_WORKERS = [
|
| 3 |
+
'/workers/settings-worker.js',
|
| 4 |
+
'/workers/app-data-worker.js',
|
| 5 |
+
'/workers/gesture-worker.js',
|
| 6 |
+
'/workers/notification-worker.js',
|
| 7 |
+
'/workers/system-sync-worker.js'
|
| 8 |
+
];
|
| 9 |
+
|
| 10 |
+
const STATIC_ASSETS = [
|
| 11 |
+
'/',
|
| 12 |
+
'/index.html',
|
| 13 |
+
'/style.css',
|
| 14 |
+
'/script.js',
|
| 15 |
+
'/components/arch-status-bar.js',
|
| 16 |
+
'/components/arch-app-icon.js',
|
| 17 |
+
'/components/arch-app-grid.js',
|
| 18 |
+
'/components/arch-app-drawer.js',
|
| 19 |
+
'/components/arch-server-panel.js',
|
| 20 |
+
'/components/arch-settings-panel.js',
|
| 21 |
+
'/components/arch-notifications.js'
|
| 22 |
+
];
|
| 23 |
+
|
| 24 |
+
// Install: Cache static assets
|
| 25 |
+
self.addEventListener('install', (event) => {
|
| 26 |
+
event.waitUntil(
|
| 27 |
+
caches.open(CACHE_NAME).then((cache) => {
|
| 28 |
+
return cache.addAll(STATIC_ASSETS);
|
| 29 |
+
})
|
| 30 |
+
);
|
| 31 |
+
self.skipWaiting();
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
// Activate: Clean old caches
|
| 35 |
+
self.addEventListener('activate', (event) => {
|
| 36 |
+
event.waitUntil(
|
| 37 |
+
caches.keys().then((cacheNames) => {
|
| 38 |
+
return Promise.all(
|
| 39 |
+
cacheNames
|
| 40 |
+
.filter((name) => name !== CACHE_NAME)
|
| 41 |
+
.map((name) => caches.delete(name))
|
| 42 |
+
);
|
| 43 |
+
})
|
| 44 |
+
);
|
| 45 |
+
self.clients.claim();
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
// Message handling from main thread
|
| 49 |
+
self.addEventListener('message', async (event) => {
|
| 50 |
+
const { type, payload } = event.data;
|
| 51 |
+
|
| 52 |
+
switch (type) {
|
| 53 |
+
case 'SYNC_SETTINGS':
|
| 54 |
+
await syncSettings(event.source.id);
|
| 55 |
+
break;
|
| 56 |
+
case 'FETCH_APPS':
|
| 57 |
+
await fetchAppsParallel(event.source.id);
|
| 58 |
+
break;
|
| 59 |
+
case 'REQUEST_PERMISSION':
|
| 60 |
+
await handlePermission(payload);
|
| 61 |
+
break;
|
| 62 |
+
}
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
// Parallel settings sync
|
| 66 |
+
async function syncSettings(clientId) {
|
| 67 |
+
const settings = await Promise.all([
|
| 68 |
+
fetch('/api/android/settings/display').catch(() => null),
|
| 69 |
+
fetch('/api/android/settings/sound').catch(() => null),
|
| 70 |
+
fetch('/api/android/settings/notifications').catch(() => null),
|
| 71 |
+
fetch('/api/android/settings/accessibility').catch(() => null)
|
| 72 |
+
]);
|
| 73 |
+
|
| 74 |
+
const client = await self.clients.get(clientId);
|
| 75 |
+
if (client) {
|
| 76 |
+
client.postMessage({
|
| 77 |
+
type: 'SETTINGS_SYNCED',
|
| 78 |
+
payload: {
|
| 79 |
+
display: settings[0],
|
| 80 |
+
sound: settings[1],
|
| 81 |
+
notifications: settings[2],
|
| 82 |
+
accessibility: settings[3]
|
| 83 |
+
}
|
| 84 |
+
});
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Parallel app data fetching with worker pool
|
| 89 |
+
async function fetchAppsParallel(clientId) {
|
| 90 |
+
const channels = ['system', 'user', 'disabled', 'work'];
|
| 91 |
+
|
| 92 |
+
const results = await Promise.all(channels.map(channel =>
|
| 93 |
+
fetch(`/api/android/apps/${channel}`).catch(() => [])
|
| 94 |
+
));
|
| 95 |
+
|
| 96 |
+
const allApps = results.flat();
|
| 97 |
+
|
| 98 |
+
const client = await self.clients.get(clientId);
|
| 99 |
+
if (client) {
|
| 100 |
+
client.postMessage({
|
| 101 |
+
type: 'APPS_FETCHED',
|
| 102 |
+
payload: allApps
|
| 103 |
+
});
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Fetch strategy with network first, cache fallback
|
| 108 |
+
self.addEventListener('fetch', (event) => {
|
| 109 |
+
event.respondWith(
|
| 110 |
+
fetch(event.request)
|
| 111 |
+
.then((response) => {
|
| 112 |
+
// Cache successful responses
|
| 113 |
+
if (response.ok) {
|
| 114 |
+
const clone = response.clone();
|
| 115 |
+
caches.open(CACHE_NAME).then((cache) => {
|
| 116 |
+
cache.put(event.request, clone);
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
+
return response;
|
| 120 |
+
})
|
| 121 |
+
.catch(() => {
|
| 122 |
+
// Fallback to cache
|
| 123 |
+
return caches.match(event.request);
|
| 124 |
+
})
|
| 125 |
+
);
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
// Background sync for settings
|
| 129 |
+
self.addEventListener('sync', (event) => {
|
| 130 |
+
if (event.tag === 'settings-sync') {
|
| 131 |
+
event.waitUntil(syncSettings(self.clients.matchAll()[0]?.id));
|
| 132 |
+
}
|
| 133 |
+
});
|
workers/app-data-worker.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// App Data Worker - Fetches installed apps from Android
|
| 2 |
+
let appCache = [];
|
| 3 |
+
let lastUpdate = 0;
|
| 4 |
+
|
| 5 |
+
self.addEventListener('message', async (e) => {
|
| 6 |
+
const { action, filter } = e.data;
|
| 7 |
+
|
| 8 |
+
switch (action) {
|
| 9 |
+
case 'FETCH_ALL':
|
| 10 |
+
const apps = await fetchInstalledApps();
|
| 11 |
+
self.postMessage({ type: 'APPS_LOADED', apps, count: apps.length });
|
| 12 |
+
break;
|
| 13 |
+
|
| 14 |
+
case 'FETCH_CATEGORIES':
|
| 15 |
+
const categories = await fetchAppCategories();
|
| 16 |
+
self.postMessage({ type: 'CATEGORIES_LOADED', categories });
|
| 17 |
+
break;
|
| 18 |
+
|
| 19 |
+
case 'LAUNCH_APP':
|
| 20 |
+
await launchApp(filter.package);
|
| 21 |
+
break;
|
| 22 |
+
|
| 23 |
+
case 'GET_APP_INFO':
|
| 24 |
+
const info = await getAppInfo(filter.package);
|
| 25 |
+
self.postMessage({ type: 'APP_INFO', info });
|
| 26 |
+
break;
|
| 27 |
+
|
| 28 |
+
case 'SEARCH':
|
| 29 |
+
const results = searchApps(filter.query);
|
| 30 |
+
self.postMessage({ type: 'SEARCH_RESULTS', results });
|
| 31 |
+
break;
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
async function fetchInstalledApps() {
|
| 36 |
+
const now = Date.now();
|
| 37 |
+
|
| 38 |
+
// Return cached if recent (< 5 seconds)
|
| 39 |
+
if (now - lastUpdate < 5000 && appCache.length > 0) {
|
| 40 |
+
return appCache;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
let apps = [];
|
| 44 |
+
|
| 45 |
+
// Try Android interface first
|
| 46 |
+
if (self.Android?.PackageManager?.getInstalledApps) {
|
| 47 |
+
apps = JSON.parse(self.Android.PackageManager.getInstalledApps());
|
| 48 |
+
} else {
|
| 49 |
+
// Fallback: Generate from localStorage or defaults
|
| 50 |
+
apps = getFallbackApps();
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Parallel icon fetching
|
| 54 |
+
const withIcons = await Promise.all(
|
| 55 |
+
apps.map(async (app) => ({
|
| 56 |
+
...app,
|
| 57 |
+
hasIcon: await checkIcon(app.package),
|
| 58 |
+
iconUrl: `/api/android/icon/${app.package}`
|
| 59 |
+
}))
|
| 60 |
+
);
|
| 61 |
+
|
| 62 |
+
appCache = withIcons;
|
| 63 |
+
lastUpdate = now;
|
| 64 |
+
|
| 65 |
+
return withIcons;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
async function fetchAppCategories() {
|
| 69 |
+
const apps = await fetchInstalledApps();
|
| 70 |
+
const categories = {};
|
| 71 |
+
|
| 72 |
+
apps.forEach(app => {
|
| 73 |
+
const cat = app.category || 'uncategorized';
|
| 74 |
+
if (!categories[cat]) categories[cat] = [];
|
| 75 |
+
categories[cat].push(app);
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
return categories;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function searchApps(query) {
|
| 82 |
+
const q = query.toLowerCase();
|
| 83 |
+
return appCache.filter(app =>
|
| 84 |
+
app.name.toLowerCase().includes(q) ||
|
| 85 |
+
app.package.toLowerCase().includes(q)
|
| 86 |
+
);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
async function launchApp(packageName) {
|
| 90 |
+
if (self.Android?.Intent?.launch) {
|
| 91 |
+
self.Android.Intent.launch(packageName);
|
| 92 |
+
return true;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// PWA fallback - try to open as URL scheme
|
| 96 |
+
try {
|
| 97 |
+
window.open(`android-app://${packageName}`, '_blank');
|
| 98 |
+
} catch (e) {
|
| 99 |
+
self.postMessage({ type: 'LAUNCH_FAILED', package: packageName });
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
async function getAppInfo(packageName) {
|
| 104 |
+
const app = appCache.find(a => a.package === packageName);
|
| 105 |
+
if (app) return app;
|
| 106 |
+
|
| 107 |
+
// Fetch fresh
|
| 108 |
+
if (self.Android?.PackageManager?.getAppInfo) {
|
| 109 |
+
return JSON.parse(self.Android.PackageManager.getAppInfo(packageName));
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return null;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
async function checkIcon(packageName) {
|
| 116 |
+
// Check if icon exists in cache
|
| 117 |
+
return true; // Simplified for worker
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
function getFallbackApps() {
|
| 121 |
+
// Comprehensive default app list for Samsung S23 Ultra
|
| 122 |
+
return [
|
| 123 |
+
// System
|
| 124 |
+
{ name: 'Settings', package: 'com.android.settings', category: 'system', icon: 'settings' },
|
| 125 |
+
{ name: 'Files', package: 'com.android.documentsui', category: 'system', icon: 'folder' },
|
| 126 |
+
{ name: 'Calculator', package: 'com.google.android.calculator', category: 'system', icon: 'hash' },
|
| 127 |
+
{ name: 'Calendar', package: 'com.samsung.android.calendar', category: 'system', icon: 'calendar' },
|
| 128 |
+
{ name: 'Clock', package: 'com.sec.android.app.clockpackage', category: 'system', icon: 'clock' },
|
| 129 |
+
{ name: 'Weather', package: 'com.sec.android.daemonapp', category: 'system', icon: 'cloud' },
|
| 130 |
+
{ name: 'Samsung Notes', package: 'com.samsung.android.app.notes', category: 'system', icon: 'file-text' },
|
| 131 |
+
{ name: 'SmartThings', package: 'com.samsung.android.oneconnect', category: 'system', icon: 'home' },
|
| 132 |
+
{ name: 'Device Care', package: 'com.samsung.android.lool', category: 'system', icon: 'shield' },
|
| 133 |
+
{ name: 'Gallery', package: 'com.samsung.android.gallery3d', category: 'media', icon: 'image' },
|
| 134 |
+
{ name: 'Camera', package: 'com.sec.android.app.camera', category: 'media', icon: 'camera' },
|
| 135 |
+
{ name: 'Voice Recorder', package: 'com.sec.android.app.voicenote', category: 'media', icon: 'mic' },
|
| 136 |
+
|
| 137 |
+
// Communication
|
| 138 |
+
{ name: 'Phone', package: 'com.samsung.android.dialer', category: 'communication', icon: 'phone' },
|
| 139 |
+
{ name: 'Messages', package: 'com.samsung.android.messaging', category: 'communication', icon: 'message-square' },
|
| 140 |
+
{ name: 'Contacts', package: 'com.samsung.android.app.contacts', category: 'communication', icon: 'users' },
|
| 141 |
+
{ name: 'Gmail', package: 'com.google.android.gm', category: 'communication', icon: 'mail' },
|
| 142 |
+
|
| 143 |
+
// Internet
|
| 144 |
+
{ name: 'Samsung Internet', package: 'com.sec.android.app.sbrowser', category: 'internet', icon: 'globe' },
|
| 145 |
+
{ name: 'Chrome', package: 'com.android.chrome', category: 'internet', icon: 'chrome' },
|
| 146 |
+
|
| 147 |
+
// Media
|
| 148 |
+
{ name: 'Spotify', package: 'com.spotify.music', category: 'media', icon: 'music' },
|
| 149 |
+
{ name: 'YouTube', package: 'com.google.android.youtube', category: 'media', icon: 'youtube' },
|
| 150 |
+
{ name: 'Netflix', package: 'com.netflix.mediaclient', category: 'media', icon: 'film' },
|
| 151 |
+
{ name: 'Samsung Music', package: 'com.sec.android.app.music', category: 'media', icon: 'headphones' },
|
| 152 |
+
|
| 153 |
+
// Productivity
|
| 154 |
+
{ name: 'Google Drive', package: 'com.google.android.apps.docs', category: 'productivity', icon: 'hard-drive' },
|
| 155 |
+
{ name: 'Samsung Wallet', package: 'com.samsung.android.spay', category: 'productivity', icon: 'credit-card' },
|
| 156 |
+
{ name: 'Maps', package: 'com.google.android.apps.maps', category: 'productivity', icon: 'map' },
|
| 157 |
+
|
| 158 |
+
// Social
|
| 159 |
+
{ name: 'Samsung Members', package: 'com.samsung.android.voc', category: 'social', icon: 'users' },
|
| 160 |
+
|
| 161 |
+
// Development
|
| 162 |
+
{ name: 'Termux', package: 'com.termux', category: 'dev', icon: 'terminal' },
|
| 163 |
+
];
|
| 164 |
+
}
|
workers/gesture-worker.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Gesture Worker - Processes touch gestures off main thread
|
| 2 |
+
const GESTURE_THRESHOLD = 50;
|
| 3 |
+
const VELOCITY_THRESHOLD = 0.5;
|
| 4 |
+
|
| 5 |
+
let gestureState = {
|
| 6 |
+
startX: 0,
|
| 7 |
+
startY: 0,
|
| 8 |
+
startTime: 0,
|
| 9 |
+
lastX: 0,
|
| 10 |
+
lastY: 0,
|
| 11 |
+
isTracking: false
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
self.addEventListener('message', (e) => {
|
| 15 |
+
const { type, event } = e.data;
|
| 16 |
+
|
| 17 |
+
switch (type) {
|
| 18 |
+
case 'TOUCH_START':
|
| 19 |
+
handleTouchStart(event);
|
| 20 |
+
break;
|
| 21 |
+
case 'TOUCH_MOVE':
|
| 22 |
+
handleTouchMove(event);
|
| 23 |
+
break;
|
| 24 |
+
case 'TOUCH_END':
|
| 25 |
+
const gesture = handleTouchEnd(event);
|
| 26 |
+
if (gesture) {
|
| 27 |
+
self.postMessage({ type: 'GESTURE_DETECTED', gesture });
|
| 28 |
+
}
|
| 29 |
+
break;
|
| 30 |
+
}
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
function handleTouchStart(event) {
|
| 34 |
+
gestureState = {
|
| 35 |
+
startX: event.x,
|
| 36 |
+
startY: event.y,
|
| 37 |
+
startTime: event.time,
|
| 38 |
+
lastX: event.x,
|
| 39 |
+
lastY: event.y,
|
| 40 |
+
isTracking: true
|
| 41 |
+
};
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
function handleTouchMove(event) {
|
| 45 |
+
if (!gestureState.isTracking) return;
|
| 46 |
+
|
| 47 |
+
gestureState.lastX = event.x;
|
| 48 |
+
gestureState.lastY = event.y;
|
| 49 |
+
|
| 50 |
+
// Calculate current delta for live feedback
|
| 51 |
+
const deltaX = event.x - gestureState.startX;
|
| 52 |
+
const deltaY = event.y - gestureState.startY;
|
| 53 |
+
|
| 54 |
+
self.postMessage({
|
| 55 |
+
type: 'GESTURE_PROGRESS',
|
| 56 |
+
deltaX, deltaY,
|
| 57 |
+
progress: Math.min(Math.abs(deltaY) / 300, 1)
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function handleTouchEnd(event) {
|
| 62 |
+
if (!gestureState.isTracking) return null;
|
| 63 |
+
|
| 64 |
+
gestureState.isTracking = false;
|
| 65 |
+
|
| 66 |
+
const deltaX = event.x - gestureState.startX;
|
| 67 |
+
const deltaY = event.y - gestureState.startY;
|
| 68 |
+
const deltaTime = event.time - gestureState.startTime;
|
| 69 |
+
const velocityX = deltaX / deltaTime;
|
| 70 |
+
const velocityY = deltaY / deltaTime;
|
| 71 |
+
|
| 72 |
+
// Detect gesture type
|
| 73 |
+
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
| 74 |
+
// Vertical gesture
|
| 75 |
+
if (Math.abs(deltaY) > GESTURE_THRESHOLD && Math.abs(velocityY) > VELOCITY_THRESHOLD) {
|
| 76 |
+
if (deltaY < 0) {
|
| 77 |
+
return { type: 'SWIPE_UP', velocity: velocityY, distance: Math.abs(deltaY) };
|
| 78 |
+
} else {
|
| 79 |
+
return { type: 'SWIPE_DOWN', velocity: velocityY, distance: Math.abs(deltaY) };
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Edge swipe detection
|
| 84 |
+
if (gestureState.startY < 50 && deltaY > 100) {
|
| 85 |
+
return { type: 'STATUS_PULL' };
|
| 86 |
+
}
|
| 87 |
+
if (gestureState.startY > (self.innerHeight - 50) && deltaY < -100) {
|
| 88 |
+
return { type: 'HOME_GESTURE' };
|
| 89 |
+
}
|
| 90 |
+
} else {
|
| 91 |
+
// Horizontal gesture
|
| 92 |
+
if (Math.abs(deltaX) > GESTURE_THRESHOLD && Math.abs(velocityX) > VELOCITY_THRESHOLD) {
|
| 93 |
+
if (deltaX < 0) {
|
| 94 |
+
return { type: 'SWIPE_LEFT', velocity: velocityX };
|
| 95 |
+
} else {
|
| 96 |
+
return { type: 'SWIPE_RIGHT', velocity: velocityX };
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Edge swipe
|
| 101 |
+
if (gestureState.startX < 20) {
|
| 102 |
+
return { type: 'BACK_GESTURE' };
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Quick settings from top-right
|
| 107 |
+
if (gestureState.startY < 50 && gestureState.startX > self.innerWidth - 100 && deltaY > 50) {
|
| 108 |
+
return { type: 'QUICK_SETTINGS' };
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Notifications from top-left
|
| 112 |
+
if (gestureState.startY < 50 && gestureState.startX < 100 && deltaY > 50) {
|
| 113 |
+
return { type: 'NOTIFICATIONS' };
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
return null;
|
| 117 |
+
}
|
workers/notification-worker.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Notification Worker - Manages notification state
|
| 2 |
+
let notifications = [];
|
| 3 |
+
let notificationHistory = [];
|
| 4 |
+
const MAX_NOTIFICATIONS = 25;
|
| 5 |
+
|
| 6 |
+
self.addEventListener('message', (e) => {
|
| 7 |
+
const { action, data } = e.data;
|
| 8 |
+
|
| 9 |
+
switch (action) {
|
| 10 |
+
case 'FETCH':
|
| 11 |
+
fetchNotifications();
|
| 12 |
+
break;
|
| 13 |
+
case 'POST':
|
| 14 |
+
postNotification(data);
|
| 15 |
+
break;
|
| 16 |
+
case 'CLEAR':
|
| 17 |
+
clearNotification(data.id);
|
| 18 |
+
break;
|
| 19 |
+
case 'CLEAR_ALL':
|
| 20 |
+
clearAllNotifications();
|
| 21 |
+
break;
|
| 22 |
+
case 'GET_HISTORY':
|
| 23 |
+
self.postMessage({ type: 'HISTORY', history: notificationHistory.slice(-50) });
|
| 24 |
+
break;
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
async function fetchNotifications() {
|
| 29 |
+
let activeNotifs = [];
|
| 30 |
+
|
| 31 |
+
// Try Android notification listener
|
| 32 |
+
if (self.Android?.NotificationListener?.getActiveNotifications) {
|
| 33 |
+
activeNotifs = JSON.parse(self.Android.NotificationListener.getActiveNotifications());
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Fallback: Generate from stored or mock for testing
|
| 37 |
+
if (activeNotifs.length === 0) {
|
| 38 |
+
activeNotifs = getMockNotifications();
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
notifications = activeNotifs;
|
| 42 |
+
|
| 43 |
+
self.postMessage({
|
| 44 |
+
type: 'NOTIFICATIONS',
|
| 45 |
+
notifications: notifications.slice(0, MAX_NOTIFICATIONS),
|
| 46 |
+
hasMore: notifications.length > MAX_NOTIFICATIONS
|
| 47 |
+
});
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function postNotification(data) {
|
| 51 |
+
const notif = {
|
| 52 |
+
id: Date.now().toString(),
|
| 53 |
+
package: 'archdroid.launcher',
|
| 54 |
+
title: data.title,
|
| 55 |
+
text: data.text,
|
| 56 |
+
timestamp: Date.now(),
|
| 57 |
+
actions: data.actions || []
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
notifications.unshift(notif);
|
| 61 |
+
|
| 62 |
+
if (notifications.length > MAX_NOTIFICATIONS) {
|
| 63 |
+
notificationHistory.push(...notifications.splice(MAX_NOTIFICATIONS));
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
self.postMessage({ type: 'NEW_NOTIFICATION', notification: notif });
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function clearNotification(id) {
|
| 70 |
+
notifications = notifications.filter(n => n.id !== id);
|
| 71 |
+
|
| 72 |
+
if (self.Android?.NotificationListener?.cancelNotification) {
|
| 73 |
+
self.Android.NotificationListener.cancelNotification(id);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
self.postMessage({ type: 'NOTIFICATION_CLEARED', id });
|
| 77 |
+
broadcastUpdate();
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function clearAllNotifications() {
|
| 81 |
+
notificationHistory.unshift(...notifications);
|
| 82 |
+
notifications = [];
|
| 83 |
+
|
| 84 |
+
self.postMessage({ type: 'ALL_CLEARED' });
|
| 85 |
+
broadcastUpdate();
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
function broadcastUpdate() {
|
| 89 |
+
self.postMessage({
|
| 90 |
+
type: 'NOTIFICATIONS',
|
| 91 |
+
notifications: notifications.slice(0, MAX_NOTIFICATIONS),
|
| 92 |
+
count: notifications.length
|
| 93 |
+
});
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
function getMockNotifications() {
|
| 97 |
+
return [
|
| 98 |
+
{
|
| 99 |
+
id: '1',
|
| 100 |
+
package: 'com.samsung.android.messaging',
|
| 101 |
+
title: 'Sarah Chen',
|
| 102 |
+
text: 'Meeting at 3pm tomorrow, don\'t forget the docs! 📄',
|
| 103 |
+
timestamp: Date.now() - 120000,
|
| 104 |
+
icon: 'message-square',
|
| 105 |
+
actions: ['Reply', 'Mark as read']
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
id: '2',
|
| 109 |
+
package: 'com.google.android.gm',
|
| 110 |
+
title: 'GitHub',
|
| 111 |
+
text: 'New pull request: "Fix gesture worker memory leak"',
|
| 112 |
+
timestamp: Date.now() - 300000,
|
| 113 |
+
icon: 'github',
|
| 114 |
+
actions: ['View', 'Archive']
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
id: '3',
|
| 118 |
+
package: 'com.samsung.android.calendar',
|
| 119 |
+
title: 'Upcoming: Stand-up',
|
| 120 |
+
text: 'In 15 minutes • Video call with Engineering team',
|
| 121 |
+
timestamp: Date.now() - 6000000,
|
| 122 |
+
icon: 'calendar',
|
| 123 |
+
actions: ['Join', 'Snooze']
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
id: '4',
|
| 127 |
+
package: 'com.spotify.music',
|
| 128 |
+
title: 'Now playing',
|
| 129 |
+
text: 'Cyberpunk 2077: I Really Want to Stay At Your House',
|
| 130 |
+
timestamp: Date.now() - 900000,
|
| 131 |
+
icon: 'music',
|
| 132 |
+
progress: 45,
|
| 133 |
+
actions: ['❚❚', '⏭']
|
| 134 |
+
}
|
| 135 |
+
];
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Periodic sync
|
| 139 |
+
setInterval(fetchNotifications, 5000);
|
workers/settings-worker.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Settings Worker - Handles Android system settings in parallel
|
| 2 |
+
self.addEventListener('message', async (e) => {
|
| 3 |
+
const { action, settings } = e.data;
|
| 4 |
+
|
| 5 |
+
switch (action) {
|
| 6 |
+
case 'READ_ALL':
|
| 7 |
+
const allSettings = await readAllSettings();
|
| 8 |
+
self.postMessage({ type: 'SETTINGS_BULK', settings: allSettings });
|
| 9 |
+
break;
|
| 10 |
+
|
| 11 |
+
case 'READ':
|
| 12 |
+
const value = await readSetting(settings.key);
|
| 13 |
+
self.postMessage({ type: 'SETTING_VALUE', key: settings.key, value });
|
| 14 |
+
break;
|
| 15 |
+
|
| 16 |
+
case 'WRITE':
|
| 17 |
+
await writeSetting(settings.key, settings.value);
|
| 18 |
+
self.postMessage({ type: 'SETTING_SAVED', key: settings.key });
|
| 19 |
+
break;
|
| 20 |
+
|
| 21 |
+
case 'WATCH':
|
| 22 |
+
await watchSetting(settings.key);
|
| 23 |
+
break;
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
const ANDROID_SETTINGS = {
|
| 28 |
+
// Display
|
| 29 |
+
'display_brightness': 'system.SCREEN_BRIGHTNESS',
|
| 30 |
+
'display_auto_rotate': 'system.ACCELEROMETER_ROTATION',
|
| 31 |
+
'display_font_size': 'system.FONT_SCALE',
|
| 32 |
+
'display_screen_timeout': 'system.SCREEN_OFF_TIMEOUT',
|
| 33 |
+
'display_night_mode': 'secure.UI_NIGHT_MODE',
|
| 34 |
+
|
| 35 |
+
// Sound
|
| 36 |
+
'sound_volume_ring': 'system.VOLUME_RING',
|
| 37 |
+
'sound_volume_media': 'system.VOLUME_MUSIC',
|
| 38 |
+
'sound_volume_alarm': 'system.VOLUME_ALARM',
|
| 39 |
+
'sound_vibrate_mode': 'system.VIBRATE_WHEN_RINGING',
|
| 40 |
+
'sound_dnd': 'global.ZEN_MODE',
|
| 41 |
+
|
| 42 |
+
// Notifications
|
| 43 |
+
'notification_enabled': 'global.ENABLED_NOTIFICATION_LISTENERS',
|
| 44 |
+
'notification_badging': 'secure.NOTIFICATION_BADGING',
|
| 45 |
+
'notification_heads_up': 'global.HEADS_UP_NOTIFICATIONS_ENABLED',
|
| 46 |
+
|
| 47 |
+
// Connectivity
|
| 48 |
+
'wifi_enabled': 'global.WIFI_ON',
|
| 49 |
+
'mobile_data': 'global.MOBILE_DATA',
|
| 50 |
+
'airplane_mode': 'global.AIRPLANE_MODE_ON',
|
| 51 |
+
'bluetooth_enabled': 'global.BLUETOOTH_ON',
|
| 52 |
+
|
| 53 |
+
// Location
|
| 54 |
+
'location_mode': 'secure.LOCATION_MODE',
|
| 55 |
+
|
| 56 |
+
// Privacy
|
| 57 |
+
'camera_disabled': 'secure.CAMERA_DISABLED',
|
| 58 |
+
'microphone_disabled': 'secure.MICROPHONE_DISABLED',
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
async function readAllSettings() {
|
| 62 |
+
const results = {};
|
| 63 |
+
const batch = [];
|
| 64 |
+
|
| 65 |
+
// Read in parallel batches of 4
|
| 66 |
+
const entries = Object.entries(ANDROID_SETTINGS);
|
| 67 |
+
for (let i = 0; i < entries.length; i += 4) {
|
| 68 |
+
const batch = entries.slice(i, i + 4).map(([key, uri]) => readSetting(key));
|
| 69 |
+
await Promise.all(batch);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return results;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
async function readSetting(key) {
|
| 76 |
+
// Try to access via Android interface or fallback
|
| 77 |
+
if (self.Android?.Settings?.get) {
|
| 78 |
+
return self.Android.Settings.get(ANDROID_SETTINGS[key]);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Fallback for PWA testing
|
| 82 |
+
return localStorage.getItem(`arch_setting_${key}`) ?? getDefaultSetting(key);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
async function writeSetting(key, value) {
|
| 86 |
+
if (self.Android?.Settings?.put) {
|
| 87 |
+
self.Android.Settings.put(ANDROID_SETTINGS[key], value);
|
| 88 |
+
}
|
| 89 |
+
localStorage.setItem(`arch_setting_${key}`, value);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function getDefaultSetting(key) {
|
| 93 |
+
const defaults = {
|
| 94 |
+
'display_brightness': 128,
|
| 95 |
+
'display_auto_rotate': 1,
|
| 96 |
+
'display_screen_timeout': 30000,
|
| 97 |
+
'sound_volume_media': 80,
|
| 98 |
+
'wifi_enabled': 1,
|
| 99 |
+
'location_mode': 3
|
| 100 |
+
};
|
| 101 |
+
return defaults[key] ?? null;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
async function watchSetting(key) {
|
| 105 |
+
// Set up content observer simulation
|
| 106 |
+
setInterval(async () => {
|
| 107 |
+
const value = await readSetting(key);
|
| 108 |
+
self.postMessage({ type: 'SETTING_CHANGED', key, value });
|
| 109 |
+
}, 1000);
|
| 110 |
+
}
|
workers/system-sync-worker.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// System Sync Worker - Syncs system state periodically
|
| 2 |
+
const SYNC_INTERVAL = 5000;
|
| 3 |
+
|
| 4 |
+
let systemState = {
|
| 5 |
+
battery: null,
|
| 6 |
+
storage: null,
|
| 7 |
+
memory: null,
|
| 8 |
+
cpu: null,
|
| 9 |
+
network: null,
|
| 10 |
+
thermal: null
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
self.addEventListener('message', (e) => {
|
| 14 |
+
const { action } = e.data;
|
| 15 |
+
|
| 16 |
+
switch (action) {
|
| 17 |
+
case 'START_SYNC':
|
| 18 |
+
startPeriodicSync();
|
| 19 |
+
break;
|
| 20 |
+
case 'STOP_SYNC':
|
| 21 |
+
stopPeriodicSync();
|
| 22 |
+
break;
|
| 23 |
+
case 'GET_STATUS':
|
| 24 |
+
self.postMessage({ type: 'SYSTEM_STATUS', state: systemState });
|
| 25 |
+
break;
|
| 26 |
+
case 'SYNC_NOW':
|
| 27 |
+
performSync();
|
| 28 |
+
break;
|
| 29 |
+
}
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
let syncInterval;
|
| 33 |
+
|
| 34 |
+
function startPeriodicSync() {
|
| 35 |
+
performSync();
|
| 36 |
+
syncInterval = setInterval(performSync, SYNC_INTERVAL);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function stopPeriodicSync() {
|
| 40 |
+
clearInterval(syncInterval);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
async function performSync() {
|
| 44 |
+
const [battery, storage, memory, network, thermal] = await Promise.all([
|
| 45 |
+
getBatteryStatus(),
|
| 46 |
+
getStorageInfo(),
|
| 47 |
+
getMemoryInfo(),
|
| 48 |
+
getNetworkStatus(),
|
| 49 |
+
getThermalStatus()
|
| 50 |
+
]);
|
| 51 |
+
|
| 52 |
+
systemState = {
|
| 53 |
+
battery,
|
| 54 |
+
storage,
|
| 55 |
+
memory,
|
| 56 |
+
network,
|
| 57 |
+
thermal,
|
| 58 |
+
timestamp: Date.now()
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
self.postMessage({
|
| 62 |
+
type: 'SYSTEM_STATUS',
|
| 63 |
+
state: systemState
|
| 64 |
+
});
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
async function getBatteryStatus() {
|
| 68 |
+
if (self.navigator?.getBattery) {
|
| 69 |
+
const battery = await self.navigator.getBattery();
|
| 70 |
+
return {
|
| 71 |
+
level: Math.round(battery.level * 100),
|
| 72 |
+
charging: battery.charging,
|
| 73 |
+
chargingTime: battery.chargingTime,
|
| 74 |
+
dischargingTime: battery.dischargingTime
|
| 75 |
+
};
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Fallback via Android interface
|
| 79 |
+
if (self.Android?.Battery?.getStatus) {
|
| 80 |
+
return JSON.parse(self.Android.Battery.getStatus());
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
return { level: 84, charging: true, temperature: 34 };
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
async function getStorageInfo() {
|
| 87 |
+
if (self.Android?.Storage?.getInfo) {
|
| 88 |
+
return JSON.parse(self.Android.Storage.getInfo());
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Estimate from storage API
|
| 92 |
+
if (self.navigator?.storage?.estimate) {
|
| 93 |
+
const estimate = await self.navigator.storage.estimate();
|
| 94 |
+
return {
|
| 95 |
+
total: 256000000000, // 256GB for S23 Ultra
|
| 96 |
+
used: estimate.usage || 128000000000,
|
| 97 |
+
available: estimate.quota - estimate.usage || 128000000000
|
| 98 |
+
};
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return { total: 256e9, used: 128e9, available: 128e9 };
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
async function getMemoryInfo() {
|
| 105 |
+
if (self.Android?.ActivityManager?.getMemoryInfo) {
|
| 106 |
+
return JSON.parse(self.Android.ActivityManager.getMemoryInfo());
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
if (self.performance?.memory) {
|
| 110 |
+
const mem = self.performance.memory;
|
| 111 |
+
return {
|
| 112 |
+
total: 8589934592, // 8GB standard
|
| 113 |
+
available: mem.jsHeapSizeLimit - mem.usedJSHeapSize,
|
| 114 |
+
used: mem.usedJSHeapSize,
|
| 115 |
+
threshold: 536870912
|
| 116 |
+
};
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
return { total: 8e9, available: 4e9, used: 4e9, threshold: 5e8 };
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
async function getNetworkStatus() {
|
| 123 |
+
const connection = self.navigator?.connection;
|
| 124 |
+
|
| 125 |
+
return {
|
| 126 |
+
type: connection?.effectiveType || '4g',
|
| 127 |
+
downlink: connection?.downlink || 10,
|
| 128 |
+
rtt: connection?.rtt || 50,
|
| 129 |
+
saveData: connection?.saveData || false,
|
| 130 |
+
wifi: true, // Would come from Android
|
| 131 |
+
mobile: false
|
| 132 |
+
};
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
async function getThermalStatus() {
|
| 136 |
+
if (self.Android?.HardwarePropertiesManager?.getDeviceTemperatures) {
|
| 137 |
+
const temps = self.Android.HardwarePropertiesManager.getDeviceTemperatures();
|
| 138 |
+
return {
|
| 139 |
+
cpu: temps[0],
|
| 140 |
+
battery: temps[1],
|
| 141 |
+
skin: temps[2],
|
| 142 |
+
status: getThermalStatusFromTemps(temps)
|
| 143 |
+
};
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
return { cpu: 42, battery: 34, skin: 32, status: 'MODERATE' };
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
function getThermalStatusFromTemps(temps) {
|
| 150 |
+
const max = Math.max(...temps);
|
| 151 |
+
if (max > 52) return 'CRITICAL';
|
| 152 |
+
if (max > 45) return 'SEVERE';
|
| 153 |
+
if (max > 38) return 'MODERATE';
|
| 154 |
+
return 'LIGHT';
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Start sync on load
|
| 158 |
+
startPeriodicSync();
|