refactor: code quality improvements based on critical review

Made-with: Cursor
This commit is contained in:
Ponshu Developer 2026-04-12 00:09:09 +09:00
parent 2074f85da8
commit 94f7ee20ea
12 changed files with 129 additions and 190 deletions

1
.gitignore vendored
View File

@ -78,6 +78,7 @@ Desktop.ini
*.tmp
*.bak
analyze_output*.txt
build_log_*.txt
# Deprecated build scripts (API keys hardcoded - use build_consumer_apks.ps1 instead)
build_all_apks.ps1

View File

@ -10,10 +10,6 @@ import 'screens/main_screen.dart';
import 'screens/license_screen.dart';
import 'services/migration_service.dart';
/// Pro解放フラグ使 isProProviderで管理
/// isProProvider (license_provider.dart) 使
const bool isProVersion = bool.fromEnvironment('IS_PRO_VERSION', defaultValue: false);
///
///
/// :
@ -57,12 +53,9 @@ void main() async {
: box.get('migration_version', defaultValue: 0) as int; // : =v0
if (storedVersion < currentMigrationVersion) {
debugPrint('🚀 Running MigrationService (v$storedVersion → v$currentMigrationVersion)...');
await MigrationService.runMigration();
await box.put('migration_version', currentMigrationVersion);
await box.put('migration_completed', true); //
} else {
debugPrint('✅ Migration up to date (v$storedVersion). Skipping.');
}
// AI解析キャッシュは使うときに初期化するLazy initialization

View File

@ -0,0 +1,17 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/gemini_service.dart';
/// GeminiService
///
///
/// _lastApiCallTime
///
///
/// 使:
/// ```dart
/// final geminiService = ref.read(geminiServiceProvider);
/// final result = await geminiService.analyzeSakeLabel(paths);
/// ```
final geminiServiceProvider = Provider<GeminiService>((ref) {
return GeminiService();
});

View File

@ -10,6 +10,7 @@ import 'package:uuid/uuid.dart';
import 'package:gal/gal.dart';
import '../services/gemini_service.dart';
import '../providers/gemini_provider.dart';
import '../services/gemini_exceptions.dart';
import '../services/image_compression_service.dart';
import '../services/gamification_service.dart'; // Badge check
@ -372,19 +373,21 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
Future<void> _analyzeImages() async {
if (_capturedImages.isEmpty) return;
// async gap context
final messenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
final isOnline = await NetworkService.isOnline();
if (!isOnline) {
// : Draft
debugPrint('📴 Offline detected: Saving as draft...');
debugPrint('Offline detected: Saving as draft...');
try {
// 🔧 FIX:
await DraftService.saveDraft(_capturedImages);
if (!mounted) return;
//
ScaffoldMessenger.of(context).showSnackBar(
messenger.showSnackBar(
const SnackBar(
content: Column(
mainAxisSize: MainAxisSize.min,
@ -407,13 +410,12 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
),
);
//
Navigator.of(context).pop();
navigator.pop();
return;
} catch (e) {
debugPrint('⚠️ Draft save error: $e');
debugPrint('Draft save error: $e');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
messenger.showSnackBar(
SnackBar(content: Text('Draft保存エラー: $e')),
);
return;
@ -421,7 +423,6 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
}
// :
// Show AnalyzingDialog
if (!mounted) return;
showDialog(
context: context,
@ -432,7 +433,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
try {
// Direct Gemini Vision Analysis (OCR removed for app size reduction)
debugPrint('📸 Starting Gemini Vision Direct Analysis for ${_capturedImages.length} images');
final geminiService = GeminiService();
final geminiService = ref.read(geminiServiceProvider);
final result = await geminiService.analyzeSakeLabel(_capturedImages);
// Create SakeItem (Schema v2.0)
@ -504,11 +505,8 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
if (!mounted) return;
// Close Dialog
Navigator.of(context).pop();
// Close Camera Screen (Return to Home)
Navigator.of(context).pop();
navigator.pop(); // Close AnalyzingDialog
navigator.pop(); // Close Camera Screen (Return to Home)
// Success Message (with EXP/Level Up/Badge info)
final isDark = Theme.of(context).brightness == Brightness.dark;
@ -554,28 +552,28 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
}
}
ScaffoldMessenger.of(context).showSnackBar(
messenger.showSnackBar(
SnackBar(
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: messageWidgets,
),
duration: Duration(seconds: newBadges.isNotEmpty ? 6 : 4), // Longer for badges
duration: Duration(seconds: newBadges.isNotEmpty ? 6 : 4),
),
);
} catch (e) {
if (mounted) {
Navigator.of(context).pop(); // Close AnalyzingDialog
navigator.pop(); // Close AnalyzingDialog
// AIサーバー混雑503
if (e is GeminiCongestionException) {
try {
await DraftService.saveDraft(_capturedImages);
if (!mounted) return;
Navigator.of(context).pop(); // Close camera screen
ScaffoldMessenger.of(context).showSnackBar(
navigator.pop(); // Close camera screen
messenger.showSnackBar(
const SnackBar(
content: Column(
mainAxisSize: MainAxisSize.min,
@ -598,9 +596,8 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
),
);
} catch (_) {
//
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
messenger.showSnackBar(
const SnackBar(content: Text('解析もドラフト保存も失敗しました。再試行してください。')),
);
}
@ -616,7 +613,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
}
final appColors = Theme.of(context).extension<AppColors>()!;
ScaffoldMessenger.of(context).showSnackBar(
messenger.showSnackBar(
SnackBar(
content: Text('解析エラー: $e'),
duration: const Duration(seconds: 5),

View File

@ -7,6 +7,7 @@ import 'package:hive_flutter/hive_flutter.dart';
import '../models/sake_item.dart';
import '../providers/sake_list_provider.dart';
import '../services/gemini_service.dart';
import '../providers/gemini_provider.dart';
class DevMenuScreen extends ConsumerWidget {
const DevMenuScreen({super.key});
@ -198,7 +199,7 @@ class DevMenuScreen extends ConsumerWidget {
final box = Hive.box<SakeItem>('sake_items');
try {
final gemini = GeminiService();
final gemini = ref.read(geminiServiceProvider);
for (final item in targets) {
if (item.displayData.imagePaths.isEmpty) continue;

View File

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../models/sake_item.dart';
import '../services/gemini_service.dart';
import '../providers/gemini_provider.dart';
import '../services/sake_recommendation_service.dart';
import '../widgets/analyzing_dialog.dart';
import '../widgets/sake_3d_carousel_with_reason.dart';
@ -331,11 +332,8 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
existingPaths.add(path);
}
}
// mounted context async gap
if (!mounted) return;
// ignore: use_build_context_synchronously
final nav = Navigator.of(context);
// ignore: use_build_context_synchronously
final messenger = ScaffoldMessenger.of(context);
if (existingPaths.isEmpty) {
@ -348,15 +346,13 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
setState(() => _isAnalyzing = true);
try {
// ignore: use_build_context_synchronously
showDialog(
context: context, // ignore: use_build_context_synchronously
context: context,
barrierDismissible: false,
builder: (context) => const AnalyzingDialog(),
);
final geminiService = GeminiService();
// forceRefresh: true
final geminiService = ref.read(geminiServiceProvider);
final result = await geminiService.analyzeSakeLabel(existingPaths, forceRefresh: true);
final newItem = _sake.copyWith(

View File

@ -1,121 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/sake_item.dart';
import '../models/user_profile.dart';
import '../models/menu_settings.dart';
import '../services/migration_service.dart';
import 'main_screen.dart';
class SplashScreen extends ConsumerStatefulWidget {
const SplashScreen({super.key});
@override
ConsumerState<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends ConsumerState<SplashScreen> {
@override
void initState() {
super.initState();
_initializeData();
}
Future<void> _initializeData() async {
// Artificial delay for branding (optional, keep it minimal)
// await Future.delayed(const Duration(milliseconds: 500));
try {
// Initialize Hive
await Hive.initFlutter();
// Register Adapters
Hive.registerAdapter(SakeItemAdapter());
Hive.registerAdapter(UserProfileAdapter());
Hive.registerAdapter(MenuSettingsAdapter());
// Phase 0 New Adapters
Hive.registerAdapter(DisplayDataAdapter());
Hive.registerAdapter(HiddenSpecsAdapter());
Hive.registerAdapter(UserDataAdapter());
Hive.registerAdapter(GamificationAdapter());
Hive.registerAdapter(MetadataAdapter());
Hive.registerAdapter(ItemTypeAdapter());
// Open all boxes (Parallel Execution)
final results = await Future.wait([
Hive.openBox('settings'),
Hive.openBox<UserProfile>('user_profile'),
Hive.openBox<SakeItem>('sake_items'),
Hive.openBox<MenuSettings>('menu_settings'),
]);
final settingsBox = results[0];
// Run Phase 0 Migration (Only once)
final migrationCompleted = settingsBox.get('migration_completed', defaultValue: false);
if (!migrationCompleted) {
debugPrint('🚀 Running MigrationService...');
await MigrationService.runMigration();
await settingsBox.put('migration_completed', true);
} else {
debugPrint('✅ Migration already completed. Skipping.');
}
if (!mounted) return;
// Navigate to MainScreen
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const MainScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 500),
),
);
} catch (e) {
debugPrint('❌ Initialization Error: $e');
// Show error UI or retry logic
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white, // Match brand color
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// App Logo
const Text(
'🍶',
style: TextStyle(fontSize: 80),
),
const SizedBox(height: 24),
Text(
'Ponshu Room',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
letterSpacing: 1.2,
),
),
const SizedBox(height: 48),
// Subtle loading indicator
SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.grey[400],
),
),
],
),
),
);
}
}

View File

@ -68,7 +68,8 @@ class AnalysisCacheService {
///
///
/// : null
/// : null
/// JSON
static Future<SakeAnalysisResult?> getCached(String imageHash) async {
await init();
if (_box == null) return null;
@ -76,33 +77,43 @@ class AnalysisCacheService {
try {
final jsonString = _box!.get(imageHash);
if (jsonString == null) {
debugPrint('🔍 Cache MISS: $imageHash');
debugPrint('Cache MISS: $imageHash');
return null;
}
final jsonMap = jsonDecode(jsonString) as Map<String, dynamic>;
debugPrint('✅ Cache HIT: ${jsonMap['name'] ?? 'Unknown'} ($imageHash)');
return SakeAnalysisResult.fromJson(jsonMap);
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
// : { data: {...}, savedAt: "..." }
if (decoded.containsKey('data') && decoded.containsKey('savedAt')) {
debugPrint('Cache HIT: ${decoded['data']['name'] ?? 'Unknown'}');
return SakeAnalysisResult.fromJson(decoded['data'] as Map<String, dynamic>);
}
// : SakeAnalysisResult JSON
debugPrint('Cache HIT (legacy): ${decoded['name'] ?? 'Unknown'}');
return SakeAnalysisResult.fromJson(decoded);
} catch (e) {
debugPrint('⚠️ Cache read error: $e');
debugPrint('Cache read error: $e');
return null;
}
}
///
///
/// JSON化してHiveに永続化
/// JSON Hive
static Future<void> saveCache(String imageHash, SakeAnalysisResult result) async {
await init();
if (_box == null) return;
try {
final jsonMap = result.toJson();
final jsonString = jsonEncode(jsonMap);
await _box!.put(imageHash, jsonString);
debugPrint('💾 Cache SAVED: ${result.name ?? 'Unknown'} ($imageHash)');
final entry = {
'data': result.toJson(),
'savedAt': DateTime.now().toIso8601String(),
};
await _box!.put(imageHash, jsonEncode(entry));
debugPrint('Cache saved: ${result.name ?? 'Unknown'}');
} catch (e) {
debugPrint('⚠️ Cache save error: $e');
debugPrint('Cache save error: $e');
}
}
@ -122,13 +133,37 @@ class AnalysisCacheService {
return _box?.length ?? 0;
}
///
/// 30
///
/// :
/// - 30
/// -
static Future<void> cleanupExpired() async {
// TODO: 30
///
///
static Future<void> cleanupExpired({int ttlDays = 30}) async {
await init();
if (_box == null) return;
final cutoff = DateTime.now().subtract(Duration(days: ttlDays));
final keysToDelete = <dynamic>[];
for (final key in _box!.keys) {
try {
final jsonString = _box!.get(key as String);
if (jsonString == null) continue;
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
if (!decoded.containsKey('savedAt')) continue; //
final savedAt = DateTime.tryParse(decoded['savedAt'] as String? ?? '');
if (savedAt != null && savedAt.isBefore(cutoff)) {
keysToDelete.add(key);
}
} catch (_) {
//
}
}
if (keysToDelete.isNotEmpty) {
await _box!.deleteAll(keysToDelete);
debugPrint('Cache cleanup: ${keysToDelete.length} expired entries removed');
}
}
// ============================================
@ -162,25 +197,29 @@ class AnalysisCacheService {
///
///
///
///
static Future<void> registerBrandIndex(String? brandName, String imageHash) async {
/// [forceUpdate] = true
/// false
static Future<void> registerBrandIndex(
String? brandName,
String imageHash, {
bool forceUpdate = false,
}) async {
if (brandName == null || brandName.isEmpty) return;
await init();
if (_brandIndexBox == null) return;
try {
final normalized = _normalizeBrandName(brandName);
final exists = _brandIndexBox!.containsKey(normalized);
//
if (!_brandIndexBox!.containsKey(normalized)) {
if (!exists || forceUpdate) {
await _brandIndexBox!.put(normalized, imageHash);
debugPrint('📝 Brand index registered: $brandName$imageHash');
debugPrint('Brand index ${exists ? "updated" : "registered"}: $brandName');
} else {
debugPrint(' Brand index already exists: $brandName (skipped)');
debugPrint('Brand index already exists: $brandName (skipped)');
}
} catch (e) {
debugPrint('⚠️ Brand index registration error: $e');
debugPrint('Brand index registration error: $e');
}
}

View File

@ -284,8 +284,12 @@ class GeminiService {
if (imagePaths.isNotEmpty) {
final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths);
await AnalysisCacheService.saveCache(imageHash, result);
// 4. v1.0.15:
await AnalysisCacheService.registerBrandIndex(result.name, imageHash);
// 4. forceRefresh
await AnalysisCacheService.registerBrandIndex(
result.name,
imageHash,
forceUpdate: forceRefresh,
);
}
if (attempt > 0) debugPrint('Succeeded on attempt $attempt (model: $modelName)');

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.30+37
version: 1.0.31+38
environment:
sdk: ^3.10.1
@ -97,8 +97,8 @@ flutter_launcher_icons:
web:
generate: true
image_path: "assets/images/app_icon.png"
background_color: "#hex_code"
theme_color: "#hex_code"
background_color: "#FDFAF5"
theme_color: "#4A3B32"
windows:
generate: true
image_path: "assets/images/app_icon.png"

View File

@ -6,15 +6,24 @@
#include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <gal/gal_plugin_c_api.h>
#include <printing/printing_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
GalPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GalPluginCApi"));
PrintingPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PrintingPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -3,9 +3,12 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
file_selector_windows
gal
printing
share_plus
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST