vaderjs-native 1.0.10 → 1.0.12

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 CHANGED
@@ -1,3 +1,5 @@
1
+ # VaderNative
2
+
1
3
  <p align="center">
2
4
  <a href="https://vader-js.pages.dev">
3
5
  <picture>
@@ -8,87 +10,156 @@
8
10
  </a>
9
11
  </p>
10
12
 
11
- # VaderNative
12
-
13
- **A modern, reactive framework for building ultra-fast native mobile apps — minimal, blazing fast, and easy to learn.**
13
+ **A modern, reactive framework for building ultra-fast native mobile apps with comprehensive Android bridge integration — minimal, blazing fast, and feature-rich.**
14
14
 
15
- [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Postr-Inc/Vader.js/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/vaderjs.svg?style=flat)](https://www.npmjs.com/package/vaderjs)
15
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Postr-Inc/Vader.js/blob/main/LICENSE)
16
+ [![npm version](https://img.shields.io/npm/v/vaderjs.svg?style=flat)](https://www.npmjs.com/package/vaderjs)
16
17
 
17
18
  ---
18
19
 
19
- ## 🚀 Quick Example
20
+ ## Why Choose VaderNative?
20
21
 
21
- ```tsx
22
- import * as Vader from "vader-native";
22
+ * ✅ **Ultra-minimal reactivity** – no virtual DOM overhead, direct DOM updates
23
+ * **Full Android bridge integration** – access native device features seamlessly
24
+ * ✅ **File-based routing** – Next.js-inspired, production-ready
25
+ * ✅ **Tiny runtime** – under 20KB gzipped
26
+ * ✅ **React-inspired API** – familiar hooks and components
27
+ * ✅ **D-pad navigation** – built-in TV and remote control support
28
+ * ✅ **Persistent file system** – read/write files directly on Android
29
+ * ✅ **Comprehensive hooks** – all essential React hooks implemented
30
+ * ✅ **TypeScript ready** – full type definitions included
23
31
 
24
- export default function App() {
25
- const [count, setCount] = Vader.useState(0);
32
+ ---
26
33
 
27
- return (
28
- <Vader.View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
29
- <Vader.Switch>
30
- <Vader.Match when={count > 10}>
31
- <Vader.Text>Count is greater than 10</Vader.Text>
32
- </Vader.Match>
33
- <Vader.Match when={count <= 10}>
34
- <Vader.Text>Count is 10 or less</Vader.Text>
35
- </Vader.Match>
36
- </Vader.Switch>
37
-
38
- <Vader.Button title="Increment" onPress={() => setCount(count + 1)} />
39
- </Vader.View>
40
- );
41
- }
42
- ```
34
+ ## 🚀 Feature Highlights
35
+
36
+ ### **Native Android Bridge**
37
+ - **File System Access**: Read, write, delete, and list files directly on Android
38
+ - **Native Dialogs**: System-level alert and confirmation dialogs
39
+ - **Toast Notifications**: Native Android toast messages
40
+ - **Permissions Management**: Request camera, storage, microphone, and notification permissions
41
+ - **D-pad Navigation**: Full TV remote and game controller support
42
+ - **Hardware Keys**: Handle back button, media keys, and navigation buttons
43
+
44
+ ### **Modern React API**
45
+ - **All Essential Hooks**: `useState`, `useEffect`, `useReducer`, `useMemo`, `useCallback`, `useRef`
46
+ - **Advanced Hooks**: `useQuery`, `useArray`, `useWindowFocus`, `useLocalStorage`, `useInterval`
47
+ - **Conditional Rendering**: `Switch`, `Match`, `Show` components
48
+ - **Context API**: Full `createContext` and `useContext` support
49
+ - **Focus Management**: Automatic D-pad navigation for TV apps
50
+
51
+ ### **Performance First**
52
+ - **Zero Virtual DOM**: Direct DOM reconciliation
53
+ - **Smart Reconciliation**: Key-based diffing algorithm
54
+ - **RequestAnimationFrame**: Optimized rendering loop
55
+ - **Memory Efficient**: Minimal garbage collection pressure
43
56
 
44
57
  ---
45
58
 
46
59
  ## 📦 Installation
47
60
 
48
61
  ```bash
62
+ # Using bun (recommended)
49
63
  bun install vaderjs@latest
64
+
65
+ # Using npm
66
+ npm install vaderjs@latest
67
+
68
+ # Using yarn
69
+ yarn add vaderjs@latest
50
70
  ```
51
71
 
52
72
  ---
53
73
 
54
- ## ⚙️ Project Structure
74
+ ## Quick Start
55
75
 
56
- VaderNative uses **file-based routing** inspired by Next.js. Simply create a `pages` folder in your project:
76
+ ### Counter App with Android Features
57
77
 
58
- ```
59
- /pages/index.jsx -> /
60
- /pages/home/[page].jsx -> /home/:page
61
- /pages/path/index.jsx -> /path/
62
- /pages/test/[[...catchall]]/index.jsx -> /test/*
63
- /pages/route/[param1]/[param2].jsx -> /route/:param1/:param2
64
- ```
78
+ ```tsx
79
+ import * as Vader from "vaderjs-native";
80
+ import { useState, useEffect, showToast, FileSystem } from "vaderjs-native";
81
+
82
+ function Counter() {
83
+ const [count, setCount] = useState(0);
84
+
85
+ // Load count from Android file system on startup
86
+ useEffect(async () => {
87
+ const data = await FileSystem.loadJSON("counter_data.json");
88
+ if (data?.count) setCount(data.count);
89
+ }, []);
90
+
91
+ // Save count to file whenever it changes
92
+ useEffect(() => {
93
+ FileSystem.saveJSON("counter_data.json", { count, timestamp: Date.now() });
94
+ }, [count]);
95
+
96
+ const increment = () => {
97
+ setCount(count + 1);
98
+ showToast(`Count: ${count + 1}`);
99
+ };
100
+
101
+ return (
102
+ <div style={{ padding: 20, backgroundColor: "#1a1a1a", minHeight: "100vh" }}>
103
+ <h1 style={{ color: "white", fontSize: 48 }}>Persistent Counter</h1>
104
+ <p style={{ color: "#ccc", fontSize: 24 }}>Count: {count}</p>
105
+ <button
106
+ onClick={increment}
107
+ style={{
108
+ padding: "12px 24px",
109
+ fontSize: 18,
110
+ backgroundColor: "#007AFF",
111
+ color: "white",
112
+ border: "none",
113
+ borderRadius: 8
114
+ }}
115
+ >
116
+ Increment (+1)
117
+ </button>
118
+ </div>
119
+ );
120
+ }
65
121
 
66
- > ⚠️ Note: Native routing works best in production builds. Dev hot-reloading is supported via `bun dev`.
122
+ Vader.render(<Counter />, document.getElementById("app"));
123
+ ```
67
124
 
68
125
  ---
69
126
 
70
- ## 🗂 Special Folders
127
+ ## 📁 Project Structure
128
+
129
+ VaderNative uses file-based routing similar to Next.js:
130
+
131
+ ```
132
+ /app
133
+ /index.jsx -> Homepage (/)
134
+ /about.jsx -> About page (/about)
135
+ /users/[id].jsx -> Dynamic route (/users/:id)
136
+ /settings/[[...slug]].jsx -> Optional catch-all (/settings/*)
137
+
138
+ /src
139
+ /components -> Shared components
140
+ /hooks -> Custom hooks
141
+ /utils -> Utility functions
71
142
 
72
- | Folder | Purpose |
73
- | --------- | --------------------------------------- |
74
- | `app/` | Contains all route `.jsx` files |
75
- | `src/` | Your components, hooks, utilities, etc. |
76
- | `public/` | Static assets like images, JSON, fonts |
143
+ /public
144
+ /assets -> Static assets (images, fonts)
145
+
146
+ vader.config.ts -> App configuration
147
+ ```
77
148
 
78
149
  ---
79
150
 
80
151
  ## 🛠 Configuration
81
152
 
82
- Create a `vader.config.ts` at your project root to define your app’s metadata, platform settings, and plugins:
153
+ Create a `vader.config.ts` file:
83
154
 
84
- ```ts
155
+ ```typescript
85
156
  import defineConfig from "vaderjs-native/config";
86
157
  import tailwind from "vaderjs-native/plugins/tailwind";
87
158
 
88
159
  export default defineConfig({
89
160
  app: {
90
- name: "VaderNative App",
91
- id: "com.vadernative.app",
161
+ name: "My VaderNative App",
162
+ id: "com.example.app",
92
163
  version: {
93
164
  code: 1,
94
165
  name: "1.0.0",
@@ -99,16 +170,18 @@ export default defineConfig({
99
170
  android: {
100
171
  minSdk: 24,
101
172
  targetSdk: 34,
102
- permissions: ["INTERNET", "ACCESS_NETWORK_STATE"],
103
- icon: "./assets/android/icon.png",
104
- splash: "./assets/android/splash.png",
173
+ permissions: [
174
+ "INTERNET",
175
+ "ACCESS_NETWORK_STATE",
176
+ "READ_EXTERNAL_STORAGE",
177
+ "WRITE_EXTERNAL_STORAGE",
178
+ "CAMERA"
179
+ ],
105
180
  },
106
181
  web: {
107
182
  title: "VaderNative App",
108
183
  themeColor: "#111827",
109
184
  },
110
- ios: {},
111
- windows: {},
112
185
  },
113
186
 
114
187
  plugins: [tailwind]
@@ -117,14 +190,281 @@ export default defineConfig({
117
190
 
118
191
  ---
119
192
 
120
- ## Why VaderNative?
193
+ ## 🔧 Core API Reference
194
+
195
+ ### **Hooks**
196
+
197
+ | Hook | Description | Example |
198
+ |------|-------------|---------|
199
+ | `useState` | Component state management | `const [count, setCount] = useState(0)` |
200
+ | `useEffect` | Side effects and lifecycle | `useEffect(() => { fetchData() }, [])` |
201
+ | `useReducer` | Complex state logic | `const [state, dispatch] = useReducer(reducer, initialState)` |
202
+ | `useMemo` | Memoize expensive calculations | `const value = useMemo(() => compute(a, b), [a, b])` |
203
+ | `useCallback` | Memoize callback functions | `const cb = useCallback(() => doSomething(), [])` |
204
+ | `useRef` | Mutable references | `const ref = useRef(null)` |
205
+ | `useContext` | Context consumption | `const value = useContext(MyContext)` |
206
+ | `useArray` | Array state with helpers | `const {array, add, remove} = useArray([])` |
207
+ | `useQuery` | Data fetching with caching | `const {data, loading} = useQuery("/api/data")` |
208
+ | `useLocalStorage` | Sync state with localStorage | `const [value, setValue] = useLocalStorage("key", init)` |
209
+ | `useInterval` | Run functions at intervals | `useInterval(() => {}, 1000)` |
210
+ | `useWindowFocus` | Track window focus state | `const isFocused = useWindowFocus()` |
211
+ | `useOnClickOutside` | Detect clicks outside element | `useOnClickOutside(ref, handler)` |
212
+
213
+ ### **Android Bridge Features**
214
+
215
+ | Feature | Method | Description |
216
+ |---------|--------|-------------|
217
+ | **File System** | `FileSystem.saveJSON()` | Save JSON data to Android storage |
218
+ | | `FileSystem.loadJSON()` | Load JSON data from storage |
219
+ | | `FileSystem.exists()` | Check if file exists |
220
+ | | `FS.readFile()` | Read any file as string |
221
+ | | `FS.writeFile()` | Write any file content |
222
+ | | `FS.deleteFile()` | Delete files |
223
+ | | `FS.listDir()` | List directory contents |
224
+ | **UI** | `showToast()` | Show native Android toast |
225
+ | | `useDialog().alert()` | Native alert dialog |
226
+ | | `useDialog().confirm()` | Native confirmation dialog |
227
+ | **Permissions** | `usePermission().request()` | Request Android permissions |
228
+ | | `usePermission().has()` | Check permission status |
229
+ | **Navigation** | `Link` component | Native-aware navigation |
230
+ | | Android back button | Automatic handling |
231
+ | **D-pad** | Auto-focus management | TV remote navigation |
232
+ | | Hardware key support | Game controllers, remotes |
233
+
234
+ ### **Components**
235
+
236
+ | Component | Description | Example |
237
+ |-----------|-------------|---------|
238
+ | `Switch` | Conditional rendering switcher | `<Switch>{matches}</Switch>` |
239
+ | `Match` | Condition for Switch | `<Match when={cond}>{content}</Match>` |
240
+ | `Show` | Conditional render | `<Show when={cond}>{content}</Show>` |
241
+ | `Link` | Navigation link | `<Link to="/about">About</Link>` |
242
+
243
+ ### **Focus Management**
244
+
245
+ VaderNative includes a sophisticated D-pad navigation system for TV apps:
246
+
247
+ ```typescript
248
+ // Automatic focus management - no setup needed!
249
+ // Just use standard HTML elements:
250
+
251
+ <button>Button 1</button> // Focusable with D-pad
252
+ <a href="#">Link</a> // Focusable with D-pad
253
+ <input type="text" /> // Focusable with D-pad
254
+
255
+ // Manual focus control:
256
+ const focusManager = new FocusManager();
257
+ focusManager.focusFirst(); // Focus first element
258
+ focusManager.focusElement(el); // Focus specific element
259
+ ```
260
+
261
+ ---
262
+
263
+ ## 📊 File System Example
264
+
265
+ ```typescript
266
+ import { FileSystem, useEffect, useState } from "vaderjs-native";
267
+
268
+ function UserProfile() {
269
+ const [user, setUser] = useState(null);
270
+
271
+ useEffect(async () => {
272
+ // Load user data from Android storage
273
+ const savedUser = await FileSystem.loadJSON("user_profile.json");
274
+ if (savedUser) {
275
+ setUser(savedUser);
276
+ } else {
277
+ // Create default profile
278
+ const defaultUser = {
279
+ name: "Guest",
280
+ theme: "dark",
281
+ preferences: { language: "en" }
282
+ };
283
+ await FileSystem.saveJSON("user_profile.json", defaultUser);
284
+ setUser(defaultUser);
285
+ }
286
+ }, []);
287
+
288
+ const saveProfile = async (updates) => {
289
+ const updated = { ...user, ...updates };
290
+ const success = await FileSystem.saveJSON("user_profile.json", updated);
291
+ if (success) {
292
+ setUser(updated);
293
+ showToast("Profile saved!");
294
+ }
295
+ };
296
+
297
+ return (
298
+ <div>
299
+ <h1>{user?.name}'s Profile</h1>
300
+ <button onClick={() => saveProfile({ theme: "light" })}>
301
+ Set Light Theme
302
+ </button>
303
+ </div>
304
+ );
305
+ }
306
+ ```
307
+
308
+ ---
309
+
310
+ ## 📱 Android-Specific Features
311
+
312
+ ### **Permission Management**
313
+
314
+ ```typescript
315
+ const permissions = usePermission();
316
+
317
+ // Request camera access
318
+ const takePhoto = async () => {
319
+ const hasCamera = await permissions.has("camera");
320
+ if (!hasCamera) {
321
+ const granted = await permissions.request("camera");
322
+ if (!granted) {
323
+ showToast("Camera permission required");
324
+ return;
325
+ }
326
+ }
327
+ // Take photo logic...
328
+ };
329
+ ```
330
+
331
+ ### **Native Dialogs**
332
+
333
+ ```typescript
334
+ const dialog = useDialog();
335
+
336
+ const deleteItem = async () => {
337
+ const confirmed = await dialog.confirm({
338
+ title: "Delete Item",
339
+ message: "Are you sure you want to delete this item?",
340
+ okText: "Delete",
341
+ cancelText: "Cancel"
342
+ });
343
+
344
+ if (confirmed) {
345
+ // Delete logic...
346
+ }
347
+ };
348
+ ```
121
349
 
122
- * **Minimal reactivity model** – no virtual DOM overhead
123
- * ✅ **Native components** – fast and lightweight
124
- * ✅ **File-based routing** – simple, built-in, production-ready
125
- * ✅ **Tiny runtime** – blazing fast updates
126
- * ✅ **React-inspired API** – familiar but simpler
350
+ ### **D-pad Navigation Events**
127
351
 
128
- Perfect for building Bun-first **cross-platform native apps** with speed and simplicity.
352
+ ```javascript
353
+ // Listen for D-pad navigation events
354
+ document.addEventListener('dpadfocus', (e) => {
355
+ console.log('Element focused:', e.detail.element);
356
+ });
357
+
358
+ // Listen for back button
359
+ document.addEventListener('dpadback', () => {
360
+ // Handle back navigation
361
+ });
362
+ ```
363
+
364
+ ---
365
+
366
+ ## 🎯 Performance Features
367
+
368
+ ### **Automatic Batching**
369
+ State updates are automatically batched for optimal performance.
370
+
371
+ ### **Key-Based Reconciliation**
372
+ Efficient DOM updates using React-like key prop support.
373
+
374
+ ### **Smart Re-rendering**
375
+ Components only re-render when their props or state actually change.
376
+
377
+ ### **Minimal Bundle Size**
378
+ - Core runtime: < 20KB
379
+ - Full feature set: < 25KB
380
+ - No external dependencies
381
+
382
+ ---
383
+
384
+ ## 🔄 Data Fetching with Caching
385
+
386
+ ```typescript
387
+ import { useQuery } from "vaderjs-native";
388
+
389
+ function DataDashboard() {
390
+ // Automatically caches for 5 minutes
391
+ const { data, loading, error, refetch } = useQuery(
392
+ "https://api.example.com/data",
393
+ { expiryMs: 300000 } // 5 minutes cache
394
+ );
395
+
396
+ if (loading) return <div>Loading...</div>;
397
+ if (error) return <div>Error: {error.message}</div>;
398
+
399
+ return (
400
+ <div>
401
+ <h1>Dashboard</h1>
402
+ <pre>{JSON.stringify(data, null, 2)}</pre>
403
+ <button onClick={refetch}>Refresh</button>
404
+ </div>
405
+ );
406
+ }
407
+ ```
408
+
409
+ ---
410
+
411
+ ## 📦 Building for Production
412
+
413
+ ```bash
414
+ # Build for Android
415
+ bun run build:android
416
+
417
+ # Build for Web
418
+ bun run build:web
419
+
420
+ # Development server with hot reload
421
+ bun run dev
422
+ ```
423
+
424
+ ---
425
+
426
+ ## 🆚 Comparison with Other Frameworks
427
+
428
+ | Feature | VaderNative | React Native | Flutter | NativeScript |
429
+ |---------|-------------|--------------|---------|--------------|
430
+ | **Bundle Size** | < 25KB | ~2MB | ~5MB | ~5MB |
431
+ | **Startup Time** | Instant | Slow | Slow | Medium |
432
+ | **Android Bridge** | Built-in | Via Native Modules | Via Plugins | Via Plugins |
433
+ | **D-pad Support** | Built-in | Requires Setup | Requires Setup | Requires Setup |
434
+ | **File System** | Direct Access | AsyncStorage | path_provider | file-system |
435
+ | **Learning Curve** | Easy (React-like) | Medium | Hard (Dart) | Medium |
436
+ | **Memory Usage** | Very Low | High | High | Medium |
437
+
438
+ ---
439
+
440
+ ## 🚀 Getting Started Template
441
+
442
+ ```bash
443
+ # Create new VaderNative project
444
+ bun create vader-native my-app
445
+ cd my-app
446
+
447
+ # Install dependencies
448
+ bun install
449
+
450
+ # Start development server
451
+ bun run dev
452
+
453
+ # Build for production
454
+ bun run build:android
455
+ ```
456
+
457
+ ---
458
+
459
+ ## 🤝 Contributing
460
+
461
+ We welcome contributions! Please see our [Contributing Guide](https://github.com/Postr-Inc/Vader.js/blob/main/CONTRIBUTING.md) for details.
462
+
463
+ ## 📄 License
464
+
465
+ VaderNative is [MIT licensed](https://github.com/Postr-Inc/Vader.js/blob/main/LICENSE).
466
+
467
+
468
+ ---
129
469
 
130
-
470
+ **Ready to build lightning-fast native apps?** [Get Started →](https://vader-js.pages.dev)
@@ -1,24 +1,34 @@
1
1
  package com.example.myapplication
2
- import androidx.activity.OnBackPressedCallback
3
2
 
3
+ import android.app.AlertDialog
4
4
  import android.annotation.SuppressLint
5
5
  import android.content.Context
6
+ import android.content.pm.PackageManager
6
7
  import android.os.Bundle
7
8
  import android.os.Message
8
9
  import android.view.KeyEvent
9
10
  import android.webkit.JavascriptInterface
11
+ import android.webkit.WebChromeClient
10
12
  import android.webkit.WebView
11
13
  import android.webkit.WebViewClient
12
- import android.webkit.WebChromeClient
13
14
  import android.widget.Toast
14
15
  import androidx.activity.ComponentActivity
16
+ import androidx.activity.OnBackPressedCallback
17
+ import androidx.core.app.ActivityCompat
18
+ import androidx.core.content.ContextCompat
19
+ import org.json.JSONArray
20
+ import java.io.FileNotFoundException
15
21
  import java.net.HttpURLConnection
16
22
  import java.net.URL
17
23
 
18
24
  class MainActivity : ComponentActivity() {
25
+
19
26
  lateinit var webView: WebView
27
+ lateinit var androidBridge: AndroidBridge
20
28
 
21
- private var baseUrl = "file:///android_asset/myapp/"
29
+ private val baseUrl = "file:///android_asset/myapp/index.html"
30
+
31
+ @SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
22
32
  override fun onCreate(savedInstanceState: Bundle?) {
23
33
  super.onCreate(savedInstanceState)
24
34
 
@@ -26,63 +36,95 @@ class MainActivity : ComponentActivity() {
26
36
  setContentView(webView)
27
37
 
28
38
  // --- WebView Settings ---
29
- val settings = webView.settings
30
- settings.javaScriptEnabled = true
31
- settings.allowFileAccess = true
32
- settings.allowContentAccess = true
33
- settings.allowFileAccessFromFileURLs = true
34
- settings.allowUniversalAccessFromFileURLs = true
35
- settings.domStorageEnabled = true
36
- settings.mediaPlaybackRequiresUserGesture = false
39
+ webView.settings.apply {
40
+ javaScriptEnabled = true
41
+ allowFileAccess = true
42
+ allowContentAccess = true
43
+ allowFileAccessFromFileURLs = true
44
+ allowUniversalAccessFromFileURLs = true
45
+ domStorageEnabled = true
46
+ databaseEnabled = true
47
+ mediaPlaybackRequiresUserGesture = false
48
+
49
+ // Basic compatibility settings
50
+ setSupportMultipleWindows(false)
51
+ loadWithOverviewMode = true
52
+ useWideViewPort = true
53
+ builtInZoomControls = true
54
+ displayZoomControls = false
55
+ setSupportZoom(true)
56
+
57
+ // Performance optimizations
58
+ javaScriptCanOpenWindowsAutomatically = false
59
+ loadsImagesAutomatically = true
60
+ }
61
+
37
62
  webView.isFocusable = true
38
63
  webView.isFocusableInTouchMode = true
39
64
  webView.requestFocus()
40
- // --- WebViewClient to block external URLs ---
65
+
66
+ // --- JS Bridge ---
67
+ androidBridge = AndroidBridge(this, webView, baseUrl)
68
+ webView.addJavascriptInterface(androidBridge, "Android")
69
+
70
+ // --- WebViewClient ---
41
71
  webView.webViewClient = object : WebViewClient() {
72
+
42
73
  override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
43
- if (url != null && url.startsWith("file:///android_asset/myapp/")) {
44
- return false
74
+ return if (url != null && url.startsWith(baseUrl)) {
75
+ false
76
+ } else {
77
+ Toast.makeText(
78
+ this@MainActivity,
79
+ "Blocked external navigation",
80
+ Toast.LENGTH_SHORT
81
+ ).show()
82
+ true
45
83
  }
46
- Toast.makeText(this@MainActivity, "Blocked external navigation", Toast.LENGTH_SHORT)
47
- .show()
48
- return true
84
+ }
85
+
86
+ override fun onPageFinished(view: WebView?, url: String?) {
87
+ view?.evaluateJavascript(
88
+ "console.log('Android bridge ready:', !!window.Android)",
89
+ null
90
+ )
49
91
  }
50
92
  }
51
93
 
52
- // --- Optional WebChromeClient for popups ---
94
+ // --- Block popups ---
53
95
  webView.webChromeClient = object : WebChromeClient() {
54
96
  override fun onCreateWindow(
55
97
  view: WebView?,
56
98
  isDialog: Boolean,
57
99
  isUserGesture: Boolean,
58
100
  resultMsg: Message?
59
- ): Boolean {
60
- // Block popups completely
61
- return false
62
- }
101
+ ): Boolean = false
63
102
  }
64
103
 
65
- // --- Add JS bridge ---
66
- webView.addJavascriptInterface(AndroidBridge(this, webView, baseUrl), "Android")
104
+ // --- Back button → JS ---
67
105
  onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
68
106
  override fun handleOnBackPressed() {
69
- // Forward back key to JS
70
107
  webView.evaluateJavascript(
71
- "(function(){if(window.onNativeKey){window.onNativeKey(4);}})()",
108
+ "window.onNativeKey && window.onNativeKey(4)",
72
109
  null
73
110
  )
74
- // Do NOT call super, JS will handle closing modals/player
75
111
  }
76
112
  })
77
- webView.webViewClient = object : WebViewClient() {
78
- override fun onPageFinished(view: WebView?, url: String?) {
79
- view?.evaluateJavascript("console.log('JS ready')", null)
80
- }
81
- }
82
- // --- Load local HTML ---
113
+
83
114
  webView.loadUrl(baseUrl)
84
115
  }
85
116
 
117
+ // --- Permission result forwarding ---
118
+ override fun onRequestPermissionsResult(
119
+ requestCode: Int,
120
+ permissions: Array<out String>,
121
+ grantResults: IntArray
122
+ ) {
123
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
124
+ androidBridge.onPermissionResult(requestCode, grantResults)
125
+ }
126
+
127
+ // --- DPAD / media keys ---
86
128
  @SuppressLint("RestrictedApi")
87
129
  override fun dispatchKeyEvent(event: KeyEvent): Boolean {
88
130
  if (event.action == KeyEvent.ACTION_DOWN) {
@@ -102,7 +144,6 @@ class MainActivity : ComponentActivity() {
102
144
  }
103
145
  true
104
146
  }
105
-
106
147
  else -> false
107
148
  }
108
149
  if (handled) return true
@@ -111,13 +152,164 @@ class MainActivity : ComponentActivity() {
111
152
  }
112
153
  }
113
154
 
114
- class AndroidBridge(private val context: Context, private val webView: WebView, private val baseUrl: String) {
155
+ // ---------------- JS BRIDGE ----------------
156
+
157
+ class AndroidBridge(
158
+ private val activity: ComponentActivity,
159
+ private val webView: WebView,
160
+ private val baseUrl: String
161
+ ) {
162
+
163
+ private val PERMISSION_REQUEST_CODE = 9001
115
164
 
165
+ // ---- Toast ----
116
166
  @JavascriptInterface
117
167
  fun showToast(message: String) {
118
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
168
+ activity.runOnUiThread {
169
+ Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
170
+ }
119
171
  }
120
172
 
173
+ // ---- File System Methods ----
174
+ @JavascriptInterface
175
+ fun writeFile(path: String, content: String): Boolean {
176
+ return try {
177
+ // Create directories if needed
178
+ val file = File(activity.filesDir, path)
179
+ file.parentFile?.mkdirs()
180
+
181
+ file.bufferedWriter().use { writer ->
182
+ writer.write(content)
183
+ }
184
+ true
185
+ } catch (e: Exception) {
186
+ e.printStackTrace()
187
+ false
188
+ }
189
+ }
190
+
191
+ @JavascriptInterface
192
+ fun readFile(path: String): String {
193
+ return try {
194
+ val file = File(activity.filesDir, path)
195
+ if (!file.exists()) {
196
+ return "{\"error\":\"File not found\"}"
197
+ }
198
+ file.bufferedReader().use { it.readText() }
199
+ } catch (e: Exception) {
200
+ e.printStackTrace()
201
+ "{\"error\":\"${e.message}\"}"
202
+ }
203
+ }
204
+
205
+ @JavascriptInterface
206
+ fun deleteFile(path: String): Boolean {
207
+ return try {
208
+ val file = File(activity.filesDir, path)
209
+ file.delete()
210
+ } catch (e: Exception) {
211
+ e.printStackTrace()
212
+ false
213
+ }
214
+ }
215
+
216
+ @JavascriptInterface
217
+ fun listFiles(path: String = ""): String {
218
+ return try {
219
+ val dir = File(activity.filesDir, path)
220
+ val files = if (dir.exists() && dir.isDirectory) {
221
+ dir.list()?.toList() ?: emptyList()
222
+ } else {
223
+ emptyList()
224
+ }
225
+ JSONArray(files).toString()
226
+ } catch (e: Exception) {
227
+ e.printStackTrace()
228
+ "[]"
229
+ }
230
+ }
231
+
232
+ // ---- Permissions ----
233
+ @JavascriptInterface
234
+ fun hasPermission(name: String): Boolean {
235
+ val permissions = mapPermission(name)
236
+ return permissions.all {
237
+ ContextCompat.checkSelfPermission(activity, it) ==
238
+ PackageManager.PERMISSION_GRANTED
239
+ }
240
+ }
241
+
242
+ @JavascriptInterface
243
+ fun requestPermission(name: String) {
244
+ val permissions = mapPermission(name)
245
+
246
+ if (permissions.isEmpty()) {
247
+ sendPermissionResult(true)
248
+ return
249
+ }
250
+
251
+ val granted = permissions.all {
252
+ ContextCompat.checkSelfPermission(activity, it) ==
253
+ PackageManager.PERMISSION_GRANTED
254
+ }
255
+
256
+ if (granted) {
257
+ sendPermissionResult(true)
258
+ return
259
+ }
260
+
261
+ ActivityCompat.requestPermissions(
262
+ activity,
263
+ permissions,
264
+ PERMISSION_REQUEST_CODE
265
+ )
266
+ }
267
+
268
+ fun onPermissionResult(requestCode: Int, grantResults: IntArray) {
269
+ if (requestCode != PERMISSION_REQUEST_CODE) return
270
+ val granted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
271
+ sendPermissionResult(granted)
272
+ }
273
+
274
+ private fun sendPermissionResult(granted: Boolean) {
275
+ webView.post {
276
+ webView.evaluateJavascript(
277
+ "window.onNativePermissionResult && window.onNativePermissionResult($granted)",
278
+ null
279
+ )
280
+ }
281
+ }
282
+
283
+ // ---- Dialog ----
284
+ @JavascriptInterface
285
+ fun showDialog(
286
+ title: String,
287
+ message: String,
288
+ okText: String = "OK",
289
+ cancelText: String = "Cancel"
290
+ ) {
291
+ activity.runOnUiThread {
292
+ AlertDialog.Builder(activity)
293
+ .setTitle(title)
294
+ .setMessage(message)
295
+ .setPositiveButton(okText) { _, _ ->
296
+ webView.evaluateJavascript(
297
+ "window.onNativeDialogResult && window.onNativeDialogResult(true)",
298
+ null
299
+ )
300
+ }
301
+ .setNegativeButton(cancelText) { _, _ ->
302
+ webView.evaluateJavascript(
303
+ "window.onNativeDialogResult && window.onNativeDialogResult(false)",
304
+ null
305
+ )
306
+ }
307
+ .setCancelable(false)
308
+ .show()
309
+ }
310
+ }
311
+
312
+ // ---- Native fetch ----
121
313
  @JavascriptInterface
122
314
  fun nativeFetch(url: String, method: String): String {
123
315
  return try {
@@ -125,26 +317,35 @@ class MainActivity : ComponentActivity() {
125
317
  connection.requestMethod = method
126
318
  connection.connectTimeout = 5000
127
319
  connection.readTimeout = 5000
128
-
129
- val responseText = connection.inputStream.bufferedReader().use { it.readText() }
320
+ val response = connection.inputStream.bufferedReader().use { it.readText() }
130
321
  connection.disconnect()
131
- responseText
322
+ response
132
323
  } catch (e: Exception) {
133
- "{\"error\": \"${e.message}\"}"
324
+ "{\"error\":\"${e.message}\"}"
134
325
  }
135
326
  }
136
327
 
137
- @JavascriptInterface
138
- fun navigate(path: String?) {
139
- // Ensure path starts with a clean structure
140
- val cleanPath = path?.trimStart('/') ?: ""
328
+ // ---- Navigation ----
329
+ @JavascriptInterface
330
+ fun navigate(path: String?) {
331
+ val clean = path?.trimStart('/') ?: ""
332
+ webView.post {
333
+ val finalUrl = "$baseUrl$clean/index.html".replace("//index", "/index")
334
+ webView.loadUrl(finalUrl)
335
+ }
336
+ }
141
337
 
142
- webView.post {
143
- // Concatenate the base (Dev or Prod) with the path
144
- // Example Dev: http://192.168.1.x:3000/settings/index.html
145
- // Example Prod: file:///android_asset/myapp/settings/index.html
146
- val finalUrl = "${baseUrl}${cleanPath}/index.html".replace("//index", "/index")
147
- webView.loadUrl(finalUrl)
148
- }
338
+ // ---- Permission map ----
339
+ private fun mapPermission(name: String): Array<String> {
340
+ return when (name) {
341
+ "storage" -> arrayOf(
342
+ android.Manifest.permission.READ_EXTERNAL_STORAGE,
343
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE
344
+ )
345
+ "camera" -> arrayOf(android.Manifest.permission.CAMERA)
346
+ "microphone" -> arrayOf(android.Manifest.permission.RECORD_AUDIO)
347
+ "notifications" -> arrayOf(android.Manifest.permission.POST_NOTIFICATIONS)
348
+ else -> emptyArray()
149
349
  }
150
- }
350
+ }
351
+ }
package/index.ts CHANGED
@@ -1,4 +1,19 @@
1
- // Android key code to key mapping
1
+ ;(window as any).onNativeDialogResult = function (confirmed: boolean) {
2
+ if (dialogResolver) {
3
+ dialogResolver(confirmed);
4
+ dialogResolver = null;
5
+ }
6
+ };
7
+
8
+ /**
9
+ * Called by Android
10
+ */
11
+ ;(window as any).onNativePermissionResult = function (granted: boolean) {
12
+ if (permissionResolver) {
13
+ permissionResolver(granted);
14
+ permissionResolver = null;
15
+ }
16
+ };
2
17
  const ANDROID_KEY_MAP: Record<number, string> = {
3
18
  19: "ArrowUp", // DPAD_UP
4
19
  20: "ArrowDown", // DPAD_DOWN
@@ -970,16 +985,335 @@ export function Show({ when, children }: { when: boolean, children: VNode[] }):
970
985
  //@ts-ignore
971
986
  return when ? children : null;
972
987
  }
973
- export function showToast (message: string, duration = 3000) {
974
- //@ts-ignore
975
- if (window.Android && typeof window.Android.showToast === "function") {
976
- //@ts-ignore
977
- window.Android.showToast(message, duration);
978
- } else {
979
- console.log(`[showToast] ${message}`);
988
+ /**
989
+ * @description Show toast allows you to invoke system level toast api to show data to user
990
+ * @param message
991
+ * @param duration
992
+ */
993
+ export function showToast(message: string, duration = 3000) {
994
+ if (typeof window !== "undefined" && (window as any).Android?.showToast) {
995
+ console.log("[Vader] Android Toast");
996
+ (window as any).Android.showToast(message);
997
+ return;
998
+ }
999
+
1000
+ // Web fallback
1001
+ console.log("[Toast]", message);
1002
+
1003
+ const toast = document.createElement("div");
1004
+ toast.textContent = message;
1005
+ Object.assign(toast.style, {
1006
+ position: "fixed",
1007
+ bottom: "24px",
1008
+ left: "50%",
1009
+ transform: "translateX(-50%)",
1010
+ background: "rgba(0,0,0,0.85)",
1011
+ color: "white",
1012
+ padding: "10px 14px",
1013
+ borderRadius: "8px",
1014
+ zIndex: 9999,
1015
+ fontSize: "14px",
1016
+ });
1017
+
1018
+ document.body.appendChild(toast);
1019
+ setTimeout(() => toast.remove(), duration);
1020
+ }
1021
+
1022
+ type PermissionName =
1023
+ | "storage"
1024
+ | "internet"
1025
+ | "camera"
1026
+ | "microphone"
1027
+ | "notifications";
1028
+
1029
+ let permissionResolver: ((granted: boolean) => void) | null = null;
1030
+
1031
+ export function usePermission() {
1032
+ const isAndroid =
1033
+ typeof window !== "undefined" &&
1034
+ (window as any).Android?.requestPermission;
1035
+
1036
+ function request(name: PermissionName): Promise<boolean> {
1037
+ if (isAndroid) {
1038
+ return new Promise<boolean>((resolve) => {
1039
+ permissionResolver = resolve;
1040
+ (window as any).Android.requestPermission(name);
1041
+ });
1042
+ }
1043
+
1044
+ // ---- Web fallback ----
1045
+ console.warn(`[Permission] ${name} auto-granted on web`);
1046
+ return Promise.resolve(true);
1047
+ }
1048
+
1049
+ function has(name: PermissionName): Promise<boolean> {
1050
+ if (isAndroid && (window as any).Android.hasPermission) {
1051
+ return Promise.resolve(
1052
+ (window as any).Android.hasPermission(name)
1053
+ );
1054
+ }
1055
+ return Promise.resolve(true);
1056
+ }
1057
+
1058
+ return {
1059
+ request,
1060
+ has,
1061
+
1062
+ // ergonomic helpers
1063
+ storage: () => request("storage"),
1064
+ camera: () => request("camera"),
1065
+ microphone: () => request("microphone"),
1066
+ notifications: () => request("notifications"),
1067
+ internet: () => request("internet")
1068
+ };
1069
+ }
1070
+
1071
+ type FS = {
1072
+ readFile(path: string): Promise<string>
1073
+ writeFile(path: string, content: string): Promise<boolean>
1074
+ deleteFile(path: string): Promise<boolean>
1075
+ listDir(path: string): Promise<string[]>
1076
+ }
1077
+
1078
+ export const FS: FS = {
1079
+ async writeFile(path: string, content: string): Promise<boolean> {
1080
+ try {
1081
+ if (!window.Android) {
1082
+ console.error('Android bridge not available')
1083
+ return false
1084
+ }
1085
+
1086
+ // Call Android bridge method
1087
+ const result = window.Android.writeFile(path, content)
1088
+ return result === true || result === 'true'
1089
+ } catch (error) {
1090
+ console.error('Error writing file:', error)
1091
+ return false
1092
+ }
1093
+ },
1094
+
1095
+ async readFile(path: string): Promise<string> {
1096
+ try {
1097
+ if (!window.Android) {
1098
+ return JSON.stringify({ error: 'Android bridge not available' })
1099
+ }
1100
+
1101
+ const result = window.Android.readFile(path)
1102
+
1103
+ // Handle both string and boolean returns
1104
+ if (typeof result === 'boolean') {
1105
+ return result ? 'true' : 'false'
1106
+ }
1107
+
1108
+ return result || ''
1109
+ } catch (error) {
1110
+ console.error('Error reading file:', error)
1111
+ return JSON.stringify({ error: error.message })
1112
+ }
1113
+ },
1114
+
1115
+ async deleteFile(path: string): Promise<boolean> {
1116
+ try {
1117
+ if (!window.Android) {
1118
+ console.error('Android bridge not available')
1119
+ return false
1120
+ }
1121
+
1122
+ // Check if deleteFile method exists on Android bridge
1123
+ if (typeof window.Android.deleteFile === 'function') {
1124
+ const result = window.Android.deleteFile(path)
1125
+ return result === true || result === 'true'
1126
+ } else {
1127
+ // Fallback: Try to write empty content
1128
+ console.warn('deleteFile not available, using writeFile fallback')
1129
+ return await this.writeFile(path, '')
1130
+ }
1131
+ } catch (error) {
1132
+ console.error('Error deleting file:', error)
1133
+ return false
1134
+ }
1135
+ },
1136
+
1137
+ async listDir(path: string = ''): Promise<string[]> {
1138
+ try {
1139
+ if (!window.Android) {
1140
+ console.error('Android bridge not available')
1141
+ return []
1142
+ }
1143
+
1144
+ // Check if listFiles method exists on Android bridge
1145
+ if (typeof window.Android.listFiles === 'function') {
1146
+ const result = window.Android.listFiles(path)
1147
+
1148
+ // Parse JSON array from string
1149
+ if (typeof result === 'string') {
1150
+ try {
1151
+ const parsed = JSON.parse(result)
1152
+ return Array.isArray(parsed) ? parsed : []
1153
+ } catch {
1154
+ // If not JSON, return as single item array or empty
1155
+ return result ? [result] : []
1156
+ }
1157
+ }
1158
+
1159
+ // If result is already an array
1160
+ if (Array.isArray(result)) {
1161
+ return result
1162
+ }
1163
+
1164
+ return []
1165
+ } else {
1166
+ console.warn('listFiles not available on Android bridge')
1167
+ return []
1168
+ }
1169
+ } catch (error) {
1170
+ console.error('Error listing directory:', error)
1171
+ return []
1172
+ }
1173
+ },
1174
+
1175
+ // Alias for backward compatibility
1176
+ write(path: string, data: string): Promise<boolean> {
1177
+ return this.writeFile(path, data)
1178
+ },
1179
+
1180
+ read(path: string): Promise<string> {
1181
+ return this.readFile(path)
980
1182
  }
1183
+ }
1184
+
1185
+ // TypeScript declarations for Android bridge
1186
+ declare global {
1187
+ interface Window {
1188
+ Android?: {
1189
+ writeFile?: (path: string, content: string) => boolean | string
1190
+ readFile?: (path: string) => string
1191
+ deleteFile?: (path: string) => boolean | string
1192
+ listFiles?: (path?: string) => string[] | string
1193
+ // Other Android bridge methods...
1194
+ showToast?: (message: string) => void
1195
+ hasPermission?: (name: string) => boolean
1196
+ requestPermission?: (name: string) => void
1197
+ showDialog?: (title: string, message: string, okText?: string, cancelText?: string) => void
1198
+ nativeFetch?: (url: string, method: string) => string
1199
+ navigate?: (path: string) => void
1200
+ }
1201
+ }
1202
+ }
1203
+
1204
+ // Utility functions for common operations
1205
+ export const FileSystem = {
1206
+ // Save JSON data
1207
+ async saveJSON(path: string, data: any): Promise<boolean> {
1208
+ return await FS.writeFile(path, JSON.stringify(data, null, 2))
1209
+ },
1210
+
1211
+ // Load JSON data
1212
+ async loadJSON<T = any>(path: string): Promise<T | null> {
1213
+ try {
1214
+ const content = await FS.readFile(path)
1215
+ if (!content || content.includes('error')) {
1216
+ return null
1217
+ }
1218
+ return JSON.parse(content)
1219
+ } catch (error) {
1220
+ console.error('Error parsing JSON:', error)
1221
+ return null
1222
+ }
1223
+ },
1224
+
1225
+ // Check if file exists
1226
+ async exists(path: string): Promise<boolean> {
1227
+ try {
1228
+ const content = await FS.readFile(path)
1229
+ return !content.includes('File not found') && !content.includes('error')
1230
+ } catch {
1231
+ return false
1232
+ }
1233
+ },
1234
+
1235
+ // Append to file
1236
+ async appendFile(path: string, content: string): Promise<boolean> {
1237
+ try {
1238
+ const existing = await FS.readFile(path)
1239
+ const newContent = existing + content
1240
+ return await FS.writeFile(path, newContent)
1241
+ } catch (error) {
1242
+ console.error('Error appending to file:', error)
1243
+ return false
1244
+ }
1245
+ },
1246
+
1247
+ // Create directory (by creating a dummy file)
1248
+ async createDirectory(path: string): Promise<boolean> {
1249
+ // Create a .nomedia file in the directory
1250
+ const dirPath = path.endsWith('/') ? path : path + '/'
1251
+ return await FS.writeFile(dirPath + '.nomedia', '')
1252
+ }
1253
+ }
1254
+ type DialogOptions = {
1255
+ title?: string;
1256
+ message: string;
1257
+ okText?: string;
1258
+ cancelText?: string;
981
1259
  };
982
1260
 
1261
+ let dialogResolver: ((value: boolean) => void) | null = null;
1262
+
1263
+ export function useDialog() {
1264
+ // ---- ANDROID IMPLEMENTATION ----
1265
+ if (typeof window !== "undefined" && (window as any).Android?.showDialog) {
1266
+ return {
1267
+ alert({ title = "", message, okText = "OK" }: DialogOptions) {
1268
+ return new Promise<void>((resolve) => {
1269
+ dialogResolver = () => resolve();
1270
+
1271
+ (window as any).Android.showDialog(
1272
+ title,
1273
+ message,
1274
+ okText,
1275
+ "" // no cancel
1276
+ );
1277
+ });
1278
+ },
1279
+
1280
+ confirm({
1281
+ title = "",
1282
+ message,
1283
+ okText = "OK",
1284
+ cancelText = "Cancel",
1285
+ }: DialogOptions) {
1286
+ return new Promise<boolean>((resolve) => {
1287
+ dialogResolver = resolve;
1288
+
1289
+ (window as any).Android.showDialog(
1290
+ title,
1291
+ message,
1292
+ okText,
1293
+ cancelText
1294
+ );
1295
+ });
1296
+ },
1297
+ };
1298
+ }
1299
+
1300
+ // ---- WEB FALLBACK ----
1301
+ return {
1302
+ alert({ title = "", message }: DialogOptions) {
1303
+ window.alert(title ? `${title}\n\n${message}` : message);
1304
+ return Promise.resolve();
1305
+ },
1306
+
1307
+ confirm({ title = "", message }: DialogOptions) {
1308
+ const result = window.confirm(
1309
+ title ? `${title}\n\n${message}` : message
1310
+ );
1311
+ return Promise.resolve(result);
1312
+ },
1313
+ };
1314
+ }
1315
+
1316
+
983
1317
  /**
984
1318
  * A React-like useRef hook for mutable references.
985
1319
  * @template T
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vaderjs-native",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Build Native Applications using Vaderjs framework.",
5
5
  "bin": {
6
6
  "vaderjs": "./main.ts"