Skip to content

Commit 6e7c739

Browse files
committed
refactor: project structure adjustment
1 parent 7e448ad commit 6e7c739

28 files changed

Lines changed: 242 additions & 151 deletions

AGENTS.md

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -79,31 +79,42 @@ Configure in `app/build.gradle.kts:58-68`.
7979

8080
#### Dependency Injection (`di/AppModule.kt`)
8181
Hilt module providing:
82-
- User preferences repository
83-
- Room database and DAO
84-
- History repository
82+
- Repositories (`UserPreferencesRepository`, `AIProviderConfigRepository`, `HistoryRepository`)
83+
- Room database and DAOs (`HistoryDao`, `AIProviderConfigDao`)
8584
- LLM handler
8685
- Ktor HTTP client with cookies and JSON serialization
8786

8887
#### Data Layer (`data/`)
89-
- **`AppDatabase.kt`**: Room database definition
90-
- **`HistoryDao.kt`**: DAO for history operations
91-
- **`HistoryRepository.kt`**: Repository managing history data
92-
- **`converters/`**: Type converters for Room (SummaryType, VideoSubtype, SummaryLength)
93-
94-
#### Domain Models (`model/`)
95-
- `SummaryData.kt`: Core summary data model
96-
- `HistorySummary.kt`: History entry model
88+
- **`local/database/`**: Room database, DAOs, entities, and type converters
89+
- `AppDatabase.kt`, `HistoryDao.kt`, `AIProviderConfigDao.kt`, `AIProviderConfigEntity.kt`, `converters/`
90+
- **`local/datastore/`**: User preferences ProtoBuf DataStore and serializer
91+
- `UserPreferencesSerializer.kt`
92+
- **`repository/`**: Single sources of truth for data access
93+
- `AIProviderConfigRepository.kt`: AI provider credentials and configurations
94+
- `HistoryRepository.kt`: Summarization history repository
95+
- `UserPreferencesRepository.kt`: User settings and preferences repository
96+
97+
#### Domain & Data Models (`model/`)
98+
- `ExtractedContent.kt`: Content extraction model
99+
- `HistorySummary.kt`: History entry entity/model
100+
- `ProviderConfig.kt`: AI provider configuration model
101+
- `SummaryData.kt`: Core summary data model interface
102+
- `SummaryLength.kt`: Summarization length enum
103+
- `SummaryOutput.kt`: LLM output model
104+
- `SummarySource.kt`: Input source model (Video, Article, Text, Document)
97105
- `SummaryType.kt`: Supported content types (YouTube, BiliBili, article, image, document, text)
106+
- `UserPreferences.kt`: User preferences state and settings model
98107
- `VideoSubtype.kt`: Video platform classifications
99-
- `SummaryException.kt`: Custom exception types
100-
- `ExtractedContent.kt`: Content extraction model
108+
109+
#### Custom Exceptions (`exception/`)
110+
- **`SummaryException.kt`**: Custom exception hierarchy with localization support
101111

102112
#### LLM Integration (`llm/`)
103113
- **`LLMHandler.kt`**: Core handler for LLM interactions, supports multiple providers
104-
- **`AIProvider.kt`**: Provider definitions (OpenAI, Gemini, Claude, DeepSeek)
114+
- **`AIProvider.kt`**: Provider definitions (OpenAI, Gemini, Claude, DeepSeek, etc.)
105115
- **`Prompts.kt`**: Prompt templates for different content types
106116
- **`CustomModel.kt`**: Custom model configuration
117+
- **`GeminiSanitizingHttpClientEngine.kt`**: Engine decorator for Google Gemini compatibility
107118
- **`tools/`**: Extraction tools
108119
- `YouTubeTranscriptTool.kt`: YouTube transcript extraction
109120
- `BiliBiliSubtitleTool.kt`: BiliBili subtitle extraction
@@ -119,13 +130,14 @@ Hilt module providing:
119130
#### UI Layer (`ui/`)
120131
- **`AppNavigation.kt`**: Navigation graph setup
121132
- **`Nav.kt`**: Route definitions
122-
- **`page/`**: Screen composables
133+
- **`page/`**: Screen composables & page-specific subcomponents
123134
- `HomeScreen.kt`: Main summary screen
124135
- `HistoryScreen.kt`: History browser with paging
125136
- `SettingsScreen.kt`: App configuration
137+
- `AdvancedSetupScreen.kt`: Advanced prompt setup
126138
- `OnboardingScreen.kt`: First-run setup
127-
- `BilibiliLoginScreen.kt`: BiliBili authentication
128-
- **`component/`**: Reusable UI components
139+
- `BilibiliLoginScreen.kt`: BiliBili authentication sheet
140+
- **`component/`**: Global reusable UI components (`SummaryCard`, `LlmSwitcher`, `LlmIndicator`, `LogoIcon`, `ClickablePasteIcon`)
129141
- **`theme/`**: Material 3 theming (colors, typography, theme)
130142

