diff --git a/packages/neon/android/app/src/main/AndroidManifest.xml b/packages/neon/android/app/src/main/AndroidManifest.xml
index b004a462..f9d05620 100644
--- a/packages/neon/android/app/src/main/AndroidManifest.xml
+++ b/packages/neon/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,7 @@
package="de.provokateurin.neon">
+
with WidgetsBindingObserver {
WidgetsBinding.instance.window.platformBrightness,
);
+ late FilesSyncBloc _filesSyncBloc;
+
@override
void didChangePlatformBrightness() {
_platformBrightness.add(WidgetsBinding.instance.window.platformBrightness);
@@ -48,6 +52,11 @@ class _NeonAppState extends State with WidgetsBindingObserver {
void initState() {
super.initState();
+ _filesSyncBloc = FilesSyncBloc(
+ Storage('files-sync', widget.sharedPreferences),
+ widget.accountsBloc,
+ );
+
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((final _) {
@@ -92,31 +101,34 @@ class _NeonAppState extends State with WidgetsBindingObserver {
}
@override
- Widget build(final BuildContext context) => StreamBuilder(
- stream: _platformBrightness,
- builder: (final context, final platformBrightnessSnapshot) => StreamBuilder(
- stream: widget.globalOptions.themeMode.stream,
- builder: (final context, final themeModeSnapshot) => StreamBuilder(
- stream: widget.globalOptions.themeOLEDAsDark.stream,
- builder: (final context, final themeOLEDAsDarkSnapshot) {
- if (!platformBrightnessSnapshot.hasData ||
- !themeOLEDAsDarkSnapshot.hasData ||
- !themeModeSnapshot.hasData) {
- return Container();
- }
- return MaterialApp(
- localizationsDelegates: AppLocalizations.localizationsDelegates,
- supportedLocales: AppLocalizations.supportedLocales,
- navigatorKey: _navigatorKey,
- theme: getThemeFromNextcloudTheme(
- _userTheme,
- themeModeSnapshot.data!,
- platformBrightnessSnapshot.data!,
- oledAsDark: themeOLEDAsDarkSnapshot.data!,
- ),
- home: Container(),
- );
- },
+ Widget build(final BuildContext context) => Provider(
+ create: (final _) => _filesSyncBloc,
+ child: StreamBuilder(
+ stream: _platformBrightness,
+ builder: (final context, final platformBrightnessSnapshot) => StreamBuilder(
+ stream: widget.globalOptions.themeMode.stream,
+ builder: (final context, final themeModeSnapshot) => StreamBuilder(
+ stream: widget.globalOptions.themeOLEDAsDark.stream,
+ builder: (final context, final themeOLEDAsDarkSnapshot) {
+ if (!platformBrightnessSnapshot.hasData ||
+ !themeOLEDAsDarkSnapshot.hasData ||
+ !themeModeSnapshot.hasData) {
+ return Container();
+ }
+ return MaterialApp(
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ navigatorKey: _navigatorKey,
+ theme: getThemeFromNextcloudTheme(
+ _userTheme,
+ themeModeSnapshot.data!,
+ platformBrightnessSnapshot.data!,
+ oledAsDark: themeOLEDAsDarkSnapshot.data!,
+ ),
+ home: Container(),
+ );
+ },
+ ),
),
),
);
diff --git a/packages/neon/lib/l10n/en.arb b/packages/neon/lib/l10n/en.arb
index 251c5bba..f95bcd5a 100644
--- a/packages/neon/lib/l10n/en.arb
+++ b/packages/neon/lib/l10n/en.arb
@@ -48,6 +48,10 @@
"no": "No",
"close": "Close",
"retry": "Retry",
+ "cancel": "Cancel",
+ "previous": "Previous",
+ "next": "Next",
+ "finish": "Finish",
"showSlashHide": "Show/Hide",
"exit": "Exit",
"disabled": "Disabled",
@@ -181,6 +185,21 @@
}
}
},
+ "filesSyncNConflicts": "{n} file conflicts",
+ "@filesSyncNConflicts": {
+ "placeholders": {
+ "n": {
+ "type": "int"
+ }
+ }
+ },
+ "filesSyncForAllConflicts": "Apply for all conflicts",
+ "filesSyncLocal": "Local",
+ "filesSyncRemote": "Remote",
+ "filesSyncSkip": "Skip",
+ "filesSyncMappings": "File sync mappings",
+ "filesSyncAddMapping": "Add file sync mapping",
+ "filesSyncConfirmRemoveMapping": "Are you sure you want to remove this file sync mapping?",
"filesOptionsShowPreviews": "Show previews for files",
"filesOptionsUploadQueueParallelism": "Upload queue parallelism",
"filesOptionsDownloadQueueParallelism": "Download queue parallelism",
diff --git a/packages/neon/lib/l10n/localizations.dart b/packages/neon/lib/l10n/localizations.dart
index 24c1e57d..a2de5cda 100644
--- a/packages/neon/lib/l10n/localizations.dart
+++ b/packages/neon/lib/l10n/localizations.dart
@@ -251,6 +251,30 @@ abstract class AppLocalizations {
/// **'Retry'**
String get retry;
+ /// No description provided for @cancel.
+ ///
+ /// In en, this message translates to:
+ /// **'Cancel'**
+ String get cancel;
+
+ /// No description provided for @previous.
+ ///
+ /// In en, this message translates to:
+ /// **'Previous'**
+ String get previous;
+
+ /// No description provided for @next.
+ ///
+ /// In en, this message translates to:
+ /// **'Next'**
+ String get next;
+
+ /// No description provided for @finish.
+ ///
+ /// In en, this message translates to:
+ /// **'Finish'**
+ String get finish;
+
/// No description provided for @showSlashHide.
///
/// In en, this message translates to:
@@ -683,6 +707,54 @@ abstract class AppLocalizations {
/// **'Are you sure you want to download a file that is bigger than {warningSize} ({actualSize})?'**
String filesConfirmDownloadSizeWarning(String warningSize, String actualSize);
+ /// No description provided for @filesSyncNConflicts.
+ ///
+ /// In en, this message translates to:
+ /// **'{n} file conflicts'**
+ String filesSyncNConflicts(int n);
+
+ /// No description provided for @filesSyncForAllConflicts.
+ ///
+ /// In en, this message translates to:
+ /// **'Apply for all conflicts'**
+ String get filesSyncForAllConflicts;
+
+ /// No description provided for @filesSyncLocal.
+ ///
+ /// In en, this message translates to:
+ /// **'Local'**
+ String get filesSyncLocal;
+
+ /// No description provided for @filesSyncRemote.
+ ///
+ /// In en, this message translates to:
+ /// **'Remote'**
+ String get filesSyncRemote;
+
+ /// No description provided for @filesSyncSkip.
+ ///
+ /// In en, this message translates to:
+ /// **'Skip'**
+ String get filesSyncSkip;
+
+ /// No description provided for @filesSyncMappings.
+ ///
+ /// In en, this message translates to:
+ /// **'File sync mappings'**
+ String get filesSyncMappings;
+
+ /// No description provided for @filesSyncAddMapping.
+ ///
+ /// In en, this message translates to:
+ /// **'Add file sync mapping'**
+ String get filesSyncAddMapping;
+
+ /// No description provided for @filesSyncConfirmRemoveMapping.
+ ///
+ /// In en, this message translates to:
+ /// **'Are you sure you want to remove this file sync mapping?'**
+ String get filesSyncConfirmRemoveMapping;
+
/// No description provided for @filesOptionsShowPreviews.
///
/// In en, this message translates to:
diff --git a/packages/neon/lib/l10n/localizations_en.dart b/packages/neon/lib/l10n/localizations_en.dart
index d4497969..5e5255c8 100644
--- a/packages/neon/lib/l10n/localizations_en.dart
+++ b/packages/neon/lib/l10n/localizations_en.dart
@@ -94,6 +94,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get retry => 'Retry';
+ @override
+ String get cancel => 'Cancel';
+
+ @override
+ String get previous => 'Previous';
+
+ @override
+ String get next => 'Next';
+
+ @override
+ String get finish => 'Finish';
+
@override
String get showSlashHide => 'Show/Hide';
@@ -326,6 +338,32 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Are you sure you want to download a file that is bigger than $warningSize ($actualSize)?';
}
+ @override
+ String filesSyncNConflicts(int n) {
+ return '$n file conflicts';
+ }
+
+ @override
+ String get filesSyncForAllConflicts => 'Apply for all conflicts';
+
+ @override
+ String get filesSyncLocal => 'Local';
+
+ @override
+ String get filesSyncRemote => 'Remote';
+
+ @override
+ String get filesSyncSkip => 'Skip';
+
+ @override
+ String get filesSyncMappings => 'File sync mappings';
+
+ @override
+ String get filesSyncAddMapping => 'Add file sync mapping';
+
+ @override
+ String get filesSyncConfirmRemoveMapping => 'Are you sure you want to remove this file sync mapping?';
+
@override
String get filesOptionsShowPreviews => 'Show previews for files';
diff --git a/packages/neon/lib/src/apps/files/app.dart b/packages/neon/lib/src/apps/files/app.dart
index 480c3eb1..08a5bb5f 100644
--- a/packages/neon/lib/src/apps/files/app.dart
+++ b/packages/neon/lib/src/apps/files/app.dart
@@ -17,6 +17,8 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart
import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/apps/files/blocs/browser.dart';
import 'package:neon/src/apps/files/blocs/files.dart';
+import 'package:neon/src/apps/files/blocs/sync.dart';
+import 'package:neon/src/apps/files/models/sync_mapping.dart';
import 'package:neon/src/blocs/accounts.dart';
import 'package:neon/src/blocs/apps.dart';
import 'package:neon/src/models/account.dart';
@@ -30,6 +32,7 @@ import 'package:settings/settings.dart';
part 'dialogs/choose_create.dart';
part 'dialogs/choose_folder.dart';
part 'dialogs/create_folder.dart';
+part 'dialogs/sync_conflict.dart';
part 'models/file_details.dart';
part 'options.dart';
part 'pages/details.dart';
@@ -38,6 +41,8 @@ part 'utils/download_task.dart';
part 'utils/upload_task.dart';
part 'widgets/browser_view.dart';
part 'widgets/file_preview.dart';
+part 'widgets/file_tile.dart';
+part 'widgets/sync_status_icon.dart';
class FilesApp extends AppImplementation {
FilesApp(super.sharedPreferences, super.requestManager, super.platform);
diff --git a/packages/neon/lib/src/apps/files/blocs/files.dart b/packages/neon/lib/src/apps/files/blocs/files.dart
index c643a7c4..caa2a105 100644
--- a/packages/neon/lib/src/apps/files/blocs/files.dart
+++ b/packages/neon/lib/src/apps/files/blocs/files.dart
@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:neon/src/apps/files/app.dart';
import 'package:neon/src/apps/files/blocs/browser.dart';
-import 'package:neon/src/models/account.dart';
import 'package:neon/src/neon.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:open_file/open_file.dart';
@@ -20,8 +19,6 @@ abstract class FilesBlocEvents {
void uploadFile(final List path, final String localPath);
- void syncFile(final List path);
-
void openFile(final List path, final String etag, final String? mimeType);
void delete(final List path);
@@ -64,37 +61,15 @@ class FilesBloc extends $FilesBloc {
final stat = await file.stat();
final task = UploadTask(
path: event.path,
- size: stat.size,
- lastModified: stat.modified,
+ stat: stat,
);
_uploadTasksSubject.add(_uploadTasksSubject.value..add(task));
- await _uploadQueue.add(() => task.execute(client, file.openRead()));
+ await _uploadQueue.add(() => task.execute(client, file));
_uploadTasksSubject.add(_uploadTasksSubject.value..removeWhere((final t) => t == task));
},
);
});
- _$syncFileEvent.listen((final path) {
- final stream = _requestManager.wrapWithoutCache(
- () async {
- final file = File(
- p.join(
- await _platform.getUserAccessibleAppDataPath(),
- client.humanReadableID,
- 'files',
- path.join(Platform.pathSeparator),
- ),
- );
- if (!file.parent.existsSync()) {
- file.parent.createSync(recursive: true);
- }
- return _downloadFile(path, file);
- },
- disableTimeout: true,
- ).asBroadcastStream();
- stream.whereError().listen(_errorsStreamController.add);
- });
-
_$openFileEvent.listen((final event) {
_wrapAction(
true,
@@ -185,17 +160,12 @@ class FilesBloc extends $FilesBloc {
final List path,
final File file,
) async {
- final sink = file.openWrite();
try {
- final task = DownloadTask(
- path: path,
- );
+ final task = DownloadTask(path: path);
_downloadTasksSubject.add(_downloadTasksSubject.value..add(task));
- await _downloadQueue.add(() => task.execute(client, sink));
+ await _downloadQueue.add(() => task.execute(client, file));
_downloadTasksSubject.add(_downloadTasksSubject.value..removeWhere((final t) => t == task));
- await sink.close();
} catch (e) {
- await sink.close();
rethrow;
}
}
diff --git a/packages/neon/lib/src/apps/files/blocs/files.rxb.g.dart b/packages/neon/lib/src/apps/files/blocs/files.rxb.g.dart
index 3cede1bf..24db75ba 100644
--- a/packages/neon/lib/src/apps/files/blocs/files.rxb.g.dart
+++ b/packages/neon/lib/src/apps/files/blocs/files.rxb.g.dart
@@ -24,9 +24,6 @@ abstract class $FilesBloc extends RxBlocBase implements FilesBlocEvents, FilesBl
/// Тhe [Subject] where events sink to by calling [uploadFile]
final _$uploadFileEvent = PublishSubject<_UploadFileEventArgs>();
- /// Тhe [Subject] where events sink to by calling [syncFile]
- final _$syncFileEvent = PublishSubject>();
-
/// Тhe [Subject] where events sink to by calling [openFile]
final _$openFileEvent = PublishSubject<_OpenFileEventArgs>();
@@ -70,9 +67,6 @@ abstract class $FilesBloc extends RxBlocBase implements FilesBlocEvents, FilesBl
localPath,
));
- @override
- void syncFile(List path) => _$syncFileEvent.add(path);
-
@override
void openFile(
List path,
@@ -149,7 +143,6 @@ abstract class $FilesBloc extends RxBlocBase implements FilesBlocEvents, FilesBl
void dispose() {
_$refreshEvent.close();
_$uploadFileEvent.close();
- _$syncFileEvent.close();
_$openFileEvent.close();
_$deleteEvent.close();
_$renameEvent.close();
diff --git a/packages/neon/lib/src/apps/files/blocs/sync.dart b/packages/neon/lib/src/apps/files/blocs/sync.dart
new file mode 100644
index 00000000..43f98b27
--- /dev/null
+++ b/packages/neon/lib/src/apps/files/blocs/sync.dart
@@ -0,0 +1,216 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:neon/src/apps/files/models/sync_mapping.dart';
+import 'package:neon/src/blocs/accounts.dart';
+import 'package:neon/src/models/account.dart';
+import 'package:neon/src/neon.dart';
+import 'package:nextcloud/nextcloud.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:rx_bloc/rx_bloc.dart';
+import 'package:rxdart/rxdart.dart';
+import 'package:watcher/watcher.dart';
+
+part 'sync.rxb.g.dart';
+
+abstract class FilesSyncBlocEvents {
+ void addMapping(final FilesSyncMapping mapping);
+
+ void removeMapping(final FilesSyncMapping mapping);
+
+ void syncMapping(final FilesSyncMapping mapping, final Map solutions);
+}
+
+abstract class FilesSyncBlocStates {
+ BehaviorSubject