v1.0.8 - Original (Ponshu Room Lite MVP Complete)

This commit is contained in:
Ponshu Developer 2026-01-11 17:17:29 +09:00
commit 5f2802728d
230 changed files with 23197 additions and 0 deletions

View File

@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(Get-ChildItem libwidgets -Filter *.dart -Recurse)"
],
"deny": [],
"ask": []
}
}

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Security
lib/secrets.dart

45
.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "19074d12f7eaf6a8180cd4036a430c1d76de904e"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: android
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: ios
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: linux
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: macos
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: web
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: windows
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

691
ANTIGRAVITY_PROMPT.md Normal file
View File

@ -0,0 +1,691 @@
# Antigravity向け - 新生ぽんるーむ実装プロンプト
**実装日**: 2025-12-29以降
**実装担当**: Antigravity
**サポート**: Claude (Anthropic)
---
## 🚨 最重要指示
### ⛔ 絶対にやってはいけないこと
```
❌ 既存のWeb版ponshu-room/lib/からコードを1行もコピーしない
❌ 既存プロジェクトを編集しない
❌ Web版のUIを踏襲しない
```
### ✅ やるべきこと
```
✅ 完全に新しいFlutterプロジェクトを作成
✅ FINAL_REQUIREMENTS.mdに100%従う
✅ スキャンアプリmai_quick_scanの成功パターンを参考にする
✅ 写真を主役にする
```
---
## 📱 プロジェクト作成(コピペして実行)
### ステップ1: 新規プロジェクト作成
```bash
# 親ディレクトリへ移動
cd C:\Users\maita\posimai-project
# 完全に新しいFlutterプロジェクトを作成
flutter create ponshu_room_reborn
cd ponshu_room_reborn
```
### ステップ2: pubspec.yaml編集
**完全に置き換え**:
```yaml
name: ponshu_room_reborn
description: "Reborn Ponshu Room - My Digital Sake Cellar"
publish_to: 'none'
version: 2.0.0+1
environment:
sdk: ^3.10.1
dependencies:
flutter:
sdk: flutter
# 状態管理(スキャンアプリと同じ)
flutter_riverpod: ^2.6.1
hooks_riverpod: ^2.6.1
flutter_hooks: ^0.20.5
# ローカルDB
hive: ^2.2.3
hive_flutter: ^1.1.0
# AI解析
google_generative_ai: ^0.4.7
http: ^1.6.0
# カメラ・画像
camera: ^0.11.0+2
image_picker: ^1.2.1
image: ^4.3.0
# UI
google_fonts: ^6.2.1
fl_chart: ^1.1.1
# 共有・保存
share_plus: ^12.0.1
path_provider: ^2.1.5
# その他
intl: ^0.20.2
package_info_plus: ^9.0.0
url_launcher: ^6.3.2
countries_world_map: ^1.3.0
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
hive_generator: ^2.0.1
build_runner: ^2.4.13
flutter:
uses-material-design: true
```
```bash
flutter pub get
```
### ステップ3: Android設定
`android/app/build.gradle.kts` を編集:
```kotlin
android {
namespace = "com.posimai.ponshu_room"
compileSdk = 36 // Android 15+ 対応
defaultConfig {
applicationId = "com.posimai.ponshu_room"
minSdk = 21
targetSdk = 35
versionCode = 1
versionName = "2.0.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("debug")
}
}
}
```
---
## 🎨 デザイン仕様(最重要)
### 写真を主役にする「ナロー・マージン」設計
#### ❌ 従来の余白設計(ダメな例)
```dart
ListView.separated(
padding: EdgeInsets.all(16), // ← 余白が大きすぎ
separatorBuilder: (context, index) => SizedBox(height: 16), // ← 隙間が大きすぎ
itemBuilder: (context, index) => SakeCard(...),
)
```
**問題点**: 写真が小さくなる、迫力がない
#### ✅ 新生ぽんるーむの余白設計(正しい例)
```dart
ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 12), // ← 最小限
separatorBuilder: (context, index) => SizedBox(height: 4), // ← 細い隙間
itemBuilder: (context, index) => SakeCard(...),
)
```
**メリット**: 写真が大きい、迫力がある、Instagram的
### カードデザイン - 「フル幅カード」
```dart
// lib/widgets/sake_card.dart
class SakeCard extends StatelessWidget {
final SakeItem item;
const SakeCard({required this.item});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Card(
margin: EdgeInsets.zero, // ← カード自体のマージンはゼロ
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0), // ← 角丸なし(フル幅)
side: BorderSide(
color: Color(0xFFE8E8E8),
width: 0.5, // ← 繊細なボーダー
),
),
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Color(0xFFE8E8E8),
width: 0.5,
),
),
),
child: Padding(
padding: EdgeInsets.all(12), // ← カード内の余白は最小限
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左: 写真(できるだけ大きく)
ClipRRect(
borderRadius: BorderRadius.circular(8), // ← 写真だけ角丸
child: Image.network(
item.imagePath,
width: 120, // ← 大きく!
height: 120,
fit: BoxFit.cover,
),
),
SizedBox(width: 12),
// 右: テキスト情報
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 銘柄名(明朝体、大きく)
Text(
item.brandName ?? '日本酒',
style: GoogleFonts.notoSerifJp(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
// 酒蔵・産地
Text(
'${item.breweryName ?? ''} | ${item.prefecture ?? ''}',
style: GoogleFonts.notoSansJp(
fontSize: 11,
color: Color(0xFF8A8A8A),
),
),
SizedBox(height: 8),
// 評価
if (item.rating != null)
Row(
children: List.generate(
5,
(index) => Icon(
index < item.rating!.round()
? Icons.star
: Icons.star_border,
size: 16,
color: Color(0xFFFFC107),
),
),
),
SizedBox(height: 4),
// 種類
if (item.type != null)
Text(
item.type!,
style: GoogleFonts.notoSansJp(
fontSize: 11,
color: Color(0xFF4A4A4A),
),
),
// キャッチコピー
if (item.catchCopy != null) ...[
SizedBox(height: 6),
Text(
item.catchCopy!,
style: GoogleFonts.notoSerifJp(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Color(0xFF376495),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
),
),
);
}
}
```
**レイアウトイメージ**:
```
┌────────────────────────────────────┐
│ [120x120] │ 獺祭 ← 明朝体18px
│ 写真 │ 旭酒造 | 山口県 ← ゴシック体11px
│ 角丸8px │ ⭐⭐⭐⭐⭐ 純米大吟醸
│ │ "夜風と楽しみたい、淡麗な一滴"
└────────────────────────────────────┘
↑ 0.5pxのボーダーで区切り
```
---
## 🍶 Gemini 3.0統合(核心機能)
### APIキー設定
```dart
// lib/secrets.dart
class Secrets {
static const String geminiApiKey = 'AIzaSyA2BSr16R2k0bHjSYcSUdmLoY8PKwaFts0';
}
```
### リアルタイム実況付きGemini解析
```dart
// lib/services/gemini_service.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'dart:convert';
import '../secrets.dart';
class GeminiService {
late final GenerativeModel _model;
GeminiService() {
_model = GenerativeModel(
model: 'gemini-2.5-flash-latest', // スキャンアプリで実績あり
apiKey: Secrets.geminiApiKey,
generationConfig: GenerationConfig(
temperature: 0.1,
),
);
}
/// リアルタイム実況付きで解析
Stream<String> analyzeSakeLabelWithCommentary(Uint8List imageBytes) async* {
// ステージ1: 開始
yield 'ラベルを読んでいます...';
await Future.delayed(Duration(milliseconds: 500));
// ステージ2: 解析中
yield 'お酒の情報を確認しています...';
try {
final prompt = '''
この画像は日本酒のボトルまたはラベルの写真です。
ラベルに書かれているテキスト情報を正確に読み取り、JSON形式で返してください。
【重要な指示】
1. ラベルに明確に書かれている情報のみを抽出してください
2. 推測や想像で値を入れないでください読めない場合はnullまたは省略
3. 数値は必ず数字のみ(単位記号%などは除く)
4. このお酒の印象を一言で表す「キャッチコピー」を自動生成してください
【出力フォーマット】
{
"brandName": "銘柄名",
"type": "特定名称",
"alcoholContent": 数値,
"polishingRatio": 数値,
"breweryName": "酒蔵名",
"prefecture": "都道府県名",
"catchCopy": "夜風と楽しみたい、淡麗な一滴"
}
**JSON以外の余計な説明は一切不要です。JSONのみを出力してください。**
''';
final content = [
Content.multi([
TextPart(prompt),
DataPart('image/jpeg', imageBytes),
])
];
final response = await _model.generateContent(content);
final text = response.text ?? '';
// ステージ3: 都道府県を検出した場合
if (text.contains('県')) {
final prefectureMatch = RegExp(r'([^\s]+県)').firstMatch(text);
if (prefectureMatch != null) {
yield 'お、これは${prefectureMatch.group(1)}の銘柄ですね...';
await Future.delayed(Duration(milliseconds: 300));
}
}
// ステージ4: データ整理中
yield 'データを整理しています...';
await Future.delayed(Duration(milliseconds: 300));
// JSON抽出
final jsonMatch = RegExp(r'\{[\s\S]*\}').firstMatch(text);
if (jsonMatch != null) {
final jsonString = jsonMatch.group(0)!;
final data = jsonDecode(jsonString);
// ステージ5: 完了
yield '解析完了!';
// データを返す(特別な形式)
yield 'DATA:$jsonString';
} else {
yield 'エラー: データを読み取れませんでした';
}
} catch (e) {
yield 'エラー: $e';
}
}
/// シンプルな解析(実況なし)
Future<Map<String, dynamic>?> analyzeSakeLabel(Uint8List imageBytes) async {
try {
final prompt = '''
この画像は日本酒のボトルまたはラベルの写真です。
ラベルに書かれているテキスト情報を正確に読み取り、JSON形式で返してください。
【重要な指示】
1. ラベルに明確に書かれている情報のみを抽出してください
2. 推測や想像で値を入れないでください読めない場合はnullまたは省略
3. 数値は必ず数字のみ(単位記号%などは除く)
4. このお酒の印象を一言で表す「キャッチコピー」を自動生成してください
【出力フォーマット】
{
"brandName": "銘柄名",
"type": "特定名称",
"alcoholContent": 数値,
"polishingRatio": 数値,
"breweryName": "酒蔵名",
"prefecture": "都道府県名",
"catchCopy": "夜風と楽しみたい、淡麗な一滴"
}
**JSON以外の余計な説明は一切不要です。JSONのみを出力してください。**
''';
final content = [
Content.multi([
TextPart(prompt),
DataPart('image/jpeg', imageBytes),
])
];
final response = await _model.generateContent(content);
final text = response.text ?? '';
final jsonMatch = RegExp(r'\{[\s\S]*\}').firstMatch(text);
if (jsonMatch != null) {
final jsonString = jsonMatch.group(0)!;
return jsonDecode(jsonString);
}
return null;
} catch (e) {
debugPrint('Gemini API Error: $e');
return null;
}
}
}
```
### リアルタイム実況の使用例
```dart
// lib/screens/input_screen.dart一部
StreamBuilder<String>(
stream: geminiService.analyzeSakeLabelWithCommentary(imageBytes),
builder: (context, snapshot) {
if (snapshot.hasData) {
final message = snapshot.data!;
// データが返ってきた場合
if (message.startsWith('DATA:')) {
final jsonString = message.substring(5);
final data = jsonDecode(jsonString);
// フォームに自動入力
_fillForm(data);
return ResultForm(data: data);
}
// 実況メッセージを表示
return AnalyzingOverlay(currentStage: message);
}
return AnalyzingOverlay(currentStage: 'カメラを起動しています...');
},
)
```
---
## 📦 データモデル
### SakeItem (Hive Model)
```dart
// lib/models/sake_item.dart
import 'package:hive/hive.dart';
part 'sake_item.g.dart';
@HiveType(typeId: 0)
class SakeItem extends HiveObject {
@HiveField(0)
String? brandName;
@HiveField(1)
String? breweryName;
@HiveField(2)
String? prefecture;
@HiveField(3)
String? type;
@HiveField(4)
double? alcoholContent;
@HiveField(5)
int? polishingRatio;
@HiveField(6)
String imagePath;
@HiveField(7)
List<String>? additionalImages;
@HiveField(8)
double? rating;
@HiveField(9)
String? memo;
@HiveField(10)
List<String>? tags;
@HiveField(11)
bool isFavorite;
@HiveField(12)
bool isWishlist;
@HiveField(13)
DateTime createdAt;
@HiveField(14)
DateTime? updatedAt;
@HiveField(15)
int? price;
@HiveField(16)
int? volume;
@HiveField(17)
String? catchCopy; // AIが生成したキャッチコピー
@HiveField(18)
double? sweetnessScore; // -1.0(辛口)~ 1.0(甘口)
@HiveField(19)
double? bodyScore; // -1.0(淡麗)~ 1.0(濃醇)
SakeItem({
this.brandName,
this.breweryName,
this.prefecture,
this.type,
this.alcoholContent,
this.polishingRatio,
required this.imagePath,
this.additionalImages,
this.rating,
this.memo,
this.tags,
this.isFavorite = false,
this.isWishlist = false,
required this.createdAt,
this.updatedAt,
this.price,
this.volume,
this.catchCopy,
this.sweetnessScore,
this.bodyScore,
});
}
```
```bash
# 生成コマンド
flutter pub run build_runner build
```
---
## 🚀 実装チェックリスト
### Phase 1: MVP5時間
- [ ] プロジェクト作成(`flutter create ponshu_room_reborn`
- [ ] `pubspec.yaml` 設定
- [ ] Android設定compileSdk: 36, targetSdk: 35
- [ ] `lib/secrets.dart` 作成APIキー
- [ ] `lib/models/sake_item.dart` 作成
- [ ] `flutter pub run build_runner build`
- [ ] `lib/theme/app_theme.dart` 作成posimaiカラー
- [ ] `lib/main.dart` 作成Hive初期化
- [ ] `lib/screens/home/home_screen.dart` 作成4タブ
- [ ] `lib/services/gemini_service.dart` 作成(リアルタイム実況)
- [ ] `lib/widgets/sake_card.dart` 作成(フル幅カード)
- [ ] `lib/widgets/analyzing_overlay.dart` 作成実況UI
- [ ] カメラ撮影機能
- [ ] 入力フォーム
- [ ] 詳細画面
- [ ] **SafeArea対応** ← Android 15必須
### Phase 2: 「美録」UI洗練3時間
- [ ] 余白調整padding: 8dp, separator: 4dp
- [ ] 明朝体×ゴシック体の適用
- [ ] Hero遷移
- [ ] Instagram用画像生成機能
### Phase 3: 「遊び心」機能拡張4時間
- [ ] フレーバー・マトリックス
- [ ] 日本酒・制覇マップ + バッジ
- [ ] AIソムリエ質問例、チャット
- [ ] マイページ統計グラフ
### Phase 4: 共有機能2時間
- [ ] シンプルテキスト共有
- [ ] Instagram用正方形画像生成
- [ ] キャッチコピー付き共有
---
## ✅ 完成基準
### MVP完成の定義
- [ ] カメラで日本酒を撮影できる
- [ ] Gemini 3.0でリアルタイム実況しながら解析
- [ ] キャッチコピーが自動生成される
- [ ] データをHiveに保存できる
- [ ] フル幅カード写真120x120で表示
- [ ] 詳細表示できる
- [ ] SafeAreaで見切れない
- [ ] Android 15 (Xiaomi 14T Pro) で動作する
### 最終完成の定義
- [ ] すべての機能が動作
- [ ] フレーバー・マトリックス表示
- [ ] 日本酒・制覇マップ + バッジ
- [ ] Instagram用正方形画像生成
- [ ] 「雑誌のような」デザイン
- [ ] 60fpsの滑らかな動作
- [ ] 「魔法のような」心地よさ
---
## 📞 進捗報告
各フェーズ完了後に以下を報告してください:
```
Phase 1-1 完了: プロジェクト初期化
Phase 1-2 完了: データモデル
...
問題点:
- XXXでエラーが発生しました
- YYYの実装方法が不明です
質問:
- ZZZはどう実装すべきですか
```
---
**頑張ってください!「魔法のような体験」を一緒に作りましょう!🍶✨**

510
CommonSpecification.md Normal file
View File

@ -0,0 +1,510 @@
# ぽんるーむ (Pon-Room) - 共通仕様書 v1.0
**作成日**: 2026-01-03
**対象**: AI開発エージェントCursor/Antigravity/Claude/Gemini
**目的**: すべての開発者・AIエージェントが参照する唯一のバイブル
---
## 📋 1. プロジェクト概要
### 1.1 アプリケーション情報
- **アプリ名**: ぽんるーむ (Pon-Room)
- **コンセプト**: 日本酒の「記録・解析・循環」を支えるAIプラットフォーム
- **バージョン**: 1.0.9+47
- **最終更新**: 2026-01-03
### 1.2 ターゲットユーザー
- **B2C (Consumer Mode)**: 一般ユーザー
- 日本酒体験の記録・診断・共有
- ゲーミフィケーション(ポイント・バッジ・レベル)
- 「酒向(しゅこう)カード」による自己表現
- **B2B (Business Mode)**: 飲食店
- 自店の日本酒メニュー作成PDF出力
- QRコード自動埋め込み
- インスタ投稿支援
### 1.3 アプリの循環ロジック
```
[飲食店] PDF + QRメニュー作成
[客] スキャンして詳細表示・ポイント獲得
[客] 自分の記録が貯まる → 酒向カード生成
[飲食店] 客の好みを理解してレコメンド
```
---
## 🛠️ 2. 技術スタック
### 2.1 フロントエンド
- **Framework**: Flutter 3.10.1
- **SDK**: Dart 3.10.1
- **対応プラットフォーム**: iOS, Android, Web
### 2.2 バックエンド・データベース
- **Database**: Cloud Firestore (Firebase)
- **Authentication**: Firebase Auth
- **Storage**: Firebase Storage (画像保存)
### 2.3 AI・機械学習
- **Local OCR**: Google ML Kit
- 用途: ラベルからのテキスト抽出
- メリット: 無料、高速、オフライン動作
- **LLM Analysis**: Gemini API
- モデル: `gemini-2.5-flash` / `gemini-3.0-flash`
- 用途: テキストの構造化、キャッチコピー生成
- 重要: 画像を直接送らず、OCRテキストのみ送信トークン節約
### 2.4 主要パッケージ
```yaml
dependencies:
# 画像・カメラ
image_picker: ^1.2.1
gal: ^2.3.0 # カメラロール保存
# PDF生成
pdf: ^3.10.0
printing: ^5.11.0
# QRコード
qr_flutter: ^4.1.0
mobile_scanner: ^3.5.0 # QRスキャン
# データベース
hive: ^2.2.3
hive_flutter: ^1.1.0
# AI
google_generative_ai: ^0.4.7
google_ml_kit: ^0.16.0 # OCR用
```
---
## 📊 3. 共通データ構造JSON Schema
### 3.1 基本方針
- **display_data**: カードUIで表示する最小限の情報シンプル維持
- **hidden_specs**: 詳細画面・PDF・分析で使用する情報
- **badges**: ゲーミフィケーション要素
- **gamification**: ポイント・レベル・診断結果
- **user_data**: ユーザーの主観的情報
- **metadata**: システム管理用
### 3.2 SakeItem完全JSON構造
```json
{
"display_data": {
"name": "獺祭 純米大吟醸 磨き二割三分",
"catch_phrase": "華やかな香りと洗練された味わい",
"image_path": "path/to/image.jpg",
"rating": 4.5
},
"hidden_specs": {
"brewery": "旭酒造株式会社",
"prefecture": "山口県",
"type": "純米大吟醸",
"alcohol_content": 16.0,
"polishing_ratio": 23,
"sake_meter_value": 4.0,
"rice_variety": "山田錦",
"yeast": "自社酵母",
"manufacturing_year_month": "2024.10",
"qr_code_url": "https://pon-room.app/sake/abc123"
},
"badges": {
"is_recommended": false,
"is_seasonal": false,
"season_tag": "春限定"
},
"gamification": {
"pon_points": 10,
"sake_mbti_type": "フルーティー・モダン型",
"rarity_level": "レア"
},
"user_data": {
"is_favorite": false,
"is_wishlist": false,
"tags": ["甘口", "フルーティー", "冷酒向き"],
"memo": "お祝い事にぴったり。冷やして飲むのがおすすめ。",
"drink_location": "○○レストラン",
"companion": "△△さん",
"purchase_location": "××酒販店",
"price": 5000
},
"metadata": {
"created_at": "2026-01-03T12:34:56Z",
"updated_at": "2026-01-03T12:34:56Z",
"app_type": "sake",
"app_mode": "consumer",
"version": "1.0",
"scanned_count": 0
}
}
```
---
## 🎨 4. UI構成ルール
### 4.1 タブ構成5タブ制
| タブ | Consumer Mode (B2C) | Business Mode (B2B) |
|------|---------------------|---------------------|
| **タブ1** | 日本酒カードリスト(自分の記録) | 日本酒カードリスト(店舗在庫) |
| **タブ2** | QRスキャン・AR情報表示 | PDFメニュー作成・QR埋め込み |
| **タブ3** | AIソムリエ・酒向カード診断 | インスタ投稿支援AI文章生成 |
| **タブ4** | 酒蔵マップ(聖地巡礼) | 店舗設定・在庫管理 |
| **タブ5** | マイページ(レベル・バッジ・設定) | アナリティクス(スキャン統計) |
### 4.2 カード表示ルール(重要)
**表示項目display_dataのみ:**
- 銘柄名 (`display_data.name`)
- 画像 (`display_data.image_path`)
- 評価 (`display_data.rating`) - 星表示
- バッジ (`badges`) - 店長推奨・季節限定アイコン
**非表示項目(詳細画面でのみ使用):**
- `hidden_specs` の全項目
- `user_data` の詳細情報
**理由:** シンプルなUIを維持し、挫折を防ぐ
### 4.3 詳細画面の表示項目
**全て表示:**
- `display_data` 全項目
- `hidden_specs` 全項目(スペック表として)
- `badges` (アイコン付き)
- `user_data` 全項目
### 4.4 PDF出力の表示項目
**主要項目:**
- `display_data.name`, `image_path`
- `hidden_specs` の以下:
- type, alcohol_content, polishing_ratio
- sake_meter_value, brewery, prefecture
- `badges` (アイコン付き)
- QRコード`hidden_specs.qr_code_url` から生成)
---
## 🤖 5. AI解析フローHybrid Analysis
### 5.1 撮影から保存まで
```
1. 撮影
├─ ImagePicker or Camera パッケージ
└─ カメラロール自動保存gal パッケージ使用)★重要
2. OCRテキスト抽出
├─ Google ML Kitローカル・無料
├─ 画像 → 全テキスト抽出
└─ 抽出テキストのみを次へ
3. AI解析構造化
├─ 抽出テキストを Gemini API へ送信
├─ プロンプト: 「JSONフォーマットで返して」
└─ 上記のJSON構造で回答を取得
4. 保存
├─ Hiveローカルに即座保存
└─ Firebaseクラウドに同期将来実装
```
### 5.2 AI解析プロンプト例
```
あなたは日本酒のラベル解析の専門家です。
以下のOCRテキストから、日本酒の情報を抽出してJSON形式で回答してください。
【OCRテキスト】
{ocrText}
【出力ルール】
1. display_data: 銘柄名と魅力的なキャッチコピー(あなたが生成)
2. hidden_specs: 詳細スペック(読み取れた範囲で)
3. 読み取れない項目はnullにする
4. JSONのみを返す```jsonなどのマークダウン記法は不要
5. キャッチコピーは20文字以内で簡潔に
【出力フォーマット】
{JSON構造をここに記載}
```
### 5.3 コスト最適化のポイント
**❌ NG: 画像を直接Geminiに送信**
- 高トークン消費
- エラー発生率が高い
**✅ OK: OCRテキストのみ送信**
- トークン消費70-90%削減
- 安定した動作
- 後から再解析が不要
---
## 🎮 6. ゲーミフィケーション機能
### 6.1 ポンポイント(仮称)
**獲得条件:**
- 日本酒を記録: +5pt
- QRスキャン: +10pt
- 酒蔵訪問: +30pt位置情報連動
- 評価・レビュー投稿: +3pt
**レベルシステム:**
```
0-49pt: 利き酒初心者
50-149pt: 日本酒愛好家
150-299pt: 酒豪
300-499pt: 利き酒師
500pt+: 酒マスター
```
### 6.2 酒向(しゅこう)カード
**概要:** MBTIライクな自己診断カード
**診断軸(レーダーチャート):**
1. 甘口 ←→ 辛口
2. 濃醇 ←→ 淡麗
3. フルーティー ←→ 米の旨味
4. 冷酒 ←→ 熱燗
**表示項目:**
- 診断タイプ(例: フルーティー・モダン型)
- レーダーチャート
- 好きな銘柄TOP3
- AIの一言コメント
**用途:**
- 店員に見せて好みを伝える
- SNSシェア画像として保存
---
## 📱 7. QR循環ロジック
### 7.1 BtoB: PDFメニュー作成時
```
1. 飲食店が店舗在庫を登録
2. PDFメニュー生成時、各銘柄にQRコード埋め込み
3. QRコード内容: https://pon-room.app/sake/{sakeId}
4. 印刷して店内に設置
```
### 7.2 BtoC: QRスキャン時
```
1. 客がアプリでQRスキャン
2. 銘柄詳細をAR風に表示オーバーレイ
3. 「記録する」ボタンで自分のリストに追加
4. ポンポイント+10pt獲得
5. 酒蔵リンクへの誘導
```
### 7.3 循環の価値
- **客**: スキャンする度にポイントが貯まる
- **店**: 客の興味データが蓄積(アナリティクス)
- **蔵元**: ECサイトへの誘導で売上向上
---
## 🎓 8. オンボーディング(使い方ガイド)
### 8.1 初回起動時
**4ステップガイド:**
1. ようこそ!日本酒の記録を始めよう
2. カメラで撮影するだけでAIが自動解析
3. お気に入りを記録して自分だけのコレクションを
4. 飲食店の方はBusinessモードへ切り替え可能
### 8.2 再表示機能
- 各画面右上の「?」アイコン
- タップでガイドを再表示
### 8.3 Businessモード専用ガイド
**3ステップモード切り替え時のみ表示:**
1. 店舗情報を設定しましょう
2. メニューを作成してPDF出力
3. QRコードで客とつながる
---
## 🚀 9. 開発優先順位Phase別
### Phase 0: 基盤整備 ✅
- [x] CommonSpecification.md 作成
- [ ] SakeItemモデルの拡張
### Phase 1: 安心の確保1-2時間
- [ ] カメラロール保存実装gal
- [ ] iOS/Android権限設定
- [ ] 撮影後の自動保存フロー
### Phase 2: BtoB機能完成4-6時間
- [ ] PDF + printing 実装
- [ ] モックアップ厳密再現
- [ ] QRコード埋め込み
- [ ] レイアウト定数化
### Phase 3: AI最適化3-4時間
- [ ] Google ML Kit 導入
- [ ] OCRテキスト抽出
- [ ] Gemini APIへのテキスト送信切り替え
### Phase 4: ゲーミフィケーション後回しOK
- [ ] ポンポイントシステム
- [ ] 酒向カード生成
- [ ] QRスキャン機能
---
## 📐 10. レイアウト定数PDF用
```dart
class PDFLayoutConstants {
// 余白
static const double pageMargin = 24.0;
static const double sectionSpacing = 20.0;
static const double itemSpacing = 12.0;
// フォントサイズ
static const double titleFontSize = 24.0;
static const double headingFontSize = 14.0;
static const double bodyFontSize = 11.0;
static const double labelFontSize = 9.0;
// 線の太さ
static const double borderWidthThin = 0.5;
static const double borderWidthMedium = 1.0;
static const double borderWidthThick = 2.0;
// 色posimaiカラー
static final PdfColor borderColor = PdfColor.fromHex('#E2E8F0');
static final PdfColor labelColor = PdfColor.fromHex('#64748B');
static final PdfColor accentColor = PdfColor.fromHex('#376495');
}
```
---
## 🔐 11. セキュリティ・プライバシー
### 11.1 データ保存場所
**✅ ローカル保存:**
- Hive DB: アプリ専用ディレクトリ
- 写真: カメラロール(ユーザー端末)
**✅ 外部送信(一時的、保存されない):**
- Gemini API: OCRテキストのみ画像は送らない
**❌ 外部送信なし:**
- 個人情報の無断クラウド保存
- サードパーティへのデータ販売
### 11.2 将来のFirebase連携
- ユーザーが明示的に「同期」を選択した場合のみ
- 画像はFirebase Storageへ
- テキストデータはFirestoreへ
---
## 🎯 12. 量産対応(ワイン・ビールアプリへの展開)
### 12.1 拡張方法
`metadata.app_type` を変更するだけで転用可能:
- `sake` → 日本酒
- `wine` → ワイン
- `beer` → クラフトビール
- `whisky` → ウイスキー
### 12.2 共通化できる機能
- 撮影・OCR・AI解析フロー
- ポイントシステム
- PDF出力・QR循環
- マイページ・設定
### 12.3 差分化が必要な箇所
- JSONの `hidden_specs` フィールド
- ワイン: ぶどう品種、産地、ヴィンテージ
- ビール: ホップ、モルト、IBU値
- キャッチコピーのトーン
---
## 📝 13. AIエージェントへの指示
### 13.1 開発時の鉄則
1. **この仕様書を最優先のルール(バイブル)として参照せよ**
2. **display_data と hidden_specs を明確に分離せよ**
3. **カードUIはシンプルに保ち、display_dataのみ使用せよ**
4. **挫折防止のため、段階的に実装せよPhase順守**
5. **モックアップがある場合は1px単位で厳密再現せよ**
### 13.2 コード生成時の注意
- null安全性を徹底
- エラーハンドリングを統一
- デバッグログを適切に配置
- コメントは日本語で簡潔に
### 13.3 質問・提案時のルール
- 仕様書と矛盾する提案をする場合は理由を明記
- 新機能追加時はPhaseを提案
- データ構造変更時はJSON例を提示
---
## 📞 14. サポート・連絡先
**プロジェクトオーナー**: posimai
**開発支援AI**: Claude (Anthropic), Gemini (Google AI), ChatGPT (OpenAI)
**バージョン管理**: Git (GitHub)
**CI/CD**: Vercel (Web), Firebase Hosting
---
## 🎉 15. 最後に
この仕様書は「挫折しない日本酒アプリ開発」のために、複数のAIの知恵を結集して作成されました。
**重要な心構え:**
- シンプルから始める
- データは豊富に持つが、表示は最小限に
- ユーザーが「楽しい」と感じる体験を最優先に
**Let's build the best sake app together! 🍶✨**
---
**End of CommonSpecification.md v1.0**

510
CommonSpecification_bk.md Normal file
View File

@ -0,0 +1,510 @@
# ぽんるーむ (Pon-Room) - 共通仕様書 v1.0
**作成日**: 2026-01-03
**対象**: AI開発エージェントCursor/Antigravity/Claude/Gemini
**目的**: すべての開発者・AIエージェントが参照する唯一のバイブル
---
## 📋 1. プロジェクト概要
### 1.1 アプリケーション情報
- **アプリ名**: ぽんるーむ (Pon-Room)
- **コンセプト**: 日本酒の「記録・解析・循環」を支えるAIプラットフォーム
- **バージョン**: 1.0.9+47
- **最終更新**: 2026-01-03
### 1.2 ターゲットユーザー
- **B2C (Consumer Mode)**: 一般ユーザー
- 日本酒体験の記録・診断・共有
- ゲーミフィケーション(ポイント・バッジ・レベル)
- 「酒向(しゅこう)カード」による自己表現
- **B2B (Business Mode)**: 飲食店
- 自店の日本酒メニュー作成PDF出力
- QRコード自動埋め込み
- インスタ投稿支援
### 1.3 アプリの循環ロジック
```
[飲食店] PDF + QRメニュー作成
[客] スキャンして詳細表示・ポイント獲得
[客] 自分の記録が貯まる → 酒向カード生成
[飲食店] 客の好みを理解してレコメンド
```
---
## 🛠️ 2. 技術スタック
### 2.1 フロントエンド
- **Framework**: Flutter 3.10.1
- **SDK**: Dart 3.10.1
- **対応プラットフォーム**: iOS, Android, Web
### 2.2 バックエンド・データベース
- **Database**: Cloud Firestore (Firebase)
- **Authentication**: Firebase Auth
- **Storage**: Firebase Storage (画像保存)
### 2.3 AI・機械学習
- **Local OCR**: Google ML Kit
- 用途: ラベルからのテキスト抽出
- メリット: 無料、高速、オフライン動作
- **LLM Analysis**: Gemini API
- モデル: `gemini-2.5-flash` / `gemini-3.0-flash`
- 用途: テキストの構造化、キャッチコピー生成
- 重要: 画像を直接送らず、OCRテキストのみ送信トークン節約
### 2.4 主要パッケージ
```yaml
dependencies:
# 画像・カメラ
image_picker: ^1.2.1
gal: ^2.3.0 # カメラロール保存
# PDF生成
pdf: ^3.10.0
printing: ^5.11.0
# QRコード
qr_flutter: ^4.1.0
mobile_scanner: ^3.5.0 # QRスキャン
# データベース
hive: ^2.2.3
hive_flutter: ^1.1.0
# AI
google_generative_ai: ^0.4.7
google_ml_kit: ^0.16.0 # OCR用
```
---
## 📊 3. 共通データ構造JSON Schema
### 3.1 基本方針
- **display_data**: カードUIで表示する最小限の情報シンプル維持
- **hidden_specs**: 詳細画面・PDF・分析で使用する情報
- **badges**: ゲーミフィケーション要素
- **gamification**: ポイント・レベル・診断結果
- **user_data**: ユーザーの主観的情報
- **metadata**: システム管理用
### 3.2 SakeItem完全JSON構造
```json
{
"display_data": {
"name": "獺祭 純米大吟醸 磨き二割三分",
"catch_phrase": "華やかな香りと洗練された味わい",
"image_path": "path/to/image.jpg",
"rating": 4.5
},
"hidden_specs": {
"brewery": "旭酒造株式会社",
"prefecture": "山口県",
"type": "純米大吟醸",
"alcohol_content": 16.0,
"polishing_ratio": 23,
"sake_meter_value": 4.0,
"rice_variety": "山田錦",
"yeast": "自社酵母",
"manufacturing_year_month": "2024.10",
"qr_code_url": "https://pon-room.app/sake/abc123"
},
"badges": {
"is_recommended": false,
"is_seasonal": false,
"season_tag": "春限定"
},
"gamification": {
"pon_points": 10,
"sake_mbti_type": "フルーティー・モダン型",
"rarity_level": "レア"
},
"user_data": {
"is_favorite": false,
"is_wishlist": false,
"tags": ["甘口", "フルーティー", "冷酒向き"],
"memo": "お祝い事にぴったり。冷やして飲むのがおすすめ。",
"drink_location": "○○レストラン",
"companion": "△△さん",
"purchase_location": "××酒販店",
"price": 5000
},
"metadata": {
"created_at": "2026-01-03T12:34:56Z",
"updated_at": "2026-01-03T12:34:56Z",
"app_type": "sake",
"app_mode": "consumer",
"version": "1.0",
"scanned_count": 0
}
}
```
---
## 🎨 4. UI構成ルール
### 4.1 タブ構成5タブ制
| タブ | Consumer Mode (B2C) | Business Mode (B2B) |
|------|---------------------|---------------------|
| **タブ1** | 日本酒カードリスト(自分の記録) | 日本酒カードリスト(店舗在庫) |
| **タブ2** | QRスキャン・AR情報表示 | PDFメニュー作成・QR埋め込み |
| **タブ3** | AIソムリエ・酒向カード診断 | インスタ投稿支援AI文章生成 |
| **タブ4** | 酒蔵マップ(聖地巡礼) | 店舗設定・在庫管理 |
| **タブ5** | マイページ(レベル・バッジ・設定) | アナリティクス(スキャン統計) |
### 4.2 カード表示ルール(重要)
**表示項目display_dataのみ:**
- 銘柄名 (`display_data.name`)
- 画像 (`display_data.image_path`)
- 評価 (`display_data.rating`) - 星表示
- バッジ (`badges`) - 店長推奨・季節限定アイコン
**非表示項目(詳細画面でのみ使用):**
- `hidden_specs` の全項目
- `user_data` の詳細情報
**理由:** シンプルなUIを維持し、挫折を防ぐ
### 4.3 詳細画面の表示項目
**全て表示:**
- `display_data` 全項目
- `hidden_specs` 全項目(スペック表として)
- `badges` (アイコン付き)
- `user_data` 全項目
### 4.4 PDF出力の表示項目
**主要項目:**
- `display_data.name`, `image_path`
- `hidden_specs` の以下:
- type, alcohol_content, polishing_ratio
- sake_meter_value, brewery, prefecture
- `badges` (アイコン付き)
- QRコード`hidden_specs.qr_code_url` から生成)
---
## 🤖 5. AI解析フローHybrid Analysis
### 5.1 撮影から保存まで
```
1. 撮影
├─ ImagePicker or Camera パッケージ
└─ カメラロール自動保存gal パッケージ使用)★重要
2. OCRテキスト抽出
├─ Google ML Kitローカル・無料
├─ 画像 → 全テキスト抽出
└─ 抽出テキストのみを次へ
3. AI解析構造化
├─ 抽出テキストを Gemini API へ送信
├─ プロンプト: 「JSONフォーマットで返して」
└─ 上記のJSON構造で回答を取得
4. 保存
├─ Hiveローカルに即座保存
└─ Firebaseクラウドに同期将来実装
```
### 5.2 AI解析プロンプト例
```
あなたは日本酒のラベル解析の専門家です。
以下のOCRテキストから、日本酒の情報を抽出してJSON形式で回答してください。
【OCRテキスト】
{ocrText}
【出力ルール】
1. display_data: 銘柄名と魅力的なキャッチコピー(あなたが生成)
2. hidden_specs: 詳細スペック(読み取れた範囲で)
3. 読み取れない項目はnullにする
4. JSONのみを返す```jsonなどのマークダウン記法は不要
5. キャッチコピーは20文字以内で簡潔に
【出力フォーマット】
{JSON構造をここに記載}
```
### 5.3 コスト最適化のポイント
**❌ NG: 画像を直接Geminiに送信**
- 高トークン消費
- エラー発生率が高い
**✅ OK: OCRテキストのみ送信**
- トークン消費70-90%削減
- 安定した動作
- 後から再解析が不要
---
## 🎮 6. ゲーミフィケーション機能
### 6.1 ポンポイント(仮称)
**獲得条件:**
- 日本酒を記録: +5pt
- QRスキャン: +10pt
- 酒蔵訪問: +30pt位置情報連動
- 評価・レビュー投稿: +3pt
**レベルシステム:**
```
0-49pt: 利き酒初心者
50-149pt: 日本酒愛好家
150-299pt: 酒豪
300-499pt: 利き酒師
500pt+: 酒マスター
```
### 6.2 酒向(しゅこう)カード
**概要:** MBTIライクな自己診断カード
**診断軸(レーダーチャート):**
1. 甘口 ←→ 辛口
2. 濃醇 ←→ 淡麗
3. フルーティー ←→ 米の旨味
4. 冷酒 ←→ 熱燗
**表示項目:**
- 診断タイプ(例: フルーティー・モダン型)
- レーダーチャート
- 好きな銘柄TOP3
- AIの一言コメント
**用途:**
- 店員に見せて好みを伝える
- SNSシェア画像として保存
---
## 📱 7. QR循環ロジック
### 7.1 BtoB: PDFメニュー作成時
```
1. 飲食店が店舗在庫を登録
2. PDFメニュー生成時、各銘柄にQRコード埋め込み
3. QRコード内容: https://pon-room.app/sake/{sakeId}
4. 印刷して店内に設置
```
### 7.2 BtoC: QRスキャン時
```
1. 客がアプリでQRスキャン
2. 銘柄詳細をAR風に表示オーバーレイ
3. 「記録する」ボタンで自分のリストに追加
4. ポンポイント+10pt獲得
5. 酒蔵リンクへの誘導
```
### 7.3 循環の価値
- **客**: スキャンする度にポイントが貯まる
- **店**: 客の興味データが蓄積(アナリティクス)
- **蔵元**: ECサイトへの誘導で売上向上
---
## 🎓 8. オンボーディング(使い方ガイド)
### 8.1 初回起動時
**4ステップガイド:**
1. ようこそ!日本酒の記録を始めよう
2. カメラで撮影するだけでAIが自動解析
3. お気に入りを記録して自分だけのコレクションを
4. 飲食店の方はBusinessモードへ切り替え可能
### 8.2 再表示機能
- 各画面右上の「?」アイコン
- タップでガイドを再表示
### 8.3 Businessモード専用ガイド
**3ステップモード切り替え時のみ表示:**
1. 店舗情報を設定しましょう
2. メニューを作成してPDF出力
3. QRコードで客とつながる
---
## 🚀 9. 開発優先順位Phase別
### Phase 0: 基盤整備 ✅
- [x] CommonSpecification.md 作成
- [ ] SakeItemモデルの拡張
### Phase 1: 安心の確保1-2時間
- [ ] カメラロール保存実装gal
- [ ] iOS/Android権限設定
- [ ] 撮影後の自動保存フロー
### Phase 2: BtoB機能完成4-6時間
- [ ] PDF + printing 実装
- [ ] モックアップ厳密再現
- [ ] QRコード埋め込み
- [ ] レイアウト定数化
### Phase 3: AI最適化3-4時間
- [ ] Google ML Kit 導入
- [ ] OCRテキスト抽出
- [ ] Gemini APIへのテキスト送信切り替え
### Phase 4: ゲーミフィケーション後回しOK
- [ ] ポンポイントシステム
- [ ] 酒向カード生成
- [ ] QRスキャン機能
---
## 📐 10. レイアウト定数PDF用
```dart
class PDFLayoutConstants {
// 余白
static const double pageMargin = 24.0;
static const double sectionSpacing = 20.0;
static const double itemSpacing = 12.0;
// フォントサイズ
static const double titleFontSize = 24.0;
static const double headingFontSize = 14.0;
static const double bodyFontSize = 11.0;
static const double labelFontSize = 9.0;
// 線の太さ
static const double borderWidthThin = 0.5;
static const double borderWidthMedium = 1.0;
static const double borderWidthThick = 2.0;
// 色posimaiカラー
static final PdfColor borderColor = PdfColor.fromHex('#E2E8F0');
static final PdfColor labelColor = PdfColor.fromHex('#64748B');
static final PdfColor accentColor = PdfColor.fromHex('#376495');
}
```
---
## 🔐 11. セキュリティ・プライバシー
### 11.1 データ保存場所
**✅ ローカル保存:**
- Hive DB: アプリ専用ディレクトリ
- 写真: カメラロール(ユーザー端末)
**✅ 外部送信(一時的、保存されない):**
- Gemini API: OCRテキストのみ画像は送らない
**❌ 外部送信なし:**
- 個人情報の無断クラウド保存
- サードパーティへのデータ販売
### 11.2 将来のFirebase連携
- ユーザーが明示的に「同期」を選択した場合のみ
- 画像はFirebase Storageへ
- テキストデータはFirestoreへ
---
## 🎯 12. 量産対応(ワイン・ビールアプリへの展開)
### 12.1 拡張方法
`metadata.app_type` を変更するだけで転用可能:
- `sake` → 日本酒
- `wine` → ワイン
- `beer` → クラフトビール
- `whisky` → ウイスキー
### 12.2 共通化できる機能
- 撮影・OCR・AI解析フロー
- ポイントシステム
- PDF出力・QR循環
- マイページ・設定
### 12.3 差分化が必要な箇所
- JSONの `hidden_specs` フィールド
- ワイン: ぶどう品種、産地、ヴィンテージ
- ビール: ホップ、モルト、IBU値
- キャッチコピーのトーン
---
## 📝 13. AIエージェントへの指示
### 13.1 開発時の鉄則
1. **この仕様書を最優先のルール(バイブル)として参照せよ**
2. **display_data と hidden_specs を明確に分離せよ**
3. **カードUIはシンプルに保ち、display_dataのみ使用せよ**
4. **挫折防止のため、段階的に実装せよPhase順守**
5. **モックアップがある場合は1px単位で厳密再現せよ**
### 13.2 コード生成時の注意
- null安全性を徹底
- エラーハンドリングを統一
- デバッグログを適切に配置
- コメントは日本語で簡潔に
### 13.3 質問・提案時のルール
- 仕様書と矛盾する提案をする場合は理由を明記
- 新機能追加時はPhaseを提案
- データ構造変更時はJSON例を提示
---
## 📞 14. サポート・連絡先
**プロジェクトオーナー**: posimai
**開発支援AI**: Claude (Anthropic), Gemini (Google AI), ChatGPT (OpenAI)
**バージョン管理**: Git (GitHub)
**CI/CD**: Vercel (Web), Firebase Hosting
---
## 🎉 15. 最後に
この仕様書は「挫折しない日本酒アプリ開発」のために、複数のAIの知恵を結集して作成されました。
**重要な心構え:**
- シンプルから始める
- データは豊富に持つが、表示は最小限に
- ユーザーが「楽しい」と感じる体験を最優先に
**Let's build the best sake app together! 🍶✨**
---
**End of CommonSpecification.md v1.0**

