TRMNL Recipes Catalog: Search, Sort, And Discover Plugins
Hey guys! Let's dive into the exciting new Recipes Catalog Screen for TRMNL, packed with features to help you discover and manage community plugins. This comprehensive guide will walk you through everything you need to know about this awesome addition, from its purpose and user stories to its intricate design and technical requirements. So, buckle up and get ready to explore the world of TRMNL plugins like never before!
Overview
The Recipes Catalog Screen is designed to display a searchable and sortable catalog of TRMNL community plugin recipes. This screen is accessible via Settings > Extras by tapping "Recipes Catalog". The primary goal is to provide users with an intuitive way to find and install useful plugins, leveraging the creativity and knowledge of the TRMNL community.
User Story
As a TRMNL user, you might be wondering, "How can this Recipes Catalog Screen benefit me?" Well, itβs all about making it easier for you to browse, search, and discover community plugin recipes. This means you can effortlessly find plugins that enhance your TRMNL experience and learn from the innovative implementations shared by fellow users. Imagine being able to quickly find that perfect plugin to customize your terminal β that's the power of the Recipes Catalog!
Data Source
The Recipes Catalog Screen pulls its data from a public API endpoint. Here's a breakdown:
- API Endpoint: 
/recipes.json(GET) β No authentication required. - Base URL: 
https://usetrmnl.com - Status: β οΈ Alpha testing β This means the endpoint may be moved to 
/api/recipesor/api/pluginsbefore the end of 2025, so keep an eye out for updates. 
API Capabilities
The API offers a couple of key functionalities:
List Recipes Endpoint:
- URL: 
GET /recipes.json - Query Parameters (all optional):
search: Use this to filter recipes based on a search term. This is super handy for quickly finding what you need!sort-by: Sort the results byoldest,newest,popularity,fork, orinstall. Talk about flexibility!page: Specify the page number for pagination (default is 1). No more endless scrolling!per_page: Define the number of items per page (default is 25). Customize your browsing experience!
 
Get Single Recipe Endpoint:
- URL: 
GET /recipes/{id}.json - This endpoint returns detailed information about a specific recipe. Perfect for when you want to dive deep into the details.
 
Recipe Data Model
Each recipe in the catalog includes a wealth of information. Hereβs a glimpse:
- Basic Info: 
id,name,icon_url,screenshot_urlβ The essentials for identifying and visualizing a recipe. - Author: 
author_bio(object with a description field) β Learn about the creators behind these awesome plugins. - Configuration: 
custom_fields(array of field configurations) β This is where things get interesting! Fields can be of types likestring,select,author_bio, etc., and have properties such askeyname,name,description,help_text,required,options, anddefault. It's like a blueprint for setting up the plugin. - Statistics: 
statsobject with:installs: Number of installations β See how popular a plugin is.forks: Number of forks/copies β Discover how many users are building upon the recipe.
 
Pagination Response
The API also provides pagination metadata to make browsing a breeze:
total: Total number of results β Know the scale of available recipes.from: Starting index β Understand your position in the catalog.to: Ending index β Track your browsing progress.per_page: Items per page β Customize your view.current_page: Current page number β Keep tabs on your location.prev_page_url: Previous page URL (or null) β Navigate back effortlessly.next_page_url: Next page URL (or null) β Jump to the next set of recipes.
Design Requirements
Material 3 Compliance
CRITICAL: The UI components MUST adhere to Material 3 design guidelines. This ensures a consistent and modern user experience.
- 
Use Material 3 Components Only:
androidx.compose.material3.*(NOTmaterialormaterial2)- Components such as 
Scaffold,TopAppBar,SearchBar,Card,FilterChip,LazyColumn, andListItemare essential. 
 - 