131143
#### Services
@@ -183,11 +195,13 @@ Release signing keys (not tracked in git)
183195
- Follow Android best practices
184196

185197
### Architecture Patterns
186-
- MVVM (Model-View-ViewModel)
187-
- Repository pattern for data access
188-
- Hilt for dependency injection
189-
- StateFlow for reactive UI state
190-
- Sealed classes for type-safe navigation and state management
198+
- **Layered Architecture & UDF**: UI Layer (Compose + ViewModels with StateFlow), Domain/Model Layer (`model/`, `exception/`), and Data Layer (`data/repository/`, `data/local/`).
199+
- **Repository Pattern**: Repositories act as the Single Source of Truth (SSOT). ViewModels never interact directly with DAOs, DataStores, or raw network clients.
200+
- **Dependency Inversion**: Models and domain logic are decoupled from UI and ViewModel layers.
201+
- **Component Placement Conventions**:
202+
- **Global Reusable Components**: Place in `ui/component/` (e.g. `SummaryCard`, `LlmSwitcher`, `LlmIndicator`, `LogoIcon`, `ClickablePasteIcon`).
203+
- **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).
204+
- **Custom Exceptions**: Centralized in `exception/SummaryException.kt` with string resource localization support.
191205

192206
### Dependency Injection
193207
- All major components are Hilt-injectable

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,21 @@ package previously.
8686

8787
- **History search**
8888

89+
## 🏗️ Architecture
90+
91+
The app follows Google's [official Android Architecture Recommendations](https://developer.android.com/topic/architecture/recommendations) and [Modern Android Development (MAD)](https://developer.android.com/courses/pathways/android-architecture) best practices:
92+
93+
- **Layered Architecture**:
94+
- **UI Layer**: Built with Jetpack Compose and Material 3 Expressive, powered by ViewModels exposing reactive `StateFlow` and following Unidirectional Data Flow (UDF).
95+
- **Domain & Model Layer (`model/`, `exception/`)**: Pure domain models and centralized custom exceptions decoupled from UI and data layers.
96+
- **Data Layer (`data/`)**: Repositories act as the Single Source of Truth (SSOT), encapsulating Room SQLite DB, ProtoBuf DataStore, Ktor network client, and Koog LLM engine.
97+
- **Dependency Injection**: Powered by Dagger Hilt for loose coupling and testability.
98+
- **Asynchronous Operations**: Kotlin Coroutines + Flow for reactive, main-safe async streams.
99+
89100
## 🌟 Credits
90101

