//! What a creator publishes: item and project kinds, the features a project //! can switch on, the AI-disclosure tiers, and how discover sorts it all. use super::str_enum::impl_str_enum; use serde::{Deserialize, Serialize}; // --- Content Insertions --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum InsertionPosition { PreRoll, MidRoll, PostRoll, } impl_str_enum!(InsertionPosition { PreRoll => "pre_roll", MidRoll => "mid_roll", PostRoll => "post_roll", }); // --- Discover sorting --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DiscoverSort { Newest, MostSold, PriceAsc, PriceDesc, } impl_str_enum!(DiscoverSort { Newest => "newest", MostSold => "most_sold", PriceAsc => "price_asc", PriceDesc => "price_desc", }); // --- Items --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ItemType { Audio, Text, Video, Image, Plugin, Preset, Sample, Course, Template, Digital, Bundle, } impl_str_enum!(ItemType { Audio => "audio", Text => "text", Video => "video", Image => "image", Plugin => "plugin", Preset => "preset", Sample => "sample", Course => "course", Template => "template", Digital => "digital", Bundle => "bundle", }); impl ItemType { /// Short human-readable label for display (replaces `helpers::get_item_type_label`). pub fn label(&self) -> &'static str { match self { Self::Audio => "Audio", Self::Text => "Text", Self::Video => "Video", Self::Image => "Image", Self::Plugin => "Plugin", Self::Preset => "Preset", Self::Sample => "Sample", Self::Course => "Course", Self::Template => "Template", Self::Digital => "Digital", Self::Bundle => "Bundle", } } /// Which wizard content-input group this type belongs to. /// /// Determines what the content step looks like: /// - `"text"` → Markdown editor /// - `"audio"` → Audio file upload /// - `"video"` → Video file upload /// - `"bundle"` → Item picker for bundle contents /// - `"file"` → Generic file upload pub fn wizard_group(&self) -> &'static str { match self { Self::Text => "text", Self::Audio => "audio", Self::Video => "video", Self::Bundle => "bundle", _ => "file", } } } // --- AI Tiers --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AiTier { Handmade, Assisted, Generated, } impl_str_enum!(AiTier { Handmade => "handmade", Assisted => "assisted", Generated => "generated", }); impl AiTier { pub fn label(&self) -> &'static str { match self { Self::Handmade => "Handmade", Self::Assisted => "Assisted", Self::Generated => "Generated", } } /// The badge modifier for this tier. A disclosure level is not lifecycle, /// so it keeps its own names rather than joining the status set, but the /// class still comes from here rather than from the serialized value. pub fn css_class(&self) -> &'static str { match self { Self::Handmade => "ai-tier-handmade", Self::Assisted => "ai-tier-assisted", Self::Generated => "ai-tier-generated", } } } /// Discover-page filter shape per `about/generative-ai.md` § "How Fans /// Use This". Distinct from `AiTier` because this is a *filter*, not a /// per-item value: `HumanLed` aggregates the Handmade + Assisted tiers. /// `None` on `DiscoverFilters.ai_tier` means "Everything", no /// restriction. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AiTierFilter { HandmadeOnly, HumanLed, } impl_str_enum!(AiTierFilter { HandmadeOnly => "handmade_only", HumanLed => "human_led", }); impl AiTierFilter { pub fn label(&self) -> &'static str { match self { Self::HandmadeOnly => "Handmade only", Self::HumanLed => "Human-led", } } } // --- Project Features --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ProjectFeature { Audio, Downloads, Text, Blog, Subscriptions, LicenseKeys, SourceCode, CloudSync, } impl_str_enum!(ProjectFeature { Audio => "audio", Downloads => "downloads", Text => "text", Blog => "blog", Subscriptions => "subscriptions", LicenseKeys => "license_keys", SourceCode => "source_code", CloudSync => "cloud_sync", }); impl ProjectFeature { /// Human-readable label for display. pub fn label(&self) -> &'static str { match self { Self::Audio => "Audio", Self::Downloads => "Downloads", Self::Text => "Text", Self::Blog => "Blog", Self::Subscriptions => "Subscriptions", Self::LicenseKeys => "License Keys", Self::SourceCode => "Source Code", Self::CloudSync => "Cloud Sync", } } /// One-line description of what this feature enables. pub fn description(&self) -> &'static str { match self { Self::Audio => "Upload and stream audio files. Player with chapters.", Self::Downloads => "Host file downloads with versioned releases.", Self::Text => "Write and publish text content with markdown.", Self::Blog => "Project blog with RSS feed.", Self::Subscriptions => "Monthly subscriber tiers.", Self::LicenseKeys => "Software license management with activation API.", Self::SourceCode => "Git repository with source browser.", Self::CloudSync => "E2E encrypted cloud sync for desktop and mobile apps.", } } /// All features as (value, label, description) tuples for form rendering. pub fn all() -> &'static [(&'static str, &'static str, &'static str)] { &[ ( "audio", "Audio", "Upload and stream audio files. Player with chapters.", ), ( "downloads", "Downloads", "Host file downloads with versioned releases.", ), ( "text", "Text", "Write and publish text content with markdown.", ), ("blog", "Blog", "Project blog with RSS feed."), ( "subscriptions", "Subscriptions", "Monthly subscriber tiers.", ), ( "license_keys", "License Keys", "Software license management with activation API.", ), ( "source_code", "Source Code", "Git repository with source browser.", ), ( "cloud_sync", "Cloud Sync", "E2E encrypted cloud sync for desktop and mobile apps.", ), ] } /// Derive the best-fit project type from a set of features. pub fn derive_project_type(features: &[String]) -> ProjectType { if features.iter().any(|f| f == "audio") { return ProjectType::Music; } if features.iter().any(|f| f == "text") && !features.iter().any(|f| f == "downloads") { return ProjectType::Blog; } if features.iter().any(|f| f == "downloads") { return ProjectType::Software; } ProjectType::General } /// Which item types a feature unlocks. pub fn allowed_item_types(&self) -> &'static [ItemType] { match self { Self::Audio => &[ItemType::Audio, ItemType::Sample, ItemType::Preset], Self::Downloads => &[ ItemType::Digital, ItemType::Plugin, ItemType::Template, ItemType::Course, ItemType::Image, ItemType::Video, ], Self::Text => &[ItemType::Text], // Non-content features don't gate item types Self::Blog | Self::Subscriptions | Self::LicenseKeys | Self::SourceCode | Self::CloudSync => &[], } } /// Compute the set of item types allowed by a project's feature list. /// If no content features are enabled, all types are allowed (permissive default). pub fn allowed_item_type_cards( features: &[String], ) -> Vec<(&'static str, &'static str, &'static str)> { let allowed: std::collections::HashSet = features .iter() .filter_map(|f| f.parse::().ok()) .flat_map(|f| f.allowed_item_types().iter().copied()) .collect(); // If no content features enabled, show all types (backwards compat) if allowed.is_empty() { return Self::all_item_type_cards().to_vec(); } Self::all_item_type_cards() .iter() .filter(|(value, _, _)| { value .parse::() .is_ok_and(|t| t == ItemType::Bundle || allowed.contains(&t)) }) .copied() .collect() } /// All item type cards: (value, label, description) tuples for form rendering. pub fn all_item_type_cards() -> &'static [(&'static str, &'static str, &'static str)] { &[ ("audio", "Audio", "Podcast, music, sound effects"), ("text", "Text", "Articles, posts, essays, guides"), ("digital", "Digital Download", "Files, archives, documents"), ("video", "Video", "Tutorials, films, recordings"), ("course", "Course", "Multi-part lessons, curricula"), ("plugin", "Plugin", "Software extensions, add-ons"), ("sample", "Sample Pack", "Audio samples, loops, one-shots"), ("preset", "Preset Pack", "Synth presets, effect chains"), ("template", "Template", "Design templates, starter kits"), ("image", "Image", "Photos, artwork, graphics"), ("bundle", "Bundle", "Collection of other items"), ] } /// Item type cards filtered to one per distinct wizard behavior group. /// /// The wizard only needs a type selector when the allowed types produce /// different content-step UIs (text editor vs audio upload vs file upload). /// Returns one card per group, using the first allowed type as the value. /// If all types share one group, returns a single card (caller should skip /// the type step entirely). pub fn wizard_type_cards( features: &[String], ) -> Vec<(&'static str, &'static str, &'static str)> { let allowed = Self::allowed_item_type_cards(features); let mut seen_groups = std::collections::HashSet::new(); let mut cards = Vec::new(); for (value, _, _) in &allowed { let Ok(item_type) = value.parse::() else { continue; }; let group = item_type.wizard_group(); if seen_groups.insert(group) { let (label, desc) = match group { "text" => ("Text", "Write in the editor"), "audio" => ("Audio", "Upload audio files"), "video" => ("Video", "Upload video files"), "bundle" => ("Bundle", "Collection of other items"), _ => ("File", "Upload any file"), }; cards.push((*value, label, desc)); } } cards } } // --- Projects --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ProjectType { Blog, Book, Podcast, Course, Music, Software, Art, Writing, #[default] General, } impl_str_enum!(ProjectType { Blog => "blog", Book => "book", Podcast => "podcast", Course => "course", Music => "music", Software => "software", Art => "art", Writing => "writing", General => "general", }); impl ProjectType { /// Human-readable label for display. pub fn label(&self) -> &'static str { match self { Self::Blog => "Blog", Self::Book => "Book", Self::Podcast => "Podcast", Self::Course => "Course", Self::Music => "Music", Self::Software => "Software", Self::Art => "Art", Self::Writing => "Writing", Self::General => "General", } } /// All valid project types as (value, label) pairs for form rendering. pub fn all() -> &'static [(&'static str, &'static str)] { &[ ("blog", "Blog"), ("book", "Book"), ("podcast", "Podcast"), ("course", "Course"), ("music", "Music"), ("software", "Software"), ("art", "Art"), ("writing", "Writing"), ("general", "General"), ] } }