android: playlists, the import doors, the share sheet, You → Sources — and three bugs the walks found

The desktop's two playlist pages on the phone: the shelf (pinned first,
mosaic covers, badges, New playlist, Import) and the page (kind, Pinned
chip, description, the meta line's warnings, Play / Shuffle / more, a
filter, rows tagged unresolved / set audio / file missing). Sheets for a
card, the page's overflow, a row, pick-a-playlist, a prompt and a confirm;
Add to playlist on every track and record sheet; a removal undoable for
seven seconds from the neutral toast (notice.svelte.js); stream rows play
through the resolver as path-less entries, a set slice starts and ends at
its cues; Add to library / Download all tracks with the desktop's bulk
rules; nav.replace for a review that became the thing it made.

The import doors that need no file — a link (YouTube · Spotify · Apple
Music · TIDAL · Deezer), a pasted tracklist, a YouTube DJ set — with the
review screen and the desktop's catalog verification pass. A share plugin
crate (crates/tauri-plugin-share): Share → SoulStream from any app keeps
the text in a ShareActivity and the WebView hands it to the import when it
comes back to the front. You → Sources: the source-quality window and the
priority per mode.

Found by the walks and fixed: set_settings takes camelCase arguments, so
the phone's normalization and library-folder saves never landed;
Cover.svelte's non-eager wrapper has no layout box, so its intersection
observer never fired and row thumbnails never loaded (the Artists tab too);
a history push while a history.go was still in flight left the nav mirror
one entry ahead and the page blank after a confirmed delete — the mirror
now applies once per microtask and never pushes during a pending traversal.

