refactor: TextEditingControllerリーク解消・エラー正規化・デザイントークン整備
- sake_detail_screen: _showTagEditDialog/TextEditDialog/BreweryEditDialog に try/finally + controller.dispose() を追加(メモリリーク修正) - sake_detail_screen: State フィールドを build() より前に移動 - 生例外の SnackBar 露出を人間可読メッセージに正規化(6ファイル・10箇所) - camera_analysis_mixin: Colors.orange を appColors.warning に置換、 ガミフィケーション色を brandAccent/success/textTertiary に統一 - sake_detail_screen: ハードコード hex 色グラデーションをトークン化 - scan_screen / pdf_preview_screen / add_set_item_dialog: 絵文字 debugPrint を除去 - sake_basic_info_section: unnecessary_non_null_assertion (warning) を解消 - license_service: revoked 永続キャッシュの意図をコメントで明確化 - dart analyze: warning 0 / error 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5d8689b7ee
commit
dd9b814174
|
|
@ -32,6 +32,7 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
// async gap 前に context 依存オブジェクトをキャプチャ
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final navigator = Navigator.of(context);
|
||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||
|
||||
final isOnline = await NetworkService.isOnline();
|
||||
if (!isOnline) {
|
||||
|
|
@ -44,25 +45,25 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
if (!mounted) return;
|
||||
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
SnackBar(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.wifiOff, color: Colors.orange, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('オフライン検知', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Icon(LucideIcons.wifiOff, color: Colors.white, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
const Text('オフライン検知', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text('写真を「解析待ち」として保存しました。'),
|
||||
Text('オンライン復帰後、ホーム画面から解析できます。'),
|
||||
const SizedBox(height: 4),
|
||||
const Text('写真を「解析待ち」として保存しました。'),
|
||||
const Text('オンライン復帰後、ホーム画面から解析できます。'),
|
||||
],
|
||||
),
|
||||
duration: Duration(seconds: 5),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 5),
|
||||
backgroundColor: appColors.warning,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -72,7 +73,7 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
debugPrint('Draft save error: $e');
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('Draft保存エラー: $e')),
|
||||
const SnackBar(content: Text('写真の一時保存に失敗しました。再度お試しください。')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -93,11 +94,11 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
Row(
|
||||
children: [
|
||||
Icon(LucideIcons.zap, color: Colors.orange, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('本日のAI解析上限(20回)に達しました',
|
||||
const Icon(LucideIcons.zap, color: Colors.white, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
const Text('本日のAI解析上限(20回)に達しました',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
|
|
@ -107,7 +108,7 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
],
|
||||
),
|
||||
duration: const Duration(seconds: 6),
|
||||
backgroundColor: Colors.orange,
|
||||
backgroundColor: appColors.warning,
|
||||
),
|
||||
);
|
||||
navigator.pop(); // カメラ画面を閉じてホームへ
|
||||
|
|
@ -115,7 +116,7 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
debugPrint('Draft save error (quota): $e');
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('保存エラー: $e')),
|
||||
const SnackBar(content: Text('写真の一時保存に失敗しました。再度お試しください。')),
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
@ -224,30 +225,27 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
navigator.pop(); // Close Camera Screen (Return to Home)
|
||||
|
||||
// Success Message
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final List<Widget> messageWidgets = [
|
||||
Text('${sakeItem.displayData.displayName} を登録しました!'),
|
||||
];
|
||||
|
||||
if (result.isFromCache) {
|
||||
messageWidgets.add(const SizedBox(height: 4));
|
||||
messageWidgets.add(const Text(
|
||||
messageWidgets.add(Text(
|
||||
'※ 解析済みの結果を使用(経験値なし)',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
style: TextStyle(fontSize: 12, color: appColors.textTertiary),
|
||||
));
|
||||
} else {
|
||||
messageWidgets.add(const SizedBox(height: 4));
|
||||
messageWidgets.add(Row(
|
||||
children: [
|
||||
Icon(LucideIcons.sparkles,
|
||||
color: isDark ? Colors.yellow.shade300 : Colors.yellow,
|
||||
size: 16),
|
||||
Icon(LucideIcons.sparkles, color: appColors.brandAccent, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'経験値 +$expGained GET!${isLevelUp ? " (Level UP!)" : ""}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.yellow.shade200 : Colors.yellowAccent,
|
||||
color: appColors.brandAccent,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -266,7 +264,7 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
'バッジ獲得: ${badge.name}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.green.shade300 : Colors.greenAccent,
|
||||
color: appColors.success,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -297,25 +295,25 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
if (!mounted) return;
|
||||
navigator.pop(); // Close camera screen
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
SnackBar(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
const Row(
|
||||
children: [
|
||||
Icon(LucideIcons.cloudOff, color: Colors.white, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('AIサーバー混雑', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text('写真を「解析待ち」として保存しました。'),
|
||||
Text('時間をおいてホーム画面から解析できます。'),
|
||||
const SizedBox(height: 4),
|
||||
const Text('写真を「解析待ち」として保存しました。'),
|
||||
const Text('時間をおいてホーム画面から解析できます。'),
|
||||
],
|
||||
),
|
||||
duration: Duration(seconds: 5),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 5),
|
||||
backgroundColor: appColors.warning,
|
||||
),
|
||||
);
|
||||
} catch (draftError) {
|
||||
|
|
@ -345,7 +343,7 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(LucideIcons.zap, color: Colors.orange, size: 16),
|
||||
Icon(LucideIcons.zap, color: Colors.white, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('本日のAI解析上限(20回)に達しました',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
|
|
@ -357,7 +355,7 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
],
|
||||
),
|
||||
duration: const Duration(seconds: 6),
|
||||
backgroundColor: Colors.orange,
|
||||
backgroundColor: appColors.warning,
|
||||
),
|
||||
);
|
||||
} catch (draftError) {
|
||||
|
|
@ -370,10 +368,10 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
|||
return;
|
||||
}
|
||||
|
||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||
debugPrint('Analysis error: $e');
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('解析エラー: $e'),
|
||||
content: const Text('解析に失敗しました。時間をおいて再試行してください。'),
|
||||
duration: const Duration(seconds: 5),
|
||||
backgroundColor: appColors.error,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ class _SommelierScreenState extends ConsumerState<SommelierScreen> {
|
|||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
debugPrint('Share error: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('シェアに失敗しました: $e')),
|
||||
const SnackBar(content: Text('シェアに失敗しました。再度お試しください。')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -541,7 +542,7 @@ class _SommelierScreenState extends ConsumerState<SommelierScreen> {
|
|||
debugPrint('Diagnosis Error: $e');
|
||||
if (!mounted) return;
|
||||
navigator.pop();
|
||||
messenger.showSnackBar(SnackBar(content: Text('エラー: $e')));
|
||||
messenger.showSnackBar(const SnackBar(content: Text('診断に失敗しました。時間をおいて再試行してください。')));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class PdfPreviewScreen extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
loadingWidget: const Center(child: CircularProgressIndicator()),
|
||||
onError: (context, error) => Center(child: Text('エラーが発生しました: $error')),
|
||||
onError: (context, error) => const Center(child: Text('PDFの表示に失敗しました')),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Column(
|
||||
|
|
@ -246,9 +246,10 @@ class PdfPreviewScreen extends ConsumerWidget {
|
|||
filename: 'oshinagaki_${DateTime.now().toString().split(' ')[0]}.pdf',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('PDF share error: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('共有エラー: $e')),
|
||||
const SnackBar(content: Text('PDFの共有に失敗しました。再度お試しください。')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -284,7 +285,7 @@ class PdfPreviewScreen extends ConsumerWidget {
|
|||
..name = fileName
|
||||
..mimeType = 'application/pdf';
|
||||
|
||||
debugPrint('[PDF_DRIVE] 📤 アップロード開始: $fileName (${bytes.length} bytes)');
|
||||
debugPrint('[PDF_DRIVE] Upload start: $fileName (${bytes.length} bytes)');
|
||||
final uploadedFile = await driveApi.files.create(
|
||||
driveFile,
|
||||
uploadMedia: drive.Media(
|
||||
|
|
@ -294,11 +295,11 @@ class PdfPreviewScreen extends ConsumerWidget {
|
|||
);
|
||||
|
||||
if (uploadedFile.id == null) {
|
||||
debugPrint('[PDF_DRIVE] ❌ アップロード失敗: ID取得不可');
|
||||
debugPrint('[PDF_DRIVE] Upload failed: no file ID returned');
|
||||
throw Exception('アップロードに失敗しました(IDなし)');
|
||||
}
|
||||
|
||||
debugPrint('[PDF_DRIVE] ✅ アップロード完了: ID=${uploadedFile.id}');
|
||||
debugPrint('[PDF_DRIVE] Upload complete: ID=${uploadedFile.id}');
|
||||
|
||||
// 5. Success notification
|
||||
if (context.mounted) {
|
||||
|
|
@ -314,11 +315,12 @@ class PdfPreviewScreen extends ConsumerWidget {
|
|||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Drive upload error: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Driveアップロードエラー: $e'),
|
||||
duration: const Duration(seconds: 4),
|
||||
const SnackBar(
|
||||
content: Text('Google Driveへの保存に失敗しました。再度お試しください。'),
|
||||
duration: Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -336,9 +338,10 @@ class PdfPreviewScreen extends ConsumerWidget {
|
|||
format: _getPageFormat(pdfSize, ref.read(pdfIsPortraitProvider)),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Print error: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('印刷エラー: $e')),
|
||||
const SnackBar(content: Text('印刷に失敗しました。再度お試しください。')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,10 +194,11 @@ class _PendingAnalysisScreenState extends ConsumerState<PendingAnalysisScreen> {
|
|||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
debugPrint('Draft delete error: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('削除エラー: $e'),
|
||||
duration: const Duration(seconds: 5),
|
||||
const SnackBar(
|
||||
content: Text('削除に失敗しました。再度お試しください。'),
|
||||
duration: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class SakeBasicInfoSection extends ConsumerWidget {
|
|||
decoration: BoxDecoration(
|
||||
color: badgeColor!.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: badgeColor!.withValues(alpha: 0.4)),
|
||||
border: Border.all(color: badgeColor.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -109,7 +109,7 @@ class SakeBasicInfoSection extends ConsumerWidget {
|
|||
Icon(LucideIcons.brainCircuit, size: 12, color: badgeColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$mbtiType ${mbtiResult!.starDisplay}',
|
||||
'$mbtiType ${mbtiResult.starDisplay}',
|
||||
style: TextStyle(
|
||||
color: badgeColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
|
|||
|
|
@ -234,9 +234,10 @@ class _SakePhotoEditModalState extends State<SakePhotoEditModal> {
|
|||
await _saveNewPhoto(savedPath);
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('Photo pick error: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('エラー: $e')),
|
||||
const SnackBar(content: Text('写真の追加に失敗しました。再度お試しください。')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ class SakeDetailScreen extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||
// To trigger rebuilds if we don't switch to a stream
|
||||
late SakeItem _sake;
|
||||
int _currentImageIndex = 0;
|
||||
// Memo logic moved to SakeDetailMemo
|
||||
bool _isAnalyzing = false;
|
||||
DateTime? _quotaLockoutTime;
|
||||
|
||||
|
||||
@override
|
||||
|
|
@ -119,14 +119,11 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: Theme.of(context).brightness == Brightness.dark
|
||||
? [
|
||||
const Color(0xFF121212), // Scaffold Background
|
||||
const Color(0xFF1E1E1E), // Slightly lighter surface
|
||||
]
|
||||
: [
|
||||
colors: [
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
Theme.of(context).primaryColor.withValues(alpha: 0.05),
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? appColors.brandSurface
|
||||
: Theme.of(context).primaryColor.withValues(alpha: 0.05),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -281,10 +278,6 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
bool _isAnalyzing = false;
|
||||
DateTime? _quotaLockoutTime;
|
||||
|
||||
|
||||
Future<void> _toggleFavorite() async {
|
||||
HapticFeedback.mediumImpact();
|
||||
final box = Hive.box<SakeItem>('sake_items');
|
||||
|
|
@ -401,8 +394,9 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
debugPrint('Reanalyze error: $e');
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('エラー: $e')),
|
||||
const SnackBar(content: Text('再解析に失敗しました。時間をおいて再試行してください。')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -413,11 +407,11 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
}
|
||||
|
||||
|
||||
void _showTagEditDialog(BuildContext context) {
|
||||
Future<void> _showTagEditDialog(BuildContext context) async {
|
||||
final TextEditingController tagController = TextEditingController();
|
||||
final allTags = _sake.hiddenSpecs.flavorTags.toSet();
|
||||
|
||||
showDialog(
|
||||
try {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
|
|
@ -498,6 +492,9 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
}
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
tagController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateTags(List<String> newTags) async {
|
||||
|
|
@ -581,6 +578,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
required Future<void> Function(String) onSave,
|
||||
}) async {
|
||||
final controller = TextEditingController(text: initialValue);
|
||||
try {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
|
|
@ -608,6 +606,9 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
],
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
controller.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// MBTI相性詳細ダイアログを表示
|
||||
|
|
@ -732,6 +733,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
Future<void> _showBreweryEditDialog(BuildContext context) async {
|
||||
final breweryController = TextEditingController(text: _sake.displayData.displayBrewery);
|
||||
final prefectureController = TextEditingController(text: _sake.displayData.displayPrefecture);
|
||||
try {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
|
|
@ -778,6 +780,10 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
],
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
breweryController.dispose();
|
||||
prefectureController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// 写真編集モーダルを表示
|
||||
|
|
|
|||
|
|
@ -77,10 +77,10 @@ class _ScanARScreenState extends ConsumerState<ScanARScreen>
|
|||
_isInitializing = false;
|
||||
});
|
||||
}
|
||||
debugPrint('✅ Scanner: Controller created successfully');
|
||||
debugPrint('[Scanner] Controller created successfully');
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Scanner: Error during initialization: $e');
|
||||
debugPrint('[Scanner] Error during initialization: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInitializing = false;
|
||||
|
|
|
|||
|
|
@ -148,17 +148,22 @@ class LicenseService {
|
|||
|
||||
if (cached == null) return LicenseStatus.free;
|
||||
|
||||
// キャッシュが古すぎる場合はfreeにフォールバック
|
||||
// pro と revoked は期限切れにしない(proは購入者を締め出さない、revokedは誤って復活させない)
|
||||
// オンライン時は _validateKeyWithServer が常に上書きするため、
|
||||
// _getCachedStatus はオフライン時専用のフォールバックとして動作する。
|
||||
//
|
||||
// TTL 判定(_cacheValidSeconds = 24h):
|
||||
// - free / offline は期限切れで free にフォールバック
|
||||
// - pro : 購入者をオフライン時に締め出さないため永続扱い
|
||||
// - revoked: 不正防止を優先するため永続扱い
|
||||
// (将来 TTL を設けたい場合は isNoExpiryStatus を条件分岐ごと差し替える)
|
||||
if (cachedAt != null) {
|
||||
final age = DateTime.now().difference(DateTime.parse(cachedAt));
|
||||
final isPermanentStatus = cached == LicenseStatus.pro.name || cached == LicenseStatus.revoked.name;
|
||||
if (age.inSeconds > _cacheValidSeconds && !isPermanentStatus) {
|
||||
final isNoExpiryStatus = cached == LicenseStatus.pro.name || cached == LicenseStatus.revoked.name;
|
||||
if (age.inSeconds > _cacheValidSeconds && !isNoExpiryStatus) {
|
||||
return LicenseStatus.free;
|
||||
}
|
||||
}
|
||||
|
||||
// Pro キャッシュはオフラインでも維持(購入者を締め出さない)
|
||||
return LicenseStatus.values.firstWhere(
|
||||
(s) => s.name == cached,
|
||||
orElse: () => LicenseStatus.free,
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class _AddSetItemDialogState extends ConsumerState<AddSetItemDialog> {
|
|||
final newlyUnlockedBadges = await GamificationService.checkAndUnlockBadges(ref);
|
||||
|
||||
if (newlyUnlockedBadges.isNotEmpty) {
|
||||
debugPrint('🏅 Badges Unlocked: ${newlyUnlockedBadges.map((b) => b.name).join(", ")}');
|
||||
debugPrint('[Gamification] Badges Unlocked: ${newlyUnlockedBadges.map((b) => b.name).join(", ")}');
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue