chore: distribution prep - Kotlin DSL signing, .gitignore, build() split

- Fix DISTRIBUTION_API_KEY_SETUP.md: Groovy -> Kotlin DSL, Windows keytool cmds
- Add release signing config to build.gradle.kts (key.properties fallback)
- Add .serena/, key.properties, ponshu_release.jks to .gitignore
- Include Claude's build() split: SakeDetailSliverAppBar, SakeBasicInfoSection
- sake_detail_screen.dart: 1099 -> 795 lines (-304 lines)
- Remove tmp_commit_msg.txt
This commit is contained in:
Ponshu Developer 2026-02-16 19:08:09 +09:00
parent 1a50c739a1
commit d760e7bf08
7 changed files with 630 additions and 349 deletions

7
.gitignore vendored
View File

@ -53,10 +53,15 @@ lib/libsecrets.dart
*.aab
*.ipa
# IDE / Editor
# IDE / Editor / MCP Tools
.claude/
.cursor/
.vscode/
.serena/
# Release signing
android/app/ponshu_release.jks
android/key.properties
# Environment / Secrets
.env

View File

@ -1,3 +1,5 @@
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
@ -6,6 +8,13 @@ plugins {
id("com.google.gms.google-services")
}
// Release signing: key.properties から読み込み存在しない場合はdebug署名にフォールバック
val keyPropertiesFile = rootProject.file("app/key.properties")
val keyProperties = Properties()
if (keyPropertiesFile.exists()) {
keyProperties.load(keyPropertiesFile.inputStream())
}
android {
namespace = "com.posimai.ponshu_room"
compileSdk = 36
@ -21,7 +30,7 @@ android {
}
defaultConfig {
// Pro version package name (different from Lite version: com.posimai.ponshu_room.lite)
// Lite版のアプリIDPro版: com.posimai.ponshu_room
applicationId = "com.posimai.ponshu_room_lite"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
@ -35,15 +44,27 @@ android {
// multiDexEnabled = true
}
signingConfigs {
if (keyPropertiesFile.exists()) {
create("release") {
keyAlias = keyProperties["keyAlias"] as String
keyPassword = keyProperties["keyPassword"] as String
storeFile = file(keyProperties["storeFile"] as String)
storePassword = keyProperties["storePassword"] as String
}
}
}
buildTypes {
getByName("release") {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
// Explicitly disable R8
// key.properties が存在すればrelease署名、なければdebug署名開発用
signingConfig = if (keyPropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
isMinifyEnabled = false
isShrinkResources = false
// proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}

View File

@ -0,0 +1,170 @@
# 配布用 Gemini API キー設定ガイド
このガイドは、知人配布用 APK に埋め込む **専用 Gemini API キー** を発行・設定する手順書です。
## なぜ専用キーが必要か
開発用キー(`lib/secrets.local.dart` に設定済み)をそのまま配布 APK に使うと:
- APK を逆コンパイルすれば誰でもキーを抽出できる
- あなたのクォータ(無料枠)を知らない第三者に使い尽くされる可能性がある
- キーを無効化すると開発環境も壊れる
**解決策**: 配布専用キーを発行し、Androidパッケージ名で制限する。
---
## 手順
### 1. Google Cloud Console で新しい API キーを発行
1. [Google AI Studio](https://aistudio.google.com/) にアクセス
2. 右上のメニューから「Get API key」→「Create API key in new project」
- または既存プロジェクトに追加: 「Create API key」→ プロジェクト選択
3. 発行されたキーをメモ(`AIzaSy...` 形式)
### 2. API キーに制限を設定
[Google Cloud Console API 認証情報ページ](https://console.cloud.google.com/apis/credentials) にアクセス:
1. 作成したキーをクリック → 「キーを編集」
2. **API の制限**:
- 「キーを制限する」を選択
- 「Generative Language API」のみ選択
3. **アプリケーションの制限**:
- 「Android アプリ」を選択
- 「アイテムを追加」をクリック
- パッケージ名: `com.posimai.ponshu_room_lite`
- SHA-1 証明書フィンガープリント: (後述)
4. 保存
### 3. リリース署名の SHA-1 フィンガープリントを取得
配布 APK はリリースキーストアで署名する必要があります。
**キーストアが未作成の場合Windowsコマンドプロンプト:**
```cmd
keytool -genkeypair -v -keystore android\app\ponshu_release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias ponshu_key
```
パスワードを設定し、組織情報を入力。
> ⚠️ **パスワードは安全な場所に記録してください。紛失するとアプリの更新ができなくなります。**
**SHA-1 の取得Windowsコマンドプロンプト:**
```cmd
keytool -list -v -keystore android\app\ponshu_release.jks -alias ponshu_key
```
出力の `SHA1:` 行をコピーして Google Cloud Console に貼り付ける。
### 4. key.properties にパスワードを設定
`android/key.properties` を新規作成(このファイルは .gitignore に含まれています):
```properties
storePassword=あなたが設定したパスワード
keyPassword=あなたが設定したパスワード
keyAlias=ponshu_key
storeFile=ponshu_release.jks
```
### 5. build.gradle.kts にリリース署名を設定
`android/app/build.gradle.kts` を以下のように変更:
```kotlin
import java.util.Properties
// key.properties からリリース署名情報を読み込み
val keyPropertiesFile = rootProject.file("app/key.properties")
val keyProperties = Properties()
if (keyPropertiesFile.exists()) {
keyProperties.load(keyPropertiesFile.inputStream())
}
android {
// ... 既存設定 ...
signingConfigs {
if (keyPropertiesFile.exists()) {
create("release") {
keyAlias = keyProperties["keyAlias"] as String
keyPassword = keyProperties["keyPassword"] as String
storeFile = file(keyProperties["storeFile"] as String)
storePassword = keyProperties["storePassword"] as String
}
}
}
buildTypes {
getByName("release") {
signingConfig = if (keyPropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
isMinifyEnabled = false
isShrinkResources = false
}
}
}
```
> key.properties が存在しない場合は自動的に debug 署名にフォールバックします。
### 6. 配布 APK のビルド
```cmd
flutter build apk --release --dart-define=GEMINI_API_KEY=AIzaSy...(配布用キー)
```
出力ファイル: `build\app\outputs\flutter-apk\app-release.apk`
---
## 配布方法
### Google Drive 経由(推奨)
1. `app-release.apk` を Google Drive にアップロード
2. 共有リンクを知人に送付
3. 受信者は Android の「設定 > セキュリティ > 提供元不明のアプリ」を有効化してインストール
### LINE で直接送信
1. APKファイルをLINEのトーク画面にドラッグドロップ
2. 受信者がファイルをタップしてインストール
### adb 直接転送(テスト時)
```cmd
adb install build\app\outputs\flutter-apk\app-release.apk
```
---
## API クォータ管理
| 設定 | 推奨値 | 説明 |
|------|--------|------|
| 1日あたりのリクエスト上限 | 100〜200 | Google Cloud Console → API → クォータで設定 |
| 無料枠 | 1,500回/日 | Gemini 2.5 Flash 無料枠 |
Google Cloud Console で **アラート** を設定しておくことを推奨:
- 「APIs & Services」→「Gemini API」→「クォータ」→「アラートの設定」
---
## チェックリスト
- [ ] 配布用 API キーを Google AI Studio で発行
- [ ] API 制限: Generative Language API のみ
- [ ] アプリ制限: `com.posimai.ponshu_room_lite` の SHA-1
- [ ] リリースキーストア(`ponshu_release.jks`)を作成
- [ ] `android/key.properties` にパスワードを設定
- [ ] キーストアと key.properties が `.gitignore` に含まれていることを確認
- [ ] `flutter build apk --release --dart-define=GEMINI_API_KEY=<配布用キー>` で APK ビルド
- [ ] テスト端末でインストール・動作確認
---
**Last Updated**: 2026-02-16

View File

@ -0,0 +1,230 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../models/sake_item.dart';
import '../../../theme/app_colors.dart';
import '../../../constants/app_constants.dart';
import '../../../providers/theme_provider.dart';
import '../../../services/mbti_compatibility_service.dart';
/// /AI確信度バッジMBTI相性バッジ
class SakeBasicInfoSection extends ConsumerWidget {
final SakeItem sake;
final VoidCallback onTapName;
final VoidCallback onTapBrewery;
final VoidCallback onTapTags;
final void Function(BuildContext context, CompatibilityResult result, AppColors appColors) onTapMbtiCompatibility;
const SakeBasicInfoSection({
super.key,
required this.sake,
required this.onTapName,
required this.onTapBrewery,
required this.onTapTags,
required this.onTapMbtiCompatibility,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appColors = Theme.of(context).extension<AppColors>()!;
// AI Confidence Logic (Theme Aware)
final score = sake.metadata.aiConfidence ?? 0;
final Color confidenceColor = score >= AppConstants.confidenceScoreHigh
? appColors.brandPrimary
: score >= AppConstants.confidenceScoreMedium
? appColors.textSecondary
: appColors.textTertiary;
// MBTI Compatibility
final userProfile = ref.watch(userProfileProvider);
final mbtiType = userProfile.mbti;
final mbtiResult = (mbtiType != null && mbtiType.isNotEmpty)
? MBTICompatibilityService.calculateCompatibility(mbtiType, sake)
: null;
final showMbti = mbtiResult != null && mbtiResult.hasResult;
final badgeColor = showMbti
? (mbtiResult.starRating >= 4
? appColors.brandPrimary
: mbtiResult.starRating >= 3
? appColors.textSecondary
: appColors.textTertiary)
: null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Confidence Badge
if (sake.metadata.aiConfidence != null && sake.itemType != ItemType.set)
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,
children: [
Icon(LucideIcons.sparkles, size: 14, color: confidenceColor),
const SizedBox(width: 6),
Text(
'AI確信度: $score%',
style: TextStyle(
color: confidenceColor,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
),
// 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
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),
],
),
),
),
const SizedBox(height: 8),
// Brand / Prefecture
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(),
),
),
),
),
),
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,
),
textAlign: TextAlign.center,
),
),
],
);
}
}

