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:
parent
1a50c739a1
commit
d760e7bf08
|
|
@ -53,10 +53,15 @@ lib/libsecrets.dart
|
||||||
*.aab
|
*.aab
|
||||||
*.ipa
|
*.ipa
|
||||||
|
|
||||||
# IDE / Editor
|
# IDE / Editor / MCP Tools
|
||||||
.claude/
|
.claude/
|
||||||
.cursor/
|
.cursor/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.serena/
|
||||||
|
|
||||||
|
# Release signing
|
||||||
|
android/app/ponshu_release.jks
|
||||||
|
android/key.properties
|
||||||
|
|
||||||
# Environment / Secrets
|
# Environment / Secrets
|
||||||
.env
|
.env
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
|
|
@ -6,6 +8,13 @@ plugins {
|
||||||
id("com.google.gms.google-services")
|
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 {
|
android {
|
||||||
namespace = "com.posimai.ponshu_room"
|
namespace = "com.posimai.ponshu_room"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
@ -21,7 +30,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// Pro version package name (different from Lite version: com.posimai.ponshu_room.lite)
|
// Lite版のアプリID(Pro版: com.posimai.ponshu_room)
|
||||||
applicationId = "com.posimai.ponshu_room_lite"
|
applicationId = "com.posimai.ponshu_room_lite"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
|
@ -35,15 +44,27 @@ android {
|
||||||
// multiDexEnabled = true
|
// 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 {
|
buildTypes {
|
||||||
getByName("release") {
|
getByName("release") {
|
||||||
// TODO: Add your own signing config for the release build.
|
// key.properties が存在すればrelease署名、なければdebug署名(開発用)
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
signingConfig = if (keyPropertiesFile.exists()) {
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfigs.getByName("release")
|
||||||
// Explicitly disable R8
|
} else {
|
||||||
|
signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
isShrinkResources = false
|
isShrinkResources = false
|
||||||
// proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import '../models/sake_item.dart';
|
import '../models/sake_item.dart';
|
||||||
|
|
@ -11,18 +10,19 @@ import '../widgets/analyzing_dialog.dart';
|
||||||
import '../widgets/sake_3d_carousel_with_reason.dart';
|
import '../widgets/sake_3d_carousel_with_reason.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import '../providers/sake_list_provider.dart';
|
import '../providers/sake_list_provider.dart';
|
||||||
import '../providers/theme_provider.dart';
|
|
||||||
import 'sake_detail/sections/sake_pricing_section.dart';
|
import 'sake_detail/sections/sake_pricing_section.dart';
|
||||||
import '../theme/app_colors.dart';
|
import '../theme/app_colors.dart';
|
||||||
import '../constants/app_constants.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_chart.dart';
|
||||||
import '../widgets/sake_detail/sake_detail_memo.dart';
|
import '../widgets/sake_detail/sake_detail_memo.dart';
|
||||||
import '../widgets/sake_detail/sake_detail_specs.dart';
|
import '../widgets/sake_detail/sake_detail_specs.dart';
|
||||||
import 'sake_detail/widgets/sake_photo_edit_modal.dart';
|
import 'sake_detail/widgets/sake_photo_edit_modal.dart';
|
||||||
import 'sake_detail/sections/sake_mbti_stamp_section.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 '../services/mbti_compatibility_service.dart';
|
||||||
import '../widgets/sakenowa/sakenowa_detail_recommendation_section.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 {
|
class SakeDetailScreen extends ConsumerStatefulWidget {
|
||||||
|
|
@ -83,15 +83,6 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
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)
|
// スマートレコメンド (Phase 1-8 Enhanced)
|
||||||
final allSakeAsync = ref.watch(allSakeItemsProvider);
|
final allSakeAsync = ref.watch(allSakeItemsProvider);
|
||||||
final allSake = allSakeAsync.asData?.value ?? [];
|
final allSake = allSakeAsync.asData?.value ?? [];
|
||||||
|
|
@ -108,139 +99,17 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SakeDetailSliverAppBar(
|
||||||
expandedHeight: 400.0,
|
sake: _sake,
|
||||||
floating: false,
|
currentImageIndex: _currentImageIndex,
|
||||||
pinned: true,
|
onToggleFavorite: _toggleFavorite,
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
onReanalyze: () => _reanalyze(context),
|
||||||
iconTheme: const IconThemeData(color: Colors.white),
|
onDelete: () {
|
||||||
actions: [
|
HapticFeedback.heavyImpact();
|
||||||
MunyunLikeButton(
|
_showDeleteDialog(context);
|
||||||
isLiked: _sake.userData.isFavorite,
|
},
|
||||||
onTap: () => _toggleFavorite(),
|
onPageChanged: (index) => setState(() => _currentImageIndex = index),
|
||||||
),
|
onPhotoEdit: () => _showPhotoEditModal(context),
|
||||||
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,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|
@ -263,197 +132,23 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Confidence Badge
|
// Basic Info: AI確信度・MBTI相性・銘柄名・蔵元・タグ・キャッチコピー (Extracted)
|
||||||
if (_sake.metadata.aiConfidence != null && _sake.itemType != ItemType.set)
|
SakeBasicInfoSection(
|
||||||
Center(
|
sake: _sake,
|
||||||
child: Container(
|
onTapName: () => _showTextEditDialog(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
context,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
title: '銘柄名を編集',
|
||||||
decoration: BoxDecoration(
|
initialValue: _sake.displayData.displayName,
|
||||||
color: confidenceColor.withValues(alpha: 0.1),
|
onSave: (value) async {
|
||||||
borderRadius: BorderRadius.circular(20),
|
final box = Hive.box<SakeItem>('sake_items');
|
||||||
border: Border.all(color: confidenceColor.withValues(alpha: 0.5)),
|
final updated = _sake.copyWith(name: value, isUserEdited: true);
|
||||||
),
|
await box.put(_sake.key, updated);
|
||||||
child: Row(
|
setState(() => _sake = updated);
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
|
onTapBrewery: () => _showBreweryEditDialog(context),
|
||||||
|
onTapTags: () => _showTagEditDialog(context),
|
||||||
|
onTapMbtiCompatibility: _showMbtiCompatibilityDialog,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
Loading…
Reference in New Issue