Compare commits

...

4 Commits

Author SHA1 Message Date
Ponshu Developer 6c96eaf01b chore: bump version to 1.0.20+31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 09:15:57 +09:00
Ponshu Developer a691be07fa feat: simplify list item — accurate info only, remove AI-inferred tags
- 酒蔵 · 都道府県: / → · 区切りに変更(詳細画面と統一)
- フレーバータグ削除: AI推測精度が低いため一覧から除去(詳細画面のみ)
- 特定名称 + アルコール度数を追加: ラベル直読みの正確な情報のみ表示
  (どちらか一方のみの場合も対応、両方ない場合は行自体を非表示)
- セット商品の説明文は維持(ユーザー入力のため)
- お気に入りアイコンは現状(右上)のまま維持

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 09:12:59 +09:00
Ponshu Developer 818f8862a1 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>
2026-04-05 14:55:21 +09:00
Ponshu Developer 4758aa5c9c fix: remove Pro-only placeholder in consumer APK, fix A-1/A-3 visibility
- sake_detail_screen: hide SakeMbtiStampSection when isProVersion=false
  (consumer APK no longer shows Pro版限定 placeholder at card bottom)
- sake_grid_item / sake_list_item: apply Pressable to actual tap targets
  (grid/list cards now animate on press instead of non-interactive LevelTitleCard)
- soul_screen / sommelier_screen: ambient glow alpha 0.07→0.12 for visibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 14:33:40 +09:00
8 changed files with 264 additions and 227 deletions

View File

@ -105,7 +105,7 @@ class _SommelierScreenState extends ConsumerState<SommelierScreen> {
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
Theme.of(context).extension<AppColors>()!.brandAccent.withValues(alpha: 0.07),
Theme.of(context).extension<AppColors>()!.brandAccent.withValues(alpha: 0.12),
Colors.transparent,
],
),

View File

@ -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,
),
),
],

View File

@ -18,6 +18,7 @@ import '../widgets/sake_detail/sake_detail_memo.dart';
import '../widgets/sake_detail/sake_detail_specs.dart';
import 'sake_detail/widgets/sake_photo_edit_modal.dart';
import 'sake_detail/sections/sake_mbti_stamp_section.dart';
import '../main.dart' show isProVersion;
import 'sake_detail/sections/sake_basic_info_section.dart';
import 'sake_detail/widgets/sake_detail_sliver_app_bar.dart';
import '../services/mbti_compatibility_service.dart';
@ -151,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) {
@ -187,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(
@ -265,9 +253,11 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
const SizedBox(height: 48),
],
// MBTI Diagnostic Stamp Section (Phase C3)
SakeMbtiStampSection(sake: _sake),
const SizedBox(height: 24),
// MBTI Diagnostic Stamp Section (Pro only)
if (isProVersion) ...[
SakeMbtiStampSection(sake: _sake),
const SizedBox(height: 24),
],
],
),
),

View File

@ -48,7 +48,7 @@ class _SoulScreenState extends ConsumerState<SoulScreen> {
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
appColors.brandPrimary.withValues(alpha: 0.07),
appColors.brandPrimary.withValues(alpha: 0.12),
Colors.transparent,
],
),

View File

@ -8,6 +8,7 @@ import '../../theme/app_theme.dart';
import '../../theme/app_colors.dart';
import '../../providers/ui_experiment_provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../common/pressable.dart';
class SakeGridItem extends ConsumerWidget {
final SakeItem sake;
@ -24,7 +25,8 @@ class SakeGridItem extends ConsumerWidget {
final isSelected = ref.watch(selectedMenuSakeIdsProvider).contains(sake.id);
final appColors = Theme.of(context).extension<AppColors>()!;
return Card(
return Pressable(
child: Card(
clipBehavior: Clip.antiAlias,
// Highlight selected
shape: isMenuMode && isSelected
@ -198,8 +200,9 @@ class SakeGridItem extends ConsumerWidget {
),
),
],
),
),
);
), // Stack
), // InkWell
), // Card
); // Pressable
}
}

View File

@ -7,7 +7,7 @@ import '../../screens/sake_detail_screen.dart';
import '../../theme/app_theme.dart';
import '../../theme/app_colors.dart';
import 'package:lucide_icons/lucide_icons.dart';
// Haptic via InkWell? No, explicit HapticFeedback used generally.
import '../common/pressable.dart';
class SakeListItem extends ConsumerWidget {
final SakeItem sake;
@ -29,9 +29,10 @@ class SakeListItem extends ConsumerWidget {
// Adaptive selection color
final selectedColor = appColors.brandAccent.withValues(alpha: 0.15);
return Card(
return Pressable(
child: Card(
clipBehavior: Clip.antiAlias,
elevation: 1, // Slight elevation
elevation: 1,
color: isMenuMode && isSelected ? selectedColor : null,
shape: isMenuMode && isSelected
? RoundedRectangleBorder(side: BorderSide(color: appColors.brandAccent, width: 2), borderRadius: BorderRadius.circular(6))
@ -160,56 +161,65 @@ class SakeListItem extends ConsumerWidget {
const Icon(Icons.favorite, color: Colors.pink, size: 16),
],
),
const SizedBox(height: AppTheme.spacingTiny),
// Brand / Prefecture ()
const SizedBox(height: 3),
// ·
if (sake.itemType != ItemType.set &&
(sake.displayData.displayBrewery.isNotEmpty || sake.displayData.displayPrefecture.isNotEmpty))
Row(
children: [
Expanded(
child: Text(
'${sake.displayData.displayBrewery} / ${sake.displayData.displayPrefecture}',
style: Theme.of(context).textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
),
],
Text(
'${sake.displayData.displayBrewery} · ${sake.displayData.displayPrefecture}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: appColors.textSecondary,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
//
//
if (sake.itemType == ItemType.set && sake.displayData.catchCopy != null)
Text(
sake.displayData.catchCopy!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: appColors.textSecondary,
fontStyle: FontStyle.italic,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (!isMenuMode && sake.hiddenSpecs.flavorTags.isNotEmpty) ...[
const SizedBox(height: AppTheme.spacingSmall),
Wrap(
spacing: 4,
runSpacing: 4,
children: sake.hiddenSpecs.flavorTags.take(3).map((tag) => Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: appColors.surfaceSubtle,
borderRadius: BorderRadius.circular(4),
),
child: Text(
tag,
style: TextStyle(fontSize: 10, color: appColors.textSecondary),
),
)).toList(),
)
]
// +
if (!isMenuMode && sake.itemType != ItemType.set) ...[
const SizedBox(height: 6),
_buildSpecLine(context, appColors),
],
],
),
),
),
],
),
), // Row
), // InkWell
), // Card
); // Pressable
}
/// +
Widget _buildSpecLine(BuildContext context, AppColors appColors) {
final type = sake.hiddenSpecs.type;
final alcohol = sake.hiddenSpecs.alcoholContent;
final parts = <String>[];
if (type != null && type.isNotEmpty) parts.add(type);
if (alcohol != null) parts.add('${alcohol.toStringAsFixed(alcohol.truncateToDouble() == alcohol ? 0 : 1)}%');
if (parts.isEmpty) return const SizedBox.shrink();
return Text(
parts.join(' · '),
style: TextStyle(
fontSize: 11,
color: appColors.textSecondary,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
}
}

View File

@ -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;

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.19+30
version: 1.0.20+31
environment:
sdk: ^3.10.1