91102
- The [original idea](https://github.com/talosross/SummaryYou)
92103
from [talosross](https://github.com/talosross)
93104

94105
- [Koog](https://koog.ai) for kotlin-based LLM interactions
106+

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package me.nanova.summaryexpressive.data
22

33
import androidx.room.Entity
44
import androidx.room.PrimaryKey
5-
import me.nanova.summaryexpressive.ProviderConfig
5+
import me.nanova.summaryexpressive.model.ProviderConfig
66
import me.nanova.summaryexpressive.util.SecurityUtil
77

88
@Entity(tableName = "ai_provider_config")

app/src/main/kotlin/me/nanova/summaryexpressive/data/converters/SummaryLengthConverter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package me.nanova.summaryexpressive.data.converters
22

33
import androidx.room.TypeConverter
4-
import me.nanova.summaryexpressive.llm.SummaryLength
4+
import me.nanova.summaryexpressive.model.SummaryLength
55

66
class SummaryLengthConverter {
77
@TypeConverter

app/src/main/kotlin/me/nanova/summaryexpressive/UserPreferencesSerializer.kt renamed to app/src/main/kotlin/me/nanova/summaryexpressive/data/local/datastore/UserPreferencesSerializer.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
package me.nanova.summaryexpressive
1+
package me.nanova.summaryexpressive.data.local.datastore
22

3+
import android.content.Context
34
import androidx.datastore.core.CorruptionException
5+
import androidx.datastore.core.DataStore
46
import androidx.datastore.core.Serializer
7+
import androidx.datastore.dataStore
58
import kotlinx.coroutines.Dispatchers
69
import kotlinx.coroutines.withContext
710
import kotlinx.serialization.ExperimentalSerializationApi
811
import kotlinx.serialization.SerializationException
912
import kotlinx.serialization.protobuf.ProtoBuf
13+
import me.nanova.summaryexpressive.model.UserPreferences
1014
import java.io.InputStream
1115
import java.io.OutputStream
1216

17+
val Context.userPreferencesDataStore: DataStore<UserPreferences> by dataStore(
18+
fileName = "user_prefs.pb",
19+
serializer = UserPreferencesSerializer
20+
)
21+
1322
object UserPreferencesSerializer : Serializer<UserPreferences> {
1423
override val defaultValue: UserPreferences = UserPreferences()
1524

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package me.nanova.summaryexpressive.data.repository
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.map
5+
import me.nanova.summaryexpressive.data.AIProviderConfigDao
6+
import me.nanova.summaryexpressive.data.AIProviderConfigEntity
7+
import me.nanova.summaryexpressive.model.ProviderConfig
8+
import javax.inject.Inject
9+
import javax.inject.Singleton
10+
11+
@Singleton
12+
class AIProviderConfigRepository @Inject constructor(
13+
private val aiProviderConfigDao: AIProviderConfigDao
14+
) {
15+
val providerConfigsFlow: Flow<Map<String, ProviderConfig>> =
16+
aiProviderConfigDao.getAllConfigsFlow().map { entities ->
17+
entities.associate { it.provider to it.toProviderConfig() }
18+
}
19+
20+
fun getConfigFlow(provider: String): Flow<ProviderConfig?> =
21+
aiProviderConfigDao.getConfigFlow(provider).map { it?.toProviderConfig() }
22+
23+
suspend fun getConfig(provider: String): ProviderConfig? =
24+
aiProviderConfigDao.getConfig(provider)?.toProviderConfig()
25+
26+
suspend fun saveConfig(provider: String, config: ProviderConfig) {
27+
aiProviderConfigDao.insertConfig(
28+
AIProviderConfigEntity.fromProviderConfig(provider, config)
29+
)
30+
}
31+
32+
suspend fun updateApiKey(provider: String, apiKey: String) {
33+
val current = getConfig(provider) ?: ProviderConfig()
34+
saveConfig(provider, current.copy(apiKey = apiKey))
35+
}
36+
37+
suspend fun updateBaseUrl(provider: String, baseUrl: String) {
38+
val current = getConfig(provider) ?: ProviderConfig()
39+
saveConfig(provider, current.copy(baseUrl = baseUrl))
40+
}
41+
42+
suspend fun updateModel(provider: String, model: String) {
43+
val current = getConfig(provider) ?: ProviderConfig()
44+
saveConfig(provider, current.copy(model = model))
45+
}
46+
}

app/src/main/kotlin/me/nanova/summaryexpressive/data/HistoryRepository.kt renamed to app/src/main/kotlin/me/nanova/summaryexpressive/data/repository/HistoryRepository.kt

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

33
import androidx.paging.Pager
44
import androidx.paging.PagingConfig
55
import androidx.paging.PagingData
66
import kotlinx.coroutines.flow.Flow
7+
import me.nanova.summaryexpressive.data.HistoryDao
78
import me.nanova.summaryexpressive.model.HistorySummary
89
import me.nanova.summaryexpressive.model.SummaryType
910
import javax.inject.Inject
@@ -30,4 +31,4 @@ class HistoryRepository @Inject constructor(private val historyDao: HistoryDao)
3031
suspend fun deleteSummary(id: String) {
3132
historyDao.deleteById(id)
3233
}
33-
}
34+
}

app/src/main/kotlin/me/nanova/summaryexpressive/UserPreferencesRepository.kt renamed to app/src/main/kotlin/me/nanova/summaryexpressive/data/repository/UserPreferencesRepository.kt

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,20 @@
1-
package me.nanova.summaryexpressive
1+
package me.nanova.summaryexpressive.data.repository
22

33
import android.content.Context
4-
import androidx.datastore.core.DataStore
5-
import androidx.datastore.dataStore
4+
import dagger.hilt.android.qualifiers.ApplicationContext
65
import kotlinx.coroutines.flow.Flow
76
import kotlinx.coroutines.flow.catch
8-
import kotlinx.serialization.Serializable
9-
import me.nanova.summaryexpressive.llm.SummaryLength
7+
import me.nanova.summaryexpressive.data.local.datastore.userPreferencesDataStore
8+
import me.nanova.summaryexpressive.model.UserPreferences
109
import java.io.IOException
11-
12-
val Context.dataStore: DataStore<UserPreferences> by dataStore(
13-
fileName = "user_prefs.pb",
14-
serializer = UserPreferencesSerializer
15-
)
16-
17-
data class ProviderConfig(
18-
val apiKey: String = "",
19-
val baseUrl: String = "",
20-
val model: String = ""
21-
)
22-
23-
@Serializable
24-
data class UserPreferences(
25-
// state
26-
val isOnboarded: Boolean = false,
27-
// settings
28-
val useOriginalLanguage: Boolean = true,
29-
val dynamicColor: Boolean = true,
30-
val theme: Int = 0,
31-
val aiProvider: String? = null,
32-
val showLength: Boolean = true,
33-
val summaryLength: String = SummaryLength.MEDIUM.name,
34-
val autoExtractUrl: Boolean = true,
35-
val sessData: String = "",
36-
val sessDataExpires: Long = 0L,
37-
val isAppendMode: Boolean = true,
38-
val customBasePrompt: String = "",
39-
val additionalSystemPrompt: String = ""
40-
)
41-
42-
class UserPreferencesRepository(private val context: Context) {
43-
val preferencesFlow: Flow<UserPreferences> = context.dataStore.data
10+
import javax.inject.Inject
11+
import javax.inject.Singleton
12+
13+
@Singleton
14+
class UserPreferencesRepository @Inject constructor(
15+
@ApplicationContext private val context: Context
16+
) {
17+
val preferencesFlow: Flow<UserPreferences> = context.userPreferencesDataStore.data
4418
.catch { exception ->
4519
if (exception is IOException) {
4620
emit(UserPreferences())
@@ -50,7 +24,7 @@ class UserPreferencesRepository(private val context: Context) {
5024
}
5125

5226
private suspend fun updatePreferences(transform: suspend (UserPreferences) -> UserPreferences) {
53-
context.dataStore.updateData { transform(it) }
27+
context.userPreferencesDataStore.updateData { transform(it) }
5428
}
5529

5630
suspend fun setUseOriginalLanguage(value: Boolean) =
@@ -88,4 +62,4 @@ class UserPreferencesRepository(private val context: Context) {
8862

8963
suspend fun setAdditionalSystemPrompt(value: String) =
9064
updatePreferences { it.copy(additionalSystemPrompt = value) }
91-
}
65+
}

app/src/main/kotlin/me/nanova/summaryexpressive/di/AppModule.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
1515
import io.ktor.client.plugins.cookies.AcceptAllCookiesStorage
1616
import io.ktor.client.plugins.cookies.HttpCookies
1717
import io.ktor.serialization.kotlinx.json.json
18-
import me.nanova.summaryexpressive.UserPreferencesRepository
1918
import me.nanova.summaryexpressive.data.AIProviderConfigDao
2019
import me.nanova.summaryexpressive.data.AppDatabase
2120
import me.nanova.summaryexpressive.data.HistoryDao
22-
import me.nanova.summaryexpressive.data.HistoryRepository
21+
import me.nanova.summaryexpressive.data.repository.AIProviderConfigRepository
22+
import me.nanova.summaryexpressive.data.repository.HistoryRepository
23+
import me.nanova.summaryexpressive.data.repository.UserPreferencesRepository
2324
import me.nanova.summaryexpressive.llm.LLMHandler
2425
import javax.inject.Singleton
2526

@@ -71,13 +72,20 @@ object AppModule {
7172
return HistoryRepository(historyDao)
7273
}
7374

75+
@Provides
76+
@Singleton
77+
fun provideAIProviderConfigRepository(aiProviderConfigDao: AIProviderConfigDao): AIProviderConfigRepository {
78+
return AIProviderConfigRepository(aiProviderConfigDao)
79+
}
80+
7481
@Provides
7582
@Singleton
7683
fun provideLLMHandler(
7784
@ApplicationContext context: Context,
78-
httpClient: HttpClient
85+
httpClient: HttpClient,
86+
userPreferencesRepository: UserPreferencesRepository
7987
): LLMHandler {
80-
return LLMHandler(context, httpClient)
88+
return LLMHandler(context, httpClient, userPreferencesRepository)
8189
}
8290

8391
@Provides

app/src/main/kotlin/me/nanova/summaryexpressive/model/SummaryException.kt renamed to app/src/main/kotlin/me/nanova/summaryexpressive/exception/SummaryException.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.nanova.summaryexpressive.model
1+
package me.nanova.summaryexpressive.exception
22

33
import androidx.annotation.StringRes
44
import me.nanova.summaryexpressive.R
@@ -96,4 +96,4 @@ sealed class SummaryException(message: String) : Exception(message) {
9696
}
9797
}
9898
}
99-
}
99+
}

0 commit comments

Comments
 (0)