ponshu-room-lite/lib/widgets/settings/backup_settings_section.dart

309 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../services/backup_service.dart';
import '../../theme/app_colors.dart';
class BackupSettingsSection extends StatefulWidget {
final String title;
const BackupSettingsSection({
super.key,
this.title = 'バックアップ・復元',
});
@override
State<BackupSettingsSection> createState() => _BackupSettingsSectionState();
}
enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
class _BackupSettingsSectionState extends State<BackupSettingsSection> {
final BackupService _backupService = BackupService();
_BackupState _state = _BackupState.idle;
@override
void initState() {
super.initState();
_initBackupService();
}
Future<void> _initBackupService() async {
await _backupService.init();
if (mounted) {
setState(() {});
}
}
Future<void> _signIn() async {
final messenger = ScaffoldMessenger.of(context);
setState(() => _state = _BackupState.signingIn);
final account = await _backupService.signIn();
if (mounted) {
setState(() => _state = _BackupState.idle);
if (account != null) {
messenger.showSnackBar(
SnackBar(content: Text('${account.email} で連携しました')),
);
} else {
messenger.showSnackBar(
const SnackBar(content: Text('連携がキャンセルされました')),
);
}
}
}
Future<void> _signOut() async {
final messenger = ScaffoldMessenger.of(context);
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('連携解除'),
content: const Text('Googleアカウントとの連携を解除しますか\n安全にログアウトできます。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('解除'),
),
],
),
);
if (confirmed == true) {
setState(() => _state = _BackupState.signingOut);
await _backupService.signOut();
if (mounted) {
setState(() => _state = _BackupState.idle);
messenger.showSnackBar(
const SnackBar(content: Text('連携を解除しました')),
);
}
}
}
Future<void> _createBackup() async {
final messenger = ScaffoldMessenger.of(context);
setState(() => _state = _BackupState.backingUp);
final success = await _backupService.createBackup();
if (mounted) {
setState(() => _state = _BackupState.idle);
messenger.showSnackBar(
SnackBar(
content: Text(success ? 'バックアップが完了しました' : 'バックアップに失敗しました'),
// Snackbars can keep Green/Red for semantic clarity, or be neutral.
// User asked to remove Green/Red icons from the UI, but feedback (Snackbar) usually stays semantic.
// However, to be safe and "Washi", let's use Sumi (Black) for success?
// Or just leave snackbars as they are ephemeral. The request was likely about the visible static UI.
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
}
Future<void> _confirmBackup() async {
final appColors = Theme.of(context).extension<AppColors>()!;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('バックアップ'),
content: const Text('Google Driveに最新データのみ保存過去分は上書きされます。\n本当に続行しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: appColors.brandPrimary,
foregroundColor: appColors.surfaceSubtle,
),
child: const Text('バックアップ'),
),
],
),
);
if (confirmed == true && mounted) {
await _createBackup();
}
}
Future<void> _restoreBackup() async {
final messenger = ScaffoldMessenger.of(context);
final appColors = Theme.of(context).extension<AppColors>()!;
// Note: hasBackup check is async
final hasBackup = await _backupService.hasBackupOnDrive();
if (!hasBackup) {
if (mounted) {
messenger.showSnackBar(
const SnackBar(content: Text('バックアップファイルが見つかりません')),
);
}
return;
}
if (!mounted) return;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(LucideIcons.alertTriangle, color: appColors.warning, size: 24),
const SizedBox(width: 8),
const Text('データ復元'),
],
),
content: const Text('現在のデータは上書きされます。\n削除されたデータは元に戻りません。\n\n本当に続行しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('キャンセル'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: appColors.warning,
foregroundColor: appColors.surfaceSubtle,
),
child: const Text('復元'),
),
],
),
);
if (confirmed == true && mounted) {
setState(() => _state = _BackupState.restoring);
final success = await _backupService.restoreBackup();
if (mounted) {
setState(() => _state = _BackupState.idle);
messenger.showSnackBar(
SnackBar(
content: Text(success ? '復元が完了しました' : '復元に失敗しました'),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final currentUser = _backupService.currentUser;
final isAnyProcessing = _state != _BackupState.idle;
final appColors = Theme.of(context).extension<AppColors>()!;
return Column(
children: [
_buildSectionHeader(context, widget.title, LucideIcons.cloud),
// Wi-Fi warning
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: appColors.info.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: appColors.info.withValues(alpha: 0.3)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(LucideIcons.wifi, color: appColors.info, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
'Wi-Fi環境下での実行を推奨します\nデータ量が100MB以上になる可能性があります',
style: TextStyle(
fontSize: 12,
color: appColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
Card(
color: appColors.surfaceSubtle,
elevation: 0,
margin: EdgeInsets.zero,
child: Column(
children: [
ListTile(
leading: Icon(
currentUser != null ? LucideIcons.checkCircle2 : LucideIcons.user,
color: currentUser != null ? appColors.success : appColors.iconSubtle,
),
title: Text(currentUser == null ? 'Googleアカウント連携' : currentUser.email,
style: TextStyle(color: appColors.textPrimary)),
subtitle: currentUser == null ? Text('Google Driveにバックアップ',
style: TextStyle(color: appColors.textSecondary)) : null,
trailing: (_state == _BackupState.signingIn || _state == _BackupState.signingOut)
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: currentUser == null ? appColors.brandPrimary : appColors.error,
foregroundColor: appColors.surfaceSubtle,
padding: const EdgeInsets.symmetric(horizontal: 16),
),
onPressed: isAnyProcessing ? null : (currentUser == null ? _signIn : _signOut),
child: Text(
currentUser == null ? '連携' : '解除',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
if (currentUser != null) ...[
Divider(height: 1, color: appColors.divider),
ListTile(
leading: Icon(LucideIcons.uploadCloud, color: appColors.iconDefault),
title: Text('バックアップ', style: TextStyle(color: appColors.textPrimary)),
trailing: _state == _BackupState.backingUp
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
: null,
onTap: isAnyProcessing ? null : _confirmBackup,
),
Divider(height: 1, color: appColors.divider),
ListTile(
leading: Icon(LucideIcons.downloadCloud, color: appColors.iconDefault),
title: Text('データ復元', style: TextStyle(color: appColors.textPrimary)),
trailing: _state == _BackupState.restoring
? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))
: null,
onTap: isAnyProcessing ? null : _restoreBackup,
),
],
],
),
),
],
);
}
Widget _buildSectionHeader(BuildContext context, String title, IconData icon) {
final appColors = Theme.of(context).extension<AppColors>()!;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row(
children: [
Icon(icon, size: 20, color: appColors.iconDefault),
const SizedBox(width: 8),
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: appColors.textPrimary,
),
),
],
),
);
}
}