TRMNL Recipes Catalog: Search, Sort, And Discover Plugins

by SLV Team 58 views
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/recipes or /api/plugins before 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 by oldest, newest, popularity, fork, or install. 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 like string, select, author_bio, etc., and have properties such as keyname, name, description, help_text, required, options, and default. It's like a blueprint for setting up the plugin.
  • Statistics: stats object 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.

  1. Use Material 3 Components Only:

    • androidx.compose.material3.* (NOT material or material2)
    • Components such as Scaffold, TopAppBar, SearchBar, Card, FilterChip, LazyColumn, and ListItem are essential.
  2. Theme-Aware Colors:

    • NEVER use hardcoded colors (e.g., Color(0xFF...), Color.Red). Always leverage MaterialTheme.colorScheme.*.
    • Key color roles include:
      • primary, onPrimary – Main brand colors
      • primaryContainer, onPrimaryContainer – For filled components
      • surface, onSurface – Backgrounds and text
      • surfaceVariant, onSurfaceVariant – Alternative surfaces
      • outline, outlineVariant – Borders and dividers
  3. Typography:

    • Utilize MaterialTheme.typography.* for all text elements.
    • Available styles include titleLarge/Medium/Small, bodyLarge/Medium/Small, and labelLarge/Medium/Small.
  4. Dynamic Color Support:

    • The app employs dynamicColor = true for Android 12+ wallpaper theming. This means all colors must work seamlessly in both light and dark themes, offering users a personalized experience.
  5. Edge-to-Edge Display:

    • Use Modifier.padding(innerPadding) with Scaffold to ensure the content respects system bars (status and navigation bars), creating a modern, immersive interface.

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.

  1. 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"),
}
  1. 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
    }
}
  1. UI Content (RecipesCatalogContent.kt):
@CircuitInject(RecipesCatalogScreen::class, AppScope::class)
@Composable
fun RecipesCatalogContent(
    state: RecipesCatalogScreen.State,
    modifier: Modifier = Modifier,
) {
    // Implement UI composition
}
  1. Use @CircuitInject annotation for both presenter and content. This is crucial for the Circuit architecture to function correctly.

Data Layer

  1. 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,
)
  1. 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>
  1. 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>
}
  1. Metro DI: Use @Inject constructor injection, and @ContributesBinding for 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