335 lines
8.6 KiB
Dart
335 lines
8.6 KiB
Dart
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;
|
||
|
||
// --- Phase 1: Draft Mode Fields (Fields 26-27) ---
|
||
@HiveField(26)
|
||
bool? _isPendingAnalysis; // true = AI解析待ち, false/null = 通常登録済み
|
||
|
||
@HiveField(27)
|
||
String? _draftPhotoPath; // 解析待ち写真の一時パス(Draft時のみ)
|
||
|
||
// --- 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,
|
||
// Phase 1: Draft Mode params
|
||
bool? isPendingAnalysis,
|
||
String? draftPhotoPath,
|
||
// 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;
|
||
_isPendingAnalysis = isPendingAnalysis;
|
||
_draftPhotoPath = draftPhotoPath;
|
||
}
|
||
|
||
// --- 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,
|
||
);
|
||
}
|
||
|
||
// Allow setting for さけのわ auto-matching
|
||
set hiddenSpecs(HiddenSpecs val) {
|
||
_hiddenSpecs = val;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Phase 1: Draft Mode Getters/Setters
|
||
bool get isPendingAnalysis => _isPendingAnalysis ?? false;
|
||
|
||
set isPendingAnalysis(bool val) {
|
||
_isPendingAnalysis = val;
|
||
}
|
||
|
||
// ignore: unnecessary_getters_setters
|
||
String? get draftPhotoPath => _draftPhotoPath;
|
||
|
||
set draftPhotoPath(String? val) {
|
||
_draftPhotoPath = 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,
|
||
// New Specs
|
||
String? specificDesignation, // Maps to hiddenSpecs.type
|
||
double? alcoholContent,
|
||
int? polishingRatio,
|
||
double? sakeMeterValue,
|
||
String? riceVariety,
|
||
String? yeast,
|
||
String? manufacturingYearMonth,
|
||
// Phase 1: Draft Mode
|
||
bool? isPendingAnalysis,
|
||
String? draftPhotoPath,
|
||
}) {
|
||
// 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,
|
||
type: specificDesignation,
|
||
alcoholContent: alcoholContent,
|
||
polishingRatio: polishingRatio,
|
||
sakeMeterValue: sakeMeterValue,
|
||
riceVariety: riceVariety,
|
||
yeast: yeast,
|
||
manufacturingYearMonth: manufacturingYearMonth,
|
||
),
|
||
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,
|
||
// Phase 1: Draft Mode
|
||
isPendingAnalysis: isPendingAnalysis ?? this.isPendingAnalysis,
|
||
draftPhotoPath: draftPhotoPath ?? this.draftPhotoPath,
|
||
);
|
||
}
|
||
// Compact JSON for QR ecosystem
|
||
String toQrJson() {
|
||
return '{"id":"$id","n":"${displayData.displayName}","b":"${displayData.displayBrewery}","p":"${displayData.displayPrefecture}","s":${hiddenSpecs.sweetnessScore ?? 0},"y":${hiddenSpecs.bodyScore ?? 0},"a":${hiddenSpecs.alcoholContent ?? 0}}';
|
||
}
|
||
}
|