feat: detail screen UI polish — badge merge, clean typography, spec peek, no dividers
変更1: AI確信度 + MBTI相性バッジを横並び1行に統合(Wrap)
変更2: 銘柄名・酒蔵から常時表示の鉛筆アイコンを除去、区切りを / → · に変更
キャッチコピーを銘柄名直下に移動、タグをpill形状(radius:20)に変更
変更3: スペック詳細アコーディオンのタイトルにアルコール度数・精米歩合をチラ見せ
変更4: Divider 4本 → SizedBox(height:32) に置換
おすすめ見出しカラーを colorScheme.onSurface → appColors.textPrimary に統一
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4758aa5c9c
commit
818f8862a1
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -152,35 +152,29 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
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<SakeDetailScreen> {
|
|||
},
|
||||
),
|
||||
|
||||
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<SakeItem>('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<SakeItem>('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(
|
||||
|
|
|
|||
|
|
@ -174,12 +174,32 @@ class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
|
|||
}
|
||||
},
|
||||
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<SakeDetailSpecs> {
|
|||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue