Compare commits
1 Commits
feat/windo
...
feat/dev_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b881cbf1a |
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -34,7 +34,3 @@ The `/api/something` endpoint is now `/api/something-else`
|
|||||||
- [ ] I have followed naming conventions/patterns in the surrounding code
|
- [ ] I have followed naming conventions/patterns in the surrounding code
|
||||||
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
|
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
|
||||||
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
|
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
|
||||||
|
|
||||||
## Please describe to which degree, if any, an LLM was used in creating this pull request.
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -138,7 +138,7 @@ jobs:
|
|||||||
name: Unit Test CLI (Windows)
|
name: Unit Test CLI (Windows)
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
||||||
runs-on: windows-2025
|
runs-on: windows-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
defaults:
|
defaults:
|
||||||
|
|||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dev.ts
|
||||||
228
dev.ts
Executable file
228
dev.ts
Executable file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
':' //; exec node --disable-warning=ExperimentalWarning --experimental-strip-types "$0" "$@"
|
||||||
|
':' /*
|
||||||
|
@echo off
|
||||||
|
node "%~dpnx0" %*
|
||||||
|
exit /b %errorlevel%
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync, type ExecSyncOptions, spawn } from 'node:child_process';
|
||||||
|
import { Dir, Dirent, existsSync, mkdirSync, opendirSync, readFileSync, rmSync } from 'node:fs';
|
||||||
|
import { platform } from 'node:os';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { parseArgs } from 'node:util';
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
const tryRun = <T>(fn: () => T, onSuccess?: (result: T) => void, onError?: (e: unknown) => void, onFinally?: (result: T | undefined) => void): T | void => {
|
||||||
|
let result: T | undefined= undefined;
|
||||||
|
try {
|
||||||
|
result = fn();
|
||||||
|
onSuccess?.(result);
|
||||||
|
return result;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
onError?.(e);
|
||||||
|
} finally {
|
||||||
|
onFinally?.(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const FALSE = () => false;
|
||||||
|
const exit0 = () => process.exit(0);
|
||||||
|
const exit1 = () => process.exit(1);
|
||||||
|
const log = (msg: string) => { console.log(msg); return msg; };
|
||||||
|
const err = (msg: string, e?: unknown) => { console.log(msg, e); return undefined; };
|
||||||
|
const errExit = (msg: string, e?: unknown) => ()=>{ console.log(msg, e); exit1(); };
|
||||||
|
|
||||||
|
|
||||||
|
const exec = (cmd: string, opts: ExecSyncOptions = { stdio: 'inherit' }) => execSync(cmd, opts);
|
||||||
|
|
||||||
|
const isWSL = () => platform() === 'linux' &&
|
||||||
|
tryRun(() => readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft'), undefined, FALSE);
|
||||||
|
|
||||||
|
const isWindows = () => platform() === 'win32';
|
||||||
|
const supportsChown = () => !isWindows() || isWSL();
|
||||||
|
|
||||||
|
const onExit = (handler: () => void) => {
|
||||||
|
['SIGINT', 'SIGTERM'].forEach(sig => process.on(sig, () => { handler(); exit0(); }));
|
||||||
|
if (isWindows()) process.on('SIGBREAK', () => { handler(); exit0(); });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Directory operations
|
||||||
|
const mkdirs = (dirs: string[]) => dirs.forEach(dir =>
|
||||||
|
tryRun(
|
||||||
|
() => mkdirSync(dir, { recursive: true }),
|
||||||
|
() => log(`Created directory: ${dir}`),
|
||||||
|
e => err(`Error creating directory ${dir}:`, e)
|
||||||
|
));
|
||||||
|
|
||||||
|
const chown = (dirs: string[], uid: string, gid: string) => {
|
||||||
|
if (!supportsChown()) {
|
||||||
|
log('Skipping ownership changes on Windows (not supported outside WSL)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const dir of dirs) {
|
||||||
|
tryRun(
|
||||||
|
() => exec(`chown -R ${uid}:${gid} "${dir}"`),
|
||||||
|
undefined,
|
||||||
|
errExit(`Permission denied when changing owner of volumes. Try running 'sudo ./dev.ts prepare-volumes' first.`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAndRemove = (path: string, target: string) => {
|
||||||
|
if (!existsSync(path)) return;
|
||||||
|
|
||||||
|
const removeLoop = (dir: Dir) => {
|
||||||
|
let dirent: Dirent | null;
|
||||||
|
while ((dirent = dir.readSync()) !== null) {
|
||||||
|
if (!dirent.isDirectory()) continue;
|
||||||
|
|
||||||
|
const itemPath = join(path, dirent.name);
|
||||||
|
if (dirent.name === target) {
|
||||||
|
log(` Removing: ${itemPath}`);
|
||||||
|
rmSync(itemPath, { recursive: true, force: true });
|
||||||
|
} else {
|
||||||
|
findAndRemove(itemPath, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tryRun(() => opendirSync(path), removeLoop, errExit( `Error opening directory ${path}`), (dir) => dir?.closeSync());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Docker DSL
|
||||||
|
const docker = {
|
||||||
|
compose: (file: string) => ({
|
||||||
|
up: (opts?: string[]) => spawn('docker', ['compose', '-f', file, 'up', ...(opts || [])], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env, COMPOSE_BAKE: 'true' },
|
||||||
|
shell: true
|
||||||
|
}),
|
||||||
|
down: () => tryRun(() => exec(`docker compose -f ${file} down --remove-orphans`))
|
||||||
|
}),
|
||||||
|
|
||||||
|
isAvailable: () => !!tryRun(() => exec('docker --version', { stdio: 'ignore' }), undefined, FALSE)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Environment configuration
|
||||||
|
const envConfig = {
|
||||||
|
volumeDirs: [
|
||||||
|
'./.pnpm-store', './web/.svelte-kit', './web/node_modules', './web/coverage',
|
||||||
|
'./e2e/node_modules', './docs/node_modules', './server/node_modules',
|
||||||
|
'./open-api/typescript-sdk/node_modules', './.github/node_modules',
|
||||||
|
'./node_modules', './cli/node_modules'
|
||||||
|
],
|
||||||
|
|
||||||
|
cleanDirs: ['node_modules', 'dist', 'build', '.svelte-kit', 'coverage', '.pnpm-store'],
|
||||||
|
|
||||||
|
composeFiles: {
|
||||||
|
dev: './docker/docker-compose.dev.yml',
|
||||||
|
e2e: './e2e/docker-compose.yml',
|
||||||
|
prod: './docker/docker-compose.prod.yml'
|
||||||
|
},
|
||||||
|
|
||||||
|
getEnv: () => ({
|
||||||
|
uid: process.env.UID || '1000',
|
||||||
|
gid: process.env.GID || '1000'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
const commands = {
|
||||||
|
'prepare-volumes': () => {
|
||||||
|
log('Preparing volumes...');
|
||||||
|
const { uid, gid } = envConfig.getEnv();
|
||||||
|
|
||||||
|
mkdirs(envConfig.volumeDirs);
|
||||||
|
chown(envConfig.volumeDirs, uid, gid);
|
||||||
|
|
||||||
|
// Handle UPLOAD_LOCATION
|
||||||
|
const uploadLocation = tryRun(() => {
|
||||||
|
const content = readFileSync('./docker/.env', 'utf-8');
|
||||||
|
const match = content.match(/^UPLOAD_LOCATION=(.+)$/m);
|
||||||
|
return match?.[1]?.trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uploadLocation) {
|
||||||
|
const targetPath = resolve('docker', uploadLocation);
|
||||||
|
mkdirs([targetPath]);
|
||||||
|
|
||||||
|
if (supportsChown()) {
|
||||||
|
tryRun(
|
||||||
|
() => {
|
||||||
|
// First chown the uploadLocation directory itself
|
||||||
|
exec(`chown ${uid}:${gid} "${targetPath}"`);
|
||||||
|
// Then chown all contents except postgres folder (using -prune to skip it entirely)
|
||||||
|
exec(`find "${targetPath}" -mindepth 1 -name postgres -prune -o -exec chown ${uid}:${gid} {} +`);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
errExit(`Permission denied when changing owner of volumes. Try running 'sudo ./dev.ts prepare-volumes' first.`)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log('Skipping ownership changes on Windows (not supported outside WSL)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Volume preparation completed.');
|
||||||
|
},
|
||||||
|
|
||||||
|
clean: () => {
|
||||||
|
log('Starting clean process...');
|
||||||
|
|
||||||
|
envConfig.cleanDirs.forEach(dir => {
|
||||||
|
log(`Removing ${dir} directories...`);
|
||||||
|
findAndRemove('.', dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
docker.isAvailable() &&
|
||||||
|
log('Stopping and removing Docker containers...') &&
|
||||||
|
docker.compose(envConfig.composeFiles.dev).down();
|
||||||
|
|
||||||
|
log('Clean process completed.');
|
||||||
|
},
|
||||||
|
|
||||||
|
down: (opts: { e2e?: boolean; prod?: boolean }) => {
|
||||||
|
const type = opts.prod ? 'prod' : opts.e2e ? 'e2e' : 'dev';
|
||||||
|
const file = envConfig.composeFiles[type];
|
||||||
|
|
||||||
|
log(`\nStopping ${type} environment...`);
|
||||||
|
docker.compose(file).down();
|
||||||
|
},
|
||||||
|
|
||||||
|
up: (opts: { e2e?: boolean; prod?: boolean }) => {
|
||||||
|
commands['prepare-volumes']();
|
||||||
|
|
||||||
|
const type = opts.prod ? 'prod' : opts.e2e ? 'e2e' : 'dev';
|
||||||
|
const file = envConfig.composeFiles[type];
|
||||||
|
const args = opts.prod ? ['--build', '-V', '--remove-orphans'] : ['--remove-orphans'];
|
||||||
|
|
||||||
|
onExit(() => commands.down(opts));
|
||||||
|
|
||||||
|
log(`Starting ${type} environment...`);
|
||||||
|
|
||||||
|
const proc = docker.compose(file).up(args);
|
||||||
|
proc.on('error',errExit('Failed to start docker compose:' ));
|
||||||
|
proc.on('exit', (code: number) => { commands.down(opts); code ? exit1() : exit0(); });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main
|
||||||
|
const { positionals, values } = parseArgs({
|
||||||
|
args: process.argv.slice(2),
|
||||||
|
allowPositionals: true,
|
||||||
|
options: {
|
||||||
|
e2e: { type: 'boolean', default: false },
|
||||||
|
prod: { type: 'boolean', default: false }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = positionals[0];
|
||||||
|
const handler = commands[command as keyof typeof commands];
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
|
log('Usage: ./dev.ts [clean|prepare-volumes|up [--e2e] [--prod]|down [--e2e] [--prod]]');
|
||||||
|
exit1();
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(values);
|
||||||
@@ -18,6 +18,7 @@ services:
|
|||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: ['immich-dev']
|
command: ['immich-dev']
|
||||||
image: immich-server-dev:latest
|
image: immich-server-dev:latest
|
||||||
|
pull_policy: never
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
@@ -80,6 +81,7 @@ services:
|
|||||||
immich-web:
|
immich-web:
|
||||||
container_name: immich_web
|
container_name: immich_web
|
||||||
image: immich-web-dev:latest
|
image: immich-web-dev:latest
|
||||||
|
pull_policy: never
|
||||||
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
# Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919
|
||||||
# user: 0:0
|
# user: 0:0
|
||||||
user: '${UID:-1000}:${GID:-1000}'
|
user: '${UID:-1000}:${GID:-1000}'
|
||||||
@@ -120,6 +122,7 @@ services:
|
|||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
image: immich-machine-learning-dev:latest
|
image: immich-machine-learning-dev:latest
|
||||||
|
pull_policy: never
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.ml.yml
|
# file: hwaccel.ml.yml
|
||||||
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference
|
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference
|
||||||
|
|||||||
@@ -61,8 +61,9 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
|
|||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface BackgroundWorkerFgHostApi {
|
interface BackgroundWorkerFgHostApi {
|
||||||
fun enable()
|
fun enableSyncWorker()
|
||||||
fun disable()
|
fun enableUploadWorker()
|
||||||
|
fun disableUploadWorker()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by BackgroundWorkerFgHostApi. */
|
/** The codec used by BackgroundWorkerFgHostApi. */
|
||||||
@@ -74,11 +75,11 @@ interface BackgroundWorkerFgHostApi {
|
|||||||
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
api.enable()
|
api.enableSyncWorker()
|
||||||
listOf(null)
|
listOf(null)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
@@ -90,11 +91,27 @@ interface BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
api.disable()
|
api.enableUploadWorker()
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.disableUploadWorker()
|
||||||
listOf(null)
|
listOf(null)
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
BackgroundWorkerPigeonUtils.wrapError(exception)
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
@@ -165,6 +182,23 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
|
|||||||
BackgroundWorkerPigeonCodec()
|
BackgroundWorkerPigeonCodec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun onLocalSync(maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||||
|
{
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$separatedMessageChannelSuffix"
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
|
channel.send(listOf(maxSecondsArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result<Unit>) -> Unit)
|
||||||
{
|
{
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ import io.flutter.embedding.engine.loader.FlutterLoader
|
|||||||
|
|
||||||
private const val TAG = "BackgroundWorker"
|
private const val TAG = "BackgroundWorker"
|
||||||
|
|
||||||
|
enum class BackgroundTaskType {
|
||||||
|
LOCAL_SYNC,
|
||||||
|
UPLOAD,
|
||||||
|
}
|
||||||
|
|
||||||
class BackgroundWorker(context: Context, params: WorkerParameters) :
|
class BackgroundWorker(context: Context, params: WorkerParameters) :
|
||||||
ListenableWorker(context, params), BackgroundWorkerBgHostApi {
|
ListenableWorker(context, params), BackgroundWorkerBgHostApi {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
@@ -79,7 +84,13 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
|
|||||||
* This method acts as a bridge between the native Android background task system and Flutter.
|
* This method acts as a bridge between the native Android background task system and Flutter.
|
||||||
*/
|
*/
|
||||||
override fun onInitialized() {
|
override fun onInitialized() {
|
||||||
flutterApi?.onAndroidUpload { handleHostResult(it) }
|
val taskTypeIndex = inputData.getInt(BackgroundWorkerApiImpl.WORKER_DATA_TASK_TYPE, 0)
|
||||||
|
val taskType = BackgroundTaskType.entries[taskTypeIndex]
|
||||||
|
|
||||||
|
when (taskType) {
|
||||||
|
BackgroundTaskType.LOCAL_SYNC -> flutterApi?.onLocalSync(null) { handleHostResult(it) }
|
||||||
|
BackgroundTaskType.UPLOAD -> flutterApi?.onAndroidUpload { handleHostResult(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package app.alextran.immich.background
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
|
import androidx.work.Data
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
@@ -14,13 +16,18 @@ private const val TAG = "BackgroundUploadImpl"
|
|||||||
|
|
||||||
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
override fun enableSyncWorker() {
|
||||||
override fun enable() {
|
|
||||||
enqueueMediaObserver(ctx)
|
enqueueMediaObserver(ctx)
|
||||||
|
Log.i(TAG, "Scheduled media observer")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disable() {
|
override fun enableUploadWorker() {
|
||||||
WorkManager.getInstance(ctx).cancelUniqueWork(OBSERVER_WORKER_NAME)
|
updateUploadEnabled(ctx, true)
|
||||||
|
Log.i(TAG, "Scheduled background upload tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableUploadWorker() {
|
||||||
|
updateUploadEnabled(ctx, false)
|
||||||
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
||||||
Log.i(TAG, "Cancelled background upload tasks")
|
Log.i(TAG, "Cancelled background upload tasks")
|
||||||
}
|
}
|
||||||
@@ -29,14 +36,25 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
||||||
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
||||||
|
|
||||||
|
const val WORKER_DATA_TASK_TYPE = "taskType"
|
||||||
|
|
||||||
|
const val SHARED_PREF_NAME = "Immich::Background"
|
||||||
|
const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled"
|
||||||
|
|
||||||
|
private fun updateUploadEnabled(context: Context, enabled: Boolean) {
|
||||||
|
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit {
|
||||||
|
putBoolean(SHARED_PREF_BACKUP_ENABLED, enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun enqueueMediaObserver(ctx: Context) {
|
fun enqueueMediaObserver(ctx: Context) {
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
|
||||||
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
|
||||||
.setTriggerContentUpdateDelay(30, TimeUnit.SECONDS)
|
.setTriggerContentUpdateDelay(5, TimeUnit.SECONDS)
|
||||||
.setTriggerContentMaxDelay(3, TimeUnit.MINUTES)
|
.setTriggerContentMaxDelay(1, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
||||||
@@ -48,13 +66,15 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
|||||||
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
|
Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enqueueBackgroundWorker(ctx: Context) {
|
fun enqueueBackgroundWorker(ctx: Context, taskType: BackgroundTaskType) {
|
||||||
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
||||||
|
|
||||||
|
val data = Data.Builder()
|
||||||
|
data.putInt(WORKER_DATA_TASK_TYPE, taskType.ordinal)
|
||||||
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
||||||
.build()
|
.setInputData(data.build()).build()
|
||||||
WorkManager.getInstance(ctx)
|
WorkManager.getInstance(ctx)
|
||||||
.enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)
|
.enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work)
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,29 @@ import androidx.work.Worker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
|
||||||
class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) {
|
class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
Log.i("MediaObserver", "Content change detected, starting background worker")
|
Log.i("MediaObserver", "Content change detected, starting background worker")
|
||||||
// Re-enqueue itself to listen for future changes
|
|
||||||
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
|
|
||||||
|
|
||||||
// Enqueue backup worker only if there are new media changes
|
// Enqueue backup worker only if there are new media changes
|
||||||
if (triggeredContentUris.isNotEmpty()) {
|
if (triggeredContentUris.isNotEmpty()) {
|
||||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
|
val type =
|
||||||
|
if (isBackupEnabled(ctx)) BackgroundTaskType.UPLOAD else BackgroundTaskType.LOCAL_SYNC
|
||||||
|
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enqueue itself to listen for future changes
|
||||||
|
BackgroundWorkerApiImpl.enqueueMediaObserver(ctx)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBackupEnabled(context: Context): Boolean {
|
||||||
|
val prefs =
|
||||||
|
context.getSharedPreferences(
|
||||||
|
BackgroundWorkerApiImpl.SHARED_PREF_NAME,
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
return prefs.getBoolean(BackgroundWorkerApiImpl.SHARED_PREF_BACKUP_ENABLED, false)
|
||||||
}
|
}
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,9 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda
|
|||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol BackgroundWorkerFgHostApi {
|
protocol BackgroundWorkerFgHostApi {
|
||||||
func enable() throws
|
func enableSyncWorker() throws
|
||||||
func disable() throws
|
func enableUploadWorker() throws
|
||||||
|
func disableUploadWorker() throws
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -83,31 +84,44 @@ class BackgroundWorkerFgHostApiSetup {
|
|||||||
/// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`.
|
/// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`.
|
||||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") {
|
||||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let enableSyncWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
enableChannel.setMessageHandler { _, reply in
|
enableSyncWorkerChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
try api.enable()
|
try api.enableSyncWorker()
|
||||||
reply(wrapResult(nil))
|
reply(wrapResult(nil))
|
||||||
} catch {
|
} catch {
|
||||||
reply(wrapError(error))
|
reply(wrapError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enableChannel.setMessageHandler(nil)
|
enableSyncWorkerChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
disableChannel.setMessageHandler { _, reply in
|
enableUploadWorkerChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
try api.disable()
|
try api.enableUploadWorker()
|
||||||
reply(wrapResult(nil))
|
reply(wrapResult(nil))
|
||||||
} catch {
|
} catch {
|
||||||
reply(wrapError(error))
|
reply(wrapError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
disableChannel.setMessageHandler(nil)
|
enableUploadWorkerChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let disableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
disableUploadWorkerChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
try api.disableUploadWorker()
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disableUploadWorkerChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +167,7 @@ class BackgroundWorkerBgHostApiSetup {
|
|||||||
}
|
}
|
||||||
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
||||||
protocol BackgroundWorkerFlutterApiProtocol {
|
protocol BackgroundWorkerFlutterApiProtocol {
|
||||||
|
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
|
||||||
@@ -167,6 +182,24 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
|
|||||||
var codec: BackgroundWorkerPigeonCodec {
|
var codec: BackgroundWorkerPigeonCodec {
|
||||||
return BackgroundWorkerPigeonCodec.shared
|
return BackgroundWorkerPigeonCodec.shared
|
||||||
}
|
}
|
||||||
|
func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
||||||
|
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync\(messageChannelSuffix)"
|
||||||
|
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
channel.sendMessage([maxSecondsArg] as [Any?]) { response in
|
||||||
|
guard let listResponse = response as? [Any?] else {
|
||||||
|
completion(.failure(createConnectionError(withChannelName: channelName)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if listResponse.count > 1 {
|
||||||
|
let code: String = listResponse[0] as! String
|
||||||
|
let message: String? = nilOrValue(listResponse[1])
|
||||||
|
let details: String? = nilOrValue(listResponse[2])
|
||||||
|
completion(.failure(PigeonError(code: code, message: message, details: details)))
|
||||||
|
} else {
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
|
||||||
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)"
|
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)"
|
||||||
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
enum BackgroundTaskType { case refresh, processing }
|
enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* DEBUG: Testing Background Tasks in Xcode
|
* DEBUG: Testing Background Tasks in Xcode
|
||||||
@@ -10,6 +10,10 @@ enum BackgroundTaskType { case refresh, processing }
|
|||||||
* 1. Pause the application in Xcode debugger
|
* 1. Pause the application in Xcode debugger
|
||||||
* 2. In the debugger console, enter one of the following commands:
|
* 2. In the debugger console, enter one of the following commands:
|
||||||
|
|
||||||
|
## For local sync (short-running sync):
|
||||||
|
|
||||||
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
||||||
|
|
||||||
## For background refresh (short-running sync):
|
## For background refresh (short-running sync):
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
||||||
@@ -20,6 +24,8 @@ enum BackgroundTaskType { case refresh, processing }
|
|||||||
|
|
||||||
* To simulate task expiration (useful for testing expiration handlers):
|
* To simulate task expiration (useful for testing expiration handlers):
|
||||||
|
|
||||||
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
||||||
|
|
||||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
||||||
@@ -114,9 +120,17 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
* This method acts as a bridge between the native iOS background task system and Flutter.
|
* This method acts as a bridge between the native iOS background task system and Flutter.
|
||||||
*/
|
*/
|
||||||
func onInitialized() throws {
|
func onInitialized() throws {
|
||||||
flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
switch self.taskType {
|
||||||
self.handleHostResult(result: result)
|
case .refreshUpload, .processingUpload:
|
||||||
})
|
flutterApi?.onIosUpload(isRefresh: self.taskType == .refreshUpload,
|
||||||
|
maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||||
|
self.handleHostResult(result: result)
|
||||||
|
})
|
||||||
|
case .localSync:
|
||||||
|
flutterApi?.onLocalSync(maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
||||||
|
self.handleHostResult(result: result)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,10 +177,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|||||||
* - Parameter success: Indicates whether the background task completed successfully
|
* - Parameter success: Indicates whether the background task completed successfully
|
||||||
*/
|
*/
|
||||||
private func complete(success: Bool) {
|
private func complete(success: Bool) {
|
||||||
if(isComplete) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isComplete = true
|
isComplete = true
|
||||||
engine.destroyContext()
|
engine.destroyContext()
|
||||||
completionHandler(success)
|
completionHandler(success)
|
||||||
|
|||||||
@@ -1,40 +1,77 @@
|
|||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
|
|
||||||
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||||
|
func enableSyncWorker() throws {
|
||||||
func enable() throws {
|
BackgroundWorkerApiImpl.scheduleLocalSync()
|
||||||
BackgroundWorkerApiImpl.scheduleRefreshWorker()
|
print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled")
|
||||||
BackgroundWorkerApiImpl.scheduleProcessingWorker()
|
|
||||||
print("BackgroundUploadImpl:enbale Background worker scheduled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func disable() throws {
|
func enableUploadWorker() throws {
|
||||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
|
BackgroundWorkerApiImpl.updateUploadEnabled(true)
|
||||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
|
|
||||||
print("BackgroundUploadImpl:disableUploadWorker Disabled background workers")
|
BackgroundWorkerApiImpl.scheduleRefreshUpload()
|
||||||
|
BackgroundWorkerApiImpl.scheduleProcessingUpload()
|
||||||
|
print("BackgroundUploadImpl:enableUploadWorker Scheduled background upload tasks")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"
|
func disableUploadWorker() throws {
|
||||||
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
|
BackgroundWorkerApiImpl.updateUploadEnabled(false)
|
||||||
|
BackgroundWorkerApiImpl.cancelUploadTasks()
|
||||||
|
print("BackgroundUploadImpl:disableUploadWorker Disabled background upload tasks")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static let backgroundUploadEnabledKey = "immich:background:backup:enabled"
|
||||||
|
|
||||||
|
private static let localSyncTaskID = "app.alextran.immich.background.localSync"
|
||||||
|
private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload"
|
||||||
|
private static let processingUploadTaskID = "app.alextran.immich.background.processingUpload"
|
||||||
|
|
||||||
|
private static func updateUploadEnabled(_ isEnabled: Bool) {
|
||||||
|
return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func cancelUploadTasks() {
|
||||||
|
BackgroundWorkerApiImpl.updateUploadEnabled(false)
|
||||||
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID);
|
||||||
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID);
|
||||||
|
}
|
||||||
|
|
||||||
public static func registerBackgroundWorkers() {
|
public static func registerBackgroundWorkers() {
|
||||||
BGTaskScheduler.shared.register(
|
BGTaskScheduler.shared.register(
|
||||||
forTaskWithIdentifier: processingTaskID, using: nil) { task in
|
forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in
|
||||||
if task is BGProcessingTask {
|
if task is BGProcessingTask {
|
||||||
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
handleBackgroundProcessing(task: task as! BGProcessingTask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BGTaskScheduler.shared.register(
|
BGTaskScheduler.shared.register(
|
||||||
forTaskWithIdentifier: refreshTaskID, using: nil) { task in
|
forTaskWithIdentifier: refreshUploadTaskID, using: nil) { task in
|
||||||
if task is BGAppRefreshTask {
|
if task is BGAppRefreshTask {
|
||||||
handleBackgroundRefresh(task: task as! BGAppRefreshTask)
|
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .refreshUpload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BGTaskScheduler.shared.register(
|
||||||
|
forTaskWithIdentifier: localSyncTaskID, using: nil) { task in
|
||||||
|
if task is BGAppRefreshTask {
|
||||||
|
handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .localSync)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func scheduleLocalSync() {
|
||||||
|
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: localSyncTaskID)
|
||||||
|
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
||||||
|
|
||||||
|
do {
|
||||||
|
try BGTaskScheduler.shared.submit(backgroundRefresh)
|
||||||
|
} catch {
|
||||||
|
print("Could not schedule the local sync task \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func scheduleRefreshWorker() {
|
private static func scheduleRefreshUpload() {
|
||||||
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshTaskID)
|
let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshUploadTaskID)
|
||||||
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -44,8 +81,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func scheduleProcessingWorker() {
|
private static func scheduleProcessingUpload() {
|
||||||
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingTaskID)
|
let backgroundProcessing = BGProcessingTaskRequest(identifier: processingUploadTaskID)
|
||||||
|
|
||||||
backgroundProcessing.requiresNetworkConnectivity = true
|
backgroundProcessing.requiresNetworkConnectivity = true
|
||||||
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
|
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins
|
||||||
@@ -57,16 +94,29 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleBackgroundRefresh(task: BGAppRefreshTask) {
|
private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) {
|
||||||
scheduleRefreshWorker()
|
let maxSeconds: Int?
|
||||||
|
|
||||||
|
switch taskType {
|
||||||
|
case .localSync:
|
||||||
|
maxSeconds = 15
|
||||||
|
scheduleLocalSync()
|
||||||
|
case .refreshUpload:
|
||||||
|
maxSeconds = 20
|
||||||
|
scheduleRefreshUpload()
|
||||||
|
case .processingUpload:
|
||||||
|
print("Unexpected background refresh task encountered")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
// Restrict the refresh task to run only for a maximum of (maxSeconds) seconds
|
||||||
runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20)
|
runBackgroundWorker(task: task, taskType: taskType, maxSeconds: maxSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
private static func handleBackgroundProcessing(task: BGProcessingTask) {
|
||||||
scheduleProcessingWorker()
|
scheduleProcessingUpload()
|
||||||
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
// There are no restrictions for processing tasks. Although, the OS could signal expiration at any time
|
||||||
runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil)
|
runBackgroundWorker(task: task, taskType: .processingUpload, maxSeconds: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,189 +1,190 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>app.alextran.immich.background.refreshUpload</string>
|
<string>app.alextran.immich.background.localSync</string>
|
||||||
<string>app.alextran.immich.background.processingUpload</string>
|
<string>app.alextran.immich.background.refreshUpload</string>
|
||||||
<string>app.alextran.immich.backgroundFetch</string>
|
<string>app.alextran.immich.background.processingUpload</string>
|
||||||
<string>app.alextran.immich.backgroundProcessing</string>
|
<string>app.alextran.immich.backgroundFetch</string>
|
||||||
</array>
|
<string>app.alextran.immich.backgroundProcessing</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
</array>
|
||||||
<true/>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<true />
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<key>CFBundleDisplayName</key>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<string>${PRODUCT_NAME}</string>
|
<key>CFBundleDisplayName</key>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<string>${PRODUCT_NAME}</string>
|
||||||
<array>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<dict>
|
<array>
|
||||||
<key>CFBundleTypeName</key>
|
<dict>
|
||||||
<string>ShareHandler</string>
|
<key>CFBundleTypeName</key>
|
||||||
<key>LSHandlerRank</key>
|
<string>ShareHandler</string>
|
||||||
<string>Alternate</string>
|
<key>LSHandlerRank</key>
|
||||||
<key>LSItemContentTypes</key>
|
<string>Alternate</string>
|
||||||
<array>
|
<key>LSItemContentTypes</key>
|
||||||
<string>public.file-url</string>
|
<array>
|
||||||
<string>public.image</string>
|
<string>public.file-url</string>
|
||||||
<string>public.text</string>
|
<string>public.image</string>
|
||||||
<string>public.movie</string>
|
<string>public.text</string>
|
||||||
<string>public.url</string>
|
<string>public.movie</string>
|
||||||
<string>public.data</string>
|
<string>public.url</string>
|
||||||
</array>
|
<string>public.data</string>
|
||||||
</dict>
|
</array>
|
||||||
</array>
|
</dict>
|
||||||
<key>CFBundleExecutable</key>
|
</array>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<key>CFBundleExecutable</key>
|
||||||
<key>CFBundleIdentifier</key>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<key>CFBundleIdentifier</key>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<string>6.0</string>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<key>CFBundleLocalizations</key>
|
<string>6.0</string>
|
||||||
<array>
|
<key>CFBundleLocalizations</key>
|
||||||
<string>en</string>
|
<array>
|
||||||
<string>ar</string>
|
<string>en</string>
|
||||||
<string>ca</string>
|
<string>ar</string>
|
||||||
<string>cs</string>
|
<string>ca</string>
|
||||||
<string>da</string>
|
<string>cs</string>
|
||||||
<string>de</string>
|
<string>da</string>
|
||||||
<string>es</string>
|
<string>de</string>
|
||||||
<string>fi</string>
|
<string>es</string>
|
||||||
<string>fr</string>
|
<string>fi</string>
|
||||||
<string>he</string>
|
<string>fr</string>
|
||||||
<string>hi</string>
|
<string>he</string>
|
||||||
<string>hu</string>
|
<string>hi</string>
|
||||||
<string>it</string>
|
<string>hu</string>
|
||||||
<string>ja</string>
|
<string>it</string>
|
||||||
<string>ko</string>
|
<string>ja</string>
|
||||||
<string>lv</string>
|
<string>ko</string>
|
||||||
<string>mn</string>
|
<string>lv</string>
|
||||||
<string>nb</string>
|
<string>mn</string>
|
||||||
<string>nl</string>
|
<string>nb</string>
|
||||||
<string>pl</string>
|
<string>nl</string>
|
||||||
<string>pt</string>
|
<string>pl</string>
|
||||||
<string>ro</string>
|
<string>pt</string>
|
||||||
<string>ru</string>
|
<string>ro</string>
|
||||||
<string>sk</string>
|
<string>ru</string>
|
||||||
<string>sl</string>
|
<string>sk</string>
|
||||||
<string>sr</string>
|
<string>sl</string>
|
||||||
<string>sv</string>
|
<string>sr</string>
|
||||||
<string>th</string>
|
<string>sv</string>
|
||||||
<string>uk</string>
|
<string>th</string>
|
||||||
<string>vi</string>
|
<string>uk</string>
|
||||||
<string>zh</string>
|
<string>vi</string>
|
||||||
</array>
|
<string>zh</string>
|
||||||
<key>CFBundleName</key>
|
</array>
|
||||||
<string>immich_mobile</string>
|
<key>CFBundleName</key>
|
||||||
<key>CFBundlePackageType</key>
|
<string>immich_mobile</string>
|
||||||
<string>APPL</string>
|
<key>CFBundlePackageType</key>
|
||||||
<key>CFBundleShortVersionString</key>
|
<string>APPL</string>
|
||||||
<string>1.140.0</string>
|
<key>CFBundleShortVersionString</key>
|
||||||
<key>CFBundleSignature</key>
|
<string>1.140.0</string>
|
||||||
<string>????</string>
|
<key>CFBundleSignature</key>
|
||||||
<key>CFBundleURLTypes</key>
|
<string>????</string>
|
||||||
<array>
|
<key>CFBundleURLTypes</key>
|
||||||
<dict>
|
<array>
|
||||||
<key>CFBundleTypeRole</key>
|
<dict>
|
||||||
<string>Editor</string>
|
<key>CFBundleTypeRole</key>
|
||||||
<key>CFBundleURLName</key>
|
<string>Editor</string>
|
||||||
<string>Share Extension</string>
|
<key>CFBundleURLName</key>
|
||||||
<key>CFBundleURLSchemes</key>
|
<string>Share Extension</string>
|
||||||
<array>
|
<key>CFBundleURLSchemes</key>
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<array>
|
||||||
</array>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
</dict>
|
</array>
|
||||||
<dict>
|
</dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<dict>
|
||||||
<string>Editor</string>
|
<key>CFBundleTypeRole</key>
|
||||||
<key>CFBundleURLName</key>
|
<string>Editor</string>
|
||||||
<string>Deep Link</string>
|
<key>CFBundleURLName</key>
|
||||||
<key>CFBundleURLSchemes</key>
|
<string>Deep Link</string>
|
||||||
<array>
|
<key>CFBundleURLSchemes</key>
|
||||||
<string>immich</string>
|
<array>
|
||||||
</array>
|
<string>immich</string>
|
||||||
</dict>
|
</array>
|
||||||
</array>
|
</dict>
|
||||||
<key>CFBundleVersion</key>
|
</array>
|
||||||
<string>219</string>
|
<key>CFBundleVersion</key>
|
||||||
<key>FLTEnableImpeller</key>
|
<string>219</string>
|
||||||
<true/>
|
<key>FLTEnableImpeller</key>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<true />
|
||||||
<false/>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<false />
|
||||||
<array>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<string>https</string>
|
<array>
|
||||||
</array>
|
<string>https</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
</array>
|
||||||
<true/>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<true />
|
||||||
<string>No</string>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<string>No</string>
|
||||||
<true/>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
<key>NSAppTransportSecurity</key>
|
<true />
|
||||||
<dict>
|
<key>NSAppTransportSecurity</key>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<dict>
|
||||||
<true/>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
</dict>
|
<true />
|
||||||
<key>NSBonjourServices</key>
|
</dict>
|
||||||
<array>
|
<key>NSBonjourServices</key>
|
||||||
<string>_googlecast._tcp</string>
|
<array>
|
||||||
<string>_CC1AD845._googlecast._tcp</string>
|
<string>_googlecast._tcp</string>
|
||||||
</array>
|
<string>_CC1AD845._googlecast._tcp</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
</array>
|
||||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
<key>NSCameraUsageDescription</key>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
<string>We need to use FaceID to allow access to your locked folder</string>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||||
<string>We need local network permission to connect to the local server using IP address and
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>We need local network permission to connect to the local server using IP address and
|
||||||
allow the casting feature to work</string>
|
allow the casting feature to work</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||||
<key>NSLocationUsageDescription</key>
|
<key>NSLocationUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>processing</string>
|
<string>processing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<false/>
|
<false />
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
<true/>
|
<true />
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -30,9 +30,11 @@ class BackgroundWorkerFgService {
|
|||||||
const BackgroundWorkerFgService(this._foregroundHostApi);
|
const BackgroundWorkerFgService(this._foregroundHostApi);
|
||||||
|
|
||||||
// TODO: Move this call to native side once old timeline is removed
|
// TODO: Move this call to native side once old timeline is removed
|
||||||
Future<void> enable() => _foregroundHostApi.enable();
|
Future<void> enableSyncService() => _foregroundHostApi.enableSyncWorker();
|
||||||
|
|
||||||
Future<void> disable() => _foregroundHostApi.disable();
|
Future<void> enableUploadService() => _foregroundHostApi.enableUploadWorker();
|
||||||
|
|
||||||
|
Future<void> disableUploadService() => _foregroundHostApi.disableUploadWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||||
@@ -91,6 +93,30 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLocalSync(int? maxSeconds) async {
|
||||||
|
try {
|
||||||
|
_logger.info('Local background syncing started');
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
|
||||||
|
final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null;
|
||||||
|
await _syncAssets(hashTimeout: timeout, syncRemote: false);
|
||||||
|
|
||||||
|
sw.stop();
|
||||||
|
_logger.info("Local sync completed in ${sw.elapsed.inSeconds}s");
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe("Failed to complete local sync", error, stack);
|
||||||
|
} finally {
|
||||||
|
await _cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We do the following on Android upload
|
||||||
|
* - Sync local assets
|
||||||
|
* - Hash local assets 3 / 6 minutes
|
||||||
|
* - Sync remote assets
|
||||||
|
* - Check and requeue upload tasks
|
||||||
|
*/
|
||||||
@override
|
@override
|
||||||
Future<void> onAndroidUpload() async {
|
Future<void> onAndroidUpload() async {
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +135,14 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* We do the following on background upload
|
||||||
|
* - Sync local assets
|
||||||
|
* - Hash local assets
|
||||||
|
* - Sync remote assets
|
||||||
|
* - Check and requeue upload tasks
|
||||||
|
*
|
||||||
|
* The native side will not send the maxSeconds value for processing tasks
|
||||||
|
*/
|
||||||
@override
|
@override
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||||
try {
|
try {
|
||||||
@@ -188,7 +222,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
Future<void> _syncAssets({Duration? hashTimeout, bool syncRemote = true}) async {
|
||||||
final futures = <Future<void>>[];
|
final futures = <Future<void>>[];
|
||||||
|
|
||||||
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
||||||
@@ -210,7 +244,10 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
});
|
});
|
||||||
|
|
||||||
futures.add(localSyncFuture);
|
futures.add(localSyncFuture);
|
||||||
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
|
if (syncRemote) {
|
||||||
|
final remoteSyncFuture = _ref.read(backgroundSyncProvider).syncRemote();
|
||||||
|
futures.add(remoteSyncFuture);
|
||||||
|
}
|
||||||
|
|
||||||
await Future.wait(futures);
|
await Future.wait(futures);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
@@ -36,7 +35,6 @@ class HashService {
|
|||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
Future<void> hashAssets() async {
|
Future<void> hashAssets() async {
|
||||||
_log.info("Starting hashing of assets");
|
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
// Sorted by backupSelection followed by isCloud
|
// Sorted by backupSelection followed by isCloud
|
||||||
final localAlbums = await _localAlbumRepository.getAll(
|
final localAlbums = await _localAlbumRepository.getAll(
|
||||||
@@ -51,7 +49,7 @@ class HashService {
|
|||||||
|
|
||||||
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
||||||
if (assetsToHash.isNotEmpty) {
|
if (assetsToHash.isNotEmpty) {
|
||||||
await _hashAssets(album, assetsToHash);
|
await _hashAssets(assetsToHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +60,7 @@ class HashService {
|
|||||||
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
||||||
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
||||||
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
||||||
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash) async {
|
Future<void> _hashAssets(List<LocalAsset> assetsToHash) async {
|
||||||
int bytesProcessed = 0;
|
int bytesProcessed = 0;
|
||||||
final toHash = <_AssetToPath>[];
|
final toHash = <_AssetToPath>[];
|
||||||
|
|
||||||
@@ -74,9 +72,6 @@ class HashService {
|
|||||||
|
|
||||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
_log.warning(
|
|
||||||
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt} from album: ${album.name}",
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,17 +79,17 @@ class HashService {
|
|||||||
toHash.add(_AssetToPath(asset: asset, path: file.path));
|
toHash.add(_AssetToPath(asset: asset, path: file.path));
|
||||||
|
|
||||||
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
|
if (toHash.length >= batchFileLimit || bytesProcessed >= batchSizeLimit) {
|
||||||
await _processBatch(album, toHash);
|
await _processBatch(toHash);
|
||||||
toHash.clear();
|
toHash.clear();
|
||||||
bytesProcessed = 0;
|
bytesProcessed = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _processBatch(album, toHash);
|
await _processBatch(toHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes a batch of assets.
|
/// Processes a batch of assets.
|
||||||
Future<void> _processBatch(LocalAlbum album, List<_AssetToPath> toHash) async {
|
Future<void> _processBatch(List<_AssetToPath> toHash) async {
|
||||||
if (toHash.isEmpty) {
|
if (toHash.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -119,9 +114,7 @@ class HashService {
|
|||||||
if (hash?.length == 20) {
|
if (hash?.length == 20) {
|
||||||
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||||
} else {
|
} else {
|
||||||
_log.warning(
|
_log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}");
|
||||||
"Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt} from album: ${album.name}",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
@@ -25,6 +26,7 @@ import 'package:immich_mobile/providers/routes.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
@@ -205,9 +207,12 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
|||||||
// needs to be delayed so that EasyLocalization is working
|
// needs to be delayed so that EasyLocalization is working
|
||||||
if (Store.isBetaTimelineEnabled) {
|
if (Store.isBetaTimelineEnabled) {
|
||||||
ref.read(backgroundServiceProvider).disableService();
|
ref.read(backgroundServiceProvider).disableService();
|
||||||
ref.read(driftBackgroundUploadFgService).enable();
|
ref.read(driftBackgroundUploadFgService).enableSyncService();
|
||||||
|
if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) {
|
||||||
|
ref.read(driftBackgroundUploadFgService).enableUploadService();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ref.read(driftBackgroundUploadFgService).disable();
|
ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
@@ -42,10 +43,12 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
|
|
||||||
await ref.read(backgroundSyncProvider).syncRemote();
|
await ref.read(backgroundSyncProvider).syncRemote();
|
||||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||||
|
await ref.read(driftBackgroundUploadFgService).enableUploadService();
|
||||||
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stopBackup() async {
|
Future<void> stopBackup() async {
|
||||||
|
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||||
await ref.read(driftBackupProvider.notifier).cancel();
|
await ref.read(driftBackupProvider.notifier).cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
|||||||
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
||||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
await ref.read(driftBackgroundUploadFgService).disable();
|
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||||
}
|
}
|
||||||
|
|
||||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||||
|
|||||||
62
mobile/lib/platform/background_worker_api.g.dart
generated
62
mobile/lib/platform/background_worker_api.g.dart
generated
@@ -59,9 +59,9 @@ class BackgroundWorkerFgHostApi {
|
|||||||
|
|
||||||
final String pigeonVar_messageChannelSuffix;
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
Future<void> enable() async {
|
Future<void> enableSyncWorker() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$pigeonVar_messageChannelSuffix';
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -82,9 +82,32 @@ class BackgroundWorkerFgHostApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> disable() async {
|
Future<void> enableUploadWorker() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disableUploadWorker() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$pigeonVar_messageChannelSuffix';
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
@@ -169,6 +192,8 @@ class BackgroundWorkerBgHostApi {
|
|||||||
abstract class BackgroundWorkerFlutterApi {
|
abstract class BackgroundWorkerFlutterApi {
|
||||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
|
Future<void> onLocalSync(int? maxSeconds);
|
||||||
|
|
||||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
||||||
|
|
||||||
Future<void> onAndroidUpload();
|
Future<void> onAndroidUpload();
|
||||||
@@ -181,6 +206,35 @@ abstract class BackgroundWorkerFlutterApi {
|
|||||||
String messageChannelSuffix = '',
|
String messageChannelSuffix = '',
|
||||||
}) {
|
}) {
|
||||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
|
{
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$messageChannelSuffix',
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: binaryMessenger,
|
||||||
|
);
|
||||||
|
if (api == null) {
|
||||||
|
pigeonVar_channel.setMessageHandler(null);
|
||||||
|
} else {
|
||||||
|
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||||
|
assert(
|
||||||
|
message != null,
|
||||||
|
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync was null.',
|
||||||
|
);
|
||||||
|
final List<Object?> args = (message as List<Object?>?)!;
|
||||||
|
final int? arg_maxSeconds = (args[0] as int?);
|
||||||
|
try {
|
||||||
|
await api.onLocalSync(arg_maxSeconds);
|
||||||
|
return wrapResponse(empty: true);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
return wrapResponse(error: e);
|
||||||
|
} catch (e) {
|
||||||
|
return wrapResponse(
|
||||||
|
error: PlatformException(code: 'error', message: e.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ import 'package:pigeon/pigeon.dart';
|
|||||||
)
|
)
|
||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class BackgroundWorkerFgHostApi {
|
abstract class BackgroundWorkerFgHostApi {
|
||||||
void enable();
|
void enableSyncWorker();
|
||||||
|
|
||||||
void disable();
|
void enableUploadWorker();
|
||||||
|
|
||||||
|
// Disables the background upload service
|
||||||
|
void disableUploadWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
@@ -29,6 +32,10 @@ abstract class BackgroundWorkerBgHostApi {
|
|||||||
|
|
||||||
@FlutterApi()
|
@FlutterApi()
|
||||||
abstract class BackgroundWorkerFlutterApi {
|
abstract class BackgroundWorkerFlutterApi {
|
||||||
|
// Android & iOS: Called when the local sync is triggered
|
||||||
|
@async
|
||||||
|
void onLocalSync(int? maxSeconds);
|
||||||
|
|
||||||
// iOS Only: Called when the iOS background upload is triggered
|
// iOS Only: Called when the iOS background upload is triggered
|
||||||
@async
|
@async
|
||||||
void onIosUpload(bool isRefresh, int? maxSeconds);
|
void onIosUpload(bool isRefresh, int? maxSeconds);
|
||||||
|
|||||||
@@ -6,5 +6,12 @@
|
|||||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
|
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
|
||||||
"engines": {
|
"engines": {
|
||||||
"pnpm": ">=10.0.0"
|
"pnpm": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node --experimental-strip-types dev.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "22.18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
pnpm-lock.yaml
generated
109
pnpm-lock.yaml
generated
@@ -15,7 +15,11 @@ pnpmfileChecksum: sha256-AG/qwrPNpmy9q60PZwCpecoYVptglTHgH+N6RKQHOM0=
|
|||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.: {}
|
.:
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: 22.18.0
|
||||||
|
version: 22.18.0
|
||||||
|
|
||||||
.github:
|
.github:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -4658,6 +4662,9 @@ packages:
|
|||||||
'@types/node@22.17.2':
|
'@types/node@22.17.2':
|
||||||
resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==}
|
resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==}
|
||||||
|
|
||||||
|
'@types/node@22.18.0':
|
||||||
|
resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==}
|
||||||
|
|
||||||
'@types/node@24.3.0':
|
'@types/node@24.3.0':
|
||||||
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
|
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
|
||||||
|
|
||||||
@@ -14530,7 +14537,7 @@ snapshots:
|
|||||||
'@jest/schemas': 29.6.3
|
'@jest/schemas': 29.6.3
|
||||||
'@types/istanbul-lib-coverage': 2.0.6
|
'@types/istanbul-lib-coverage': 2.0.6
|
||||||
'@types/istanbul-reports': 3.0.4
|
'@types/istanbul-reports': 3.0.4
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
'@types/yargs': 17.0.33
|
'@types/yargs': 17.0.33
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
||||||
@@ -16349,7 +16356,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/accepts@1.3.7':
|
'@types/accepts@1.3.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/archiver@6.0.3':
|
'@types/archiver@6.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16363,22 +16370,22 @@ snapshots:
|
|||||||
|
|
||||||
'@types/bcrypt@6.0.0':
|
'@types/bcrypt@6.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/bonjour@3.5.13':
|
'@types/bonjour@3.5.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/braces@3.0.5': {}
|
'@types/braces@3.0.5': {}
|
||||||
|
|
||||||
'@types/bunyan@1.8.11':
|
'@types/bunyan@1.8.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/byte-size@8.1.2': {}
|
'@types/byte-size@8.1.2': {}
|
||||||
|
|
||||||
@@ -16397,21 +16404,21 @@ snapshots:
|
|||||||
|
|
||||||
'@types/cli-progress@3.11.6':
|
'@types/cli-progress@3.11.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/compression@1.8.1':
|
'@types/compression@1.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/express': 5.0.3
|
'@types/express': 5.0.3
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/connect-history-api-fallback@1.5.4':
|
'@types/connect-history-api-fallback@1.5.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/express-serve-static-core': 5.0.6
|
'@types/express-serve-static-core': 5.0.6
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/content-disposition@0.5.9': {}
|
'@types/content-disposition@0.5.9': {}
|
||||||
|
|
||||||
@@ -16428,11 +16435,11 @@ snapshots:
|
|||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/express': 5.0.3
|
'@types/express': 5.0.3
|
||||||
'@types/keygrip': 1.0.6
|
'@types/keygrip': 1.0.6
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16442,13 +16449,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/docker-modem@3.0.6':
|
'@types/docker-modem@3.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
'@types/ssh2': 1.15.5
|
'@types/ssh2': 1.15.5
|
||||||
|
|
||||||
'@types/dockerode@3.3.42':
|
'@types/dockerode@3.3.42':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/docker-modem': 3.0.6
|
'@types/docker-modem': 3.0.6
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
'@types/ssh2': 1.15.5
|
'@types/ssh2': 1.15.5
|
||||||
|
|
||||||
'@types/dom-to-image@2.6.7': {}
|
'@types/dom-to-image@2.6.7': {}
|
||||||
@@ -16471,14 +16478,14 @@ snapshots:
|
|||||||
|
|
||||||
'@types/express-serve-static-core@4.19.6':
|
'@types/express-serve-static-core@4.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
'@types/qs': 6.14.0
|
'@types/qs': 6.14.0
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.5
|
'@types/send': 0.17.5
|
||||||
|
|
||||||
'@types/express-serve-static-core@5.0.6':
|
'@types/express-serve-static-core@5.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
'@types/qs': 6.14.0
|
'@types/qs': 6.14.0
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.5
|
'@types/send': 0.17.5
|
||||||
@@ -16504,7 +16511,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/fluent-ffmpeg@2.1.27':
|
'@types/fluent-ffmpeg@2.1.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/geojson-vt@3.2.5':
|
'@types/geojson-vt@3.2.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16541,7 +16548,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/http-proxy@1.17.16':
|
'@types/http-proxy@1.17.16':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/inquirer@8.2.11':
|
'@types/inquirer@8.2.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16579,7 +16586,7 @@ snapshots:
|
|||||||
'@types/http-errors': 2.0.5
|
'@types/http-errors': 2.0.5
|
||||||
'@types/keygrip': 1.0.6
|
'@types/keygrip': 1.0.6
|
||||||
'@types/koa-compose': 3.2.8
|
'@types/koa-compose': 3.2.8
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/leaflet@1.9.19':
|
'@types/leaflet@1.9.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16605,7 +16612,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/memcached@2.2.10':
|
'@types/memcached@2.2.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/methods@1.1.4': {}
|
'@types/methods@1.1.4': {}
|
||||||
|
|
||||||
@@ -16617,7 +16624,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/mock-fs@4.13.4':
|
'@types/mock-fs@4.13.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
@@ -16627,16 +16634,16 @@ snapshots:
|
|||||||
|
|
||||||
'@types/mysql@2.15.27':
|
'@types/mysql@2.15.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/node-fetch@2.6.12':
|
'@types/node-fetch@2.6.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
form-data: 4.0.3
|
form-data: 4.0.3
|
||||||
|
|
||||||
'@types/node-forge@1.3.11':
|
'@types/node-forge@1.3.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/node@17.0.45': {}
|
'@types/node@17.0.45': {}
|
||||||
|
|
||||||
@@ -16656,6 +16663,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/node@22.18.0':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/node@24.3.0':
|
'@types/node@24.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.10.0
|
undici-types: 7.10.0
|
||||||
@@ -16663,17 +16674,17 @@ snapshots:
|
|||||||
|
|
||||||
'@types/nodemailer@6.4.17':
|
'@types/nodemailer@6.4.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/oidc-provider@9.1.2':
|
'@types/oidc-provider@9.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/keygrip': 1.0.6
|
'@types/keygrip': 1.0.6
|
||||||
'@types/koa': 3.0.0
|
'@types/koa': 3.0.0
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/oracledb@6.5.2':
|
'@types/oracledb@6.5.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/parse5@5.0.3': {}
|
'@types/parse5@5.0.3': {}
|
||||||
|
|
||||||
@@ -16683,13 +16694,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/pg@8.15.4':
|
'@types/pg@8.15.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
pg-protocol: 1.10.3
|
pg-protocol: 1.10.3
|
||||||
pg-types: 2.2.0
|
pg-types: 2.2.0
|
||||||
|
|
||||||
'@types/pg@8.15.5':
|
'@types/pg@8.15.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
pg-protocol: 1.10.3
|
pg-protocol: 1.10.3
|
||||||
pg-types: 2.2.0
|
pg-types: 2.2.0
|
||||||
|
|
||||||
@@ -16697,13 +16708,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/pngjs@6.0.5':
|
'@types/pngjs@6.0.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/prismjs@1.26.5': {}
|
'@types/prismjs@1.26.5': {}
|
||||||
|
|
||||||
'@types/qrcode@1.5.5':
|
'@types/qrcode@1.5.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/qs@6.14.0': {}
|
'@types/qs@6.14.0': {}
|
||||||
|
|
||||||
@@ -16743,7 +16754,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/readdir-glob@1.1.5':
|
'@types/readdir-glob@1.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/retry@0.12.0': {}
|
'@types/retry@0.12.0': {}
|
||||||
|
|
||||||
@@ -16753,14 +16764,14 @@ snapshots:
|
|||||||
|
|
||||||
'@types/sax@1.2.7':
|
'@types/sax@1.2.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/semver@7.7.0': {}
|
'@types/semver@7.7.0': {}
|
||||||
|
|
||||||
'@types/send@0.17.5':
|
'@types/send@0.17.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mime': 1.3.5
|
'@types/mime': 1.3.5
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/serve-index@1.9.4':
|
'@types/serve-index@1.9.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16769,20 +16780,20 @@ snapshots:
|
|||||||
'@types/serve-static@1.15.8':
|
'@types/serve-static@1.15.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.5
|
'@types/http-errors': 2.0.5
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
'@types/send': 0.17.5
|
'@types/send': 0.17.5
|
||||||
|
|
||||||
'@types/sockjs@0.3.36':
|
'@types/sockjs@0.3.36':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/ssh2-streams@0.1.12':
|
'@types/ssh2-streams@0.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/ssh2@0.5.52':
|
'@types/ssh2@0.5.52':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
'@types/ssh2-streams': 0.1.12
|
'@types/ssh2-streams': 0.1.12
|
||||||
|
|
||||||
'@types/ssh2@1.15.5':
|
'@types/ssh2@1.15.5':
|
||||||
@@ -16793,7 +16804,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/cookiejar': 2.1.5
|
'@types/cookiejar': 2.1.5
|
||||||
'@types/methods': 1.1.4
|
'@types/methods': 1.1.4
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
form-data: 4.0.3
|
form-data: 4.0.3
|
||||||
|
|
||||||
'@types/supercluster@7.1.3':
|
'@types/supercluster@7.1.3':
|
||||||
@@ -16807,11 +16818,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/tedious@4.0.14':
|
'@types/tedious@4.0.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/through@0.0.33':
|
'@types/through@0.0.33':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/ua-parser-js@0.7.39': {}
|
'@types/ua-parser-js@0.7.39': {}
|
||||||
|
|
||||||
@@ -16825,7 +16836,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3': {}
|
'@types/yargs-parser@21.0.3': {}
|
||||||
|
|
||||||
@@ -18915,7 +18926,7 @@ snapshots:
|
|||||||
engine.io@6.6.4:
|
engine.io@6.6.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/cors': 2.8.19
|
'@types/cors': 2.8.19
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
@@ -19339,7 +19350,7 @@ snapshots:
|
|||||||
|
|
||||||
eval@0.1.8:
|
eval@0.1.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
require-like: 0.1.2
|
require-like: 0.1.2
|
||||||
|
|
||||||
event-emitter@0.3.5:
|
event-emitter@0.3.5:
|
||||||
@@ -20591,7 +20602,7 @@ snapshots:
|
|||||||
jest-util@29.7.0:
|
jest-util@29.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 3.9.0
|
ci-info: 3.9.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -20599,13 +20610,13 @@ snapshots:
|
|||||||
|
|
||||||
jest-worker@27.5.1:
|
jest-worker@27.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
|
|
||||||
jest-worker@29.7.0:
|
jest-worker@29.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
jest-util: 29.7.0
|
jest-util: 29.7.0
|
||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
@@ -23098,7 +23109,7 @@ snapshots:
|
|||||||
'@protobufjs/path': 1.1.2
|
'@protobufjs/path': 1.1.2
|
||||||
'@protobufjs/pool': 1.1.0
|
'@protobufjs/pool': 1.1.0
|
||||||
'@protobufjs/utf8': 1.1.0
|
'@protobufjs/utf8': 1.1.0
|
||||||
'@types/node': 22.17.2
|
'@types/node': 22.18.0
|
||||||
long: 5.3.2
|
long: 5.3.2
|
||||||
|
|
||||||
protocol-buffers-schema@3.6.0: {}
|
protocol-buffers-schema@3.6.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user