horeg-audio 1.0.0 → 1.0.2
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/LICENSE +21 -0
- package/README.md +183 -47
- package/dist/horeg-audio.cjs +125 -75
- package/dist/horeg-audio.cjs.map +1 -1
- package/dist/horeg-audio.global.js +124 -74
- package/dist/horeg-audio.global.js.map +1 -1
- package/dist/horeg-audio.js +617 -463
- package/dist/horeg-audio.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/package.json +14 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mario (ryomario)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -12,13 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
## ✨ Fitur Utama
|
|
14
14
|
|
|
15
|
-
- **Zero CSS Leakage**: Menggunakan native **Shadow DOM** (`mode: 'open'`) sehingga style pemutar musik 100% terisolasi dari stylesheet proyek induk (
|
|
16
|
-
- **Sound Horeg Aesthetics**: Desain box speaker subwoofer panggung, grill tekstur radial, baut sudut industrial, dan tombol play dengan animasi neon pulse.
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
15
|
+
- **Zero CSS Leakage**: Menggunakan native **Shadow DOM** (`mode: 'open'`) sehingga style pemutar musik 100% terisolasi dari stylesheet proyek induk tanpa perlu file CSS terpisah (aman dipadukan dengan Tailwind, Bootstrap, Bulma, atau CSS global apa pun).
|
|
16
|
+
- **Sound Horeg Aesthetics**: Desain box speaker subwoofer panggung, grill tekstur radial, baut sudut industrial, dan tombol play dengan animasi neon glow pulse.
|
|
17
|
+
- **Real-time Scrubbing & Cursor Grab**: Geser timeline lagu secara instan dan *real-time* tanpa jeda animasi (`transition: none !important`), dengan respon kursor `cursor: grab` saat diarahkan dan `cursor: grabbing` saat menggeser track.
|
|
18
|
+
- **6 Presets Tema (Dark & Light Mode)**: Pilihan tema gelap dan terang siap pakai yang otomatis menyesuaikan seluruh palet komponen internal (cabinet, surface, drawer, fader, dan soft elevation shadows).
|
|
19
|
+
- **Dynamic EQ / VU Visualizer**: Bar equalizer mini dinamis multi-band yang responsif bergerak saat audio diputar.
|
|
20
|
+
- **Multi-Track Playlist & Rack Drawer**: Panel drawer geser bertema *rack mount audio* dengan antrean trek, nomor urut, durasi, dan tombol hapus lagu.
|
|
21
|
+
- **Dukungan File Lokal & Audio URL**: Tambahkan trek audio komputer (.mp3, .wav, .flac, .ogg, .m4a) secara instan via file dialog maupun drag-and-drop langsung ke player, serta form tambah audio streaming URL online.
|
|
22
|
+
- **Zero Third-Party Dependencies**: Berbasis TypeScript / JavaScript murni tanpa ketergantungan library luar (*micro bundle* ~14 KB gzip).
|
|
23
|
+
- **Full Keyboard & ARIA a11y**: Navigasi ramah aksesibilitas keyboard (`Space`, panah kiri/kanan untuk seek, panah atas/bawah untuk volume).
|
|
24
|
+
- **Universal Distribution**: Siap pakai via NPM (ESM, CJS, TypeScript `.d.ts`), CDN browser tag `<script src="...">`, maupun native browser `<script type="module">`.
|
|
22
25
|
|
|
23
26
|
---
|
|
24
27
|
|
|
@@ -30,17 +33,25 @@ Gunakan **pnpm** (atau package manager pilihan Anda):
|
|
|
30
33
|
pnpm add horeg-audio
|
|
31
34
|
```
|
|
32
35
|
|
|
36
|
+
Atau menggunakan npm / yarn:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install horeg-audio
|
|
40
|
+
# atau
|
|
41
|
+
yarn add horeg-audio
|
|
42
|
+
```
|
|
43
|
+
|
|
33
44
|
---
|
|
34
45
|
|
|
35
46
|
## 🚀 Cara Penggunaan
|
|
36
47
|
|
|
37
|
-
### 1. Modern Frameworks & Bundlers (ESM / TypeScript)
|
|
48
|
+
### 1. Modern Frameworks & Bundlers (ESM / TypeScript / Vite / Next.js)
|
|
38
49
|
|
|
39
50
|
```typescript
|
|
40
51
|
import { HoregAudio } from 'horeg-audio';
|
|
41
52
|
|
|
42
53
|
const player = new HoregAudio({
|
|
43
|
-
container: '#music-player',
|
|
54
|
+
container: '#music-player', // Selektor string atau HTMLElement
|
|
44
55
|
playlist: [
|
|
45
56
|
{
|
|
46
57
|
id: 1,
|
|
@@ -59,55 +70,172 @@ const player = new HoregAudio({
|
|
|
59
70
|
}
|
|
60
71
|
],
|
|
61
72
|
theme: {
|
|
62
|
-
variant: 'horeg-classic'
|
|
63
|
-
primaryGlowColor: '#f59e0b'
|
|
73
|
+
variant: 'horeg-classic'
|
|
64
74
|
},
|
|
75
|
+
autoplay: false,
|
|
76
|
+
volume: 0.8,
|
|
77
|
+
loop: 'all',
|
|
65
78
|
onPlay: (track) => console.log('Playing:', track.title),
|
|
66
|
-
onPause: () => console.log('Paused')
|
|
79
|
+
onPause: () => console.log('Paused'),
|
|
80
|
+
onTrackChange: (track, index) => console.log(`Track #${index + 1}: ${track.title}`)
|
|
67
81
|
});
|
|
68
82
|
```
|
|
69
83
|
|
|
84
|
+
---
|
|
85
|
+
|
|
70
86
|
### 2. Browser Langsung via CDN (IIFE / Global Script)
|
|
71
87
|
|
|
88
|
+
Anda dapat langsung menggunakan `horeg-audio` pada file HTML biasa **tanpa bundler atau build step** (*zero-config*).
|
|
89
|
+
|
|
90
|
+
> [!NOTE]
|
|
91
|
+
> **Tidak membutuhkan link stylesheet/CSS terpisah!** Seluruh style visual, ikon SVG, dan animasi dirangkum mandiri di dalam Shadow DOM internal pemutar.
|
|
92
|
+
|
|
93
|
+
#### Opsi A: Tag `<script>` Tradisional (Global `window.HoregAudio`)
|
|
94
|
+
|
|
95
|
+
Saat file script dimuat, class `HoregAudio` otomatis didaftarkan ke objek `window`:
|
|
96
|
+
|
|
72
97
|
```html
|
|
73
|
-
|
|
74
|
-
<
|
|
98
|
+
<!DOCTYPE html>
|
|
99
|
+
<html lang="id">
|
|
100
|
+
<head>
|
|
101
|
+
<meta charset="UTF-8" />
|
|
102
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
103
|
+
<title>Horeg Audio Player</title>
|
|
104
|
+
<style>
|
|
105
|
+
body {
|
|
106
|
+
background-color: #0b0c10;
|
|
107
|
+
color: #f4f4f5;
|
|
108
|
+
font-family: sans-serif;
|
|
109
|
+
padding: 40px 16px;
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
align-items: center;
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
|
|
118
|
+
<!-- 1. Elemen mount player -->
|
|
119
|
+
<div id="player-mount" style="max-width: 520px; width: 100%;"></div>
|
|
120
|
+
|
|
121
|
+
<!-- 2. Muat Script Bundle IIFE (Pilih salah satu CDN atau file lokal) -->
|
|
122
|
+
<!-- Via jsDelivr CDN: -->
|
|
123
|
+
<script src="https://cdn.jsdelivr.net/npm/horeg-audio/dist/horeg-audio.global.js"></script>
|
|
124
|
+
<!-- Atau via unpkg CDN: -->
|
|
125
|
+
<!-- <script src="https://unpkg.com/horeg-audio/dist/horeg-audio.global.js"></script> -->
|
|
126
|
+
<!-- Atau dari folder lokal repository: -->
|
|
127
|
+
<!-- <script src="./dist/horeg-audio.global.js"></script> -->
|
|
128
|
+
|
|
129
|
+
<!-- 3. Inisialisasi Player -->
|
|
130
|
+
<script>
|
|
131
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
132
|
+
// Inisialisasi HoregAudio global
|
|
133
|
+
const player = new HoregAudio({
|
|
134
|
+
container: '#player-mount', // Selektor CSS string atau elemen DOM langsung
|
|
135
|
+
playlist: [
|
|
136
|
+
{
|
|
137
|
+
id: 1,
|
|
138
|
+
title: 'Bass Horeg Jedag-Jedug Extreme',
|
|
139
|
+
artist: 'DJ Riswanda Karnaval',
|
|
140
|
+
album: 'Festival Sound Jatim 2026',
|
|
141
|
+
src: 'https://cdn.example.com/audio/horeg-bass.mp3',
|
|
142
|
+
coverArt: 'https://cdn.example.com/images/cover.jpg',
|
|
143
|
+
duration: 215
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 2,
|
|
147
|
+
title: 'Karnaval Audio Rig Battle',
|
|
148
|
+
artist: 'Brewog Audio Master',
|
|
149
|
+
src: 'https://cdn.example.com/audio/karnaval.mp3'
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
theme: {
|
|
153
|
+
variant: 'horeg-classic' // 'horeg-classic' | 'horeg-nightclub' | 'horeg-stealth' | 'horeg-light' | 'horeg-light-clean' | 'horeg-light-minimal'
|
|
154
|
+
},
|
|
155
|
+
volume: 0.85,
|
|
156
|
+
autoplay: false,
|
|
157
|
+
loop: 'all',
|
|
158
|
+
onPlay: function (track) {
|
|
159
|
+
console.log('Sedang memutar:', track.title);
|
|
160
|
+
},
|
|
161
|
+
onTimeUpdate: function (currentTime, duration) {
|
|
162
|
+
// Detik realtime
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Kontrol pemutar secara dinamis via JavaScript:
|
|
167
|
+
// player.play();
|
|
168
|
+
// player.pause();
|
|
169
|
+
// player.setTheme({ variant: 'horeg-light' });
|
|
170
|
+
|
|
171
|
+
// Mengakses daftar preset tema bawaan dari properti statis:
|
|
172
|
+
console.log('Preset yang tersedia:', HoregAudio.THEME_PRESETS);
|
|
173
|
+
});
|
|
174
|
+
</script>
|
|
175
|
+
</body>
|
|
176
|
+
</html>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### Opsi B: Menggunakan Native Browser ES Module (`<script type="module">`)
|
|
180
|
+
|
|
181
|
+
Untuk browser modern, Anda juga dapat mengimpor langsung versi ESM via CDN tanpa proses bundling:
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<div id="player-mount"></div>
|
|
185
|
+
|
|
186
|
+
<script type="module">
|
|
187
|
+
import HoregAudio, { THEME_PRESETS } from 'https://cdn.jsdelivr.net/npm/horeg-audio/dist/horeg-audio.js';
|
|
75
188
|
|
|
76
|
-
<!-- Load Script -->
|
|
77
|
-
<script src="https://cdn.jsdelivr.net/npm/horeg-audio/dist/horeg-audio.global.js"></script>
|
|
78
|
-
<script>
|
|
79
189
|
const player = new HoregAudio({
|
|
80
|
-
container: '#player-
|
|
190
|
+
container: '#player-mount',
|
|
81
191
|
playlist: [
|
|
82
192
|
{
|
|
83
|
-
title: 'Karnaval
|
|
84
|
-
artist: '
|
|
193
|
+
title: 'Karnaval Sound Horeg',
|
|
194
|
+
artist: 'Audio Crew',
|
|
85
195
|
src: 'https://example.com/audio.mp3'
|
|
86
196
|
}
|
|
87
|
-
]
|
|
197
|
+
],
|
|
198
|
+
theme: {
|
|
199
|
+
variant: 'horeg-light'
|
|
200
|
+
}
|
|
88
201
|
});
|
|
89
202
|
</script>
|
|
90
203
|
```
|
|
91
204
|
|
|
92
205
|
---
|
|
93
206
|
|
|
94
|
-
## 🎨 Tema Preset (
|
|
207
|
+
## 🎨 Tema Preset (Dark & Light Edition)
|
|
95
208
|
|
|
96
|
-
|
|
97
|
-
|---|---|---|
|
|
98
|
-
| `horeg-classic` | Hitam matte khas box speaker + aksen amber neon karnaval | `#f59e0b` |
|
|
99
|
-
| `horeg-nightclub` | Cyberpunk night vibe dengan aksen neon cyan & strobe magenta | `#06b6d4` & `#ec4899` |
|
|
100
|
-
| `horeg-stealth` | Monokrom taktis metalik dan aksen perak | `#94a3b8` |
|
|
209
|
+
Tersedia 6 varian tema siap pakai yang secara otomatis mengonfigurasi seluruh aspek visual player:
|
|
101
210
|
|
|
102
|
-
|
|
211
|
+
| Preset Variant | Mode | Deskripsi | Warna Glow / Aksen | Background / Teks |
|
|
212
|
+
|---|---|---|---|---|
|
|
213
|
+
| `horeg-classic` | Dark | Hitam matte box subwoofer + aksen amber neon karnaval | `#f59e0b` & `#ef4444` | `#121214` / `#f4f4f5` |
|
|
214
|
+
| `horeg-nightclub` | Dark | Cyberpunk club vibe dengan aksen neon cyan & strobe pink | `#06b6d4` & `#ec4899` | `#0b0c10` / `#f8fafc` |
|
|
215
|
+
| `horeg-stealth` | Dark | Monokrom taktis metalik dan aksen perak industrial | `#94a3b8` & `#e2e8f0` | `#0f1115` / `#e2e8f0` |
|
|
216
|
+
| `horeg-light` | Light | Horeg Daylight Rig putih bersih dengan aksen amber & red punch | `#d97706` & `#dc2626` | `#ffffff` / `#0f172a` |
|
|
217
|
+
| `horeg-light-clean` | Light | Studio white modern dengan aksen neon sky blue & pink | `#0284c7` & `#db2777` | `#ffffff` / `#0f172a` |
|
|
218
|
+
| `horeg-light-minimal` | Light | Minimalist industrial slate dengan aksen royal cobalt | `#475569` & `#2563eb` | `#f8fafc` / `#1e293b` |
|
|
219
|
+
|
|
220
|
+
### Mengubah Tema Secara Dinamis (`player.setTheme`)
|
|
221
|
+
|
|
222
|
+
Saat `setTheme({ variant })` dipanggil, seluruh atribut warna preset baru langsung diterapkan secara utuh:
|
|
103
223
|
|
|
104
224
|
```typescript
|
|
225
|
+
// Beralih ke tema Light Mode
|
|
226
|
+
player.setTheme({
|
|
227
|
+
variant: 'horeg-light'
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Beralih ke tema Nightclub dengan kustomisasi warna aksen khusus
|
|
105
231
|
player.setTheme({
|
|
106
232
|
variant: 'horeg-nightclub',
|
|
107
|
-
primaryGlowColor: '#
|
|
108
|
-
accentColor: '#ec4899',
|
|
233
|
+
primaryGlowColor: '#00f2fe',
|
|
109
234
|
borderRadius: '16px'
|
|
110
235
|
});
|
|
236
|
+
|
|
237
|
+
// Melihat konfigurasi tema yang sedang aktif
|
|
238
|
+
console.log(player.getTheme());
|
|
111
239
|
```
|
|
112
240
|
|
|
113
241
|
---
|
|
@@ -118,14 +246,14 @@ player.setTheme({
|
|
|
118
246
|
|
|
119
247
|
```typescript
|
|
120
248
|
interface HoregPlayerOptions {
|
|
121
|
-
container: string | HTMLElement; // Target
|
|
122
|
-
playlist: Track[]; // Daftar lagu
|
|
249
|
+
container: string | HTMLElement; // Target CSS selektor atau elemen DOM
|
|
250
|
+
playlist: Track[]; // Daftar lagu awal
|
|
123
251
|
initialIndex?: number; // Indeks lagu awal (default: 0)
|
|
124
|
-
autoplay?: boolean; // Otomatis memutar
|
|
252
|
+
autoplay?: boolean; // Otomatis memutar saat dimuat (default: false)
|
|
125
253
|
loop?: 'none' | 'all' | 'one'; // Mode perulangan (default: 'all')
|
|
126
254
|
shuffle?: boolean; // Acak urutan lagu (default: false)
|
|
127
255
|
volume?: number; // Level volume 0.0 - 1.0 (default: 0.8)
|
|
128
|
-
theme?: HoregTheme; // Konfigurasi tema
|
|
256
|
+
theme?: HoregTheme; // Konfigurasi tema awal
|
|
129
257
|
onPlay?: (track: Track) => void;
|
|
130
258
|
onPause?: () => void;
|
|
131
259
|
onTrackChange?: (track: Track, index: number) => void;
|
|
@@ -136,25 +264,31 @@ interface HoregPlayerOptions {
|
|
|
136
264
|
}
|
|
137
265
|
```
|
|
138
266
|
|
|
139
|
-
### Metode Publik
|
|
267
|
+
### Metode Publik (`player.*`)
|
|
140
268
|
|
|
141
|
-
- `player.play(): Promise<void>` - Memutar lagu.
|
|
142
|
-
- `player.pause(): void` - Menjeda
|
|
143
|
-
- `player.toggle(): void` - Toggle play/pause.
|
|
269
|
+
- `player.play(): Promise<void>` - Memutar lagu aktif.
|
|
270
|
+
- `player.pause(): void` - Menjeda pemutaran.
|
|
271
|
+
- `player.toggle(): void` - Toggle play / pause.
|
|
144
272
|
- `player.next(): void` - Berpindah ke lagu berikutnya.
|
|
145
273
|
- `player.prev(): void` - Kembali ke awal lagu atau lagu sebelumnya.
|
|
146
|
-
- `player.seek(seconds: number): void` - Lompat ke detik tertentu.
|
|
274
|
+
- `player.seek(seconds: number): void` - Lompat ke detik pemutaran tertentu secara instan.
|
|
147
275
|
- `player.setVolume(level: number): void` - Mengatur volume (`0.0` sampai `1.0`).
|
|
148
|
-
- `player.loadTrack(indexOrTrack: number | Track): void` - Memuat trek tertentu.
|
|
149
|
-
- `player.addTrack(track: Track, autoPlay?: boolean): number` - Menambahkan lagu ke playlist.
|
|
276
|
+
- `player.loadTrack(indexOrTrack: number | Track, autoPlay?: boolean): void` - Memuat trek tertentu.
|
|
277
|
+
- `player.addTrack(track: Track, autoPlay?: boolean): number` - Menambahkan satu lagu ke playlist.
|
|
150
278
|
- `player.addTracks(tracks: Track[], autoPlay?: boolean): void` - Menambahkan kumpulan lagu ke playlist.
|
|
151
|
-
- `player.addTrackFromFile(file: File, autoPlay?: boolean): Promise<Track>` - Menambahkan lagu dari file lokal komputer (otomatis ekstrak nama dan durasi).
|
|
279
|
+
- `player.addTrackFromFile(file: File, autoPlay?: boolean): Promise<Track>` - Menambahkan lagu dari file lokal komputer (otomatis ekstrak nama file dan durasi).
|
|
152
280
|
- `player.addTrackFromFiles(files: FileList | File[], autoPlay?: boolean): Promise<Track[]>` - Menambahkan banyak file audio lokal sekaligus.
|
|
153
|
-
- `player.addTrackFromUrl(url: string, meta?: Partial<Track>, autoPlay?: boolean): Promise<Track>` - Menambahkan lagu dari
|
|
281
|
+
- `player.addTrackFromUrl(url: string, meta?: Partial<Track>, autoPlay?: boolean): Promise<Track>` - Menambahkan lagu dari tautan audio streaming URL.
|
|
154
282
|
- `player.removeTrack(index: number): void` - Menghapus lagu dari playlist berdasarkan indeks.
|
|
155
|
-
- `player.getPlaylist(): Track[]` - Mengambil daftar lagu saat ini.
|
|
156
|
-
- `player.setTheme(themeConfig: Partial<HoregTheme>): void` - Mengganti tema secara realtime.
|
|
157
|
-
- `player.
|
|
283
|
+
- `player.getPlaylist(): Track[]` - Mengambil daftar seluruh lagu di playlist saat ini.
|
|
284
|
+
- `player.setTheme(themeConfig: Partial<HoregTheme>): void` - Mengganti tema atau properti warna secara realtime.
|
|
285
|
+
- `player.getTheme(): HoregTheme` - Mengambil konfigurasi tema yang sedang diterapkan.
|
|
286
|
+
- `player.destroy(): void` - Membersihkan seluruh audio stream, event listener global, dan menghapus DOM Shadow Root.
|
|
287
|
+
|
|
288
|
+
### Properti & Fungsi Statis (`HoregAudio.*`)
|
|
289
|
+
|
|
290
|
+
- `HoregAudio.THEME_PRESETS` - Objek kamus preset tema bawaan (`Record<ThemeVariant, Partial<HoregTheme>>`).
|
|
291
|
+
- `HoregAudio.generateStyles(theme?)` - Fungsi generator string CSS Shadow DOM terenkapsulasi.
|
|
158
292
|
|
|
159
293
|
---
|
|
160
294
|
|
|
@@ -186,5 +320,7 @@ pnpm run build
|
|
|
186
320
|
|
|
187
321
|
## 📄 Lisensi
|
|
188
322
|
|
|
189
|
-
MIT
|
|
323
|
+
Didistribusikan di bawah Lisensi MIT. Lihat berkas [`LICENSE`](LICENSE) untuk informasi lebih lanjut.
|
|
324
|
+
|
|
325
|
+
Copyright © 2026 Mario ([@ryomario](https://github.com/ryomario)).
|
|
190
326
|
|