vaderjs-native 1.0.10 → 1.0.11
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/app-template/app/src/main/java/myapp/MainActivity.kt +253 -52
- package/index.ts +342 -8
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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(
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
// ---
|
|
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
|
-
// ---
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
+
response
|
|
132
323
|
} catch (e: Exception) {
|
|
133
|
-
"{\"error\"
|
|
324
|
+
"{\"error\":\"${e.message}\"}"
|
|
134
325
|
}
|
|
135
326
|
}
|
|
136
327
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|