diff --git a/Cargo.lock b/Cargo.lock index 94da5108..b6046c98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1614,6 +1614,16 @@ dependencies = [ "log", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.12" @@ -2230,6 +2240,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -2312,6 +2331,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "stride_background" +version = "0.1.0" +dependencies = [ + "async-trait", + "log", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "stride_cli" version = "0.1.0" @@ -2398,6 +2427,7 @@ dependencies = [ "stride_backend", "stride_backend_git", "stride_backend_taskchampion", + "stride_background", "stride_core", "stride_crdt", "stride_database", @@ -2680,7 +2710,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 188c9bcd..ef0659a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/serialization", "crates/logging", "crates/database", + "crates/background", "crates/backend", "crates/backends/git", "crates/backends/taskchampion", @@ -34,6 +35,7 @@ stride_crdt = { version = "~0.1.0", path = "crates/crdt" } stride_serialize = { version = "~0.1.0", path = "crates/serialization" } stride_logging = { version = "~0.1.0", path = "crates/logging" } stride_database = { version = "~0.1.0", path = "crates/database" } +stride_background = { version = "~0.1.0", path = "crates/background" } stride_backend = { version = "~0.1.0", path = "crates/backend" } stride_backend_git = { version = "~0.1.0", path = "crates/backends/git" } stride_backend_taskchampion = { version = "~0.1.0", path = "crates/backends/taskchampion" } @@ -89,6 +91,7 @@ url = { version = "2.5.8", features = ["serde"] } thiserror = "2.0.18" indoc = "2.0.7" vint64 = "=1.0.1" +async-trait = "0.1.89" [profile.release] # strip = "debug" diff --git a/app/lib/background.dart b/app/lib/background.dart new file mode 100644 index 00000000..17180a04 --- /dev/null +++ b/app/lib/background.dart @@ -0,0 +1,465 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:stride/bridge/api/background.dart' as background; +import 'package:stride/bridge/api/error.dart'; +import 'package:stride/bridge/api/logging.dart' as logging; +import 'package:stride/bridge/frb_generated.dart'; +import 'package:uuid/uuid.dart'; +import 'package:workmanager/workmanager.dart'; + +@immutable +abstract class BackgroundTask { + String uniqueName(); + String taskName() { + return uniqueName(); + } + + Map toInputData(); +} + +@immutable +class TaskSyncBackgroundTask implements BackgroundTask { + final UuidValue repositoryId; + const TaskSyncBackgroundTask({required this.repositoryId}); + + @override + Map toInputData() { + return { + 'repository': {'id': repositoryId.toString()}, + }; + } + + @override + String uniqueName() { + return '${taskName()}:$repositoryId'; + } + + @override + String taskName() { + return 'task.sync'; + } +} + +@immutable +class Output { + final Name name; + final Map inputData; + final Object? error; + final bool done; + const Output({ + required this.name, + required this.inputData, + this.error, + this.done = false, + }); + + @override + bool operator ==(Object other) { + if (other is! Output) { + return false; + } + return name == other.name; + } + + @override + int get hashCode => name.hashCode; + + @override + String toString() => '$name :: done:$done, error:$error'; +} + +@immutable +class Background { + static late Set? _outputs; + static late StreamController>? _streamController; + static late Stream>? _stream; + static late ReceivePort? _receivePort; + + static Stream> stream() => _stream!; + + static Future init() async { + _outputs = {}; + _streamController = StreamController(); + + _receivePort = ReceivePort('worker'); + _receivePort?.listen((message) { + final output = message as Output; + print('Background :: $output'); + + _outputs?.remove(output); + _outputs!.add(output); + _streamController!.sink.add({...?_outputs}); + }); + _stream = _streamController?.stream.asBroadcastStream(); + + IsolateNameServer.removePortNameMapping(_GLOBAL_PORT_NAME); + IsolateNameServer.registerPortWithName( + _receivePort!.sendPort, + _GLOBAL_PORT_NAME, + ); + + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + WorkmanagerPlatform.instance = await _DesktopWorkmanager.create(); + } + await Workmanager().initialize(_callbackDispatcher); + } + + static Future run( + BackgroundTask task, { + Duration? initialDelay, + ExistingWorkPolicy? existingWorkPolicy, + }) async { + final uniqueName = task.uniqueName(); + final taskName = task.taskName(); + return Workmanager().registerOneOffTask( + uniqueName, + taskName, + inputData: task.toInputData(), + initialDelay: initialDelay, + existingWorkPolicy: existingWorkPolicy, + ); + } + + static Future periodic( + BackgroundTask task, { + required Duration frequency, + Duration? initialDelay, + ExistingPeriodicWorkPolicy? existingWorkPolicy, + }) async { + final uniqueName = task.uniqueName(); + final taskName = task.taskName(); + return Workmanager().registerPeriodicTask( + uniqueName, + taskName, + inputData: task.toInputData(), + initialDelay: initialDelay, + frequency: frequency, + existingWorkPolicy: existingWorkPolicy, + ); + } + static Future cancel(BackgroundTask task) async { + return Workmanager().cancelByUniqueName(task.uniqueName()); + } + + static Future cancelAll() async { + return Workmanager().cancelAll(); + } +} + +@pragma('vm:entry-point') +void _callbackDispatcher() { + Workmanager().executeTask(_executeTask); +} + +const String _GLOBAL_PORT_NAME = 'worker-port'; + +Future _executeTask(String task, Map? inputData) async { + final input = inputData ?? {}; + + await RustLib.init(); + + logging.trace(message: 'Background task: $task, inputData: $inputData'); + + final port = IsolateNameServer.lookupPortByName(_GLOBAL_PORT_NAME); + + final name = Name.fromString(task); + + port?.send(Output(name: name, inputData: input)); + try { + await background.execute( + task: jsonEncode({'method': name.method, 'params': input}), + ); + port?.send(Output(name: name, inputData: input, done: true)); + } on RustError catch (e) { + port?.send( + Output( + name: name, + inputData: input, + error: e.toErrorString(), + done: true, + ), + ); + } + // ignore: avoid_catches_without_on_clauses + catch (error) { + port?.send(Output(name: name, inputData: input, error: error, done: true)); + } + + return Future.value(true); +} + +@immutable +class Name { + final String method; + final String? unique; + + const Name({required this.method, this.unique}); + + factory Name.fromString(String value) { + final separatorIndex = value.indexOf(':'); + if (separatorIndex == -1) { + return Name(method: value); + } + + return Name( + method: value.substring(0, separatorIndex), + unique: value.substring(separatorIndex + 1), + ); + } + + @override + bool operator ==(Object other) { + if (other is! Name) { + return false; + } + return method == other.method && unique == other.unique; + } + + @override + int get hashCode => Object.hash(method, unique); + + @override + String toString() { + if (unique == null) { + return method; + } else { + return '$method:$unique'; + } + } +} + +@immutable +class ComputeArgs { + final RootIsolateToken token; + final BTask task; + const ComputeArgs({required this.token, required this.task}); +} + +@pragma('vm:entry-point') +Future _compute(ComputeArgs args) async { + BackgroundIsolateBinaryMessenger.ensureInitialized(args.token); + await _executeTask(args.task.name.toString(), args.task.inputData); +} + +@pragma('vm:entry-point') +Future _entryPoint(SendPort sender) async { + final receiver = ReceivePort('worker'); + sender.send(receiver.sendPort); + + final stream = receiver.asBroadcastStream(); + final token = (await stream.first) as RootIsolateToken; + + await RustLib.init(); + + final tasks = {}; + final tagIndex = >{}; + + await for (final value in stream) { + if (value is _CancelByName) { + logging.info(message: 'Canceling task by name: ${value.uniqueName}'); + final name = Name.fromString(value.uniqueName); + tasks.remove(name)?.cancel(); + continue; + } + if (value is _CancelByTag) { + logging.info(message: 'Canceling tasks by tag: ${value.tag}'); + final names = tagIndex.remove(value.tag) ?? {}; + for (final name in names) { + tasks.remove(name)?.cancel(); + } + continue; + } + if (value is _CancelAll) { + for (final timer in tasks.values) { + timer.cancel(); + } + tasks.clear(); + tagIndex.clear(); + continue; + } + + final task = value as BTask; + logging.trace(message: '${task.name}'); + + if (task.frequency == null) { + compute(_compute, ComputeArgs(token: token, task: task)); + } else { + tasks[task.name]?.cancel(); + tasks[task.name] = Timer.periodic(task.frequency!, (timer) async { + await compute(_compute, ComputeArgs(token: token, task: task)); + }); + if (task.tag != null) { + tagIndex.putIfAbsent(task.tag!, () => {}).add(task.name); + } + } + } + logging.info(message: 'DONE'); +} + +@immutable +class BTask { + final Name name; + final Duration? frequency; + final Map inputData; + final String? tag; + + const BTask({required this.name, required this.inputData, this.frequency, this.tag}); +} + +@immutable +class _CancelByName { + final String uniqueName; + const _CancelByName(this.uniqueName); +} + +@immutable +class _CancelByTag { + final String tag; + const _CancelByTag(this.tag); +} + +@immutable +class _CancelAll { + const _CancelAll(); +} + +@immutable +class _DesktopWorkmanager extends WorkmanagerPlatform { + final Isolate _isolate; + final ReceivePort _receiver; + final SendPort _sender; + + _DesktopWorkmanager._internal({ + required Isolate isolate, + required ReceivePort receiver, + required SendPort sender, + }) : _sender = sender, + _receiver = receiver, + _isolate = isolate; + + static Future<_DesktopWorkmanager> create() async { + final receiver = ReceivePort('main'); + final isolate = await Isolate.spawn( + _entryPoint, + receiver.sendPort, + debugName: 'background-worker', + ); + + final stream = receiver.asBroadcastStream(); + final sender = (await stream.first) as SendPort; + + // ignore: cascade_invocations + sender.send(RootIsolateToken.instance!); + + return _DesktopWorkmanager._internal( + isolate: isolate, + receiver: receiver, + sender: sender, + ); + } + + @override + Future initialize( + Function callbackDispatcher, { + @Deprecated( + 'Use WorkmanagerDebug handlers instead. This parameter has no effect.', + ) + bool isInDebugMode = false, + }) async {} + + @override + Future registerOneOffTask( + String uniqueName, + String taskName, { + Map? inputData, + Duration? initialDelay, + Constraints? constraints, + ExistingWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + Duration? backoffPolicyDelay, + String? tag, + OutOfQuotaPolicy? outOfQuotaPolicy, + }) async { + _sender.send( + BTask(name: Name.fromString(uniqueName), inputData: inputData ?? {}, tag: tag), + ); + } + + @override + Future registerPeriodicTask( + String uniqueName, + String taskName, { + Duration? frequency, + Duration? flexInterval, + Map? inputData, + Duration? initialDelay, + Constraints? constraints, + ExistingPeriodicWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + Duration? backoffPolicyDelay, + String? tag, + }) async { + _sender.send( + BTask( + name: Name.fromString(uniqueName), + inputData: inputData ?? {}, + frequency: frequency, + tag: tag, + ), + ); + } + + @override + Future registerProcessingTask( + String uniqueName, + String taskName, { + Duration? initialDelay, + Map? inputData, + Constraints? constraints, + }) async { + throw UnimplementedError( + 'No implementation found for workmanager on this platform.', + ); + } + + @override + Future cancelByUniqueName(String uniqueName) async { + _sender.send(_CancelByName(uniqueName)); + } + + @override + Future cancelByTag(String tag) async { + _sender.send(_CancelByTag(tag)); + } + + @override + Future cancelAll() async { + _sender.send(const _CancelAll()); + } + + @override + Future isScheduledByUniqueName(String uniqueName) async { + throw UnimplementedError( + 'No implementation found for workmanager on this platform.', + ); + } + + @override + Future printScheduledTasks() async { + throw UnimplementedError( + 'No implementation found for workmanager on this platform.', + ); + } + + Future dispose() async { + // await _taskStreamController.close(); + // await _streamSubscription.cancel(); + _isolate.kill(); + } +} diff --git a/app/lib/blocs/tasks_bloc.dart b/app/lib/blocs/tasks_bloc.dart index fdd0059c..e823b31e 100644 --- a/app/lib/blocs/tasks_bloc.dart +++ b/app/lib/blocs/tasks_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; +import 'package:stride/background.dart'; import 'package:stride/blocs/dialog_bloc.dart'; import 'package:stride/blocs/log_bloc.dart'; import 'package:stride/blocs/settings_bloc.dart'; @@ -9,10 +10,12 @@ import 'package:stride/bridge/api/error.dart'; import 'package:stride/bridge/api/filter.dart'; import 'package:stride/bridge/api/git.dart'; import 'package:stride/bridge/api/repository.dart'; +import 'package:stride/bridge/api/settings.dart'; import 'package:stride/bridge/third_party/stride_backend_git/known_hosts.dart'; import 'package:stride/bridge/third_party/stride_core/event.dart'; import 'package:stride/bridge/third_party/stride_core/task.dart'; import 'package:uuid/uuid.dart'; +import 'package:workmanager/workmanager.dart' hide TaskStatus; @immutable abstract class TaskEvent {} @@ -76,27 +79,32 @@ class TaskBloc extends Bloc { Repository? database; Filter? filter; - Timer? syncTimer; - void _initializeSettingsStream() { - if (settingsBloc.settings.periodicSync) { - syncTimer = Timer.periodic( - const Duration(minutes: 5), - (timer) => add(TaskSyncEvent()), - ); - } - repositoryUuid ??= settingsBloc.settings.currentRepositoryUuidOrFirst(); - settingsSubscription = settingsBloc.stream.listen((event) { - if (event.settings.periodicSync) { - syncTimer ??= Timer.periodic( - const Duration(minutes: 5), - (timer) => add(TaskSyncEvent()), - ); + _registerPeriodicTasks(settingsBloc.settings); + + var previousSettings = settingsBloc.settings; + + settingsSubscription = settingsBloc.stream.listen((event) async { + final previous = previousSettings; + final next = event.settings; + previousSettings = next; + + if (!next.periodicSync && previous.periodicSync) { + // Sync was turned off — cancel all repository tasks. + await _unregisterPeriodicTasks(previous.repositories); } else { - syncTimer?.cancel(); - syncTimer = null; + // Sync is on — register/replace tasks for current repositories. + await _registerPeriodicTasks(next); + + // Cancel tasks for repositories that were removed. + final removedRepositories = previous.repositories + .where((r) => !next.repositories.any((n) => n.uuid == r.uuid)) + .toList(); + if (removedRepositories.isNotEmpty) { + await _unregisterPeriodicTasks(removedRepositories); + } } final nextRepositoryUuid = event.settings.currentRepositoryUuidOrFirst(); @@ -108,6 +116,24 @@ class TaskBloc extends Bloc { }); } + Future _registerPeriodicTasks(Settings settings) async { + if (!settings.periodicSync) return; + for (final repository in settings.repositories.asMap().entries) { + await Background.periodic( + TaskSyncBackgroundTask(repositoryId: repository.value.uuid), + initialDelay: const Duration(seconds: 10), + frequency: Duration(minutes: 5, seconds: repository.key * 10), + existingWorkPolicy: ExistingPeriodicWorkPolicy.replace, + ); + } + } + + Future _unregisterPeriodicTasks(List repositories) async { + for (final repository in repositories) { + await Background.cancel(TaskSyncBackgroundTask(repositoryId: repository.uuid)); + } + } + Repository? repository() { if (database != null) { return database; @@ -168,7 +194,14 @@ class TaskBloc extends Bloc { emit(TaskState(tasks: tasks, syncing: true)); try { - await repository()?.sync_(); + final uuid = + repositoryUuid ?? settingsBloc.settings.currentRepositoryUuidOrFirst(); + if (uuid != null) { + await Background.run( + TaskSyncBackgroundTask(repositoryId: uuid), + existingWorkPolicy: ExistingWorkPolicy.replace, + ); + } } catch (error) { emit(TaskState(tasks: tasks, syncingError: error)); rethrow; diff --git a/app/lib/bridge/api/background.dart b/app/lib/bridge/api/background.dart new file mode 100644 index 00000000..ec5fe513 --- /dev/null +++ b/app/lib/bridge/api/background.dart @@ -0,0 +1,42 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: avoid_unused_constructor_parameters +// ignore_for_file: avoid_dynamic_calls +// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes +// ignore_for_file: argument_type_not_assignable +// ignore_for_file: inference_failure_on_instance_creation + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:freezed_annotation/freezed_annotation.dart' hide protected; +import 'package:stride/bridge/api/error.dart'; +import 'package:stride/bridge/frb_generated.dart'; + +part 'background.freezed.dart'; + +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `BgTask`, `BridgeHook`, `Method`, `RepositorySpec`, `State`, `TaskSync` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `on_task_result`, `on_task_start` + +Stream init() => + RustLib.instance.api.crateApiBackgroundInit(); + +Future execute({required String task}) => + RustLib.instance.api.crateApiBackgroundExecute(task: task); + +@freezed +sealed class BackgroundResult with _$BackgroundResult { + const BackgroundResult._(); + + const factory BackgroundResult.start({required String task}) = + BackgroundResult_Start; + const factory BackgroundResult.done({ + required String task, + required bool success, + }) = BackgroundResult_Done; + const factory BackgroundResult.error({ + required String task, + required RustError error, + }) = BackgroundResult_Error; +} diff --git a/app/lib/bridge/api/background.freezed.dart b/app/lib/bridge/api/background.freezed.dart new file mode 100644 index 00000000..74f3cdbc --- /dev/null +++ b/app/lib/bridge/api/background.freezed.dart @@ -0,0 +1,413 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'background.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$BackgroundResult { + + String get task; +/// Create a copy of BackgroundResult +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BackgroundResultCopyWith get copyWith => _$BackgroundResultCopyWithImpl(this as BackgroundResult, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BackgroundResult&&(identical(other.task, task) || other.task == task)); +} + + +@override +int get hashCode => Object.hash(runtimeType,task); + +@override +String toString() { + return 'BackgroundResult(task: $task)'; +} + + +} + +/// @nodoc +abstract mixin class $BackgroundResultCopyWith<$Res> { + factory $BackgroundResultCopyWith(BackgroundResult value, $Res Function(BackgroundResult) _then) = _$BackgroundResultCopyWithImpl; +@useResult +$Res call({ + String task +}); + + + + +} +/// @nodoc +class _$BackgroundResultCopyWithImpl<$Res> + implements $BackgroundResultCopyWith<$Res> { + _$BackgroundResultCopyWithImpl(this._self, this._then); + + final BackgroundResult _self; + final $Res Function(BackgroundResult) _then; + +/// Create a copy of BackgroundResult +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? task = null,}) { + return _then(_self.copyWith( +task: null == task ? _self.task : task // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [BackgroundResult]. +extension BackgroundResultPatterns on BackgroundResult { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( BackgroundResult_Start value)? start,TResult Function( BackgroundResult_Done value)? done,TResult Function( BackgroundResult_Error value)? error,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case BackgroundResult_Start() when start != null: +return start(_that);case BackgroundResult_Done() when done != null: +return done(_that);case BackgroundResult_Error() when error != null: +return error(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( BackgroundResult_Start value) start,required TResult Function( BackgroundResult_Done value) done,required TResult Function( BackgroundResult_Error value) error,}){ +final _that = this; +switch (_that) { +case BackgroundResult_Start(): +return start(_that);case BackgroundResult_Done(): +return done(_that);case BackgroundResult_Error(): +return error(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( BackgroundResult_Start value)? start,TResult? Function( BackgroundResult_Done value)? done,TResult? Function( BackgroundResult_Error value)? error,}){ +final _that = this; +switch (_that) { +case BackgroundResult_Start() when start != null: +return start(_that);case BackgroundResult_Done() when done != null: +return done(_that);case BackgroundResult_Error() when error != null: +return error(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( String task)? start,TResult Function( String task, bool success)? done,TResult Function( String task, RustError error)? error,required TResult orElse(),}) {final _that = this; +switch (_that) { +case BackgroundResult_Start() when start != null: +return start(_that.task);case BackgroundResult_Done() when done != null: +return done(_that.task,_that.success);case BackgroundResult_Error() when error != null: +return error(_that.task,_that.error);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( String task) start,required TResult Function( String task, bool success) done,required TResult Function( String task, RustError error) error,}) {final _that = this; +switch (_that) { +case BackgroundResult_Start(): +return start(_that.task);case BackgroundResult_Done(): +return done(_that.task,_that.success);case BackgroundResult_Error(): +return error(_that.task,_that.error);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( String task)? start,TResult? Function( String task, bool success)? done,TResult? Function( String task, RustError error)? error,}) {final _that = this; +switch (_that) { +case BackgroundResult_Start() when start != null: +return start(_that.task);case BackgroundResult_Done() when done != null: +return done(_that.task,_that.success);case BackgroundResult_Error() when error != null: +return error(_that.task,_that.error);case _: + return null; + +} +} + +} + +/// @nodoc + + +class BackgroundResult_Start extends BackgroundResult { + const BackgroundResult_Start({required this.task}): super._(); + + +@override final String task; + +/// Create a copy of BackgroundResult +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BackgroundResult_StartCopyWith get copyWith => _$BackgroundResult_StartCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BackgroundResult_Start&&(identical(other.task, task) || other.task == task)); +} + + +@override +int get hashCode => Object.hash(runtimeType,task); + +@override +String toString() { + return 'BackgroundResult.start(task: $task)'; +} + + +} + +/// @nodoc +abstract mixin class $BackgroundResult_StartCopyWith<$Res> implements $BackgroundResultCopyWith<$Res> { + factory $BackgroundResult_StartCopyWith(BackgroundResult_Start value, $Res Function(BackgroundResult_Start) _then) = _$BackgroundResult_StartCopyWithImpl; +@override @useResult +$Res call({ + String task +}); + + + + +} +/// @nodoc +class _$BackgroundResult_StartCopyWithImpl<$Res> + implements $BackgroundResult_StartCopyWith<$Res> { + _$BackgroundResult_StartCopyWithImpl(this._self, this._then); + + final BackgroundResult_Start _self; + final $Res Function(BackgroundResult_Start) _then; + +/// Create a copy of BackgroundResult +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? task = null,}) { + return _then(BackgroundResult_Start( +task: null == task ? _self.task : task // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class BackgroundResult_Done extends BackgroundResult { + const BackgroundResult_Done({required this.task, required this.success}): super._(); + + +@override final String task; + final bool success; + +/// Create a copy of BackgroundResult +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BackgroundResult_DoneCopyWith get copyWith => _$BackgroundResult_DoneCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BackgroundResult_Done&&(identical(other.task, task) || other.task == task)&&(identical(other.success, success) || other.success == success)); +} + + +@override +int get hashCode => Object.hash(runtimeType,task,success); + +@override +String toString() { + return 'BackgroundResult.done(task: $task, success: $success)'; +} + + +} + +/// @nodoc +abstract mixin class $BackgroundResult_DoneCopyWith<$Res> implements $BackgroundResultCopyWith<$Res> { + factory $BackgroundResult_DoneCopyWith(BackgroundResult_Done value, $Res Function(BackgroundResult_Done) _then) = _$BackgroundResult_DoneCopyWithImpl; +@override @useResult +$Res call({ + String task, bool success +}); + + + + +} +/// @nodoc +class _$BackgroundResult_DoneCopyWithImpl<$Res> + implements $BackgroundResult_DoneCopyWith<$Res> { + _$BackgroundResult_DoneCopyWithImpl(this._self, this._then); + + final BackgroundResult_Done _self; + final $Res Function(BackgroundResult_Done) _then; + +/// Create a copy of BackgroundResult +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? task = null,Object? success = null,}) { + return _then(BackgroundResult_Done( +task: null == task ? _self.task : task // ignore: cast_nullable_to_non_nullable +as String,success: null == success ? _self.success : success // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +/// @nodoc + + +class BackgroundResult_Error extends BackgroundResult { + const BackgroundResult_Error({required this.task, required this.error}): super._(); + + +@override final String task; + final RustError error; + +/// Create a copy of BackgroundResult +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BackgroundResult_ErrorCopyWith get copyWith => _$BackgroundResult_ErrorCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BackgroundResult_Error&&(identical(other.task, task) || other.task == task)&&(identical(other.error, error) || other.error == error)); +} + + +@override +int get hashCode => Object.hash(runtimeType,task,error); + +@override +String toString() { + return 'BackgroundResult.error(task: $task, error: $error)'; +} + + +} + +/// @nodoc +abstract mixin class $BackgroundResult_ErrorCopyWith<$Res> implements $BackgroundResultCopyWith<$Res> { + factory $BackgroundResult_ErrorCopyWith(BackgroundResult_Error value, $Res Function(BackgroundResult_Error) _then) = _$BackgroundResult_ErrorCopyWithImpl; +@override @useResult +$Res call({ + String task, RustError error +}); + + + + +} +/// @nodoc +class _$BackgroundResult_ErrorCopyWithImpl<$Res> + implements $BackgroundResult_ErrorCopyWith<$Res> { + _$BackgroundResult_ErrorCopyWithImpl(this._self, this._then); + + final BackgroundResult_Error _self; + final $Res Function(BackgroundResult_Error) _then; + +/// Create a copy of BackgroundResult +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? task = null,Object? error = null,}) { + return _then(BackgroundResult_Error( +task: null == task ? _self.task : task // ignore: cast_nullable_to_non_nullable +as String,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as RustError, + )); +} + + +} + +// dart format on diff --git a/app/lib/bridge/api/error.dart b/app/lib/bridge/api/error.dart index 99afccbe..2389fd4c 100644 --- a/app/lib/bridge/api/error.dart +++ b/app/lib/bridge/api/error.dart @@ -14,12 +14,14 @@ import 'package:stride/bridge/frb_generated.dart'; import 'package:stride/bridge/third_party/stride_backend_git/known_hosts.dart'; // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ErrorKind`, `ExportError`, `ImportError`, `SettingsError` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `source` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `source` // Rust type: RustOpaqueMoi> abstract class RustError implements RustOpaqueInterface { Host? asUnknownHost(); + bool isBackground(); + bool isOutOfFuelTrapCode(); String? pluginName(); diff --git a/app/lib/bridge/api/settings.dart b/app/lib/bridge/api/settings.dart index 965bb1fc..c768b9ab 100644 --- a/app/lib/bridge/api/settings.dart +++ b/app/lib/bridge/api/settings.dart @@ -20,7 +20,6 @@ import 'package:uuid/uuid.dart'; part 'settings.freezed.dart'; // These functions are ignored because they are not marked as `pub`: `application_cache_path`, `application_log_path`, `application_support_path`, `default_repository_name`, `default_theme_mode`, `ssh_key_path` -// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `State` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt` // These functions are ignored (category: IgnoreBecauseOwnerTyShouldIgnore): `default` diff --git a/app/lib/bridge/frb_generated.dart b/app/lib/bridge/frb_generated.dart index 7e722243..418cb360 100644 --- a/app/lib/bridge/frb_generated.dart +++ b/app/lib/bridge/frb_generated.dart @@ -13,6 +13,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:stride/bridge/api/background.dart'; import 'package:stride/bridge/api/error.dart'; import 'package:stride/bridge/api/filter.dart'; import 'package:stride/bridge/api/git.dart'; @@ -85,7 +86,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => 1935758670; + int get rustContentHash => 174991090; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -189,6 +190,8 @@ abstract class RustLibApi extends BaseApi { Host? crateApiErrorRustErrorAsUnknownHost({required RustError that}); + bool crateApiErrorRustErrorIsBackground({required RustError that}); + bool crateApiErrorRustErrorIsOutOfFuelTrapCode({required RustError that}); String? crateApiErrorRustErrorPluginName({required RustError that}); @@ -238,6 +241,8 @@ abstract class RustLibApi extends BaseApi { Future crateApiLoggingError({required String message}); + Future crateApiBackgroundExecute({required String task}); + Future crateApiFilterFilterDefault(); Future crateApiLoggingGetLogs(); @@ -248,6 +253,8 @@ abstract class RustLibApi extends BaseApi { Future crateApiLoggingInfo({required String message}); + Stream crateApiBackgroundInit(); + Future strideBackendGitKnownHostsKnownHostsDefault(); Future strideBackendGitKnownHostsKnownHostsLoad(); @@ -1254,7 +1261,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - bool crateApiErrorRustErrorIsOutOfFuelTrapCode({required RustError that}) { + bool crateApiErrorRustErrorIsBackground({required RustError that}) { return handler.executeSync( SyncTask( callFfi: () { @@ -1269,6 +1276,35 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_bool, decodeErrorData: null, ), + constMeta: kCrateApiErrorRustErrorIsBackgroundConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiErrorRustErrorIsBackgroundConstMeta => + const TaskConstMeta( + debugName: 'RustError_is_background', + argNames: ['that'], + ); + + @override + bool crateApiErrorRustErrorIsOutOfFuelTrapCode({required RustError that}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRustError( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: null, + ), constMeta: kCrateApiErrorRustErrorIsOutOfFuelTrapCodeConstMeta, argValues: [that], apiImpl: this, @@ -1292,7 +1328,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -1321,7 +1357,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1349,7 +1385,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 29, + funcId: 30, port: port_, ); }, @@ -1379,7 +1415,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1405,7 +1441,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 31, + funcId: 32, port: port_, ); }, @@ -1438,7 +1474,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 32, + funcId: 33, port: port_, ); }, @@ -1476,7 +1512,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 33, + funcId: 34, port: port_, ); }, @@ -1509,7 +1545,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 35)!; }, codec: SseCodec( decodeSuccessData: sse_decode_Uuid, @@ -1537,7 +1573,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 35, + funcId: 36, port: port_, ); }, @@ -1564,7 +1600,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 36, + funcId: 37, port: port_, ); }, @@ -1594,7 +1630,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 37, + funcId: 38, port: port_, ); }, @@ -1624,7 +1660,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 39, port: port_, ); }, @@ -1656,7 +1692,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 40, port: port_, ); }, @@ -1695,7 +1731,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 41, port: port_, ); }, @@ -1727,7 +1763,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 42, port: port_, ); }, @@ -1756,7 +1792,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 43, port: port_, ); }, @@ -1774,6 +1810,35 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiLoggingErrorConstMeta => const TaskConstMeta(debugName: 'error', argNames: ['message']); + @override + Future crateApiBackgroundExecute({required String task}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(task, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 44, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRustError, + ), + constMeta: kCrateApiBackgroundExecuteConstMeta, + argValues: [task], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiBackgroundExecuteConstMeta => + const TaskConstMeta(debugName: 'execute', argNames: ['task']); + @override Future crateApiFilterFilterDefault() { return handler.executeNormal( @@ -1783,7 +1848,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 45, port: port_, ); }, @@ -1810,7 +1875,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 46, port: port_, ); }, @@ -1835,7 +1900,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_host_key_type(keyType, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 45)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 47)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1863,7 +1928,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 48, port: port_, ); }, @@ -1892,7 +1957,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 47, + funcId: 49, port: port_, ); }, @@ -1910,6 +1975,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiLoggingInfoConstMeta => const TaskConstMeta(debugName: 'info', argNames: ['message']); + @override + Stream crateApiBackgroundInit() { + final streamSink = RustStreamSink(); + unawaited( + handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_StreamSink_background_result_Sse(streamSink, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 50, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateApiBackgroundInitConstMeta, + argValues: [streamSink], + apiImpl: this, + ), + ), + ); + return streamSink.stream; + } + + TaskConstMeta get kCrateApiBackgroundInitConstMeta => + const TaskConstMeta(debugName: 'init', argNames: ['streamSink']); + @override Future strideBackendGitKnownHostsKnownHostsDefault() { return handler.executeNormal( @@ -1919,7 +2016,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 48, + funcId: 51, port: port_, ); }, @@ -1946,7 +2043,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 49, + funcId: 52, port: port_, ); }, @@ -1976,7 +2073,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 50, + funcId: 53, port: port_, ); }, @@ -2004,7 +2101,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 51, + funcId: 54, port: port_, ); }, @@ -2032,7 +2129,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 52, + funcId: 55, port: port_, ); }, @@ -2060,7 +2157,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 53, + funcId: 56, port: port_, ); }, @@ -2093,7 +2190,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 54, + funcId: 57, port: port_, ); }, @@ -2126,7 +2223,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 55, + funcId: 58, port: port_, ); }, @@ -2159,7 +2256,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 56, + funcId: 59, port: port_, ); }, @@ -2192,7 +2289,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 57, + funcId: 60, port: port_, ); }, @@ -2225,7 +2322,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 58, + funcId: 61, port: port_, ); }, @@ -2260,7 +2357,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 59, + funcId: 62, port: port_, ); }, @@ -2292,7 +2389,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { manifest, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 60)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 63)!; }, codec: SseCodec( decodeSuccessData: sse_decode_opt_String, @@ -2324,7 +2421,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { manifest, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 61)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 64)!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -2355,7 +2452,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { manifest, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 62)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 65)!; }, codec: SseCodec( decodeSuccessData: sse_decode_manifest_event, @@ -2386,7 +2483,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { manifest, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 63)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 66)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -2417,7 +2514,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { manifest, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 64)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 67)!; }, codec: SseCodec( decodeSuccessData: sse_decode_manifest_permission, @@ -2446,7 +2543,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 65, + funcId: 68, port: port_, ); }, @@ -2475,7 +2572,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 66, + funcId: 69, port: port_, ); }, @@ -2503,7 +2600,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 67, + funcId: 70, port: port_, ); }, @@ -2532,7 +2629,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 68, + funcId: 71, port: port_, ); }, @@ -2561,7 +2658,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 69, + funcId: 72, port: port_, ); }, @@ -2594,7 +2691,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 70, + funcId: 73, port: port_, ); }, @@ -2626,7 +2723,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 71, + funcId: 74, port: port_, ); }, @@ -2653,7 +2750,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 72, + funcId: 75, port: port_, ); }, @@ -2683,7 +2780,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 73, + funcId: 76, port: port_, ); }, @@ -2708,7 +2805,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 74)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 77)!; }, codec: SseCodec( decodeSuccessData: sse_decode_settings, @@ -2734,7 +2831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 75, + funcId: 78, port: port_, ); }, @@ -2762,7 +2859,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 79, port: port_, ); }, @@ -2791,7 +2888,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 80, port: port_, ); }, @@ -2816,7 +2913,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(title, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 78)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 81)!; }, codec: SseCodec( decodeSuccessData: sse_decode_task, @@ -2842,7 +2939,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 82, port: port_, ); }, @@ -2872,7 +2969,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 83, port: port_, ); }, @@ -2899,7 +2996,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 84, port: port_, ); }, @@ -2927,7 +3024,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 85, port: port_, ); }, @@ -2955,7 +3052,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_box_autoadd_task(that, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 83)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 86)!; }, codec: SseCodec( decodeSuccessData: sse_decode_f_32, @@ -2985,7 +3082,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 87, port: port_, ); }, @@ -3013,7 +3110,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 88, port: port_, ); }, @@ -3042,7 +3139,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 89, port: port_, ); }, @@ -3069,7 +3166,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 90, port: port_, ); }, @@ -3097,7 +3194,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 91, port: port_, ); }, @@ -3314,6 +3411,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return Set.from(dco_decode_list_task_status(raw)); } + @protected + RustStreamSink dco_decode_StreamSink_background_result_Sse( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(); + } + @protected RustStreamSink dco_decode_StreamSink_settings_Sse(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -3379,6 +3484,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + BackgroundResult dco_decode_background_result(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return BackgroundResult_Start(task: dco_decode_String(raw[1])); + case 1: + return BackgroundResult_Done( + task: dco_decode_String(raw[1]), + success: dco_decode_bool(raw[2]), + ); + case 2: + return BackgroundResult_Error( + task: dco_decode_String(raw[1]), + error: + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRustError( + raw[2], + ), + ); + default: + throw Exception('unreachable'); + } + } + @protected bool dco_decode_bool(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -4263,6 +4392,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return Set.from(inner); } + @protected + RustStreamSink sse_decode_StreamSink_background_result_Sse( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + throw UnimplementedError('Unreachable ()'); + } + @protected RustStreamSink sse_decode_StreamSink_settings_Sse( SseDeserializer deserializer, @@ -4332,6 +4469,31 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + BackgroundResult sse_decode_background_result(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + final tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + final var_task = sse_decode_String(deserializer); + return BackgroundResult_Start(task: var_task); + case 1: + final var_task = sse_decode_String(deserializer); + final var_success = sse_decode_bool(deserializer); + return BackgroundResult_Done(task: var_task, success: var_success); + case 2: + final var_task = sse_decode_String(deserializer); + final var_error = + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRustError( + deserializer, + ); + return BackgroundResult_Error(task: var_task, error: var_error); + default: + throw UnimplementedError(''); + } + } + @protected bool sse_decode_bool(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5431,6 +5593,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_task_status(self.toList(), serializer); } + @protected + void sse_encode_StreamSink_background_result_Sse( + RustStreamSink self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String( + self.setupAndSerialize( + codec: SseCodec( + decodeSuccessData: sse_decode_background_result, + decodeErrorData: sse_decode_AnyhowException, + ), + ), + serializer, + ); + } + @protected void sse_encode_StreamSink_settings_Sse( RustStreamSink self, @@ -5506,6 +5685,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.config, serializer); } + @protected + void sse_encode_background_result( + BackgroundResult self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case BackgroundResult_Start(task: final task): + sse_encode_i_32(0, serializer); + sse_encode_String(task, serializer); + case BackgroundResult_Done(task: final task, success: final success): + sse_encode_i_32(1, serializer); + sse_encode_String(task, serializer); + sse_encode_bool(success, serializer); + case BackgroundResult_Error(task: final task, error: final error): + sse_encode_i_32(2, serializer); + sse_encode_String(task, serializer); + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerRustError( + error, + serializer, + ); + } + } + @protected void sse_encode_bool(bool self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6466,6 +6669,9 @@ class RustErrorImpl extends RustOpaque implements RustError { Host? asUnknownHost() => RustLib.instance.api.crateApiErrorRustErrorAsUnknownHost(that: this); + bool isBackground() => + RustLib.instance.api.crateApiErrorRustErrorIsBackground(that: this); + bool isOutOfFuelTrapCode() => RustLib.instance.api .crateApiErrorRustErrorIsOutOfFuelTrapCode(that: this); diff --git a/app/lib/bridge/frb_generated.io.dart b/app/lib/bridge/frb_generated.io.dart index 053f6335..433222ec 100644 --- a/app/lib/bridge/frb_generated.io.dart +++ b/app/lib/bridge/frb_generated.io.dart @@ -14,6 +14,7 @@ import 'dart:convert'; import 'dart:ffi' as ffi; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; +import 'package:stride/bridge/api/background.dart'; import 'package:stride/bridge/api/error.dart'; import 'package:stride/bridge/api/filter.dart'; import 'package:stride/bridge/api/git.dart'; @@ -158,6 +159,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Set dco_decode_Set_task_status_None(dynamic raw); + @protected + RustStreamSink dco_decode_StreamSink_background_result_Sse( + dynamic raw, + ); + @protected RustStreamSink dco_decode_StreamSink_settings_Sse(dynamic raw); @@ -179,6 +185,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BackendRecord dco_decode_backend_record(dynamic raw); + @protected + BackgroundResult dco_decode_background_result(dynamic raw); + @protected bool dco_decode_bool(dynamic raw); @@ -540,6 +549,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Set sse_decode_Set_task_status_None(SseDeserializer deserializer); + @protected + RustStreamSink sse_decode_StreamSink_background_result_Sse( + SseDeserializer deserializer, + ); + @protected RustStreamSink sse_decode_StreamSink_settings_Sse( SseDeserializer deserializer, @@ -565,6 +579,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BackendRecord sse_decode_backend_record(SseDeserializer deserializer); + @protected + BackgroundResult sse_decode_background_result(SseDeserializer deserializer); + @protected bool sse_decode_bool(SseDeserializer deserializer); @@ -993,6 +1010,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_StreamSink_background_result_Sse( + RustStreamSink self, + SseSerializer serializer, + ); + @protected void sse_encode_StreamSink_settings_Sse( RustStreamSink self, @@ -1023,6 +1046,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_backend_record(BackendRecord self, SseSerializer serializer); + @protected + void sse_encode_background_result( + BackgroundResult self, + SseSerializer serializer, + ); + @protected void sse_encode_bool(bool self, SseSerializer serializer); diff --git a/app/lib/main.dart b/app/lib/main.dart index 4f9be08b..bd4892e2 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import 'package:stride/background.dart'; import 'package:stride/blocs/dialog_bloc.dart'; import 'package:stride/blocs/log_bloc.dart'; import 'package:stride/blocs/plugin_manager_bloc.dart'; @@ -46,6 +47,8 @@ Future main() async { ), ); + await Background.init(); + final pluginPath = path.join(supportPath, 'plugins'); await pm.load(pluginPath: pluginPath); final plugins = await pm.pluginManifests(); diff --git a/app/lib/routes/tasks_route.dart b/app/lib/routes/tasks_route.dart index 59d4c7a3..b48eb2c9 100644 --- a/app/lib/routes/tasks_route.dart +++ b/app/lib/routes/tasks_route.dart @@ -2,10 +2,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:stride/background.dart'; import 'package:stride/blocs/plugin_manager_bloc.dart'; import 'package:stride/blocs/settings_bloc.dart'; import 'package:stride/blocs/tasks_bloc.dart'; import 'package:stride/bridge/api/filter.dart'; +import 'package:stride/bridge/api/settings.dart'; import 'package:stride/bridge/third_party/stride_core/event.dart'; import 'package:stride/bridge/third_party/stride_core/task.dart'; import 'package:stride/routes/initial_route.dart'; @@ -14,6 +16,7 @@ import 'package:stride/routes/task_filter_route.dart'; import 'package:stride/routes/task_route.dart'; import 'package:stride/utils/functions.dart'; import 'package:stride/widgets/custom_app_bar.dart'; +import 'package:stride/widgets/infinite_rotation_animation.dart'; import 'package:stride/widgets/task_item_widget.dart'; class TasksRoute extends StatefulWidget { @@ -157,7 +160,21 @@ class _TasksRouteState extends State { ); } - Drawer _drawer() { + CustomDrawer _drawer() { + return CustomDrawer(); + } +} + +class CustomDrawer extends StatefulWidget { + const CustomDrawer({super.key}); + + @override + State createState() => _CustomDrawerState(); +} + +class _CustomDrawerState extends State { + @override + Widget build(BuildContext context) { final taskBloc = context.read(); return Drawer( child: SingleChildScrollView( @@ -194,110 +211,140 @@ class _TasksRouteState extends State { ], ), const Divider(), - ListView.builder( - shrinkWrap: true, - itemCount: repositories.length, - itemBuilder: (context, index) { - final repository = repositories[index]; - final selected = - repository.uuid == taskBloc.repositoryUuid; - return Card( - child: ListTile( - title: Text(repository.name), - subtitle: Text(repository.uuid.toString()), - subtitleTextStyle: const TextStyle(fontSize: 8), - selected: selected, - selectedColor: Colors.amber[900], - onTap: selected - ? null - : () => context.read().add( - SettingsUpdateEvent( - settings: settings.copyWith( - currentRepository: repository.uuid, - ), - ), - ), - onLongPress: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => RepositoryRoute( - repositoryUuid: repository.uuid, - ), - ), - ); - }, - ), - ); - }, - ), + _repositories(repositories, taskBloc, settings), const SizedBox(height: 16), const Text( 'Filters', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), const Divider(), - ListView.builder( - shrinkWrap: true, - itemCount: filters.length, - itemBuilder: (context, index) { - final filter = filters[index]; - var selected = false; - if (settings.selectedFilter - is FilterSelection_Predefined) { - final predefined = - settings.selectedFilter! - as FilterSelection_Predefined; - selected = filter.uuid == predefined.uuid; - } - return Card( - child: ListTile( - title: Text(filter.name), - selected: selected, - selectedColor: Colors.amber[900], - onLongPress: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - TaskFilterRoute(filter: filter), - ), - ); - }, - onTap: () { - if (selected) { - context.read().add( - SettingsUpdateEvent( - settings: settings.copyWith( - selectedFilter: null, - ), - ), - ); - context.read().add(TaskFilterEvent()); - return; - } + _filters(filters, settings), + ], + ), + ); + }, + ), + ), + ); + } - context.read().add( - SettingsUpdateEvent( - settings: settings.copyWith( - selectedFilter: FilterSelection.predefined( - uuid: filter.uuid, - ), + StreamBuilder _repositories( + List repositories, + TaskBloc taskBloc, + Settings settings, + ) { + return StreamBuilder>( + stream: Background.stream(), + builder: (context, asyncSnapshot) { + return ListView.builder( + shrinkWrap: true, + itemCount: repositories.length, + itemBuilder: (context, index) { + final repository = repositories[index]; + final selected = repository.uuid == taskBloc.repositoryUuid; + return Card( + child: ListTile( + title: Wrap( + children: [ + Text(repository.name), + if (!(asyncSnapshot.data + ?.lookup( + Output( + name: Name( + method: 'task.sync', + unique: repository.uuid.toString(), ), + inputData: {}, ), - ); - context.read().add( - TaskFilterEvent(filter: filter), - ); - }, + ) + ?.done ?? + true)) + IconButton( + onPressed: () {}, + icon: const InfiniteRotationAnimation( + child: Icon(Icons.sync), ), - ); - }, - ), - ], + ), + ], + ), + subtitle: Text(repository.uuid.toString()), + subtitleTextStyle: const TextStyle(fontSize: 8), + selected: selected, + selectedColor: Colors.amber[900], + onTap: selected + ? null + : () => context.read().add( + SettingsUpdateEvent( + settings: settings.copyWith( + currentRepository: repository.uuid, + ), + ), + ), + onLongPress: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + RepositoryRoute(repositoryUuid: repository.uuid), + ), + ); + }, ), ); }, - ), - ), + ); + }, + ); + } + + ListView _filters(List filters, Settings settings) { + return ListView.builder( + shrinkWrap: true, + itemCount: filters.length, + itemBuilder: (context, index) { + final filter = filters[index]; + var selected = false; + if (settings.selectedFilter is FilterSelection_Predefined) { + final predefined = + settings.selectedFilter! as FilterSelection_Predefined; + selected = filter.uuid == predefined.uuid; + } + return Card( + child: ListTile( + title: Text(filter.name), + selected: selected, + selectedColor: Colors.amber[900], + onLongPress: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => TaskFilterRoute(filter: filter), + ), + ); + }, + onTap: () { + if (selected) { + context.read().add( + SettingsUpdateEvent( + settings: settings.copyWith(selectedFilter: null), + ), + ); + context.read().add(TaskFilterEvent()); + return; + } + + context.read().add( + SettingsUpdateEvent( + settings: settings.copyWith( + selectedFilter: FilterSelection.predefined( + uuid: filter.uuid, + ), + ), + ), + ); + context.read().add(TaskFilterEvent(filter: filter)); + }, + ), + ); + }, ); } } diff --git a/app/pubspec.lock b/app/pubspec.lock index c739c7fd..7c090425 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -42,7 +42,7 @@ packages: source: hosted version: "2.7.0" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" @@ -932,6 +932,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + workmanager: + dependency: "direct main" + description: + name: workmanager + sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10" + url: "https://pub.dev" + source: hosted + version: "0.9.0+3" + workmanager_android: + dependency: transitive + description: + name: workmanager_android + sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7" + url: "https://pub.dev" + source: hosted + version: "0.9.0+2" + workmanager_apple: + dependency: transitive + description: + name: workmanager_apple + sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca" + url: "https://pub.dev" + source: hosted + version: "0.9.1+2" + workmanager_platform_interface: + dependency: transitive + description: + name: workmanager_platform_interface + sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47 + url: "https://pub.dev" + source: hosted + version: "0.9.1+1" xdg_directories: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 0763b631..a04b3d34 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -31,6 +31,8 @@ dependencies: intersperse: ^2.0.0 http: ^1.6.0 flutter_native_splash: ^2.4.7 + workmanager: ^0.9.0+3 + async: ^2.13.0 dev_dependencies: flutter_test: diff --git a/crates/backends/git/src/lib.rs b/crates/backends/git/src/lib.rs index d61216cf..edda5381 100644 --- a/crates/backends/git/src/lib.rs +++ b/crates/backends/git/src/lib.rs @@ -310,7 +310,7 @@ impl GitBackend { self.push(&repository, true)?; } - log::info!("Repository {} cloned successfully!", &self.config.origin); + log::info!("Repository {} cloned successfully!", self.config.origin); Ok(()) } diff --git a/crates/background/Cargo.toml b/crates/background/Cargo.toml new file mode 100644 index 00000000..9f4ed0e1 --- /dev/null +++ b/crates/background/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "stride_background" +description = "Stride's background task implementation" +keywords = ["stride"] +categories = [] +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +log.workspace = true +thiserror.workspace = true +async-trait.workspace = true +tokio = { workspace = true, features = ["full"] } + +[dev-dependencies] + +[features] + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true diff --git a/crates/background/src/error.rs b/crates/background/src/error.rs new file mode 100644 index 00000000..697db7f7 --- /dev/null +++ b/crates/background/src/error.rs @@ -0,0 +1,36 @@ +use std::sync::Arc; + +pub type Result = std::result::Result; + +pub trait ToBackgroundError: std::any::Any + std::error::Error + Send + Sync + 'static {} + +impl From for Error { + fn from(value: T) -> Self { + Self::Task(Arc::new(value)) + } +} + +#[derive(thiserror::Error, Debug, Clone)] +pub enum Error { + #[error("background thread is closed")] + BackgroundThreadClosed, + #[error("task error: {0}")] + Task(#[source] Arc), +} + +impl Error { + #[must_use] + pub fn as_task_error(&self) -> Option<&Arc> { + if let Self::Task(error) = &self { + return Some(error); + } + None + } + + #[must_use] + pub fn downcast_ref(&self) -> Option<&T> { + let task_error = self.as_task_error()?; + let x: &dyn std::any::Any = task_error.as_ref(); + x.downcast_ref::() + } +} diff --git a/crates/background/src/lib.rs b/crates/background/src/lib.rs new file mode 100644 index 00000000..633da39d --- /dev/null +++ b/crates/background/src/lib.rs @@ -0,0 +1,223 @@ +//! Stride's background task crate implementation. + +use std::{ + collections::HashMap, + fmt::{Debug, Display}, + str::FromStr, + sync::Arc, + thread::JoinHandle, + time::Duration, +}; + +use async_trait::async_trait; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +pub use crate::error::{Error, Result, ToBackgroundError}; + +mod error; + +#[cfg(test)] +mod tests; + +pub trait Specification: Debug { + fn initial_delay(&self) -> Option { + None + } + + fn frequency(&self) -> Option { + None + } + + fn tag(&self) -> Option> { + None + } +} + +#[async_trait] +pub trait AsyncRunnable: Specification + Send + 'static { + /// Run the task. + /// + /// # Errors + /// + /// This function returns an error if the task fails to run. + async fn run(&mut self) -> Result; +} + +pub trait Runnable: Specification + Send + 'static { + /// Run the task. + /// + /// # Errors + /// + /// This function returns an error if the task fails to run. + fn run(&mut self) -> Result; +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Name { + pub method: Arc, + pub unique: Option>, +} + +impl Display for Name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(unique) = &self.unique { + write!(f, "{}:{unique}", self.method) + } else { + f.write_str(&self.method) + } + } +} + +impl FromStr for Name { + type Err = Error; + fn from_str(s: &str) -> std::result::Result { + let Some((method, unique)) = s.split_once(':') else { + return Ok(Name { + method: s.into(), + unique: None, + }); + }; + Ok(Name { + method: method.into(), + unique: Some(unique.into()), + }) + } +} + +#[derive(Debug)] +enum Message { + Task { + name: Name, + task: Box, + }, + Close, +} + +async fn background_main(hook: Arc, mut receiver: UnboundedReceiver) { + let mut tasks: HashMap>, Vec>> = HashMap::new(); + while let Some(message) = receiver.recv().await { + match message { + Message::Task { name, mut task } => { + let hook = hook.clone(); + let tag = task.tag(); + let join = tokio::spawn(async move { + if let Some(duration) = task.initial_delay() { + tokio::time::sleep(duration).await; + } + + loop { + hook.on_task_start(name.clone()); + + let result = task.run().await; + hook.on_task_result(name.clone(), result); + + let Some(frequency) = task.frequency() else { + break; + }; + + log::trace!("Attempt :: {name} :: {frequency:?}"); + tokio::time::sleep(frequency).await; + } + }); + + tasks.entry(tag).or_default().push(join); + } + Message::Close => break, + } + } + + for (_, joins) in tasks { + for join in joins { + join.abort(); + } + } +} + +fn background_thread(hook: Arc, receiver: UnboundedReceiver) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async move { + background_main(hook, receiver).await; + }); +} + +#[derive(Debug)] +pub struct Background { + sender: UnboundedSender, + #[allow(unused)] + join_handle: Option>, +} + +impl Background { + #[must_use] + pub fn new(hook: Arc) -> Self { + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); + + let join_handle = std::thread::spawn(move || background_thread(hook, receiver)); + + Self { + sender, + join_handle: join_handle.into(), + } + } + + /// Enqueue a task to be run in the background. + /// + /// # Errors + /// + /// This function returns an error if the background thread has been closed. + pub fn enqueue(&mut self, name: Name, task: Box) -> Result<()> { + self.sender + .send(Message::Task { name, task }) + .map_err(|_| Error::BackgroundThreadClosed)?; + Ok(()) + } +} + +impl Drop for Background { + fn drop(&mut self) { + drop(self.sender.send(Message::Close)); + if let Some(join_handle) = self.join_handle.take() { + join_handle.join().unwrap(); + } + } +} + +#[derive(Debug)] +struct AsyncWrapper { + runnable: Box, +} + +impl Specification for AsyncWrapper { + fn initial_delay(&self) -> Option { + self.runnable.initial_delay() + } + fn frequency(&self) -> Option { + self.runnable.frequency() + } + fn tag(&self) -> Option> { + self.runnable.tag() + } +} + +#[async_trait] +impl AsyncRunnable for AsyncWrapper { + async fn run(&mut self) -> Result { + self.runnable.run() + } +} + +impl From> for Box { + fn from(runnable: Box) -> Self { + Box::new(AsyncWrapper { runnable }) + } +} + +pub trait Reactor: Debug + Send + Sync + 'static { + fn on_task_start(&self, name: Name) { + drop(name); + } + fn on_task_result(&self, name: Name, result: Result) { + drop(name); + drop(result); + } +} diff --git a/crates/background/src/tests.rs b/crates/background/src/tests.rs new file mode 100644 index 00000000..bf0baa9d --- /dev/null +++ b/crates/background/src/tests.rs @@ -0,0 +1,183 @@ +use std::{ + fmt::Display, + sync::{ + Arc, Condvar, Mutex, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; + +use async_trait::async_trait; + +use crate::{ + AsyncRunnable, Background, Name, Reactor, Specification, + error::{Result, ToBackgroundError}, +}; + +#[derive(Debug, Default)] +struct Waitable { + mutex: Mutex<()>, + condvar: Condvar, + flag: AtomicBool, +} + +#[allow(unused)] +impl Waitable { + fn new() -> Self { + Self::default() + } + + fn wait_for(&self, dur: Duration) -> bool { + if self.flag.load(Ordering::SeqCst) { + let lock = self.mutex.lock().unwrap(); + let (_lock, result) = self.condvar.wait_timeout(lock, dur).unwrap(); + result.timed_out() + } else { + false + } + } + + fn notify(&self) { + if self + .flag + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + == Ok(true) + { + self.condvar.notify_all(); + } + } + + fn reset(&self) { + if self + .flag + .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) + == Ok(false) + {} + } +} + +#[derive(Debug)] +struct State { + waitable: Waitable, + result: Mutex>, +} + +#[derive(Debug)] +struct TestTask { + state: Arc, +} + +impl Specification for TestTask {} + +#[async_trait] +impl AsyncRunnable for TestTask { + async fn run(&mut self) -> Result { + self.state.waitable.notify(); + self.state.result.lock().unwrap().clone() + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +struct TestError {} + +impl Display for TestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:#?}") + } +} + +impl std::error::Error for TestError {} + +impl ToBackgroundError for TestError {} + +#[derive(Debug)] +struct TestHook { + tx: std::sync::mpsc::Sender<(Name, Result)>, +} + +impl Reactor for TestHook { + fn on_task_result(&self, task_name: Name, result: Result) { + self.tx.send((task_name, result)).unwrap(); + } +} + +#[test] +fn run_task_success() { + let name = Name { + method: "task.sync".into(), + unique: None, + }; + + let state = Arc::new(State { + waitable: Waitable::new(), + result: Mutex::new(Ok(true)), + }); + let (tx, rx) = std::sync::mpsc::channel(); + let hook = Arc::new(TestHook { tx }); + let mut background = Background::new(hook); + background + .enqueue( + name.clone(), + Box::new(TestTask { + state: state.clone(), + }), + ) + .unwrap(); + + assert!( + !state.waitable.wait_for(Duration::from_secs(3)), + "task did not run" + ); + + let Ok((task_name, result)) = rx.recv_timeout(Duration::from_secs(5)) else { + panic!("task should generate output"); + }; + + let Ok(success) = result else { + panic!("expected output done, got: {result:#?}"); + }; + + assert_eq!(task_name, name); + assert!(success); +} + +#[test] +fn run_task_error() { + let name = Name { + method: "task.sync".into(), + unique: None, + }; + + let state = Arc::new(State { + waitable: Waitable::new(), + result: Mutex::new(Err(TestError {}.into())), + }); + let (tx, rx) = std::sync::mpsc::channel(); + let hook = Arc::new(TestHook { tx }); + let mut background = Background::new(hook); + background + .enqueue( + name.clone(), + Box::new(TestTask { + state: state.clone(), + }), + ) + .unwrap(); + + assert!( + !state.waitable.wait_for(Duration::from_secs(3)), + "task did not run" + ); + + let Ok((task_name, result)) = rx.recv_timeout(Duration::from_secs(5)) else { + panic!("task should generate output"); + }; + + let Err(error) = result else { + panic!("expected output done, got: {result:#?}"); + }; + + assert_eq!(task_name, name); + + assert_eq!(error.downcast_ref::(), Some(&TestError {})); +} diff --git a/crates/flutter_bridge/Cargo.toml b/crates/flutter_bridge/Cargo.toml index 06abb397..9debd0a4 100644 --- a/crates/flutter_bridge/Cargo.toml +++ b/crates/flutter_bridge/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["lib", "cdylib", "staticlib"] [dependencies] stride_core = { workspace = true, features = ["taskchampion"] } +stride_background.workspace = true stride_crdt.workspace = true stride_logging.workspace = true stride_database.workspace = true diff --git a/crates/flutter_bridge/src/api/background.rs b/crates/flutter_bridge/src/api/background.rs new file mode 100644 index 00000000..2c4808c2 --- /dev/null +++ b/crates/flutter_bridge/src/api/background.rs @@ -0,0 +1,128 @@ +use flutter_rust_bridge::frb; +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, LazyLock, Mutex}; +use stride_background::{Background, Name, Reactor}; +use uuid::Uuid; + +use crate::{ErrorKind, RustError, api::repository::Repository, frb_generated::StreamSink}; + +static STATE: LazyLock>> = LazyLock::new(Mutex::default); + +#[frb(ignore)] +struct State { + #[allow(unused)] + background: Background, + stream_sink: StreamSink, +} + +#[derive(Debug)] +#[frb(ignore)] +struct BridgeHook; + +impl Reactor for BridgeHook { + fn on_task_start(&self, name: Name) { + STATE.clear_poison(); + let mut lock = STATE.lock().unwrap(); + let Some(state) = lock.as_mut() else { + return; + }; + + let result = state.stream_sink.add(BackgroundResult::Start { + task: name.to_string(), + }); + + drop(result); + } + + fn on_task_result(&self, name: Name, result: Result) { + STATE.clear_poison(); + let mut lock = STATE.lock().unwrap(); + let Some(state) = lock.as_mut() else { + return; + }; + + let result = match result { + Ok(success) => BackgroundResult::Done { + task: name.to_string(), + success, + }, + Err(error) => BackgroundResult::Error { + task: name.to_string(), + error: RustError::from(error), + }, + }; + state.stream_sink.add(result).unwrap(); + } +} + +#[frb(non_opaque)] +pub enum BackgroundResult { + Start { task: String }, + Done { task: String, success: bool }, + Error { task: String, error: RustError }, +} + +pub fn init(stream_sink: StreamSink) { + STATE.clear_poison(); + let mut lock = STATE.lock().unwrap(); + + match lock.as_mut() { + Some(state) => { + // TODO: clear previous tasks + // state.background.clear(); + state.stream_sink = stream_sink; + } + None => { + *lock = Some(State { + background: Background::new(Arc::new(BridgeHook)), + stream_sink, + }); + } + } +} + +#[frb(ignore)] +#[derive(Debug, Serialize, Deserialize)] +struct RepositorySpec { + id: Uuid, +} + +#[frb(ignore)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "method", content = "params")] +#[serde(rename_all = "kebab-case")] +enum Method { + #[serde(rename = "task.sync")] + TaskSync { repository: RepositorySpec }, +} + +#[frb(ignore)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct BgTask { + #[serde(flatten)] + method: Method, +} + +#[frb(ignore)] +#[derive(Debug)] +struct TaskSync { + repository: RepositorySpec, +} + +pub fn execute(task: &str) -> Result<(), RustError> { + let task = serde_json::from_str::(task).map_err(|e| { + RustError::from(ErrorKind::Other { + message: format!("invalid background task payload: {e}").into(), + }) + })?; + + match task.method { + Method::TaskSync { repository } => { + let mut repository = Repository::open(repository.id)?; + repository.sync()?; + } + } + + Ok(()) +} diff --git a/crates/flutter_bridge/src/api/error.rs b/crates/flutter_bridge/src/api/error.rs index 2b6ef36b..69cfd4a3 100644 --- a/crates/flutter_bridge/src/api/error.rs +++ b/crates/flutter_bridge/src/api/error.rs @@ -82,6 +82,8 @@ pub enum ErrorKind { Backend(#[from] BackendError), #[error("ssh error: {0}")] Ssh(#[from] SshError), + #[error("background task: {0}")] + Background(#[from] stride_background::Error), #[error("other error: {message}")] Other { message: Box }, } @@ -141,6 +143,16 @@ impl RustError { error.plugin_name().map(Into::into) } + + #[frb(sync)] + #[must_use] + pub fn is_background(&self) -> bool { + let ErrorKind::Background(_) = self.repr.as_ref() else { + return false; + }; + + true + } } impl> From for RustError { diff --git a/crates/flutter_bridge/src/api/mod.rs b/crates/flutter_bridge/src/api/mod.rs index 4cf30bcd..3f0d1b8f 100644 --- a/crates/flutter_bridge/src/api/mod.rs +++ b/crates/flutter_bridge/src/api/mod.rs @@ -2,6 +2,7 @@ // Do not put code in `mod.rs`, but put in e.g. `simple.rs`. // +pub mod background; pub mod error; pub mod filter; pub mod git; diff --git a/crates/flutter_bridge/src/frb_generated.rs b/crates/flutter_bridge/src/frb_generated.rs index 9453e8b8..041bdccc 100644 --- a/crates/flutter_bridge/src/frb_generated.rs +++ b/crates/flutter_bridge/src/frb_generated.rs @@ -42,7 +42,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1935758670; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 174991090; // Section: executor @@ -1185,6 +1185,54 @@ fn wire__crate__api__error__RustError_as_unknown_host_impl( }, ) } +fn wire__crate__api__error__RustError_is_background_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "RustError_is_background", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok(crate::api::error::RustError::is_background( + &*api_that_guard, + ))?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__error__RustError_is_out_of_fuel_trap_code_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -1841,6 +1889,39 @@ fn wire__crate__api__logging__error_impl( }, ) } +fn wire__crate__api__background__execute_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "execute", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_task = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, RustError>((move || { + let output_ok = crate::api::background::execute(&api_task)?; + Ok(output_ok) + })()) + } + }, + ) +} fn wire__crate__api__filter__filter_default_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -2005,6 +2086,44 @@ fn wire__crate__api__logging__info_impl( }, ) } +fn wire__crate__api__background__init_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "init", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_stream_sink = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::background::init(api_stream_sink); + })?; + Ok(output_ok) + })()) + } + }, + ) +} fn wire__stride_backend_git__known_hosts__known_hosts_default_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -3744,6 +3863,19 @@ impl SseDecode for std::collections::HashSet { } } +impl SseDecode + for StreamSink< + crate::api::background::BackgroundResult, + flutter_rust_bridge::for_generated::SseCodec, + > +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return StreamSink::deserialize(inner); + } +} + impl SseDecode for StreamSink { @@ -3824,6 +3956,38 @@ impl SseDecode for crate::api::repository::BackendRecord { } } +impl SseDecode for crate::api::background::BackgroundResult { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_task = ::sse_decode(deserializer); + return crate::api::background::BackgroundResult::Start { task: var_task }; + } + 1 => { + let mut var_task = ::sse_decode(deserializer); + let mut var_success = ::sse_decode(deserializer); + return crate::api::background::BackgroundResult::Done { + task: var_task, + success: var_success, + }; + } + 2 => { + let mut var_task = ::sse_decode(deserializer); + let mut var_error = ::sse_decode(deserializer); + return crate::api::background::BackgroundResult::Error { + task: var_task, + error: var_error, + }; + } + _ => { + unimplemented!(""); + } + } + } +} + impl SseDecode for bool { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4716,150 +4880,152 @@ fn pde_ffi_dispatcher_primary_impl( rust_vec_len, data_len, ), - 29 => wire__crate__api__settings__SshKey_generate_impl(port, ptr, rust_vec_len, data_len), - 31 => wire__crate__api__settings__SshKey_remove_key_impl(port, ptr, rust_vec_len, data_len), - 32 => wire__crate__api__settings__SshKey_save_impl(port, ptr, rust_vec_len, data_len), - 33 => wire__crate__api__settings__SshKey_update_impl(port, ptr, rust_vec_len, data_len), - 35 => wire__stride_core__task__annotation__annotation_now_impl( + 30 => wire__crate__api__settings__SshKey_generate_impl(port, ptr, rust_vec_len, data_len), + 32 => wire__crate__api__settings__SshKey_remove_key_impl(port, ptr, rust_vec_len, data_len), + 33 => wire__crate__api__settings__SshKey_save_impl(port, ptr, rust_vec_len, data_len), + 34 => wire__crate__api__settings__SshKey_update_impl(port, ptr, rust_vec_len, data_len), + 36 => wire__stride_core__task__annotation__annotation_now_impl( port, ptr, rust_vec_len, data_len, ), - 36 => wire__crate__api__settings__application_paths_default_impl( + 37 => wire__crate__api__settings__application_paths_default_impl( port, ptr, rust_vec_len, data_len, ), - 37 => { + 38 => { wire__crate__api__plugin_manager__create_stream_impl(port, ptr, rust_vec_len, data_len) } - 38 => wire__crate__api__logging__debug_impl(port, ptr, rust_vec_len, data_len), - 39 => wire__crate__api__plugin_manager__disable_impl(port, ptr, rust_vec_len, data_len), - 40 => wire__crate__api__plugin_manager__emit_impl(port, ptr, rust_vec_len, data_len), - 41 => { + 39 => wire__crate__api__logging__debug_impl(port, ptr, rust_vec_len, data_len), + 40 => wire__crate__api__plugin_manager__disable_impl(port, ptr, rust_vec_len, data_len), + 41 => wire__crate__api__plugin_manager__emit_impl(port, ptr, rust_vec_len, data_len), + 42 => { wire__crate__api__plugin_manager__emit_broadcast_impl(port, ptr, rust_vec_len, data_len) } - 42 => wire__crate__api__logging__error_impl(port, ptr, rust_vec_len, data_len), - 43 => wire__crate__api__filter__filter_default_impl(port, ptr, rust_vec_len, data_len), - 44 => wire__crate__api__logging__get_logs_impl(port, ptr, rust_vec_len, data_len), - 46 => wire__crate__api__plugin_manager__import_impl(port, ptr, rust_vec_len, data_len), - 47 => wire__crate__api__logging__info_impl(port, ptr, rust_vec_len, data_len), - 48 => wire__stride_backend_git__known_hosts__known_hosts_default_impl( + 43 => wire__crate__api__logging__error_impl(port, ptr, rust_vec_len, data_len), + 44 => wire__crate__api__background__execute_impl(port, ptr, rust_vec_len, data_len), + 45 => wire__crate__api__filter__filter_default_impl(port, ptr, rust_vec_len, data_len), + 46 => wire__crate__api__logging__get_logs_impl(port, ptr, rust_vec_len, data_len), + 48 => wire__crate__api__plugin_manager__import_impl(port, ptr, rust_vec_len, data_len), + 49 => wire__crate__api__logging__info_impl(port, ptr, rust_vec_len, data_len), + 50 => wire__crate__api__background__init_impl(port, ptr, rust_vec_len, data_len), + 51 => wire__stride_backend_git__known_hosts__known_hosts_default_impl( port, ptr, rust_vec_len, data_len, ), - 49 => wire__stride_backend_git__known_hosts__known_hosts_load_impl( + 52 => wire__stride_backend_git__known_hosts__known_hosts_load_impl( port, ptr, rust_vec_len, data_len, ), - 50 => wire__stride_backend_git__known_hosts__known_hosts_save_impl( + 53 => wire__stride_backend_git__known_hosts__known_hosts_save_impl( port, ptr, rust_vec_len, data_len, ), - 51 => wire__crate__api__plugin_manager__load_impl(port, ptr, rust_vec_len, data_len), - 52 => wire__stride_plugin_manager__manifest__manifest_event_default_impl( + 54 => wire__crate__api__plugin_manager__load_impl(port, ptr, rust_vec_len, data_len), + 55 => wire__stride_plugin_manager__manifest__manifest_event_default_impl( port, ptr, rust_vec_len, data_len, ), - 53 => wire__stride_plugin_manager__manifest__manifest_event_task_default_impl( + 56 => wire__stride_plugin_manager__manifest__manifest_event_task_default_impl( port, ptr, rust_vec_len, data_len, ), - 54 => wire__stride_plugin_manager__manifest__manifest_event_timer_default_impl( + 57 => wire__stride_plugin_manager__manifest__manifest_event_timer_default_impl( port, ptr, rust_vec_len, data_len, ), - 55 => wire__stride_plugin_manager__manifest__manifest_permission_default_impl( + 58 => wire__stride_plugin_manager__manifest__manifest_permission_default_impl( port, ptr, rust_vec_len, data_len, ), - 56 => wire__stride_plugin_manager__manifest__manifest_permission_network_default_impl( + 59 => wire__stride_plugin_manager__manifest__manifest_permission_network_default_impl( port, ptr, rust_vec_len, data_len, ), - 57 => wire__stride_plugin_manager__manifest__manifest_permission_storage_default_impl( + 60 => wire__stride_plugin_manager__manifest__manifest_permission_storage_default_impl( port, ptr, rust_vec_len, data_len, ), - 58 => wire__stride_plugin_manager__manifest__manifest_permission_task_default_impl( + 61 => wire__stride_plugin_manager__manifest__manifest_permission_task_default_impl( port, ptr, rust_vec_len, data_len, ), - 59 => { + 62 => { wire__crate__api__plugin_manager__parse_plugin_impl(port, ptr, rust_vec_len, data_len) } - 65 => wire__crate__api__plugin_manager__plugin_manifests_impl( + 68 => wire__crate__api__plugin_manager__plugin_manifests_impl( port, ptr, rust_vec_len, data_len, ), - 66 => wire__crate__api__plugin_manager__process_host_event_impl( + 69 => wire__crate__api__plugin_manager__process_host_event_impl( port, ptr, rust_vec_len, data_len, ), - 67 => wire__crate__api__plugin_manager__process_plugin_event_impl( + 70 => wire__crate__api__plugin_manager__process_plugin_event_impl( port, ptr, rust_vec_len, data_len, ), - 68 => wire__crate__api__plugin_manager__remove_impl(port, ptr, rust_vec_len, data_len), - 69 => wire__crate__api__settings__repository_specification_default_impl( + 71 => wire__crate__api__plugin_manager__remove_impl(port, ptr, rust_vec_len, data_len), + 72 => wire__crate__api__settings__repository_specification_default_impl( port, ptr, rust_vec_len, data_len, ), - 70 => wire__crate__api__settings__settings_create_stream_impl( + 73 => wire__crate__api__settings__settings_create_stream_impl( port, ptr, rust_vec_len, data_len, ), - 71 => wire__crate__api__settings__settings_default_impl(port, ptr, rust_vec_len, data_len), - 72 => wire__crate__api__settings__settings_get_impl(port, ptr, rust_vec_len, data_len), - 73 => wire__crate__api__settings__settings_load_impl(port, ptr, rust_vec_len, data_len), - 75 => wire__crate__api__settings__settings_save_impl(port, ptr, rust_vec_len, data_len), - 76 => wire__crate__api__settings__ssh_keys_impl(port, ptr, rust_vec_len, data_len), - 77 => wire__stride_core__task__task_default_impl(port, ptr, rust_vec_len, data_len), - 79 => wire__stride_core__task__task_priority_as_str_impl(port, ptr, rust_vec_len, data_len), - 80 => { + 74 => wire__crate__api__settings__settings_default_impl(port, ptr, rust_vec_len, data_len), + 75 => wire__crate__api__settings__settings_get_impl(port, ptr, rust_vec_len, data_len), + 76 => wire__crate__api__settings__settings_load_impl(port, ptr, rust_vec_len, data_len), + 78 => wire__crate__api__settings__settings_save_impl(port, ptr, rust_vec_len, data_len), + 79 => wire__crate__api__settings__ssh_keys_impl(port, ptr, rust_vec_len, data_len), + 80 => wire__stride_core__task__task_default_impl(port, ptr, rust_vec_len, data_len), + 82 => wire__stride_core__task__task_priority_as_str_impl(port, ptr, rust_vec_len, data_len), + 83 => { wire__stride_core__task__task_priority_default_impl(port, ptr, rust_vec_len, data_len) } - 81 => wire__stride_core__task__task_status_default_impl(port, ptr, rust_vec_len, data_len), - 82 => { + 84 => wire__stride_core__task__task_status_default_impl(port, ptr, rust_vec_len, data_len), + 85 => { wire__stride_core__task__task_status_is_pending_impl(port, ptr, rust_vec_len, data_len) } - 84 => wire__stride_core__task__task_with_id_impl(port, ptr, rust_vec_len, data_len), - 85 => wire__crate__api__plugin_manager__toggle_impl(port, ptr, rust_vec_len, data_len), - 86 => wire__crate__api__logging__trace_impl(port, ptr, rust_vec_len, data_len), - 87 => wire__stride_core__task__uda__uda_default_impl(port, ptr, rust_vec_len, data_len), - 88 => wire__crate__api__logging__warn_impl(port, ptr, rust_vec_len, data_len), + 87 => wire__stride_core__task__task_with_id_impl(port, ptr, rust_vec_len, data_len), + 88 => wire__crate__api__plugin_manager__toggle_impl(port, ptr, rust_vec_len, data_len), + 89 => wire__crate__api__logging__trace_impl(port, ptr, rust_vec_len, data_len), + 90 => wire__stride_core__task__uda__uda_default_impl(port, ptr, rust_vec_len, data_len), + 91 => wire__crate__api__logging__warn_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -4880,44 +5046,45 @@ fn pde_ffi_dispatcher_sync_impl( 6 => wire__stride_core__event__HostEvent_timer_impl(ptr, rust_vec_len, data_len), 14 => wire__crate__api__repository__Repository_open_impl(ptr, rust_vec_len, data_len), 25 => wire__crate__api__error__RustError_as_unknown_host_impl(ptr, rust_vec_len, data_len), - 26 => wire__crate__api__error__RustError_is_out_of_fuel_trap_code_impl( + 26 => wire__crate__api__error__RustError_is_background_impl(ptr, rust_vec_len, data_len), + 27 => wire__crate__api__error__RustError_is_out_of_fuel_trap_code_impl( ptr, rust_vec_len, data_len, ), - 27 => wire__crate__api__error__RustError_plugin_name_impl(ptr, rust_vec_len, data_len), - 28 => wire__crate__api__error__RustError_to_error_string_impl(ptr, rust_vec_len, data_len), - 30 => wire__crate__api__settings__SshKey_public_key_impl(ptr, rust_vec_len, data_len), - 34 => wire__crate__api__settings__SshKey_uuid_impl(ptr, rust_vec_len, data_len), - 45 => wire__crate__api__git__host_key_type_name_impl(ptr, rust_vec_len, data_len), - 60 => wire__crate__api__plugin__plugin_instance_manifest_disabled_reason_impl( + 28 => wire__crate__api__error__RustError_plugin_name_impl(ptr, rust_vec_len, data_len), + 29 => wire__crate__api__error__RustError_to_error_string_impl(ptr, rust_vec_len, data_len), + 31 => wire__crate__api__settings__SshKey_public_key_impl(ptr, rust_vec_len, data_len), + 35 => wire__crate__api__settings__SshKey_uuid_impl(ptr, rust_vec_len, data_len), + 47 => wire__crate__api__git__host_key_type_name_impl(ptr, rust_vec_len, data_len), + 63 => wire__crate__api__plugin__plugin_instance_manifest_disabled_reason_impl( ptr, rust_vec_len, data_len, ), - 61 => wire__crate__api__plugin__plugin_instance_manifest_enabled_impl( + 64 => wire__crate__api__plugin__plugin_instance_manifest_enabled_impl( ptr, rust_vec_len, data_len, ), - 62 => wire__crate__api__plugin__plugin_instance_manifest_event_impl( + 65 => wire__crate__api__plugin__plugin_instance_manifest_event_impl( ptr, rust_vec_len, data_len, ), - 63 => wire__crate__api__plugin__plugin_instance_manifest_name_impl( + 66 => wire__crate__api__plugin__plugin_instance_manifest_name_impl( ptr, rust_vec_len, data_len, ), - 64 => wire__crate__api__plugin__plugin_instance_manifest_permission_impl( + 67 => wire__crate__api__plugin__plugin_instance_manifest_permission_impl( ptr, rust_vec_len, data_len, ), - 74 => wire__crate__api__settings__settings_new_impl(ptr, rust_vec_len, data_len), - 78 => wire__stride_core__task__task_new_impl(ptr, rust_vec_len, data_len), - 83 => wire__stride_core__task__task_urgency_impl(ptr, rust_vec_len, data_len), + 77 => wire__crate__api__settings__settings_new_impl(ptr, rust_vec_len, data_len), + 81 => wire__stride_core__task__task_new_impl(ptr, rust_vec_len, data_len), + 86 => wire__stride_core__task__task_urgency_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -5073,6 +5240,42 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::background::BackgroundResult { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::api::background::BackgroundResult::Start { task } => { + [0.into_dart(), task.into_into_dart().into_dart()].into_dart() + } + crate::api::background::BackgroundResult::Done { task, success } => [ + 1.into_dart(), + task.into_into_dart().into_dart(), + success.into_into_dart().into_dart(), + ] + .into_dart(), + crate::api::background::BackgroundResult::Error { task, error } => [ + 2.into_dart(), + task.into_into_dart().into_dart(), + error.into_into_dart().into_dart(), + ] + .into_dart(), + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::background::BackgroundResult +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::background::BackgroundResult +{ + fn into_into_dart(self) -> crate::api::background::BackgroundResult { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::filter::Filter { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -5736,6 +5939,18 @@ impl SseEncode for std::collections::HashSet { } } +impl SseEncode + for StreamSink< + crate::api::background::BackgroundResult, + flutter_rust_bridge::for_generated::SseCodec, + > +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + unimplemented!("") + } +} + impl SseEncode for StreamSink { @@ -5795,6 +6010,31 @@ impl SseEncode for crate::api::repository::BackendRecord { } } +impl SseEncode for crate::api::background::BackgroundResult { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::api::background::BackgroundResult::Start { task } => { + ::sse_encode(0, serializer); + ::sse_encode(task, serializer); + } + crate::api::background::BackgroundResult::Done { task, success } => { + ::sse_encode(1, serializer); + ::sse_encode(task, serializer); + ::sse_encode(success, serializer); + } + crate::api::background::BackgroundResult::Error { task, error } => { + ::sse_encode(2, serializer); + ::sse_encode(task, serializer); + ::sse_encode(error, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + impl SseEncode for bool { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {