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,86 +55,80 @@ class SakeBasicInfoSection extends ConsumerWidget {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Confidence Badge
|
// 変更1: AI確信度 + MBTI相性 を横並び1行に統合
|
||||||
if (sake.metadata.aiConfidence != null && sake.itemType != ItemType.set)
|
if ((sake.metadata.aiConfidence != null && sake.itemType != ItemType.set) || showMbti)
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Padding(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
if (sake.metadata.aiConfidence != null && sake.itemType != ItemType.set)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: confidenceColor.withValues(alpha: 0.1),
|
color: confidenceColor.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: confidenceColor.withValues(alpha: 0.5)),
|
border: Border.all(color: confidenceColor.withValues(alpha: 0.4)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(LucideIcons.sparkles, size: 14, color: confidenceColor),
|
Icon(LucideIcons.sparkles, size: 12, color: confidenceColor),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'AI確信度: $score%',
|
'AI $score%',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: confidenceColor,
|
color: confidenceColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// MBTI Compatibility Badge
|
|
||||||
if (showMbti)
|
if (showMbti)
|
||||||
Center(
|
GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: () => onTapMbtiCompatibility(context, mbtiResult!, appColors),
|
||||||
onTap: () => onTapMbtiCompatibility(context, mbtiResult, appColors),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: badgeColor!.withValues(alpha: 0.1),
|
color: badgeColor!.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: badgeColor.withValues(alpha: 0.3)),
|
border: Border.all(color: badgeColor.withValues(alpha: 0.4)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
Icon(LucideIcons.brainCircuit, size: 12, color: badgeColor),
|
||||||
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'$mbtiType相性: ',
|
'$mbtiType ${mbtiResult!.starDisplay}',
|
||||||
style: TextStyle(
|
|
||||||
color: appColors.textSecondary,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
mbtiResult.starDisplay,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: badgeColor,
|
color: badgeColor,
|
||||||
fontSize: 14,
|
fontWeight: FontWeight.bold,
|
||||||
letterSpacing: 1,
|
fontSize: 11,
|
||||||
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
|
||||||
Icon(
|
|
||||||
LucideIcons.info,
|
|
||||||
size: 12,
|
|
||||||
color: appColors.iconSubtle,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Brand Name
|
// 変更2: 銘柄名から鉛筆アイコンを除去(タップで編集は維持)
|
||||||
Center(
|
Center(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTapName,
|
onTap: onTapName,
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(8),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
Flexible(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
sake.displayData.displayName,
|
sake.displayData.displayName,
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
|
|
@ -144,84 +138,85 @@ class SakeBasicInfoSection extends ConsumerWidget {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
Icon(LucideIcons.pencil, size: 18, color: appColors.iconSubtle),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 6),
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// Brand / Prefecture
|
// 変更2: 酒蔵 / 都道府県 — 区切りを · に変更・鉛筆アイコン除去
|
||||||
if (sake.itemType != ItemType.set)
|
if (sake.itemType != ItemType.set)
|
||||||
Center(
|
Center(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTapBrewery,
|
onTap: onTapBrewery,
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(8),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
Flexible(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'${sake.displayData.displayBrewery} / ${sake.displayData.displayPrefecture}',
|
'${sake.displayData.displayBrewery} · ${sake.displayData.displayPrefecture}',
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: appColors.textSecondary,
|
color: appColors.textSecondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
Icon(LucideIcons.pencil, size: 16, color: appColors.iconSubtle),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Tags Row
|
// キャッチコピー(位置を銘柄名直下に移動して存在感を強調)
|
||||||
|
if (sake.displayData.catchCopy != null && sake.itemType != ItemType.set)
|
||||||
|
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)
|
if (sake.hiddenSpecs.flavorTags.isNotEmpty)
|
||||||
Padding(
|
Center(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTapTags,
|
onTap: onTapTags,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
spacing: 8,
|
spacing: 6,
|
||||||
children: sake.hiddenSpecs.flavorTags
|
runSpacing: 6,
|
||||||
.map((tag) => Chip(
|
children: sake.hiddenSpecs.flavorTags.map((tag) => Container(
|
||||||
label: Text(tag, style: const TextStyle(fontSize: 10)),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
visualDensity: VisualDensity.compact,
|
decoration: BoxDecoration(
|
||||||
backgroundColor:
|
color: appColors.brandPrimary.withValues(alpha: 0.08),
|
||||||
Theme.of(context).primaryColor.withValues(alpha: 0.1),
|
borderRadius: BorderRadius.circular(20),
|
||||||
))
|
border: Border.all(
|
||||||
.toList(),
|
color: appColors.brandPrimary.withValues(alpha: 0.2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
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(
|
child: Text(
|
||||||
sake.displayData.catchCopy!,
|
tag,
|
||||||
style: GoogleFonts.zenOldMincho(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 11,
|
||||||
height: 1.5,
|
color: appColors.brandPrimary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: Theme.of(context).primaryColor,
|
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
|
)).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -152,19 +152,15 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
onTapMbtiCompatibility: _showMbtiCompatibilityDialog,
|
onTapMbtiCompatibility: _showMbtiCompatibilityDialog,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 32),
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Taste Radar Chart (Extracted) with Manual Edit
|
// Taste Radar Chart
|
||||||
SakeDetailChart(
|
SakeDetailChart(
|
||||||
sake: _sake,
|
sake: _sake,
|
||||||
onTasteStatsEdited: (newStats) => _updateTasteStats(newStats),
|
onTasteStatsEdited: (newStats) => _updateTasteStats(newStats),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 32),
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Description
|
// Description
|
||||||
if (_sake.hiddenSpecs.description != null && _sake.itemType != ItemType.set)
|
if (_sake.hiddenSpecs.description != null && _sake.itemType != ItemType.set)
|
||||||
|
|
@ -177,10 +173,8 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// AI Specs Accordion (Extracted)
|
// AI Specs Accordion
|
||||||
SakeDetailSpecs(
|
SakeDetailSpecs(
|
||||||
sake: _sake,
|
sake: _sake,
|
||||||
onUpdate: (updatedSake) {
|
onUpdate: (updatedSake) {
|
||||||
|
|
@ -188,22 +182,15 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 24),
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Memo Field (Extracted)
|
// Memo Field
|
||||||
SakeDetailMemo(
|
SakeDetailMemo(
|
||||||
initialMemo: _sake.userData.memo,
|
initialMemo: _sake.userData.memo,
|
||||||
onUpdate: (value) async {
|
onUpdate: (value) async {
|
||||||
// Auto-save
|
|
||||||
final box = Hive.box<SakeItem>('sake_items');
|
final box = Hive.box<SakeItem>('sake_items');
|
||||||
final updated = _sake.copyWith(memo: value, isUserEdited: true);
|
final updated = _sake.copyWith(memo: value, isUserEdited: true);
|
||||||
await box.put(_sake.key, updated);
|
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);
|
setState(() => _sake = updated);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -211,21 +198,21 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
// Related Items 3D Carousel
|
// Related Items 3D Carousel
|
||||||
if (_sake.itemType != ItemType.set) ...[ // Hide for Set Items
|
if (_sake.itemType != ItemType.set) ...[
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(LucideIcons.sparkles, size: 16, color: Theme.of(context).colorScheme.onSurface),
|
Icon(LucideIcons.sparkles, size: 16, color: appColors.iconDefault),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'おすすめの日本酒',
|
'おすすめの日本酒',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: appColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'五味チャート・タグ・酒蔵・産地から自動選出',
|
'五味チャート・タグ・酒蔵・産地から自動選出',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
|
|
||||||
|
|
@ -174,13 +174,33 @@ class _SakeDetailSpecsState extends State<SakeDetailSpecs> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leading: Icon(LucideIcons.sparkles, color: appColors.iconDefault),
|
leading: Icon(LucideIcons.sparkles, color: appColors.iconDefault),
|
||||||
title: Text(
|
title: Row(
|
||||||
'詳細',
|
children: [
|
||||||
|
Text(
|
||||||
|
'詳細スペック',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: textColor,
|
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
|
trailing: _isEditing
|
||||||
? Row(
|
? Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -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) {
|
void _showDatePicker(BuildContext context) {
|
||||||
if (!_isEditing) return;
|
if (!_isEditing) return;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue