android: the phone, on the real core — the mockup shell, playback, the media card, the catalog, the resolver, Save, the dev flavour
The same Tauri 2 project builds for Android: mobile.html as a third Vite
entry (src/mobile/, the mockup on real data), tauri.android.conf.json, the
mobile stubs (updater, lyric alignment, vocal separation), rustls reqwest
on Android, the AAudio buffer, and a hand-driven detour build
(src-tauri/android/{env,build,deploy,dev}.sh) while Developer Mode is off.
ANDROID.md is the recipe.
Playback: the media-session plugin crate (lock-screen card, MediaStyle
notification, audio focus, headset buttons, a foreground service that
promotes itself first — Android 14 kills the app otherwise), session
restore across reload and restart, solid chrome after measuring the
backdrop-filter lag.
The catalog: Search is the desktop's top bar (the top-result rule, artist
cards), the artist page and the record page are the desktop's, Home is the
desktop's landing page, Gaps per followed shelf. Playing goes through the
desktop's own source rules, moved verbatim from state.svelte.js into
src/lib/sources.js so both apps share one resolver; the phone readies the
next three tracks and crosses boundaries gapless. Save fetches a record or
a track into the library by the download rules; Transfers shows the
burst. The raw Soulseek search stays, offered on request, a peer's copy
streaming with the lane rising as the bytes arrive. Community is the
Soulseek side (rooms, chat, buddies); the private layer waits.
The opener and storage plugins (social links, the picked library folder,
all-files access), the volume normalization switch, and the harness:
tests/mobile/smoke.mjs walks the mock (a fake network behind
src/lib/events.js) and the real phone over CDP, perf.mjs measures frames.
This commit is contained in:
1 parent
972dd103cb
commit
bb828ff2dc
102 files changed
+12258
-830
No files matched your search
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "tauri-plugin-storage"
|
||||
version = "0.1.0"
|
||||
description = "SoulStream's storage on the phone: the folder picker (a Storage Access Framework tree, resolved to a path when the volume allows), and Android's all-files access — checked and requested — so the library can live outside the app's private folder"
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
# required by tauri-plugin's build script; must equal the crate name
|
||||
links = "tauri-plugin-storage"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2", features = ["build"] }
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
@@ -0,0 +1,28 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.soulstream.storage"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
// the picker's ActivityResult
|
||||
implementation("androidx.activity:activity-ktx:1.9.3")
|
||||
implementation(project(":tauri-android"))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- A library folder in shared storage (the phone's Music folder): the
|
||||
Rust scanner and the download pipeline work on real paths, which
|
||||
under scoped storage needs all-files access. Declared here; granted
|
||||
only by the user, on the system screen `request_all_files_access`
|
||||
opens. Nothing is read outside the folder the user picked. -->
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<!-- reading media by path on Android 13+, and the older storage read on
|
||||
Android 12 and below -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
</manifest>
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.soulstream.storage
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.ActivityResult
|
||||
import app.tauri.annotation.ActivityCallback
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
|
||||
/**
|
||||
* The phone's storage for the library: a folder picked through Android's
|
||||
* document tree picker (its content URI kept with a persistable grant, and
|
||||
* resolved to a real path when the tree is on a volume that has one), and
|
||||
* all-files access — the permission that lets the Rust side read and write
|
||||
* that path under scoped storage — checked, and requested on the system
|
||||
* screen that grants it.
|
||||
*/
|
||||
@TauriPlugin
|
||||
class StoragePlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
@Command
|
||||
fun pickFolder(invoke: Invoke) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
intent.addFlags(
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
)
|
||||
startActivityForResult(invoke, intent, "folderPicked")
|
||||
}
|
||||
|
||||
@ActivityCallback
|
||||
fun folderPicked(invoke: Invoke, result: ActivityResult) {
|
||||
when (result.resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
val uri = result.data?.data
|
||||
if (uri == null) { invoke.reject("no folder came back"); return }
|
||||
try {
|
||||
activity.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
} catch (_: Exception) { /* the grant is a courtesy; the path is what counts */ }
|
||||
val out = JSObject()
|
||||
out.put("uri", uri.toString())
|
||||
out.put("path", pathOf(uri))
|
||||
invoke.resolve(out)
|
||||
}
|
||||
Activity.RESULT_CANCELED -> invoke.reject("cancelled")
|
||||
else -> invoke.reject("the picker failed")
|
||||
}
|
||||
}
|
||||
|
||||
/** "primary:Music/Flac" → /storage/emulated/0/Music/Flac; an SD card's
|
||||
* "1234-ABCD:Music" → /storage/1234-ABCD/Music; anything else (a cloud
|
||||
* provider's tree) has no path. */
|
||||
private fun pathOf(uri: Uri): String? {
|
||||
val id = try { DocumentsContract.getTreeDocumentId(uri) } catch (_: Exception) { return null }
|
||||
val i = id.indexOf(':')
|
||||
if (i < 0) return null
|
||||
val volume = id.substring(0, i)
|
||||
val rel = id.substring(i + 1).trimEnd('/')
|
||||
if (uri.authority != "com.android.externalstorage.documents") return null
|
||||
val root = if (volume == "primary") Environment.getExternalStorageDirectory().absolutePath else "/storage/$volume"
|
||||
return if (rel.isEmpty()) root else "$root/$rel"
|
||||
}
|
||||
|
||||
@Command
|
||||
fun allFilesAccess(invoke: Invoke) {
|
||||
val out = JSObject()
|
||||
out.put("granted", Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager())
|
||||
invoke.resolve(out)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun requestAllFilesAccess(invoke: Invoke) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
||||
Uri.parse("package:" + activity.packageName)
|
||||
)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
invoke.resolve()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// The three commands, as the Rust functions are named; the ACL slugs them
|
||||
// (`allow-pick-folder`, …) into permissions/autogenerated.
|
||||
const COMMANDS: &[&str] = &["pick_folder", "all_files_access", "request_all_files_access"];
|
||||
|
||||
fn main() {
|
||||
// Registers the command permissions and, on Android, hands the Gradle
|
||||
// module under android/ to tauri-build, which includes it in the app
|
||||
// project (tauri.settings.gradle) — only with TAURI_ANDROID_PROJECT_PATH
|
||||
// set, see ANDROID.md.
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
.android_path("android")
|
||||
.build();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-all-files-access"
|
||||
description = "Enables the all_files_access command without any pre-configured scope."
|
||||
commands.allow = ["all_files_access"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-all-files-access"
|
||||
description = "Denies the all_files_access command without any pre-configured scope."
|
||||
commands.deny = ["all_files_access"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-pick-folder"
|
||||
description = "Enables the pick_folder command without any pre-configured scope."
|
||||
commands.allow = ["pick_folder"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-pick-folder"
|
||||
description = "Denies the pick_folder command without any pre-configured scope."
|
||||
commands.deny = ["pick_folder"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-request-all-files-access"
|
||||
description = "Enables the request_all_files_access command without any pre-configured scope."
|
||||
commands.allow = ["request_all_files_access"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-request-all-files-access"
|
||||
description = "Denies the request_all_files_access command without any pre-configured scope."
|
||||
commands.deny = ["request_all_files_access"]
|
||||
@@ -0,0 +1,99 @@
|
||||
## Default Permission
|
||||
|
||||
The phone's storage: pick a library folder (a Storage Access Framework tree,
|
||||
resolved to a path when the volume allows), and Android's all-files access —
|
||||
whether it is granted, and the system screen that grants it.
|
||||
|
||||
#### This default permission set includes the following:
|
||||
|
||||
- `allow-pick-folder`
|
||||
- `allow-all-files-access`
|
||||
- `allow-request-all-files-access`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`storage:allow-all-files-access`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the all_files_access command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`storage:deny-all-files-access`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the all_files_access command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`storage:allow-pick-folder`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the pick_folder command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`storage:deny-pick-folder`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the pick_folder command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`storage:allow-request-all-files-access`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the request_all_files_access command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`storage:deny-request-all-files-access`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the request_all_files_access command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,9 @@
|
||||
"$schema" = "schemas/schema.json"
|
||||
|
||||
[default]
|
||||
description = """
|
||||
The phone's storage: pick a library folder (a Storage Access Framework tree,
|
||||
resolved to a path when the volume allows), and Android's all-files access —
|
||||
whether it is granted, and the system screen that grants it.
|
||||
"""
|
||||
permissions = ["allow-pick-folder", "allow-all-files-access", "allow-request-all-files-access"]
|
||||
@@ -0,0 +1,342 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PermissionFile",
|
||||
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"description": "The default permission set for the plugin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DefaultPermission"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"set": {
|
||||
"description": "A list of permissions sets defined",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSet"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
"description": "Allowed or denied commands when using this permission.",
|
||||
"default": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Commands"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Allowed or denied scoped when using this permission.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Scopes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Data that defines what is allowed by the scope.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Value": {
|
||||
"description": "All supported ACL values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents a null JSON value.",
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`bool`].",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"description": "Represents a valid ACL [`Number`].",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`String`].",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Represents a list of other [`Value`]s.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Number": {
|
||||
"description": "A valid ACL number.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents an [`i64`].",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`f64`].",
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Target": {
|
||||
"description": "Platform target.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the all_files_access command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-all-files-access",
|
||||
"markdownDescription": "Enables the all_files_access command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the all_files_access command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-all-files-access",
|
||||
"markdownDescription": "Denies the all_files_access command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the pick_folder command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-pick-folder",
|
||||
"markdownDescription": "Enables the pick_folder command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the pick_folder command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-pick-folder",
|
||||
"markdownDescription": "Denies the pick_folder command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_all_files_access command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-request-all-files-access",
|
||||
"markdownDescription": "Enables the request_all_files_access command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_all_files_access command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-request-all-files-access",
|
||||
"markdownDescription": "Denies the request_all_files_access command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "The phone's storage: pick a library folder (a Storage Access Framework tree,\nresolved to a path when the volume allows), and Android's all-files access —\nwhether it is granted, and the system screen that grants it.\n\n#### This default permission set includes:\n\n- `allow-pick-folder`\n- `allow-all-files-access`\n- `allow-request-all-files-access`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "The phone's storage: pick a library folder (a Storage Access Framework tree,\nresolved to a path when the volume allows), and Android's all-files access —\nwhether it is granted, and the system screen that grants it.\n\n#### This default permission set includes:\n\n- `allow-pick-folder`\n- `allow-all-files-access`\n- `allow-request-all-files-access`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
//! Storage on the phone.
|
||||
//!
|
||||
//! The library on Android lives in the app's private folder unless the user
|
||||
//! points it somewhere else. Pointing it somewhere else needs two things the
|
||||
//! WebView cannot do: a folder picker (Android's document tree picker, whose
|
||||
//! answer is a content URI — resolved here to a real path when the volume
|
||||
//! allows, which the Rust scanner and the download pipeline work on), and
|
||||
//! for a folder in shared storage, Android's all-files access, which only
|
||||
//! the user can grant on a system screen.
|
||||
//!
|
||||
//! Three commands: `pick_folder` → `{ uri, path }` (path null when the tree
|
||||
//! is not on a volume with a path), `all_files_access` → `{ granted }`, and
|
||||
//! `request_all_files_access`, which opens the system screen. On the desktop
|
||||
//! the plugin is registered so the permission set resolves, and every
|
||||
//! command answers "not here".
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Runtime,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PickedFolder {
|
||||
pub uri: String,
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccessState {
|
||||
pub granted: bool,
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub struct Storage<R: Runtime>(tauri::plugin::PluginHandle<R>);
|
||||
|
||||
#[tauri::command]
|
||||
async fn pick_folder<R: Runtime>(app: tauri::AppHandle<R>) -> Result<PickedFolder, String> {
|
||||
#[cfg(mobile)]
|
||||
{
|
||||
use tauri::Manager;
|
||||
let storage = app.state::<Storage<R>>();
|
||||
storage
|
||||
.0
|
||||
.run_mobile_plugin::<PickedFolder>("pickFolder", ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
#[cfg(not(mobile))]
|
||||
{
|
||||
let _ = app;
|
||||
Err("the folder picker here is the desktop's dialog".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn all_files_access<R: Runtime>(app: tauri::AppHandle<R>) -> Result<AccessState, String> {
|
||||
#[cfg(mobile)]
|
||||
{
|
||||
use tauri::Manager;
|
||||
let storage = app.state::<Storage<R>>();
|
||||
storage
|
||||
.0
|
||||
.run_mobile_plugin::<AccessState>("allFilesAccess", ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
#[cfg(not(mobile))]
|
||||
{
|
||||
let _ = app;
|
||||
Ok(AccessState { granted: true })
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn request_all_files_access<R: Runtime>(app: tauri::AppHandle<R>) -> Result<(), String> {
|
||||
#[cfg(mobile)]
|
||||
{
|
||||
use tauri::Manager;
|
||||
let storage = app.state::<Storage<R>>();
|
||||
storage
|
||||
.0
|
||||
.run_mobile_plugin::<serde_json::Value>("requestAllFilesAccess", ())
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
#[cfg(not(mobile))]
|
||||
{
|
||||
let _ = app;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("storage")
|
||||
.invoke_handler(tauri::generate_handler![pick_folder, all_files_access, request_all_files_access])
|
||||
.setup(|app, api| {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
use tauri::Manager;
|
||||
let handle = api.register_android_plugin("com.soulstream.storage", "StoragePlugin")?;
|
||||
app.manage(Storage(handle));
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = (app, api);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
Reference in new issue
Block a user