diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4c4e3..ed772f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ # Mobi Sync Client An App for wireless syncing of photos and videos from devices to home server. (https://mobisync.eu) + +## 1.0.9 Release notes (2024-09-02) + +### Enhancements +* Files synced from the newest to the oldest +* Requesting permissions for managing external storage +* Fixed not responding buttons in release + ## 1.0.8 Release notes (2024-08-23) ### Enhancements diff --git a/android/app/build.gradle b/android/app/build.gradle index 8f42de4..104e620 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -14,12 +14,12 @@ if (localPropertiesFile.exists()) { def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '108' + flutterVersionCode = '109' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '1.0.8' + flutterVersionName = '1.0.9' } def keystoreProperties = new Properties() @@ -72,6 +72,11 @@ android { // Signing with the debug keys for now, // so `flutter run --release` works. signingConfig signingConfigs.release + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile( + 'proguard-android-optimize.txt'), + 'proguard-rules.pro' } } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f27f892..d25e242 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,16 +1,4 @@ - - - - - - - - - - - - + + + + + + + + + + + diff --git a/lib/config/router/app_router.dart b/lib/config/router/app_router.dart index 568278f..5bde55e 100644 --- a/lib/config/router/app_router.dart +++ b/lib/config/router/app_router.dart @@ -17,8 +17,8 @@ limitations under the License. import 'package:go_router/go_router.dart'; import 'package:sync_client/screens/screens.dart'; -GoRouter getAppRouter(bool isAuthenticated) { - return GoRouter(initialLocation: isAuthenticated ? '/' : "/login", routes: [ +GoRouter getAppRouter() { + return GoRouter(initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), diff --git a/lib/core/impl/background.dart b/lib/core/impl/background.dart index 5457109..128003b 100644 --- a/lib/core/impl/background.dart +++ b/lib/core/impl/background.dart @@ -60,7 +60,8 @@ class BackgroundAction implements IAction { Future _uploadFiles(StreamController syncFileController, List files, String userName) async { - for (var file in files) { + final reversedFiles = files.reversed; + for (FileSystemEntity file in reversedFiles) { if (!FileSystemEntity.isDirectorySync(file.path)) { DateTime lastFileDate = await File(file.path).lastModified(); String dateClassifier = "${lastFileDate.year}-${lastFileDate.month}"; @@ -69,10 +70,20 @@ class BackgroundAction implements IAction { f.filename.toLowerCase() == file.path.toLowerCase() && (f.errorMessage ?? "").trim() == "" || f.failedAttempts > 3); - if (!fileHadBeenSynced) { - if (p.extension(file.path).isNotEmpty) { - var syncedFile = await _transfers.sendFile( - syncFileController, file.path, userName, dateClassifier); + if (fileHadBeenSynced) { + if (!syncFileController.isClosed) { + syncFileController.add(SyncedFile(file.path)); + } + if (currentDeviceSettings.deleteLocalFilesEnabled ?? false) { + await File(file.path).delete(); + currentDeviceSettings.syncedFiles.removeWhere( + (f) => f.filename.toLowerCase() == file.path.toLowerCase()); + } + } else { + if (file is File && p.extension(file.path).isNotEmpty) { + final fileLength = file.lengthSync(); + var syncedFile = await _transfers.sendFile(syncFileController, + file.path, userName, dateClassifier, fileLength); if (syncedFile != null) { if ((currentDeviceSettings.deleteLocalFilesEnabled ?? false) && diff --git a/lib/core/impl/server_api.dart b/lib/core/impl/server_api.dart index 37b90cb..d5bdc3b 100644 --- a/lib/core/impl/server_api.dart +++ b/lib/core/impl/server_api.dart @@ -37,9 +37,10 @@ Future?> apiGetFolders(String userName, String deviceId) async { if (response.statusCode == 200) { final List result = json.decode(response.body); - final List folders = result + final List folders = result.reversed .map((item) => NetFolder(item["Year"], subFolders: (List.from(item["Months"]) + .reversed .map((m) => NetFolder(m)) .toList()))) .toList(); @@ -70,7 +71,8 @@ Future> apiGetFiles( if (response.statusCode == 200) { final List result = json.decode(response.body); - final List files = result.map((item) => item.toString()).toList(); + final List files = + result.reversed.map((item) => item.toString()).toList(); return files; } } catch (err) { diff --git a/lib/core/impl/transfers.dart b/lib/core/impl/transfers.dart index dc8fe8f..bfebf9d 100644 --- a/lib/core/impl/transfers.dart +++ b/lib/core/impl/transfers.dart @@ -27,13 +27,18 @@ import 'request_utils.dart'; class Transfers { Transfers(); - Future sendFile(StreamController syncFileController, - String filename, String userName, String dateClassifier) async { + Future sendFile( + StreamController syncFileController, + String filename, + String userName, + String dateClassifier, + int fileLength) async { SyncedFile? result; var request = MultipartRequest('POST', getUrl("upload")); final hdr = { "user": utf8.encode(userName).toString(), - "date": dateClassifier + "date": dateClassifier, + "fileLength": fileLength.toString() }; request.headers.addEntries(hdr.entries); diff --git a/lib/main.dart b/lib/main.dart index 1cf763e..3dc350a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,6 +25,7 @@ import 'package:permission_handler/permission_handler.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await loadDeviceSettings(); if (Platform.isAndroid) { final mediaStorePlugin = MediaStore(); await mediaStorePlugin.getPlatformSDKInt(); @@ -33,7 +34,6 @@ void main() async { await requestPermissions(); } - await loadDeviceSettings(); runApp(const BlocProviders()); } @@ -41,6 +41,8 @@ Future requestPermissions() async { List permissions = [ Permission.storage, ]; + permissions.add(Permission.manageExternalStorage); + permissions.add(Permission.accessMediaLocation); permissions.add(Permission.storage); permissions.add(Permission.photos); permissions.add(Permission.audio); @@ -75,11 +77,10 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - final deviceService = context.read(); return MaterialApp.router( title: 'Mobi Sync Client', debugShowCheckedModeBanner: false, - routerConfig: getAppRouter(deviceService.isAuthenticated()), + routerConfig: getAppRouter(), theme: AppTheme.getTheme(context)); } } diff --git a/lib/screens/components/edit_server.dart b/lib/screens/components/edit_server.dart index 19a3689..182286c 100644 --- a/lib/screens/components/edit_server.dart +++ b/lib/screens/components/edit_server.dart @@ -65,8 +65,10 @@ class EditServerFormState extends State { String newServer) async { if (_formKey.currentState!.validate()) { await deviceService.edit((state) { + if (state.serverUrl != newServer) { + state.syncedFiles.clear(); + } state.serverUrl = newServer; - state.syncedFiles.clear(); state.lastErrorMessage = null; state.lastSyncDateTime = null; state.deleteLocalFilesEnabled = false; diff --git a/lib/screens/components/folder_item.dart b/lib/screens/components/folder_item.dart index 83783d3..63c0631 100644 --- a/lib/screens/components/folder_item.dart +++ b/lib/screens/components/folder_item.dart @@ -44,7 +44,6 @@ class FolderItem extends StatelessWidget { if (menuItem == FolderMenuOption.delete) { await deviceService.edit((state) { state.mediaDirectories.remove(folder); - state.syncedFiles.clear(); state.lastSyncDateTime = null; state.lastErrorMessage = null; }); diff --git a/lib/screens/components/status_widgets.dart b/lib/screens/components/status_widgets.dart index f180bd6..fbe2766 100644 --- a/lib/screens/components/status_widgets.dart +++ b/lib/screens/components/status_widgets.dart @@ -29,7 +29,8 @@ List getStateWidgets( List children; if (snapshot.hasError) { - children = _errorState(context, syncService, snapshot.error as CustomError); + children = _errorState( + context, syncService, deviceService, snapshot.error as CustomError); } else { switch (snapshot.connectionState) { case ConnectionState.none: @@ -45,15 +46,28 @@ List getStateWidgets( return children; } -List _errorState( - BuildContext context, SyncServicesCubit syncService, CustomError error) { +List _errorState(BuildContext context, SyncServicesCubit syncService, + DeviceServicesCubit deviceService, CustomError error) { return [ const Icon(Icons.error_outline, color: Colors.red, size: 30), - Padding( + Center( + child: Padding( padding: const EdgeInsets.only(top: 2), child: Text( '${error is SyncCanceledError ? "" : "Error: "}${error.message}'), - ), + )), + okButton(context, "Stop", onPressed: () { + if (syncService.state != null) { + if (!syncService.state!.isClosed) { + syncService.state!.addError(SyncCanceledError()); + syncService.state!.close(); + syncService.reset(); + } + } + deviceService.edit((state) { + state.isSyncing = false; + }); + }), ]; } diff --git a/lib/screens/deleting_enabled_screen.dart b/lib/screens/deleting_enabled_screen.dart index 0f9bd4f..5485398 100644 --- a/lib/screens/deleting_enabled_screen.dart +++ b/lib/screens/deleting_enabled_screen.dart @@ -87,7 +87,6 @@ class _DeletingEnabledScreenView extends StatelessWidget { await deviceService.edit((state) { state.deleteLocalFilesEnabled = !(state.deleteLocalFilesEnabled ?? false); - state.syncedFiles.clear(); state.lastErrorMessage = null; }); } else { @@ -117,7 +116,6 @@ class _DeletingEnabledScreenView extends StatelessWidget { await deviceService.edit((state) { state.deleteLocalFilesEnabled = !(state.deleteLocalFilesEnabled ?? false); - state.syncedFiles.clear(); state.lastErrorMessage = null; }); if (!context.mounted) return; diff --git a/lib/screens/folders_list_screen.dart b/lib/screens/folders_list_screen.dart index c2a1fe5..b3047d4 100644 --- a/lib/screens/folders_list_screen.dart +++ b/lib/screens/folders_list_screen.dart @@ -42,12 +42,10 @@ class FoldersListScreen extends StatelessWidget { BuildContext context, DeviceServicesCubit deviceService) async { String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); - if (selectedDirectory == null) { - return; - } await deviceService.edit((state) { - state.mediaDirectories.add(selectedDirectory); - state.syncedFiles.clear(); + if (selectedDirectory != null) { + state.mediaDirectories.add(selectedDirectory); + } state.lastErrorMessage = null; }); } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index a00d60e..44811f0 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -71,6 +71,9 @@ class HomeScreenState extends State { } Future> getAllFolders(DeviceServicesCubit deviceService) async { + if ((deviceService.state.serverUrl ?? "") == "") { + return []; + } List? folders = await apiGetFolders( deviceService.state.currentUser!.email, deviceService.state.id); @@ -78,6 +81,17 @@ class HomeScreenState extends State { return allFolders; } + Future> getAllFiles( + DeviceServicesCubit deviceService, String folder) async { + if ((deviceService.state.serverUrl ?? "") == "") { + return []; + } + List? files = await apiGetFiles( + deviceService.state.currentUser!.email, deviceService.state.id, folder); + + return files; + } + Widget itemsView(BuildContext context) { final DeviceServicesCubit deviceService = context.read(); @@ -90,51 +104,59 @@ class HomeScreenState extends State { return Container( margin: const EdgeInsets.only( left: 10.0, right: 10.0, top: 30.0, bottom: 30.0), - child: FutureBuilder>( - future: getAllFolders(deviceService), - builder: (context, snapshot) { - if (snapshot.hasError) { - return Text((snapshot.error as CustomError).message); - } else { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - return const CircularProgressIndicator(); - case ConnectionState.active: - case ConnectionState.done: - if (snapshot.hasData) { - final folders = snapshot.data!; - return ListView( - physics: const PageScrollPhysics(), - children: photoGridWidgets(folders, deviceService)); + child: ((deviceService.state.serverUrl ?? "") == "") + ? const Text( + "There is no files synced to the server or the server is not configured.") + : FutureBuilder>( + future: getAllFolders(deviceService), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Text((snapshot.error as CustomError).message); } else { - return const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "There is no synced photos/videos from this device and nickname.", - textAlign: TextAlign.center, - ), - Text( - "Please go the menu and select 'Sync' to setup configurations.", - textAlign: TextAlign.center, - ), - Padding( - padding: EdgeInsets.only(top: 10), - child: Text( - "Go to MOBISYNC.EU for help.", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, fontWeight: FontWeight.bold), - )) - ])); + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + return const CircularProgressIndicator(); + case ConnectionState.active: + case ConnectionState.done: + if (snapshot.hasData) { + final folders = snapshot.data!; + return folders.isEmpty + ? const Text( + "There is no files synced to the server or the server is not configured.") + : ListView( + physics: const PageScrollPhysics(), + children: + photoGridWidgets(folders, deviceService)); + } else { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "There is no synced photos/videos from this device and nickname.", + textAlign: TextAlign.center, + ), + Text( + "Please go the menu and select 'Sync' to setup configurations.", + textAlign: TextAlign.center, + ), + Padding( + padding: EdgeInsets.only(top: 10), + child: Text( + "Go to MOBISYNC.EU for help.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold), + )) + ])); + } + } } - } - } - }, - )); + }, + )); } List photoGridWidgets( @@ -147,8 +169,7 @@ class HomeScreenState extends State { child: Text(folder, style: const TextStyle(fontWeight: FontWeight.bold))))); result.add(FutureBuilder>( - future: apiGetFiles(deviceService.state.currentUser!.email, - deviceService.state.id, folder), + future: getAllFiles(deviceService, folder), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Container(); diff --git a/lib/storage/storage.dart b/lib/storage/storage.dart index 900e76f..b86250c 100644 --- a/lib/storage/storage.dart +++ b/lib/storage/storage.dart @@ -17,7 +17,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:uuid/uuid.dart'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sync_client/storage/schema.dart'; @@ -29,15 +28,7 @@ late DeviceSettings currentDeviceSettings; Future updateCurrentDevice( DeviceSettings deviceSettings) async { - final deviceInfoPlugin = DeviceInfoPlugin(); - final deviceInfo = await deviceInfoPlugin.deviceInfo; - - String? deviceId = deviceInfo.data["deviceId"]; - deviceId ??= deviceInfo.data["id"]; - deviceId ??= deviceInfo.data["systemGUID"]; - deviceId ??= const Uuid().v4(); - deviceSettings.id = deviceId; - deviceSettings.model = deviceInfo.data["model"]; + deviceSettings.id = const Uuid().v4(); return deviceSettings; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 80159e7..e777c67 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,8 @@ import FlutterMacOS import Foundation -import device_info_plus import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 253aba1..afa6ff0 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,26 +1,20 @@ PODS: - - device_info_plus (0.0.1): - - FlutterMacOS - FlutterMacOS (1.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS DEPENDENCIES: - - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) EXTERNAL SOURCES: - device_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos FlutterMacOS: :path: Flutter/ephemeral path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin SPEC CHECKSUMS: - device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 diff --git a/pubspec.yaml b/pubspec.yaml index df2d4b1..0af1b0f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: sync_client description: Mobile and Desktop device application for automatically uploading image, video and audio files publish_to: 'none' -version: 1.0.8 +version: 1.0.9 environment: sdk: '>=3.2.0 <5.0.0' @@ -15,7 +15,6 @@ dependencies: cupertino_icons: ^1.0.2 file_picker: ^8.0.6 - device_info_plus: ^10.1.0 http_parser: ^4.0.2 mime: ^1.0.4 path: ^1.8.3