Compare commits

..

No commits in common. "main" and "v1.0.49" have entirely different histories.

6 changed files with 35 additions and 161 deletions

View File

@ -358,15 +358,6 @@ class _PendingAnalysisScreenState extends ConsumerState<PendingAnalysisScreen> {
width: 60, width: 60,
height: 60, height: 60,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: appColors.divider,
borderRadius: BorderRadius.circular(8),
),
child: Icon(LucideIcons.image, color: appColors.iconSubtle),
),
), ),
) )
: Container( : Container(

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart'; import 'package:lucide_icons/lucide_icons.dart';
import '../providers/theme_provider.dart'; import '../providers/theme_provider.dart';
import '../theme/app_colors.dart';
import '../widgets/settings/display_settings_section.dart'; import '../widgets/settings/display_settings_section.dart';
import '../widgets/settings/other_settings_section.dart'; import '../widgets/settings/other_settings_section.dart';
import '../widgets/settings/backup_settings_section.dart'; import '../widgets/settings/backup_settings_section.dart';
@ -23,7 +22,7 @@ class _ShopSettingsScreenState extends ConsumerState<ShopSettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userProfile = ref.watch(userProfileProvider); final userProfile = ref.watch(userProfileProvider);
final appColors = Theme.of(context).extension<AppColors>()!; final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -36,9 +35,9 @@ class _ShopSettingsScreenState extends ConsumerState<ShopSettingsScreen> {
// Business Config Section // Business Config Section
_buildSectionHeader(context, '価格設定', LucideIcons.briefcase), _buildSectionHeader(context, '価格設定', LucideIcons.briefcase),
Card( Card(
color: appColors.surfaceElevated, color: isDark ? const Color(0xFF1E1E1E) : null,
child: ListTile( child: ListTile(
leading: Icon(LucideIcons.percent, color: appColors.iconAccent), leading: Icon(LucideIcons.percent, color: isDark ? Colors.orange[300] : Theme.of(context).primaryColor),
title: const Text('基本掛率'), title: const Text('基本掛率'),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -46,15 +45,15 @@ class _ShopSettingsScreenState extends ConsumerState<ShopSettingsScreen> {
Text('×', style: TextStyle( Text('×', style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,
color: appColors.textSecondary, color: isDark ? Colors.grey[400] : Colors.grey[600],
)), )),
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: appColors.surfaceSubtle, color: isDark ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: appColors.divider), border: Border.all(color: isDark ? Colors.grey[700]! : Colors.grey[300]!),
), ),
child: DropdownButton<double>( child: DropdownButton<double>(
value: userProfile.defaultMarkup, value: userProfile.defaultMarkup,
@ -97,18 +96,18 @@ class _ShopSettingsScreenState extends ConsumerState<ShopSettingsScreen> {
} }
Widget _buildSectionHeader(BuildContext context, String title, IconData icon) { Widget _buildSectionHeader(BuildContext context, String title, IconData icon) {
final appColors = Theme.of(context).extension<AppColors>()!; final isDark = Theme.of(context).brightness == Brightness.dark;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row( child: Row(
children: [ children: [
Icon(icon, size: 20, color: appColors.iconAccent), Icon(icon, size: 20, color: isDark ? Colors.orange[300] : Theme.of(context).primaryColor),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
title, title,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: appColors.textPrimary, color: isDark ? Colors.grey[300] : Theme.of(context).primaryColor,
), ),
), ),
], ],

View File

@ -11,15 +11,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../models/sake_item.dart'; import '../models/sake_item.dart';
///
/// UI /使
class PreRestoreBackupException implements Exception {
const PreRestoreBackupException();
@override
String toString() => 'PreRestoreBackupException: pre-restore safety backup failed';
}
/// Google Driveへのバックアップ /// Google Driveへのバックアップ
/// ///
/// ///
@ -334,11 +325,8 @@ class BackupService {
final driveApi = drive.DriveApi(authClient); final driveApi = drive.DriveApi(authClient);
// 3. 退 // 3. 退
final preBackupOk = await _createPreRestoreBackup(); await _createPreRestoreBackup();
if (!preBackupOk) {
throw const PreRestoreBackupException();
}
// 4. Google Driveからダウンロード // 4. Google Driveからダウンロード
final zipFile = await _downloadFromDrive(driveApi); final zipFile = await _downloadFromDrive(driveApi);
@ -354,55 +342,26 @@ class BackupService {
await zipFile.delete(); await zipFile.delete();
return success; return success;
} on PreRestoreBackupException {
rethrow;
} catch (error) { } catch (error) {
debugPrint('[RESTORE] Restore error: $error'); debugPrint('[RESTORE] Restore error: $error');
return false; return false;
} }
} }
/// 退 true false /// 退
Future<bool> _createPreRestoreBackup() async { Future<void> _createPreRestoreBackup() async {
try { try {
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final backupPath = path.join(tempDir.path, 'pre_restore_backup.zip'); final backupPath = path.join(tempDir.path, 'pre_restore_backup.zip');
final zipFile = await _createBackupZip(); final zipFile = await _createBackupZip();
if (zipFile == null) { if (zipFile != null) {
debugPrint('[RESTORE] Pre-restore backup: ZIP creation failed');
return false;
}
await zipFile.copy(backupPath); await zipFile.copy(backupPath);
await zipFile.delete(); await zipFile.delete();
debugPrint('[RESTORE] Pre-restore backup saved: $backupPath'); debugPrint('[RESTORE] Pre-restore backup saved: $backupPath');
return true; }
} catch (error) { } catch (error) {
debugPrint('[RESTORE] Pre-restore backup error: $error'); debugPrint('[RESTORE] Pre-restore backup error: $error');
return false;
}
}
///
Future<bool> restoreBackupSkippingPreBackup() async {
try {
final account = _googleSignIn.currentUser;
if (account == null) return false;
final authClient = await _googleSignIn.authenticatedClient();
if (authClient == null) return false;
final driveApi = drive.DriveApi(authClient);
final zipFile = await _downloadFromDrive(driveApi);
if (zipFile == null) return false;
final success = await _restoreFromZip(zipFile);
await zipFile.delete();
return success;
} catch (error) {
debugPrint('[RESTORE] Force restore error: $error');
return false;
} }
} }
@ -434,14 +393,7 @@ class BackupService {
// 3. // 3.
final sink = downloadFile.openWrite(); final sink = downloadFile.openWrite();
await media.stream.pipe(sink).timeout( await media.stream.pipe(sink);
const Duration(minutes: 3),
onTimeout: () {
sink.close();
try { downloadFile.deleteSync(); } catch (_) {}
throw TimeoutException('Backup download timed out after 3 minutes');
},
);
debugPrint('[RESTORE] Download complete: $downloadPath'); debugPrint('[RESTORE] Download complete: $downloadPath');
return downloadFile; return downloadFile;
@ -532,7 +484,7 @@ class BackupService {
isUserEdited: data['userData']['isUserEdited'] as bool, isUserEdited: data['userData']['isUserEdited'] as bool,
price: data['userData']['price'] as int?, price: data['userData']['price'] as int?,
costPrice: data['userData']['costPrice'] as int?, costPrice: data['userData']['costPrice'] as int?,
markup: (data['userData']['markup'] as num?)?.toDouble() ?? 3.0, markup: (data['userData']['markup'] as num).toDouble(),
priceVariants: data['userData']['priceVariants'] != null priceVariants: data['userData']['priceVariants'] != null
? Map<String, int>.from(data['userData']['priceVariants'] as Map) ? Map<String, int>.from(data['userData']['priceVariants'] as Map)
: null, : null,

View File

@ -272,7 +272,6 @@ class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
_isEditing, _isEditing,
suffixIcon: LucideIcons.calendar, suffixIcon: LucideIcons.calendar,
onSuffixTap: () => _showDatePicker(context), onSuffixTap: () => _showDatePicker(context),
helperText: '例: 2023-10',
), ),
], ],
), ),
@ -304,11 +303,10 @@ class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
void _showDatePicker(BuildContext context) { void _showDatePicker(BuildContext context) {
if (!_isEditing) return; if (!_isEditing) return;
// Parse current value or use now (AI出力 "2023.10" "2023-10" ) // Parse current value or use now
DateTime initialDate = DateTime.now(); DateTime initialDate = DateTime.now();
try { try {
final normalized = _manufacturingController.text.replaceAll('.', '-'); final parts = _manufacturingController.text.split('-');
final parts = normalized.split('-');
if (parts.length >= 2) { if (parts.length >= 2) {
final year = int.parse(parts[0]); final year = int.parse(parts[0]);
final month = int.parse(parts[1]); final month = int.parse(parts[1]);

View File

@ -1,12 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart'; import 'package:lucide_icons/lucide_icons.dart';
import '../../providers/sake_list_provider.dart';
import '../../providers/theme_provider.dart';
import '../../services/backup_service.dart'; import '../../services/backup_service.dart';
import '../../theme/app_colors.dart'; import '../../theme/app_colors.dart';
class BackupSettingsSection extends ConsumerStatefulWidget { class BackupSettingsSection extends StatefulWidget {
final String title; final String title;
const BackupSettingsSection({ const BackupSettingsSection({
@ -15,12 +12,12 @@ class BackupSettingsSection extends ConsumerStatefulWidget {
}); });
@override @override
ConsumerState<BackupSettingsSection> createState() => _BackupSettingsSectionState(); State<BackupSettingsSection> createState() => _BackupSettingsSectionState();
} }
enum _BackupState { idle, signingIn, signingOut, backingUp, restoring } enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
class _BackupSettingsSectionState extends ConsumerState<BackupSettingsSection> { class _BackupSettingsSectionState extends State<BackupSettingsSection> {
final BackupService _backupService = BackupService(); final BackupService _backupService = BackupService();
_BackupState _state = _BackupState.idle; _BackupState _state = _BackupState.idle;
@ -31,11 +28,7 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
} }
Future<void> _initBackupService() async { Future<void> _initBackupService() async {
try {
await _backupService.init(); await _backupService.init();
} catch (e) {
debugPrint('[Backup] Init error (silent sign-in failed): $e');
}
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
} }
@ -139,7 +132,7 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
Future<void> _restoreBackup() async { Future<void> _restoreBackup() async {
final messenger = ScaffoldMessenger.of(context); final messenger = ScaffoldMessenger.of(context);
final appColors = Theme.of(context).extension<AppColors>()!; final appColors = Theme.of(context).extension<AppColors>()!;
// Note: hasBackup check is async
final hasBackup = await _backupService.hasBackupOnDrive(); final hasBackup = await _backupService.hasBackupOnDrive();
if (!hasBackup) { if (!hasBackup) {
if (mounted) { if (mounted) {
@ -180,29 +173,11 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
), ),
); );
if (confirmed != true || !mounted) return; if (confirmed == true && mounted) {
await _executeRestore(forceSkipPreBackup: false);
}
/// PreRestoreBackupException
Future<void> _executeRestore({required bool forceSkipPreBackup}) async {
final messenger = ScaffoldMessenger.of(context);
final appColors = Theme.of(context).extension<AppColors>()!;
setState(() => _state = _BackupState.restoring); setState(() => _state = _BackupState.restoring);
final success = await _backupService.restoreBackup();
try {
final success = forceSkipPreBackup
? await _backupService.restoreBackupSkippingPreBackup()
: await _backupService.restoreBackup();
if (mounted) { if (mounted) {
setState(() => _state = _BackupState.idle); setState(() => _state = _BackupState.idle);
if (success) {
ref.invalidate(rawSakeListItemsProvider);
ref.invalidate(sakeSortOrderProvider);
ref.invalidate(userProfileProvider);
}
messenger.showSnackBar( messenger.showSnackBar(
SnackBar( SnackBar(
content: Text(success ? '復元が完了しました' : '復元に失敗しました'), content: Text(success ? '復元が完了しました' : '復元に失敗しました'),
@ -210,47 +185,6 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring }
), ),
); );
} }
} on PreRestoreBackupException {
if (!mounted) return;
setState(() => _state = _BackupState.idle);
//
final continueAnyway = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(LucideIcons.alertTriangle, color: appColors.error, size: 24),
const SizedBox(width: 8),
const Text('安全バックアップに失敗'),
],
),
content: const Text(
'復元前の安全コピー作成に失敗しました。\n'
'このまま続行すると、現在のデータが失われた場合に\n'
'元に戻せない可能性があります。\n\n'
'続行しますか?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('中断'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: appColors.error,
foregroundColor: appColors.surfaceSubtle,
),
child: const Text('それでも続行'),
),
],
),
);
if (continueAnyway == true && mounted) {
await _executeRestore(forceSkipPreBackup: true);
}
} }
} }

View File

@ -1,19 +1,19 @@
{ {
"version": "v1.0.49", "version": "v1.0.47",
"name": "Ponshu Room 1.0.49 (2026-04-23)", "name": "Ponshu Room 1.0.47 (2026-04-23)",
"date": "2026-04-23", "date": "2026-04-23",
"apks": { "apks": {
"maita": { "maita": {
"lite": { "lite": {
"filename": "ponshu_room_consumer_maita.apk", "filename": "ponshu_room_consumer_maita.apk",
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.49/ponshu_room_consumer_maita.apk", "url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.47/ponshu_room_consumer_maita.apk",
"size_mb": 91 "size_mb": 91
} }
}, },
"eiji": { "eiji": {
"lite": { "lite": {
"filename": "ponshu_room_consumer_eiji.apk", "filename": "ponshu_room_consumer_eiji.apk",
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.49/ponshu_room_consumer_eiji.apk", "url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.47/ponshu_room_consumer_eiji.apk",
"size_mb": 91 "size_mb": 91
} }
} }