From 6d0115120f08c4012e049699d21432f7b426dff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0tefan=C4=8Da?= Date: Wed, 4 Mar 2026 20:21:50 +0100 Subject: [PATCH 1/3] add config keys for provider, and enabled --- lib/config_provider.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/config_provider.dart b/lib/config_provider.dart index fc848b9..5729a6f 100644 --- a/lib/config_provider.dart +++ b/lib/config_provider.dart @@ -60,6 +60,9 @@ class ConfigKey { static const String requirePassword = "requirePassword"; static const String biometricUnlock = "biometricUnlock"; static const String passwordHash = "passwordHash"; + // Synchronization settings + static const String syncEnabled = "syncEnabled"; + static const String syncProvider = "syncProvider"; // DEPRECATED static const String imageQuality = "imageQuality"; } @@ -121,6 +124,8 @@ class ConfigProvider with ChangeNotifier { ConfigKey.showflashbackGoodDay: true, ConfigKey.showflashbackRandomDay: true, ConfigKey.hideImagesInGallery: false, + ConfigKey.syncEnabled: false, + ConfigKey.syncProvider: 'none', }; final Map _secureConfig = { From 2fc7c4da148bf625713094b0079071822968c2bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0tefan=C4=8Da?= Date: Wed, 4 Mar 2026 20:22:46 +0100 Subject: [PATCH 2/3] implement simple placeholder UI, add abstract synchronization provider class --- .../settings/synchronization_settings.dart | 88 +++++++++++++++++++ lib/pages/settings_page.dart | 20 ++++- .../synchronization_provider.dart | 17 ++++ pubspec.lock | 10 +-- 4 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 lib/pages/settings/synchronization_settings.dart create mode 100644 lib/synchronization/synchronization_provider.dart diff --git a/lib/pages/settings/synchronization_settings.dart b/lib/pages/settings/synchronization_settings.dart new file mode 100644 index 0000000..43c5ac1 --- /dev/null +++ b/lib/pages/settings/synchronization_settings.dart @@ -0,0 +1,88 @@ +import 'package:daily_you/backup_restore_utils.dart'; +import 'package:daily_you/config_provider.dart'; +import 'package:daily_you/import_utils.dart'; +import 'package:daily_you/utils/export_utils.dart'; +import 'package:daily_you/widgets/settings_icon_action.dart'; +import 'package:daily_you/widgets/settings_toggle.dart'; +import 'package:flutter/material.dart'; +import 'package:daily_you/l10n/generated/app_localizations.dart'; +import 'package:provider/provider.dart'; + +class SynchronizationSettings extends StatefulWidget { + const SynchronizationSettings({super.key}); + + @override + State createState() => + _SynchronizationSettingsState(); +} + +class _SynchronizationSettingsState extends State { + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final configProvider = Provider.of(context); + + return Scaffold( + appBar: AppBar( + title: Text("Synchronization"), // TODO: localization + centerTitle: true, + ), + body: ListView( + children: [ + SettingsToggle( + title: "Enable synchronization", + settingsKey: ConfigKey.syncEnabled, + onChanged: (value) { + configProvider.set(ConfigKey.syncEnabled, value); + }), + if (configProvider.get(ConfigKey.syncEnabled)) ...[ + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Divider(), + ), + Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: DropdownButtonFormField( + initialValue: configProvider.get(ConfigKey.syncProvider), + decoration: InputDecoration( + labelText: 'Sync Provider', + border: OutlineInputBorder(), + ), + items: ['none', 'Dropbox'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + configProvider.set( + ConfigKey.syncProvider, newValue ?? "none"); + }); + }, + ), + ), + SizedBox(height: 16), + SettingsIconAction(title: "Synchronize", icon: Icon(Icons.sync), onPressed: () => { + + }) + ], + ), + ), + ] + ], + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 85a81ae..1d04819 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -7,6 +7,7 @@ import 'package:daily_you/pages/settings/language_settings.dart'; import 'package:daily_you/pages/settings/notification_settings.dart'; import 'package:daily_you/pages/settings/security_settings.dart'; import 'package:daily_you/pages/settings/storage_settings.dart'; +import 'package:daily_you/pages/settings/synchronization_settings.dart'; import 'package:daily_you/pages/settings/templates_page.dart'; import 'package:daily_you/providers/entries_provider.dart'; import 'package:daily_you/widgets/settings_category.dart'; @@ -48,20 +49,29 @@ class _SettingsPageState extends State { text: AppLocalizations.of(context)!.settingsMadeWithLove, style: TextStyle( fontSize: 14, - color: Theme.of(context).colorScheme.secondary)), + color: Theme + .of(context) + .colorScheme + .secondary)), if (entriesProvider.entries.length > 30) TextSpan( text: " ", style: TextStyle( fontSize: 14, - color: Theme.of(context).colorScheme.secondary)), + color: Theme + .of(context) + .colorScheme + .secondary)), if (entriesProvider.entries.length > 30) TextSpan( text: AppLocalizations.of(context)! .settingsConsiderSupporting, style: TextStyle( fontSize: 14, - color: Theme.of(context).colorScheme.primary, + color: Theme + .of(context) + .colorScheme + .primary, decoration: TextDecoration.underline, ), recognizer: TapGestureRecognizer() @@ -116,6 +126,10 @@ class _SettingsPageState extends State { title: AppLocalizations.of(context)!.settingsBackupRestoreTitle, icon: Icons.settings_backup_restore_rounded, page: BackupRestoreSettings()), + SettingsCategory( + title: "Synchronization", + icon: Icons.sync, + page: SynchronizationSettings()), SettingsCategory( title: AppLocalizations.of(context)!.settingsAboutTitle, icon: Icons.info_rounded, diff --git a/lib/synchronization/synchronization_provider.dart b/lib/synchronization/synchronization_provider.dart new file mode 100644 index 0000000..7eb4631 --- /dev/null +++ b/lib/synchronization/synchronization_provider.dart @@ -0,0 +1,17 @@ +enum SynchronizationResult { + success, + failure, + conflict, + unauthorized, +} + +abstract class SynchronizationProvider { + late final String name; + late final List requiredUrls; + + Future synchronize(); + Future authorize(); + Future storeSecret(String key, String value); + Future getSecret(String key); + Future deleteSecret(String key); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index e3cbb92..8c40b87 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -580,10 +580,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" media_scanner: dependency: "direct main" description: @@ -1263,5 +1263,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.1 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.0" From 964b079e619f56010bf9a797d2c70b8e6cad4ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0tefan=C4=8Da?= Date: Sat, 7 Mar 2026 11:03:24 +0100 Subject: [PATCH 3/3] add synchronization provider, WebDav support, secret management and simple upload/download --- ios/Runner/DebugProfile.entitlements | 8 + ios/Runner/Release.entitlements | 8 + lib/database/app_database.dart | 16 +- .../settings/synchronization_settings.dart | 242 ++++++++++++++-- .../providers/webdav_provider.dart | 269 ++++++++++++++++++ .../synchronization_provider.dart | 44 ++- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 80 ++++++ pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 647 insertions(+), 33 deletions(-) create mode 100644 ios/Runner/DebugProfile.entitlements create mode 100644 ios/Runner/Release.entitlements create mode 100644 lib/synchronization/providers/webdav_provider.dart diff --git a/ios/Runner/DebugProfile.entitlements b/ios/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..fbad023 --- /dev/null +++ b/ios/Runner/DebugProfile.entitlements @@ -0,0 +1,8 @@ + + + + + keychain-access-groups + + + diff --git a/ios/Runner/Release.entitlements b/ios/Runner/Release.entitlements new file mode 100644 index 0000000..fbad023 --- /dev/null +++ b/ios/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + keychain-access-groups + + + diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index c269111..eebc91c 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:daily_you/config_provider.dart'; import 'package:daily_you/database/image_storage.dart'; @@ -144,6 +145,18 @@ class AppDatabase { } } + Future getDatabaseBytes() { + return FileLayer.getFileBytes(_internalPath!, useExternalPath: false); + } + + Future restoreFromBytes(List remoteBytes) async { + await close(); + await FileLayer.writeFileBytes(_internalPath!, Uint8List.fromList(remoteBytes), + useExternalPath: false); + await open(); + } + + /// Pull in remote changes if the external database is newer or if forceOverwrite is set Future _syncWithExternalDatabase({bool forceOverwrite = false}) async { // Check if external database exists @@ -284,5 +297,4 @@ DROP TABLE old_entries; '''); }); } - } -} + }} diff --git a/lib/pages/settings/synchronization_settings.dart b/lib/pages/settings/synchronization_settings.dart index 43c5ac1..e218202 100644 --- a/lib/pages/settings/synchronization_settings.dart +++ b/lib/pages/settings/synchronization_settings.dart @@ -1,11 +1,7 @@ -import 'package:daily_you/backup_restore_utils.dart'; import 'package:daily_you/config_provider.dart'; -import 'package:daily_you/import_utils.dart'; -import 'package:daily_you/utils/export_utils.dart'; -import 'package:daily_you/widgets/settings_icon_action.dart'; +import 'package:daily_you/synchronization/synchronization_provider.dart'; import 'package:daily_you/widgets/settings_toggle.dart'; import 'package:flutter/material.dart'; -import 'package:daily_you/l10n/generated/app_localizations.dart'; import 'package:provider/provider.dart'; class SynchronizationSettings extends StatefulWidget { @@ -18,29 +14,173 @@ class SynchronizationSettings extends StatefulWidget { class _SynchronizationSettingsState extends State { final _formKey = GlobalKey(); + SynchronizationProvider? _currentProvider; + bool _isAuthorizing = false; + bool _isSynchronizing = false; @override void initState() { super.initState(); } + void _initializeProvider(String providerType) { + try { + setState(() { + _currentProvider = ProviderFactory.createProvider(providerType); + }); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error initializing provider: $e')), + ); + } + } + + Future _handleAuthorize() async { + if (_currentProvider == null) return; + + setState(() => _isAuthorizing = true); + + try { + final success = await _currentProvider!.authorize(); + + if (mounted) { + setState(() => _isAuthorizing = false); + + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Authorization successful!'), + backgroundColor: Colors.green, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Authorization failed!'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + if (mounted) { + setState(() => _isAuthorizing = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Authorization error: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _handleSynchronize({bool? preferRemote}) async { + if (_currentProvider == null) return; + + setState(() => _isSynchronizing = true); + + try { + final result = await _currentProvider!.synchronize(preferRemote: preferRemote); + + if (mounted) { + setState(() => _isSynchronizing = false); + + String message; + Color backgroundColor; + bool showRemoteLocalQuestion = false; + + switch (result) { + case SynchronizationResult.success: + message = 'Synchronization completed successfully!'; + backgroundColor = Colors.green; + break; + case SynchronizationResult.failure: + message = 'Synchronization failed!'; + backgroundColor = Colors.red; + break; + case SynchronizationResult.conflict: + message = 'Conflict detected during synchronization!'; + backgroundColor = Colors.orange; + showRemoteLocalQuestion = true; + break; + case SynchronizationResult.unauthorized: + message = 'Unauthorized! Please authorize first.'; + backgroundColor = Colors.red; + break; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: backgroundColor, + ), + ); + + if (showRemoteLocalQuestion) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Conflict Detected'), + content: const Text( + 'A conflict was detected during synchronization. Do you want to prefer remote changes?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _handleSynchronize(preferRemote: true); + }, + child: const Text('Prefer Remote'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _handleSynchronize(preferRemote: false); + }, + child: const Text('Prefer Local'), + ), + ], + ), + ); + } + } + } catch (e) { + if (mounted) { + setState(() => _isSynchronizing = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Synchronization error: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + @override Widget build(BuildContext context) { final configProvider = Provider.of(context); + final providerItems = {'none', ...ProviderFactory.supportedProviders}.toList(); + final savedProvider = configProvider.get(ConfigKey.syncProvider) as String?; + final selectedProvider = providerItems.contains(savedProvider) ? savedProvider : 'none'; return Scaffold( appBar: AppBar( - title: Text("Synchronization"), // TODO: localization + title: const Text("Synchronization"), // TODO: localization centerTitle: true, ), body: ListView( children: [ SettingsToggle( - title: "Enable synchronization", - settingsKey: ConfigKey.syncEnabled, - onChanged: (value) { - configProvider.set(ConfigKey.syncEnabled, value); - }), + title: "Enable synchronization", + settingsKey: ConfigKey.syncEnabled, + onChanged: (value) { + configProvider.set(ConfigKey.syncEnabled, value); + if (!value) { + setState(() => _currentProvider = null); + } + }, + ), if (configProvider.get(ConfigKey.syncEnabled)) ...[ Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), @@ -53,30 +193,84 @@ class _SynchronizationSettingsState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: DropdownButtonFormField( - initialValue: configProvider.get(ConfigKey.syncProvider), - decoration: InputDecoration( + initialValue: selectedProvider, + decoration: const InputDecoration( labelText: 'Sync Provider', border: OutlineInputBorder(), ), - items: ['none', 'Dropbox'] - .map>((String value) { + items: providerItems.map>((String value) { return DropdownMenuItem( value: value, - child: Text(value), + child: Text(value == 'none' ? 'NONE' : value.toUpperCase()), ); }).toList(), onChanged: (String? newValue) { - setState(() { - configProvider.set( - ConfigKey.syncProvider, newValue ?? "none"); - }); + if (newValue != null && newValue != 'none') { + configProvider.set(ConfigKey.syncProvider, newValue); + _initializeProvider(newValue); + } else { + configProvider.set(ConfigKey.syncProvider, 'none'); + setState(() => _currentProvider = null); + } }, ), ), - SizedBox(height: 16), - SettingsIconAction(title: "Synchronize", icon: Icon(Icons.sync), onPressed: () => { - - }) + // Provider settings widget + if (_currentProvider != null) ...[ + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: _currentProvider!.getSettingsWidget(), + ), + const SizedBox(height: 24), + // Action buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: + _isAuthorizing ? null : _handleAuthorize, + icon: _isAuthorizing + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.login), + label: Text(_isAuthorizing + ? 'Authorizing...' + : 'Authorize'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: _isSynchronizing + ? null + : _handleSynchronize, + icon: _isSynchronizing + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.sync), + label: Text(_isSynchronizing + ? 'Syncing...' + : 'Synchronize'), + ), + ), + ], + ), + ), + ], + const SizedBox(height: 16), ], ), ), diff --git a/lib/synchronization/providers/webdav_provider.dart b/lib/synchronization/providers/webdav_provider.dart new file mode 100644 index 0000000..12bd5eb --- /dev/null +++ b/lib/synchronization/providers/webdav_provider.dart @@ -0,0 +1,269 @@ +import 'package:daily_you/database/app_database.dart'; +import 'package:daily_you/synchronization/synchronization_provider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:webdav_client/webdav_client.dart'; + +/* +WebDav testing: docker run --rm -p 8080:8080 -v ./data:/data -e RCLONE_USER=username -e RCLONE_PASS=password rclone/rclone:1.71.2 serve webdav /data --addr :8080 --baseurl '/webdav' + */ +class WebdavProvider extends SynchronizationProvider { + @override + Future authorize() async { + try { + // Verify required credentials are present. + final hasCredentials = await Future.wait([ + hasSecret('webdav_server_url'), + hasSecret('webdav_username'), + hasSecret('webdav_password'), + ]); + + if (!hasCredentials.every((exists) => exists)) { + return false; + } + + final client = await _createClient(); + final path = + await getSecret('webdav_path', defaultValue: '/daily-you/') ?? + '/daily-you/'; + + try { + await client.ping(); + await client.mkdirAll(path); + + return true; + } catch (e) { + return false; + } + } catch (_) { + return false; + } + } + + Future _createClient() async { + final serverUrl = await getSecret('webdav_server_url'); + final username = await getSecret('webdav_username'); + final password = await getSecret('webdav_password'); + + if (serverUrl == null || username == null || password == null) { + throw Exception('Missing WebDAV credentials'); + } + + var client = newClient(serverUrl, user: username, password: password); + client.setHeaders({'accept-charset': 'utf-8'}); + + // Set the connection server timeout time in milliseconds. + client.setConnectTimeout(8000); + + // Set send data timeout time in milliseconds. + client.setSendTimeout(8000); + + // Set transfer data time in milliseconds. + client.setReceiveTimeout(8000); + return client; + } + + @override + Future synchronize({bool? preferRemote}) async { + var bytes = await AppDatabase.instance.getDatabaseBytes(); + if (bytes == null) { + return SynchronizationResult.failure; + } + if (!await authorize()) { + return SynchronizationResult.unauthorized; + } + + final client = await _createClient(); + final path = await getSecret('webdav_path', defaultValue: '/daily-you/') ?? + '/daily-you/'; + + try { + final existingFiles = await client.readDir(path); + // TODO check correctly for the conflicts + if (existingFiles.any((file) => file.name == 'daily_you_backup.db')) { + if (preferRemote == null) { + return SynchronizationResult.conflict; + } else if (preferRemote) { + try { + final remoteBytes = await client.read('$path/daily_you_backup.db'); + await AppDatabase.instance.restoreFromBytes(remoteBytes); + return SynchronizationResult.success; + } catch (e) { + return SynchronizationResult.failure; + } + } + } + } catch (e) { + // If the directory doesn't exist, we'll create it during upload, so we can ignore this error. + } + + try { + await client.write('$path/daily_you_backup.db', bytes); + return SynchronizationResult.success; + } catch (e) { + return SynchronizationResult.failure; + } + } + + @override + StatefulWidget getSettingsWidget() { + return WebdavSettingsWidget(provider: this); + } +} + +class WebdavSettingsWidget extends StatefulWidget { + final WebdavProvider provider; + final VoidCallback? onSaved; + + const WebdavSettingsWidget({ + super.key, + required this.provider, + this.onSaved, + }); + + @override + _WebdavSettingsWidgetState createState() => _WebdavSettingsWidgetState(); +} + +class _WebdavSettingsWidgetState extends State { + late TextEditingController _serverUrlController; + late TextEditingController _usernameController; + late TextEditingController _passwordController; + late TextEditingController _pathController; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _serverUrlController = TextEditingController(); + _usernameController = TextEditingController(); + _passwordController = TextEditingController(); + _pathController = TextEditingController(); + _loadSettings(); + } + + Future _loadSettings() async { + setState(() => _isLoading = true); + try { + final serverUrl = await widget.provider.getSecret('webdav_server_url'); + final username = await widget.provider.getSecret('webdav_username'); + final password = await widget.provider.getSecret('webdav_password'); + final path = await widget.provider.getSecret('webdav_path'); + + if (mounted) { + setState(() { + _serverUrlController.text = serverUrl ?? ''; + _usernameController.text = username ?? ''; + _passwordController.text = password ?? ''; + _pathController.text = path ?? '/daily-you/'; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error loading settings: $e')), + ); + } + } + } + + Future _saveSettings() async { + setState(() => _isLoading = true); + try { + await widget.provider + .storeSecret('webdav_server_url', _serverUrlController.text); + await widget.provider + .storeSecret('webdav_username', _usernameController.text); + await widget.provider + .storeSecret('webdav_password', _passwordController.text); + await widget.provider.storeSecret('webdav_path', _pathController.text); + + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Settings saved successfully')), + ); + widget.onSaved?.call(); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error saving settings: $e')), + ); + } + } + } + + @override + void dispose() { + _serverUrlController.dispose(); + _usernameController.dispose(); + _passwordController.dispose(); + _pathController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "WebDAV Settings", + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + TextField( + controller: _serverUrlController, + decoration: const InputDecoration( + labelText: "Server URL", + hintText: "https://example.com/webdav", + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.url, + ), + const SizedBox(height: 16), + TextField( + controller: _usernameController, + decoration: const InputDecoration( + labelText: "Username", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + decoration: const InputDecoration( + labelText: "Password", + border: OutlineInputBorder(), + ), + obscureText: true, + ), + const SizedBox(height: 16), + TextField( + controller: _pathController, + decoration: const InputDecoration( + labelText: "Path", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: _saveSettings, + icon: const Icon(Icons.save), + label: const Text('Save Settings'), + ), + ], + ), + ); + } +} diff --git a/lib/synchronization/synchronization_provider.dart b/lib/synchronization/synchronization_provider.dart index 7eb4631..98d5c9e 100644 --- a/lib/synchronization/synchronization_provider.dart +++ b/lib/synchronization/synchronization_provider.dart @@ -1,3 +1,7 @@ +import 'package:daily_you/synchronization/providers/webdav_provider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + enum SynchronizationResult { success, failure, @@ -6,12 +10,38 @@ enum SynchronizationResult { } abstract class SynchronizationProvider { - late final String name; - late final List requiredUrls; - - Future synchronize(); + final storage = FlutterSecureStorage(); + Future synchronize({bool? preferRemote}); Future authorize(); - Future storeSecret(String key, String value); - Future getSecret(String key); - Future deleteSecret(String key); + Future storeSecret(String key, String value) { + return storage.write(key: key, value: value); + } + Future getSecret(String key, {String? defaultValue}) async { + if (await storage.containsKey(key: key)) { + return storage.read(key: key); + } else { + return defaultValue; + } + } + Future deleteSecret(String key) { + return storage.delete(key: key); + } + Future hasSecret(String key) { + return storage.containsKey(key: key); + } + StatefulWidget getSettingsWidget(); +} + +class ProviderFactory { + static const List supportedProviders = ['webdav', 'dropbox']; + static SynchronizationProvider createProvider(String type) { + switch (type) { + case 'webdav': + return WebdavProvider(); + case 'dropbox': + throw UnimplementedError('DropboxProvider not implemented yet'); + default: + throw ArgumentError('Unknown provider type: $type'); + } + } } \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d978d19..8866282 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index cef119c..5cfdd00 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + flutter_secure_storage_linux sqlite3_flutter_libs system_theme url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0708a93..28f8b45 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import file_picker import file_selector_macos import flutter_image_compress_macos import flutter_local_notifications +import flutter_secure_storage_darwin import local_auth_darwin import package_info_plus import path_provider_foundation @@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 8c40b87..aaa7d0a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" cross_file: dependency: transitive description: @@ -121,6 +129,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.3" + dio: + dependency: transitive + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" easy_debounce: dependency: "direct main" description: @@ -355,6 +379,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.28" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + url: "https://pub.dev" + source: hosted + version: "10.0.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter_svg: dependency: "direct main" description: @@ -1214,6 +1286,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + webdav_client: + dependency: "direct main" + description: + name: webdav_client + sha256: "682fffc50b61dc0e8f46717171db03bf9caaa17347be41c0c91e297553bf86b2" + url: "https://pub.dev" + source: hosted + version: "1.2.2" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fc2bd05..657dc80 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,8 @@ dependencies: word_count: ^1.0.4 xml: ^6.6.1 csv: ^6.0.0 + flutter_secure_storage: ^10.0.0 + webdav_client: ^1.2.2 dev_dependencies: flutter_lints: ^5.0.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 17c01df..88ea9eb 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 6643c86..1b03afc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + flutter_secure_storage_windows local_auth_windows permission_handler_windows share_plus