Implement storage architecture from ai_blueprint.md
Primary sync: replace PersistenceActor JSON file with SwiftData + CloudKit - Add SavedCardModel (@Model class) and PersistenceController (ModelContainer with .automatic CloudKit, fallback to local). BackgroundPersistenceActor (@ModelActor) handles all DB I/O off the main thread. - One-time migration imports user_collection.json into SwiftData and renames the original file to prevent re-import. - Inject modelContainer into SwiftUI environment in IYmtgApp. Image storage: Documents/UserContent/ subfolder (blueprint requirement) - ImageManager.dir now targets iCloud Documents/UserContent/ (or local equiv). - migrateImagesToUserContent() moves existing JPGs to the new subfolder on first launch; called during the SwiftData migration. Firebase: demoted to optional manual backup (metadata only, no images) - Remove all automatic CloudEngine.save/delete/batchUpdatePrices calls from CollectionViewModel mutations. - Add backupAllToFirebase() for user-triggered metadata sync. - Add isFirebaseBackupEnabled to AppConfig (default false). - Add Cloud Backup section in Library settings with iCloud vs Firebase explanation and "Backup Metadata to Firebase Now" button. Also: full modular refactor (Data/, Features/, Services/ directories) and README updated with CloudKit setup steps and revised release checklist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
import os, sys, argparse
|
||||
|
||||
# Dependency Check
|
||||
try:
|
||||
@@ -17,17 +16,34 @@ OUTPUT_DIR = os.path.join(BASE_DIR, "Ready_Assets")
|
||||
|
||||
# Target Dimensions (Width, Height)
|
||||
ASSETS = {
|
||||
"AppIcon": (1024, 1024),
|
||||
"logo_header": (300, 80),
|
||||
"scanner_frame": (600, 800),
|
||||
"empty_library": (800, 800),
|
||||
"share_watermark": (400, 100),
|
||||
"card_placeholder": (600, 840)
|
||||
"AppIcon": (1024, 1024),
|
||||
"logo_header": (300, 80),
|
||||
"scanner_frame": (600, 800),
|
||||
"empty_library": (800, 800),
|
||||
"share_watermark": (400, 100),
|
||||
"card_placeholder": (600, 840),
|
||||
}
|
||||
|
||||
def process_assets():
|
||||
# Assets whose target aspect ratio differs significantly from a typical AI square output.
|
||||
# These use thumbnail+pad (letterbox) instead of center-crop to preserve full image content.
|
||||
PAD_ASSETS = {"logo_header", "share_watermark"}
|
||||
|
||||
def resize_fit(img, size):
|
||||
"""Center-crop and scale to exact size. Best when source matches target aspect ratio."""
|
||||
return ImageOps.fit(img, size, method=Image.Resampling.LANCZOS)
|
||||
|
||||
def resize_pad(img, size, bg_color=(0, 0, 0, 0)):
|
||||
"""Scale to fit within size, then pad with bg_color to reach exact dimensions.
|
||||
Preserves the full image — nothing is cropped. Ideal for logos and banners."""
|
||||
img.thumbnail(size, Image.Resampling.LANCZOS)
|
||||
canvas = Image.new("RGBA", size, bg_color)
|
||||
offset = ((size[0] - img.width) // 2, (size[1] - img.height) // 2)
|
||||
canvas.paste(img, offset)
|
||||
return canvas
|
||||
|
||||
def process_assets(force_pad=False, force_crop=False):
|
||||
print(f"📂 Working Directory: {BASE_DIR}")
|
||||
|
||||
|
||||
# Create directories if they don't exist
|
||||
if not os.path.exists(SOURCE_DIR):
|
||||
os.makedirs(SOURCE_DIR)
|
||||
@@ -51,19 +67,32 @@ def process_assets():
|
||||
img = Image.open(source_path)
|
||||
if img.mode != 'RGBA': img = img.convert('RGBA')
|
||||
|
||||
# Smart Crop & Resize (Center focus)
|
||||
processed_img = ImageOps.fit(img, size, method=Image.Resampling.LANCZOS)
|
||||
|
||||
# Choose resize strategy:
|
||||
# - PAD_ASSETS (and --pad flag) use thumbnail+letterbox to avoid cropping banner images.
|
||||
# - Everything else uses center-crop (ImageOps.fit) for a clean full-bleed fill.
|
||||
use_pad = (name in PAD_ASSETS or force_pad) and not force_crop
|
||||
if use_pad:
|
||||
processed_img = resize_pad(img, size)
|
||||
mode_label = "pad"
|
||||
else:
|
||||
processed_img = resize_fit(img, size)
|
||||
mode_label = "crop"
|
||||
|
||||
output_path = os.path.join(OUTPUT_DIR, name + ".png")
|
||||
processed_img.save(output_path, "PNG")
|
||||
print(f"✅ Generated: {name}.png ({size[0]}x{size[1]})")
|
||||
print(f"✅ Generated: {name}.png ({size[0]}x{size[1]}, {mode_label})")
|
||||
found = True
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing {name}: {e}")
|
||||
|
||||
|
||||
if not found:
|
||||
print(f"⚠️ Skipped: {name} (File not found in Raw_Assets)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
process_assets()
|
||||
parser = argparse.ArgumentParser(description="Resize raw AI assets to Xcode-ready dimensions.")
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--pad", action="store_true", help="Force thumbnail+pad for ALL assets (no cropping).")
|
||||
group.add_argument("--crop", action="store_true", help="Force center-crop for ALL assets (overrides default pad assets).")
|
||||
args = parser.parse_args()
|
||||
process_assets(force_pad=args.pad, force_crop=args.crop)
|
||||
|
||||
Reference in New Issue
Block a user