diff --git a/CHANGELOG.md b/CHANGELOG.md index 119a109..87c923e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,17 @@ # Vup Changelog -## main +## Beta 0.12.0 - Added "Delete permanently" action for files and directories in trash +- Added experimental support for storing encrypted file data on custom remotes (S3 and WebDAV) +- Added skylink health indicator and pin tool to file details view - Improved "Delete local copies" action - Added timeout to prevent sync operations from locking up - Added "Copy link" button for already shared directories - Added "Gallery" view type when sharing files - Custom themes: Added support for background images +- Improved button label readability +- Fixed some bugs ## Beta 0.11.0 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ba1c71d..72300c6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,8 @@ + + diff --git a/assets/icon/outline.png b/assets/icon/outline.png new file mode 100644 index 0000000..6e912f8 Binary files /dev/null and b/assets/icon/outline.png differ diff --git a/assets/icon/vup-logo-single.svg b/assets/icon/vup-logo-single.svg new file mode 100644 index 0000000..cc49c6c --- /dev/null +++ b/assets/icon/vup-logo-single.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/generic/state.dart b/lib/generic/state.dart index f48dabf..d8a8a59 100644 --- a/lib/generic/state.dart +++ b/lib/generic/state.dart @@ -109,7 +109,6 @@ const customThemesPath = 'vup.hns/config/custom_themes.json'; final scriptsStatus = {}; // Pools -final uploadPool = Pool(3); final downloadPool = Pool(3); // Error handling diff --git a/lib/main.dart b/lib/main.dart index e8f7989..87eb5da 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -131,7 +131,8 @@ Future initApp() async { await logger.init(vupTempDir); vupConfigDir = join( - vupConfigDir, 'default', + vupConfigDir, + 'default', // 'debug', ); diff --git a/lib/page/settings.dart b/lib/page/settings.dart index 253b5eb..56e841d 100644 --- a/lib/page/settings.dart +++ b/lib/page/settings.dart @@ -88,7 +88,7 @@ class _SettingsPageState extends State { ), if (devModeEnabled && isYTDlIntegrationEnabled) SettingsPane( - title: 'Hooks/tasks/workflows/scripts (Advanced)', + title: 'Scripts (Advanced)', build: () => ScriptsSettingsPage(), ), /* SettingsPane( @@ -158,7 +158,7 @@ class _SettingsPageState extends State { onTap: () { showAboutDialog( applicationLegalese: - 'Copyright © 2022 redsolver. All rights reserved.', + 'Copyright © 2022 redsolver. Licensed under the terms of the EUPL-1.2 license.', applicationName: 'Vup', applicationVersion: packageInfo.version, context: context, diff --git a/lib/page/settings/jellyfin.dart b/lib/page/settings/jellyfin.dart index af86c03..9fd7075 100644 --- a/lib/page/settings/jellyfin.dart +++ b/lib/page/settings/jellyfin.dart @@ -186,7 +186,7 @@ class _JellyfinServerSettingsPageState ), if (isJellyfinServerEnabled) SelectableText( - '\nWarning: Authentication is not enforced because some players and API endpoints don\'t fully support it (yet). Please only run the server on localhost (this is the default) to prevent outside connections.\n\nJellyfin server running at ${jellyfinServerBindIp}:${jellyfinServerPort}\nStop the Jellyfin server if you want to change any settings.', + '\nWarning: Authentication is not enforced because some players and API endpoints don\'t fully support it (yet). Please only run the server on localhost (this is the default) to prevent outside connections.\n\nJellyfin server running at http://${jellyfinServerBindIp}:${jellyfinServerPort}\nStop the Jellyfin server if you want to change any settings.', ), ], ), diff --git a/lib/page/settings/remotes.dart b/lib/page/settings/remotes.dart index 0d0d15c..7c44292 100644 --- a/lib/page/settings/remotes.dart +++ b/lib/page/settings/remotes.dart @@ -48,7 +48,7 @@ class _RemotesSettingsPageState extends State { await showInfoDialog( context, 'Create remote manually', - 'Warning: You can break quite a lot of things with this tool. Please only use it when you know what you\'re doing', + 'Warning: You can break quite a lot of things with this tool. Please only use it if you know what you\'re doing', ); final res = await showTextInputDialog( context: context, @@ -63,6 +63,7 @@ class _RemotesSettingsPageState extends State { storageService.dac.customRemotes[res[0]] = json.decode(res[1]); await storageService.dac.saveRemotes(); + storageService.dac.loadRemotes(); context.pop(); setState(() {}); } catch (e, st) { @@ -94,6 +95,77 @@ class _RemotesSettingsPageState extends State { SelectableText( '${json.encode(storageService.dac.customRemotes[id])}', ), + SizedBox( + height: 8, + ), + ElevatedButton( + onPressed: () async { + final res = await showTextInputDialog( + context: context, + textFields: [ + DialogTextField(hintText: 'uri'), + ], + ); + try { + showLoadingDialog(context, 'updating...'); + final String uri = res![0]; + final map = storageService.dac.customRemotes[id]!; + map['used_for_uris'] ??= []; + + map['used_for_uris'].add(uri); + + storageService.dac.customRemotes[id] = map; + + await storageService.dac.saveRemotes(); + storageService.dac.loadRemotes(); + context.pop(); + setState(() {}); + } catch (e, st) { + context.pop(); + setState(() {}); + showErrorDialog(context, e, st); + } + }, + child: Text( + 'Add SkyFS path', + ), + ), + for (final uri in storageService + .dac.customRemotes[id]!['used_for_uris'] ?? + []) + Row( + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SelectableText(uri), + ), + ), + TextButton( + onPressed: () async { + try { + showLoadingDialog(context, 'removing uri...'); + + final map = storageService.dac.customRemotes[id]!; + map['used_for_uris'].remove(uri); + storageService.dac.customRemotes[id] = map; + + await storageService.dac.saveRemotes(); + storageService.dac.loadRemotes(); + context.pop(); + setState(() {}); + } catch (e, st) { + context.pop(); + setState(() {}); + showErrorDialog(context, e, st); + } + }, + child: Text( + 'Remove', + ), + ) + ], + ), ], ), ), diff --git a/lib/page/settings/web_server.dart b/lib/page/settings/web_server.dart index 7fe11b2..b89ebf4 100644 --- a/lib/page/settings/web_server.dart +++ b/lib/page/settings/web_server.dart @@ -73,7 +73,7 @@ class _WebServerSettingsPageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: SelectableText( - 'Web server running at ${webServerBindIp}:${webServerPort}/home/\nStop the web server if you want to change any settings.', + 'Web server running at http://${webServerBindIp}:${webServerPort}/home/\nStop the web server if you want to change any settings.', ), ), ], diff --git a/lib/page/settings/webdav.dart b/lib/page/settings/webdav.dart index 942bd77..548dc48 100644 --- a/lib/page/settings/webdav.dart +++ b/lib/page/settings/webdav.dart @@ -118,7 +118,7 @@ class _WebDavSettingsPageState extends State { Padding( padding: const EdgeInsets.all(16.0), child: SelectableText( - 'WebDav server running at ${webDavServerBindIp}:${webDavServerPort}\nStop the WebDav server if you want to change any settings.', + 'WebDav server running at http://${webDavServerBindIp}:${webDavServerPort}\nStop the WebDav server if you want to change any settings.', ), ), ], diff --git a/lib/service/mysky.dart b/lib/service/mysky.dart index 9f5f2f0..c5ea622 100644 --- a/lib/service/mysky.dart +++ b/lib/service/mysky.dart @@ -19,6 +19,8 @@ class MySkyService extends VupService { late final Vault usedMySkyPathsVault; + final useSecureStorage = true; + void setup(String cookie) { final dbDir = Directory(join( vupDataDir, @@ -80,7 +82,7 @@ class MySkyService extends VupService { dataBox.put('deviceId', newDeviceId); } - Future.delayed(Duration(seconds: 50)).then((value) { + Future.delayed(const Duration(seconds: 50)).then((value) { updateDeviceList(); }); } @@ -131,7 +133,7 @@ class MySkyService extends VupService { secureStorage = const FlutterSecureStorage(); - if (!Platform.isMacOS) { + if (!Platform.isMacOS && useSecureStorage) { if (dataBox.containsKey('seed')) { await secureStorage.write(key: 'seed', value: dataBox.get('seed')); await dataBox.delete('seed'); @@ -144,7 +146,7 @@ class MySkyService extends VupService { } Future storeSeedPhrase(String seed) async { - if (!Platform.isMacOS) { + if (!Platform.isMacOS && useSecureStorage) { await secureStorage.write(key: 'seed', value: seed); } else { await dataBox.put('seed', seed); @@ -152,7 +154,7 @@ class MySkyService extends VupService { } Future loadSeedPhrase() async { - if (!Platform.isMacOS) { + if (!Platform.isMacOS && useSecureStorage) { return secureStorage.read(key: 'seed'); } else { return dataBox.get('seed'); diff --git a/lib/service/storage.dart b/lib/service/storage.dart index 6e47494..ced0a5d 100644 --- a/lib/service/storage.dart +++ b/lib/service/storage.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:hive/hive.dart'; +import 'package:pool/pool.dart'; import 'package:random_string/random_string.dart'; import 'package:stash/stash_api.dart'; import 'package:vup/generic/state.dart'; @@ -32,6 +33,7 @@ import 'package:skynet/src/encode_endian/base.dart'; import 'package:skynet/src/mysky/encrypted_files.dart'; import 'package:vup/utils/process_image.dart'; import 'package:watcher/watcher.dart'; +import 'package:skynet/src/utils/base32.dart'; import 'mysky.dart'; @@ -634,6 +636,7 @@ class StorageService extends VupService { multihash, encryptedCacheFile, fileStateNotifier: fileStateNotifier, + customRemote: getCustomRemoteForPath(path), ); return res; } catch (e, st) { @@ -781,7 +784,13 @@ class StorageService extends VupService { ), ); - return await uploadPool.withResource( + final customRemote = getCustomRemoteForPath(path); + if (!uploadPools.containsKey(customRemote)) { + uploadPools[customRemote] = Pool(customRemote == null ? 3 : 16); + } + final pool = uploadPools[customRemote]!; + + return await pool.withResource( () => uploadOneFile( path, file, @@ -796,6 +805,10 @@ class StorageService extends VupService { ); } + final _webDavClientCache = {}; + + final uploadPools = {}; + Future syncDirectory( Directory dir, String remotePath, @@ -1082,7 +1095,10 @@ class StorageService extends VupService { Future getMultiHashForFile(File file) async { if (Platform.isLinux) { final res = await Process.run('sha256sum', [file.path]); - final String hash = res.stdout.split(' ').first; + String hash = res.stdout.split(' ').first; + if (hash.startsWith('\\')) { + hash = hash.substring(1); + } if (hash.length != 64) { throw 'Hash function failed'; } @@ -1095,7 +1111,12 @@ class StorageService extends VupService { Future getSHA1HashForFile(File file) async { if (Platform.isLinux) { final res = await Process.run('sha1sum', [file.path]); - final String hash = res.stdout.split(' ').first; + String hash = res.stdout.split(' ').first; + + if (hash.startsWith('\\')) { + hash = hash.substring(1); + } + if (hash.length != 40) { throw 'Hash function failed'; } @@ -1192,7 +1213,10 @@ class StorageService extends VupService { if (doDownload) { if (fileData.encryptionType == 'libsodium_secretbox') { - final stream = await dac.downloadAndDecryptFileInChunks(fileData); + final stream = await dac.downloadAndDecryptFileInChunks(fileData, + downloadConfig: fileData.url.startsWith('remote-') + ? (await generateDownloadConfig(fileData)) + : null); decryptedFile.createSync(recursive: true); final sink = decryptedFile.openWrite(); await sink.addStream(stream.map((e) => e.toList())); @@ -1415,6 +1439,7 @@ class StorageService extends VupService { String fileMultiHash, File outFile, { FileStateNotifier? fileStateNotifier, + required String? customRemote, }) async { int padding = 0; const maxChunkSize = 1 * 1000 * 1000; // 1 MiB @@ -1548,39 +1573,79 @@ class StorageService extends VupService { final TUS_CHUNK_SIZE = (1 << 22) * 10; // ~ 41 MB - if (false && (outFile.lengthSync() > TUS_CHUNK_SIZE)) { - final remote = dac.customRemotes['unraid']!['config']! as Map; - var client = webdav.newClient( - remote['url'] as String, - user: remote['user'] as String, - password: remote['pass'] as String, - debug: true, - ); + if (customRemote != null) { + final rem = dac.customRemotes[customRemote]!; + final config = rem['config']! as Map; + final type = rem['type']!; - final fileId = randomAlphaNumeric( - 32, - provider: CoreRandomProvider.from( - Random.secure(), - ), - ).toLowerCase(); + final fileId = base32.encode(dac.sodium.randombytes.buf(32)).replaceAll( + '=', + '', + ); - final c = dio.CancelToken(); - await client.c.wdWriteWithStream( - client, - '/skyfs/$fileId', - outFile.openRead(), - outFile.lengthSync(), - onProgress: (c, t) { - fileStateNotifier!.updateFileState( - FileState( - type: FileStateType.uploading, - progress: c / t, - ), + if (type == 'webdav') { + var path = ''; + + for (int i = 0; i < 8; i += 2) { + path += '/${fileId.substring(i, i + 2)}'; + } + + final filename = fileId.substring(8); + + if (!_webDavClientCache.containsKey(customRemote)) { + _webDavClientCache[customRemote] = webdav.newClient( + config['url'] as String, + user: config['user'] as String, + password: config['pass'] as String, + debug: false, ); - }, - cancelToken: c, - ); - blobUrl = 'remote-unraid://$fileId'; + } + final client = _webDavClientCache[customRemote]!; + + final c = dio.CancelToken(); + await client.c.wdWriteWithStream( + client, + '/skyfs$path/$filename', + outFile.openRead(), + outFile.lengthSync(), + onProgress: (c, t) { + fileStateNotifier!.updateFileState( + FileState( + type: FileStateType.uploading, + progress: c / t, + ), + ); + }, + cancelToken: c, + ); + blobUrl = 'remote-$customRemote:/$path/$filename'; + } else if (type == 's3') { + final client = dac.getS3Client(customRemote, config); + + final bucket = config['bucket'] as String; + + final totalBytes = outFile.lengthSync(); + + final res = await client.putObject( + bucket, + 'skyfs/$fileId', + outFile.openRead().map((event) => Uint8List.fromList(event)), + onProgress: (bytes) { + fileStateNotifier!.updateFileState( + FileState( + type: FileStateType.uploading, + progress: bytes / totalBytes, + ), + ); + }, + ); + if (res.isEmpty) { + throw 'S3: Empty upload response'; + } + blobUrl = 'remote-$customRemote://$fileId'; + } else { + throw 'Remote type "$type" not supported'; + } } else { if (outFile.lengthSync() > TUS_CHUNK_SIZE) { blobUrl = await mySky.skynetClient.upload.uploadLargeFile( @@ -1708,6 +1773,24 @@ class StorageService extends VupService { padding: null, ); } + + String? getCustomRemoteForPath(String path) { + final uri = dac.parsePath(path).toString(); + + for (final remoteId in storageService.dac.customRemotes.keys) { + final List usedForUris = + storageService.dac.customRemotes[remoteId]!['used_for_uris'] ?? + const []; + + for (final usedForUri in usedForUris) { + if (usedForUri == uri || uri.startsWith(usedForUri)) { + return remoteId; + } + } + } + + return null; + } } class PlaintextChunk { diff --git a/lib/service/web_server/serve_chunked_file.dart b/lib/service/web_server/serve_chunked_file.dart index 25b921b..dc26e4e 100644 --- a/lib/service/web_server/serve_chunked_file.dart +++ b/lib/service/web_server/serve_chunked_file.dart @@ -14,6 +14,7 @@ import 'package:skynet/src/encode_endian/encode_endian.dart'; import 'package:skynet/src/encode_endian/base.dart'; import 'package:http/http.dart' as http; +import 'package:vup/utils/download/generate_download_config.dart'; Future handleChunkedFile( HttpRequest req, @@ -169,22 +170,10 @@ Stream> openRead(DirectoryFile df, int start, int totalSize, Map? customHeaders; if (df.file.url.startsWith('remote-')) { - final uri = Uri.parse(df.file.url); - final remoteId = uri.scheme.substring(7); - print('remote "$remoteId" "${uri.host}"'); + final dc = await generateDownloadConfig(df.file); - final remote = storageService.dac.customRemotes[remoteId]!; - - final Map remoteConfig = remote['config'] as Map; - - if (remote['type'] == 'webdav') { - url = Uri.parse('${remoteConfig['url']}/skyfs/${uri.host}'); - - customHeaders = { - 'Authorization': - 'Basic ${base64.encode(utf8.encode('${remoteConfig['user']}:${remoteConfig['pass']}'))}' - }; - } + url = Uri.parse(dc.url); + customHeaders = dc.headers; } else { url = Uri.parse( storageService.mySky.skynetClient.resolveSkylink( diff --git a/lib/theme.dart b/lib/theme.dart index 1080569..24ff737 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -136,7 +136,7 @@ class AppThemeState extends State { } ThemeData _buildThemeData(String theme) { - var _accentColor = Color(0xff1ed660); + final _accentColor = Color(0xff1ed660); /* = RainbowColorTween([ Colors.orange, Colors.red, @@ -208,6 +208,16 @@ class AppThemeState extends State { brightness == Brightness.dark ? Colors.grey[500] : Colors.grey[500], primaryColor: accentColor, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + accentColor, + ), + foregroundColor: MaterialStateProperty.all( + accentColor.computeLuminance() < 0.48 ? Colors.white : Colors.black, + ), + ), + ), visualDensity: VisualDensity.adaptivePlatformDensity, toggleableActiveColor: accentColor, diff --git a/lib/utils/download/generate_download_config.dart b/lib/utils/download/generate_download_config.dart index 9f618dd..0b05671 100644 --- a/lib/utils/download/generate_download_config.dart +++ b/lib/utils/download/generate_download_config.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:filesystem_dac/dac.dart'; import 'package:vup/generic/state.dart'; Future generateDownloadConfig(FileData fileData) async { @@ -24,12 +25,24 @@ Future generateDownloadConfig(FileData fileData) async { if (remote['type'] == 'webdav') { return DownloadConfig( - '${remoteConfig['url']}/${fileData.url.substring(scheme.length + 3)}', + '${remoteConfig['url']}/skyfs/${fileData.url.substring(scheme.length + 3)}', { 'Authorization': 'Basic ${base64.encode(utf8.encode('${remoteConfig['user']}:${remoteConfig['pass']}'))}' }, ); + } else if (remote['type'] == 's3') { + final client = storageService.dac.getS3Client(remoteId, remoteConfig); + + final url = await client.presignedGetObject( + remoteConfig['bucket'], + 'skyfs/${fileData.url.substring(scheme.length + 3)}', + ); + + return DownloadConfig( + url, + {}, + ); } else { throw 'Remote type ${remote['type']} not supported.'; } @@ -37,10 +50,3 @@ Future generateDownloadConfig(FileData fileData) async { return DownloadConfig(fileData.url, {}); } } - -class DownloadConfig { - DownloadConfig(this.url, this.headers); - - String url; - Map headers; -} diff --git a/lib/utils/skynet/health_status.dart b/lib/utils/skynet/health_status.dart new file mode 100644 index 0000000..141a981 --- /dev/null +++ b/lib/utils/skynet/health_status.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +// Method from https://github.com/riftdweb/rift/blob/main/packages/core/src/components/SkylinkInfo.tsx + +const excellent = 8; +const good = 6; +const poor = 4; + +class HealthStatus { + final Color color; + final String label; + final int redundancy; + final bool isPending; + + HealthStatus({ + required this.color, + required this.label, + required this.redundancy, + required this.isPending, + }); +} + +HealthStatus getHealthStatus( + {required int redundancy, bool isPending = false}) { + if (isPending && redundancy >= excellent) { + return HealthStatus( + color: Colors.green, + label: 'Excellent health ($redundancy)', + redundancy: redundancy, + isPending: isPending, + ); + } + if (isPending && redundancy >= good) { + return HealthStatus( + color: Colors.green, + label: 'At least good health ($redundancy)', + redundancy: redundancy, + isPending: isPending, + ); + } + if (isPending) { + return HealthStatus( + color: Colors.grey, + label: 'Checking health ($redundancy)', + redundancy: redundancy, + isPending: isPending, + ); + } + + if (redundancy >= excellent) { + return HealthStatus( + color: Colors.green, + label: 'Excellent health ($redundancy)', + redundancy: redundancy, + isPending: isPending, + ); + } + if (redundancy >= good) { + return HealthStatus( + color: Colors.green, + label: 'Good health ($redundancy)', + redundancy: redundancy, + isPending: isPending, + ); + } + if (redundancy >= poor) { + return HealthStatus( + color: Colors.red, + label: 'Poor health ($redundancy)', + redundancy: redundancy, + isPending: isPending, + ); + } + return HealthStatus( + color: Colors.red, + label: 'Health at risk ($redundancy)', + redundancy: redundancy, + isPending: isPending, + ); +} diff --git a/lib/view/browse.dart b/lib/view/browse.dart index c4c14ce..e46e45e 100644 --- a/lib/view/browse.dart +++ b/lib/view/browse.dart @@ -214,6 +214,9 @@ class _BrowseViewState extends State { } } } + final customRemote = + storageService.getCustomRemoteForPath(uri); + return DropTarget( onDragDone: (detail) async { logger.verbose( @@ -347,8 +350,6 @@ class _BrowseViewState extends State { LayoutBuilder(builder: (context, cons) { final actions = []; - - for (final ai in generateActions( false, null, @@ -663,7 +664,7 @@ class _BrowseViewState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'This MySky directory is synchronized with "${activeSyncTask!.localPath}"', + 'This SkyFS directory is synchronized with "${activeSyncTask!.localPath}"', style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold, @@ -865,6 +866,21 @@ class _BrowseViewState extends State { ), ), ), + if (customRemote != null) + Container( + width: double.infinity, + color: Theme.of(context).primaryColor, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'New files uploaded to this directory are stored on the "$customRemote" remote', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), if (isDirectorySharedReadOnly) Container( width: double.infinity, diff --git a/lib/view/file_details_dialog.dart b/lib/view/file_details_dialog.dart index 8e90f56..2c09b57 100644 --- a/lib/view/file_details_dialog.dart +++ b/lib/view/file_details_dialog.dart @@ -1,6 +1,7 @@ import 'package:filesize/filesize.dart'; import 'package:vup/app.dart'; import 'package:vup/utils/date_format.dart'; +import 'package:vup/widget/skylink_health.dart'; class FileDetailsDialog extends StatefulWidget { final DirectoryFile file; @@ -110,6 +111,12 @@ class FileDetailsDialogState extends State { '${filesize(file.file.padding ?? 0)} (${file.file.padding} bytes)', ), _buildRow('Blob URI', file.file.url), + if (file.file.url.startsWith('sia://')) + Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: SkylinkHealthWidget(file.file.url.substring(6)), + )), _buildRow( 'Chunk Size', '${filesize(file.file.chunkSize ?? 0)} (${file.file.chunkSize} bytes)', diff --git a/lib/view/share_dialog.dart b/lib/view/share_dialog.dart index 70e1bb9..8d577e5 100644 --- a/lib/view/share_dialog.dart +++ b/lib/view/share_dialog.dart @@ -330,6 +330,7 @@ class _ShareDialogState extends State { storageService.dac.copyFile( fileUri, shareUri.toString(), + generatePresignedUrls: true, ), ); } diff --git a/lib/widget/skylink_health.dart b/lib/widget/skylink_health.dart new file mode 100644 index 0000000..fcaf63d --- /dev/null +++ b/lib/widget/skylink_health.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; + +import 'package:vup/app.dart'; +import 'package:vup/utils/skynet/health_status.dart'; + +class SkylinkHealthWidget extends StatefulWidget { + final String skylink; + + const SkylinkHealthWidget(this.skylink, {Key? key}) : super(key: key); + + @override + State createState() => _SkylinkHealthWidgetState(); +} + +class _SkylinkHealthWidgetState extends State { + @override + void initState() { + _loadData(); + super.initState(); + } + + void _loadData() async { + _fetch(1); + _fetch(5); + _fetch(10); + _fetch(); + } + + bool isPending = true; + + void _fetch([int? timeout]) async { + final res = await mySky.skynetClient.httpClient.get( + Uri.parse( + // ignore: prefer_interpolation_to_compose_strings + 'https://${mySky.skynetClient.portalHost}/skynet/health/skylink/${widget.skylink}' + + (timeout == null ? '' : '?timeout=$timeout'), + ), + ); + if (timeout == null) isPending = false; + status = getHealthStatus( + redundancy: json.decode(res.body)['basesectorredundancy'], + isPending: isPending, + ); + setState(() {}); + } + + HealthStatus? status; + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'File health on Skynet', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + height: 4, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + status?.isPending ?? true + ? Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + color: status?.color, + ), + ), + ) + : Icon( + UniconsLine.check_circle, + color: status?.color, + ), + SizedBox( + width: 8, + ), + if (status != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(status!.label), + // Text('redundancy: ${status!.redundancy}'), + ], + ) + ], + ), + SizedBox( + height: 4, + ), + ElevatedButton( + onPressed: () async { + try { + showLoadingDialog(context, 'Pinning skylink...'); + await storageService.dac.client.pinSkylink(widget.skylink); + context.pop(); + setState(() { + isPending = true; + status = null; + }); + _loadData(); + } catch (e, st) { + context.pop(); + showErrorDialog(context, e, st); + } + }, + child: Text( + 'Pin now', + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/widget/theme_switch.dart b/lib/widget/theme_switch.dart index b79f287..2cf966a 100644 --- a/lib/widget/theme_switch.dart +++ b/lib/widget/theme_switch.dart @@ -40,7 +40,7 @@ class _ThemeSwitchState extends State { 'dark': 'Dark', }, activeStyle: TextStyle( - color: Colors.white, + color: Colors.black, fontWeight: FontWeight.w600, ), backgroundColor: diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 248c9ff..dc4f8fa 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = Vup Cloud Storage PRODUCT_BUNDLE_IDENTIFIER = app.vup // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2022 redsolver. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2022 redsolver. Licensed under the terms of the EUPL-1.2 license. diff --git a/pubspec.lock b/pubspec.lock index 8f0ef6b..f71bf16 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -102,7 +102,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" audio_session: dependency: transitive description: @@ -173,6 +173,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + buffer: + dependency: transitive + description: + name: buffer + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" build: dependency: transitive description: @@ -242,7 +249,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -284,7 +291,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -512,7 +519,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" feedback: dependency: "direct main" description: @@ -611,7 +618,7 @@ packages: description: path: dac ref: HEAD - resolved-ref: "0e244853210394c3e539c33599659483c0c31a4c" + resolved-ref: "989ee0601d2a42ec60fcad09bf503a1ea1f2175c" url: "https://github.com/redsolver/skyfs" source: git version: "0.7.0" @@ -731,21 +738,21 @@ packages: name: flutter_secure_storage url: "https://pub.dartlang.org" source: hosted - version: "5.0.2" + version: "6.0.0" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -1035,14 +1042,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -1056,7 +1063,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: "direct main" description: @@ -1071,6 +1078,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + minio: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: "0a5e8fd756fbf447cc104469768400ee3f8067b5" + url: "https://github.com/redsolver/minio-dart.git" + source: git + version: "3.5.0" mno_commons: dependency: transitive description: @@ -1568,7 +1584,7 @@ packages: description: path: "." ref: experimental - resolved-ref: "0a524b6902595ff8366e8c6fc1ff104538af46fe" + resolved-ref: "846db49700e431911becffabec17a53466b57c15" url: "https://github.com/redsolver/skynet.git" source: git version: "5.0.0" @@ -1606,7 +1622,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" sprintf: dependency: transitive description: @@ -1662,7 +1678,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" string_validator: dependency: "direct main" description: @@ -1690,14 +1706,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" time: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 610d94e..6ea0dff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.11.1+1101 +version: 0.12.0+1200 environment: sdk: ">=2.13.0 <3.0.0" @@ -36,7 +36,6 @@ dependencies: git: url: https://github.com/redsolver/skynet.git ref: experimental - # TODO Use biometric_storage or https://pub.dev/packages/flutter_secure_storage simple_observable: ^2.0.0 flutter_svg: ^1.0.1 path_provider: ^2.0.2 @@ -135,7 +134,7 @@ dependencies: xml2json: ^5.3.2 dart_chromecast: ^0.3.3 contextmenu: ^3.0.0 - flutter_secure_storage: ^5.0.2 + flutter_secure_storage: ^6.0.0 local_auth: ^2.1.0 dart_discord_rpc: git: https://github.com/alexmercerind/dart_discord_rpc.git diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index a757380..5f4d221 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -69,7 +69,7 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" #ifdef FLUTTER_BUILD_NAME #define VERSION_AS_STRING #FLUTTER_BUILD_NAME #else -#define VERSION_AS_STRING "0.11.0" +#define VERSION_AS_STRING "0.12.0" #endif VS_VERSION_INFO VERSIONINFO @@ -93,7 +93,7 @@ BEGIN VALUE "FileDescription", "Vup Cloud Storage" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "vup" "\0" - VALUE "LegalCopyright", "Copyright (C) 2022 redsolver. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 redsolver. Licensed under the terms of the EUPL-1.2 license." "\0" VALUE "OriginalFilename", "vup.exe" "\0" VALUE "ProductName", "vup" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"