View File

@ -0,0 +1,113 @@
CommonSpecification.md (v1.0)
1. プロジェクト概要
アプリ名: ぽんるーむPon-Room
コンセプト: 日本酒の「記録・解析・循環」を支えるAIアプリ。
ターゲット: - B2C: 一般ユーザー(自分の日本酒体験を記録・診断・共有)
B2B: 飲食店自店の日本酒メニューをPDF/QR付きで作成・印刷
2. 技術スタック
Frontend: Flutter (iOS/Android/Web)
Backend/DB: Cloud Firestore, Firebase Auth
AI/ML: - Local OCR: Google ML Kit (テキスト抽出)
LLM Analysis: Gemini API (2.5 Flash / 3 Flash)
Library: - gal: カメラロール保存用
pdf, printing: PDF生成・印刷用
qr_flutter: QRコード生成用
3. 共通データ構造 (JSON Schema)
すべてのAI解析およびデータ保存はこの形式に従うこと。
JSON
{
"display_data": {
"name": "銘柄名",
"catch_phrase": "AI生成のキャッチコピー",
"image_path": "local/path/to/image.jpg",
"rating": 4.5
},
"hidden_specs": {
"brewery": "蔵元名",
"prefecture": "都道府県",
"type": "特定名称(純米大吟醸等)",
"alcohol_content": 16.0,
"polishing_ratio": 23,
"rice_variety": "使用米",
"sake_meter_value": 0.0,
"qr_code_url": "https://pon-room.app/sake/12345"
},
"badges": {
"is_recommended": false,
"is_seasonal": false,
"season_tag": "春限定"
},
"gamification": {
"pon_points": 10,
"sake_mbti_type": "フルーティー・モダン型"
},
"user_data": {
"is_favorite": false,
"memo": "テイスティングメモ",
"created_at": "ISO8601形式"
},
"metadata": {
"app_type": "sake",
"version": "1.0"
}
}
4. 機能別ガイドライン
4.1 撮影・解析フロー (Hybrid Analysis)
撮影: 写真撮影後、即座に「カメラロール端末のギャラリー」へ保存するgalパッケージ使用
OCR: Google ML Kitで画像から生テキストを抽出。
AI解析: 抽出テキストのみをGeminiへ送信し、上記JSON形式で構造化データを取得。
注意: 画像を直接AIに送るのではなく、テキストを介することでトークン節約とエラー回避を行う。
4.2 UI構成 (5タブ・切り替え制)
タブ1 (リスト): カード形式。display_dataのみを表示し、極力シンプルに保つ。
タブ2 (スキャン/作成): - Consumerモード: QRスキャン他店メニューの読み取り・ポイント獲得
Businessモード: PDFメニュー作成QRコード自動埋め込み
タブ3 (AI/診断): AIソムリエ、酒蔵マップ将来拡張
タブ4 (マップ): 酒蔵巡りマップ(未実装)。
タブ5 (マイページ): - 酒向(しゅこう)カード: MBTI風の自己診断結果、バッジ、獲得ポイントを表示。
設定: 右上の「歯車アイコン」内にモード切り替え、使い方ガイド、システム設定を集約。
4.3 オンボーディング (Onboarding)
初回起動時に4枚のステップガイドを表示。
各画面右上の「?」アイコンでガイドを再表示。
Businessモード切り替え時は専用の3ステップガイドを追加表示。
4.4 B2B/B2C 循環ロジック
B2B: PDF出力時に各銘柄の qr_code_url をQRコードとして埋め込む。
B2C: そのQRをスキャンすると、詳細情報が表示され「ポンポイント」が貯まる仕組みを想定。
5. 開発優先順位 (Roadmap)
Phase 1: CommonSpecification.md に基づくデータモデルの再定義。
Phase 2: カメラロール保存機能の実装(データ消失リスク回避)。
Phase 3: PDF + printing 実装Antigravity評価用のB2B機能
Phase 4: OCR + Gemini API連携の最適化。
Phase 5: マイページ(酒向カード)およびゲーミフィケーション実装。

