jonin925 commited on
Commit
3a08226
·
verified ·
1 Parent(s): 6d0320a

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 CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Archdroid Parallel Workers
3
- emoji: 👀
4
  colorFrom: yellow
5
- colorTo: purple
 
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
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
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
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();