Skip to content

Commit 28a2dc8

Browse files
authored
feat: custom model management and provider reordering (#23)
* feat: custom model management and provider reordering * refactor: modulize home page * ui: align ProviderModelSheetContent with MD3 expressive
1 parent 6e7c739 commit 28a2dc8

51 files changed

Lines changed: 3654 additions & 1695 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 104 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
# CLAUDE.md
1+
# AGENTS.md
22

3-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
3+
This file provides guidance for AI agents working with code in this repository.
44

55
## Project Overview
66

7-
SummaryExpressive is an AI/LLM summarizer FOSS Android app that summarizes YouTube videos, web articles, images, and documents. It follows [MAD](https://developer.android.com/courses/pathways/android-architecture) principles using pure Kotlin + Jetpack Compose + Material 3 Expressive. The app is BYOK (Bring Your Own Key), allowing users to configure their own LLM API keys.
7+
SummaryExpressive is an AI/LLM summarizer FOSS Android app that summarizes YouTube/BiliBili videos, web articles, images, and documents. It follows [MAD (Modern Android Development)](https://developer.android.com/courses/pathways/android-architecture) principles using pure Kotlin + Jetpack Compose + Material 3 Expressive. The app is BYOK (Bring Your Own Key), allowing users to configure their own LLM API keys.
8+
9+
---
810

911
## Common Commands
1012

11-
### Building the App
13+
### Building
1214
```bash
1315
# Clean and build debug APK
1416
./gradlew clean assembleDebug
1517

1618
# Build release APK (requires keystore.properties setup)
1719
./gradlew assembleRelease
1820

19-
# Build specific variant
21+
# Build specific flavor variant
2022
./gradlew assembleGmsRelease
2123
./gradlew assembleStandaloneRelease
2224
```
@@ -26,56 +28,83 @@ SummaryExpressive is an AI/LLM summarizer FOSS Android app that summarizes YouTu
2628
# Run unit tests
2729
./gradlew clean test
2830

29-
# Run instrumented tests
30-
./gradlew connectedAndroidTest
31-
32-
# Run a specific test class
31+
# Run a specific unit test class
3332
./gradlew testDebugUnitTest --tests="me.nanova.summaryexpressive.ExampleUnitTest"
33+
34+
# Run instrumented tests on connected device/emulator
35+
./gradlew connectedAndroidTest
3436
```
3537

36-
### Code Quality
38+
### Code Quality & Lint
3739
```bash
3840
# Run lint analysis
3941
./gradlew lint
40-
41-
# Format code (follow Kotlin style guide)
4242
```
4343

44-
## Code Style Guidelines
45-
- Make the code structure as simple as possible
46-
- Never use full-qualified class name inline, use import statement instead
47-
- Follow [Kotlin style guide](https://developer.android.com/kotlin/style-guide)
48-
49-
## Dev Environment Tips
50-
- Use `./gradlew clean assembleDebug` to verify the build
51-
- Use `./gradlew clean test` to run tests
52-
- Configure SDK paths in `local.properties`
53-
- Set up signing keys in `keystore.properties` for release builds
54-
55-
## Architecture & Code Structure
56-
57-
### Technology Stack
58-
- **Language**: Kotlin
59-
- **UI Framework**: Jetpack Compose with Material 3 Expressive (alpha version for expressive features)
60-
- **DI**: Dagger/Hilt
61-
- **Database**: Room (SQLite)
62-
- **Networking**: Ktor 3.x
44+
> [!TIP]
45+
> Coding agents should use the `android-cli` skill for CLI workflows, SDK management, running/debugging apps on emulators/devices, UI inspection, and taking screenshots.
46+
47+
---
48+
49+
## Development Guidelines
50+
51+
### Code Style
52+
- **Simple structure**: Keep the code structure as simple and readable as possible.
53+
- **Imports**: Never use fully-qualified class names inline; always use import statements.
54+
- **Style guide**: Follow the [Kotlin Android Style Guide](https://developer.android.com/kotlin/style-guide).
55+
56+
### Material 3 Expressive UI
57+
- **Expressive Compliance**: Use the `material-3` skill to check and ensure that any newly added or updated UI complies with Material Design 3 Expressive guidelines (expressive shapes, spring motion physics, tonal elevation, dynamic color, and tokens).
58+
- **Material 3 Version**: The app targets Compose Material 3 Expressive features using `1.5.0-alpha26`.
59+
60+
---
61+
62+
## Architecture
63+
64+
### Architectural Patterns
65+
- **Layered Architecture & UDF**:
66+
- **UI Layer**: Compose + ViewModels exposing reactive `StateFlow`, adhering to Unidirectional Data Flow (UDF).
67+
- **Domain / Model Layer (`model/`, `exception/`)**: Pure domain models and centralized exceptions decoupled from UI and Data layers.
68+
- **Data Layer (`data/`)**: Repositories act as the Single Source of Truth (SSOT). ViewModels never interact directly with DAOs, DataStores, or raw network clients.
69+
- **Component Placement Conventions**:
70+
- **Global Reusable Components**: Place in `ui/component/` (e.g. `SummaryCard`, `LlmSwitcher`, `LlmIndicator`, `LogoIcon`, `ClickablePasteIcon`).
71+
- **Page-Specific Components**: Place alongside the screen in `ui/page/` (or `ui/page/<feature>/`) scoped to that specific screen/feature (e.g. `BilibiliLoginScreen.kt` sheet).
72+
- **Dependency Injection (Hilt)**:
73+
- All major dependencies are Hilt-injectable using `@Singleton` for app-wide dependencies or `@ActivityScoped` for activity-level dependencies.
74+
- **Database (Room)**:
75+
- Database name: `summary_expressive_db`.
76+
- Main entity: `HistorySummary` with `HistoryDao`.
77+
- Custom type converters reside in `data/converters/`.
78+
- **Custom Exceptions**:
79+
- Centralized in `exception/SummaryException.kt` with string resource localization support.
80+
81+
### Technology Stack & Key Dependencies
82+
- **Language**: Kotlin 2.4.x
83+
- **UI Framework**: Jetpack Compose with Material 3 Expressive (`1.5.0-alpha26` for expressive features)
84+
- **Dependency Injection**: Dagger / Hilt
85+
- **Database**: Room (SQLite) 2.8.x with Paging 3
86+
- **Networking**: Ktor Client 3.x
87+
- **LLM Integration**: Koog library (`ai.koog:koog-agents`, client executors for OpenAI, Gemini, Anthropic, DeepSeek, Mistral, Qwen, Ollama, OpenRouter)
88+
- **HTML Parsing**: Jsoup
89+
- **Image Loading**: Coil (`io.coil-kt:coil-compose`)
6390
- **Async**: Kotlin Coroutines + Flow
64-
- **ML Kit**: For text recognition from images (build flavor dependent)
65-
- **LLM Integration**: Koog library
91+
- **ML Kit**: Text recognition from images (Google Play Services / standalone bundled)
92+
93+
---
6694

67-
### Build Flavors
68-
The app uses two product flavors:
69-
- **gms**: Uses Google Play Services ML Kit (smaller APK size, requires Google Play Services)
70-
- **standalone**: Bundles ML model in APK (larger package size, works without Google Play Services)
95+
## Code Structure
7196

72-
Configure in `app/build.gradle.kts:58-68`.
97+
### Build Flavors & Distribution
98+
The app defines two product flavors under the `distribution` dimension (`app/build.gradle.kts`):
99+
- **`gms`**: Uses Google Play Services ML Kit (`com.google.android.gms:play-services-mlkit-text-recognition`). Smaller APK size, requires Google Play Services. Used for Google Play Store releases (signed with Google-managed key).
100+
- **`standalone`**: Bundles ML model in the APK (`com.google.mlkit:text-recognition`). Larger package size, functions offline without Google Play Services.
73101

74102
### Project Structure
75103

76104
#### Core Application (`app/src/main/kotlin/me/nanova/summaryexpressive/`)
77-
- **`App.kt`**: Application class with `@HiltAndroidApp` annotation
78-
- **`MainActivity.kt`**: Main activity handling deep links, share intents, and initialization
105+
- **`App.kt`**: Application entry point with `@HiltAndroidApp`
106+
- **`MainActivity.kt`**: Main activity handling deep links, share intents, and navigation
107+
- **`InstantSummaryActivity.kt`**: Overlay activity for instant summarization via share sheet or text selection
79108

80109
#### Dependency Injection (`di/AppModule.kt`)
81110
Hilt module providing:
@@ -140,54 +169,33 @@ Hilt module providing:
140169
- **`component/`**: Global reusable UI components (`SummaryCard`, `LlmSwitcher`, `LlmIndicator`, `LogoIcon`, `ClickablePasteIcon`)
141170
- **`theme/`**: Material 3 theming (colors, typography, theme)
142171

143-
#### Services
144-
- **`InstantSummaryActivity.kt`**: Overlay activity for instant summaries via share sheet
172+
---
145173

146-
### Supported Content Types
174+
## Supported Content & LLM Providers
147175

148-
| Type | Source | Processing Method |
149-
|------|--------|-------------------|
150-
| YouTube videos | Video URL | Transcript extraction via `YouTubeTranscriptTool` |
151-
| BiliBili videos | Video URL | Subtitle extraction via `BiliBiliSubtitleTool` |
152-
| Articles | URL | Content extraction via `ArticleExtractorTool` |
153-
| Images | File/URI | ML Kit text recognition (flavor-dependent) |
154-
| Documents | File/URI | File parsing via `FileExtractorTool` |
155-
| Text | Direct input | Direct LLM processing |
156-
157-
### LLM Providers
158-
159-
The app supports multiple LLM providers configured in settings:
160-
- **OpenAI**: gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, gpt-40, gpt-4o-mini, gpt-5, gpt-5-mini, gpt-5-nano, o1, o3, o3-mini, o4-mini
161-
- **Gemini**: gemini-2.0-flash, gemini-2.0-flash-001, gemini-2.0-flash-lite, gemini-2.0-flash-lite-001, gemini-2.5-flash, gemini-2.5-pro
162-
- **Claude**: claude-3-haiku, claude-3-opus, claude-3-5-haiku, claude-3-5-sonnet, claude-3-7-sonnet, claude-opus-4-0, claude-sonnet-4-0
163-
- **DeepSeek**: deepseek-chat, deepseek-reasoner
164-
- **Custom models**: User-configurable endpoints
165-
166-
## Key Configuration Files
167-
168-
### `build.gradle.kts` (root)
169-
Defines plugin versions:
170-
- Android Gradle Plugin: 9.3.1
171-
- Kotlin: 2.4.0
172-
- KSP: 2.3.11
173-
- Hilt: 2.60
174-
175-
### `app/build.gradle.kts`
176-
Main app configuration:
177-
- Min SDK: 33 (Android 13)
178-
- Target SDK: 37
179-
- Java 25 toolchain
180-
- Compose BOM: 2026.06.00
181-
- Room: 2.8.4
182-
- Key signing configuration in `keystore.properties`
183-
184-
### `local.properties`
185-
Local SDK paths (not tracked in git)
186-
187-
### `keystore.properties`
188-
Release signing keys (not tracked in git)
176+
### Supported Content Types
189177

190-
## Development Guidelines
178+
| Type | Source | Processing Method |
179+
|-----------------|--------------|---------------------------------------------------|
180+
| YouTube videos | Video URL | Transcript extraction via `YouTubeTranscriptTool` |
181+
| BiliBili videos | Video URL | Subtitle extraction via `BiliBiliSubtitleTool` |
182+
| Articles | URL | Content extraction via `ArticleExtractorTool` |
183+
| Images | File / URI | ML Kit text recognition (flavor-dependent) |
184+
| Documents | File / URI | File parsing via `FileExtractorTool` (PDF, DOCX) |
185+
| Text | Direct input | Direct LLM processing |
186+
187+
### Supported LLM Providers
188+
189+
- **OpenAI**
190+
- **Gemini**
191+
- **Claude**
192+
- **DeepSeek**
193+
- **Mistral**
194+
- **Ollama**
195+
- **OpenRouter**
196+
- **DashScope (Qwen)**
197+
- **Bedrock**
198+
- **Custom models** (OpenAI-compatible endpoints)
191199

192200
### Code Style
193201
- Follow [Kotlin Android Style Guide](https://developer.android.com/kotlin/style-guide)
@@ -203,62 +211,14 @@ Release signing keys (not tracked in git)
203211
- **Page-Specific Components**: Place alongside the screen in `ui/page/` (or `ui/page/<feature>/`) scoped specifically to that screen/feature (e.g. `BilibiliLoginScreen.kt` sheet).
204212
- **Custom Exceptions**: Centralized in `exception/SummaryException.kt` with string resource localization support.
205213

206-
### Dependency Injection
207-
- All major components are Hilt-injectable
208-
- Use `@Singleton` for app-wide dependencies
209-
- Use `@ActivityScoped` for activity-level dependencies (when needed)
210-
211-
### Room Database
212-
- Database: `summary_expressive_db`
213-
- Main entity: `HistorySummary` with DAO operations
214-
- Type converters in `converters/` package
215-
216-
### Material 3 Expressive
217-
The app uses Material 3 Expressive alpha features for enhanced UI. Material version: `1.5.0-alpha26`
218-
219-
## Testing
220-
- Unit tests in `src/test/kotlin/`
221-
- Instrumented tests in `src/androidTest/kotlin/`
222-
- Use `./gradlew test` for unit tests
223-
- Use `./gradlew connectedAndroidTest` for instrumented tests
224-
225-
## Build & Release
226-
227-
### Development
228-
- Debug builds are debuggable with application ID suffix `.debug`
229-
- Use `assembleDebug` for development builds
230-
231-
### Release
232-
- Release builds are minified and shrunk
233-
- Requires `keystore.properties` for signing
234-
- Two distribution channels:
235-
1. **GitHub Releases**: Both gms and standalone variants
236-
2. **Google Play Store**: GMS variant with Google-managed signing
237-
238-
### Version Info
239-
Current version: 1.3.2 (versionCode 49)
240-
- Update version in `app/build.gradle.kts:21-22`
241-
242-
## Permissions & Security
243-
The app likely requires:
244-
- Internet access (for LLM calls and content extraction)
245-
- Storage access (for document/image processing)
246-
- Clipboard access (for instant summary feature)
247-
- Overlay permission (for instant summary overlay)
248-
249-
## Known Dependencies
250-
- `ai.koog:koog-agents:1.1.1` - Kotlin-based LLM interactions
251-
- `io.ktor:ktor-client-android:3.5.2` - HTTP client
252-
- `org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0` - JSON serialization
253-
- `org.jsoup:jsoup:1.23.1` - HTML parsing
254-
- `io.coil-kt:coil-compose:2.7.0` - Image loading
255-
256-
## Build Warnings & Notes
257-
- ML Kit dependency is flavor-specific (see `app/build.gradle.kts:145-150`)
258-
- ProGuard/R8 rules configured in `app/proguard-rules.pro`
259-
- Some resources excluded from APK (see packaging section)
260-
- Lint rule: MissingTranslation disabled for localization flexibility
261-
262-
---
263-
264-
**Note**: Content from AGENTS.md has been merged into this file.
214+
### Key Configuration Files
215+
- **`build.gradle.kts`**: Root build file defining build plugins (AGP, Kotlin, KSP, Hilt).
216+
- **`app/build.gradle.kts`**: App build configuration (Min SDK: 33, Target SDK: 37, Java 25 toolchain, flavors, packaging).
217+
- **`gradle/libs.versions.toml`**: Centralized version catalog for dependencies and plugins.
218+
- **`local.properties`**: Local Android SDK paths (not tracked in git).
219+
- **`keystore.properties`**: Release signing credentials (not tracked in git).
220+
221+
### Build & Packaging Notes
222+
- **ProGuard / R8**: Release builds enable minification and resource shrinking with rules in `app/proguard-rules.pro`.
223+
- **Packaging Exclusions**: Certain license and netty property files are excluded from the APK in `packaging.resources`.
224+
- **Lint**: `MissingTranslation` rule is disabled in `app/build.gradle.kts` for localization flexibility.

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,19 @@ package previously.
7474
- Mistral
7575
- OpenRouter
7676
- Ollama
77+
- AWS Bedrock
78+
- Custom OpenAI-compatible endpoints
79+
80+
- **Custom Model Management**: Add, edit, delete, and drag-and-drop reorder models per provider with fallback to Koog defaults
81+
82+
- **Quick Model Switcher**: Easily switch providers and active models on the Home screen following your custom configured order
7783

7884
- **[Material 3 Expressive](https://m3.material.io/blog/building-with-m3-expressive) UI**: Engaging
7985
and easier to use, light/dark theme and dynamic color theme
8086

81-
- **Instant summarize via share sheet or text selection toolbar**: Convenient to trigger summarization, show result in overlayer
87+
- **Instant summarize via share sheet or text selection toolbar**: Convenient to trigger summarization, show result in overlay
8288

83-
- **Configurable LLM settings**
89+
- **Configurable LLM settings**: Custom API keys, base URLs, prompt customization, and summary lengths
8490

8591
- **Text to speech for the summaries with multilingual and speed adjustment support**
8692

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ dependencies {
189189

190190
// Testing
191191
testImplementation(libs.junit.jupiter)
192+
testImplementation(libs.kotlinx.coroutines.test)
192193
testRuntimeOnly(libs.junit.platform.launcher)
193194
androidTestImplementation(platform(libs.androidx.compose.bom))
194195
androidTestImplementation(libs.androidx.junit)

app/src/main/kotlin/me/nanova/summaryexpressive/MainActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class MainActivity : ComponentActivity() {
7676
viewModel.onEvent(AppStartAction(content))
7777
}
7878
}
79+
7980
Intent.ACTION_VIEW -> {
8081
if (intent.data?.host != "clipboard") return
8182
// To avoid re-triggering on configuration change, we clear the data.

app/src/main/kotlin/me/nanova/summaryexpressive/data/AIProviderConfigDao.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface AIProviderConfigDao {
1616

1717
@Query("SELECT * FROM ai_provider_config")
1818
fun getAllConfigsFlow(): Flow<List<AIProviderConfigEntity>>
19-
19+
2020
@Query("SELECT * FROM ai_provider_config")
2121
suspend fun getAllConfigs(): List<AIProviderConfigEntity>
2222

app/src/main/kotlin/me/nanova/summaryexpressive/data/AIProviderConfigEntity.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
package me.nanova.summaryexpressive.data
22

3+
import androidx.room.ColumnInfo
34
import androidx.room.Entity
45
import androidx.room.PrimaryKey
6+
import androidx.room.TypeConverters
7+
import me.nanova.summaryexpressive.data.converters.StringListConverter
58
import me.nanova.summaryexpressive.model.ProviderConfig
69
import me.nanova.summaryexpressive.util.SecurityUtil
710

811
@Entity(tableName = "ai_provider_config")
12+
@TypeConverters(StringListConverter::class)
913
data class AIProviderConfigEntity(
1014
@PrimaryKey val provider: String,
1115
val apiKey: String,
1216
val baseUrl: String,
13-
val model: String
17+
@ColumnInfo(name = "active_model") val activeModel: String,
18+
val models: List<String>? = null,
1419
) {
1520
fun toProviderConfig(): ProviderConfig {
1621
return ProviderConfig(
1722
apiKey = SecurityUtil.decrypt(apiKey),
1823
baseUrl = baseUrl,
19-
model = model
24+
activeModel = activeModel,
25+
models = models ?: emptyList()
2026
)
2127
}
2228

@@ -26,7 +32,8 @@ data class AIProviderConfigEntity(
2632
provider = provider,
2733
apiKey = SecurityUtil.encrypt(config.apiKey),
2834
baseUrl = config.baseUrl,
29-
model = config.model
35+
activeModel = config.activeModel,
36+
models = config.models.takeIf { it.isNotEmpty() }
3037
)
3138
}
3239
}

0 commit comments

Comments
 (0)