capacitor-ota 1.0.0

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 ADDED
@@ -0,0 +1,290 @@
1
+ # OTA Service
2
+
3
+ Capacitor uygulamaları için Over-The-Air (OTA) güncelleme servisi.
4
+
5
+ ## Mimari
6
+
7
+ ```
8
+ ┌──────────────────────────────────────────────────────────────┐
9
+ │ ADMIN (Nadir) │
10
+ ├──────────────────────────────────────────────────────────────┤
11
+ │ CLI/Panel → POST /api/versions/upload (Workers) │
12
+ │ ↓ │
13
+ │ 1. Bundle → R2: ota.silgi.dev/app/production/1.0.0.zip │
14
+ │ 2. JSON → R2: ota.silgi.dev/updates/com.example.app.json │
15
+ └──────────────────────────────────────────────────────────────┘
16
+
17
+ ┌──────────────────────────────────────────────────────────────┐
18
+ │ CİHAZ (Sık) │
19
+ ├──────────────────────────────────────────────────────────────┤
20
+ │ App açılış → GET ota.silgi.dev/updates/com.example.app.json│
21
+ │ ↓ (CDN HIT - Workers YOK) │
22
+ │ Güncelleme var mı? │
23
+ │ ↓ evet │
24
+ │ GET ota.silgi.dev/bundle.zip (CDN HIT - Workers YOK) │
25
+ │ ↓ │
26
+ │ CapacitorUpdater.download() → set() │
27
+ └──────────────────────────────────────────────────────────────┘
28
+ ```
29
+
30
+ **Sonuç:** Cihazlar için 0 Workers request, sadece R2 + CDN.
31
+
32
+ ---
33
+
34
+ ## CLI Kullanımı
35
+
36
+ ### Kurulum
37
+
38
+ ```bash
39
+ cd ota-service
40
+ pnpm install
41
+ ```
42
+
43
+ ### Komutlar
44
+
45
+ #### Login
46
+ ```bash
47
+ pnpm cli login <username> <password>
48
+ ```
49
+ Token `~/.ota-cli.json` dosyasına kaydedilir.
50
+
51
+ #### Uygulamaları Listele
52
+ ```bash
53
+ pnpm cli apps
54
+ ```
55
+
56
+ Çıktı:
57
+ ```
58
+ 📱 Apps:
59
+
60
+ ID Name Created
61
+ ──────────────────────────────────────────────────
62
+ 9fa3a103d8d0 MyApp 1/22/2026
63
+ ```
64
+
65
+ #### Versiyonları Listele
66
+ ```bash
67
+ pnpm cli versions <app_id>
68
+ ```
69
+
70
+ Çıktı:
71
+ ```
72
+ 📦 Versions:
73
+
74
+ ID Version Channel Active Downloads Created
75
+ ─────────────────────────────────────────────────────────────────
76
+ 1 1.0.0 production ✅ 150 1/20/2026
77
+ 2 1.0.1 production 45 1/22/2026
78
+ ```
79
+
80
+ #### Yeni Versiyon Yükle
81
+ ```bash
82
+ pnpm cli upload <app_id> <path> [options]
83
+ ```
84
+
85
+ `<path>` bir dizin veya `.zip` dosyası olabilir. Dizin verilirse CLI otomatik zipleyecektir.
86
+
87
+ **Seçenekler:**
88
+ | Flag | Kısa | Açıklama | Varsayılan |
89
+ |------|------|----------|------------|
90
+ | `--version` | `-v` | Versiyon numarası | 1.0.0 |
91
+ | `--channel` | `-c` | Kanal | production |
92
+ | `--capacitor-app-id` | | Capacitor app ID (static JSON için) | - |
93
+ | `--min-native` | | Minimum native versiyon | - |
94
+ | `--notes` | | Release notes | - |
95
+
96
+ **Örnekler:**
97
+ ```bash
98
+ # Dizin ile (otomatik ziplenir)
99
+ pnpm cli upload 9fa3a103d8d0 ./dist \
100
+ -v 1.0.5 \
101
+ --capacitor-app-id com.example.app
102
+
103
+ # Zip dosyası ile
104
+ pnpm cli upload 9fa3a103d8d0 ./bundle.zip -v 1.0.5
105
+ ```
106
+
107
+ Çıktı:
108
+ ```
109
+ 📦 Zipping directory: dist...
110
+ 📤 Uploading...
111
+
112
+ ✅ Upload successful!
113
+ Version: 1.0.5
114
+ Checksum: a1b2c3d4...
115
+ ```
116
+
117
+ #### Versiyon Sil
118
+ ```bash
119
+ pnpm cli delete <version_id>
120
+ ```
121
+
122
+ #### Rollback
123
+ ```bash
124
+ pnpm cli rollback <version_id>
125
+ ```
126
+ Belirtilen versiyonu aktif yapar.
127
+
128
+ #### Konfigürasyon
129
+ ```bash
130
+ # Mevcut ayarları göster
131
+ pnpm cli config
132
+
133
+ # API URL değiştir
134
+ pnpm cli set-url https://my-ota.workers.dev
135
+ ```
136
+
137
+ ---
138
+
139
+ ## API Endpoints
140
+
141
+ ### Public (Auth Gerektirmez)
142
+
143
+ | Method | Endpoint | Açıklama |
144
+ |--------|----------|----------|
145
+ | GET | `/api/updates` | Update check (legacy) |
146
+
147
+ ### Protected (Auth Gerektirir)
148
+
149
+ | Method | Endpoint | Açıklama |
150
+ |--------|----------|----------|
151
+ | POST | `/api/auth/login` | Login |
152
+ | GET | `/api/apps` | App listesi |
153
+ | POST | `/api/apps` | App oluştur |
154
+ | DELETE | `/api/apps/:id` | App sil |
155
+ | GET | `/api/versions` | Versiyon listesi |
156
+ | POST | `/api/versions/upload` | Versiyon yükle |
157
+ | DELETE | `/api/versions/:id` | Versiyon sil |
158
+ | POST | `/api/versions/:id/activate` | Versiyonu aktif yap |
159
+ | POST | `/api/versions/:id/rollback` | Rollback |
160
+
161
+ ---
162
+
163
+ ## Capacitor Entegrasyonu
164
+
165
+ ### 1. Plugin Kur
166
+ ```bash
167
+ npm install @capgo/capacitor-updater
168
+ npx cap sync
169
+ ```
170
+
171
+ ### 2. Capacitor Config
172
+ ```typescript
173
+ // capacitor.config.ts
174
+ const config: CapacitorConfig = {
175
+ plugins: {
176
+ CapacitorUpdater: {
177
+ autoUpdate: false, // Manuel kontrol
178
+ },
179
+ },
180
+ }
181
+ ```
182
+
183
+ ### 3. Vue Composable
184
+ ```typescript
185
+ // src/composables/useOtaUpdate.ts
186
+ import { CapacitorUpdater } from '@capgo/capacitor-updater'
187
+
188
+ const CONFIG = {
189
+ staticUrl: 'https://ota.silgi.dev',
190
+ appId: 'com.example.app',
191
+ }
192
+
193
+ export function useOtaUpdate() {
194
+ async function checkForUpdate() {
195
+ const url = `${CONFIG.staticUrl}/updates/${CONFIG.appId}.json`
196
+ const response = await fetch(url)
197
+ const data = await response.json()
198
+ // ... version karşılaştırma ve indirme
199
+ }
200
+
201
+ async function downloadUpdate(url: string, version: string, checksum: string) {
202
+ const bundle = await CapacitorUpdater.download({ url, version, checksum })
203
+ return bundle
204
+ }
205
+
206
+ async function applyUpdate(bundleId: string) {
207
+ await CapacitorUpdater.set({ id: bundleId })
208
+ // Otomatik reload
209
+ }
210
+
211
+ return { checkForUpdate, downloadUpdate, applyUpdate }
212
+ }
213
+ ```
214
+
215
+ ### 4. App.vue
216
+ ```vue
217
+ <script setup>
218
+ import { onMounted } from 'vue'
219
+ import { useOtaUpdate } from '@/composables/useOtaUpdate'
220
+
221
+ const ota = useOtaUpdate()
222
+
223
+ onMounted(async () => {
224
+ await ota.notifyAppReady() // Rollback'i önle
225
+ await ota.checkForUpdate()
226
+ })
227
+ </script>
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Static JSON Format
233
+
234
+ R2'de saklanan güncelleme bilgisi:
235
+
236
+ ```
237
+ GET https://ota.silgi.dev/updates/com.example.app.json
238
+ ```
239
+
240
+ ```json
241
+ {
242
+ "version": "1.0.5",
243
+ "url": "https://ota.silgi.dev/9fa3a103d8d0/production/1.0.5.zip",
244
+ "checksum": "a1b2c3d4e5f6...",
245
+ "min_native_version": "1.0.0",
246
+ "release_notes": "Bug fixes",
247
+ "updated_at": "2026-01-22T10:30:00.000Z"
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Deployment
254
+
255
+ ```bash
256
+ # Local development
257
+ pnpm dev
258
+
259
+ # Deploy to Cloudflare Workers
260
+ pnpm build
261
+ npx wrangler deploy
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Environment Variables
267
+
268
+ ### Workers (wrangler.toml)
269
+ ```toml
270
+ [vars]
271
+ ADMIN_PASSWORD_HASH = "bcrypt_hash_here"
272
+ R2_PUBLIC_URL = "https://ota.silgi.dev"
273
+ ```
274
+
275
+ ### Client (.env)
276
+ ```env
277
+ VITE_OTA_STATIC_URL=https://ota.silgi.dev
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Maliyet Avantajı
283
+
284
+ | Senaryo | Workers Request | R2 Request |
285
+ |---------|-----------------|------------|
286
+ | 1000 kullanıcı, 10 açılış/gün | 0 | 10,000 |
287
+ | Admin upload (ayda ~10) | 10 | 10 |
288
+
289
+ **Workers:** Neredeyse ücretsiz (sadece admin)
290
+ **R2:** İlk 10M request/ay ücretsiz
@@ -0,0 +1,86 @@
1
+ //#region src/types.d.ts
2
+ interface OtaConfig {
3
+ staticUrl: string;
4
+ appId: string;
5
+ appVersion: string;
6
+ theme?: OtaTheme;
7
+ texts?: OtaTexts;
8
+ autoApply?: boolean;
9
+ showUI?: boolean;
10
+ }
11
+ interface OtaTheme {
12
+ primaryColor?: string;
13
+ gradientFrom?: string;
14
+ gradientTo?: string;
15
+ textColor?: string;
16
+ }
17
+ interface OtaTexts {
18
+ title?: string;
19
+ downloading?: string;
20
+ downloadComplete?: string;
21
+ applyButton?: string;
22
+ applying?: string;
23
+ errorTitle?: string;
24
+ retryButton?: string;
25
+ }
26
+ interface RemoteVersion {
27
+ version: string;
28
+ url: string;
29
+ checksum: string;
30
+ min_native_version?: string;
31
+ release_notes?: string | null;
32
+ }
33
+ type OtaStatus = 'idle' | 'checking' | 'downloading' | 'ready' | 'applying' | 'error';
34
+ interface OtaState {
35
+ status: OtaStatus;
36
+ progress: number;
37
+ currentVersion: string;
38
+ newVersion: string | null;
39
+ error: string | null;
40
+ }
41
+ interface DownloadedBundle {
42
+ id: string;
43
+ version: string;
44
+ }
45
+ //#endregion
46
+ //#region src/OtaUpdater.d.ts
47
+ type EventCallback = (data?: any) => void;
48
+ declare class OtaUpdater {
49
+ private config;
50
+ private listeners;
51
+ private downloadedBundle;
52
+ state: OtaState;
53
+ constructor(config: OtaConfig);
54
+ on(event: string, callback: EventCallback): void;
55
+ off(event: string, callback: EventCallback): void;
56
+ private emit;
57
+ private setState;
58
+ notifyAppReady(): Promise<void>;
59
+ check(): Promise<boolean>;
60
+ download(): Promise<boolean>;
61
+ apply(): Promise<void>;
62
+ }
63
+ declare function getOtaUpdater(): OtaUpdater | null;
64
+ //#endregion
65
+ //#region src/OtaUpdateElement.d.ts
66
+ declare class OtaUpdateElement extends HTMLElement {
67
+ private shadow;
68
+ private texts;
69
+ private theme?;
70
+ private visible;
71
+ constructor();
72
+ setTexts(texts?: OtaTexts): void;
73
+ setTheme(theme?: OtaTheme): void;
74
+ show(): void;
75
+ hide(): void;
76
+ connectedCallback(): void;
77
+ private getProgressOffset;
78
+ private render;
79
+ private renderContent;
80
+ private attachEventListeners;
81
+ }
82
+ //#endregion
83
+ //#region src/index.d.ts
84
+ declare function initOtaUpdate(config: OtaConfig): Promise<OtaUpdater>;
85
+ //#endregion
86
+ export { DownloadedBundle, OtaConfig, OtaState, OtaStatus, OtaTexts, OtaTheme, OtaUpdateElement, OtaUpdater, RemoteVersion, getOtaUpdater, initOtaUpdate };
package/dist/index.mjs ADDED
@@ -0,0 +1,582 @@
1
+ //#region src/utils.ts
2
+ function compareVersions(a, b) {
3
+ const pa = a.split(".").map(Number);
4
+ const pb = b.split(".").map(Number);
5
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
6
+ const na = pa[i] || 0;
7
+ const nb = pb[i] || 0;
8
+ if (na > nb) return 1;
9
+ if (na < nb) return -1;
10
+ }
11
+ return 0;
12
+ }
13
+ function isNativePlatform() {
14
+ try {
15
+ return typeof Capacitor !== "undefined" && Capacitor.isNativePlatform();
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ //#endregion
22
+ //#region src/OtaUpdater.ts
23
+ var OtaUpdater = class {
24
+ config;
25
+ listeners = /* @__PURE__ */ new Map();
26
+ downloadedBundle = null;
27
+ state = {
28
+ status: "idle",
29
+ progress: 0,
30
+ currentVersion: "",
31
+ newVersion: null,
32
+ error: null
33
+ };
34
+ constructor(config) {
35
+ this.config = config;
36
+ this.state.currentVersion = config.appVersion;
37
+ }
38
+ on(event, callback) {
39
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
40
+ this.listeners.get(event).add(callback);
41
+ }
42
+ off(event, callback) {
43
+ this.listeners.get(event)?.delete(callback);
44
+ }
45
+ emit(event, data) {
46
+ this.listeners.get(event)?.forEach((cb) => cb(data));
47
+ }
48
+ setState(partial) {
49
+ Object.assign(this.state, partial);
50
+ this.emit("stateChange", { ...this.state });
51
+ if (partial.progress !== void 0) this.emit("progress", partial.progress);
52
+ if (partial.error) this.emit("error", partial.error);
53
+ }
54
+ async notifyAppReady() {
55
+ if (!isNativePlatform()) return;
56
+ try {
57
+ const { CapacitorUpdater } = await import("@capgo/capacitor-updater");
58
+ await CapacitorUpdater.notifyAppReady();
59
+ } catch {}
60
+ }
61
+ async check() {
62
+ this.setState({
63
+ status: "checking",
64
+ error: null
65
+ });
66
+ try {
67
+ const url = `${this.config.staticUrl}/updates/${this.config.appId}.json?_=${Date.now()}`;
68
+ const response = await fetch(url);
69
+ if (!response.ok) {
70
+ if (response.status === 404) {
71
+ this.setState({ status: "idle" });
72
+ return false;
73
+ }
74
+ throw new Error(`HTTP ${response.status}`);
75
+ }
76
+ const data = await response.json();
77
+ if (compareVersions(data.version, this.config.appVersion) > 0) {
78
+ this.setState({ newVersion: data.version });
79
+ return true;
80
+ }
81
+ this.setState({ status: "idle" });
82
+ return false;
83
+ } catch (e) {
84
+ const error = e instanceof Error ? e.message : "Update check failed";
85
+ this.setState({
86
+ status: "error",
87
+ error
88
+ });
89
+ return false;
90
+ }
91
+ }
92
+ async download() {
93
+ if (!this.state.newVersion) {
94
+ this.setState({
95
+ status: "error",
96
+ error: "No update info available"
97
+ });
98
+ return false;
99
+ }
100
+ this.setState({
101
+ status: "downloading",
102
+ progress: 0,
103
+ error: null
104
+ });
105
+ if (!isNativePlatform()) {
106
+ for (let i = 0; i <= 100; i += 5) {
107
+ this.setState({ progress: i });
108
+ await new Promise((r) => setTimeout(r, 50));
109
+ }
110
+ this.downloadedBundle = {
111
+ id: "web-sim",
112
+ version: this.state.newVersion
113
+ };
114
+ this.setState({ status: "ready" });
115
+ this.emit("updateReady");
116
+ return true;
117
+ }
118
+ try {
119
+ const url = `${this.config.staticUrl}/updates/${this.config.appId}.json`;
120
+ const data = await (await fetch(url)).json();
121
+ const { CapacitorUpdater } = await import("@capgo/capacitor-updater");
122
+ const listener = await CapacitorUpdater.addListener("download", (event) => {
123
+ this.setState({ progress: event.percent });
124
+ });
125
+ const bundle = await CapacitorUpdater.download({
126
+ url: data.url,
127
+ version: data.version,
128
+ checksum: data.checksum
129
+ });
130
+ listener.remove();
131
+ this.downloadedBundle = {
132
+ id: bundle.id,
133
+ version: bundle.version
134
+ };
135
+ this.setState({ status: "ready" });
136
+ this.emit("updateReady");
137
+ return true;
138
+ } catch (e) {
139
+ const error = e instanceof Error ? e.message : "Download failed";
140
+ this.setState({
141
+ status: "error",
142
+ error
143
+ });
144
+ return false;
145
+ }
146
+ }
147
+ async apply() {
148
+ this.setState({ status: "applying" });
149
+ if (!isNativePlatform()) {
150
+ await new Promise((r) => setTimeout(r, 500));
151
+ this.emit("updateApplied");
152
+ window.location.reload();
153
+ return;
154
+ }
155
+ if (!this.downloadedBundle) {
156
+ this.setState({
157
+ status: "error",
158
+ error: "No downloaded bundle"
159
+ });
160
+ return;
161
+ }
162
+ try {
163
+ const { CapacitorUpdater } = await import("@capgo/capacitor-updater");
164
+ await CapacitorUpdater.set({ id: this.downloadedBundle.id });
165
+ this.emit("updateApplied");
166
+ } catch (e) {
167
+ const error = e instanceof Error ? e.message : "Apply failed";
168
+ this.setState({
169
+ status: "error",
170
+ error
171
+ });
172
+ }
173
+ }
174
+ };
175
+ let instance = null;
176
+ function getOtaUpdater() {
177
+ return instance;
178
+ }
179
+ function createOtaUpdater(config) {
180
+ instance = new OtaUpdater(config);
181
+ return instance;
182
+ }
183
+
184
+ //#endregion
185
+ //#region src/styles.ts
186
+ function getStyles(theme) {
187
+ return `
188
+ :host {
189
+ --ota-primary: ${theme?.primaryColor || "#10b981"};
190
+ --ota-gradient-from: ${theme?.gradientFrom || "#10b981"};
191
+ --ota-gradient-to: ${theme?.gradientTo || "#059669"};
192
+ --ota-text: ${theme?.textColor || "#ffffff"};
193
+ --ota-text-muted: rgba(255, 255, 255, 0.7);
194
+ --ota-overlay-bg: linear-gradient(to bottom, var(--ota-gradient-from), var(--ota-gradient-to));
195
+ }
196
+
197
+ * {
198
+ margin: 0;
199
+ padding: 0;
200
+ box-sizing: border-box;
201
+ }
202
+
203
+ .overlay {
204
+ position: fixed;
205
+ inset: 0;
206
+ z-index: 9999;
207
+ background: var(--ota-overlay-bg);
208
+ display: flex;
209
+ flex-direction: column;
210
+ align-items: center;
211
+ justify-content: center;
212
+ padding: 2rem;
213
+ color: var(--ota-text);
214
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
215
+ animation: fadeIn 0.3s ease;
216
+ }
217
+
218
+ .overlay.hidden {
219
+ display: none;
220
+ }
221
+
222
+ @keyframes fadeIn {
223
+ from { opacity: 0; }
224
+ to { opacity: 1; }
225
+ }
226
+
227
+ .safe-area-top {
228
+ padding-top: env(safe-area-inset-top);
229
+ }
230
+
231
+ .safe-area-bottom {
232
+ padding-bottom: env(safe-area-inset-bottom);
233
+ }
234
+
235
+ .container {
236
+ display: flex;
237
+ flex-direction: column;
238
+ align-items: center;
239
+ text-align: center;
240
+ width: 100%;
241
+ max-width: 320px;
242
+ }
243
+
244
+ .icon {
245
+ width: 96px;
246
+ height: 96px;
247
+ background: white;
248
+ border-radius: 22px;
249
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ margin-bottom: 2rem;
254
+ }
255
+
256
+ .icon svg {
257
+ width: 56px;
258
+ height: 56px;
259
+ color: var(--ota-primary);
260
+ }
261
+
262
+ h1 {
263
+ font-size: 1.5rem;
264
+ font-weight: 700;
265
+ margin-bottom: 0.5rem;
266
+ }
267
+
268
+ .version {
269
+ color: var(--ota-text-muted);
270
+ margin-bottom: 2rem;
271
+ }
272
+
273
+ .content {
274
+ width: 100%;
275
+ }
276
+
277
+ /* Progress Circle */
278
+ .progress-container {
279
+ position: relative;
280
+ width: 128px;
281
+ height: 128px;
282
+ margin: 0 auto 1.5rem;
283
+ }
284
+
285
+ .progress-svg {
286
+ width: 100%;
287
+ height: 100%;
288
+ transform: rotate(-90deg);
289
+ }
290
+
291
+ .progress-bg {
292
+ fill: none;
293
+ stroke: rgba(255, 255, 255, 0.2);
294
+ stroke-width: 8;
295
+ }
296
+
297
+ .progress-bar {
298
+ fill: none;
299
+ stroke: white;
300
+ stroke-width: 8;
301
+ stroke-linecap: round;
302
+ transition: stroke-dashoffset 0.3s ease;
303
+ }
304
+
305
+ .progress-text {
306
+ position: absolute;
307
+ inset: 0;
308
+ display: flex;
309
+ align-items: center;
310
+ justify-content: center;
311
+ font-size: 1.875rem;
312
+ font-weight: 700;
313
+ }
314
+
315
+ .status-text {
316
+ color: var(--ota-text-muted);
317
+ }
318
+
319
+ /* Ready State */
320
+ .check-icon {
321
+ width: 64px;
322
+ height: 64px;
323
+ background: rgba(255, 255, 255, 0.2);
324
+ border-radius: 50%;
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ margin: 0 auto 1.5rem;
329
+ }
330
+
331
+ .check-icon svg {
332
+ width: 32px;
333
+ height: 32px;
334
+ }
335
+
336
+ .apply-btn {
337
+ width: 100%;
338
+ padding: 1rem;
339
+ background: white;
340
+ color: var(--ota-primary);
341
+ font-size: 1rem;
342
+ font-weight: 600;
343
+ border: none;
344
+ border-radius: 1rem;
345
+ cursor: pointer;
346
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
347
+ transition: transform 0.15s ease;
348
+ }
349
+
350
+ .apply-btn:active {
351
+ transform: scale(0.95);
352
+ }
353
+
354
+ /* Applying State */
355
+ .spinner {
356
+ width: 32px;
357
+ height: 32px;
358
+ border: 3px solid rgba(255, 255, 255, 0.3);
359
+ border-top-color: white;
360
+ border-radius: 50%;
361
+ margin: 0 auto 1rem;
362
+ animation: spin 0.8s linear infinite;
363
+ }
364
+
365
+ @keyframes spin {
366
+ to { transform: rotate(360deg); }
367
+ }
368
+
369
+ /* Error State */
370
+ .error-icon {
371
+ width: 64px;
372
+ height: 64px;
373
+ background: rgba(239, 68, 68, 0.2);
374
+ border-radius: 50%;
375
+ display: flex;
376
+ align-items: center;
377
+ justify-content: center;
378
+ margin: 0 auto 1.5rem;
379
+ }
380
+
381
+ .error-icon svg {
382
+ width: 32px;
383
+ height: 32px;
384
+ }
385
+
386
+ .error-title {
387
+ color: #fecaca;
388
+ margin-bottom: 0.5rem;
389
+ }
390
+
391
+ .error-message {
392
+ color: var(--ota-text-muted);
393
+ font-size: 0.875rem;
394
+ margin-bottom: 1.5rem;
395
+ }
396
+
397
+ .retry-btn {
398
+ width: 100%;
399
+ padding: 1rem;
400
+ background: rgba(255, 255, 255, 0.2);
401
+ color: white;
402
+ font-size: 1rem;
403
+ font-weight: 600;
404
+ border: none;
405
+ border-radius: 1rem;
406
+ cursor: pointer;
407
+ transition: transform 0.15s ease;
408
+ }
409
+
410
+ .retry-btn:active {
411
+ transform: scale(0.95);
412
+ }
413
+ `;
414
+ }
415
+
416
+ //#endregion
417
+ //#region src/OtaUpdateElement.ts
418
+ const DEFAULT_TEXTS = {
419
+ title: "Güncelleme Mevcut",
420
+ downloading: "İndiriliyor...",
421
+ downloadComplete: "İndirme tamamlandı!",
422
+ applyButton: "Şimdi Güncelle",
423
+ applying: "Güncelleme uygulanıyor...",
424
+ errorTitle: "Güncelleme başarısız",
425
+ retryButton: "Tekrar Dene"
426
+ };
427
+ var OtaUpdateElement = class extends HTMLElement {
428
+ shadow;
429
+ texts;
430
+ theme;
431
+ visible = false;
432
+ constructor() {
433
+ super();
434
+ this.shadow = this.attachShadow({ mode: "open" });
435
+ this.texts = { ...DEFAULT_TEXTS };
436
+ }
437
+ setTexts(texts) {
438
+ this.texts = {
439
+ ...DEFAULT_TEXTS,
440
+ ...texts
441
+ };
442
+ this.render();
443
+ }
444
+ setTheme(theme) {
445
+ this.theme = theme;
446
+ this.render();
447
+ }
448
+ show() {
449
+ this.visible = true;
450
+ this.render();
451
+ }
452
+ hide() {
453
+ this.visible = false;
454
+ this.render();
455
+ }
456
+ connectedCallback() {
457
+ const ota = getOtaUpdater();
458
+ if (ota) ota.on("stateChange", () => this.render());
459
+ this.render();
460
+ }
461
+ getProgressOffset(progress) {
462
+ const circumference = 2 * Math.PI * 45;
463
+ return circumference - circumference * progress / 100;
464
+ }
465
+ render() {
466
+ const state = getOtaUpdater()?.state || {
467
+ status: "idle",
468
+ progress: 0,
469
+ currentVersion: "",
470
+ newVersion: null,
471
+ error: null
472
+ };
473
+ const circumference = 2 * Math.PI * 45;
474
+ this.shadow.innerHTML = `
475
+ <style>${getStyles(this.theme)}</style>
476
+ <div class="overlay ${this.visible ? "" : "hidden"}">
477
+ <div class="safe-area-top"></div>
478
+ <div class="container">
479
+ <!-- App Icon -->
480
+ <div class="icon">
481
+ <svg viewBox="0 0 24 24" fill="currentColor">
482
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
483
+ </svg>
484
+ </div>
485
+
486
+ <!-- Title -->
487
+ <h1>${this.texts.title}</h1>
488
+
489
+ <!-- Version -->
490
+ <p class="version">v${state.currentVersion} → v${state.newVersion || "..."}</p>
491
+
492
+ <!-- Content -->
493
+ <div class="content">
494
+ ${this.renderContent(state, circumference)}
495
+ </div>
496
+ </div>
497
+ <div class="safe-area-bottom"></div>
498
+ </div>
499
+ `;
500
+ this.attachEventListeners();
501
+ }
502
+ renderContent(state, circumference) {
503
+ switch (state.status) {
504
+ case "downloading": return `
505
+ <div class="progress-container">
506
+ <svg class="progress-svg" viewBox="0 0 100 100">
507
+ <circle class="progress-bg" cx="50" cy="50" r="45" />
508
+ <circle
509
+ class="progress-bar"
510
+ cx="50" cy="50" r="45"
511
+ stroke-dasharray="${circumference}"
512
+ stroke-dashoffset="${this.getProgressOffset(state.progress)}"
513
+ />
514
+ </svg>
515
+ <div class="progress-text">${state.progress}%</div>
516
+ </div>
517
+ <p class="status-text">${this.texts.downloading}</p>
518
+ `;
519
+ case "ready": return `
520
+ <div class="check-icon">
521
+ <svg fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
522
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
523
+ </svg>
524
+ </div>
525
+ <p class="status-text" style="margin-bottom: 1.5rem;">${this.texts.downloadComplete}</p>
526
+ <button class="apply-btn" data-action="apply">${this.texts.applyButton}</button>
527
+ `;
528
+ case "applying": return `
529
+ <div class="spinner"></div>
530
+ <p class="status-text">${this.texts.applying}</p>
531
+ `;
532
+ case "error": return `
533
+ <div class="error-icon">
534
+ <svg fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
535
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
536
+ </svg>
537
+ </div>
538
+ <p class="error-title">${this.texts.errorTitle}</p>
539
+ <p class="error-message">${state.error || ""}</p>
540
+ <button class="retry-btn" data-action="retry">${this.texts.retryButton}</button>
541
+ `;
542
+ default: return "";
543
+ }
544
+ }
545
+ attachEventListeners() {
546
+ const ota = getOtaUpdater();
547
+ if (!ota) return;
548
+ const applyBtn = this.shadow.querySelector("[data-action=\"apply\"]");
549
+ const retryBtn = this.shadow.querySelector("[data-action=\"retry\"]");
550
+ applyBtn?.addEventListener("click", () => {
551
+ ota.apply();
552
+ });
553
+ retryBtn?.addEventListener("click", async () => {
554
+ if (!await ota.download()) this.render();
555
+ });
556
+ }
557
+ };
558
+ function registerOtaUpdateElement() {
559
+ if (!customElements.get("ota-update")) customElements.define("ota-update", OtaUpdateElement);
560
+ }
561
+
562
+ //#endregion
563
+ //#region src/index.ts
564
+ async function initOtaUpdate(config) {
565
+ const ota = createOtaUpdater(config);
566
+ if (config.showUI !== false) {
567
+ registerOtaUpdateElement();
568
+ const element = document.createElement("ota-update");
569
+ element.setTheme(config.theme);
570
+ element.setTexts(config.texts);
571
+ document.body.appendChild(element);
572
+ await ota.notifyAppReady();
573
+ if (await ota.check()) {
574
+ element.show();
575
+ if (await ota.download() && config.autoApply) await ota.apply();
576
+ }
577
+ } else await ota.notifyAppReady();
578
+ return ota;
579
+ }
580
+
581
+ //#endregion
582
+ export { OtaUpdateElement, OtaUpdater, getOtaUpdater, initOtaUpdate };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "capacitor-ota",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "dist/index.mjs",
6
+ "types": "dist/index.d.mts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "types": "./dist/index.d.mts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsdown src/index.ts --format esm --dts",
18
+ "dev": "tsdown src/index.ts --format esm --dts --watch",
19
+ "panel:dev": "vite dev --host",
20
+ "panel:build": "vite build",
21
+ "panel:preview": "vite preview",
22
+ "db:migrate": "wrangler d1 migrations apply ota-db --local",
23
+ "db:migrate:remote": "wrangler d1 migrations apply ota-db --remote",
24
+ "cli": "tsx cli/ota.ts"
25
+ },
26
+ "peerDependencies": {
27
+ "@capgo/capacitor-updater": ">=6.0.0",
28
+ "@capacitor/core": ">=8.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@cloudflare/workers-types": "^4.20260123.0",
32
+ "@tailwindcss/vite": "^4.1.18",
33
+ "@vitejs/plugin-vue": "^6.0.3",
34
+ "nitro": "3.0.1-alpha.2",
35
+ "pinia": "^3.0.4",
36
+ "tailwindcss": "^4.1.18",
37
+ "tsdown": "^0.20.1",
38
+ "tsx": "^4.21.0",
39
+ "typescript": "^5.9.3",
40
+ "vite": "8.0.0-beta.8",
41
+ "vue": "^3.5.27",
42
+ "vue-router": "^4.6.4",
43
+ "wrangler": "^4.60.0"
44
+ }
45
+ }