diff --git a/lib/screens/sake_detail/sections/sake_basic_info_section.dart b/lib/screens/sake_detail/sections/sake_basic_info_section.dart index e48fbef..a28ecf6 100644 --- a/lib/screens/sake_detail/sections/sake_basic_info_section.dart +++ b/lib/screens/sake_detail/sections/sake_basic_info_section.dart @@ -55,173 +55,168 @@ class SakeBasicInfoSection extends ConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Confidence Badge - if (sake.metadata.aiConfidence != null && sake.itemType != ItemType.set) + // 変更1: AI確信度 + MBTI相性 を横並び1行に統合 + if ((sake.metadata.aiConfidence != null && sake.itemType != ItemType.set) || showMbti) Center( - child: Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: confidenceColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: confidenceColor.withValues(alpha: 0.5)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, children: [ - Icon(LucideIcons.sparkles, size: 14, color: confidenceColor), - const SizedBox(width: 6), - Text( - 'AI確信度: $score%', - style: TextStyle( - color: confidenceColor, - fontWeight: FontWeight.bold, - fontSize: 12, + if (sake.metadata.aiConfidence != null && sake.itemType != ItemType.set) + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: confidenceColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: confidenceColor.withValues(alpha: 0.4)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.sparkles, size: 12, color: confidenceColor), + const SizedBox(width: 4), + Text( + 'AI $score%', + style: TextStyle( + color: confidenceColor, + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ), + ], + ), + ), + if (showMbti) + GestureDetector( + onTap: () => onTapMbtiCompatibility(context, mbtiResult!, appColors), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: badgeColor!.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: badgeColor.withValues(alpha: 0.4)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.brainCircuit, size: 12, color: badgeColor), + const SizedBox(width: 4), + Text( + '$mbtiType ${mbtiResult!.starDisplay}', + style: TextStyle( + color: badgeColor, + fontWeight: FontWeight.bold, + fontSize: 11, + letterSpacing: 0.5, + ), + ), + ], + ), + ), ), - ), ], ), ), ), - // MBTI Compatibility Badge - if (showMbti) - Center( - child: GestureDetector( - onTap: () => onTapMbtiCompatibility(context, mbtiResult, appColors), - child: Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: BoxDecoration( - color: badgeColor!.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: badgeColor.withValues(alpha: 0.3)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '$mbtiType相性: ', - style: TextStyle( - color: appColors.textSecondary, - fontSize: 12, - ), - ), - Text( - mbtiResult.starDisplay, - style: TextStyle( - color: badgeColor, - fontSize: 14, - letterSpacing: 1, - ), - ), - const SizedBox(width: 4), - Icon( - LucideIcons.info, - size: 12, - color: appColors.iconSubtle, - ), - ], - ), - ), - ), - ), - - // Brand Name + // 変更2: 銘柄名から鉛筆アイコンを除去(タップで編集は維持) Center( child: InkWell( onTap: onTapName, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - sake.displayData.displayName, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - textAlign: TextAlign.center, - ), - ), - const SizedBox(width: 8), - Icon(LucideIcons.pencil, size: 18, color: appColors.iconSubtle), - ], + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + sake.displayData.displayName, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + textAlign: TextAlign.center, + ), ), ), ), - const SizedBox(height: 8), + const SizedBox(height: 6), - // Brand / Prefecture + // 変更2: 酒蔵 / 都道府県 — 区切りを · に変更・鉛筆アイコン除去 if (sake.itemType != ItemType.set) Center( child: InkWell( onTap: onTapBrewery, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - '${sake.displayData.displayBrewery} / ${sake.displayData.displayPrefecture}', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: appColors.textSecondary, - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(width: 8), - Icon(LucideIcons.pencil, size: 16, color: appColors.iconSubtle), - ], - ), - ), - ), - const SizedBox(height: 16), - - // Tags Row - if (sake.hiddenSpecs.flavorTags.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onTapTags, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Wrap( - alignment: WrapAlignment.center, - spacing: 8, - children: sake.hiddenSpecs.flavorTags - .map((tag) => Chip( - label: Text(tag, style: const TextStyle(fontSize: 10)), - visualDensity: VisualDensity.compact, - backgroundColor: - Theme.of(context).primaryColor.withValues(alpha: 0.1), - )) - .toList(), - ), + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + '${sake.displayData.displayBrewery} · ${sake.displayData.displayPrefecture}', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: appColors.textSecondary, + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.center, ), ), ), ), + const SizedBox(height: 20), - const SizedBox(height: 24), - - // AI Catchcopy (Mincho) + // キャッチコピー(位置を銘柄名直下に移動して存在感を強調) if (sake.displayData.catchCopy != null && sake.itemType != ItemType.set) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - sake.displayData.catchCopy!, - style: GoogleFonts.zenOldMincho( - fontSize: 24, - height: 1.5, - fontWeight: FontWeight.w500, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Theme.of(context).primaryColor, + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + sake.displayData.catchCopy!, + style: GoogleFonts.zenOldMincho( + fontSize: 22, + height: 1.6, + fontWeight: FontWeight.w500, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white70 + : appColors.brandPrimary.withValues(alpha: 0.8), + ), + textAlign: TextAlign.center, + ), + ), + ), + + const SizedBox(height: 20), + + // タグ — pill 形状・中央揃え + if (sake.hiddenSpecs.flavorTags.isNotEmpty) + Center( + child: InkWell( + onTap: onTapTags, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 6, + runSpacing: 6, + children: sake.hiddenSpecs.flavorTags.map((tag) => Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: appColors.brandPrimary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: appColors.brandPrimary.withValues(alpha: 0.2), + ), + ), + child: Text( + tag, + style: TextStyle( + fontSize: 11, + color: appColors.brandPrimary, + fontWeight: FontWeight.w500, + ), + ), + )).toList(), + ), ), - textAlign: TextAlign.center, ), ), ], diff --git a/lib/screens/sake_detail_screen.dart b/lib/screens/sake_detail_screen.dart index 06423db..0f0b305 100644 --- a/lib/screens/sake_detail_screen.dart +++ b/lib/screens/sake_detail_screen.dart @@ -152,35 +152,29 @@ class _SakeDetailScreenState extends ConsumerState { onTapMbtiCompatibility: _showMbtiCompatibilityDialog, ), - const SizedBox(height: 24), - const Divider(), - const SizedBox(height: 24), + const SizedBox(height: 32), - // Taste Radar Chart (Extracted) with Manual Edit + // Taste Radar Chart SakeDetailChart( sake: _sake, onTasteStatsEdited: (newStats) => _updateTasteStats(newStats), ), - - const SizedBox(height: 24), - const Divider(), - const SizedBox(height: 24), + + const SizedBox(height: 32), // Description if (_sake.hiddenSpecs.description != null && _sake.itemType != ItemType.set) - Text( - _sake.hiddenSpecs.description!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - height: 1.8, - fontSize: 16, - ), - ), + Text( + _sake.hiddenSpecs.description!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + height: 1.8, + fontSize: 16, + ), + ), const SizedBox(height: 32), - const Divider(), - const SizedBox(height: 16), - // AI Specs Accordion (Extracted) + // AI Specs Accordion SakeDetailSpecs( sake: _sake, onUpdate: (updatedSake) { @@ -188,44 +182,37 @@ class _SakeDetailScreenState extends ConsumerState { }, ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 16), + const SizedBox(height: 24), - // Memo Field (Extracted) + // Memo Field SakeDetailMemo( initialMemo: _sake.userData.memo, onUpdate: (value) async { - // Auto-save - final box = Hive.box('sake_items'); - final updated = _sake.copyWith(memo: value, isUserEdited: true); - await box.put(_sake.key, updated); - // Note: setState is needed to update the 'updated' variable locally - // But the text field manages its own state, so we don't strictly need to rebuild the text field - // However, other parts might depend on _sake.userData.memo? Unlikely. - // Actually, we should update _sake here to keep consistency. - setState(() => _sake = updated); + final box = Hive.box('sake_items'); + final updated = _sake.copyWith(memo: value, isUserEdited: true); + await box.put(_sake.key, updated); + setState(() => _sake = updated); }, ), const SizedBox(height: 48), // Related Items 3D Carousel - if (_sake.itemType != ItemType.set) ...[ // Hide for Set Items + if (_sake.itemType != ItemType.set) ...[ Row( children: [ - Icon(LucideIcons.sparkles, size: 16, color: Theme.of(context).colorScheme.onSurface), + Icon(LucideIcons.sparkles, size: 16, color: appColors.iconDefault), const SizedBox(width: 8), Text( 'おすすめの日本酒', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, + color: appColors.textPrimary, ), ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 6), Text( '五味チャート・タグ・酒蔵・産地から自動選出', style: Theme.of(context).textTheme.bodySmall?.copyWith( diff --git a/lib/widgets/sake_detail/sake_detail_specs.dart b/lib/widgets/sake_detail/sake_detail_specs.dart index 5f188ed..e893071 100644 --- a/lib/widgets/sake_detail/sake_detail_specs.dart +++ b/lib/widgets/sake_detail/sake_detail_specs.dart @@ -174,12 +174,32 @@ class _SakeDetailSpecsState extends State { } }, leading: Icon(LucideIcons.sparkles, color: appColors.iconDefault), - title: Text( - '詳細', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: textColor, - ), + title: Row( + children: [ + Text( + '詳細スペック', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: textColor, + ), + ), + // チラ見せ: アルコール度数・精米歩合を折りたたみ状態でも表示 + if (!_isEditing) ...[ + const SizedBox(width: 10), + if (widget.sake.hiddenSpecs.alcoholContent != null) + _buildPeekChip( + '${widget.sake.hiddenSpecs.alcoholContent}%', + appColors, + ), + if (widget.sake.hiddenSpecs.polishingRatio != null) ...[ + const SizedBox(width: 4), + _buildPeekChip( + '精米${widget.sake.hiddenSpecs.polishingRatio}%', + appColors, + ), + ], + ], + ], ), trailing: _isEditing ? Row( @@ -261,6 +281,25 @@ class _SakeDetailSpecsState extends State { ); } + Widget _buildPeekChip(String label, AppColors appColors) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: appColors.surfaceSubtle, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: appColors.divider), + ), + child: Text( + label, + style: TextStyle( + fontSize: 10, + color: appColors.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ); + } + void _showDatePicker(BuildContext context) { if (!_isEditing) return;