feat(mobile): upload queue ui and continuous capture page
Task 9 (P3 plan): queue tab replaces the stub, continuous capture page enqueues per shot, album detail gains an upload FAB, and the upload engine lifecycle is now bound to the login session. - Engine scope: session account drives UploadEngine start/stop; wired from app boot, login success, logout, and 401 eviction. Tasks stay in the store across logout (accountId hard isolation). - Queue tab: album x state x time filter bar, task rows with thumbnail, state chip, localized error copy, attempt count, per-row progress via a broadcast progress hub, state-dependent actions (re-pick retry, retryNow, cancel, delete), batch retry bar, 200ms-debounced live refresh from drift table updates. - Capture page: camera preview via injected CaptureSource, one enqueue per shutter press, persistent album/category/badges overlay with a switch-only-affects-new-shots hint, gallery multi-pick, exit confirm while uploads are pending, permission-denied and init-failure views. - POST_NOTIFICATIONS runtime permission via a small MethodChannel (photolive/permissions) + PermissionPlugin.kt: single runtime permission need, avoids permission_handler dependency; requested once on first enqueue, silent degradation when denied or unavailable. - Album detail FAB: capture-and-upload / pick-from-gallery sheet. - l10n: ~50 new keys in zh/en arbs; tests: +45 (filter matrix, engine scope lifecycle, permission gate, queue tab, capture page, FAB entry); existing page tests updated to inject session/task-store fakes. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -8,5 +8,16 @@ class MainActivity : FlutterActivity() {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
// 上传桥接(P3 Task 7):MethodChannel 命令 + EventChannel 事件流。
|
||||
UploadPlugin.register(applicationContext, flutterEngine)
|
||||
// 运行时权限桥接(P3 Task 9):POST_NOTIFICATIONS 请求。
|
||||
PermissionPlugin.register(this, flutterEngine)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray,
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
PermissionPlugin.onRequestPermissionsResult(requestCode, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.photolive.photo_live
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
/**
|
||||
* 运行时权限桥接(P3 Task 9):MethodChannel `photolive/permissions`。
|
||||
*
|
||||
* 当前仅一个方法:requestPostNotifications——Android 13+(TIRAMISU)的
|
||||
* POST_NOTIFICATIONS 运时请求(上传前台服务通知可见性;未授权时传输
|
||||
* 继续,Dart 侧静默降级)。SDK < 33 无需运行时请求,直接回 granted。
|
||||
*
|
||||
* 实现说明:App 全部运行时权限需求仅此一项,有意不引入
|
||||
* permission_handler 依赖(见 Dart 侧 notification_permission.dart 注释)。
|
||||
* 并发请求单飞:仅首个挂起,后续直接回 denied(Dart 侧本就单次语义)。
|
||||
*/
|
||||
object PermissionPlugin {
|
||||
|
||||
const val CHANNEL = "photolive/permissions"
|
||||
private const val METHOD_REQUEST_POST_NOTIFICATIONS = "requestPostNotifications"
|
||||
private const val REQUEST_CODE = 4101
|
||||
|
||||
private var pending: MethodChannel.Result? = null
|
||||
|
||||
fun register(activity: MainActivity, engine: FlutterEngine) {
|
||||
MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
METHOD_REQUEST_POST_NOTIFICATIONS ->
|
||||
handleRequestPostNotifications(activity, result)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRequestPostNotifications(
|
||||
activity: MainActivity,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
result.success("granted")
|
||||
return
|
||||
}
|
||||
val granted = ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
result.success("granted")
|
||||
return
|
||||
}
|
||||
if (pending != null) {
|
||||
// 已有请求在途:本 App 侧为单次语义,直接按未授权回包。
|
||||
result.success("denied")
|
||||
return
|
||||
}
|
||||
pending = result
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
REQUEST_CODE,
|
||||
)
|
||||
}
|
||||
|
||||
/** MainActivity.onRequestPermissionsResult 转发(回包挂起的首个请求)。 */
|
||||
fun onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
|
||||
if (requestCode != REQUEST_CODE) return
|
||||
val result = pending ?: return
|
||||
pending = null
|
||||
val granted = grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
result.success(if (granted) "granted" else "denied")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user