fix: improve Android media folder integration

This commit is contained in:
wolfmanster 2026-08-19 21:37:27 +08:00
parent 4c04a34fda
commit 2235546d56
9 changed files with 61 additions and 4 deletions

View file

@ -31,7 +31,21 @@ android {
kotlinOptions { jvmTarget = "1.8" }
sourceSets["main"].assets.srcDir("../../../apps/client/dist")
val clientDist = file("../../../apps/client/dist")
val webAssets = layout.buildDirectory.dir("generated/web-assets")
val prepareWebAssets = tasks.register<Sync>("prepareWebAssets") {
from(clientDist)
into(webAssets.map { it.dir("public") })
doFirst {
check(clientDist.isDirectory) {
"Client assets are missing. Run 'pnpm --dir apps/client build' first."
}
}
}
sourceSets["main"].assets.srcDir(webAssets)
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("Assets") }
.configureEach { dependsOn(prepareWebAssets) }
}
dependencies {

View file

@ -42,6 +42,9 @@ interface ArchiveDao {
@Query("DELETE FROM media WHERE jobId NOT IN (SELECT id FROM jobs)")
suspend fun deleteOrphanMedia()
@Query("UPDATE jobs SET status = 'queued', progress = 0, error = NULL, updatedAt = :updatedAt WHERE status IN ('resolving', 'downloading')")
suspend fun requeueInterrupted(updatedAt: String): Int
@Transaction
suspend fun replaceMedia(jobId: String, media: List<MediaEntity>) {
deleteMedia(jobId)

View file

@ -2,6 +2,8 @@ package com.xmedia.archive.plugin
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
@ -71,6 +73,30 @@ class LocalArchivePlugin : Plugin() {
@PluginMethod
fun openMedia(call: PluginCall) = launchMediaIntent(call, Intent.ACTION_VIEW)
@PluginMethod
fun openMediaFolder(call: PluginCall) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
DocumentsContract.buildTreeDocumentUri(
"com.android.externalstorage.documents",
"primary:Download/X Media Archive",
),
)
}
}
runCatching {
context.startActivity(intent)
call.resolve()
}.onFailure { error ->
call.reject(error.message ?: "无法打开媒体文件夹")
}
}
@PluginMethod
fun shareMedia(call: PluginCall) = launchMediaIntent(call, Intent.ACTION_SEND)

View file

@ -52,6 +52,8 @@ class ArchiveRepository(context: Context) {
suspend fun update(job: JobEntity) = dao.upsertJob(job.copy(updatedAt = Instant.now().toString()))
suspend fun requeueInterrupted() = dao.requeueInterrupted(Instant.now().toString())
suspend fun replaceMedia(jobId: String, media: List<MediaEntity>) = dao.replaceMedia(jobId, media)
suspend fun clearHistory(): Int {

View file

@ -59,6 +59,7 @@ class DownloadForegroundService : Service() {
private suspend fun runQueue() {
try {
repository.requeueInterrupted()
while (true) {
val jobs = ArchiveDatabase.get(this).dao().listJobs().filter { it.status == JobStatus.QUEUED.name.lowercase() }
if (jobs.isEmpty()) break

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.XMediaArchive" parent="android:style/Theme.Material.Light.NoActionBar">
<style name="Theme.XMediaArchive" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:fontFamily">sans</item>
<item name="android:colorAccent">#F05A3B</item>
<item name="android:navigationBarColor">#F3F0E9</item>

View file

@ -142,7 +142,17 @@ function JobCard({ job, onAction }: { job: DownloadJob; onAction: (action: 'canc
<div className="job-actions">
{active && <button className="text-button danger-text" onClick={() => onAction('cancel', job.id)}><X size={15} />取消</button>}
{(job.status === 'failed' || job.status === 'canceled') && <button className="text-button" onClick={() => onAction('retry', job.id)}><RotateCcw size={14} />重试</button>}
{complete && <a className="text-button" href={job.media[0]?.downloadUrl}><ArrowDownToLine size={15} />{job.media.length > 1 ? '逐个下载' : '下载文件'}</a>}
{complete && <a
className="text-button"
href={job.media[0]?.downloadUrl}
onClick={(event) => {
const firstMedia = job.media[0];
if (firstMedia?.downloadUrl.startsWith('content:')) {
event.preventDefault();
void openMedia(firstMedia.id);
}
}}
><ArrowDownToLine size={15} />{job.media.length > 1 ? '逐个下载' : '下载文件'}</a>}
</div>
</footer>
</article>

View file

@ -44,7 +44,7 @@ export function clearHistory() {
}
export function openMediaFolder() {
if (native) return Promise.resolve({ ok: true as const });
if (native) return LocalArchive.openMediaFolder().then(() => ({ ok: true as const }));
return request<{ ok: true }>('/api/open-media-folder', { method: 'POST' });
}

View file

@ -13,6 +13,7 @@ export interface LocalArchivePlugin {
clearHistory(): Promise<{ removed: number }>;
getHealth(): Promise<{ ok: boolean; local: boolean }>;
openMedia(input: { id: string }): Promise<void>;
openMediaFolder(): Promise<void>;
shareMedia(input: { id: string }): Promise<void>;
addListener(eventName: 'jobsChanged', listenerFunc: (event: { jobs: DownloadJob[] }) => void): Promise<PluginListenerHandle>;
}