View File

@ -0,0 +1,167 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../models/sake_item.dart';
import '../../../theme/app_colors.dart';
import '../../../widgets/common/munyun_like_button.dart';
/// SakeDetailScreen SliverAppBar
/// /FAB付き写真編集ボタン
class SakeDetailSliverAppBar extends StatelessWidget {
final SakeItem sake;
final int currentImageIndex;
final VoidCallback onToggleFavorite;
final VoidCallback onReanalyze;
final VoidCallback onDelete;
final ValueChanged<int> onPageChanged;
final VoidCallback onPhotoEdit;
const SakeDetailSliverAppBar({
super.key,
required this.sake,
required this.currentImageIndex,
required this.onToggleFavorite,
required this.onReanalyze,
required this.onDelete,
required this.onPageChanged,
required this.onPhotoEdit,
});
@override
Widget build(BuildContext context) {
final appColors = Theme.of(context).extension<AppColors>()!;
return SliverAppBar(
expandedHeight: 400.0,
floating: false,
pinned: true,
backgroundColor: Theme.of(context).primaryColor,
iconTheme: const IconThemeData(color: Colors.white),
actions: [
MunyunLikeButton(
isLiked: sake.userData.isFavorite,
onTap: onToggleFavorite,
),
IconButton(
icon: const Icon(LucideIcons.refreshCw),
color: Colors.white,
tooltip: 'AI再解析',
onPressed: onReanalyze,
),
IconButton(
icon: const Icon(LucideIcons.trash2),
color: Colors.white,
tooltip: '削除',
onPressed: onDelete,
),
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
sake.displayData.imagePaths.length > 1
? Stack(
fit: StackFit.expand,
children: [
PageView.builder(
itemCount: sake.displayData.imagePaths.length,
onPageChanged: onPageChanged,
itemBuilder: (context, index) {
final imageWidget = Image.file(
File(sake.displayData.imagePaths[index]),
fit: BoxFit.cover,
);
// Apply Hero only to the first image for smooth transition from Grid/List
if (index == 0) {
return Hero(
tag: sake.id,
child: imageWidget,
);
}
return imageWidget;
},
),
// Simple Indicator
Positioned(
bottom: 16,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'${currentImageIndex + 1} / ${sake.displayData.imagePaths.length}',
style: const TextStyle(
color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
),
),
// Photo Edit Button
Positioned(
bottom: 16,
left: 16,
child: FloatingActionButton.small(
heroTag: 'photo_edit',
backgroundColor: Colors.white,
onPressed: onPhotoEdit,
child: Icon(LucideIcons.image, color: appColors.iconDefault),
),
),
],
)
: Stack(
fit: StackFit.expand,
children: [
Hero(
tag: sake.id,
child: sake.displayData.imagePaths.isNotEmpty
? Image.file(
File(sake.displayData.imagePaths.first),
fit: BoxFit.cover,
)
: Container(
color: appColors.surfaceSubtle,
child: Icon(LucideIcons.image, size: 80, color: appColors.iconSubtle),
),
),
// Photo Edit Button for single image
Positioned(
bottom: 16,
left: 16,
child: FloatingActionButton.small(
heroTag: 'photo_edit_single',
backgroundColor: Colors.white,
onPressed: onPhotoEdit,
child: Icon(LucideIcons.image, color: appColors.iconDefault),
),
),
],
),
// Scrim for Header Icons Visibility
Positioned(
top: 0,
left: 0,
right: 0,
height: 100,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
],
),
),
),
),
],
),
),
);
}
}

