diff --git a/app/src/main/assets/database/app.db b/app/src/main/assets/database/app.db index 818b2ab6..1413d4a6 100644 Binary files a/app/src/main/assets/database/app.db and b/app/src/main/assets/database/app.db differ diff --git a/app/src/main/java/app/closer/ui/components/CategoryGlyph.kt b/app/src/main/java/app/closer/ui/components/CategoryGlyph.kt index 9488de97..65bf5274 100644 --- a/app/src/main/java/app/closer/ui/components/CategoryGlyph.kt +++ b/app/src/main/java/app/closer/ui/components/CategoryGlyph.kt @@ -124,14 +124,31 @@ fun categoryGlyphStyle( ) } - val key = (iconName ?: categoryId).lowercase().trim() - return when (key) { - "communication", "chat", "question", "questions" -> GlyphStyle( + // The curated per-category glyph wins; a pack's declared icon_name is the fallback. + // Packs ship Material icon names ("shield", "paid", "forum") — a different vocabulary from the + // keys below — so letting icon_name take precedence sent almost every category to the default + // star. Keeping icon_name as a fallback is what gives an imported/purchased pack a real glyph: + // its category_id won't be listed here, so its declared icon_name resolves instead. + return glyphStyleForKey(categoryId.lowercase().trim()) + ?: iconName?.let { glyphStyleForKey(it.lowercase().trim()) } + ?: GlyphStyle( + icon = ImageVector.vectorResource(R.drawable.glyph_star), + tint = CloserPalette.PurpleDeep, + container = CloserPalette.PurpleMist + ) +} + +/** Resolves one key (a category id, or a pack's icon_name) to a glyph, or null if unknown. */ +@Composable +private fun glyphStyleForKey(key: String): GlyphStyle? = + when (key) { + "communication", "chat", "question", "questions", + "chat_bubble_outline", "forum" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_chat), tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleMist ) - "emotional_intimacy", "intimacy", "heart" -> GlyphStyle( + "emotional_intimacy", "intimacy", "heart", "favorite", "volunteer_activism" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_heart), tint = CloserPalette.PinkAccentDeep, container = CloserPalette.PinkMist @@ -141,22 +158,22 @@ fun categoryGlyphStyle( tint = CloserPalette.Romantic, container = CloserPalette.Romantic.copy(alpha = 0.12f) ) - "trust", "rebuilding_trust", "privacy" -> GlyphStyle( + "trust", "rebuilding_trust", "privacy", "shield", "verified_user" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_shield), tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleGlow ) - "conflict", "conflict_repair", "repair" -> GlyphStyle( + "conflict", "conflict_repair", "repair", "healing" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_sync), tint = CloserPalette.PurpleRich, container = CloserPalette.PurpleMist ) - "future", "timeline" -> GlyphStyle( + "future", "timeline", "explore" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_timeline), tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleSoft ) - "money", "payments" -> GlyphStyle( + "money", "payments", "paid" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_attach_money), tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleMist @@ -166,22 +183,23 @@ fun categoryGlyphStyle( tint = CloserPalette.PurpleRich, container = CloserPalette.PurpleGlow ) - "gratitude", "star" -> GlyphStyle( + "gratitude", "star", "celebration" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_star), tint = CloserPalette.PinkAccentDeep, container = CloserPalette.PinkMist ) - "parenting", "family", "people" -> GlyphStyle( + "parenting", "family", "people", "family_restroom" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_couple), tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleSoft ) - "date_night", "date", "calendar" -> GlyphStyle( + "date_night", "date", "calendar", "calendar_heart", + "quality_time", "schedule_heart" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_calendar), tint = CloserPalette.PinkAccentDeep, container = CloserPalette.PinkMist ) - "fun", "play" -> GlyphStyle( + "fun", "play", "emoji_emotions" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_play), tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleGlow @@ -191,7 +209,7 @@ fun categoryGlyphStyle( tint = CloserPalette.Danger, container = CloserPalette.Danger.copy(alpha = 0.10f) ) - "values", "marriage" -> GlyphStyle( + "values", "marriage", "favorite_border" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_question_answer), tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleMist @@ -206,15 +224,10 @@ fun categoryGlyphStyle( tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleGlow ) - "predict", "psychology" -> GlyphStyle( + "predict", "psychology", "self_improvement" -> GlyphStyle( icon = ImageVector.vectorResource(R.drawable.glyph_psychology), tint = CloserPalette.PurpleDeep, container = CloserPalette.PurpleGlow ) - else -> GlyphStyle( - icon = ImageVector.vectorResource(R.drawable.glyph_star), - tint = CloserPalette.PurpleDeep, - container = CloserPalette.PurpleMist - ) + else -> null } -} diff --git a/app/src/main/java/app/closer/ui/questions/QuestionPackLibraryScreen.kt b/app/src/main/java/app/closer/ui/questions/QuestionPackLibraryScreen.kt index a5e46a89..b356598f 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionPackLibraryScreen.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionPackLibraryScreen.kt @@ -367,10 +367,11 @@ private fun QuestionPackItem.metadataLabels(): List { "free" -> "Free" else -> category.access.displayCategoryName() } - return listOf( - access, - category.iconName.ifBlank { "question" }.displayCategoryName() - ).filterNot { it == "Question" }.distinct() + // Only access is shown. `icon_name` used to be listed here too, but it is a rendering detail, + // not a topic: packs declare Material icon names, so it surfaced chips like "Chat Bubble + // Outline". It went unnoticed because every pack once carried the placeholder icon "question", + // which the list filtered out — so this chip has never actually been visible. + return listOf(access).distinct() } @Composable diff --git a/seed/build_db.py b/seed/build_db.py index 64015c74..ff0afd18 100644 --- a/seed/build_db.py +++ b/seed/build_db.py @@ -169,6 +169,21 @@ def build_answer_config(question: Dict[str, Any], where: str) -> Dict[str, Any]: # Validation — every problem is fatal # -------------------------------------------------------------------------- +def pack_id_for(category: Dict[str, Any]) -> str: + """The pack a question belongs to. + + `question.pack_id` is plumbed all the way to the domain model and is what lets content be + handled per-pack (query it, gate it, remove it) rather than only per-category. Bundled packs + get one too, so a later purchased/imported pack is not a special case — it is just another + pack_id in the same table. + + Prefer the logical pack id a pack declares in its metadata (the daily pack ships as category + `daily_fun_mc` but is logically `daily_single_choice_weekly_v1`); otherwise the category id. + """ + meta = category.get("metadata") or {} + return meta.get("pack_id") or category["id"] + + def load_packs() -> List[Dict[str, Any]]: """Load and hard-validate every pack. Raises on the first structural problem.""" files = sorted(JSON_DIR.glob("*.json")) @@ -255,6 +270,7 @@ def build(packs: List[Dict[str, Any]], db_schema: Dict[str, Any], out: Path) -> total = 0 for pack in packs: c = pack["category"] + pack_id = pack_id_for(c) cur.execute( "INSERT OR REPLACE INTO question_category " "(id, display_name, description, access, icon_name) VALUES (?, ?, ?, ?, ?)", @@ -276,7 +292,7 @@ def build(packs: List[Dict[str, Any]], db_schema: Dict[str, Any], out: Path) -> q["type"], json.dumps(q.get("tags", []), separators=(",", ":")), json.dumps(build_answer_config(q, where), separators=(",", ":")), - None, + pack_id, pack["mtime"], "active", q.get("sex"),