forked from akai/readest
feat: request manage external storage permission when changing data directory to sdcard root on Android (#2142)
This commit is contained in:
@@ -24,7 +24,8 @@ echo "🔢 Computed versionCode: $VERSION_CODE"
|
||||
|
||||
PROPERTIES_FILE="./src-tauri/gen/android/app/tauri.properties"
|
||||
MANIFEST="./src-tauri/gen/android/app/src/main/AndroidManifest.xml"
|
||||
PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
|
||||
INSTALL_PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
|
||||
STORAGE_PERMISSION_LINE='<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>'
|
||||
|
||||
if [[ ! -f "$PROPERTIES_FILE" ]]; then
|
||||
echo "❌ File not found: $PROPERTIES_FILE"
|
||||
@@ -38,13 +39,34 @@ mv "$tmpfile" "$PROPERTIES_FILE"
|
||||
|
||||
echo "✅ Updated $PROPERTIES_FILE"
|
||||
|
||||
ised() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "$@"
|
||||
else
|
||||
sed -i "$@"
|
||||
fi
|
||||
|
||||
return $?
|
||||
}
|
||||
|
||||
# --- REMOVE PERMISSION BEFORE BUILD ---
|
||||
if grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST"; then
|
||||
echo "🧹 Removing REQUEST_INSTALL_PACKAGES from AndroidManifest.xml"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "/REQUEST_INSTALL_PACKAGES/d" "$MANIFEST"
|
||||
if ised "/REQUEST_INSTALL_PACKAGES/d" "$MANIFEST"; then
|
||||
echo "✅ Successfully removed REQUEST_INSTALL_PACKAGES"
|
||||
else
|
||||
sed -i "/REQUEST_INSTALL_PACKAGES/d" "$MANIFEST"
|
||||
echo "❌ Failed to remove REQUEST_INSTALL_PACKAGES" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -q 'MANAGE_EXTERNAL_STORAGE' "$MANIFEST"; then
|
||||
echo "🧹 Removing MANAGE_EXTERNAL_STORAGE from AndroidManifest.xml"
|
||||
if ised "/MANAGE_EXTERNAL_STORAGE/d" "$MANIFEST"; then
|
||||
echo "✅ Successfully removed MANAGE_EXTERNAL_STORAGE"
|
||||
else
|
||||
echo "❌ Failed to remove MANAGE_EXTERNAL_STORAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -54,13 +76,16 @@ pnpm tauri android build
|
||||
# --- ADD PERMISSION BACK AFTER BUILD ---
|
||||
if ! grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST"; then
|
||||
echo "♻️ Restoring REQUEST_INSTALL_PACKAGES in AndroidManifest.xml"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "/android.permission.INTERNET/a\\
|
||||
$PERMISSION_LINE
|
||||
" "$MANIFEST"
|
||||
else
|
||||
sed -i "/android.permission.INTERNET/a \ $PERMISSION_LINE" "$MANIFEST"
|
||||
fi
|
||||
ised "/android.permission.INTERNET/a\\
|
||||
$INSTALL_PERMISSION_LINE
|
||||
" "$MANIFEST"
|
||||
fi
|
||||
|
||||
if ! grep -q 'MANAGE_EXTERNAL_STORAGE' "$MANIFEST"; then
|
||||
echo "♻️ Restoring MANAGE_EXTERNAL_STORAGE in AndroidManifest.xml"
|
||||
ised "/android.permission.WRITE_EXTERNAL_STORAGE/a\\
|
||||
$STORAGE_PERMISSION_LINE
|
||||
" "$MANIFEST"
|
||||
fi
|
||||
|
||||
source .env.google-play.local
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
@@ -114,6 +115,12 @@
|
||||
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="com.readest.native_tts.MediaForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
+62
-1
@@ -1,10 +1,13 @@
|
||||
package com.readest.native_bridge
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.KeyEvent
|
||||
import android.view.WindowInsets
|
||||
@@ -13,15 +16,19 @@ import android.view.WindowInsetsController
|
||||
import android.graphics.Color
|
||||
import android.webkit.WebView
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.fonts.SystemFonts
|
||||
import android.graphics.fonts.Font
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.Permission
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
@@ -67,7 +74,11 @@ interface KeyDownInterceptor {
|
||||
fun interceptBackKey(enabled: Boolean)
|
||||
}
|
||||
|
||||
@TauriPlugin
|
||||
@TauriPlugin(
|
||||
permissions = [
|
||||
Permission(strings = [Manifest.permission.MANAGE_EXTERNAL_STORAGE], alias = "manageStorage")
|
||||
]
|
||||
)
|
||||
class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
private val implementation = NativeBridge()
|
||||
private var redirectScheme = "readest"
|
||||
@@ -75,6 +86,7 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
|
||||
companion object {
|
||||
var pendingInvoke: Invoke? = null
|
||||
private const val REQUEST_MANAGE_STORAGE = 1001
|
||||
}
|
||||
|
||||
override fun load(webView: WebView) {
|
||||
@@ -402,4 +414,53 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun request_manage_storage_permission(invoke: Invoke) {
|
||||
val ret = JSObject()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
intent.data = Uri.parse("package:${activity.packageName}")
|
||||
activity.startActivityForResult(intent, REQUEST_MANAGE_STORAGE)
|
||||
ret.put("manageStorage", "denied")
|
||||
invoke.resolve(ret)
|
||||
} catch (e: Exception) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||
activity.startActivity(intent)
|
||||
ret.put("manageStorage", "denied")
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
} else {
|
||||
ret.put("manageStorage", "granted")
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
} else {
|
||||
val readPermission = ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
val writePermission = ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
if (readPermission == PackageManager.PERMISSION_GRANTED &&
|
||||
writePermission == PackageManager.PERMISSION_GRANTED) {
|
||||
ret.put("manageStorage", "granted")
|
||||
invoke.resolve(ret)
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
),
|
||||
REQUEST_MANAGE_STORAGE
|
||||
)
|
||||
ret.put("manageStorage", "prompt")
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ const COMMANDS: &[&str] = &[
|
||||
"iap_restore_purchases",
|
||||
"get_system_color_scheme",
|
||||
"get_safe_area_insets",
|
||||
"request_manage_storage_permission",
|
||||
"checkPermissions",
|
||||
"requestPermissions",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-checkPermissions"
|
||||
description = "Enables the checkPermissions command without any pre-configured scope."
|
||||
commands.allow = ["checkPermissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-checkPermissions"
|
||||
description = "Denies the checkPermissions command without any pre-configured scope."
|
||||
commands.deny = ["checkPermissions"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-requestPermissions"
|
||||
description = "Enables the requestPermissions command without any pre-configured scope."
|
||||
commands.allow = ["requestPermissions"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-requestPermissions"
|
||||
description = "Denies the requestPermissions command without any pre-configured scope."
|
||||
commands.deny = ["requestPermissions"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-request-manage-storage-permission"
|
||||
description = "Enables the request_manage_storage_permission command without any pre-configured scope."
|
||||
commands.allow = ["request_manage_storage_permission"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-request-manage-storage-permission"
|
||||
description = "Denies the request_manage_storage_permission command without any pre-configured scope."
|
||||
commands.deny = ["request_manage_storage_permission"]
|
||||
+81
@@ -20,6 +20,9 @@ Default permissions for the plugin
|
||||
- `allow-iap-restore-purchases`
|
||||
- `allow-get-system-color-scheme`
|
||||
- `allow-get-safe-area-insets`
|
||||
- `allow-request-manage-storage-permission`
|
||||
- `allow-checkPermissions`
|
||||
- `allow-requestPermissions`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -85,6 +88,32 @@ Denies the auth_with_safari command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-checkPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the checkPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-checkPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the checkPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-copy-uri-to-path`
|
||||
|
||||
</td>
|
||||
@@ -397,6 +426,58 @@ Denies the lock_screen_orientation command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-requestPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the requestPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-requestPermissions`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the requestPermissions command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-request-manage-storage-permission`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the request_manage_storage_permission command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-request-manage-storage-permission`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the request_manage_storage_permission command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-set-system-ui-visibility`
|
||||
|
||||
</td>
|
||||
|
||||
@@ -17,4 +17,7 @@ permissions = [
|
||||
"allow-iap-restore-purchases",
|
||||
"allow-get-system-color-scheme",
|
||||
"allow-get-safe-area-insets",
|
||||
"allow-request-manage-storage-permission",
|
||||
"allow-checkPermissions",
|
||||
"allow-requestPermissions",
|
||||
]
|
||||
|
||||
+38
-2
@@ -318,6 +318,18 @@
|
||||
"const": "deny-auth-with-safari",
|
||||
"markdownDescription": "Denies the auth_with_safari command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the checkPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-checkPermissions",
|
||||
"markdownDescription": "Enables the checkPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the checkPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-checkPermissions",
|
||||
"markdownDescription": "Denies the checkPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the copy_uri_to_path command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -462,6 +474,30 @@
|
||||
"const": "deny-lock-screen-orientation",
|
||||
"markdownDescription": "Denies the lock_screen_orientation command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the requestPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-requestPermissions",
|
||||
"markdownDescription": "Enables the requestPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the requestPermissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-requestPermissions",
|
||||
"markdownDescription": "Denies the requestPermissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_manage_storage_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-request-manage-storage-permission",
|
||||
"markdownDescription": "Enables the request_manage_storage_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_manage_storage_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-request-manage-storage-permission",
|
||||
"markdownDescription": "Denies the request_manage_storage_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_system_ui_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -487,10 +523,10 @@
|
||||
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`",
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`"
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -126,3 +126,10 @@ pub(crate) async fn get_safe_area_insets<R: Runtime>(
|
||||
) -> Result<GetSafeAreaInsetsResponse> {
|
||||
app.native_bridge().get_safe_area_insets()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn request_manage_storage_permission<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<RequestManageStoragePermissionResponse> {
|
||||
app.native_bridge().request_manage_storage_permission()
|
||||
}
|
||||
|
||||
@@ -106,4 +106,10 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_safe_area_insets(&self) -> crate::Result<GetSafeAreaInsetsResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
) -> crate::Result<RequestManageStoragePermissionResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::iap_restore_purchases,
|
||||
commands::get_system_color_scheme,
|
||||
commands::get_safe_area_insets,
|
||||
commands::request_manage_storage_permission,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -169,3 +169,13 @@ impl<R: Runtime> NativeBridge<R> {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn request_manage_storage_permission(
|
||||
&self,
|
||||
) -> crate::Result<RequestManageStoragePermissionResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("request_manage_storage_permission", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,3 +167,9 @@ pub struct GetSafeAreaInsetsResponse {
|
||||
pub left: f64,
|
||||
pub right: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RequestManageStoragePermissionResponse {
|
||||
pub manage_storage: String, // "granted", "denied", or "prompt"
|
||||
}
|
||||
|
||||
+2
-2
@@ -150,10 +150,10 @@ class MediaForegroundService : Service() {
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("TTS Media Session")
|
||||
.setContentText("Text-to-speech playback active")
|
||||
.setSmallIcon(android.R.drawable.ic_media_play) // Replace with your icon
|
||||
.setSmallIcon(android.R.drawable.ic_media_play)
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.build()
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
RiErrorWarningFill,
|
||||
RiLoader2Line,
|
||||
} from 'react-icons/ri';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { documentDir, join } from '@tauri-apps/api/path';
|
||||
import { invoke, PermissionState } from '@tauri-apps/api/core';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -40,6 +41,10 @@ interface MigrationProgress {
|
||||
currentFile?: string;
|
||||
}
|
||||
|
||||
interface Permissions {
|
||||
manageStorage: PermissionState;
|
||||
}
|
||||
|
||||
export const MigrateDataWindow = () => {
|
||||
const _ = useTranslation();
|
||||
const { appService, envConfig } = useEnv();
|
||||
@@ -56,12 +61,14 @@ export const MigrateDataWindow = () => {
|
||||
const [filesToMigrate, setFilesToMigrate] = useState<FileItem[]>([]);
|
||||
const [currentDirFileCount, setCurrentDirFileCount] = useState('');
|
||||
const [currentDirFileSize, setCurrentDirFileSize] = useState(0);
|
||||
const [androidNewDirs, setAndroidNewDirs] = useState<{ path: string; label: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleCustomEvent = (event: CustomEvent) => {
|
||||
setIsOpen(event.detail.visible);
|
||||
if (event.detail.visible) {
|
||||
loadCurrentDataDir();
|
||||
loadAndroidDirs();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,6 +100,27 @@ export const MigrateDataWindow = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadAndroidDirs = async () => {
|
||||
const sdcardDirs = [
|
||||
{ path: '/storage/emulated/0', label: '/sdcard' },
|
||||
{ path: '/storage/emulated/0/Books', label: '/sdcard/Books' },
|
||||
{ path: '/storage/emulated/0/Documents', label: '/sdcard/Documents' },
|
||||
{ path: '/storage/emulated/0/Download', label: '/sdcard/Download' },
|
||||
];
|
||||
try {
|
||||
if (appService?.isAndroidApp) {
|
||||
const localDocumentDir = await documentDir();
|
||||
setAndroidNewDirs([
|
||||
// For Google Play version we won't request permission to access root of /sdcard
|
||||
...(appService?.distChannel === 'playstore' ? [] : sdcardDirs),
|
||||
{ path: localDocumentDir, label: '/sdcard/APPDATA/Documents' },
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading app local data directory:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectNewDir = async () => {
|
||||
setMigrationStatus('selecting');
|
||||
setErrorMessage('');
|
||||
@@ -117,6 +145,16 @@ export const MigrateDataWindow = () => {
|
||||
const handleSelectedNewDir = async (dir: string) => {
|
||||
setErrorMessage('');
|
||||
|
||||
if (!dir.includes('Android/data')) {
|
||||
let permission = await invoke<Permissions>('plugin:native-bridge|checkPermissions');
|
||||
if (permission.manageStorage !== 'granted') {
|
||||
permission = await invoke<Permissions>(
|
||||
'plugin:native-bridge|request_manage_storage_permission',
|
||||
);
|
||||
}
|
||||
if (permission.manageStorage !== 'granted') return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newDataDir = await join(dir, DATA_SUBDIR);
|
||||
await appService?.createDir(newDataDir, 'None', true);
|
||||
@@ -221,11 +259,6 @@ export const MigrateDataWindow = () => {
|
||||
const fileRevealLabel =
|
||||
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
|
||||
|
||||
const androidNewDirs = [
|
||||
{ path: '/storage/emulated/0/Documents' },
|
||||
{ path: '/storage/emulated/0/Download' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id='migrate_data_dir_window'
|
||||
@@ -299,9 +332,9 @@ export const MigrateDataWindow = () => {
|
||||
{androidNewDirs.map((dir) => (
|
||||
<MenuItem
|
||||
key={dir.path}
|
||||
toggled={newDataDir === dir.path}
|
||||
toggled={newDataDir.split(`/${DATA_SUBDIR}`)[0] === dir.path}
|
||||
transient
|
||||
label={dir.path}
|
||||
label={dir.label}
|
||||
onClick={() => handleSelectedNewDir(dir.path)}
|
||||
/>
|
||||
))}
|
||||
@@ -387,7 +420,7 @@ export const MigrateDataWindow = () => {
|
||||
<span className='font-medium'>{_('Migration failed')}</span>
|
||||
</div>
|
||||
<div className='bg-error/10 border-error/20 rounded-lg border p-3'>
|
||||
<p className='text-error/80 text-sm'>{errorMessage}</p>
|
||||
<p className='text-error/80 break-all text-sm'>{errorMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SystemSettings } from '@/types/settings';
|
||||
import {
|
||||
AppPlatform,
|
||||
AppService,
|
||||
DistChannel,
|
||||
FileItem,
|
||||
OsPlatform,
|
||||
ResolvedPath,
|
||||
@@ -86,7 +87,7 @@ export abstract class BaseAppService implements AppService {
|
||||
hasUpdater = false;
|
||||
hasOrientationLock = false;
|
||||
canCustomizeRootDir = false;
|
||||
distChannel = 'readest';
|
||||
distChannel = 'readest' as DistChannel;
|
||||
|
||||
protected abstract fs: FileSystem;
|
||||
protected abstract resolvePath(fp: string, base: BaseDir): ResolvedPath;
|
||||
|
||||
@@ -26,7 +26,14 @@ import {
|
||||
} from '@tauri-apps/api/path';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
|
||||
import { FileSystem, BaseDir, AppPlatform, ResolvedPath, FileItem } from '@/types/system';
|
||||
import {
|
||||
FileSystem,
|
||||
BaseDir,
|
||||
AppPlatform,
|
||||
ResolvedPath,
|
||||
FileItem,
|
||||
DistChannel,
|
||||
} from '@/types/system';
|
||||
import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { getDirPath, getFilename } from '@/utils/path';
|
||||
import { NativeFile, RemoteFile } from '@/utils/file';
|
||||
@@ -342,7 +349,7 @@ export class NativeAppService extends BaseAppService {
|
||||
override hasOrientationLock =
|
||||
(OS_TYPE === 'ios' && getOSPlatform() === 'ios') || OS_TYPE === 'android';
|
||||
override canCustomizeRootDir = OS_TYPE !== 'ios';
|
||||
override distChannel = process.env['NEXT_PUBLIC_DIST_CHANNEL'] || 'readest';
|
||||
override distChannel = (process.env['NEXT_PUBLIC_DIST_CHANNEL'] || 'readest') as DistChannel;
|
||||
|
||||
private execDir?: string = undefined;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export type OsPlatform = 'android' | 'ios' | 'macos' | 'windows' | 'linux' | 'un
|
||||
export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Fonts' | 'Log' | 'Cache' | 'Temp' | 'None';
|
||||
export type DeleteAction = 'cloud' | 'local' | 'both';
|
||||
export type SelectDirectoryMode = 'read' | 'write';
|
||||
export type DistChannel = 'readest' | 'playstore' | 'appstore' | 'unknown';
|
||||
|
||||
export type ResolvedPath = {
|
||||
baseDir: number;
|
||||
@@ -60,7 +61,7 @@ export interface AppService {
|
||||
isPortableApp: boolean;
|
||||
isDesktopApp: boolean;
|
||||
canCustomizeRootDir: boolean;
|
||||
distChannel: string;
|
||||
distChannel: DistChannel;
|
||||
|
||||
init(): Promise<void>;
|
||||
openFile(path: string, base: BaseDir): Promise<File>;
|
||||
|
||||
Reference in New Issue
Block a user