rutbhics 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/css/style.css +66 -0
- package/index.html +23 -1
- package/js/apps.js +172 -3
- package/js/desktop.js +129 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,6 +51,22 @@ Nginx, atau hosting statis apa pun) — tidak butuh backend.
|
|
|
51
51
|
tersimpan di `localStorage`.
|
|
52
52
|
- **Pengaturan** — ganti wallpaper (4 pilihan) dan tema terang/gelap,
|
|
53
53
|
tersimpan di `localStorage` dan diterapkan saat desktop dibuka lagi.
|
|
54
|
+
- **Musik** — player audio & video mini: putar/jeda, trek sebelum/
|
|
55
|
+
sesudah, geser posisi, volume, daftar putar. Trek demo dari URL publik
|
|
56
|
+
(butuh internet); trek tambahan (URL audio/video apa pun) bisa
|
|
57
|
+
ditambahkan dan tersimpan di `localStorage`.
|
|
58
|
+
|
|
59
|
+
## Notifikasi & pusat kontrol (taskbar)
|
|
60
|
+
|
|
61
|
+
- Tombol lonceng 🔔 di taskbar dengan badge jumlah notifikasi belum dibaca.
|
|
62
|
+
- Panel notifikasi: daftar notifikasi (waktu WIB), hapus semua, dan
|
|
63
|
+
otomatis tandai sudah dibaca saat panel dibuka. Notifikasi tersimpan di
|
|
64
|
+
`localStorage`, maksimal 30 terbaru.
|
|
65
|
+
- Pusat kontrol di panel yang sama: ganti tema terang/gelap (tersambung ke
|
|
66
|
+
Pengaturan lewat `localStorage`), mode Jangan Ganggu (mematikan toast
|
|
67
|
+
pop-up), tombol buka Pengaturan, dan tombol kirim notifikasi tes.
|
|
68
|
+
- Toast pop-up muncul tiap notifikasi baru (kecuali mode Jangan Ganggu
|
|
69
|
+
aktif), hilang sendiri setelah 4 detik atau saat diklik.
|
|
54
70
|
|
|
55
71
|
## Peta jalan (roadmap)
|
|
56
72
|
|
|
@@ -58,8 +74,8 @@ Urutan pembangunan berikutnya (satu item per jadwal build):
|
|
|
58
74
|
|
|
59
75
|
1. Aplikasi Files (penjelajah berkas virtual + localStorage) — selesai 26 Okt 2026
|
|
60
76
|
2. Aplikasi Pengaturan (ganti wallpaper, tema terang/gelap) — selesai 26 Okt 2026
|
|
61
|
-
3. Notifikasi & pusat kontrol di taskbar
|
|
62
|
-
4. Musik/video player mini
|
|
77
|
+
3. Notifikasi & pusat kontrol di taskbar — selesai 26 Okt 2026
|
|
78
|
+
4. Musik/video player mini — selesai 26 Okt 2026
|
|
63
79
|
5. Terminal web (perintah semu: help, about, echo, date)
|
|
64
80
|
6. Mode jendela maximize + snap kiri/kanan
|
|
65
81
|
7. Aplikasi Gambar (kanvas coret-coret + simpan)
|
|
@@ -72,11 +88,11 @@ Urutan pembangunan berikutnya (satu item per jadwal build):
|
|
|
72
88
|
## Struktur
|
|
73
89
|
|
|
74
90
|
```
|
|
75
|
-
index.html — shell: wallpaper, ikon desktop, taskbar, menu Start
|
|
91
|
+
index.html — shell: wallpaper, ikon desktop, taskbar, menu Start, panel notifikasi & pusat kontrol
|
|
76
92
|
login.html — halaman login (akun lokal perangkat) + kunci desktop
|
|
77
93
|
css/style.css — seluruh tampilan desktop & aplikasi
|
|
78
|
-
js/desktop.js — window manager (drag, minimize, focus, taskbar, jam WIB, terapkan pengaturan tersimpan)
|
|
79
|
-
js/apps.js — aplikasi bawaan (obrolan, peramban, catatan, kalkulator, files, pengaturan)
|
|
94
|
+
js/desktop.js — window manager (drag, minimize, focus, taskbar, jam WIB, terapkan pengaturan tersimpan, notifikasi & pusat kontrol)
|
|
95
|
+
js/apps.js — aplikasi bawaan (obrolan, peramban, catatan, kalkulator, files, pengaturan, musik)
|
|
80
96
|
package.json — metadata package npm + perintah `rutbhics`
|
|
81
97
|
bin/rutbhics.js — server statis lokal (tanpa dependensi) untuk perintah `rutbhics`
|
|
82
98
|
```
|
package/css/style.css
CHANGED
|
@@ -250,3 +250,69 @@ body.light-theme .calc-grid button { background: rgba(15, 23, 42, .06); }
|
|
|
250
250
|
body.light-theme .calc-grid button:hover { background: rgba(15, 23, 42, .12); }
|
|
251
251
|
body.light-theme .chat-contact.active { background: rgba(99, 102, 241, .15); }
|
|
252
252
|
body.light-theme #wallpaper::after { opacity: .06; }
|
|
253
|
+
|
|
254
|
+
/* Taskbar: tombol notifikasi */
|
|
255
|
+
#notif-btn {
|
|
256
|
+
position: relative; border: 0; border-radius: 10px; padding: 8px 12px;
|
|
257
|
+
background: transparent; color: var(--ink); font-size: 16px;
|
|
258
|
+
}
|
|
259
|
+
#notif-btn:hover { background: rgba(255, 255, 255, .1); }
|
|
260
|
+
#notif-badge {
|
|
261
|
+
position: absolute; top: 2px; right: 2px; min-width: 18px; height: 18px;
|
|
262
|
+
padding: 0 4px; border-radius: 999px; background: #ef4444; color: #fff;
|
|
263
|
+
font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center;
|
|
264
|
+
}
|
|
265
|
+
#notif-badge.hidden { display: none; }
|
|
266
|
+
|
|
267
|
+
/* Panel notifikasi & pusat kontrol */
|
|
268
|
+
#notif-panel {
|
|
269
|
+
position: fixed; z-index: 99; right: 10px; bottom: 60px; width: 340px;
|
|
270
|
+
max-height: 60vh; overflow-y: auto;
|
|
271
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
272
|
+
border-radius: var(--radius); padding: 14px; box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
|
|
273
|
+
}
|
|
274
|
+
#notif-panel.hidden { display: none; }
|
|
275
|
+
.notif-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
|
|
276
|
+
.notif-panel-head strong { font-size: 14px; }
|
|
277
|
+
.notif-item { padding: 8px 10px; border-radius: 10px; background: rgba(255, 255, 255, .05); margin-bottom: 6px; }
|
|
278
|
+
.notif-item.unread { background: rgba(99, 102, 241, .2); }
|
|
279
|
+
.notif-text { display: block; font-size: 13px; line-height: 1.4; }
|
|
280
|
+
.notif-time { font-size: 11px; color: var(--muted); }
|
|
281
|
+
.control-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
282
|
+
|
|
283
|
+
/* Toast notifikasi */
|
|
284
|
+
#toast-wrap {
|
|
285
|
+
position: fixed; z-index: 101; right: 10px; bottom: 60px;
|
|
286
|
+
display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
|
|
287
|
+
}
|
|
288
|
+
.toast {
|
|
289
|
+
max-width: 320px; padding: 10px 14px; border-radius: 12px; font-size: 13px;
|
|
290
|
+
background: var(--surface-solid); border: 1px solid var(--border);
|
|
291
|
+
box-shadow: 0 12px 30px rgba(0, 0, 0, .5); cursor: pointer;
|
|
292
|
+
}
|
|
293
|
+
body.light-theme #notif-btn:hover { background: rgba(15, 23, 42, .08); }
|
|
294
|
+
body.light-theme .notif-item { background: rgba(15, 23, 42, .05); }
|
|
295
|
+
body.light-theme .notif-item.unread { background: rgba(99, 102, 241, .15); }
|
|
296
|
+
|
|
297
|
+
/* Aplikasi: Musik */
|
|
298
|
+
.music-stage { margin-bottom: 12px; }
|
|
299
|
+
.music-title { font-weight: 600; font-size: 14px; margin-bottom: 8px; }
|
|
300
|
+
.music-media { width: 100%; border-radius: 10px; background: #000; }
|
|
301
|
+
.music-media.hidden { display: none; }
|
|
302
|
+
.music-error { color: #f87171; font-size: 13px; margin-top: 8px; }
|
|
303
|
+
.music-error.hidden { display: none; }
|
|
304
|
+
.music-controls { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
|
|
305
|
+
.music-time { font-size: 13px; color: var(--muted); }
|
|
306
|
+
.music-seek { width: 100%; margin-bottom: 10px; accent-color: var(--accent); }
|
|
307
|
+
.music-volrow { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
308
|
+
.music-vol { flex: 1; accent-color: var(--accent); }
|
|
309
|
+
.music-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 14px; }
|
|
310
|
+
.music-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; }
|
|
311
|
+
.music-row:hover { background: rgba(255, 255, 255, .07); }
|
|
312
|
+
.music-row.active { background: rgba(99, 102, 241, .25); }
|
|
313
|
+
.music-name { flex: 1; border: 0; background: transparent; color: var(--ink); text-align: left; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
314
|
+
.music-add { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
315
|
+
.music-add input { flex: 1; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--border); background: rgba(255, 255, 255, .06); outline: none; }
|
|
316
|
+
body.light-theme .music-row:hover { background: rgba(15, 23, 42, .08); }
|
|
317
|
+
body.light-theme .music-row.active { background: rgba(99, 102, 241, .15); }
|
|
318
|
+
body.light-theme .music-add input { background: rgba(15, 23, 42, .05); }
|
package/index.html
CHANGED
|
@@ -35,13 +35,35 @@
|
|
|
35
35
|
<footer id="taskbar">
|
|
36
36
|
<button id="start-btn" title="Mulai">⊞ <span>Mulai</span></button>
|
|
37
37
|
<div id="task-buttons"></div>
|
|
38
|
+
<button id="notif-btn" title="Notifikasi & pusat kontrol">🔔<span id="notif-badge" class="hidden">0</span></button>
|
|
38
39
|
<div id="tray">
|
|
39
40
|
<span id="clock-time">--:--</span>
|
|
40
41
|
<span id="clock-date">--</span>
|
|
41
42
|
</div>
|
|
42
43
|
</footer>
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
<!-- Panel notifikasi & pusat kontrol -->
|
|
46
|
+
<div id="notif-panel" class="hidden">
|
|
47
|
+
<div class="notif-panel-head">
|
|
48
|
+
<strong>Notifikasi</strong>
|
|
49
|
+
<button id="notif-clear" class="chip">Hapus semua</button>
|
|
50
|
+
</div>
|
|
51
|
+
<div id="notif-list"></div>
|
|
52
|
+
<div class="notif-panel-head" style="margin-top:14px">
|
|
53
|
+
<strong>Pusat kontrol</strong>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="control-row">
|
|
56
|
+
<button id="cc-theme" class="chip">☀️ Ganti ke terang</button>
|
|
57
|
+
<button id="cc-dnd" class="chip">🔔 Jangan ganggu: nonaktif</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="control-row">
|
|
60
|
+
<button id="cc-settings" class="btn-primary">Buka Pengaturan</button>
|
|
61
|
+
<button id="cc-test" class="chip">Kirim notifikasi tes</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div id="toast-wrap"></div>
|
|
65
|
+
|
|
45
66
|
<script src="js/apps.js"></script>
|
|
67
|
+
<script src="js/desktop.js"></script>
|
|
46
68
|
</body>
|
|
47
69
|
</html>
|
package/js/apps.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Web Desktop — aplikasi bawaan: Obrolan, Peramban, Catatan, Kalkulator, Files, Pengaturan.
|
|
2
|
-
// Data Obrolan, Catatan &
|
|
1
|
+
// Web Desktop — aplikasi bawaan: Obrolan, Peramban, Catatan, Kalkulator, Files, Pengaturan, Musik.
|
|
2
|
+
// Data Obrolan, Catatan, Files & Musik tersimpan di localStorage perangkat yang membuka web ini.
|
|
3
3
|
(function () {
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
@@ -353,12 +353,181 @@
|
|
|
353
353
|
body.appendChild(el('<p class="hint">Pengaturan tersimpan di perangkat ini dan langsung diterapkan saat desktop dibuka lagi.</p>'));
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
// ---------- Musik (player audio & video mini) ----------
|
|
357
|
+
// Trek demo diputar dari URL publik (butuh internet). Trek tambahan
|
|
358
|
+
// (URL audio/video apa pun) tersimpan di localStorage perangkat.
|
|
359
|
+
var DEMO_TRACKS = [
|
|
360
|
+
{ title: "SoundHelix Song 1 (demo)", url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", type: "audio" },
|
|
361
|
+
{ title: "SoundHelix Song 2 (demo)", url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3", type: "audio" },
|
|
362
|
+
{ title: "Flower — demo video (MDN)", url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4", type: "video" }
|
|
363
|
+
];
|
|
364
|
+
function guessType(url) {
|
|
365
|
+
var u = url.split("?")[0].toLowerCase();
|
|
366
|
+
if (/\.(mp4|webm|mov|mkv)$/.test(u)) return "video";
|
|
367
|
+
return "audio";
|
|
368
|
+
}
|
|
369
|
+
function fmtTime(s) {
|
|
370
|
+
if (!isFinite(s)) return "0:00";
|
|
371
|
+
var m = Math.floor(s / 60), sec = Math.floor(s % 60);
|
|
372
|
+
return m + ":" + (sec < 10 ? "0" : "") + sec;
|
|
373
|
+
}
|
|
374
|
+
function renderMusic(body) {
|
|
375
|
+
var custom = store("wd-music", []);
|
|
376
|
+
if (!Array.isArray(custom)) custom = [];
|
|
377
|
+
var tracks = DEMO_TRACKS.concat(custom);
|
|
378
|
+
var current = 0;
|
|
379
|
+
var media = null; // elemen <audio> atau <video> yang sedang aktif
|
|
380
|
+
|
|
381
|
+
var stage = el('<div class="music-stage"></div>');
|
|
382
|
+
var titleEl = el('<p class="music-title"></p>');
|
|
383
|
+
var audioEl = el('<audio class="music-media" preload="metadata"></audio>');
|
|
384
|
+
var videoEl = el('<video class="music-media hidden" preload="metadata"></video>');
|
|
385
|
+
stage.appendChild(titleEl);
|
|
386
|
+
stage.appendChild(audioEl);
|
|
387
|
+
stage.appendChild(videoEl);
|
|
388
|
+
var errEl = el('<p class="music-error hidden"></p>');
|
|
389
|
+
stage.appendChild(errEl);
|
|
390
|
+
|
|
391
|
+
var controls = el('<div class="music-controls"></div>');
|
|
392
|
+
var prevBtn = el('<button class="chip">⏮</button>');
|
|
393
|
+
var playBtn = el('<button class="btn-primary">▶ Putar</button>');
|
|
394
|
+
var nextBtn = el('<button class="chip">⏭</button>');
|
|
395
|
+
var timeEl = el('<span class="music-time">0:00 / 0:00</span>');
|
|
396
|
+
controls.appendChild(prevBtn); controls.appendChild(playBtn);
|
|
397
|
+
controls.appendChild(nextBtn); controls.appendChild(timeEl);
|
|
398
|
+
|
|
399
|
+
var seek = el('<input class="music-seek" type="range" min="0" max="100" value="0">');
|
|
400
|
+
var volRow = el('<div class="music-volrow"></div>');
|
|
401
|
+
var volLabel = el('<span class="hint" style="margin:0">Volume</span>');
|
|
402
|
+
var vol = el('<input class="music-vol" type="range" min="0" max="100" value="80">');
|
|
403
|
+
volRow.appendChild(volLabel); volRow.appendChild(vol);
|
|
404
|
+
|
|
405
|
+
var listEl = el('<div class="music-list"></div>');
|
|
406
|
+
|
|
407
|
+
var addRow = el('<div class="music-add"></div>');
|
|
408
|
+
var urlInput = el('<input type="text" placeholder="Tempel URL audio/video, mis. https://.../lagu.mp3">');
|
|
409
|
+
var addBtn = el('<button class="btn-primary">Tambah</button>');
|
|
410
|
+
addRow.appendChild(urlInput); addRow.appendChild(addBtn);
|
|
411
|
+
|
|
412
|
+
body.appendChild(stage); body.appendChild(controls);
|
|
413
|
+
body.appendChild(seek); body.appendChild(volRow);
|
|
414
|
+
body.appendChild(listEl); body.appendChild(addRow);
|
|
415
|
+
body.appendChild(el('<p class="hint">Trek demo butuh internet. Trek tambahan tersimpan di perangkat ini.</p>'));
|
|
416
|
+
|
|
417
|
+
function currentTrack() { return tracks[current]; }
|
|
418
|
+
function tryPlay() {
|
|
419
|
+
if (!media) return;
|
|
420
|
+
try {
|
|
421
|
+
var p = media.play();
|
|
422
|
+
if (p && p.then) {
|
|
423
|
+
p.then(function () { playBtn.textContent = "⏸ Jeda"; }).catch(function () {});
|
|
424
|
+
} else {
|
|
425
|
+
playBtn.textContent = "⏸ Jeda";
|
|
426
|
+
}
|
|
427
|
+
} catch (e) { /* abaikan: pengguna bisa menekan Putar */ }
|
|
428
|
+
}
|
|
429
|
+
function setMedia() {
|
|
430
|
+
audioEl.pause();
|
|
431
|
+
videoEl.pause();
|
|
432
|
+
var t = currentTrack();
|
|
433
|
+
var isVideo = t.type === "video";
|
|
434
|
+
audioEl.classList.toggle("hidden", isVideo);
|
|
435
|
+
videoEl.classList.toggle("hidden", !isVideo);
|
|
436
|
+
media = isVideo ? videoEl : audioEl;
|
|
437
|
+
media.src = t.url;
|
|
438
|
+
media.volume = vol.value / 100;
|
|
439
|
+
titleEl.textContent = (isVideo ? "🎬 " : "🎵 ") + t.title;
|
|
440
|
+
errEl.classList.add("hidden");
|
|
441
|
+
playBtn.textContent = "▶ Putar";
|
|
442
|
+
seek.value = 0;
|
|
443
|
+
timeEl.textContent = "0:00 / 0:00";
|
|
444
|
+
drawList();
|
|
445
|
+
}
|
|
446
|
+
function playTrack(i) {
|
|
447
|
+
current = (i + tracks.length) % tracks.length;
|
|
448
|
+
setMedia();
|
|
449
|
+
tryPlay();
|
|
450
|
+
}
|
|
451
|
+
function drawList() {
|
|
452
|
+
listEl.innerHTML = "";
|
|
453
|
+
tracks.forEach(function (t, i) {
|
|
454
|
+
var row = el('<div class="music-row' + (i === current ? " active" : "") + '"></div>');
|
|
455
|
+
var name = el('<button class="music-name"></button>');
|
|
456
|
+
name.textContent = (t.type === "video" ? "🎬 " : "🎵 ") + t.title;
|
|
457
|
+
name.addEventListener("click", function () { playTrack(i); });
|
|
458
|
+
row.appendChild(name);
|
|
459
|
+
if (i >= DEMO_TRACKS.length) {
|
|
460
|
+
var del = el('<button class="chip">Hapus</button>');
|
|
461
|
+
del.addEventListener("click", function () {
|
|
462
|
+
custom = custom.filter(function (c) { return c.url !== t.url; });
|
|
463
|
+
save("wd-music", custom);
|
|
464
|
+
tracks = DEMO_TRACKS.concat(custom);
|
|
465
|
+
if (current >= tracks.length) current = tracks.length - 1;
|
|
466
|
+
setMedia();
|
|
467
|
+
});
|
|
468
|
+
row.appendChild(del);
|
|
469
|
+
}
|
|
470
|
+
listEl.appendChild(row);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function bindMediaEvents(m) {
|
|
474
|
+
m.addEventListener("timeupdate", function () {
|
|
475
|
+
if (m.duration) seek.value = Math.floor(m.currentTime / m.duration * 100);
|
|
476
|
+
timeEl.textContent = fmtTime(m.currentTime) + " / " + fmtTime(m.duration);
|
|
477
|
+
});
|
|
478
|
+
m.addEventListener("loadedmetadata", function () {
|
|
479
|
+
timeEl.textContent = fmtTime(m.currentTime) + " / " + fmtTime(m.duration);
|
|
480
|
+
});
|
|
481
|
+
m.addEventListener("ended", function () { playTrack(current + 1); });
|
|
482
|
+
m.addEventListener("error", function () {
|
|
483
|
+
errEl.textContent = "Gagal memuat media ini. Periksa URL atau koneksi internet, lalu coba trek lain.";
|
|
484
|
+
errEl.classList.remove("hidden");
|
|
485
|
+
playBtn.textContent = "▶ Putar";
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
bindMediaEvents(audioEl);
|
|
489
|
+
bindMediaEvents(videoEl);
|
|
490
|
+
|
|
491
|
+
playBtn.addEventListener("click", function () {
|
|
492
|
+
if (!media) return;
|
|
493
|
+
if (media.paused) {
|
|
494
|
+
tryPlay();
|
|
495
|
+
} else {
|
|
496
|
+
media.pause();
|
|
497
|
+
playBtn.textContent = "▶ Putar";
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
prevBtn.addEventListener("click", function () { playTrack(current - 1); });
|
|
501
|
+
nextBtn.addEventListener("click", function () { playTrack(current + 1); });
|
|
502
|
+
seek.addEventListener("input", function () {
|
|
503
|
+
if (media && media.duration) media.currentTime = seek.value / 100 * media.duration;
|
|
504
|
+
});
|
|
505
|
+
vol.addEventListener("input", function () {
|
|
506
|
+
if (media) media.volume = vol.value / 100;
|
|
507
|
+
});
|
|
508
|
+
addBtn.addEventListener("click", function () {
|
|
509
|
+
var url = urlInput.value.trim();
|
|
510
|
+
if (!url) return;
|
|
511
|
+
var title = url.split("/").pop().split("?")[0] || url;
|
|
512
|
+
try { title = decodeURIComponent(title); } catch (e) {}
|
|
513
|
+
custom.push({ title: title, url: url, type: guessType(url) });
|
|
514
|
+
save("wd-music", custom);
|
|
515
|
+
tracks = DEMO_TRACKS.concat(custom);
|
|
516
|
+
urlInput.value = "";
|
|
517
|
+
drawList();
|
|
518
|
+
});
|
|
519
|
+
urlInput.addEventListener("keydown", function (e) { if (e.key === "Enter") addBtn.click(); });
|
|
520
|
+
|
|
521
|
+
setMedia();
|
|
522
|
+
}
|
|
523
|
+
|
|
356
524
|
window.APPS = {
|
|
357
525
|
chat: { emoji: "💬", title: "Obrolan", render: renderChat },
|
|
358
526
|
browser: { emoji: "🌐", title: "Peramban", render: renderBrowser },
|
|
359
527
|
notes: { emoji: "📝", title: "Catatan", render: renderNotes },
|
|
360
528
|
calc: { emoji: "🧮", title: "Kalkulator", render: renderCalc },
|
|
361
529
|
files: { emoji: "📁", title: "Files", render: renderFiles },
|
|
362
|
-
settings: { emoji: "⚙️", title: "Pengaturan", render: renderSettings }
|
|
530
|
+
settings: { emoji: "⚙️", title: "Pengaturan", render: renderSettings },
|
|
531
|
+
music: { emoji: "🎵", title: "Musik", render: renderMusic }
|
|
363
532
|
};
|
|
364
533
|
})();
|
package/js/desktop.js
CHANGED
|
@@ -181,5 +181,134 @@
|
|
|
181
181
|
}
|
|
182
182
|
} catch (e) {}
|
|
183
183
|
|
|
184
|
+
// ---------- Notifikasi & pusat kontrol (taskbar) ----------
|
|
185
|
+
// Notifikasi tersimpan di localStorage (wd-notifs); mode jangan-ganggu di wd-dnd.
|
|
186
|
+
function notifStore(fallback) {
|
|
187
|
+
try {
|
|
188
|
+
var raw = localStorage.getItem("wd-notifs");
|
|
189
|
+
return raw ? JSON.parse(raw) : fallback;
|
|
190
|
+
} catch (e) { return fallback; }
|
|
191
|
+
}
|
|
192
|
+
function notifSave(list) {
|
|
193
|
+
try { localStorage.setItem("wd-notifs", JSON.stringify(list)); } catch (e) { /* abaikan */ }
|
|
194
|
+
}
|
|
195
|
+
var notifs = notifStore([]);
|
|
196
|
+
function isDnd() {
|
|
197
|
+
try { return localStorage.getItem("wd-dnd") === "1"; } catch (e) { return false; }
|
|
198
|
+
}
|
|
199
|
+
function timeWIB() {
|
|
200
|
+
return new Intl.DateTimeFormat("id-ID", {
|
|
201
|
+
timeZone: "Asia/Jakarta", hour: "2-digit", minute: "2-digit", hour12: false
|
|
202
|
+
}).format(new Date());
|
|
203
|
+
}
|
|
204
|
+
var notifBtn = document.getElementById("notif-btn");
|
|
205
|
+
var notifBadge = document.getElementById("notif-badge");
|
|
206
|
+
var notifPanel = document.getElementById("notif-panel");
|
|
207
|
+
var notifListEl = document.getElementById("notif-list");
|
|
208
|
+
var toastWrap = document.getElementById("toast-wrap");
|
|
209
|
+
|
|
210
|
+
function unreadCount() {
|
|
211
|
+
return notifs.filter(function (n) { return !n.read; }).length;
|
|
212
|
+
}
|
|
213
|
+
function drawNotifs() {
|
|
214
|
+
var unread = unreadCount();
|
|
215
|
+
if (unread > 0) {
|
|
216
|
+
notifBadge.textContent = unread > 9 ? "9+" : String(unread);
|
|
217
|
+
notifBadge.classList.remove("hidden");
|
|
218
|
+
} else {
|
|
219
|
+
notifBadge.classList.add("hidden");
|
|
220
|
+
}
|
|
221
|
+
notifListEl.innerHTML = "";
|
|
222
|
+
if (!notifs.length) {
|
|
223
|
+
var empty = document.createElement("p");
|
|
224
|
+
empty.className = "hint";
|
|
225
|
+
empty.textContent = "Belum ada notifikasi.";
|
|
226
|
+
notifListEl.appendChild(empty);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
notifs.slice().reverse().forEach(function (n) {
|
|
230
|
+
var row = document.createElement("div");
|
|
231
|
+
row.className = "notif-item" + (n.read ? "" : " unread");
|
|
232
|
+
var t = document.createElement("span");
|
|
233
|
+
t.className = "notif-text";
|
|
234
|
+
t.textContent = n.text;
|
|
235
|
+
var time = document.createElement("span");
|
|
236
|
+
time.className = "notif-time";
|
|
237
|
+
time.textContent = n.time;
|
|
238
|
+
row.appendChild(t);
|
|
239
|
+
row.appendChild(time);
|
|
240
|
+
notifListEl.appendChild(row);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function showToast(text) {
|
|
244
|
+
var toast = document.createElement("div");
|
|
245
|
+
toast.className = "toast";
|
|
246
|
+
toast.textContent = text;
|
|
247
|
+
toast.addEventListener("click", function () { toast.remove(); });
|
|
248
|
+
toastWrap.appendChild(toast);
|
|
249
|
+
setTimeout(function () { toast.remove(); }, 4000);
|
|
250
|
+
}
|
|
251
|
+
function pushNotif(text, opts) {
|
|
252
|
+
opts = opts || {};
|
|
253
|
+
notifs.push({ text: text, time: timeWIB(), read: false });
|
|
254
|
+
if (notifs.length > 30) notifs = notifs.slice(-30);
|
|
255
|
+
notifSave(notifs);
|
|
256
|
+
drawNotifs();
|
|
257
|
+
if (!opts.silent && !isDnd()) showToast(text);
|
|
258
|
+
}
|
|
259
|
+
function markAllRead() {
|
|
260
|
+
notifs.forEach(function (n) { n.read = true; });
|
|
261
|
+
notifSave(notifs);
|
|
262
|
+
drawNotifs();
|
|
263
|
+
}
|
|
264
|
+
notifBtn.addEventListener("click", function () {
|
|
265
|
+
var willOpen = notifPanel.classList.contains("hidden");
|
|
266
|
+
notifPanel.classList.toggle("hidden");
|
|
267
|
+
startMenu.classList.add("hidden");
|
|
268
|
+
if (willOpen) markAllRead();
|
|
269
|
+
});
|
|
270
|
+
document.addEventListener("click", function (e) {
|
|
271
|
+
if (!notifPanel.classList.contains("hidden") &&
|
|
272
|
+
!e.target.closest("#notif-panel") && !e.target.closest("#notif-btn")) {
|
|
273
|
+
notifPanel.classList.add("hidden");
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
document.getElementById("notif-clear").addEventListener("click", function () {
|
|
277
|
+
notifs = [];
|
|
278
|
+
notifSave(notifs);
|
|
279
|
+
drawNotifs();
|
|
280
|
+
});
|
|
281
|
+
var ccTheme = document.getElementById("cc-theme");
|
|
282
|
+
function refreshThemeBtn() {
|
|
283
|
+
ccTheme.textContent = document.body.classList.contains("light-theme")
|
|
284
|
+
? "🌙 Ganti ke gelap" : "☀️ Ganti ke terang";
|
|
285
|
+
}
|
|
286
|
+
ccTheme.addEventListener("click", function () {
|
|
287
|
+
var light = document.body.classList.toggle("light-theme");
|
|
288
|
+
try { localStorage.setItem("wd-theme", light ? "light" : "dark"); } catch (e) {}
|
|
289
|
+
refreshThemeBtn();
|
|
290
|
+
});
|
|
291
|
+
var ccDnd = document.getElementById("cc-dnd");
|
|
292
|
+
function refreshDndBtn() {
|
|
293
|
+
ccDnd.textContent = isDnd() ? "🔕 Jangan ganggu: aktif" : "🔔 Jangan ganggu: nonaktif";
|
|
294
|
+
}
|
|
295
|
+
ccDnd.addEventListener("click", function () {
|
|
296
|
+
try { localStorage.setItem("wd-dnd", isDnd() ? "0" : "1"); } catch (e) {}
|
|
297
|
+
refreshDndBtn();
|
|
298
|
+
});
|
|
299
|
+
document.getElementById("cc-settings").addEventListener("click", function () {
|
|
300
|
+
notifPanel.classList.add("hidden");
|
|
301
|
+
openApp("settings");
|
|
302
|
+
});
|
|
303
|
+
document.getElementById("cc-test").addEventListener("click", function () {
|
|
304
|
+
pushNotif("Ini notifikasi tes dari pusat kontrol.");
|
|
305
|
+
});
|
|
306
|
+
refreshThemeBtn();
|
|
307
|
+
refreshDndBtn();
|
|
308
|
+
drawNotifs();
|
|
309
|
+
if (!notifs.length) {
|
|
310
|
+
pushNotif("Selamat datang di Rutbhics! Ketuk lonceng di taskbar untuk notifikasi & pusat kontrol.");
|
|
311
|
+
}
|
|
312
|
+
|
|
184
313
|
renderLaunchers("");
|
|
185
314
|
})();
|
package/package.json
CHANGED