923
FINAL_REQUIREMENTS.md Normal file
View File

@ -0,0 +1,923 @@
# 新生ぽんるーむ - 最終完全要件定義書
**プロジェクト名**: 新生ぽんるーむ (Reborn Ponshu Room)
**バージョン**: 2.0 - "My Digital Sake Cellar"
**作成日**: 2025-12-29
**設計**: Claude (Anthropic) + Gemini (Google AI) + posimai
**実装担当**: Antigravity + Claude
---
## 🎯 プロジェクトビジョン
> **「目の前の一本を撮るだけで、魔法のようにデータが溜まっていく」**
### 3つの核心コンセプト
#### 🍶 1. 「瞬撮」- 魔法の解析体験
カメラを向けて撮るだけで、AIが全てを読み取る。
ユーザーはただ「撮る」だけ。
#### 🎨 2. 「美録」- インスタ映えする情報の見せ方
雑誌のようなレイアウトで、いつでも見返したくなる。
そのままインスタに投稿できる美しさ。
#### 🧩 3. 「遊び心」- 意味のあるデータ分析
自分の好みを「発見」する楽しさ。
日本全国制覇マップ、フレーバーマトリックス。
---
## 📱 技術スタック
### 必須要件
```yaml
Flutter SDK: 3.38.3+
Dart: 3.10.1+
Android:
compileSdk: 36
targetSdk: 35 (Android 15対応)
minSdk: 21 (Android 5.0+)
```
### 依存パッケージ
```yaml
dependencies:
flutter:
sdk: flutter
# 状態管理Riverpod - スキャンアプリで実績あり)
flutter_riverpod: ^2.6.1
hooks_riverpod: ^2.6.1
flutter_hooks: ^0.20.5
# ローカルDB
hive: ^2.2.3
hive_flutter: ^1.1.0
# AI解析Gemini 3.0
google_generative_ai: ^0.4.7
http: ^1.6.0
# カメラ・画像
camera: ^0.11.0+2
image_picker: ^1.2.1
image: ^4.3.0
# UI
google_fonts: ^6.2.1
fl_chart: ^1.1.1
# 共有・保存
share_plus: ^12.0.1
path_provider: ^2.1.5
# その他
intl: ^0.20.2
package_info_plus: ^9.0.0
url_launcher: ^6.3.2
countries_world_map: ^1.3.0
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
hive_generator: ^2.0.1
build_runner: ^2.4.13
```
---
## 🎨 UI/UXデザイン原則
### デザインコンセプト
**「雑誌のような洗練、魔法のような心地よさ」**
### カラーパレット
```dart
// posimaiブランドカラー
static const Color posimaiBlue = Color(0xFF376495);
// ベースカラー
static const Color warmOffWhite = Color(0xFFFAFAF9); // 背景
static const Color richBlack = Color(0xFF1A1A1A); // テキスト
static const Color charcoalGray = Color(0xFF4A4A4A); // サブテキスト
static const Color warmGray = Color(0xFF8A8A8A); // 補助テキスト
// アクセントカラー
static const Color dustyPink = Color(0xFFE8B4B8); // お気に入り
static const Color softYellow = Color(0xFFFEF3C7); // ウィッシュリスト
```
### タイポグラフィ
```dart
// 銘柄名・タイトル: 明朝体(格調高く)
headlineMedium: GoogleFonts.notoSerifJp(
fontSize: 20,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
color: richBlack,
)
// データ・ボディ: ゴシック体(読みやすく)
bodyMedium: GoogleFonts.notoSansJp(
fontSize: 13,
fontWeight: FontWeight.w400,
color: charcoalGray,
)
// ラベル・キャプション: ゴシック体
bodySmall: GoogleFonts.notoSansJp(
fontSize: 11,
fontWeight: FontWeight.w500,
color: warmGray,
)
```
---
## 🍶 1. 「瞬撮」- 魔法の解析体験
### APIキー設定
```dart
// lib/secrets.dart
class Secrets {
static const String geminiApiKey = 'AIzaSyA2BSr16R2k0bHjSYcSUdmLoY8PKwaFts0';
}
```
### Gemini 3.0統合
**重要**: 以下のいずれかを使用
```dart
// オプション1: 最新の3.0プレビュー(推奨)
model: 'gemini-3.0-flash-latest'
// オプション2: 安定版2.5 Flashスキャンアプリで実績
model: 'gemini-2.5-flash-latest'
```
### AIソムリエのリアルタイム実況
**解析中のUI**:
```dart
class AnalyzingOverlay extends StatelessWidget {
final String currentStage;
const AnalyzingOverlay({required this.currentStage});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withOpacity(0.7),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(posimaiBlue),
),
SizedBox(height: 24),
Text(
currentStage,
style: GoogleFonts.notoSansJp(
fontSize: 16,
color: Colors.white,
),
),
],
),
),
);
}
}
```
**ステージの例**:
```dart
[
'ラベルを読んでいます...',
'お、これは〇〇県の銘柄ですね...',
'精米歩合を確認中...',
'データを整理しています...',
]
```
### 自動ポエム生成
Geminiのレスポンスに以下を追加:
```dart
final prompt = '''
この画像は日本酒のボトルまたはラベルの写真です。
ラベルに書かれているテキスト情報を正確に読み取り、JSON形式で返してください。
【重要な指示】
1. ラベルに明確に書かれている情報のみを抽出してください
2. 推測や想像で値を入れないでください読めない場合はnullまたは省略
3. 数値は必ず数字のみ(単位記号%などは除く)
4. このお酒の印象を一言で表す「キャッチコピー」を自動生成してください
【出力フォーマット】
{
"brandName": "銘柄名",
"type": "特定名称",
"alcoholContent": 数値,
"polishingRatio": 数値,
"breweryName": "酒蔵名",
"prefecture": "都道府県名",
"catchCopy": "夜風と楽しみたい、淡麗な一滴" // ← 自動生成
}
**JSON以外の余計な説明は一切不要です。JSONのみを出力してください。**
''';
```
---
## 🎨 2. 「美録」- インスタ映えする情報の見せ方
### モダン・カタログ・カード
**Web版の縦並びを捨て、左右2段構成を採用**:
```dart
// lib/widgets/sake_card.dart
class SakeCard extends StatelessWidget {
final SakeItem item;
const SakeCard({required this.item});
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Color(0xFFE8E8E8), width: 1),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左: 写真角丸12px
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
item.imagePath,
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
SizedBox(width: 16),
// 右: テキスト情報
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 銘柄名(明朝体、大きく)
Text(
item.brandName ?? '日本酒',
style: GoogleFonts.notoSerifJp(
fontSize: 18,
fontWeight: FontWeight.w600,
color: richBlack,
),
),
SizedBox(height: 4),
// 酒蔵・産地(ゴシック体、控えめ)
Text(
'${item.breweryName ?? ''} | ${item.prefecture ?? ''}',
style: GoogleFonts.notoSansJp(
fontSize: 11,
color: warmGray,
),
),
SizedBox(height: 8),
// 評価
StarRating(rating: item.rating ?? 0),
SizedBox(height: 4),
// 種類
Text(
item.type ?? '',
style: GoogleFonts.notoSansJp(
fontSize: 11,
color: charcoalGray,
),
),
// キャッチコピーNEW!
if (item.catchCopy != null) ...[
SizedBox(height: 8),
Text(
item.catchCopy!,
style: GoogleFonts.notoSerifJp(
fontSize: 12,
fontStyle: FontStyle.italic,
color: posimaiBlue,
),
),
],
],
),
),
],
),
),
);
}
}
```
### インスタ専用・共有カード生成
**正方形1:1の画像を自動生成**:
```dart
// lib/services/instagram_share_service.dart
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
class InstagramShareService {
static Future<String> generateInstagramImage(SakeItem item) async {
// 1. 画像を読み込み
final imageBytes = await File(item.imagePath).readAsBytes();
final image = img.decodeImage(imageBytes)!;
// 2. 正方形1080x1080にクロップ
final size = 1080;
final square = img.copyResizeCropSquare(image, size: size);
// 3. 下半分にposimaiカラーのオーバーレイ
final overlay = img.Image(width: size, height: size ~/ 2);
img.fillRect(
overlay,
x1: 0,
y1: 0,
x2: size,
y2: size ~/ 2,
color: img.ColorRgb8(55, 100, 149), // posimaiBlue
);
// 4. オーバーレイを合成
img.compositeImage(
square,
overlay,
dstX: 0,
dstY: size ~/ 2,
);
// 5. テキストを描画(銘柄名、評価、ハッシュタグ)
// TODO: 白抜き明朝体でテキスト描画
// 6. 保存
final directory = await getTemporaryDirectory();
final path = '${directory.path}/instagram_${DateTime.now().millisecondsSinceEpoch}.jpg';
await File(path).writeAsBytes(img.encodeJpg(square));
return path;
}
static Future<void> shareToInstagram(SakeItem item) async {
// インスタ用画像を生成
final imagePath = await generateInstagramImage(item);
// 共有
await Share.shareXFiles(
[XFile(imagePath)],
text: '''
🍶 ${item.brandName ?? '日本酒'}
${item.catchCopy ?? ''}
#ぽんるーむ #日本酒 #${item.prefecture ?? ''} #日本酒好きと繋がりたい
''',
);
}
}
```
**レイアウトイメージ**:
```
┌────────────────────┐
│ │
│ [日本酒ラベル写真] │ ← 上半分540px
│ │
├────────────────────┤
│ posimaiブルー背景 │ ← 下半分540px
│ │
│ 獺祭(明朝体・白) │ ← 銘柄名
│ ⭐⭐⭐⭐⭐ │ ← 評価
│ │
│ 夜風と楽しみたい、 │ ← キャッチコピー
│ 淡麗な一滴 │
│ │
#ぽんるーむ │ ← ハッシュタグ
│ [logo] │ ← posimaiロゴ右下
└────────────────────┘
```
---
## 🧩 3. 「遊び心」- 意味のあるデータ分析
### フレーバー・マトリックス
**4象限チャートで味の傾向を可視化**:
```
甘口
濃醇 ←─┼─→ 淡麗
辛口
```
**実装**:
```dart
// lib/widgets/flavor_matrix.dart
class FlavorMatrix extends StatelessWidget {
final List<SakeItem> items;
const FlavorMatrix({required this.items});
@override
Widget build(BuildContext context) {
// AIが解析したフレーバータグから傾向を計算
// 例: "甘口"タグが多い → 甘口寄り
// "フルーティー"タグが多い → 淡麗寄り
return Container(
height: 200,
child: CustomPaint(
painter: FlavorMatrixPainter(
userPosition: _calculateUserPosition(),
),
),
);
}
Offset _calculateUserPosition() {
// ユーザーの好みを計算
// 甘口/辛口、濃醇/淡麗の2軸で位置を決定
return Offset(0.3, 0.6); // 例: やや甘口、やや淡麗
}
}
```
### 日本酒・制覇マップ
**都道府県マップをposimaiカラーで塗りつぶし**:
```dart
// lib/screens/home/map_tab.dart
SimpleMap(
instructions: SMapJapan.instructions,
defaultColor: warmGray, // 未踏破
colors: SMapJapanColors(
// データから自動計算
..._prefectureColors(),
).toMap(),
callback: (id, name, tapDetails) {
// タップで詳細表示
_showPrefectureDetail(name);
},
)
Map<String, Color> _prefectureColors() {
final counts = _countByPrefecture();
return counts.map((prefecture, count) {
if (count == 0) return MapEntry(prefecture, warmGray);
if (count < 3) return MapEntry(prefecture, posimaiBlue.withOpacity(0.3));
return MapEntry(prefecture, posimaiBlue);
});
}
```
**バッジ表示**:
```dart
// マップの上部に表示
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
'制覇: ${_completedPrefectures()} / 47',
style: GoogleFonts.notoSerifJp(
fontSize: 24,
fontWeight: FontWeight.bold,
color: posimaiBlue,
),
),
SizedBox(height: 8),
Text(
'あと${47 - _completedPrefectures()}県で全国制覇!',
style: GoogleFonts.notoSansJp(
fontSize: 13,
color: charcoalGray,
),
),
],
),
)
```
### Synology バックアップ・ステータス
**マイページの右上に控えめに表示**:
```dart
// lib/screens/home/profile_tab.dart
Positioned(
top: 16,
right: 16,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.green[50],
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.green[200]!),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cloud_done, size: 16, color: Colors.green[700]),
SizedBox(width: 4),
Text(
'Home Lab Sync: OK',
style: GoogleFonts.notoSansJp(
fontSize: 11,
color: Colors.green[700],
fontWeight: FontWeight.w500,
),
),
],
),
),
)
```
---
## 📦 データモデル(更新版)
### SakeItem (Hive Model)
**キャッチコピーを追加**:
```dart
import 'package:hive/hive.dart';
part 'sake_item.g.dart';
@HiveType(typeId: 0)
class SakeItem extends HiveObject {
// 基本情報
@HiveField(0)
String? brandName;
@HiveField(1)
String? breweryName;
@HiveField(2)
String? prefecture;
@HiveField(3)
String? type;
@HiveField(4)
double? alcoholContent;
@HiveField(5)
int? polishingRatio;
// 画像
@HiveField(6)
String imagePath;
@HiveField(7)
List<String>? additionalImages;
// 評価・メモ
@HiveField(8)
double? rating;
@HiveField(9)
String? memo;
@HiveField(10)
List<String>? tags;
// フラグ
@HiveField(11)
bool isFavorite;
@HiveField(12)
bool isWishlist;
// 日時
@HiveField(13)
DateTime createdAt;
@HiveField(14)
DateTime? updatedAt;
// 価格(オプション)
@HiveField(15)
int? price;
@HiveField(16)
int? volume;
// 🆕 AIが生成したキャッチコピー
@HiveField(17)
String? catchCopy;
// 🆕 フレーバープロファイル(甘口/辛口、濃醇/淡麗)
@HiveField(18)
double? sweetnessScore; // -1.0(辛口)~ 1.0(甘口)
@HiveField(19)
double? bodyScore; // -1.0(淡麗)~ 1.0(濃醇)
SakeItem({
this.brandName,
this.breweryName,
this.prefecture,
this.type,
this.alcoholContent,
this.polishingRatio,
required this.imagePath,
this.additionalImages,
this.rating,
this.memo,
this.tags,
this.isFavorite = false,
this.isWishlist = false,
required this.createdAt,
this.updatedAt,
this.price,
this.volume,
this.catchCopy,
this.sweetnessScore,
this.bodyScore,
});
}
```
---
## 🚀 実装優先順位
### Phase 1: MVP5時間
#### チェックリスト
- [ ] プロジェクト初期化(`flutter create ponshu_room_reborn`
- [ ] Android設定compileSdk: 36, targetSdk: 35
- [ ] 依存関係追加
- [ ] Hiveセットアップ
- [ ] SakeItemモデルキャッチコピー含む
- [ ] posimaiテーマ
- [ ] ホーム画面骨組み4タブ
- [ ] **Gemini 3.0解析 + リアルタイム実況**
- [ ] カメラ撮影
- [ ] 入力フォーム
- [ ] 詳細画面
- [ ] SafeArea対応
### Phase 2: 「美録」UI洗練3時間
#### チェックリスト
- [ ] モダン・カタログ・カード左右2段構成
- [ ] 明朝体×ゴシック体の適用
- [ ] インスタ専用画像生成機能
- [ ] Hero遷移
- [ ] アニメーション200-300ms
### Phase 3: 「遊び心」機能拡張4時間
#### チェックリスト
- [ ] フレーバー・マトリックス
- [ ] 日本酒・制覇マップ + バッジ
- [ ] Synologyバックアップ・ステータス表示
- [ ] AIソムリエ質問例、チャット形式
- [ ] マイページ統計グラフ
- [ ] 検索・フィルタ・ソート
### Phase 4: 共有機能2時間
#### チェックリスト
- [ ] シンプルテキスト共有
- [ ] Instagram用正方形画像生成
- [ ] キャッチコピー付き共有
**合計所要時間**: 14時間
---
## 📐 画面構成
### ホーム画面HomeScreen
#### ボトムナビゲーション
```
┌─────┬─────┬─────┬─────┐
│ 🍶 │ 🗺️ │ 🤖 │ 👤 │
│ 酒 │ マップ│ AI │ MY │
└─────┴─────┴─────┴─────┘
```
#### タブ1: 酒リストListTab
**モダン・カタログ・カード**:
```
┌────────────────────────────┐
│ [100x100] │ 獺祭 │ ← 明朝体、大きく
│ 写真 │ 旭酒造 | 山口県 │ ← ゴシック体、控えめ
│ 角丸12px │ ⭐⭐⭐⭐⭐ 純米大吟醸 │
│ │ "夜風と楽しみたい、淡麗な一滴" │ ← キャッチコピー
└────────────────────────────┘
```
#### タブ2: マップMapTab
**日本酒・制覇マップ**:
- 未踏破: warmGray
- 3本未満: posimaiBlue (30% opacity)
- 3本以上: posimaiBlue (100%)
- バッジ: 「制覇: 5 / 47」「あと42県で全国制覇
#### タブ3: AIソムリエAiTab
**質問例ボタン**:
```dart
Wrap(
spacing: 8,
children: [
OutlinedButton(
child: Text('純米大吟醸とは?'),
onPressed: () => _askAI('純米大吟醸について詳しく教えて'),
),
OutlinedButton(
child: Text('山田錦について教えて'),
onPressed: () => _askAI('山田錦という酒米の特徴を教えて'),
),
OutlinedButton(
child: Text('刺身に合う日本酒は?'),
onPressed: () => _askAI('刺身に合う日本酒のおすすめを教えて'),
),
OutlinedButton(
child: Text('初心者におすすめの銘柄'),
onPressed: () => _askAI('日本酒初心者におすすめの銘柄を教えて'),
),
],
)
```
#### タブ4: マイページProfileTab
**セクション構成**:
1. **Synologyバックアップステータス**(右上)
```
🏠 Home Lab Sync: OK
```
2. **酒蔵サマリー**
```
┌─────────┬─────────┬─────────┐
│ 1 │ 0 │ 0 │
│ 飲んだ本数│お気に入り│ 買いたい │
└─────────┴─────────┴─────────┘
```
3. **フレーバー・マトリックス**
```
甘口
│ ●(あなた)
濃醇 ←─┼─→ 淡麗
辛口
あなたが選ぶ酒は、フルーティーな甘口に偏っています
```
4. **よく飲む都道府県**
```
🥇 青森県 ━━━━━━━━━━ 1本
```
5. **飲酒傾向グラフ**
- 月別飲酒本数直近6ヶ月
- 評価分布1-5星
---
## 🔐 プライバシー・セキュリティ
### データ保存場所
#### デフォルト(全ユーザー)
```
✅ ローカルHive DBのみ
✅ 写真はアプリ専用ディレクトリ
❌ 外部サーバーへの送信なし
```
#### オプションposimai専用
```
✅ Synology NAS連携WebDAV/FTP
✅ 自動バックアップ
✅ "Home Lab Sync: OK" ステータス表示
```
---
## 📄 ファイル構成
```
lib/
├── main.dart
├── secrets.dart
├── models/
│ ├── sake_item.dart
│ └── sake_item.g.dart
├── providers/
│ ├── sake_repository_provider.dart
│ ├── gemini_provider.dart
│ └── camera_provider.dart
├── services/
│ ├── hive_service.dart
│ ├── gemini_service.dart
│ ├── share_service.dart
│ └── instagram_share_service.dart ← 🆕
├── screens/
│ ├── home/
│ │ ├── home_screen.dart
│ │ ├── list_tab.dart
│ │ ├── map_tab.dart
│ │ ├── ai_tab.dart
│ │ └── profile_tab.dart
│ ├── detail_screen.dart
│ └── input_screen.dart
├── widgets/
│ ├── sake_card.dart ← 🆕 左右2段構成
│ ├── star_rating.dart
│ ├── prefecture_dropdown.dart
│ ├── tag_chip.dart
│ ├── flavor_matrix.dart ← 🆕
│ └── analyzing_overlay.dart ← 🆕 リアルタイム実況
└── theme/
└── app_theme.dart
```
---
## ✅ 完成基準
### MVP完成の定義
- [ ] カメラで日本酒を撮影できる
- [ ] Gemini 3.0でリアルタイム実況しながら解析
- [ ] キャッチコピーが自動生成される
- [ ] データをHiveに保存できる
- [ ] モダン・カタログ・カード左右2段で表示
- [ ] 詳細表示できる
- [ ] SafeAreaで見切れない
- [ ] Android 15 (Xiaomi 14T Pro) で動作する
### 最終完成の定義
- [ ] すべての機能が動作
- [ ] フレーバー・マトリックス表示
- [ ] 日本酒・制覇マップ + バッジ
- [ ] Instagram用正方形画像生成
- [ ] Synologyバックアップステータス表示
- [ ] 「雑誌のような」デザイン
- [ ] 60fpsの滑らかな動作
- [ ] 「魔法のような」心地よさ
---
**最終更新**: 2025-12-29
**設計**: Claude (Anthropic) + Gemini (Google AI) + posimai
**実装担当**: Antigravity + Claude
**Let's create the magic! 🍶✨**

177
GEMINI_PRO_SETUP.md Normal file
View File

@ -0,0 +1,177 @@
# Gemini Pro API セットアップガイド
## 現在の状況
### Google One Proについて
**重要:** Google One Pro会員の特典とGemini APIの料金は**別物**です。
- **Google One Pro特典**: Gmail、Docs、DriveでのGemini機能が使える
- **Gemini API (開発者向け)**: アプリ開発用のAPI、無料枠と有料枠がある
**このアプリで使用しているのは開発者向けGemini APIです。**
---
## API制限の解除方法
### 方法1: 時間を待つ(無料)
現在レート制限エラーが出ている場合:
1. **1-2分待つ** - RPM(15回/分)制限は1分で解除
2. **アプリを再起動** - 内部カウンターがリセット
3. **5秒以上間隔を空けて解析** - 自動的に待機するようになりました
### 方法2: 別のGoogleアカウントを使用
新しいAPIキーを取得:
1. 別のGoogleアカウントでログイン
2. [Google AI Studio](https://aistudio.google.com/apikey) にアクセス
3. 新しいAPIキーを生成
4. `lib/secrets.dart` のAPIキーを差し替え
5. アプリを再ビルド
**注意:** 同じIPアドレスから使用すると制限が共有される可能性があります。
---
## Gemini API 有料プランPay-as-you-goへの移行
### 重要な説明
**有料プラン ≠ より高性能なモデル**
有料プランにすると得られるのは:
- ✅ **RPMリクエスト数制限の大幅緩和**: 15回/分 → 1,000回/分
- ✅ **TPMトークン数制限の緩和**: 100万/分 → 400万/分
- ❌ **モデルの性能向上ではない**
**モデルの違い:**
| モデル | 特徴 | 速度 | 精度 | 料金 |
|--------|------|------|------|------|
| **gemini-2.5-flash** (現在使用中) | 高速・軽量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 無料枠: 15RPM<br>有料: $0.075/1M入力 |
| **gemini-2.5-pro** | 高精度・重い | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 無料枠: 2RPM<br>有料: $1.25/1M入力 |
**推奨:**
- **このアプリでは `gemini-2.5-flash` で十分な精度**が出ています
- モデルは変更せず、**有料プランに移行するだけで制限が解除**されます
- Pro版は約17倍高額なので、必要性がない限り不要
### 料金体系Pay-as-you-go
#### gemini-2.5-flash推奨
- **無料版**: 15回/分、100万トークン/分
- **有料版**: 1,000回/分、400万トークン/分
- **料金**: $0.075/100万入力トークン、$0.30/100万出力トークン
**実際のコスト例:**
- 日本酒画像1枚解析 ≒ 5万トークン入力 = **約$0.004 (約0.6円)**
- 100枚解析しても **約60円**
#### gemini-2.5-pro高精度が必要な場合のみ
- **無料版**: 2回/分、3.2万トークン/分
- **有料版**: 1,000回/分、400万トークン/分
- **料金**: $1.25/100万入力トークンflashの約17倍
### 有料版への移行手順
#### 1. Google AI Studioで課金設定
```
1. https://aistudio.google.com/ にアクセス
2. 左メニュー「Billing」をクリック
3. 「Enable Pay-as-you-go」を選択
4. クレジットカード情報を登録
5. 利用上限を設定(例: 月$10まで
```
#### 2. アプリのモデル設定(変更不要)
`lib/services/gemini_service.dart` の18行目:
```dart
// 現在の設定(推奨: このまま)
static const String _modelName = 'gemini-2.5-flash';
// より高精度が必要な場合のみ17倍高額
// static const String _modelName = 'gemini-2.5-pro';
```
**注意:** モデルを変更しなくても、課金設定するだけで**制限が大幅に緩和**されます
#### 3. アプリを再ビルド(モデル変更時のみ)
```bash
# モデルを変更した場合のみ必要
flutter clean
flutter build apk --release
```
**注意:** 課金設定だけなら**再ビルド不要**です。同じAPKで制限が緩和されます。
---
## 現在の制限対策(無料版)
アプリに実装済みの対策:
### 1. 自動レート制限保護
- **5秒間隔の強制**: 連続解析時に自動的に待機
- トークン消費量のログ出力(デバッグ時)
### 2. 画像サイズの最適化
- カメラ解像度: **high (1080p) → medium (720p)**
- ファイルサイズ約50%削減
- 認識精度は維持
### 3. ユーザー向け情報表示
- ホーム画面に「ℹ️」アイコン追加
- API制限の詳細説明
- 推奨事項の表示
### 4. 詳細なエラーメッセージ
```
AI使用制限に達しました。
無料版は1分間に15回までの制限があります。
1〜2分後に再度お試しください。
```
---
## おすすめの運用方法
### 無料版で運用する場合
- ✅ **5秒以上間隔を空けて解析**(自動化済み)
- ✅ 同じ画像を再解析しない
- ✅ エラーが出たら1-2分待つ
- ✅ 1日あたり100-200枚程度まで
### 有料版に移行する場合(推奨)
- ✅ 月数百円で制限をほぼ気にせず使える
- ✅ RPM 1,000回/分 → 実質無制限
- ✅ ビジネス利用に最適
---
## トラブルシューティング
### Q: 新しいAPIキーでもすぐエラーが出る
A: 同じIPアドレスから利用している可能性があります。モバイルデータ通信に切り替えてテストしてください。
### Q: Google One Proで無制限にならないの
A: Google One ProはGmail/Docs用の特典です。API料金は別途発生します。
### Q: 有料版にしたらいくらかかる?
A: 1日10枚程度なら月100円以下、100枚/日でも月700円程度です。
### Q: APIキーが流出したらどうなる
A: Google AI Studioで即座に無効化し、新しいキーを発行してください。
---
## 参考リンク
- [Google AI Studio](https://aistudio.google.com/)
- [Gemini API料金表](https://ai.google.dev/pricing)
- [API使用量の確認](https://aistudio.google.com/quota)
---
**最終更新:** 2025-12-31

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# ponshu_room_lite
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

177
START_HERE.md Normal file
View File

@ -0,0 +1,177 @@
# 🚀 新生ぽんるーむ - スタートガイド
**プロジェクト名**: 新生ぽんるーむ (Reborn Ponshu Room)
**作成日**: 2025-12-29
**このフォルダの状態**: 完全にクリーン(設計書のみ)
---
## ✅ このフォルダの状況
```
現在のフォルダ: C:\Users\maita\posimai-project\ponshu_room_reborn
含まれるもの:
✅ FINAL_REQUIREMENTS.md最終仕様書
✅ ANTIGRAVITY_PROMPT.md実装手順書
✅ UI_UX_DECISION_GUIDE.mdデザイン決定ガイド
✅ START_HERE.mdこのファイル
含まれないもの:
❌ 古いWeb版のコード完全に除外
❌ lib/ フォルダ
❌ pubspec.yaml
❌ 何のコードもありません
```
**これは意図的です。完全にゼロから始めます。**
---
## 🎯 Antigravity向け - 最終プロンプト
以下をAntigravityに送ってください
---
### 📝 Antigravityへの指示
```
@Antigravity
新生ぽんるーむをゼロから実装します。
このフォルダponshu_room_rebornは完全にクリーンです。
📂 現在のフォルダ:
C:\Users\maita\posimai-project\ponshu_room_reborn
📄 読み込むべきドキュメント:
1. ANTIGRAVITY_PROMPT.mdメイン実装手順
2. FINAL_REQUIREMENTS.md完全仕様書
3. UI_UX_DECISION_GUIDE.mdデザインガイド
🔑 新しいAPIキー:
AIzaSyA2BSr16R2k0bHjSYcSUdmLoY8PKwaFts0
🎨 デザイン方針:
- 余白は最小限padding: 8dp, separator: 4dp
- 写真を主役にする120x120
- フル幅カード
- 極細ボーダー0.5px)で区切り
⚠️ 絶対にやってはいけないこと:
❌ ../ponshu-room/ フォルダを参照しない
❌ 古いコードをコピーしない
❌ Web版のUIを踏襲しない
✅ やるべきこと:
1. このフォルダで flutter create . を実行
2. ANTIGRAVITY_PROMPT.md の手順に従って実装
3. Gemini 2.5-flash-latest または gemini-3.0-flash-latest を使用
4. リアルタイム実況付きAI解析を実装
5. SafeAreaを徹底使用Android 15対応
🚀 開始:
ANTIGRAVITY_PROMPT.md の「プロジェクト作成」セクションから開始してください。
Phase 1-1プロジェクト初期化完了後に報告をお願いします。
```
---
## 📋 実装の流れ
### Phase 0: 準備(今ここ)
- [x] 新しいフォルダ作成
- [x] 設計書をコピー
- [ ] Antigravityに指示を送る
### Phase 1: MVP5時間
- [ ] プロジェクト初期化(`flutter create .`
- [ ] pubspec.yaml設定
- [ ] Android設定compileSdk: 36
- [ ] secrets.dart作成新APIキー
- [ ] Hiveセットアップ
- [ ] Gemini解析リアルタイム実況
- [ ] フル幅カード
- [ ] SafeArea対応
### Phase 2: 美録3時間
- [ ] Instagram用画像生成
- [ ] Hero遷移
### Phase 3: 遊び心4時間
- [ ] フレーバー・マトリックス
- [ ] 日本酒・制覇マップ
### Phase 4: 共有2時間
- [ ] キャッチコピー付き共有
---
## 🔐 APIキーの設定
Antigravityが `lib/secrets.dart` を作成した後、以下が正しく設定されているか確認してください:
```dart
// lib/secrets.dart
class Secrets {
static const String geminiApiKey = 'AIzaSyA2BSr16R2k0bHjSYcSUdmLoY8PKwaFts0';
}
```
---
## ✅ 完成確認チェックリスト
### MVP完成の確認
- [ ] カメラで日本酒を撮影できる
- [ ] Gemini解析で「ラベルを読んでいます...」と表示される
- [ ] キャッチコピーが自動生成される
- [ ] フル幅カード写真120x120で表示される
- [ ] Android 15 (Xiaomi 14T Pro) で見切れない
- [ ] データがHiveに保存される
### 最終完成の確認
- [ ] すべての機能が動作
- [ ] 「雑誌のような」デザイン
- [ ] 60fpsの滑らかな動作
- [ ] 「魔法のような」心地よさ
---
## 📞 トラブルシューティング
### もしAntigravityが古いコードを参照しようとしたら
**即座に指摘してください**:
```
@Antigravity
古い ponshu-room フォルダは参照しないでください。
このフォルダponshu_room_reborn内のドキュメントだけを見てください。
```
### もし余白が大きすぎたら
**即座に修正を依頼してください**:
```
@Antigravity
余白が大きすぎます。以下に修正してください:
- ListView.padding: EdgeInsets.symmetric(horizontal: 8, vertical: 12)
- separatorBuilder: SizedBox(height: 4)
- 写真サイズ: 120x120
```
---
## 🎊 準備完了!
すべての準備が整いました。
**次のアクション**:
1. 上記の「Antigravityへの指示」をコピー
2. Antigravityに送信
3. 実装開始を待つ
**新生ぽんるーむの誕生を楽しみにしています!🍶✨**

397
START_HERE_FINAL.md Normal file
View File

@ -0,0 +1,397 @@
# 🚀 ぽんるーむ Lite - 完全スタートガイド
**プロジェクト名**: ぽんるーむ Lite (Ponshu Room Lite)
**バージョン**: 2.0 - "My Digital Sake Cellar with Personality"
**作成日**: 2025-12-29
**このフォルダの状態**: 完全にクリーン(設計書のみ)
---
## 💡 「Lite」の意味
**「Lite」= 機能制限ではなく、「洗練されたコア体験」**
- ✅ 写真を主役にした雑誌のようなUI
- ✅ 完全ローカル保存(プライバシー最優先)
- ✅ MBTI・四柱推命と日本酒を掛け合わせた「あなただけの診断」
- ✅ フォント切り替え機能(明朝体 ⇔ ゴシック体)
**将来の「Pro」版**:
- Synology NAS連携
- 高度な統計分析
- チーム共有機能
---
## 🎯 このアプリの3つの魂
### 🍶 1. 「瞬撮」- 魔法の解析体験
カメラを向けて撮るだけで、Gemini 3.0がリアルタイム実況しながら解析。
```
「ラベルを読んでいます...」
「お、これは山口県の銘柄ですね...」
「データを整理しています...」
```
### 🎨 2. 「美録」- インスタ映えする情報の見せ方
雑誌のようなレイアウト、選べるフォント、Instagram用正方形画像生成。
### 🧩 3. 「遊び心」- あなただけのパーソナライズ
- **MBTI × 日本酒相性診断**: 「このお酒はENFPのあなたにピッタリ
- **四柱推命 × 今日の一滴**: 「今日の運気を上げる辛口な一本」
- **フレーバー・マトリックス**: あなたの好みを可視化
---
## ✅ このフォルダの状況
```
現在のフォルダ: C:\Users\maita\posimai-project\ponshu_room_lite
含まれるもの:
✅ FINAL_REQUIREMENTS.md最終仕様書
✅ ANTIGRAVITY_PROMPT.md実装手順書
✅ UI_UX_DECISION_GUIDE.mdデザイン決定ガイド
✅ START_HERE_FINAL.mdこのファイル
含まれないもの:
❌ 古いWeb版のコード完全に除外
❌ lib/ フォルダ
❌ pubspec.yaml
❌ 何のコードもありません
```
**これは意図的です。完全にゼロから始めます。**
---
## 🎯 Antigravity向け - 最終完全プロンプト
以下をAntigravityに送ってください
---
```
@Antigravity
ぽんるーむ Lite をゼロから実装します。
このフォルダponshu_room_liteは完全にクリーンです。
📂 現在のフォルダ:
C:\Users\maita\posimai-project\ponshu_room_lite
📄 読み込むべきドキュメント:
1. START_HERE_FINAL.md最初に読む
2. ANTIGRAVITY_PROMPT.mdメイン実装手順
3. FINAL_REQUIREMENTS.md完全仕様書
🔑 新しいAPIキー:
AIzaSyA2BSr16R2k0bHjSYcSUdmLoY8PKwaFts0
🎨 デザイン方針Lite版の魂:
【写真最大化】
- 余白は最小限padding: 8dp, separator: 4dp
- 写真サイズ: 120x120
- フル幅カード
- 極細ボーダー0.5px)で区切り
【フォント戦略】
- デフォルト: Noto Sans JP親しみやすいゴシック体
- 設定で切り替え可能: Noto Serif JP高級感のある明朝体
- ユーザーが好みで選べるようにする
【パーソナライズ機能の準備】
- UserProfileモデルを作成:
- MBTI診断結果16タイプ
- 生年月日(四柱推命用)
- 好みのフォント設定
- これらは Phase 2以降で使用今は枠だけ準備
⚠️ 絶対にやってはいけないこと:
❌ ../ponshu-room/ フォルダを参照しない
❌ 古いコードをコピーしない
❌ Web版のUIを踏襲しない
✅ やるべきことPhase 1:
【1-1. プロジェクト初期化】
1. このフォルダで flutter create . を実行
2. pubspec.yaml を設定google_fonts, hive, riverpod, etc.
3. Android設定compileSdk: 36, targetSdk: 35
【1-2. データモデル】
1. SakeItem モデル:
- 基本情報銘柄名、酒蔵、都道府県、etc.
- キャッチコピーGeminiが自動生成
- フレーバースコアsweetnessScore, bodyScore
2. UserProfile モデル(🆕):
- mbtiType: String? // 例: "ENFP"
- birthDate: DateTime? // 四柱推命用
- fontPreference: String // "serif" or "sans"
- createdAt: DateTime
【1-3. テーマ設定】
1. AppTheme を作成:
- posimaiカラー#376495
- 動的フォント切り替え対応
- デフォルトは Noto Sans JP
2. FontProviderRiverpodを作成:
- UserProfileからフォント設定を読み込み
- アプリ全体でリアクティブに切り替え
【1-4. Gemini AI解析】
1. GeminiService:
- モデル: gemini-2.5-flash-latest
- リアルタイム実況機能
- キャッチコピー自動生成
【1-5. UI実装】
1. ホーム画面4タブ:
- 酒リスト(フル幅カード)
- マップ
- AIソムリエ
- プロフィール
2. フル幅カード:
- 左: 写真120x120
- 右: 銘柄名(選択したフォント)、データ
3. プロフィール画面:
- フォント切り替えスイッチ
- MBTI入力欄今は空でOK
- 生年月日入力欄今は空でOK
【1-6. SafeArea対応】
- すべての画面でSafeAreaを徹底使用
- Android 15で見切れ・重なりゼロ
🚀 開始:
ANTIGRAVITY_PROMPT.md の「プロジェクト作成」セクションから開始してください。
Phase 1-1プロジェクト初期化完了後に報告をお願いします。
💡 重要な追加仕様:
【フォント切り替えの実装】
設定画面に以下を追加:
```dart
// 設定画面の一部
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('フォント設定'),
SegmentedButton(
segments: [
ButtonSegment(value: 'sans', label: Text('ゴシック')),
ButtonSegment(value: 'serif', label: Text('明朝')),
],
selected: {currentFont},
onSelectionChanged: (Set<String> newSelection) {
// UserProfileを更新
// AppThemeを再構築
},
),
],
)
```
【UserProfileモデルの完全コード】
```dart
// lib/models/user_profile.dart
import 'package:hive/hive.dart';
part 'user_profile.g.dart';
@HiveType(typeId: 1)
class UserProfile extends HiveObject {
@HiveField(0)
String? mbtiType; // "ENFP", "INTJ", etc.
@HiveField(1)
DateTime? birthDate; // 四柱推命用
@HiveField(2)
String fontPreference; // "serif" or "sans"
@HiveField(3)
DateTime createdAt;
@HiveField(4)
DateTime? updatedAt;
UserProfile({
this.mbtiType,
this.birthDate,
this.fontPreference = 'sans', // デフォルトはゴシック
required this.createdAt,
this.updatedAt,
});
}
```
【Phase 2以降の予告】
- MBTI × 日本酒相性診断
- 四柱推命 × 今日の一滴
- フレーバー・マトリックス
- Instagram用画像生成診断結果入り
これらは Phase 1完了後に実装します。
今は UserProfile の枠だけ準備してください。
```
---
## 📋 実装の流れ
### Phase 0: 準備(今ここ)
- [x] 新しいフォルダ作成ponshu_room_lite
- [x] 設計書をコピー
- [ ] Antigravityに指示を送る
### Phase 1: MVP5-6時間
- [ ] プロジェクト初期化
- [ ] データモデルSakeItem + UserProfile
- [ ] フォント切り替え機能
- [ ] Gemini解析リアルタイム実況
- [ ] フル幅カード
- [ ] SafeArea対応
### Phase 2: パーソナライズ3-4時間
- [ ] MBTI入力UI
- [ ] MBTI × 日本酒相性診断
- [ ] 四柱推命 × 今日の一滴
- [ ] フレーバー・マトリックス
### Phase 3: 共有機能2時間
- [ ] Instagram用画像生成診断結果入り
- [ ] キャッチコピー付き共有
### Phase 4: 遊び心3時間
- [ ] 日本酒・制覇マップ
- [ ] AIソムリエチャット
**合計: 13-15時間**
---
## 🎨 フォント選択のガイド
### デフォルト: Noto Sans JPゴシック体
```
印象: 親しみやすい、読みやすい、診断コンテンツと相性◎
おすすめな人: MBTIや診断を楽しみたい、カジュアルに記録したい
```
### オプション: Noto Serif JP明朝体
```
印象: 高級感、伝統、雑誌のような洗練
おすすめな人: 日本酒の本格的な雰囲気を大切にしたい
```
**いつでも切り替え可能!設定画面からワンタップ。**
---
## 🔐 APIキーの設定
Antigravityが `lib/secrets.dart` を作成した後、以下が正しく設定されているか確認してください:
```dart
// lib/secrets.dart
class Secrets {
static const String geminiApiKey = 'AIzaSyA2BSr16R2k0bHjSYcSUdmLoY8PKwaFts0';
}
```
---
## ✅ 完成確認チェックリスト
### Phase 1 MVP完成の確認
- [ ] カメラで日本酒を撮影できる
- [ ] Gemini解析で「ラベルを読んでいます...」と表示される
- [ ] キャッチコピーが自動生成される
- [ ] フル幅カード写真120x120で表示される
- [ ] フォント切り替えが動作する(ゴシック ⇔ 明朝)
- [ ] Android 15で見切れない
- [ ] UserProfileが保存される
### Phase 2 パーソナライズ完成の確認
- [ ] MBTI診断結果を入力できる
- [ ] 「このお酒はあなたENFPにピッタリ」と表示される
- [ ] フレーバー・マトリックスが表示される
### 最終完成の確認
- [ ] すべての機能が動作
- [ ] 「雑誌のような」デザイン
- [ ] 60fpsの滑らかな動作
- [ ] 「魔法のような」心地よさ
- [ ] 知人に見せたくなる
---
## 📞 トラブルシューティング
### もしAntigravityが古いコードを参照しようとしたら
**即座に指摘してください**:
```
@Antigravity
古い ponshu-room フォルダは参照しないでください。
このフォルダponshu_room_lite内のドキュメントだけを見てください。
```
### もし余白が大きすぎたら
```
@Antigravity
余白が大きすぎます。以下に修正してください:
- ListView.padding: EdgeInsets.symmetric(horizontal: 8, vertical: 12)
- separatorBuilder: SizedBox(height: 4)
- 写真サイズ: 120x120
```
### もしフォント切り替えが動作しなかったら
```
@Antigravity
フォント切り替えが動作しません。
UserProfileのfontPreferenceを変更したら、AppThemeが再構築されるようにしてください。
Riverpodのproviderを使って、リアクティブに更新してください。
```
---
## 🎊 準備完了!
すべての準備が整いました。
**次のアクション**:
1. 上記の「Antigravityへの指示」をコピー
2. Antigravityに送信
3. 実装開始を待つ
**「ぽんるーむ Lite」の誕生を楽しみにしています🍶✨**
---
## 💡 将来の拡張Pro版へ
Phase 1-4が完了し、知人にも好評だったら
### ぽんるーむ Pro構想
- Synology NAS自動バックアップ
- 複数デバイス同期
- チーム共有機能
- 高度な統計分析
- カスタムテーマ
- エクスポート機能強化
**まずは Lite を完璧に仕上げましょう!**

456
UI_UX_DECISION_GUIDE.md Normal file
View File

@ -0,0 +1,456 @@
# 新生ぽんるーむ - UI/UX決定ガイド
**対象**: プロダクトオーナーposimai
**目的**: シンプル・スタイリッシュ・今風なUIを作るための意思決定ガイド
---
## 🎨 今風なUI/UXとは
### 2025年のモバイルアプリトレンド
#### ✅ 採用すべき要素
1. **ミニマリズム**
- 余白をたっぷり取る
- 要素を最小限に絞る
- 色数を3-4色に抑える
2. **マテリアル3デザイン**
- elevation: 0フラット
- border: 1px程度の細いライン
- 角丸: 8-12px程度
3. **タイポグラフィのメリハリ**
- 見出し: 大きく20-24px
- 本文: 適度13-15px
- キャプション: 小さく11-12px
- フォントは2種類まで
4. **自然なアニメーション**
- 200-300msの短いアニメーション
- easeInOut curve
- Hero遷移
5. **ダークモード非対応でOK**
- ライトモードのみでシンプルに
- 日本酒の写真が映えるのはライトモード
#### ❌ 避けるべき要素
1. グラデーション多用
2. 派手なドロップシャドウelevation: 8以上
3. 過度な装飾・パターン
4. 5色以上のカラーパレット
5. 遅いアニメーション500ms以上
---
## 📋 意思決定チェックリスト
あなたが決めるべき5つの要素を選択してください。
### 1. カラースキーム
**質問**: posimaiブルー (#376495) 以外のアクセントカラーは?
#### オプションA: 柔らかいパステル(推奨)
```dart
お気に入り: Color(0xFFE8B4B8) // 淡いピンク
買いたい: Color(0xFFFEF3C7) // 淡い黄色
```
**印象**: 優しい、女性受け良い、Instagram映え
#### オプションB: 渋い和風
```dart
お気に入り: Color(0xFFB38867) // 茶色
買いたい: Color(0xFFDAA520) // 金色
```
**印象**: 格調高い、男性受け良い、日本酒らしい
#### オプションC: モノクロ+posimaiブルーのみ
```dart
お気に入り: posimaiBlue
買いたい: Color(0xFF4A4A4A) // グレー
```
**印象**: 究極にシンプル、プロフェッショナル
**あなたの選択**: [ A / B / C ]
---
### 2. カードデザイン
**質問**: リスト表示のカードレイアウトは?
#### オプションA: 縦型カード(推奨)
```
┌──────────┐
│ │
│ [画像] │ ← 正方形
│ │
├──────────┤
│ 獺祭 │
│ ★★★★★ │
└──────────┘
```
**メリット**: Instagram的、写真が大きい、スクロールしやすい
#### オプションB: 横型カード
```
┌──────┬────────────┐
│ │ 獺祭 │
│[画像]│ 旭酒造 │
│ │ ★★★★★ │
└──────┴────────────┘
```
**メリット**: 情報が見やすい、1画面に多く表示
#### オプションC: グリッド表示2列
```
┌────┬────┐
│[画] │[画] │
│獺祭 │久保田│
└────┴────┘
```
**メリット**: コレクション感、Pinterest的
**あなたの選択**: [ A / B / C ]
---
### 3. アニメーション
**質問**: 画面遷移のアニメーションは?
#### オプションA: Hero + Fade推奨
```dart
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => DetailScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);
```
**印象**: 滑らか、高級感、Instagram的
#### オプションB: スライド
```dart
Navigator.push(
context,
CupertinoPageRoute(builder: (context) => DetailScreen()),
);
```
**印象**: iOS的、シンプル、速い
#### オプションC: なし
```dart
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailScreen()),
);
```
**印象**: 最速、シンプル、パフォーマンス優先
**あなたの選択**: [ A / B / C ]
---
### 4. ボトムナビゲーション
**質問**: アイコンのスタイルは?
#### オプションA: Material Icons推奨
```dart
Icons.liquor // 酒
Icons.map // マップ
Icons.smart_toy // AI
Icons.person // マイページ
```
**印象**: シンプル、認識しやすい、Androidらしい
#### オプションB: Cupertino Icons
```dart
CupertinoIcons.sparkles // 酒
CupertinoIcons.map // マップ
CupertinoIcons.chat_bubble // AI
CupertinoIcons.person // マイページ
```
**印象**: iOS的、おしゃれ、一貫性
#### オプションC: カスタムアイコン
```
🍶 徳利・おちょこ
🗺️ 地図
🤖 AIロボット
👤 人型
```
**印象**: 個性的、日本酒らしい、作成コスト高
**あなたの選択**: [ A / B / C ]
---
### 5. FloatingActionButtonカメラボタン
**質問**: カメラボタンの動作は?
#### オプションA: タップでカメラ、長押しでギャラリー(推奨)
```dart
GestureDetector(
onTap: () => launchCamera(),
onLongPress: () => launchGallery(),
child: FloatingActionButton(...),
)
```
**メリット**: 1ボタンでシンプル、上級者向け
#### オプションB: タップで選択ダイアログ表示
```dart
onPressed: () {
showDialog(
child: AlertDialog(
title: Text('写真を選択'),
actions: [
TextButton(child: Text('カメラ'), onPressed: ...),
TextButton(child: Text('ギャラリー'), onPressed: ...),
],
),
);
}
```
**メリット**: 分かりやすい、初心者向け
#### オプションC: 2つのボタンカメラ・ギャラリー
```dart
Column(
children: [
FloatingActionButton(icon: Icons.camera, ...),
SizedBox(height: 8),
FloatingActionButton(icon: Icons.photo_library, ...),
],
)
```
**メリット**: 直感的、すぐ選べる
**あなたの選択**: [ A / B / C ]
---
## 📐 追加の細かい決定事項
### 6. 評価表示
**質問**: 星の評価表示は?
#### オプションA: 星アイコン(推奨)
```
★★★★★ 5.0
```
#### オプションB: 数値のみ
```
5.0 / 5.0
```
#### オプションC: プログレスバー
```
━━━━━ 100%
```
**あなたの選択**: [ A / B / C ]
---
### 7. 検索バー
**質問**: 検索バーのデザインは?
#### オプションA: 角丸ピル型(推奨)
```dart
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[100],
),
)
```
#### オプションB: 角丸ボックス型
```dart
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
)
```
**あなたの選択**: [ A / B ]
---
### 8. 写真の表示比率
**質問**: カード内の写真の比率は?
#### オプションA: 正方形1:1推奨
```dart
AspectRatio(aspectRatio: 1)
```
**メリット**: Instagram的、統一感
#### オプションB: 4:3
```dart
AspectRatio(aspectRatio: 4/3)
```
**メリット**: 情報量が多い、カメラ標準
#### オプションC: 16:9
```dart
AspectRatio(aspectRatio: 16/9)
```
**メリット**: 横長、映画的
**あなたの選択**: [ A / B / C ]
---
### 9. タグ表示
**質問**: タグのデザインは?
#### オプションA: 角丸チップ(推奨)
```dart
Chip(
label: Text('甘口'),
backgroundColor: Colors.blue[50],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
)
```
#### オプションB: ボックス型
```dart
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(4),
),
child: Text('甘口'),
)
```
**あなたの選択**: [ A / B ]
---
### 10. ローディング表示
**質問**: AI解析中のローディングは
#### オプションA: CircularProgressIndicator推奨
```dart
Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(posimaiBlue),
),
)
```
#### オプションB: LinearProgressIndicator
```dart
LinearProgressIndicator(
valueColor: AlwaysStoppedAnimation(posimaiBlue),
)
```
#### オプションC: カスタムアニメーション
```
🍶 徳利がくるくる回る
```
**あなたの選択**: [ A / B / C ]
---
## 🎯 推奨設定(まとめ)
もし迷ったら、この設定で進めてください:
```yaml
カラースキーム: オプションA柔らかいパステル
カードデザイン: オプションA縦型カード
アニメーション: オプションAHero + Fade
ボトムナビゲーション: オプションAMaterial Icons
FABの動作: オプションAタップでカメラ、長押しでギャラリー
評価表示: オプションA星アイコン
検索バー: オプションA角丸ピル型
写真比率: オプションA正方形 1:1
タグ表示: オプションA角丸チップ
ローディング: オプションACircularProgressIndicator
```
**この設定の印象**: Instagram的、洗練、女性受け良、シンプル、今風
---
## 📱 参考アプリ
### 同じ方向性のアプリ
1. **Instagram** - 写真重視、ミニマル
2. **Pinterest** - グリッド表示、コレクション感
3. **Apple Music** - 角丸カード、余白たっぷり
4. **Spotify** - ダークモード、アルバムアート重視
5. **Google Keep** - シンプル、カラフルなタグ
### 避けるべき方向性
1. **古いSkeuomorphic** - リアルな質感
2. **ゴチャゴチャしたUI** - 情報過多
3. **派手なアニメーション** - ゲーム的
---
## ✅ 決定シート
以下をコピーして、あなたの選択を記入してください:
```
=== 新生ぽんるーむ UI/UX決定シート ===
1. カラースキーム: [ A / B / C ]
2. カードデザイン: [ A / B / C ]
3. アニメーション: [ A / B / C ]
4. ボトムナビゲーション: [ A / B / C ]
5. FABの動作: [ A / B / C ]
6. 評価表示: [ A / B / C ]
7. 検索バー: [ A / B ]
8. 写真比率: [ A / B / C ]
9. タグ表示: [ A / B ]
10. ローディング: [ A / B / C ]
その他の要望:
署名: posimai
日付: 2025-12-29
```
---
**このシートを埋めて、Antigravityに渡してください**

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

96
analysis_output.txt Normal file
View File

@ -0,0 +1,96 @@
Resolving dependencies...
Downloading packages...
_fe_analyzer_shared 67.0.0 (92.0.0 available)
analyzer 6.4.1 (9.0.0 available)
analyzer_plugin 0.11.3 (0.13.11 available)
build 2.4.1 (4.0.3 available)
build_config 1.1.2 (1.2.0 available)
build_resolvers 2.4.2 (3.0.4 available)
build_runner 2.4.13 (2.10.4 available)
build_runner_core 7.3.2 (9.3.2 available)
characters 1.4.0 (1.4.1 available)
custom_lint 0.5.11 (0.8.1 available)
custom_lint_core 0.5.14 (0.8.2 available)
dart_style 2.3.6 (3.1.3 available)
freezed_annotation 2.4.4 (3.1.0 available)
image 4.5.4 (4.7.2 available)
matcher 0.12.17 (0.12.18 available)
material_color_utilities 0.11.1 (0.13.0 available)
package_info_plus 8.3.1 (9.0.0 available)
riverpod_analyzer_utils 1.0.0-dev.1 (1.0.0-dev.8 available)
riverpod_annotation 3.0.0-dev.3 (4.0.0 available)
riverpod_generator 3.0.0-dev.11 (4.0.0+1 available)
rxdart 0.27.7 (0.28.0 available)
shelf_web_socket 2.0.1 (3.0.0 available)
source_gen 1.5.0 (4.1.1 available)
source_helper 1.3.5 (1.3.9 available)
test 1.26.3 (1.28.0 available)
test_api 0.7.7 (0.7.8 available)
test_core 0.6.12 (0.6.14 available)
Got dependencies!
27 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.
Analyzing ponshu_room_lite...
info - The import of 'models/schema/display_data.dart' is unnecessary because all of the used elements are also provided by the import of 'models/sake_item.dart' - lib\main.dart:11:8 - unnecessary_import
info - The import of 'models/schema/hidden_specs.dart' is unnecessary because all of the used elements are also provided by the import of 'models/sake_item.dart' - lib\main.dart:12:8 - unnecessary_import
info - The import of 'models/schema/user_data.dart' is unnecessary because all of the used elements are also provided by the import of 'models/sake_item.dart' - lib\main.dart:13:8 - unnecessary_import
info - The import of 'models/schema/gamification.dart' is unnecessary because all of the used elements are also provided by the import of 'models/sake_item.dart' - lib\main.dart:14:8 - unnecessary_import
info - The import of 'models/schema/metadata.dart' is unnecessary because all of the used elements are also provided by the import of 'models/sake_item.dart' - lib\main.dart:15:8 - unnecessary_import
warning - Unused import: 'package:hive_flutter/hive_flutter.dart' - lib\providers\menu_providers.dart:2:8 - unused_import
warning - Unused import: '../models/sake_item.dart' - lib\providers\menu_providers.dart:3:8 - unused_import
info - The private field _capturedImages could be 'final' - lib\screens\camera_screen.dart:59:16 - prefer_final_fields
info - Don't invoke 'print' in production code - lib\screens\camera_screen.dart:133:7 - avoid_print
warning - Unused import: '../widgets/quota_warning_dialog.dart' - lib\screens\home_screen.dart:19:8 - unused_import
info - Don't use 'BuildContext's across async gaps - lib\screens\home_screen.dart:45:34 - use_build_context_synchronously
warning - The value of the local variable 'selectedTag' isn't used - lib\screens\home_screen.dart:54:11 - unused_local_variable
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:145:55 - deprecated_member_use
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:468:54 - deprecated_member_use
info - Unnecessary use of multiple underscores - lib\screens\menu_pricing_screen.dart:76:18 - unnecessary_underscores
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\menu_pricing_screen.dart:114:38 - deprecated_member_use
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\menu_pricing_screen.dart:115:39 - deprecated_member_use
info - Unnecessary use of multiple underscores - lib\screens\menu_pricing_screen.dart:160:43 - unnecessary_underscores
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\menu_pricing_screen.dart:176:47 - deprecated_member_use
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\menu_pricing_screen.dart:260:36 - deprecated_member_use
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\menu_pricing_screen.dart:261:34 - deprecated_member_use
warning - Unused import: '../services/pdf_service.dart' - lib\screens\menu_settings_screen.dart:8:8 - unused_import
warning - Unused import: 'package:printing/printing.dart' - lib\screens\menu_settings_screen.dart:12:8 - unused_import
info - Unnecessary use of multiple underscores - lib\screens\menu_settings_screen.dart:157:18 - unnecessary_underscores
info - Unnecessary use of 'toList' in a spread - lib\screens\menu_settings_screen.dart:194:26 - unnecessary_to_list_in_spreads
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\menu_settings_screen.dart:227:86 - deprecated_member_use
info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\menu_settings_screen.dart:417:48 - deprecated_member_use
info - Don't use 'BuildContext's across async gaps - lib\screens\sake_detail_screen.dart:451:27 - use_build_context_synchronously
info - Unnecessary braces in a string interpolation - lib\screens\sake_detail_screen.dart:668:26 - unnecessary_brace_in_string_interps
warning - The value of the local variable 'price' isn't used - lib\screens\sake_detail_screen.dart:711:17 - unused_local_variable
info - Unnecessary use of 'toList' in a spread - lib\screens\sake_detail_screen.dart:866:31 - unnecessary_to_list_in_spreads
info - Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check - lib\screens\sake_detail_screen.dart:986:23 - use_build_context_synchronously
info - Don't use 'BuildContext's across async gaps, guarded by an unrelated 'mounted' check - lib\screens\sake_detail_screen.dart:987:30 - use_build_context_synchronously
info - 'activeColor' is deprecated and shouldn't be used. Use activeThumbColor instead. This feature was deprecated after v3.31.0-2.0.pre - lib\screens\soul_screen.dart:128:22 - deprecated_member_use
info - Unnecessary braces in a string interpolation - lib\services\image_compression_service.dart:39:63 - unnecessary_brace_in_string_interps
info - Unnecessary braces in a string interpolation - lib\services\image_compression_service.dart:54:55 - unnecessary_brace_in_string_interps
info - Unnecessary braces in a string interpolation - lib\services\image_compression_service.dart:54:88 - unnecessary_brace_in_string_interps
info - Don't invoke 'print' in production code - lib\services\migration_service.dart:11:5 - avoid_print
info - Don't invoke 'print' in production code - lib\services\migration_service.dart:32:9 - avoid_print
info - Don't invoke 'print' in production code - lib\services\migration_service.dart:51:9 - avoid_print
info - Don't invoke 'print' in production code - lib\services\migration_service.dart:53:9 - avoid_print
info - Don't invoke 'print' in production code - lib\services\migration_service.dart:60:7 - avoid_print
info - Don't invoke 'print' in production code - lib\services\migration_service.dart:80:11 - avoid_print
info - Don't invoke 'print' in production code - lib\services\migration_service.dart:86:7 - avoid_print
info - Don't invoke 'print' in production code - lib\services\migration_service.dart:88:7 - avoid_print
info - Don't invoke 'print' in production code - lib\services\pdf_service.dart:37:14 - avoid_print
warning - The declaration '_getChildAspectRatio' isn't referenced - lib\services\pdf_service.dart:355:17 - unused_element
info - 'scale' is deprecated and shouldn't be used. Use scaleByVector3, scaleByVector4, or scaleByDouble instead - lib\widgets\sake_3d_carousel.dart:82:11 - deprecated_member_use
info - Can't use a relative path to import a library in 'lib' - tool\check_models.dart:3:8 - avoid_relative_lib_imports
info - Don't invoke 'print' in production code - tool\check_models.dart:6:3 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:10:5 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:26:7 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:32:14 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:33:14 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:34:14 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:35:14 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:39:9 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:42:7 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:44:7 - avoid_print
info - Don't invoke 'print' in production code - tool\check_models.dart:48:5 - avoid_print
60 issues found. (ran in 3.4s)

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,54 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
namespace = "com.posimai.ponshu_room_lite"
compileSdk = 36
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
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.
minSdk = 24
targetSdk = 34
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
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")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
debug {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
flutter {
source = "../.."
}
dependencies {
// 日本語OCR認識のためのML Kitライブラリ型番スキャンアプリと同じ設定
implementation("com.google.mlkit:text-recognition-japanese:16.0.1")
}

View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "1086358251978",
"project_id": "gen-lang-client-0086450305",
"storage_bucket": "gen-lang-client-0086450305.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:1086358251978:android:997279498c984e4bae4733",
"android_client_info": {
"package_name": "com.posimai.ponshu_room_lite"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDesRm4WlV3Aa31bY4vkJr8MeoKdYH-038"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

31
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,31 @@
# Google ML Kit Text Recognition
-keep class com.google.mlkit.vision.text.** { *; }
-dontwarn com.google.mlkit.vision.text.**
# Keep all classes referenced by ML Kit
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.android.gms.**
# ML Kit Japanese OCR model classes - IMPORTANT: do not remove
-keep class com.google.mlkit.vision.text.japanese.** { *; }
-keep class com.google.mlkit.vision.text.chinese.** { *; }
-keep class com.google.mlkit.vision.text.devanagari.** { *; }
-keep class com.google.mlkit.vision.text.korean.** { *; }
-dontwarn com.google.mlkit.vision.text.chinese.**
-dontwarn com.google.mlkit.vision.text.devanagari.**
-dontwarn com.google.mlkit.vision.text.japanese.**
-dontwarn com.google.mlkit.vision.text.korean.**
# TensorFlow Lite (ML Kit dependency)
-keep class org.tensorflow.lite.** { *; }
-dontwarn org.tensorflow.lite.**
# Native libraries
-keepclasseswithmembernames class * {
native <methods>;
}
# ML Kit internal classes
-keep class com.google.mlkit.common.** { *; }
-keep class com.google.mlkit.** { *; }

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,58 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Network permissions for Gemini API -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Camera and device permissions -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<application
android:label="ponshu_room_lite"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.posimai.ponshu_room_lite
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@ -0,0 +1,27 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("com.google.gms.google-services") version "4.4.0" apply false
}
include(":app")

Binary file not shown.

BIN
assets/images/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.posimai.ponshuRoomLite;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.posimai.ponshuRoomLite.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.posimai.ponshuRoomLite.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.posimai.ponshuRoomLite.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.posimai.ponshuRoomLite;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.posimai.ponshuRoomLite;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

53
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Ponshu Room Lite</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ponshu_room_lite</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>日本酒のラベルを撮影するためにカメラを使用します</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>撮影した日本酒の写真をギャラリーに保存します</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

BIN
keytool_output.txt Normal file

Binary file not shown.

85
lib/main.dart Normal file
View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; // Localization
import 'package:hive_flutter/hive_flutter.dart';
import 'models/sake_item.dart';
import 'models/user_profile.dart';
import 'models/menu_settings.dart';
import 'providers/theme_provider.dart';
import 'screens/main_screen.dart';
import 'models/schema/display_data.dart';
import 'models/schema/hidden_specs.dart';
import 'models/schema/user_data.dart';
import 'models/schema/gamification.dart';
import 'models/schema/metadata.dart';
import 'models/schema/item_type.dart';
import 'services/migration_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive
await Hive.initFlutter();
// Register Adapters
Hive.registerAdapter(SakeItemAdapter());
Hive.registerAdapter(UserProfileAdapter());
Hive.registerAdapter(MenuSettingsAdapter());
// Phase 0 New Adapters
Hive.registerAdapter(DisplayDataAdapter());
Hive.registerAdapter(HiddenSpecsAdapter());
Hive.registerAdapter(UserDataAdapter());
Hive.registerAdapter(GamificationAdapter());
Hive.registerAdapter(MetadataAdapter());
Hive.registerAdapter(ItemTypeAdapter());
// Run Phase 0 Migration (Backup & Convert)
await MigrationService.runMigration();
// Open Boxes
final userProfileBox = await Hive.openBox<UserProfile>('user_profile');
await Hive.openBox<SakeItem>('sake_items'); // Already opened by Migration, but safe to call again
await Hive.openBox('settings'); // Generic box for app settings (sort order)
await Hive.openBox<MenuSettings>('menu_settings'); // Menu display settings
runApp(
ProviderScope(
overrides: [
userProfileBoxProvider.overrideWithValue(userProfileBox),
],
child: const MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final lightTheme = ref.watch(lightThemeProvider);
final darkTheme = ref.watch(darkThemeProvider);
final themeMode = ref.watch(themeModeProvider);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Ponshu Room Lite',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeMode,
// Localization Fix for DatePicker & Menu
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('ja', 'JP'),
],
home: const MainScreen(),
);
}
}

View File

@ -0,0 +1,127 @@
class JapanMapData {
// 0: Empty/Ocean
// 1-47: Prefecture IDs (JIS Code)
// High-Resolution 8-bit Map (26 cols x 32 rows)
// Designed to show Hokkaido size, Honshu curve, and close Kyushu/Shikoku
static final List<List<int>> gridLayout = [
// Hokkaido (Top) - Huge & Diamond shape approx
// Hokkaido (Top) - Refined Diamond Shape
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], // Tip
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], // Widen
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0], // Widest top
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], // Mid
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], // Narrowing
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], // Oshinima Peninsula
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Tsugaru Strait
// Tohoku (The vertical stick)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0], // Aomori
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 3, 3, 0, 0, 0, 0, 0, 0, 0], // Akita, Iwate
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 4, 4, 0, 0, 0, 0, 0, 0, 0], // Yamagata, Miyagi
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0], // Niigata, Fukushima
// Kanto & Chubu (The bulging turn)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 15, 10, 9, 9, 8, 8, 0, 0, 0, 0, 0, 0], // Niigata, Gunma, Tochigi, Ibaraki
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 16, 20, 20, 11, 11, 12, 12, 0, 0, 0, 0, 0, 0], // Ishikawa, Toyama, Nagano, Saitama, Chiba
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 21, 20, 19, 13, 13, 12, 0, 0, 0, 0, 0, 0, 0], // Fukui, Gifu, Nagano, Yamanashi, Tokyo, Chiba
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 25, 21, 23, 22, 14, 14, 0, 0, 0, 0, 0, 0, 0], // Fukui, Shiga, Gifu, Aichi, Shizuoka, Kanagawa
// Kansai & Chugoku (Stretching West)
[0, 0, 0, 0, 0, 0, 0, 32, 31, 28, 26, 25, 24, 23, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Shimane, Tottori, Hyogo, Kyoto, Shiga, Mie, Aichi, Shizuoka
[0, 0, 0, 0, 0, 35, 34, 33, 28, 27, 29, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Yamaguchi, Hiroshima, Okayama, Hyogo, Osaka, Nara, Mie
// Shikoku (Nestled under) & Wakayama
[0, 0, 0, 0, 0, 0, 0, 37, 36, 0, 30, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Kagawa, Tokushima, Wakayama
[0, 0, 0, 0, 0, 0, 38, 38, 39, 39, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Ehime, Kochi
// Kyushu (Connecting West)
[0, 0, 0, 41, 40, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Saga, Fukuoka, Oita
[0, 42, 42, 43, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Nagasaki, Kumamoto, Oita
[0, 42, 43, 43, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Nagasaki, Kumamoto, Miyazaki
[0, 0, 46, 46, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Kagoshima, Miyazaki
[0, 0, 46, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Kagoshima
// Gap
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[47, 47, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Okinawa
[0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Okinawa
];
static const Map<int, String> prefectureNames = {
1: '北海道',
2: '青森県',
3: '岩手県',
4: '宮城県',
5: '秋田県',
6: '山形県',
7: '福島県',
8: '茨城県',
9: '栃木県',
10: '群馬県',
11: '埼玉県',
12: '千葉県',
13: '東京都',
14: '神奈川県',
15: '新潟県',
16: '富山県',
17: '石川県',
18: '福井県',
19: '山梨県',
20: '長野県',
21: '岐阜県',
22: '静岡県',
23: '愛知県',
24: '三重県',
25: '滋賀県',
26: '京都府',
27: '大阪府',
28: '兵庫県',
29: '奈良県',
30: '和歌山県',
31: '鳥取県',
32: '島根県',
33: '岡山県',
34: '広島県',
35: '山口県',
36: '徳島県',
37: '香川県',
38: '愛媛県',
39: '高知県',
40: '福岡県',
41: '佐賀県',
42: '長崎県',
43: '熊本県',
44: '大分県',
45: '宮崎県',
46: '鹿児島県',
47: '沖縄県',
};
// Region grouping for visual rhythm (coloring)
// 1: Hokkaido, 2: Tohoku, 3: Kanto, 4: Chubu, 5: Kansai, 6: Chugoku, 7: Shikoku, 8: Kyushu/Okinawa
static const Map<int, String> regionNames = {
1: '北海道',
2: '東北',
3: '関東',
4: '中部',
5: '近畿',
6: '中国',
7: '四国',
8: '九州・沖縄',
};
static int getRegionId(int prefId) {
if (prefId == 1) return 1;
if (prefId >= 2 && prefId <= 7) return 2;
if (prefId >= 8 && prefId <= 14) return 3;
if (prefId >= 15 && prefId <= 23) return 4;
if (prefId >= 24 && prefId <= 30) return 5;
if (prefId >= 31 && prefId <= 35) return 6;
if (prefId >= 36 && prefId <= 39) return 7;
if (prefId >= 40 && prefId <= 47) return 8;
return 0;
}
}

View File

@ -0,0 +1,69 @@
import 'package:hive/hive.dart';
part 'menu_settings.g.dart';
@HiveType(typeId: 3)
class MenuSettings extends HiveObject {
@HiveField(0)
String title;
@HiveField(1)
bool includePhoto;
@HiveField(2)
bool includePoem;
@HiveField(3)
bool includeChart;
@HiveField(4)
bool includePrice;
@HiveField(5)
bool includeDate;
@HiveField(8)
bool? includeQr; // Nullable for compatibility
@HiveField(6)
String pdfSize; // 'a4', 'a5', 'b5'
@HiveField(7)
bool isMonochrome;
MenuSettings({
this.title = '',
this.includePhoto = true,
this.includePoem = true,
this.includeChart = true,
this.includePrice = true,
this.includeDate = true,
this.includeQr = false,
this.pdfSize = 'a4',
this.isMonochrome = false,
});
MenuSettings copyWith({
String? title,
bool? includePhoto,
bool? includePoem,
bool? includeChart,
bool? includePrice,
bool? includeDate,
bool? includeQr,
String? pdfSize,
bool? isMonochrome,
}) {
return MenuSettings(
title: title ?? this.title,
includePhoto: includePhoto ?? this.includePhoto,
includePoem: includePoem ?? this.includePoem,
includeChart: includeChart ?? this.includeChart,
includePrice: includePrice ?? this.includePrice,
includeDate: includeDate ?? this.includeDate,
includeQr: includeQr ?? this.includeQr,
pdfSize: pdfSize ?? this.pdfSize,
isMonochrome: isMonochrome ?? this.isMonochrome,
);
}
}

View File

@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'menu_settings.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class MenuSettingsAdapter extends TypeAdapter<MenuSettings> {
@override
final int typeId = 3;
@override
MenuSettings read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return MenuSettings(
title: fields[0] as String,
includePhoto: fields[1] as bool,
includePoem: fields[2] as bool,
includeChart: fields[3] as bool,
includePrice: fields[4] as bool,
includeDate: fields[5] as bool,
includeQr: fields[8] as bool?,
pdfSize: fields[6] as String,
isMonochrome: fields[7] as bool,
);
}
@override
void write(BinaryWriter writer, MenuSettings obj) {
writer
..writeByte(9)
..writeByte(0)
..write(obj.title)
..writeByte(1)
..write(obj.includePhoto)
..writeByte(2)
..write(obj.includePoem)
..writeByte(3)
..write(obj.includeChart)
..writeByte(4)
..write(obj.includePrice)
..writeByte(5)
..write(obj.includeDate)
..writeByte(8)
..write(obj.includeQr)
..writeByte(6)
..write(obj.pdfSize)
..writeByte(7)
..write(obj.isMonochrome);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MenuSettingsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

282
lib/models/sake_item.dart Normal file
View File

@ -0,0 +1,282 @@
import 'package:hive/hive.dart';
import 'schema/display_data.dart';
import 'schema/hidden_specs.dart';
import 'schema/user_data.dart';
import 'schema/gamification.dart';
import 'schema/metadata.dart';
import 'schema/item_type.dart';
export 'schema/display_data.dart';
export 'schema/hidden_specs.dart';
export 'schema/user_data.dart';
export 'schema/gamification.dart';
export 'schema/metadata.dart';
export 'schema/item_type.dart';
part 'sake_item.g.dart';
@HiveType(typeId: 0)
class SakeItem extends HiveObject {
// --- Root Identity ---
@HiveField(0)
final String id;
// --- New Hierarchical Data (Fields 20+) ---
@HiveField(20)
DisplayData? _displayData;
@HiveField(21)
HiddenSpecs? _hiddenSpecs;
@HiveField(22)
UserData? _userData;
@HiveField(23)
Gamification? _gamification;
@HiveField(24)
Metadata? _metadata;
@HiveField(25)
ItemType? _itemType;
// --- Legacy Fields (Deprecated: Kept for Migration) ---
@HiveField(1)
final String? legacyName;
@HiveField(2)
final String? legacyBrand;
@HiveField(3)
final String? legacyPrefecture;
@HiveField(4)
final String? legacyDescription;
@HiveField(5)
final String? legacyCatchCopy;
@HiveField(6)
final List<String>? legacyImagePaths;
@HiveField(7)
final double? legacySweetnessScore;
@HiveField(8)
final double? legacyBodyScore;
@HiveField(9)
final DateTime? legacyCreatedAt;
@HiveField(10)
final int? legacyConfidenceScore;
@HiveField(11)
final List<String>? legacyFlavorTags;
@HiveField(12)
final bool? legacyIsFavorite;
@HiveField(13)
final Map<String, int>? legacyTasteStats;
@HiveField(14)
final bool? legacyIsUserEdited;
@HiveField(15)
final int? legacyCostPrice;
@HiveField(16)
final int? legacyManualPrice;
@HiveField(17)
final double? legacyMarkup;
@HiveField(18)
final Map<String, int>? legacyPriceVariants;
SakeItem({
required this.id,
DisplayData? displayData,
HiddenSpecs? hiddenSpecs,
UserData? userData,
Gamification? gamification,
Metadata? metadata,
ItemType? itemType,
// Legacy params for migration compatibility (optional)
this.legacyName,
this.legacyBrand,
this.legacyPrefecture,
this.legacyDescription,
this.legacyCatchCopy,
this.legacyImagePaths,
this.legacySweetnessScore,
this.legacyBodyScore,
this.legacyCreatedAt,
this.legacyConfidenceScore,
this.legacyFlavorTags,
this.legacyIsFavorite,
this.legacyTasteStats,
this.legacyIsUserEdited,
this.legacyCostPrice,
this.legacyManualPrice,
this.legacyMarkup,
this.legacyPriceVariants,
}) {
_displayData = displayData;
_hiddenSpecs = hiddenSpecs;
_userData = userData;
_gamification = gamification;
_metadata = metadata;
_itemType = itemType;
}
// --- Smart Getters (Auto-Migration / Fallback) ---
DisplayData get displayData {
if (_displayData != null) return _displayData!;
// Fallback: Construct from Legacy
return DisplayData(
name: legacyName ?? 'Unknown',
brewery: legacyBrand ?? 'Unknown',
prefecture: legacyPrefecture ?? 'Unknown',
catchCopy: legacyCatchCopy,
imagePaths: legacyImagePaths ?? [],
rating: null, // Legacy didn't have rating explicit in display
);
}
// Allow setting for UI updates
set displayData(DisplayData val) {
_displayData = val;
save(); // Auto-save on set? Or just update memory. HiveObject usually requires save().
// Better to just update memory here.
}
HiddenSpecs get hiddenSpecs {
if (_hiddenSpecs != null) return _hiddenSpecs!;
return HiddenSpecs(
description: legacyDescription,
tasteStats: legacyTasteStats ?? {},
flavorTags: legacyFlavorTags ?? [],
sweetnessScore: legacySweetnessScore,
bodyScore: legacyBodyScore,
);
}
UserData get userData {
if (_userData != null) return _userData!;
return UserData(
isFavorite: legacyIsFavorite ?? false,
isUserEdited: legacyIsUserEdited ?? false,
price: legacyManualPrice,
costPrice: legacyCostPrice,
markup: legacyMarkup ?? 3.0,
priceVariants: legacyPriceVariants,
);
}
Gamification get gamification {
if (_gamification != null) return _gamification!;
return Gamification(
ponPoints: 0, // Legacy didn't track
);
}
Metadata get metadata {
if (_metadata != null) return _metadata!;
return Metadata(
createdAt: legacyCreatedAt ?? DateTime.now(),
aiConfidence: legacyConfidenceScore,
);
}
ItemType get itemType {
// Default to 'sake' for existing data
return _itemType ?? ItemType.sake;
}
set itemType(ItemType val) {
_itemType = val;
}
// --- Migration Method ---
// Returns true if migration was performed (i.e., was legacy)
bool ensureMigrated() {
if (_displayData != null) return false; // Already new structure
// Create New Objects from Legacy Fields
_displayData = displayData; // Uses getter logic
_hiddenSpecs = hiddenSpecs;
_userData = userData;
_gamification = gamification;
_metadata = metadata;
return true;
}
// --- CopyWith for Immutable Updates (Backend Compatible) ---
SakeItem copyWith({
String? name,
String? brand, // Maps to Brewery
String? prefecture,
String? description,
String? catchCopy,
List<String>? imagePaths,
double? sweetnessScore,
double? bodyScore,
int? confidenceScore, // Maps to aiConfidence
List<String>? flavorTags,
bool? isFavorite,
Map<String, int>? tasteStats,
bool? isUserEdited,
String? memo,
int? costPrice,
int? manualPrice, // Maps to price
double? markup,
Map<String, int>? priceVariants,
ItemType? itemType,
}) {
// Ensure we have current data structures
final currentDisplay = displayData;
final currentHidden = hiddenSpecs;
final currentUser = userData;
final currentMeta = metadata;
final currentGame = gamification;
return SakeItem(
id: id,
displayData: currentDisplay.copyWith(
name: name,
brewery: brand,
prefecture: prefecture,
catchCopy: catchCopy,
imagePaths: imagePaths,
),
hiddenSpecs: currentHidden.copyWith(
description: description,
tasteStats: tasteStats,
flavorTags: flavorTags,
sweetnessScore: sweetnessScore,
bodyScore: bodyScore,
),
userData: currentUser.copyWith(
isFavorite: isFavorite,
isUserEdited: isUserEdited,
memo: memo,
price: manualPrice,
costPrice: costPrice,
markup: markup,
priceVariants: priceVariants,
),
gamification: currentGame,
metadata: currentMeta.copyWith(
aiConfidence: confidenceScore,
),
itemType: itemType ?? this.itemType,
);
}
// Compact JSON for QR ecosystem
String toQrJson() {
return '{"id":"$id","n":"${displayData.name}","b":"${displayData.brewery}","p":"${displayData.prefecture ?? ""}","s":${hiddenSpecs.sweetnessScore ?? 0},"y":${hiddenSpecs.bodyScore ?? 0},"a":${hiddenSpecs.alcoholContent ?? 0}}';
}
}

113
lib/models/sake_item.g.dart Normal file
View File

@ -0,0 +1,113 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sake_item.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class SakeItemAdapter extends TypeAdapter<SakeItem> {
@override
final int typeId = 0;
@override
SakeItem read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return SakeItem(
id: fields[0] as String,
legacyName: fields[1] as String?,
legacyBrand: fields[2] as String?,
legacyPrefecture: fields[3] as String?,
legacyDescription: fields[4] as String?,
legacyCatchCopy: fields[5] as String?,
legacyImagePaths: (fields[6] as List?)?.cast<String>(),
legacySweetnessScore: fields[7] as double?,
legacyBodyScore: fields[8] as double?,
legacyCreatedAt: fields[9] as DateTime?,
legacyConfidenceScore: fields[10] as int?,
legacyFlavorTags: (fields[11] as List?)?.cast<String>(),
legacyIsFavorite: fields[12] as bool?,
legacyTasteStats: (fields[13] as Map?)?.cast<String, int>(),
legacyIsUserEdited: fields[14] as bool?,
legacyCostPrice: fields[15] as int?,
legacyManualPrice: fields[16] as int?,
legacyMarkup: fields[17] as double?,
legacyPriceVariants: (fields[18] as Map?)?.cast<String, int>(),
)
.._displayData = fields[20] as DisplayData?
.._hiddenSpecs = fields[21] as HiddenSpecs?
.._userData = fields[22] as UserData?
.._gamification = fields[23] as Gamification?
.._metadata = fields[24] as Metadata?
.._itemType = fields[25] as ItemType?;
}
@override
void write(BinaryWriter writer, SakeItem obj) {
writer
..writeByte(25)
..writeByte(0)
..write(obj.id)
..writeByte(20)
..write(obj._displayData)
..writeByte(21)
..write(obj._hiddenSpecs)
..writeByte(22)
..write(obj._userData)
..writeByte(23)
..write(obj._gamification)
..writeByte(24)
..write(obj._metadata)
..writeByte(25)
..write(obj._itemType)
..writeByte(1)
..write(obj.legacyName)
..writeByte(2)
..write(obj.legacyBrand)
..writeByte(3)
..write(obj.legacyPrefecture)
..writeByte(4)
..write(obj.legacyDescription)
..writeByte(5)
..write(obj.legacyCatchCopy)
..writeByte(6)
..write(obj.legacyImagePaths)
..writeByte(7)
..write(obj.legacySweetnessScore)
..writeByte(8)
..write(obj.legacyBodyScore)
..writeByte(9)
..write(obj.legacyCreatedAt)
..writeByte(10)
..write(obj.legacyConfidenceScore)
..writeByte(11)
..write(obj.legacyFlavorTags)
..writeByte(12)
..write(obj.legacyIsFavorite)
..writeByte(13)
..write(obj.legacyTasteStats)
..writeByte(14)
..write(obj.legacyIsUserEdited)
..writeByte(15)
..write(obj.legacyCostPrice)
..writeByte(16)
..write(obj.legacyManualPrice)
..writeByte(17)
..write(obj.legacyMarkup)
..writeByte(18)
..write(obj.legacyPriceVariants);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SakeItemAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -0,0 +1,51 @@
import 'package:hive/hive.dart';
part 'display_data.g.dart';
@HiveType(typeId: 11)
class DisplayData extends HiveObject {
@HiveField(0)
final String name;
@HiveField(1)
final String brewery;
@HiveField(2)
final String prefecture;
@HiveField(3)
final String? catchCopy;
@HiveField(4)
final List<String> imagePaths;
@HiveField(5)
final double? rating;
DisplayData({
required this.name,
required this.brewery,
required this.prefecture,
this.catchCopy,
required this.imagePaths,
this.rating,
});
DisplayData copyWith({
String? name,
String? brewery,
String? prefecture,
String? catchCopy,
List<String>? imagePaths,
double? rating,
}) {
return DisplayData(
name: name ?? this.name,
brewery: brewery ?? this.brewery,
prefecture: prefecture ?? this.prefecture,
catchCopy: catchCopy ?? this.catchCopy,
imagePaths: imagePaths ?? this.imagePaths,
rating: rating ?? this.rating,
);
}
}

View File

@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'display_data.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class DisplayDataAdapter extends TypeAdapter<DisplayData> {
@override
final int typeId = 11;
@override
DisplayData read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return DisplayData(
name: fields[0] as String,
brewery: fields[1] as String,
prefecture: fields[2] as String,
catchCopy: fields[3] as String?,
imagePaths: (fields[4] as List).cast<String>(),
rating: fields[5] as double?,
);
}
@override
void write(BinaryWriter writer, DisplayData obj) {
writer
..writeByte(6)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.brewery)
..writeByte(2)
..write(obj.prefecture)
..writeByte(3)
..write(obj.catchCopy)
..writeByte(4)
..write(obj.imagePaths)
..writeByte(5)
..write(obj.rating);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DisplayDataAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -0,0 +1,33 @@
import 'package:hive/hive.dart';
part 'gamification.g.dart';
@HiveType(typeId: 14)
class Gamification extends HiveObject {
@HiveField(0)
final int ponPoints;
@HiveField(1)
final String? sakeMbtiType;
@HiveField(2)
final String? rarityLevel;
Gamification({
this.ponPoints = 0,
this.sakeMbtiType,
this.rarityLevel,
});
Gamification copyWith({
int? ponPoints,
String? sakeMbtiType,
String? rarityLevel,
}) {
return Gamification(
ponPoints: ponPoints ?? this.ponPoints,
sakeMbtiType: sakeMbtiType ?? this.sakeMbtiType,
rarityLevel: rarityLevel ?? this.rarityLevel,
);
}
}

View File

@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'gamification.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class GamificationAdapter extends TypeAdapter<Gamification> {
@override
final int typeId = 14;
@override
Gamification read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return Gamification(
ponPoints: fields[0] as int,
sakeMbtiType: fields[1] as String?,
rarityLevel: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, Gamification obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.ponPoints)
..writeByte(1)
..write(obj.sakeMbtiType)
..writeByte(2)
..write(obj.rarityLevel);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GamificationAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

Some files were not shown because too many files have changed in this diff Show More