The harness names unmocked commands, carries console warnings' frames (the
smoke bundle is built unminified), probes the history mirror on every step,
mocks app_status / play_local / waveform / lyrics_get / player_state and
applies settings patches, runs adb steps on the phone (the share intent),
and walks playlists, the imports, the share and the settings on both the
mock and the phone. ANDROID.md documents all of it.
This commit is contained in:
KeelrLeek committed 2026-09-13 17:56:52 -05:00
1 parent bb828ff2dc
commit 6e39dc43f3
38 files changed
+2383 -88

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
[package]
name = "tauri-plugin-share"
version = "0.1.0"
description = "SoulStream on the phone's share sheet: a link or a tracklist sent from another app lands in the playlist import"
edition = "2021"
rust-version = "1.77.2"
# required by tauri-plugin's build script; must equal the crate name
links = "tauri-plugin-share"
[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,26 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.soulstream.share"
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")
implementation(project(":tauri-android"))
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<!-- SoulStream on the share sheet: plain text — a playlist link, a
pasted tracklist — from any app. The activity keeps the text
for the WebView (pending_share), brings the app to the front and
is gone; it never shows a screen of its own. -->
<activity
android:name="com.soulstream.share.ShareActivity"
android:exported="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,27 @@
package com.soulstream.share
import android.app.Activity
import android.content.Intent
import android.os.Bundle
/**
* The share sheet's target. Takes the text (Spotify sends the link inside a
* sentence; the WebView finds the URL in it), keeps it for the plugin,
* brings the app's own task to the front through its launcher intent — the
* running one if there is one, so the page and its player are untouched —
* and finishes without ever drawing.
*/
class ShareActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val text = intent?.getStringExtra(Intent.EXTRA_TEXT)
?: intent?.getStringExtra(Intent.EXTRA_SUBJECT)
if (!text.isNullOrBlank()) SharePlugin.pending = text
val launch = packageManager.getLaunchIntentForPackage(packageName)
if (launch != null) {
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(launch)
}
finish()
}
}
@@ -0,0 +1,32 @@
package com.soulstream.share
import android.app.Activity
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
/**
* What another app shared to SoulStream, held until the WebView asks.
* ShareActivity puts it here (the two live in one process, so a static
* holds across the hand-off, a cold start included); `pendingShare` gives
* it out once — the second ask answers nothing.
*/
@TauriPlugin
class SharePlugin(private val activity: Activity) : Plugin(activity) {
companion object {
@Volatile
var pending: String? = null
}
@Command
fun pendingShare(invoke: Invoke) {
val out = JSObject()
val text = pending
pending = null
if (text != null) out.put("text", text)
invoke.resolve(out)
}
}
+13
View File
@@ -0,0 +1,13 @@
// The one command, as the Rust function is named; the ACL slugs it
// (`allow-pending-share`) into permissions/autogenerated.
const COMMANDS: &[&str] = &["pending_share"];
fn main() {
// Registers the command permission and, on Android, hands the Gradle
// module under android/ to tauri-build (only with
// TAURI_ANDROID_PROJECT_PATH set, see ANDROID.md). The module carries
// the ShareActivity and its intent filter, merged into the app's manifest.
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-pending-share"
description = "Enables the pending_share command without any pre-configured scope."
commands.allow = ["pending_share"]
[[permission]]
identifier = "deny-pending-share"
description = "Denies the pending_share command without any pre-configured scope."
commands.deny = ["pending_share"]
@@ -0,0 +1,44 @@
## Default Permission
The share sheet: what another app sent to SoulStream (a playlist link, a
pasted tracklist), read once by the import.
#### This default permission set includes the following:
- `allow-pending-share`
## Permission Table
<table>
<tr>
<th>Identifier</th>
<th>Description</th>
</tr>
<tr>
<td>
`share:allow-pending-share`
</td>
<td>
Enables the pending_share command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`share:deny-pending-share`
</td>
<td>
Denies the pending_share command without any pre-configured scope.
</td>
</tr>
</table>
@@ -0,0 +1,8 @@
"$schema" = "schemas/schema.json"
[default]
description = """
The share sheet: what another app sent to SoulStream (a playlist link, a
pasted tracklist), read once by the import.
"""
permissions = ["allow-pending-share"]
@@ -0,0 +1,318 @@
{
"$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 pending_share command without any pre-configured scope.",
"type": "string",
"const": "allow-pending-share",
"markdownDescription": "Enables the pending_share command without any pre-configured scope."
},
{
"description": "Denies the pending_share command without any pre-configured scope.",
"type": "string",
"const": "deny-pending-share",
"markdownDescription": "Denies the pending_share command without any pre-configured scope."
},
{
"description": "The share sheet: what another app sent to SoulStream (a playlist link, a\npasted tracklist), read once by the import.\n\n#### This default permission set includes:\n\n- `allow-pending-share`",
"type": "string",
"const": "default",
"markdownDescription": "The share sheet: what another app sent to SoulStream (a playlist link, a\npasted tracklist), read once by the import.\n\n#### This default permission set includes:\n\n- `allow-pending-share`"
}
]
}
}
}
+61
View File
@@ -0,0 +1,61 @@
//! The share sheet, on the phone.
//!
//! Another app's Share → SoulStream (a Spotify playlist, a YouTube set, a
//! pasted tracklist) starts the plugin's `ShareActivity`, which keeps the
//! text and brings the app to the front. The WebView asks for it with
//! `pending_share` → `{ text }` — at boot, and whenever the app comes back
//! to the front — and hands it to the playlist import. Read once: the
//! second ask answers nothing. On the desktop the plugin is registered so
//! the permission set resolves, and the command answers nothing.
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime,
};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Shared {
pub text: Option<String>,
}
#[cfg(mobile)]
pub struct Share<R: Runtime>(tauri::plugin::PluginHandle<R>);
#[tauri::command]
fn pending_share<R: Runtime>(app: tauri::AppHandle<R>) -> Result<Shared, String> {
#[cfg(mobile)]
{
use tauri::Manager;
let share = app.state::<Share<R>>();
share
.0
.run_mobile_plugin::<Shared>("pendingShare", ())
.map_err(|e| e.to_string())
}
#[cfg(not(mobile))]
{
let _ = app;
Ok(Shared::default())
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("share")
.invoke_handler(tauri::generate_handler![pending_share])
.setup(|app, api| {
#[cfg(target_os = "android")]
{
use tauri::Manager;
let handle = api.register_android_plugin("com.soulstream.share", "SharePlugin")?;
app.manage(Share(handle));
}
#[cfg(not(target_os = "android"))]
{
let _ = (app, api);
}
Ok(())
})
.build()
}