From e4fd75ac5a90274afad2a1c2f02361f94046859e Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Mon, 11 Sep 2023 19:20:29 +0800 Subject: [PATCH] #157 new: `icloud` sync --- ios/Runner.xcodeproj/project.pbxproj | 4 +- ios/Runner/RunnerDebug.entitlements | 20 +++ lib/core/update.dart | 2 +- lib/core/utils/backup.dart | 78 +++++++++ lib/core/utils/icloud.dart | 159 ++++++++++++++++++ lib/core/utils/ui.dart | 9 + lib/data/model/app/error.dart | 16 ++ lib/data/model/app/json.dart | 7 + lib/data/model/app/syncable.dart | 9 + lib/data/res/path.dart | 38 ++++- lib/data/store/setting.dart | 7 + lib/main.dart | 30 +++- lib/view/page/backup.dart | 115 +++---------- lib/view/page/setting/entry.dart | 22 +-- lib/view/page/storage/local.dart | 2 +- lib/view/page/storage/sftp.dart | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 6 + macos/Runner.xcodeproj/project.pbxproj | 10 +- macos/Runner/RunnerDebug.entitlements | 26 +++ pubspec.lock | 8 + pubspec.yaml | 1 + 22 files changed, 448 insertions(+), 125 deletions(-) create mode 100644 ios/Runner/RunnerDebug.entitlements create mode 100644 lib/core/utils/backup.dart create mode 100644 lib/core/utils/icloud.dart create mode 100644 lib/data/model/app/json.dart create mode 100644 lib/data/model/app/syncable.dart create mode 100644 macos/Runner/RunnerDebug.entitlements diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index dabfb129..6331a629 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ E33A3E3F2A626DCE009744AB /* StatusWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusWidget.swift; sourceTree = ""; }; E33A3E412A626DCE009744AB /* StatusWidget.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = StatusWidget.intentdefinition; sourceTree = ""; }; E33A3E442A626DD0009744AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E35C79672AAEDF57007577EC /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; E398BF6A29BDB34500FE4FD5 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; E3DB67EC2A31FE200027B8CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -151,6 +152,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + E35C79672AAEDF57007577EC /* RunnerDebug.entitlements */, E398BF6A29BDB34500FE4FD5 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -601,7 +603,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CURRENT_PROJECT_VERSION = 539; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements new file mode 100644 index 00000000..86d341a8 --- /dev/null +++ b/ios/Runner/RunnerDebug.entitlements @@ -0,0 +1,20 @@ + + + + + aps-environment + production + com.apple.developer.icloud-container-identifiers + + iCloud.tech.lolli.serverbox + + com.apple.developer.icloud-services + + CloudDocuments + + com.apple.developer.ubiquity-container-identifiers + + iCloud.tech.lolli.serverbox + + + diff --git a/lib/core/update.dart b/lib/core/update.dart index 033ff181..8d5c1386 100644 --- a/lib/core/update.dart +++ b/lib/core/update.dart @@ -119,4 +119,4 @@ Future _rmDownloadApks() async { } } -Future get _dlDir async => joinPath((await docDir).path, 'Download'); +Future get _dlDir async => joinPath(await docDir, 'Download'); diff --git a/lib/core/utils/backup.dart b/lib/core/utils/backup.dart new file mode 100644 index 00000000..65534a5a --- /dev/null +++ b/lib/core/utils/backup.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import '../../data/model/app/backup.dart'; +import '../../data/res/path.dart'; +import '../../data/store/docker.dart'; +import '../../data/store/private_key.dart'; +import '../../data/store/server.dart'; +import '../../data/store/setting.dart'; +import '../../data/store/snippet.dart'; +import '../../locator.dart'; + +final _server = locator(); +final _snippet = locator(); +final _privateKey = locator(); +final _dockerHosts = locator(); +final _setting = locator(); + +Future get backupPath async => '${await docDir}/srvbox_bak.json'; + +const backupFormatVersion = 1; + +Future backup() async { + final result = _diyEncrtpt( + json.encode( + Backup( + version: backupFormatVersion, + date: DateTime.now().toString().split('.').first, + spis: _server.fetch(), + snippets: _snippet.fetch(), + keys: _privateKey.fetch(), + dockerHosts: _dockerHosts.fetchAll(), + settings: _setting.toJson(), + ), + ), + ); + await File(await backupPath).writeAsString(result); +} + +void restore(Backup backup) { + for (final s in backup.snippets) { + _snippet.put(s); + } + for (final s in backup.spis) { + _server.put(s); + } + for (final s in backup.keys) { + _privateKey.put(s); + } + for (final k in backup.dockerHosts.keys) { + final val = backup.dockerHosts[k]; + if (val != null && val is String && val.isNotEmpty) { + _dockerHosts.put(k, val); + } + } +} + +Future decodeBackup(String raw) async { + return await compute(_decode, raw); +} + +Backup _decode(String raw) { + final decrypted = _diyDecrypt(raw); + return Backup.fromJson(json.decode(decrypted)); +} + +String _diyEncrtpt(String raw) => + json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false)); +String _diyDecrypt(String raw) { + final list = json.decode(raw); + final sb = StringBuffer(); + for (final e in list) { + sb.writeCharCode((e - 1) ~/ 2); + } + return sb.toString(); +} diff --git a/lib/core/utils/icloud.dart b/lib/core/utils/icloud.dart new file mode 100644 index 00000000..38133505 --- /dev/null +++ b/lib/core/utils/icloud.dart @@ -0,0 +1,159 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:icloud_storage/icloud_storage.dart'; +import 'package:logging/logging.dart'; + +import '../../data/model/app/error.dart'; +import '../../data/model/app/json.dart'; +import '../../data/res/path.dart'; + +final _logger = Logger('iCloud'); + +class ICloud { + static const _containerId = 'iCloud.tech.lolli.serverbox'; + + const ICloud(); + + /// Upload file to iCloud + /// + /// - [relativePath] is the path relative to [docDir], + /// must not starts with `/` + /// - [localPath] has higher priority than [relativePath], but only apply + /// to the local path instead of iCloud path + /// + /// Return `null` if upload success, `ICloudErr` otherwise + static Future upload({ + required String relativePath, + String? localPath, + }) async { + final completer = Completer(); + await ICloudStorage.upload( + containerId: _containerId, + filePath: localPath ?? '${await docDir}/$relativePath', + destinationRelativePath: relativePath, + onProgress: (stream) { + stream.listen( + null, + onDone: () => completer.complete(null), + onError: (e) => completer.complete( + ICloudErr(type: ICloudErrType.generic, message: '$e'), + ), + ); + }, + ); + return completer.future; + } + + static Future> getAll() async { + return await ICloudStorage.gather( + containerId: _containerId, + ); + } + + static Future delete(String relativePath) async { + await ICloudStorage.delete( + containerId: _containerId, + relativePath: relativePath, + ); + } + + /// Download file from iCloud + /// + /// - [relativePath] is the path relative to [docDir], + /// must not starts with `/` + /// - [localPath] has higher priority than [relativePath], but only apply + /// to the local path instead of iCloud path + /// + /// Return `null` if upload success, `ICloudErr` otherwise + static Future download({ + required String relativePath, + String? localPath, + }) async { + final completer = Completer(); + await ICloudStorage.download( + containerId: _containerId, + relativePath: relativePath, + destinationFilePath: localPath ?? '${await docDir}/$relativePath', + onProgress: (stream) { + stream.listen( + null, + onDone: () => completer.complete(null), + onError: (e) => completer.complete( + ICloudErr(type: ICloudErrType.generic, message: '$e'), + ), + ); + }, + ); + return completer.future; + } + + /// Sync file between iCloud and local + /// + /// - [relativePath] is the path relative to [docDir], + /// must not starts with `/` + /// + /// Return `null` if upload success, `ICloudErr` otherwise + /// + /// TODO: consider merge strategy, use [SyncAble] and [JsonSerializable] + static Future?> sync({ + required Iterable relativePaths, + }) async { + try { + final errs = []; + + final allFiles = await getAll(); + // remove files not in relativePaths + allFiles.removeWhere((e) => !relativePaths.contains(e.relativePath)); + + // upload files not in iCloud + final missed = relativePaths.where((e) { + return !allFiles.any((f) => f.relativePath == e); + }); + for (final e in missed) { + final err = await upload(relativePath: e); + if (err != null) { + errs.add(err); + } + } + + final docPath = await docDir; + // compare files in iCloud and local + for (final file in allFiles) { + final relativePath = file.relativePath; + + /// Check date + final localFile = File('$docPath/$relativePath'); + if (!localFile.existsSync()) { + /// Local file not found, download remote file + final err = await download(relativePath: relativePath); + if (err != null) { + errs.add(err); + } + continue; + } + final localDate = await localFile.lastModified(); + if (file.contentChangeDate.isBefore(localDate)) { + /// Local is newer than remote, so upload local file + final err = await upload(relativePath: relativePath); + if (err != null) { + errs.add(err); + } + continue; + } + + /// Remote is newer than local, so download remote + final err = await download(relativePath: relativePath); + if (err != null) { + errs.add(err); + } + } + _logger.info('Errs: $errs'); + + return errs.isEmpty ? null : errs; + } catch (e, s) { + _logger.warning('Sync failed: $relativePaths', e, s); + return [ICloudErr(type: ICloudErrType.generic, message: '$e')]; + } + } +} diff --git a/lib/core/utils/ui.dart b/lib/core/utils/ui.dart index 1d9b26a1..295cc96e 100644 --- a/lib/core/utils/ui.dart +++ b/lib/core/utils/ui.dart @@ -43,6 +43,15 @@ void showSnackBarWithAction( )); } +void showRestartSnackbar(BuildContext context, S s) { + showSnackBarWithAction( + context, + '${s.success}\n${s.needRestart}', + s.restart, + () => rebuildAll(context), + ); +} + Future openUrl(String url) async { return await launchUrl(url.uri, mode: LaunchMode.externalApplication); } diff --git a/lib/data/model/app/error.dart b/lib/data/model/app/error.dart index ce6f40dd..08366e0e 100644 --- a/lib/data/model/app/error.dart +++ b/lib/data/model/app/error.dart @@ -52,3 +52,19 @@ class DockerErr extends Err { return 'DockerErr<$type>: $message'; } } + +enum ICloudErrType { + generic, + notFound, + multipleFiles, +} + +class ICloudErr extends Err { + ICloudErr({required ICloudErrType type, String? message}) + : super(from: ErrFrom.docker, type: type, message: message); + + @override + String toString() { + return 'ICloudErr<$type>: $message'; + } +} diff --git a/lib/data/model/app/json.dart b/lib/data/model/app/json.dart new file mode 100644 index 00000000..2566d85f --- /dev/null +++ b/lib/data/model/app/json.dart @@ -0,0 +1,7 @@ +abstract class JsonSerializable { + /// Convert [this] to json + Map toJson(); + + /// Create [this] from json + T fromJson(Map json); +} diff --git a/lib/data/model/app/syncable.dart b/lib/data/model/app/syncable.dart new file mode 100644 index 00000000..f2829d07 --- /dev/null +++ b/lib/data/model/app/syncable.dart @@ -0,0 +1,9 @@ +abstract class SyncAble { + /// If [other] is newer than [this] then return true, + /// else return false + bool needSync(T other); + + /// Merge [other] into [this], + /// return [this] after merge + T merge(T other); +} diff --git a/lib/data/res/path.dart b/lib/data/res/path.dart index d4da69bd..0e940fbf 100644 --- a/lib/data/res/path.dart +++ b/lib/data/res/path.dart @@ -3,23 +3,43 @@ import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:toolbox/core/utils/platform.dart'; -Future get docDir async { +String? _docDir; +String? _sftpDir; +String? _fontDir; + +Future get docDir async { + if (_docDir != null) { + return _docDir!; + } if (isAndroid) { final dir = await getExternalStorageDirectory(); if (dir != null) { - return dir; + _docDir = dir.path; + return dir.path; } // fallthrough to getApplicationDocumentsDirectory } - return await getApplicationDocumentsDirectory(); + final dir = await getApplicationDocumentsDirectory(); + _docDir = dir.path; + return dir.path; } -Future get sftpDir async { - final dir = Directory('${(await docDir).path}/sftp'); - return dir.create(recursive: true); +Future get sftpDir async { + if (_sftpDir != null) { + return _sftpDir!; + } + _sftpDir = '${await docDir}/sftp'; + final dir = Directory(_sftpDir!); + await dir.create(recursive: true); + return _sftpDir!; } -Future get fontDir async { - final dir = Directory('${(await docDir).path}/font'); - return dir.create(recursive: true); +Future get fontDir async { + if (_fontDir != null) { + return _fontDir!; + } + _fontDir = '${await docDir}/font'; + final dir = Directory(_fontDir!); + await dir.create(recursive: true); + return _fontDir!; } diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 67814526..9f8358f8 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -198,6 +198,13 @@ class SettingStore extends PersistentStore { false, ); + /// Only valid on iOS and macOS + late final icloudSync = StoreProperty( + box, + 'icloudSync', + false, + ); + // Never show these settings for users // Guide for these settings: // - key should start with `_` and be shorter as possible diff --git a/lib/main.dart b/lib/main.dart index 1e963a3d..68c2f8d0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; @@ -6,6 +7,8 @@ import 'package:logging/logging.dart'; import 'package:macos_window_utils/window_manipulator.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:toolbox/core/utils/icloud.dart'; +import 'package:toolbox/data/res/path.dart'; import 'app.dart'; import 'core/analysis.dart'; @@ -93,14 +96,18 @@ Future initApp() async { loadFontFile(settings.fontPath.fetch()); primaryColor = Color(settings.primaryColor.fetch()); - // Android only - if (!isAndroid) return; - // Only start service when [bgRun] is true. - if (locator().bgRun.fetch()) { - bgRunChannel.invokeMethod('startService'); + if (isIOS || isMacOS) { + if (settings.icloudSync.fetch()) _syncApple(); + } + + if (isAndroid) { + // Only start service when [bgRun] is true. + if (locator().bgRun.fetch()) { + bgRunChannel.invokeMethod('startService'); + } + // SharedPreferences is only used on Android for saving home widgets settings. + SharedPreferences.setPrefix(''); } - // SharedPreferences is only used on Android for saving home widgets settings. - SharedPreferences.setPrefix(''); } void _setupProviders() { @@ -144,3 +151,12 @@ Future _initMacOSWindow() async { WindowManipulator.hideTitle(); await CustomAppBar.updateTitlebarHeight(); } + +Future _syncApple() async { + final docPath = await docDir; + final dir = Directory(docPath); + final files = await dir.list().toList(); + files.removeWhere((e) => !e.path.endsWith('.hive')); + final paths = files.map((e) => e.path.replaceFirst('$docPath/', '')); + ICloud.sync(relativePaths: paths); +} diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart index bfdab3b2..8b179f68 100644 --- a/lib/view/page/backup.dart +++ b/lib/view/page/backup.dart @@ -1,35 +1,23 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:toolbox/core/extension/context.dart'; -import 'package:toolbox/data/res/path.dart'; +import 'package:toolbox/core/utils/backup.dart'; +import 'package:toolbox/core/utils/platform.dart'; import 'package:toolbox/view/widget/round_rect_card.dart'; import '../../core/utils/misc.dart'; import '../../core/utils/ui.dart'; -import '../../data/model/app/backup.dart'; import '../../data/res/ui.dart'; -import '../../data/store/docker.dart'; -import '../../data/store/private_key.dart'; -import '../../data/store/server.dart'; import '../../data/store/setting.dart'; -import '../../data/store/snippet.dart'; import '../../locator.dart'; import '../widget/custom_appbar.dart'; -const backupFormatVersion = 1; - class BackupPage extends StatelessWidget { BackupPage({Key? key}) : super(key: key); - final _server = locator(); - final _snippet = locator(); - final _privateKey = locator(); - final _dockerHosts = locator(); final _setting = locator(); @override @@ -44,12 +32,12 @@ class BackupPage extends StatelessWidget { } Widget _buildBody(BuildContext context, S s) { - final media = MediaQuery.of(context); - return Center( - child: Column( + return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ + if (isMacOS || isIOS) _buildIcloudSync(context), + height13, Padding( padding: const EdgeInsets.all(37), child: Text( @@ -61,7 +49,6 @@ class BackupPage extends StatelessWidget { _buildCard( s.restore, Icons.download, - media, () => _onRestore(context, s), ), height13, @@ -73,17 +60,18 @@ class BackupPage extends StatelessWidget { _buildCard( s.backup, Icons.save, - media, - () => _onBackup(context, s), + () async { + await backup(); + await shareFiles(context, [await backupPath]); + }, ) ], - )); + ); } Widget _buildCard( String text, IconData icon, - MediaQueryData media, FutureOr Function() onTap, ) { return RoundRectCard( @@ -105,6 +93,20 @@ class BackupPage extends StatelessWidget { ); } + Widget _buildIcloudSync(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'iCloud', + textAlign: TextAlign.center, + ), + width13, + buildSwitch(context, _setting.icloudSync) + ], + ); + } + Future _onRestore(BuildContext context, S s) async { final path = await pickOneFile(); if (path == null) { @@ -120,25 +122,6 @@ class BackupPage extends StatelessWidget { _import(text, context, s); } - Future _onBackup(BuildContext context, S s) async { - final result = _diyEncrtpt( - json.encode( - Backup( - version: backupFormatVersion, - date: DateTime.now().toString().split('.').first, - spis: _server.fetch(), - snippets: _snippet.fetch(), - keys: _privateKey.fetch(), - dockerHosts: _dockerHosts.fetchAll(), - settings: _setting.toJson(), - ), - ), - ); - final path = '${(await docDir).path}/srvbox_bak.json'; - await File(path).writeAsString(result); - await shareFiles(context, [path]); - } - Future _import(String text, BuildContext context, S s) async { if (text.isEmpty) { showSnackBar(context, Text(s.fieldMustNotEmpty)); @@ -149,7 +132,7 @@ class BackupPage extends StatelessWidget { Future _importBackup(String raw, BuildContext context, S s) async { try { - final backup = await compute(_decode, raw); + final backup = await decodeBackup(raw); if (backupFormatVersion != backup.version) { showSnackBar(context, Text(s.backupVersionNotMatch)); return; @@ -166,37 +149,9 @@ class BackupPage extends StatelessWidget { ), TextButton( onPressed: () async { - for (final s in backup.snippets) { - _snippet.put(s); - } - for (final s in backup.spis) { - _server.put(s); - } - for (final s in backup.keys) { - _privateKey.put(s); - } - for (final k in backup.dockerHosts.keys) { - final val = backup.dockerHosts[k]; - if (val != null && val is String && val.isNotEmpty) { - _dockerHosts.put(k, val); - } - } + restore(backup); context.pop(); - showRoundDialog( - context: context, - title: Text(s.restore), - child: Text(s.restoreSuccess), - actions: [ - TextButton( - onPressed: () => rebuildAll(context), - child: Text(s.restart), - ), - TextButton( - onPressed: () => context.pop(), - child: Text(s.cancel), - ), - ], - ); + showRestartSnackbar(context, s); }, child: Text(s.ok), ), @@ -208,19 +163,3 @@ class BackupPage extends StatelessWidget { } } } - -Backup _decode(String raw) { - final decrypted = _diyDecrypt(raw); - return Backup.fromJson(json.decode(decrypted)); -} - -String _diyEncrtpt(String raw) => - json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false)); -String _diyDecrypt(String raw) { - final list = json.decode(raw); - final sb = StringBuffer(); - for (final e in list) { - sb.writeCharCode((e - 1) ~/ 2); - } - return sb.toString(); -} diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index 948963f3..fa4d4606 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -368,7 +368,7 @@ class _SettingPageState extends State { _setting.primaryColor.put(_selectedColorValue.value); primaryColor = color; context.pop(); - _showRestartSnackbar(); + showRestartSnackbar(context, _s); } // Widget _buildLaunchPage() { @@ -560,7 +560,7 @@ class _SettingPageState extends State { onPressed: () { _setting.fontPath.delete(); context.pop(); - _showRestartSnackbar(); + showRestartSnackbar(context, _s); }, child: Text(_s.clear), ) @@ -577,29 +577,19 @@ class _SettingPageState extends State { if (isIOS) { _setting.fontPath.put(path); } else { - final fontDir_ = await fontDir; final fontFile = File(path); - final newPath = '${fontDir_.path}/${path.split('/').last}'; + final newPath = '${await fontDir}/${path.split('/').last}'; await fontFile.copy(newPath); _setting.fontPath.put(newPath); } context.pop(); - _showRestartSnackbar(); + showRestartSnackbar(context, _s); return; } showSnackBar(context, Text(_s.failed)); } - void _showRestartSnackbar() { - showSnackBarWithAction( - context, - '${_s.success}\n${_s.needRestart}', - _s.restart, - () => rebuildAll(context), - ); - } - Widget _buildBgRun() { return ListTile( title: Text(_s.bgRun), @@ -681,7 +671,7 @@ class _SettingPageState extends State { onSelected: (String idx) { _localeCode.value = idx; _setting.locale.put(idx); - _showRestartSnackbar(); + showRestartSnackbar(context, _s); }, child: Text( _s.languageName, @@ -772,7 +762,7 @@ class _SettingPageState extends State { trailing: buildSwitch( context, _setting.fullScreen, - func: (_) => _showRestartSnackbar(), + func: (_) => showRestartSnackbar(context, _s), ), ); } diff --git a/lib/view/page/storage/local.dart b/lib/view/page/storage/local.dart index d4e8cfd4..2695c08e 100644 --- a/lib/view/page/storage/local.dart +++ b/lib/view/page/storage/local.dart @@ -50,7 +50,7 @@ class _LocalStoragePageState extends State { } else { sftpDir.then((dir) { setState(() { - _path = LocalPath(dir.path); + _path = LocalPath(dir); }); }); } diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index 17d666ba..87b9619a 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -642,7 +642,7 @@ class _SftpPageState extends State with AfterLayoutMixin { } Future _getLocalPath(String remotePath) async { - return '${(await sftpDir).path}$remotePath'; + return '${await sftpDir}$remotePath'; } /// Only return true if the path is changed diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 97320641..d192c478 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import dynamic_color +import icloud_storage import macos_window_utils import path_provider_foundation import share_plus @@ -14,6 +15,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + IcloudStoragePlugin.register(with: registry.registrar(forPlugin: "IcloudStoragePlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 3e99a2b6..f3479157 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,6 +2,8 @@ PODS: - dynamic_color (0.0.2): - FlutterMacOS - FlutterMacOS (1.0.0) + - icloud_storage (0.0.1): + - FlutterMacOS - macos_window_utils (1.0.0): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -18,6 +20,7 @@ PODS: DEPENDENCIES: - dynamic_color (from `Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - icloud_storage (from `Flutter/ephemeral/.symlinks/plugins/icloud_storage/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) @@ -29,6 +32,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/dynamic_color/macos FlutterMacOS: :path: Flutter/ephemeral + icloud_storage: + :path: Flutter/ephemeral/.symlinks/plugins/icloud_storage/macos macos_window_utils: :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos path_provider_foundation: @@ -43,6 +48,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + icloud_storage: 33b05299e26d1391d724da8d62860e702380a1cd macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a55c42a8..6eec5e99 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -86,6 +86,7 @@ 8B1B5C0D431C7ED686B9704F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; C1C758C41C4E208965A68933 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E35696982AAF0AF400C087D8 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; E5004715DDBC0F274A8B776F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; FFF2DE54D5055D5B946114D7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -175,6 +176,7 @@ 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( + E35696982AAF0AF400C087D8 /* RunnerDebug.entitlements */, 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, @@ -567,8 +569,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -693,9 +697,11 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -715,8 +721,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/macos/Runner/RunnerDebug.entitlements b/macos/Runner/RunnerDebug.entitlements new file mode 100644 index 00000000..6ff1c43c --- /dev/null +++ b/macos/Runner/RunnerDebug.entitlements @@ -0,0 +1,26 @@ + + + + + com.apple.developer.icloud-container-identifiers + + iCloud.tech.lolli.serverbox + + com.apple.developer.icloud-services + + CloudDocuments + + com.apple.developer.ubiquity-container-identifiers + + iCloud.tech.lolli.serverbox + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/pubspec.lock b/pubspec.lock index 599814dd..c5fe884e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -454,6 +454,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + icloud_storage: + dependency: "direct main" + description: + name: icloud_storage + sha256: fa91d9c3b4264651f01a4f5b99cffa354ffe455623b13ecf92be86d88b1e26ea + url: "https://pub.dev" + source: hosted + version: "2.2.0" image: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f3ae03cb..1bee845b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: crypto: ^3.0.3 macos_window_utils: ^1.2.0 dynamic_color: ^1.6.6 + icloud_storage: ^2.2.0 dev_dependencies: flutter_native_splash: ^2.1.6