diff --git a/ios/Runner/DebugProfile.entitlements b/ios/Runner/DebugProfile.entitlements
new file mode 100644
index 00000000..fbad0237
--- /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 00000000..fbad0237
--- /dev/null
+++ b/ios/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ keychain-access-groups
+
+
+
diff --git a/lib/config_provider.dart b/lib/config_provider.dart
index fc848b98..5729a6f7 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 = {
diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart
index c269111b..eebc91c5 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
new file mode 100644
index 00000000..e2182024
--- /dev/null
+++ b/lib/pages/settings/synchronization_settings.dart
@@ -0,0 +1,282 @@
+import 'package:daily_you/config_provider.dart';
+import 'package:daily_you/synchronization/synchronization_provider.dart';
+import 'package:daily_you/widgets/settings_toggle.dart';
+import 'package:flutter/material.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();
+ 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: const Text("Synchronization"), // TODO: localization
+ centerTitle: true,
+ ),
+ body: ListView(
+ children: [
+ SettingsToggle(
+ 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),
+ child: Divider(),
+ ),
+ Form(
+ key: _formKey,
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: DropdownButtonFormField(
+ initialValue: selectedProvider,
+ decoration: const InputDecoration(
+ labelText: 'Sync Provider',
+ border: OutlineInputBorder(),
+ ),
+ items: providerItems.map>((String value) {
+ return DropdownMenuItem(
+ value: value,
+ child: Text(value == 'none' ? 'NONE' : value.toUpperCase()),
+ );
+ }).toList(),
+ onChanged: (String? newValue) {
+ if (newValue != null && newValue != 'none') {
+ configProvider.set(ConfigKey.syncProvider, newValue);
+ _initializeProvider(newValue);
+ } else {
+ configProvider.set(ConfigKey.syncProvider, 'none');
+ setState(() => _currentProvider = null);
+ }
+ },
+ ),
+ ),
+ // 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/pages/settings_page.dart b/lib/pages/settings_page.dart
index 85a81ae5..1d04819c 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/providers/webdav_provider.dart b/lib/synchronization/providers/webdav_provider.dart
new file mode 100644
index 00000000..12bd5ebe
--- /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
new file mode 100644
index 00000000..98d5c9e4
--- /dev/null
+++ b/lib/synchronization/synchronization_provider.dart
@@ -0,0 +1,47 @@
+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,
+ conflict,
+ unauthorized,
+}
+
+abstract class SynchronizationProvider {
+ final storage = FlutterSecureStorage();
+ Future synchronize({bool? preferRemote});
+ Future authorize();
+ 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 d978d19b..88662820 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 cef119cf..5cfdd005 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 0708a938..28f8b45e 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 e3cbb92e..aaa7d0a3 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:
@@ -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:
@@ -580,10 +652,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:
@@ -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:
@@ -1263,5 +1343,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"
diff --git a/pubspec.yaml b/pubspec.yaml
index fc2bd05e..657dc803 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 17c01dfa..88ea9eb7 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 6643c864..1b03afc7 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