Theme-Aware Colors:
- NEVER use hardcoded colors (e.g., 
Color(0xFF...),Color.Red). Always leverageMaterialTheme.colorScheme.*. - Key color roles include:
primary,onPrimaryβ Main brand colorsprimaryContainer,onPrimaryContainerβ For filled componentssurface,onSurfaceβ Backgrounds and textsurfaceVariant,onSurfaceVariantβ Alternative surfacesoutline,outlineVariantβ Borders and dividers
 
 - NEVER use hardcoded colors (e.g., 
 - 
Typography:
- Utilize 
MaterialTheme.typography.*for all text elements. - Available styles include 
titleLarge/Medium/Small,bodyLarge/Medium/Small, andlabelLarge/Medium/Small. 
 - Utilize 
 - 
Dynamic Color Support:
- The app employs 
dynamicColor = truefor Android 12+ wallpaper theming. This means all colors must work seamlessly in both light and dark themes, offering users a personalized experience. 
 - The app employs 
 - 
Edge-to-Edge Display:
- Use 
Modifier.padding(innerPadding)withScaffoldto ensure the content respects system bars (status and navigation bars), creating a modern, immersive interface. 
 - Use 
 
Screen Layout
The screen layout is thoughtfully designed to provide an intuitive browsing experience. Imagine a structure like this:
βββββββββββββββββββββββββββββββββββββββ
β β Recipes Catalog                   β β TopAppBar
βββββββββββββββββββββββββββββββββββββββ€
β π Search recipes...          [x]   β β SearchBar (Material 3)
βββββββββββββββββββββββββββββββββββββββ€
β [Newest] [Popular] [Most Installed] β β FilterChip Row (sort options)
βββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββββββββββββββββββββββ β
β β π€οΈ  Weather Chum          β    β β β Card with icon + details
β β 1,230 installs β’ 1 fork         β β
β βββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββββββββ β
β β π¬  Matrix                 β    β β
β β 176 forks β’ 25 installs         β β
β βββββββββββββββββββββββββββββββββββ β
β ...                                 β
β βββββββββββββββββββββββββββββββββββ β
β β     [Load More] (page 2)        β β β Pagination button
β βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββ
Architecture Requirements
Circuit UDF Pattern
The project follows the Circuit UDF (Unidirectional Data Flow) architecture pattern. This ensures a clean and predictable flow of data and state management.
- Screen Definition:
 
@Parcelize
data object RecipesCatalogScreen : Screen {
    data class State(
        val recipes: List<Recipe> = emptyList(),
        val searchQuery: String = "",
        val selectedSort: SortOption = SortOption.NEWEST,
        val isLoading: Boolean = false,
        val isLoadingMore: Boolean = false,
        val error: String? = null,
        val currentPage: Int = 1,
        val hasMorePages: Boolean = false,
        val totalRecipes: Int = 0,
        val eventSink: (Event) -> Unit = {},
    ) : CircuitUiState
    sealed class Event : CircuitUiEvent {
        data object BackClicked : Event()
        data class SearchQueryChanged(val query: String) : Event()
        data object SearchClicked : Event()
        data object ClearSearchClicked : Event()
        data class SortSelected(val sort: SortOption) : Event()
        data class RecipeClicked(val recipe: Recipe) : Event()
        data object LoadMoreClicked : Event()
        data object RetryClicked : Event()
    }
}
enum class SortOption(val apiValue: String, val displayName: String) {
    NEWEST("newest", "Newest"),
    OLDEST("oldest", "Oldest"),
    POPULARITY("popularity", "Popular"),
    INSTALLS("install", "Most Installed"),
    FORKS("fork", "Most Forked"),
}
- Presenter (
RecipesCatalogPresenter.kt): 
@Inject
class RecipesCatalogPresenter(
    @Assisted private val navigator: Navigator,
    private val recipesRepository: RecipesRepository,
) : Presenter<RecipesCatalogScreen.State> {
    @Composable
    override fun present(): RecipesCatalogScreen.State {
        // Implement state management
        // - Fetch recipes from repository
        // - Handle search and sort
        // - Handle pagination
        // - Handle navigation
    }
}
- UI Content (
RecipesCatalogContent.kt): 
@CircuitInject(RecipesCatalogScreen::class, AppScope::class)
@Composable
fun RecipesCatalogContent(
    state: RecipesCatalogScreen.State,
    modifier: Modifier = Modifier,
) {
    // Implement UI composition
}
- Use 
@CircuitInjectannotation for both presenter and content. This is crucial for the Circuit architecture to function correctly. 
Data Layer
- Create Data Models in 
api/src/main/java/ink/trmnl/android/buddy/api/models/: 
@Serializable
data class Recipe(
    val id: Int,
    val name: String,
    @SerialName("icon_url")
    val iconUrl: String?,
    @SerialName("screenshot_url")
    val screenshotUrl: String?,
    @SerialName("author_bio")
    val authorBio: AuthorBio? = null,
    @SerialName("custom_fields")
    val customFields: List<CustomField> = emptyList(),
    val stats: RecipeStats,
)
@Serializable
data class AuthorBio(
    val keyname: String? = null,
    val name: String? = null,
    @SerialName("field_type")
    val fieldType: String? = null,
    val description: String? = null,
)
@Serializable
data class CustomField(
    val keyname: String,
    val name: String,
    @SerialName("field_type")
    val fieldType: String,
    val description: String? = null,
    val placeholder: String? = null,
    @SerialName("help_text")
    val helpText: String? = null,
    val required: Boolean? = null,
    val options: List<String>? = null,
    val default: String? = null,
)
@Serializable
data class RecipeStats(
    val installs: Int,
    val forks: Int,
)
@Serializable
data class RecipesResponse(
    val data: List<Recipe>,
    val total: Int,
    val from: Int,
    val to: Int,
    @SerialName("per_page")
    val perPage: Int,
    @SerialName("current_page")
    val currentPage: Int,
    @SerialName("prev_page_url")
    val prevPageUrl: String?,
    @SerialName("next_page_url")
    val nextPageUrl: String?,
)
@Serializable
data class RecipeDetailResponse(
    val data: Recipe,
)
- Add API Service Method in 
TrmnlApiService.kt: 
// Note: This is a public endpoint on usetrmnl.com, not the /api base URL
@GET("https://usetrmnl.com/recipes.json")
suspend fun getRecipes(
    @Query("search") search: String? = null,
    @Query("sort-by") sortBy: String? = null,
    @Query("page") page: Int? = null,
    @Query("per_page") perPage: Int? = null,
): ApiResult<RecipesResponse, ApiError>
@GET("https://usetrmnl.com/recipes/{id}.json")
suspend fun getRecipe(
    @Path("id") id: Int,
): ApiResult<RecipeDetailResponse, ApiError>
- Create Repository in 
data/RecipesRepository.kt: 
interface RecipesRepository {
    suspend fun getRecipes(
        search: String? = null,
        sortBy: String? = null,
        page: Int = 1,
        perPage: Int = 25,
    ): Result<RecipesResponse>
    
    suspend fun getRecipe(id: Int): Result<Recipe>
}
- Metro DI: Use 
@Injectconstructor injection, and@ContributesBindingfor implementations. This makes dependency management a breeze. 
UI Components
Let's break down the key UI components that bring this screen to life.
1. SearchBar (Top Section)
Use the Material 3 SearchBar component. Itβs your gateway to quick recipe discovery!
- Displays a search icon and the hint text