Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ios/Runner/DebugProfile.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
8 changes: 8 additions & 0 deletions ios/Runner/Release.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>
5 changes: 5 additions & 0 deletions lib/config_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down Expand Up @@ -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<String, dynamic> _secureConfig = {
Expand Down
16 changes: 14 additions & 2 deletions lib/database/app_database.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -144,6 +145,18 @@ class AppDatabase {
}
}

Future<Uint8List?> getDatabaseBytes() {
return FileLayer.getFileBytes(_internalPath!, useExternalPath: false);
}

Future<void> restoreFromBytes(List<int> 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<bool> _syncWithExternalDatabase({bool forceOverwrite = false}) async {
// Check if external database exists
Expand Down Expand Up @@ -284,5 +297,4 @@ DROP TABLE old_entries;
''');
});
}
}
}
}}
282 changes: 282 additions & 0 deletions lib/pages/settings/synchronization_settings.dart
Original file line number Diff line number Diff line change
@@ -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<SynchronizationSettings> createState() =>
_SynchronizationSettingsState();
}

class _SynchronizationSettingsState extends State<SynchronizationSettings> {
final _formKey = GlobalKey<FormState>();
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<void> _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<void> _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<ConfigProvider>(context);
final providerItems = <String>{'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<String>(
initialValue: selectedProvider,
decoration: const InputDecoration(
labelText: 'Sync Provider',
border: OutlineInputBorder(),
),
items: providerItems.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
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),
],
),
),
]
],
),
);
}
}
Loading