View File

@ -1,7 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../models/sake_item.dart';
@ -11,18 +10,19 @@ import '../widgets/analyzing_dialog.dart';
import '../widgets/sake_3d_carousel_with_reason.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../providers/sake_list_provider.dart';
import '../providers/theme_provider.dart';
import 'sake_detail/sections/sake_pricing_section.dart';
import '../theme/app_colors.dart';
import '../constants/app_constants.dart';
import '../widgets/common/munyun_like_button.dart';
import '../widgets/sake_detail/sake_detail_chart.dart';
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 'sake_detail/sections/sake_basic_info_section.dart';
import 'sake_detail/widgets/sake_detail_sliver_app_bar.dart';
import '../services/mbti_compatibility_service.dart';
import '../widgets/sakenowa/sakenowa_detail_recommendation_section.dart';
// Note: google_fonts and theme_provider are now used in sake_basic_info_section.dart
class SakeDetailScreen extends ConsumerStatefulWidget {
@ -83,15 +83,6 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
Widget build(BuildContext context) {
final appColors = Theme.of(context).extension<AppColors>()!;
// Determine confidence text color (CRITICAL FIX: Use AppColors for theme consistency)
// AI Confidence Logic (Theme Aware)
final score = _sake.metadata.aiConfidence ?? 0;
final Color confidenceColor = score >= AppConstants.confidenceScoreHigh
? appColors.brandPrimary // High confidence: Primary brand color
: score >= AppConstants.confidenceScoreMedium
? appColors.textSecondary // Medium confidence: Secondary text color
: appColors.textTertiary; // Low confidence: Tertiary (muted)
// (Phase 1-8 Enhanced)
final allSakeAsync = ref.watch(allSakeItemsProvider);
final allSake = allSakeAsync.asData?.value ?? [];
@ -108,139 +99,17 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 400.0,
floating: false,
pinned: true,
backgroundColor: Theme.of(context).primaryColor,
iconTheme: const IconThemeData(color: Colors.white),
actions: [
MunyunLikeButton(
isLiked: _sake.userData.isFavorite,
onTap: () => _toggleFavorite(),
),
IconButton(
icon: const Icon(LucideIcons.refreshCw),
color: Colors.white,
tooltip: 'AI再解析',
onPressed: () => _reanalyze(context),
),
IconButton(
icon: const Icon(LucideIcons.trash2),
color: Colors.white,
tooltip: '削除',
onPressed: () {
HapticFeedback.heavyImpact();
_showDeleteDialog(context);
},
),
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
_sake.displayData.imagePaths.length > 1
? Stack(
fit: StackFit.expand,
children: [
PageView.builder(
itemCount: _sake.displayData.imagePaths.length,
onPageChanged: (index) => setState(() => _currentImageIndex = index),
itemBuilder: (context, index) {
final imageWidget = Image.file(
File(_sake.displayData.imagePaths[index]),
fit: BoxFit.cover,
);
// Apply Hero only to the first image for smooth transition from Grid/List
if (index == 0) {
return Hero(
tag: _sake.id,
child: imageWidget,
);
}
return imageWidget;
},
),
// Simple Indicator
Positioned(
bottom: 16,
right: 16,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(16),
),
child: Text(
'${_currentImageIndex + 1} / ${_sake.displayData.imagePaths.length}',
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
),
),
),
// Photo Edit Button
Positioned(
bottom: 16,
left: 16,
child: FloatingActionButton.small(
heroTag: 'photo_edit',
backgroundColor: Colors.white,
onPressed: () => _showPhotoEditModal(context),
child: Icon(LucideIcons.image, color: appColors.iconDefault),
),
),
],
)
: Stack(
fit: StackFit.expand,
children: [
Hero(
tag: _sake.id,
child: _sake.displayData.imagePaths.isNotEmpty
? Image.file(
File(_sake.displayData.imagePaths.first),
fit: BoxFit.cover,
)
: Container(
color: appColors.surfaceSubtle,
child: Icon(LucideIcons.image, size: 80, color: appColors.iconSubtle),
),
),
// Photo Edit Button for single image
Positioned(
bottom: 16,
left: 16,
child: FloatingActionButton.small(
heroTag: 'photo_edit_single',
backgroundColor: Colors.white,
onPressed: () => _showPhotoEditModal(context),
child: Icon(LucideIcons.image, color: appColors.iconDefault),
),
),
],
),
// Scrim for Header Icons Visibility
Positioned(
top: 0,
left: 0,
right: 0,
height: 100,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.7),
Colors.transparent,
],
),
),
),
),
],
),
),
SakeDetailSliverAppBar(
sake: _sake,
currentImageIndex: _currentImageIndex,
onToggleFavorite: _toggleFavorite,
onReanalyze: () => _reanalyze(context),
onDelete: () {
HapticFeedback.heavyImpact();
_showDeleteDialog(context);
},
onPageChanged: (index) => setState(() => _currentImageIndex = index),
onPhotoEdit: () => _showPhotoEditModal(context),
),
SliverToBoxAdapter(
child: Container(
@ -263,197 +132,23 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Confidence Badge
if (_sake.metadata.aiConfidence != null && _sake.itemType != ItemType.set)
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,
children: [
Icon(LucideIcons.sparkles, size: 14, color: confidenceColor),
const SizedBox(width: 6),
Text(
'AI確信度: $score%',
style: TextStyle(
color: confidenceColor,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
),
// MBTI Compatibility Badge (Star Rating Pattern B)
Consumer(
builder: (context, ref, child) {
final userProfile = ref.watch(userProfileProvider);
final mbtiType = userProfile.mbti;
if (mbtiType == null || mbtiType.isEmpty) return const SizedBox.shrink();
final result = MBTICompatibilityService.calculateCompatibility(mbtiType, _sake);
if (!result.hasResult) return const SizedBox.shrink();
final badgeColor = result.starRating >= 4
? appColors.brandPrimary
: result.starRating >= 3
? appColors.textSecondary
: appColors.textTertiary;
return Center(
child: GestureDetector(
onTap: () => _showMbtiCompatibilityDialog(context, result, 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(
result.starDisplay,
style: TextStyle(
color: badgeColor,
fontSize: 14,
letterSpacing: 1,
),
),
const SizedBox(width: 4),
Icon(
LucideIcons.info,
size: 12,
color: appColors.iconSubtle,
),
],
),
),
),
);
},
),
// Brand Name
Center(
child: InkWell(
onTap: () => _showTextEditDialog(
context,
title: '銘柄名を編集',
initialValue: _sake.displayData.displayName,
onSave: (value) async {
final box = Hive.box<SakeItem>('sake_items');
final updated = _sake.copyWith(name: value, isUserEdited: true);
await box.put(_sake.key, updated);
setState(() => _sake = updated);
},
),
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),
],
),
),
),
const SizedBox(height: 8),
// Brand / Prefecture
if (_sake.itemType != ItemType.set)
Center(
child: InkWell(
onTap: () => _showBreweryEditDialog(context),
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: () => _showTagEditDialog(context),
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(),
),
),
),
),
),
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, // Adaptive
),
textAlign: TextAlign.center,
// Basic Info: AI確信度MBTI相性 (Extracted)
SakeBasicInfoSection(
sake: _sake,
onTapName: () => _showTextEditDialog(
context,
title: '銘柄名を編集',
initialValue: _sake.displayData.displayName,
onSave: (value) async {
final box = Hive.box<SakeItem>('sake_items');
final updated = _sake.copyWith(name: value, isUserEdited: true);
await box.put(_sake.key, updated);
setState(() => _sake = updated);
},
),
onTapBrewery: () => _showBreweryEditDialog(context),
onTapTags: () => _showTagEditDialog(context),
onTapMbtiCompatibility: _showMbtiCompatibilityDialog,
),
const SizedBox(height: 24),

View File

@ -1,7 +0,0 @@
chore: Clean up repository - remove temp files, strengthen .gitignore
- Remove APK build artifacts from working directory
- Move old review/analysis reports to docs/archive/
- Move initial setup docs to docs/archive/
- Add .apk/.aab/.ipa/.claude/.cursor/.env to .gitignore
- Remove .claude/ from git tracking