├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── github
│ │ └── yohannestz
│ │ └── satori
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_satori_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── yohannestz
│ │ │ └── satori
│ │ │ ├── App.kt
│ │ │ ├── data
│ │ │ ├── local
│ │ │ │ ├── DatabaseConverters.kt
│ │ │ │ ├── DatabaseMigrations.kt
│ │ │ │ ├── SatoriDatabase.kt
│ │ │ │ ├── itembookmarks
│ │ │ │ │ ├── ItemDao.kt
│ │ │ │ │ ├── ItemEntity.kt
│ │ │ │ │ └── ItemMapper.kt
│ │ │ │ └── searchhistory
│ │ │ │ │ ├── SearchHistoryDao.kt
│ │ │ │ │ ├── SearchHistoryEntity.kt
│ │ │ │ │ └── SearchHistoryMapper.kt
│ │ │ ├── model
│ │ │ │ ├── Filter.kt
│ │ │ │ ├── OrderBy.kt
│ │ │ │ ├── PrintType.kt
│ │ │ │ ├── ViewMode.kt
│ │ │ │ ├── VolumeCategory.kt
│ │ │ │ ├── contributors
│ │ │ │ │ └── Contributor.kt
│ │ │ │ └── volume
│ │ │ │ │ ├── AccessInfo.kt
│ │ │ │ │ ├── BookMarkItem.kt
│ │ │ │ │ ├── Dimensions.kt
│ │ │ │ │ ├── Epub.kt
│ │ │ │ │ ├── ImageLinks.kt
│ │ │ │ │ ├── IndustryIdentifier.kt
│ │ │ │ │ ├── Item.kt
│ │ │ │ │ ├── PanelizationSummary.kt
│ │ │ │ │ ├── Pdf.kt
│ │ │ │ │ ├── ReadingModes.kt
│ │ │ │ │ ├── SaleInfo.kt
│ │ │ │ │ ├── SearchHistory.kt
│ │ │ │ │ ├── SearchInfo.kt
│ │ │ │ │ ├── Volume.kt
│ │ │ │ │ ├── VolumeDetail.kt
│ │ │ │ │ └── VolumeInfo.kt
│ │ │ ├── remote
│ │ │ │ ├── KtorClient.kt
│ │ │ │ └── service
│ │ │ │ │ ├── GithubApi.kt
│ │ │ │ │ └── GoogleBooksApi.kt
│ │ │ └── repository
│ │ │ │ ├── BookMarkRepository.kt
│ │ │ │ ├── BookRepository.kt
│ │ │ │ ├── GithubRepository.kt
│ │ │ │ ├── PreferencesRepository.kt
│ │ │ │ └── SearchHistoryRepository.kt
│ │ │ ├── di
│ │ │ ├── DatabaseModule.kt
│ │ │ ├── DatastoreModule.kt
│ │ │ ├── NetworkModule.kt
│ │ │ ├── RepositoryModule.kt
│ │ │ └── ViewModelModule.kt
│ │ │ ├── ui
│ │ │ ├── about
│ │ │ │ ├── AboutUiState.kt
│ │ │ │ ├── AboutView.kt
│ │ │ │ ├── AboutViewModel.kt
│ │ │ │ └── composable
│ │ │ │ │ └── ContributorsItem.kt
│ │ │ ├── base
│ │ │ │ ├── BottomDestination.kt
│ │ │ │ ├── StartTab.kt
│ │ │ │ ├── TabRowItem.kt
│ │ │ │ ├── ThemeStyle.kt
│ │ │ │ ├── event
│ │ │ │ │ ├── PagedUiEvent.kt
│ │ │ │ │ └── UiEvent.kt
│ │ │ │ ├── navigation
│ │ │ │ │ ├── NavActionManager.kt
│ │ │ │ │ └── Route.kt
│ │ │ │ ├── state
│ │ │ │ │ ├── PagedUiState.kt
│ │ │ │ │ └── UiState.kt
│ │ │ │ └── viewmodel
│ │ │ │ │ └── BaseViewModel.kt
│ │ │ ├── bookmarks
│ │ │ │ ├── BookMarksEvent.kt
│ │ │ │ ├── BookMarksUiState.kt
│ │ │ │ ├── BookMarksView.kt
│ │ │ │ ├── BookMarksViewModel.kt
│ │ │ │ └── composables
│ │ │ │ │ ├── BookMarksGridItem.kt
│ │ │ │ │ └── BookMarksListItem.kt
│ │ │ ├── composables
│ │ │ │ ├── BaseListItem.kt
│ │ │ │ ├── CommonIconButton.kt
│ │ │ │ ├── CommonText.kt
│ │ │ │ ├── DefaultScaffoldWithMediumTopAppBar.kt
│ │ │ │ ├── DefaultScaffoldWithSmallTopAppBar.kt
│ │ │ │ ├── DefaultScaffoldWithTopAppBar.kt
│ │ │ │ ├── DefaultTopAppBar.kt
│ │ │ │ ├── HorizontalListHeader.kt
│ │ │ │ ├── HorizontalPlaceHolder.kt
│ │ │ │ ├── LazyListState.kt
│ │ │ │ ├── PosterImage.kt
│ │ │ │ ├── ScoreIndicator.kt
│ │ │ │ ├── TabRowWithPager.kt
│ │ │ │ ├── TextIcon.kt
│ │ │ │ ├── TopBannerView.kt
│ │ │ │ └── preferences
│ │ │ │ │ ├── ListPreferenceView.kt
│ │ │ │ │ ├── PlainPreferenceView.kt
│ │ │ │ │ └── SwitchPreferenceView.kt
│ │ │ ├── details
│ │ │ │ ├── VolumeDetailEvent.kt
│ │ │ │ ├── VolumeDetailUiState.kt
│ │ │ │ ├── VolumeDetailView.kt
│ │ │ │ ├── VolumeDetailViewModel.kt
│ │ │ │ └── composables
│ │ │ │ │ ├── InfoView.kt
│ │ │ │ │ └── InfoViewWithContent.kt
│ │ │ ├── home
│ │ │ │ ├── HomeEvent.kt
│ │ │ │ ├── HomeUiState.kt
│ │ │ │ ├── HomeView.kt
│ │ │ │ ├── HomeViewModel.kt
│ │ │ │ └── composables
│ │ │ │ │ ├── CategoriesCard.kt
│ │ │ │ │ └── HorizontalBookItem.kt
│ │ │ ├── latest
│ │ │ │ ├── LatestEvent.kt
│ │ │ │ ├── LatestUiState.kt
│ │ │ │ ├── LatestView.kt
│ │ │ │ ├── LatestViewModel.kt
│ │ │ │ └── composable
│ │ │ │ │ └── LatestListItem.kt
│ │ │ ├── main
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── MainNavigation.kt
│ │ │ │ ├── MainViewModel.kt
│ │ │ │ └── composables
│ │ │ │ │ ├── MainBottomNavBar.kt
│ │ │ │ │ ├── MainNavigationRail.kt
│ │ │ │ │ └── MainTopAppBar.kt
│ │ │ ├── more
│ │ │ │ ├── MoreEvent.kt
│ │ │ │ ├── MoreView.kt
│ │ │ │ ├── MoreViewViewModel.kt
│ │ │ │ └── composable
│ │ │ │ │ ├── MoreItem.kt
│ │ │ │ │ └── SendFeedbackDialog.kt
│ │ │ ├── search
│ │ │ │ ├── SearchEvent.kt
│ │ │ │ ├── SearchUiState.kt
│ │ │ │ ├── SearchView.kt
│ │ │ │ ├── SearchViewModel.kt
│ │ │ │ └── composable
│ │ │ │ │ ├── NoResultsText.kt
│ │ │ │ │ ├── SearchHistoryItem.kt
│ │ │ │ │ ├── SearchListGridItem.kt
│ │ │ │ │ └── SearchListItem.kt
│ │ │ ├── settings
│ │ │ │ ├── SettingsEvent.kt
│ │ │ │ ├── SettingsUiState.kt
│ │ │ │ ├── SettingsView.kt
│ │ │ │ ├── SettingsViewModel.kt
│ │ │ │ └── composables
│ │ │ │ │ └── SettingsTitle.kt
│ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── volumelist
│ │ │ │ ├── VolumeListEvent.kt
│ │ │ │ ├── VolumeListUiState.kt
│ │ │ │ ├── VolumeListView.kt
│ │ │ │ ├── VolumeListViewModel.kt
│ │ │ │ └── composables
│ │ │ │ ├── VolumeListGridItem.kt
│ │ │ │ └── VolumeListItem.kt
│ │ │ └── utils
│ │ │ ├── Constants.kt
│ │ │ └── Extensions.kt
│ └── res
│ │ ├── drawable-anydpi
│ │ └── ic_telegram_icon.xml
│ │ ├── drawable-hdpi
│ │ └── ic_telegram_icon.png
│ │ ├── drawable-mdpi
│ │ └── ic_telegram_icon.png
│ │ ├── drawable-xhdpi
│ │ └── ic_telegram_icon.png
│ │ ├── drawable-xxhdpi
│ │ └── ic_telegram_icon.png
│ │ ├── drawable
│ │ ├── ic_book_spark_4.xml
│ │ ├── ic_content_copy_24.xml
│ │ ├── ic_expand_less_24.xml
│ │ ├── ic_expand_more_24.xml
│ │ ├── ic_github_icon.xml
│ │ ├── ic_history_24.xml
│ │ ├── ic_info_sided.xml
│ │ ├── ic_link_outward_24.xml
│ │ ├── ic_open_in_browser.xml
│ │ ├── ic_outline_collections_bookmark_24.xml
│ │ ├── ic_outline_fire_24.xml
│ │ ├── ic_outline_home_24.xml
│ │ ├── ic_round_arrow_back_24.xml
│ │ ├── ic_round_arrow_forward_24.xml
│ │ ├── ic_round_attach_email_24.xml
│ │ ├── ic_round_bug_report_24.xml
│ │ ├── ic_round_campaign_24.xml
│ │ ├── ic_round_close_24.xml
│ │ ├── ic_round_cloud_download_24.xml
│ │ ├── ic_round_collections_bookmark_24.xml
│ │ ├── ic_round_color_lens_24.xml
│ │ ├── ic_round_delete_24.xml
│ │ ├── ic_round_feedback_24.xml
│ │ ├── ic_round_fire_24.xml
│ │ ├── ic_round_format_list_bulleted_24.xml
│ │ ├── ic_round_grid_view_24.xml
│ │ ├── ic_round_home_24.xml
│ │ ├── ic_round_info_24.xml
│ │ ├── ic_round_more_horiz_24.xml
│ │ ├── ic_round_power_settings_new_24.xml
│ │ ├── ic_round_search_24.xml
│ │ ├── ic_round_settings_24.xml
│ │ ├── ic_round_share_24.xml
│ │ ├── ic_round_star_24.xml
│ │ ├── ic_round_view_list_24.xml
│ │ ├── ic_satori_launcher.xml
│ │ ├── ic_satori_launcher_foreground.xml
│ │ └── ic_star_filled_20.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_satori_launcher.xml
│ │ └── ic_satori_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_satori_launcher.webp
│ │ └── ic_satori_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_satori_launcher.webp
│ │ └── ic_satori_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_satori_launcher.webp
│ │ └── ic_satori_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_satori_launcher.webp
│ │ └── ic_satori_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_satori_launcher.webp
│ │ └── ic_satori_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_satori_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── github
│ └── yohannestz
│ └── satori
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshots
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── banner.png
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /app/release
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Satori
4 |
5 |
6 | Welcome to Satori, an app to explore and discover books using the Google Books API. "Satori" means "enlightenment" in Japanese, and the app aims to bring knowledge and light through books.
7 |
8 | ## Download
9 |
10 | You can download the latest release from [here](https://github.com/yohannesTz/satori/releases).
11 |
12 | ## Screenshots
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ## Features
23 |
24 | - **Search for Books**: Find books by title, author, or genre.
25 | - **View Book Details**: Get detailed information about books including title, author, description, and cover image.
26 | - **Favorite Books**: Save your favorite books for later.
27 | - **Smooth UI**: Built with modern Jetpack Compose to ensure a smooth user experience.
28 |
29 | ## Tech Stack
30 |
31 | - **Jetpack Compose**: For building the user interface.
32 | - **MVVM Architecture**: The app uses Model-View-ViewModel pattern to separate UI from business logic.
33 | - **Koin**: For dependency injection.
34 | - **Ktor**: For handling network requests to Google Books API.
35 | - **Room Database**: To store and manage your favorite books offline.
36 | - **Coil**: For loading images efficiently in Compose.
37 | - **AndroidX**: Core libraries like Lifecycle, Navigation, and DataStore for better state and data management.
38 |
39 | ## Libraries Used
40 |
41 | ### Core/AndroidX
42 | - `androidx.core:core-ktx`: Kotlin extensions for Android core library.
43 | - `androidx.lifecycle:lifecycle-runtime-ktx`: Lifecycle-aware components.
44 | - `androidx.activity:activity-compose`: Compose support for Activity.
45 | - `androidx.core:core-splashscreen`: For creating a splash screen.
46 | - `androidx.lifecycle:lifecycle-viewmodel-compose`: ViewModel integration with Compose.
47 | - `androidx.navigation:navigation-compose`: Navigation within Compose.
48 | - `androidx.palette:palette-ktx`: Generate palette colors from book covers.
49 | - `androidx.datastore:datastore-preferences`: Storing user preferences locally.
50 |
51 | ### Compose
52 | - `androidx.compose`: All essential Compose libraries.
53 | - `androidx.ui.graphics`: Compose support for drawing and graphics.
54 | - `androidx.material3`: Modern Material 3 UI elements for Compose.
55 |
56 | ### Room Database
57 | - `androidx.room`: For saving favorite books locally.
58 | - `androidx.work`: Background tasks to sync data.
59 | - `androidx.browser`: Opening links and web pages.
60 | - `androidx.material3.window-size`: Window size class support for responsive UI.
61 |
62 | ### Coil
63 | - `coil-compose`: Load book cover images in Compose.
64 | - `coil-network-okhttp`: Network support for Coil.
65 |
66 | ### Koin (Dependency Injection)
67 | - `koin.core`: Core Koin library for dependency injection.
68 | - `koin.android`: Koin Android integration.
69 | - `koin.compose`: Compose support for Koin DI.
70 |
71 | ### Networking with Ktor
72 | - `ktor.client.okhttp`: OkHttp engine for Ktor.
73 | - `ktor.serialization.kotlinx-json`: JSON serialization with Kotlinx.
74 | - `ktor.client.content-negotiation`: Content negotiation for Ktor.
75 | - `ktor.client.auth`: Handle authentication.
76 | - `ktor.client.logging`: Logging for network requests.
77 |
78 | ### Placeholder
79 | - `placeholder.material3`: Material 3 design for placeholder loading.
80 |
81 | ## App Architecture
82 |
83 | The app follows MVVM (Model-View-ViewModel) architecture:
84 |
85 | - **Model**: Represents the data layer, includes the Room database, repositories for fetching data from Google Books API.
86 | - **ViewModel**: Handles all the business logic and communicates with the Model to get the necessary data.
87 | - **View (Compose)**: Displays data on the screen and listens to changes from the ViewModel.
88 |
89 |
90 | ## License
91 |
92 | See the [LICENSE](./LICENSE) file for more information.
93 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/github/yohannestz/satori/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.github.yohannestz.satori", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/ic_satori_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/ic_satori_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/App.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori
2 |
3 | import android.app.Application
4 | import coil3.ImageLoader
5 | import coil3.PlatformContext
6 | import coil3.SingletonImageLoader
7 | import coil3.disk.DiskCache
8 | import coil3.disk.directory
9 | import coil3.memory.MemoryCache
10 | import coil3.request.crossfade
11 | import com.github.yohannestz.satori.di.dataStoreModule
12 | import com.github.yohannestz.satori.di.databaseModule
13 | import com.github.yohannestz.satori.di.networkModule
14 | import com.github.yohannestz.satori.di.repositoryModule
15 | import com.github.yohannestz.satori.di.viewModelModule
16 | import org.koin.android.ext.koin.androidContext
17 | import org.koin.core.component.KoinComponent
18 | import org.koin.core.context.startKoin
19 |
20 | class App : Application(), KoinComponent, SingletonImageLoader.Factory {
21 | override fun onCreate() {
22 | super.onCreate()
23 |
24 | startKoin {
25 | androidContext(this@App)
26 | modules(
27 | databaseModule,
28 | networkModule,
29 | repositoryModule,
30 | viewModelModule,
31 | dataStoreModule
32 | )
33 | }
34 | }
35 |
36 | override fun newImageLoader(context: PlatformContext): ImageLoader {
37 | return ImageLoader.Builder(this)
38 | .memoryCache {
39 | MemoryCache.Builder()
40 | .maxSizePercent(context, percent = 0.25)
41 | .build()
42 | }
43 | .diskCache {
44 | DiskCache.Builder()
45 | .directory(cacheDir.resolve("image_cache"))
46 | .maxSizePercent(0.02)
47 | .build()
48 | }
49 | .crossfade(300)
50 | .build()
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/DatabaseConverters.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local
2 |
3 | import androidx.room.TypeConverter
4 | import java.util.Date
5 |
6 | object DatabaseConverters {
7 |
8 | @TypeConverter
9 | fun timestampToLocalDateTime(value: Long?): Date? {
10 | return value?.let {
11 | Date(it)
12 | }
13 | }
14 |
15 | @TypeConverter
16 | fun localDateTimeToTimestamp(value: Date?): Long? {
17 | return value?.time
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/DatabaseMigrations.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local
2 |
3 | import androidx.room.migration.Migration
4 | import androidx.sqlite.db.SupportSQLiteDatabase
5 |
6 | object DatabaseMigrations {
7 | val migrations: Array by lazy {
8 | arrayOf(
9 | MIGRATION_1_2,
10 | MIGRATION_2_3
11 | )
12 | }
13 |
14 | private val MIGRATION_1_2 = object : Migration(1, 2) {
15 | override fun migrate(db: SupportSQLiteDatabase) {
16 | db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_search_history_query ON search_history(query)")
17 | }
18 | }
19 |
20 | private val MIGRATION_2_3 = object : Migration(2, 3) {
21 | override fun migrate(db: SupportSQLiteDatabase) {
22 | val cursorTitle = db.query("PRAGMA table_info(item)")
23 | val hasTitleColumn = cursorTitle.use {
24 | var columnExists = false
25 | while (it.moveToNext()) {
26 | val columnName = it.getString(it.getColumnIndexOrThrow("name"))
27 | if (columnName == "title") {
28 | columnExists = true
29 | break
30 | }
31 | }
32 | columnExists
33 | }
34 |
35 | if (!hasTitleColumn) {
36 | db.execSQL("ALTER TABLE item ADD COLUMN title TEXT")
37 | }
38 |
39 | val cursorImageUrl = db.query("PRAGMA table_info(item)")
40 | val hasImageUrlColumn = cursorImageUrl.use {
41 | var columnExists = false
42 | while (it.moveToNext()) {
43 | val columnName = it.getString(it.getColumnIndexOrThrow("name"))
44 | if (columnName == "imageUrl") {
45 | columnExists = true
46 | break
47 | }
48 | }
49 | columnExists
50 | }
51 |
52 | if (!hasImageUrlColumn) {
53 | db.execSQL("ALTER TABLE item ADD COLUMN imageUrl TEXT")
54 | }
55 | }
56 | }
57 |
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/SatoriDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.TypeConverters
6 | import com.github.yohannestz.satori.data.local.itembookmarks.ItemDao
7 | import com.github.yohannestz.satori.data.local.itembookmarks.ItemEntity
8 | import com.github.yohannestz.satori.data.local.searchhistory.SearchHistoryDao
9 | import com.github.yohannestz.satori.data.local.searchhistory.SearchHistoryEntity
10 |
11 | @Database(
12 | entities = [
13 | SearchHistoryEntity::class,
14 | ItemEntity::class
15 | ],
16 | version = 3,
17 | )
18 | @TypeConverters(DatabaseConverters::class)
19 | abstract class SatoriDatabase : RoomDatabase() {
20 | abstract fun searchHistoryDao(): SearchHistoryDao
21 | abstract fun itemDao(): ItemDao
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/itembookmarks/ItemDao.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local.itembookmarks
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface ItemDao {
12 | @Query("SELECT * FROM item ORDER BY timestamp DESC LIMIT 10")
13 | fun getItemBookmarks(): Flow>
14 |
15 | @Query("SELECT * FROM item LIMIT :pageSize OFFSET :startIndex")
16 | fun getPaginatedItemBookmarks(startIndex: Int, pageSize: Int): Flow>
17 |
18 | @Insert(onConflict = OnConflictStrategy.REPLACE)
19 | suspend fun insertItemBookmark(entity: ItemEntity)
20 |
21 | @Delete
22 | suspend fun deleteItemBookmark(entity: ItemEntity)
23 |
24 | @Query("DELETE FROM item WHERE id = :id")
25 | suspend fun deleteItemBookmarkById(id: String)
26 |
27 | @Query("SELECT COUNT(*) FROM item WHERE id = :itemId")
28 | suspend fun isItemBookmarked(itemId: String): Boolean
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/itembookmarks/ItemEntity.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local.itembookmarks
2 |
3 | import androidx.room.Entity
4 | import androidx.room.Index
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(
8 | tableName = "item",
9 | indices = [Index(value = ["id"], unique = true)],
10 | )
11 | data class ItemEntity(
12 | @PrimaryKey
13 | val id: String,
14 | val etag: String,
15 | val authors: String?,
16 | val title: String?,
17 | val imageUrl: String?,
18 | val smallThumbnail: String?,
19 | val publisher: String?,
20 | val publishedDate: String?,
21 | val timestamp: Long = System.currentTimeMillis()
22 | )
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/itembookmarks/ItemMapper.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local.itembookmarks
2 |
3 | import com.github.yohannestz.satori.data.model.volume.BookMarkItem
4 | import com.github.yohannestz.satori.data.model.volume.Item
5 |
6 | fun Item.toItemEntity(): ItemEntity {
7 | return ItemEntity(
8 | id = this.id,
9 | etag = this.etag,
10 | authors = this.volumeInfo.authors?.joinToString(", "),
11 | title = this.volumeInfo.title,
12 | imageUrl = this.volumeInfo.imageLinks?.thumbnail,
13 | smallThumbnail = this.volumeInfo.imageLinks?.smallThumbnail,
14 | publisher = this.volumeInfo.publisher,
15 | publishedDate = this.volumeInfo.publishedDate
16 | )
17 | }
18 |
19 | fun BookMarkItem.toItemEntity(): ItemEntity {
20 | return ItemEntity(
21 | id = this.id,
22 | etag = this.etag,
23 | authors = this.authors,
24 | title = this.title,
25 | imageUrl = this.imageUrl,
26 | smallThumbnail = this.smallThumbnail,
27 | publisher = this.publisher,
28 | publishedDate = this.publishedDate
29 | )
30 | }
31 |
32 | fun ItemEntity.toBookMarkItem(): BookMarkItem {
33 | return BookMarkItem(
34 | id = this.id,
35 | etag = this.etag,
36 | title = this.title,
37 | authors = this.authors,
38 | imageUrl = this.imageUrl,
39 | smallThumbnail = this.smallThumbnail,
40 | publisher = this.publisher,
41 | publishedDate = this.publishedDate
42 | )
43 | }
44 |
45 | fun List- .toItemEntityList(): List {
46 | return map(Item::toItemEntity)
47 | }
48 |
49 | fun List.toBookMarkItemList(): List {
50 | return map(ItemEntity::toBookMarkItem)
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/searchhistory/SearchHistoryDao.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local.searchhistory
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface SearchHistoryDao {
12 | @Query("SELECT * FROM search_history ORDER BY timestamp DESC LIMIT 10")
13 | fun getSearchHistory(): Flow
>
14 |
15 | @Insert(onConflict = OnConflictStrategy.REPLACE)
16 | suspend fun insertSearchHistory(entity: SearchHistoryEntity)
17 |
18 | @Query("DELETE FROM search_history WHERE `query` = :query")
19 | suspend fun deleteSearchHistoryByQuery(query: String)
20 |
21 | @Delete
22 | suspend fun deleteSearchHistory(entity: SearchHistoryEntity)
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/searchhistory/SearchHistoryEntity.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local.searchhistory
2 |
3 | import androidx.room.Entity
4 | import androidx.room.Index
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(
8 | tableName = "search_history",
9 | indices = [Index(value = ["query"], unique = true)],
10 | )
11 | data class SearchHistoryEntity(
12 | @PrimaryKey(autoGenerate = true)
13 | val id: Int = 0,
14 | val query: String,
15 | val timestamp: Long = System.currentTimeMillis()
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/local/searchhistory/SearchHistoryMapper.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.local.searchhistory
2 |
3 | import com.github.yohannestz.satori.data.model.volume.SearchHistory
4 |
5 | fun SearchHistoryEntity.toSearchHistory(): SearchHistory {
6 | return SearchHistory(
7 | query = query,
8 | timestamp = timestamp
9 | )
10 | }
11 |
12 | fun SearchHistory.toSearchHistoryEntity(): SearchHistoryEntity {
13 | return SearchHistoryEntity(
14 | query = query,
15 | timestamp = timestamp
16 | )
17 | }
18 |
19 | fun List.toSearchHistoryList(): List {
20 | return map(SearchHistoryEntity::toSearchHistory)
21 | }
22 |
23 | fun List.toSearchHistoryEntityList(): List {
24 | return map(SearchHistory::toSearchHistoryEntity)
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/Filter.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model
2 |
3 | enum class Filter(val value: String) {
4 | EBOOKS("ebooks"),
5 | FREE_EBOOKS("free-ebooks"),
6 | FULL("full"),
7 | PAID_EBOOKS("paid-ebooks"),
8 | PARTIAL("partial"),
9 | EMPTY("")
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/OrderBy.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model
2 |
3 | enum class OrderBy(val value: String) {
4 | RELEVANCE("relevance"),
5 | NEWEST("newest")
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/PrintType.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model
2 |
3 | enum class PrintType(val value: String) {
4 | ALL("all"),
5 | BOOK("book"),
6 | MAGAZINE("magazine");
7 |
8 | companion object {
9 | fun valueOfOrNull(value: String) = try {
10 | PrintType.valueOf(value)
11 | } catch (e: IllegalArgumentException) {
12 | null
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/ViewMode.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model
2 |
3 | import com.github.yohannestz.satori.R
4 |
5 | enum class ViewMode(val label: Int) {
6 | GRID(R.string.grid),
7 | LIST(R.string.list);
8 |
9 | companion object {
10 | fun valueOfOrNull(value: String) = try {
11 | valueOf(value)
12 | } catch (e: IllegalArgumentException) {
13 | null
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/VolumeCategory.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model
2 |
3 | import android.os.Bundle
4 | import androidx.navigation.NavType
5 | import com.github.yohannestz.satori.R
6 |
7 | enum class VolumeCategory(val value: String, val label: Int) {
8 | BUSINESS_ECONOMICS("business and economics", R.string.business_economics),
9 | HISTORY("history", R.string.history),
10 | BIOGRAPHY("biography", R.string.biography),
11 | PHILOSOPHY("philosophy", R.string.philosophy),
12 | SCIENCE_MATH("science and math", R.string.science_math),
13 | ROMANCE("romance", R.string.romance),
14 | FICTION("fiction", R.string.fiction),
15 | AUTOBIOGRAPHY("autobiography", R.string.autobiography),
16 | COMPUTER_TECHNOLOGY("computer and technology", R.string.computer_technology),
17 | SELF_HELP("self-help", R.string.self_help);
18 |
19 | companion object {
20 | val navType = object : NavType(isNullableAllowed = false) {
21 | override fun get(bundle: Bundle, key: String): VolumeCategory? {
22 | return try {
23 | bundle.getString(key)?.let {
24 | VolumeCategory.valueOf(it)
25 | }
26 | } catch (_: IllegalStateException) {
27 | null
28 | }
29 | }
30 |
31 | override fun parseValue(value: String): VolumeCategory {
32 | return try {
33 | VolumeCategory.valueOf(value)
34 | } catch (_: IllegalStateException) {
35 | BUSINESS_ECONOMICS
36 | }
37 | }
38 |
39 | override fun put(bundle: Bundle, key: String, value: VolumeCategory) {
40 | bundle.putString(key, value.name)
41 | }
42 | }
43 | }
44 |
45 | override fun toString(): String {
46 | return name
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/contributors/Contributor.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.contributors
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class Contributor(
9 | @SerialName("avatar_url")
10 | val avatarUrl: String,
11 | @SerialName("contributions")
12 | val contributions: Int,
13 | @SerialName("events_url")
14 | val eventsUrl: String,
15 | @SerialName("followers_url")
16 | val followersUrl: String,
17 | @SerialName("following_url")
18 | val followingUrl: String,
19 | @SerialName("gists_url")
20 | val gistsUrl: String,
21 | @SerialName("gravatar_id")
22 | val gravatarId: String,
23 | @SerialName("html_url")
24 | val htmlUrl: String,
25 | @SerialName("id")
26 | val id: Int,
27 | @SerialName("login")
28 | val login: String,
29 | @SerialName("node_id")
30 | val nodeId: String,
31 | @SerialName("organizations_url")
32 | val organizationsUrl: String,
33 | @SerialName("received_events_url")
34 | val receivedEventsUrl: String,
35 | @SerialName("repos_url")
36 | val reposUrl: String,
37 | @SerialName("site_admin")
38 | val siteAdmin: Boolean,
39 | @SerialName("starred_url")
40 | val starredUrl: String,
41 | @SerialName("subscriptions_url")
42 | val subscriptionsUrl: String,
43 | @SerialName("type")
44 | val type: String,
45 | @SerialName("url")
46 | val url: String
47 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/AccessInfo.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class AccessInfo(
9 | @SerialName("accessViewStatus")
10 | val accessViewStatus: String,
11 | @SerialName("country")
12 | val country: String,
13 | @SerialName("embeddable")
14 | val embeddable: Boolean,
15 | @SerialName("epub")
16 | val epub: Epub,
17 | @SerialName("pdf")
18 | val pdf: Pdf,
19 | @SerialName("publicDomain")
20 | val publicDomain: Boolean,
21 | @SerialName("quoteSharingAllowed")
22 | val quoteSharingAllowed: Boolean,
23 | @SerialName("textToSpeechPermission")
24 | val textToSpeechPermission: String,
25 | @SerialName("viewability")
26 | val viewability: String,
27 | @SerialName("webReaderLink")
28 | val webReaderLink: String
29 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/BookMarkItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 | data class BookMarkItem(
4 | val id: String,
5 | val etag: String,
6 | val title: String?,
7 | val authors: String?,
8 | val imageUrl: String?,
9 | val smallThumbnail: String?,
10 | val publisher: String?,
11 | val publishedDate: String?,
12 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/Dimensions.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class Dimensions(
9 | @SerialName("height")
10 | val height: String
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/Epub.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class Epub(
9 | @SerialName("downloadLink")
10 | val downloadLink: String? = null,
11 | @SerialName("isAvailable")
12 | val isAvailable: Boolean
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/ImageLinks.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class ImageLinks(
9 | @SerialName("smallThumbnail")
10 | val smallThumbnail: String,
11 | @SerialName("thumbnail")
12 | val thumbnail: String
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/IndustryIdentifier.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class IndustryIdentifier(
9 | @SerialName("identifier")
10 | val identifier: String,
11 | @SerialName("type")
12 | val type: String
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/Item.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class Item(
9 | @SerialName("accessInfo")
10 | val accessInfo: AccessInfo,
11 | @SerialName("etag")
12 | val etag: String,
13 | @SerialName("id")
14 | val id: String,
15 | @SerialName("kind")
16 | val kind: String,
17 | @SerialName("saleInfo")
18 | val saleInfo: SaleInfo,
19 | @SerialName("searchInfo")
20 | val searchInfo: SearchInfo? = null,
21 | @SerialName("selfLink")
22 | val selfLink: String,
23 | @SerialName("volumeInfo")
24 | val volumeInfo: VolumeInfo
25 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/PanelizationSummary.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class PanelizationSummary(
9 | @SerialName("containsEpubBubbles")
10 | val containsEpubBubbles: Boolean,
11 | @SerialName("containsImageBubbles")
12 | val containsImageBubbles: Boolean
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/Pdf.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class Pdf(
9 | @SerialName("isAvailable")
10 | val isAvailable: Boolean,
11 |
12 | @SerialName("acsTokenLink")
13 | val acsTokenLink: String? = null
14 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/ReadingModes.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class ReadingModes(
9 | @SerialName("image")
10 | val image: Boolean,
11 | @SerialName("text")
12 | val text: Boolean
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/SaleInfo.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class SaleInfo(
9 | @SerialName("buyLink")
10 | val buyLink: String? = null,
11 | @SerialName("country")
12 | val country: String,
13 | @SerialName("isEbook")
14 | val isEbook: Boolean,
15 | @SerialName("saleability")
16 | val saleability: String
17 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/SearchHistory.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 | data class SearchHistory(
4 | val query: String,
5 | val timestamp: Long
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/SearchInfo.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class SearchInfo(
9 | @SerialName("textSnippet")
10 | val textSnippet: String
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/Volume.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class Volume(
9 | @SerialName("items")
10 | val items: List- ,
11 | @SerialName("kind")
12 | val kind: String,
13 | @SerialName("totalItems")
14 | val totalItems: Int
15 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/VolumeDetail.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class VolumeDetail(
9 | @SerialName("accessInfo")
10 | val accessInfo: AccessInfo,
11 | @SerialName("etag")
12 | val etag: String,
13 | @SerialName("id")
14 | val id: String,
15 | @SerialName("kind")
16 | val kind: String,
17 | @SerialName("saleInfo")
18 | val saleInfo: SaleInfo,
19 | @SerialName("selfLink")
20 | val selfLink: String,
21 | @SerialName("volumeInfo")
22 | val volumeInfo: VolumeInfo
23 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/model/volume/VolumeInfo.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.model.volume
2 |
3 | import kotlinx.serialization.KSerializer
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 | import kotlinx.serialization.descriptors.PrimitiveKind
7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
8 | import kotlinx.serialization.descriptors.SerialDescriptor
9 | import kotlinx.serialization.encoding.Decoder
10 | import kotlinx.serialization.encoding.Encoder
11 |
12 | object AverageRatingSerializer : KSerializer {
13 | override val descriptor: SerialDescriptor
14 | get() = PrimitiveSerialDescriptor("FloatOrInt", PrimitiveKind.FLOAT)
15 |
16 | override fun deserialize(decoder: Decoder): Float {
17 | return when (val value = decoder.decodeDouble()) {
18 | value.toInt().toDouble() -> value.toInt().toFloat()
19 | else -> value.toFloat()
20 | }
21 | }
22 |
23 | override fun serialize(encoder: Encoder, value: Float) {
24 | encoder.encodeFloat(value)
25 | }
26 |
27 | }
28 |
29 | @Serializable
30 | data class VolumeInfo(
31 | @SerialName("allowAnonLogging")
32 | val allowAnonLogging: Boolean,
33 | @SerialName("canonicalVolumeLink")
34 | val canonicalVolumeLink: String,
35 | @SerialName("categories")
36 | val categories: List? = null,
37 | @SerialName("contentVersion")
38 | val contentVersion: String,
39 | @SerialName("description")
40 | val description: String? = null,
41 | @SerialName("imageLinks")
42 | val imageLinks: ImageLinks? = null,
43 | @SerialName("industryIdentifiers")
44 | val industryIdentifiers: List? = null,
45 | @SerialName("infoLink")
46 | val infoLink: String,
47 | @SerialName("language")
48 | val language: String,
49 | @SerialName("maturityRating")
50 | val maturityRating: String,
51 | @SerialName("pageCount")
52 | val pageCount: Int? = null,
53 | @SerialName("panelizationSummary")
54 | val panelizationSummary: PanelizationSummary? = null,
55 | @SerialName("previewLink")
56 | val previewLink: String,
57 | @SerialName("printType")
58 | val printType: String,
59 | @SerialName("publishedDate")
60 | val publishedDate: String? = null,
61 | @SerialName("readingModes")
62 | val readingModes: ReadingModes,
63 | @SerialName("title")
64 | val title: String? = null,
65 | @SerialName("dimensions")
66 | val dimensions: Dimensions? = null,
67 | @SerialName("authors")
68 | val authors: List? = null,
69 | @SerialName("publisher")
70 | val publisher: String? = null,
71 | @Serializable(with = AverageRatingSerializer::class)
72 | @SerialName("averageRating")
73 | val averageRating: Float? = null,
74 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/remote/KtorClient.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.remote
2 |
3 | import com.github.yohannestz.satori.utils.GOOGLE_API_BASE_URL
4 | import io.ktor.client.HttpClient
5 | import io.ktor.client.engine.okhttp.OkHttp
6 | import io.ktor.client.plugins.DefaultRequest
7 | import io.ktor.client.plugins.cache.HttpCache
8 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
9 | import io.ktor.client.plugins.logging.ANDROID
10 | import io.ktor.client.plugins.logging.LogLevel
11 | import io.ktor.client.plugins.logging.Logger
12 | import io.ktor.client.plugins.logging.Logging
13 | import io.ktor.serialization.kotlinx.json.json
14 | import kotlinx.serialization.json.Json
15 |
16 | val ktorHttpClient = HttpClient(OkHttp) {
17 | expectSuccess = false
18 |
19 | install(ContentNegotiation) {
20 | json(
21 | Json {
22 | coerceInputValues = true
23 | isLenient = true
24 | ignoreUnknownKeys = true
25 | }
26 | )
27 | }
28 |
29 | install(HttpCache)
30 |
31 | install(Logging) {
32 | logger = Logger.ANDROID
33 | level = LogLevel.INFO
34 | }
35 |
36 | install(DefaultRequest) {
37 | url(GOOGLE_API_BASE_URL)
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/remote/service/GithubApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.remote.service
2 |
3 | import com.github.yohannestz.satori.data.model.contributors.Contributor
4 | import io.ktor.client.HttpClient
5 | import io.ktor.client.call.body
6 | import io.ktor.client.request.get
7 | import io.ktor.http.URLProtocol
8 |
9 | class GithubApi(private val client: HttpClient) {
10 |
11 | suspend fun getContributors(repoSlug: String): Result
> {
12 | return try {
13 | val response: List = client.get("/repos/$repoSlug/contributors") {
14 | url {
15 | protocol = URLProtocol.HTTPS
16 | host = "api.github.com"
17 | }
18 | }.body()
19 | Result.success(response)
20 | } catch (e: Exception) {
21 | Result.failure(e)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/remote/service/GoogleBooksApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.remote.service
2 |
3 | import com.github.yohannestz.satori.data.model.volume.Volume
4 | import com.github.yohannestz.satori.data.model.volume.VolumeDetail
5 | import io.ktor.client.HttpClient
6 | import io.ktor.client.call.body
7 | import io.ktor.client.request.get
8 | import io.ktor.client.request.parameter
9 |
10 | class GoogleBooksApi(private val client: HttpClient) {
11 |
12 | suspend fun searchVolume(
13 | query: String,
14 | startIndex: Int,
15 | maxResults: Int,
16 | orderBy: String = "relevance",
17 | printType: String? = null,
18 | filter: String? = null,
19 | ): Result {
20 | return try {
21 | val response: Volume = client.get("/books/v1/volumes") {
22 | parameter("q", query)
23 | parameter("orderBy", orderBy)
24 | parameter("startIndex", startIndex)
25 | parameter("maxResults", maxResults)
26 | printType?.let { parameter("printType", it) }
27 | filter?.let { parameter("filter", it) }
28 | }.body()
29 |
30 | Result.success(response)
31 | } catch (e: Exception) {
32 | Result.failure(e)
33 | }
34 | }
35 |
36 | suspend fun getVolumeById(id: String): Result {
37 | return try {
38 | val response: VolumeDetail = client.get("/books/v1/volumes/$id").body()
39 | Result.success(response)
40 | } catch (e: Exception) {
41 | Result.failure(e)
42 | }
43 | }
44 |
45 | suspend fun getVolumesByCategory(
46 | category: String,
47 | startIndex: Int,
48 | maxResults: Int,
49 | orderBy: String = "relevance",
50 | printType: String? = null,
51 | filter: String? = null,
52 | ): Result {
53 | return try {
54 | val volumes: Volume = client.get("/books/v1/volumes") {
55 | parameter("q", "subject:$category")
56 | parameter("orderBy", orderBy)
57 | parameter("startIndex", startIndex)
58 | parameter("maxResults", maxResults)
59 | printType?.let { parameter("printType", it) }
60 | filter?.let { parameter("filter", it) }
61 | }.body()
62 |
63 | Result.success(volumes)
64 | } catch (e: Exception) {
65 | Result.failure(e)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/repository/BookMarkRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.repository
2 |
3 | import com.github.yohannestz.satori.data.local.itembookmarks.ItemDao
4 | import com.github.yohannestz.satori.data.local.itembookmarks.ItemEntity
5 | import com.github.yohannestz.satori.data.local.itembookmarks.toBookMarkItemList
6 | import com.github.yohannestz.satori.data.model.volume.BookMarkItem
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 |
10 | class BookMarkRepository(
11 | private val dao: ItemDao
12 | ) {
13 | fun getBookMarkedItems(): Flow> {
14 | return dao.getItemBookmarks().map(List::toBookMarkItemList)
15 | }
16 |
17 | fun getPaginatedBookMarkedItems(startIndex: Int, pageSize: Int): Flow> {
18 | return dao.getPaginatedItemBookmarks(startIndex, pageSize)
19 | .map(List::toBookMarkItemList)
20 | }
21 |
22 | suspend fun addItem(item: ItemEntity) {
23 | dao.insertItemBookmark(item)
24 | }
25 |
26 | suspend fun deleteItem(item: ItemEntity) {
27 | dao.deleteItemBookmark(item)
28 | }
29 |
30 | suspend fun deleteItemById(id: String) {
31 | dao.deleteItemBookmarkById(id)
32 | }
33 |
34 | suspend fun isItemBookmarked(itemId: String): Boolean {
35 | return dao.isItemBookmarked(itemId)
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/repository/BookRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.repository
2 |
3 | import com.github.yohannestz.satori.data.remote.service.GoogleBooksApi
4 |
5 | class BookRepository(
6 | private val googleBooksApi: GoogleBooksApi
7 | ) {
8 | suspend fun searchVolume(
9 | query: String,
10 | startIndex: Int,
11 | maxResults: Int,
12 | orderBy: String,
13 | printType: String?,
14 | filter: String?
15 | ) = googleBooksApi.searchVolume(
16 | query = query,
17 | startIndex = startIndex,
18 | maxResults = maxResults,
19 | orderBy = orderBy,
20 | printType = printType.takeIf { !it.isNullOrBlank() },
21 | filter = filter.takeIf { !it.isNullOrBlank() }
22 | )
23 |
24 | suspend fun getVolume(
25 | volumeId: String,
26 | ) = googleBooksApi.getVolumeById(
27 | id = volumeId
28 | )
29 |
30 | suspend fun getVolumesByCategory(
31 | category: String,
32 | startIndex: Int,
33 | maxResults: Int,
34 | orderBy: String,
35 | printType: String?,
36 | filter: String?
37 | ) = googleBooksApi.getVolumesByCategory(
38 | category = category,
39 | startIndex = startIndex,
40 | maxResults = maxResults,
41 | orderBy = orderBy,
42 | printType = printType.takeIf { !it.isNullOrBlank() },
43 | filter = filter.takeIf { !it.isNullOrBlank() }
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/repository/GithubRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.repository
2 |
3 | import com.github.yohannestz.satori.data.remote.service.GithubApi
4 |
5 | class GithubRepository(
6 | private val githubApi: GithubApi
7 | ) {
8 | suspend fun getContributors(repoSlug: String) = githubApi.getContributors(repoSlug)
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/repository/PreferencesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.repository
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import androidx.datastore.preferences.core.booleanPreferencesKey
6 | import androidx.datastore.preferences.core.intPreferencesKey
7 | import androidx.datastore.preferences.core.stringPreferencesKey
8 | import com.github.yohannestz.satori.data.model.PrintType
9 | import com.github.yohannestz.satori.data.model.ViewMode
10 | import com.github.yohannestz.satori.di.getValue
11 | import com.github.yohannestz.satori.di.setValue
12 | import com.github.yohannestz.satori.ui.base.StartTab
13 | import com.github.yohannestz.satori.ui.base.ThemeStyle
14 | import kotlinx.coroutines.flow.map
15 |
16 | class PreferencesRepository(
17 | private val dataStore: DataStore
18 | ) {
19 | val theme = dataStore.getValue(THEME_KEY, ThemeStyle.FOLLOW_SYSTEM.name)
20 | .map { ThemeStyle.valueOfOrNull(it) ?: ThemeStyle.LIGHT }
21 |
22 | suspend fun setTheme(value: ThemeStyle) {
23 | dataStore.setValue(THEME_KEY, value.name)
24 | }
25 |
26 | val useBlackColors = dataStore.getValue(USE_BLACK_COLORS_KEY, true)
27 | suspend fun setUseBlackColors(value: Boolean) {
28 | dataStore.setValue(USE_BLACK_COLORS_KEY, value)
29 | }
30 |
31 | val useDynamicColors = dataStore.getValue(USE_DYNAMIC_COLORS_KEY, false)
32 | suspend fun setUseDynamicColors(value: Boolean) {
33 | dataStore.setValue(USE_DYNAMIC_COLORS_KEY, value)
34 | }
35 |
36 | val startTab = dataStore.getValue(START_TAB_KEY, StartTab.LAST_USED.value)
37 | .map { StartTab.valueOf(tabName = it) }
38 |
39 | suspend fun setStartTab(value: StartTab?) {
40 | dataStore.setValue(START_TAB_KEY, value!!.value)
41 | }
42 |
43 | val lastTab = dataStore.getValue(LAST_TAB_KEY, 0)
44 | suspend fun setLastTab(value: Int) {
45 | dataStore.setValue(LAST_TAB_KEY, value)
46 | }
47 |
48 | val volumeListViewMode = dataStore.getValue(VOLUME_LIST_VIEW_MODE, ViewMode.LIST.name)
49 | .map { ViewMode.valueOfOrNull(it) ?: ViewMode.LIST }
50 |
51 | suspend fun setVolumeListViewMode(value: ViewMode) {
52 | dataStore.setValue(VOLUME_LIST_VIEW_MODE, value.name)
53 | }
54 |
55 | val onlyShowFreeContent = dataStore.getValue(ONLY_SHOW_FREE_CONTENT, false)
56 | suspend fun setOnlyShowFreeContent(value: Boolean) {
57 | dataStore.setValue(ONLY_SHOW_FREE_CONTENT, value)
58 | }
59 |
60 | val defaultPrintType = dataStore.getValue(DEFAULT_PRINT_TYPE, PrintType.ALL.value)
61 | .map { PrintType.valueOfOrNull(it) ?: PrintType.ALL }
62 |
63 | suspend fun setDefaultPrintType(value: PrintType) {
64 | dataStore.setValue(DEFAULT_PRINT_TYPE, value.value)
65 | }
66 |
67 | companion object {
68 | private val THEME_KEY = stringPreferencesKey("theme")
69 | private val USE_BLACK_COLORS_KEY = booleanPreferencesKey("use_black_colors")
70 | private val USE_DYNAMIC_COLORS_KEY = booleanPreferencesKey("use_dynamic_colors")
71 | private val LAST_TAB_KEY = intPreferencesKey("last_tab")
72 | private val START_TAB_KEY = stringPreferencesKey("start_tab")
73 | private val VOLUME_LIST_VIEW_MODE = stringPreferencesKey("volume_list_view_mode")
74 | private val ONLY_SHOW_FREE_CONTENT = booleanPreferencesKey("only_show_free_content")
75 | private val DEFAULT_PRINT_TYPE = stringPreferencesKey("default_print_type")
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/data/repository/SearchHistoryRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.data.repository
2 |
3 | import com.github.yohannestz.satori.data.local.searchhistory.SearchHistoryDao
4 | import com.github.yohannestz.satori.data.local.searchhistory.SearchHistoryEntity
5 | import com.github.yohannestz.satori.data.local.searchhistory.toSearchHistoryEntity
6 | import com.github.yohannestz.satori.data.local.searchhistory.toSearchHistoryList
7 | import com.github.yohannestz.satori.data.model.volume.SearchHistory
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.map
10 |
11 | class SearchHistoryRepository(
12 | private val dao: SearchHistoryDao
13 | ) {
14 | fun getSearchHistoryItems(): Flow> {
15 | return dao.getSearchHistory().map(List::toSearchHistoryList)
16 | }
17 |
18 | suspend fun addItem(query: String) {
19 | val trimmedQuery = query.trim()
20 |
21 | if (trimmedQuery.isNotBlank()) {
22 | dao.insertSearchHistory(
23 | SearchHistoryEntity(
24 | query = trimmedQuery
25 | )
26 | )
27 | }
28 | }
29 |
30 | suspend fun deleteItem(item: SearchHistory) {
31 | dao.deleteSearchHistory(item.toSearchHistoryEntity())
32 | }
33 |
34 | suspend fun deleteItemByQuery(query: String) {
35 | dao.deleteSearchHistoryByQuery(query)
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/di/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.github.yohannestz.satori.data.local.DatabaseMigrations
6 | import com.github.yohannestz.satori.data.local.SatoriDatabase
7 | import com.github.yohannestz.satori.data.local.itembookmarks.ItemDao
8 | import com.github.yohannestz.satori.data.local.searchhistory.SearchHistoryDao
9 | import com.github.yohannestz.satori.data.repository.BookMarkRepository
10 | import com.github.yohannestz.satori.data.repository.SearchHistoryRepository
11 | import org.koin.android.ext.koin.androidContext
12 | import org.koin.core.module.dsl.singleOf
13 | import org.koin.dsl.module
14 |
15 | val databaseModule = module {
16 | single { provideDatabase(androidContext()) }
17 | single { get().searchHistoryDao() }
18 | single { get().itemDao() }
19 |
20 | singleOf(::SearchHistoryRepository)
21 | singleOf(::BookMarkRepository)
22 | }
23 |
24 | private fun provideDatabase(context: Context): SatoriDatabase {
25 | return Room
26 | .databaseBuilder(
27 | context = context,
28 | klass = SatoriDatabase::class.java,
29 | name = "satori_database"
30 | )
31 | .addMigrations(*DatabaseMigrations.migrations)
32 | .build()
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/di/DatastoreModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.di
2 |
3 | import android.content.Context
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory
6 | import androidx.datastore.preferences.core.Preferences
7 | import androidx.datastore.preferences.core.edit
8 | import androidx.datastore.preferences.preferencesDataStoreFile
9 | import kotlinx.coroutines.flow.first
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.runBlocking
12 | import org.koin.dsl.module
13 |
14 | const val SATORI_DEFAULT_DATASTORE = "satori_datastore"
15 |
16 | val dataStoreModule = module {
17 | single {
18 | provideDataStore(get(), SATORI_DEFAULT_DATASTORE)
19 | }
20 | }
21 |
22 | private fun provideDataStore(context: Context, name: String) =
23 | PreferenceDataStoreFactory.create {
24 | context.preferencesDataStoreFile(name)
25 | }
26 |
27 | fun DataStore.getValue(key: Preferences.Key) = data.map { it[key] }
28 |
29 | fun DataStore.getValue(
30 | key: Preferences.Key,
31 | default: T,
32 | ) = data.map { it[key] ?: default }
33 |
34 | suspend fun DataStore.setValue(
35 | key: Preferences.Key,
36 | value: T?
37 | ) = edit {
38 | if (value != null) it[key] = value
39 | else it.remove(key)
40 | }
41 |
42 | fun DataStore.getValueBlocking(key: Preferences.Key) =
43 | runBlocking { data.first() }[key]
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.di
2 |
3 | import com.github.yohannestz.satori.data.remote.ktorHttpClient
4 | import com.github.yohannestz.satori.data.remote.service.GithubApi
5 | import com.github.yohannestz.satori.data.remote.service.GoogleBooksApi
6 | import org.koin.core.module.dsl.singleOf
7 | import org.koin.dsl.module
8 |
9 | val networkModule = module {
10 | single { ktorHttpClient }
11 | singleOf(::GoogleBooksApi)
12 | singleOf(::GithubApi)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.di
2 |
3 | import com.github.yohannestz.satori.data.repository.BookRepository
4 | import com.github.yohannestz.satori.data.repository.GithubRepository
5 | import com.github.yohannestz.satori.data.repository.PreferencesRepository
6 | import org.koin.core.module.dsl.singleOf
7 | import org.koin.dsl.module
8 |
9 | val repositoryModule = module {
10 | singleOf(::BookRepository)
11 | singleOf(::GithubRepository)
12 | single { PreferencesRepository(get()) }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/di/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.di
2 |
3 | import com.github.yohannestz.satori.ui.about.AboutViewModel
4 | import com.github.yohannestz.satori.ui.bookmarks.BookMarksViewModel
5 | import com.github.yohannestz.satori.ui.details.VolumeDetailViewModel
6 | import com.github.yohannestz.satori.ui.home.HomeViewModel
7 | import com.github.yohannestz.satori.ui.latest.LatestViewModel
8 | import com.github.yohannestz.satori.ui.main.MainViewModel
9 | import com.github.yohannestz.satori.ui.search.SearchViewModel
10 | import com.github.yohannestz.satori.ui.settings.SettingsViewModel
11 | import com.github.yohannestz.satori.ui.volumelist.VolumeListViewModel
12 | import org.koin.androidx.viewmodel.dsl.viewModelOf
13 | import org.koin.dsl.module
14 |
15 | val viewModelModule = module {
16 | viewModelOf(::MainViewModel)
17 | viewModelOf(::HomeViewModel)
18 | viewModelOf(::LatestViewModel)
19 | viewModelOf(::VolumeDetailViewModel)
20 | viewModelOf(::VolumeListViewModel)
21 | viewModelOf(::SearchViewModel)
22 | viewModelOf(::BookMarksViewModel)
23 | viewModelOf(::SettingsViewModel)
24 | viewModelOf(::AboutViewModel)
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/about/AboutUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.about
2 |
3 | import com.github.yohannestz.satori.data.model.contributors.Contributor
4 | import com.github.yohannestz.satori.ui.base.state.UiState
5 |
6 | data class AboutUiState(
7 | val contributors: List = emptyList(),
8 | override val isLoading: Boolean = false,
9 | override val message: String? = null
10 | ) : UiState() {
11 | override fun setLoading(value: Boolean) = copy(isLoading = value)
12 | override fun setMessage(value: String?) = copy(message = value)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/about/AboutViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.about
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.github.yohannestz.satori.data.repository.GithubRepository
5 | import com.github.yohannestz.satori.ui.base.viewmodel.BaseViewModel
6 | import com.github.yohannestz.satori.utils.GITHUB_SLUG
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.update
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.withContext
12 |
13 | class AboutViewModel(
14 | private val githubApiRepository: GithubRepository
15 | ) : BaseViewModel() {
16 | override val mutableUiState: MutableStateFlow = MutableStateFlow(AboutUiState())
17 |
18 | init {
19 | viewModelScope.launch {
20 | mutableUiState.update { it.copy(isLoading = true) }
21 | withContext(Dispatchers.IO) {
22 | val result = githubApiRepository.getContributors(GITHUB_SLUG)
23 | if (result.isSuccess) {
24 | mutableUiState.update {
25 | it.copy(
26 | contributors = result.getOrNull() ?: emptyList(),
27 | isLoading = false
28 | )
29 | }
30 | } else {
31 | mutableUiState.update {
32 | it.copy(
33 | isLoading = false
34 | )
35 | }
36 | }
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/about/composable/ContributorsItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.about.composable
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.clip
18 | import androidx.compose.ui.graphics.painter.ColorPainter
19 | import androidx.compose.ui.layout.ContentScale
20 | import androidx.compose.ui.res.painterResource
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.compose.ui.text.style.TextOverflow
23 | import androidx.compose.ui.unit.dp
24 | import androidx.compose.ui.unit.sp
25 | import coil3.compose.AsyncImage
26 | import com.github.yohannestz.satori.R
27 | import com.github.yohannestz.satori.data.model.contributors.Contributor
28 |
29 | @Composable
30 | fun ContributorsItem(
31 | item: Contributor,
32 | onClick: () -> Unit
33 | ) {
34 | Row(
35 | modifier = Modifier.padding(8.dp)
36 | .fillMaxWidth()
37 | .clickable(onClick = onClick),
38 | verticalAlignment = Alignment.CenterVertically,
39 | ) {
40 | AsyncImage(
41 | model = item.avatarUrl,
42 | contentDescription = "avatar",
43 | contentScale = ContentScale.Crop,
44 | placeholder = ColorPainter(MaterialTheme.colorScheme.outline),
45 | error = ColorPainter(MaterialTheme.colorScheme.outline),
46 | fallback = ColorPainter(MaterialTheme.colorScheme.outline),
47 | modifier = Modifier
48 | .size(48.dp)
49 | .clip(CircleShape)
50 | )
51 |
52 | Column {
53 | Text(
54 | text = item.login,
55 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
56 | color = MaterialTheme.colorScheme.onSurface,
57 | fontSize = MaterialTheme.typography.titleMedium.fontSize,
58 | lineHeight = 22.sp,
59 | overflow = TextOverflow.Ellipsis,
60 | maxLines = 1
61 | )
62 | Text(
63 | text = "${item.contributions} ${stringResource(R.string.contributions)}",
64 | modifier = Modifier.padding(horizontal = 16.dp),
65 | color = MaterialTheme.colorScheme.onSurfaceVariant,
66 | fontSize = MaterialTheme.typography.bodyMedium.fontSize,
67 | overflow = TextOverflow.Ellipsis,
68 | maxLines = 2
69 | )
70 | }
71 |
72 | Spacer(modifier = Modifier.weight(1f))
73 |
74 | Icon(
75 | modifier = Modifier.size(32.dp),
76 | painter = painterResource(
77 | id = R.drawable.ic_link_outward_24
78 | ),
79 | contentDescription = null
80 | )
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/BottomDestination.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.res.painterResource
7 | import androidx.compose.ui.res.stringResource
8 | import androidx.navigation.NavBackStackEntry
9 | import androidx.navigation.NavDestination.Companion.hasRoute
10 | import androidx.navigation.NavDestination.Companion.hierarchy
11 | import com.github.yohannestz.satori.R
12 | import com.github.yohannestz.satori.ui.base.navigation.Route
13 |
14 | sealed class BottomDestination(
15 | val value: String,
16 | val route: Any,
17 | @StringRes val title: Int,
18 | @DrawableRes val icon: Int,
19 | @DrawableRes val iconSelected: Int
20 | ) {
21 |
22 | data object Home : BottomDestination(
23 | value = "home",
24 | route = Route.Tab.Home,
25 | title = R.string.title_home,
26 | icon = R.drawable.ic_outline_home_24,
27 | iconSelected = R.drawable.ic_round_home_24
28 | )
29 |
30 | data object Latest : BottomDestination(
31 | value = "latest",
32 | route = Route.Tab.Latest,
33 | title = R.string.title_latest,
34 | icon = R.drawable.ic_outline_fire_24,
35 | iconSelected = R.drawable.ic_round_fire_24
36 | )
37 |
38 | data object Bookmarks : BottomDestination(
39 | value = "bookmarks",
40 | route = Route.Tab.Bookmarks,
41 | title = R.string.title_bookmarks,
42 | icon = R.drawable.ic_outline_collections_bookmark_24,
43 | iconSelected = R.drawable.ic_round_collections_bookmark_24
44 | )
45 |
46 | data object More : BottomDestination(
47 | value = "settings",
48 | route = Route.Tab.More,
49 | title = R.string.title_more,
50 | icon = R.drawable.ic_round_more_horiz_24,
51 | iconSelected = R.drawable.ic_round_more_horiz_24
52 | )
53 |
54 | companion object {
55 | val values = listOf(Home, Latest, Bookmarks, More)
56 | val railValues = listOf(Home, Latest, Bookmarks, More)
57 | private val topAppBarDisallowed = listOf(Bookmarks, More)
58 |
59 | fun String.toBottomDestinationIndex() = when (this) {
60 | Home.value -> 0
61 | Latest.value -> 1
62 | Bookmarks.value -> 2
63 | More.value -> 3
64 | else -> null
65 | }
66 |
67 | fun NavBackStackEntry.isBottomDestination() =
68 | destination.hierarchy.any { dest ->
69 | values.any { value -> dest.hasRoute(value.route::class) }
70 | }
71 |
72 | fun NavBackStackEntry.isTopAppBarDisallowed() =
73 | destination.hierarchy.any { dest ->
74 | topAppBarDisallowed.any { value -> dest.hasRoute(value.route::class) }
75 | }
76 |
77 | @Composable
78 | fun BottomDestination.Icon(selected: Boolean) {
79 | androidx.compose.material3.Icon(
80 | painter = painterResource(if (selected) iconSelected else icon),
81 | contentDescription = stringResource(title)
82 | )
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/StartTab.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base
2 |
3 | import com.github.yohannestz.satori.R
4 |
5 | enum class StartTab(
6 | val value: String
7 | ) {
8 | LAST_USED("last_used"),
9 | HOME("home"),
10 | LATEST("latest"),
11 | BOOKMARKS("bookmarks"),
12 | MORE("more");
13 |
14 | val stringRes
15 | get() = when (this) {
16 | HOME -> R.string.title_home
17 | LATEST -> R.string.title_top
18 | BOOKMARKS -> R.string.title_bookmarks
19 | MORE -> R.string.title_settings
20 | LAST_USED -> R.string.last_used
21 | }
22 |
23 | companion object {
24 | fun valueOf(tabName: String) = entries.find { it.value == tabName }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/TabRowItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 |
6 | data class TabRowItem(
7 | val value: T,
8 | @StringRes val title: Int?,
9 | @DrawableRes val icon: Int? = null,
10 | )
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/ThemeStyle.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base
2 |
3 | import com.github.yohannestz.satori.R
4 |
5 |
6 | enum class ThemeStyle {
7 | FOLLOW_SYSTEM, LIGHT, DARK;
8 |
9 | val stringRes
10 | get() = when (this) {
11 | FOLLOW_SYSTEM -> R.string.theme_system
12 | LIGHT -> R.string.theme_light
13 | DARK -> R.string.theme_dark
14 | }
15 |
16 | companion object {
17 | fun valueOfOrNull(value: String) = try {
18 | valueOf(value)
19 | } catch (e: IllegalArgumentException) {
20 | null
21 | }
22 |
23 | val entriesLocalized = entries.associateWith { it.stringRes }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/event/PagedUiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base.event
2 |
3 | interface PagedUiEvent: UiEvent {
4 | fun loadMore()
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/event/UiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base.event
2 |
3 | interface UiEvent {
4 | fun showMessage(message: String?)
5 | fun onMessageDisplayed()
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/navigation/NavActionManager.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Immutable
5 | import androidx.compose.runtime.remember
6 | import androidx.navigation.NavHostController
7 | import androidx.navigation.compose.rememberNavController
8 |
9 | @Immutable
10 | class NavActionManager(
11 | private val navController: NavHostController
12 | ) {
13 |
14 | fun navigateTo(route: Route) {
15 | navController.navigate(route)
16 | }
17 | fun navigateToDetail(id: String) {
18 | navController.navigate("${Route.VolumeDetail.BASE_ROUTE}/$id")
19 | }
20 |
21 | fun navigateUp() {
22 | navController.navigateUp()
23 | }
24 |
25 | fun goBack() {
26 | navController.popBackStack()
27 | }
28 |
29 | fun goBack(route: String) {
30 | navController.popBackStack(route, false)
31 | }
32 |
33 | companion object {
34 | @Composable
35 | fun rememberNavActionManager(
36 | navController: NavHostController = rememberNavController()
37 | ) = remember {
38 | NavActionManager(navController)
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/navigation/Route.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base.navigation
2 |
3 | import com.github.yohannestz.satori.data.model.VolumeCategory
4 | import com.github.yohannestz.satori.utils.VOLUME_DETAIL
5 | import kotlinx.serialization.Serializable
6 |
7 | sealed interface Route {
8 | sealed interface Tab : Route {
9 | @Serializable
10 | data object Home : Tab
11 |
12 | @Serializable
13 | data object Latest : Tab
14 |
15 | @Serializable
16 | data object Bookmarks : Tab
17 |
18 | @Serializable
19 | data object More : Tab
20 | }
21 |
22 | @Serializable
23 | data object Search : Route
24 |
25 | @Serializable
26 | data object Settings: Route
27 |
28 | @Serializable
29 | data object About: Route
30 |
31 | @Serializable
32 | data class VolumeList(val volumeCategory: VolumeCategory) : Route {
33 | companion object {
34 | private const val BASE_ROUTE = "volume_list"
35 | fun withArgs(volumeCategory: VolumeCategory) = "$BASE_ROUTE/${volumeCategory.name}"
36 | }
37 | }
38 |
39 | @Serializable
40 | data class VolumeDetail(val volumeId: String) : Route {
41 | companion object {
42 | const val BASE_ROUTE = VOLUME_DETAIL
43 | const val DEEPLINK_ROUTE = "https://play.google.com/store/books/details?id={id}"
44 | fun withArgs(volumeId: String) = "$BASE_ROUTE/$volumeId"
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/state/PagedUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base.state
2 |
3 | abstract class PagedUiState : UiState() {
4 | abstract val nextPage: Int?
5 | abstract val loadMore: Boolean
6 |
7 | val canLoadMore get() = nextPage != null && !isLoading
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/state/UiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base.state
2 |
3 | abstract class UiState {
4 | abstract val isLoading: Boolean
5 | abstract val message: String?
6 |
7 | // These methods are required because we can't have an abstract data class
8 | // so we need to manually implement the copy() method
9 |
10 | /**
11 | * copy(isLoading = value)
12 | */
13 | abstract fun setLoading(value: Boolean): UiState
14 |
15 | /**
16 | * copy(message = value)
17 | */
18 | abstract fun setMessage(value: String?): UiState
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/base/viewmodel/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.base.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.github.yohannestz.satori.ui.base.event.UiEvent
5 | import com.github.yohannestz.satori.ui.base.state.UiState
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.StateFlow
8 | import kotlinx.coroutines.flow.asStateFlow
9 | import kotlinx.coroutines.flow.update
10 |
11 |
12 | abstract class BaseViewModel : ViewModel(), UiEvent {
13 |
14 | protected abstract val mutableUiState: MutableStateFlow
15 | val uiState: StateFlow by lazy { mutableUiState.asStateFlow() }
16 |
17 | @Suppress("UNCHECKED_CAST")
18 | fun setLoading(value: Boolean) {
19 | mutableUiState.update { it.setLoading(value) as S }
20 | }
21 |
22 | @Suppress("UNCHECKED_CAST")
23 | override fun showMessage(message: String?) {
24 | mutableUiState.update { it.setMessage(message ?: GENERIC_ERROR) as S }
25 | }
26 |
27 | @Suppress("UNCHECKED_CAST")
28 | override fun onMessageDisplayed() {
29 | mutableUiState.update { it.setMessage(null) as S }
30 | }
31 |
32 | companion object {
33 | private const val GENERIC_ERROR = "Generic Error"
34 | const val FLOW_TIMEOUT = 5_000L
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/bookmarks/BookMarksEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.bookmarks
2 |
3 | import com.github.yohannestz.satori.data.model.volume.BookMarkItem
4 | import com.github.yohannestz.satori.ui.base.event.PagedUiEvent
5 |
6 | interface BookMarksEvent: PagedUiEvent {
7 | fun refreshList()
8 | fun onDeleteFromBookMarksClicked(item: BookMarkItem)
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/bookmarks/BookMarksUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.bookmarks
2 |
3 | import androidx.compose.runtime.Stable
4 | import com.github.yohannestz.satori.data.model.ViewMode
5 | import com.github.yohannestz.satori.data.model.volume.BookMarkItem
6 | import com.github.yohannestz.satori.ui.base.state.PagedUiState
7 |
8 | @Stable
9 | data class BookMarksUiState(
10 | val bookMarks: List = emptyList(),
11 | val viewMode: ViewMode = ViewMode.LIST,
12 | val errorMessage: String? = null,
13 | val isLoadingMore: Boolean = false,
14 | val noResult: Boolean = false,
15 | override val nextPage: Int? = null,
16 | override val loadMore: Boolean = true,
17 | override val isLoading: Boolean = false,
18 | override val message: String? = null
19 | ) : PagedUiState() {
20 | override fun setLoading(value: Boolean) = copy(isLoading = value)
21 | override fun setMessage(value: String?) = copy(message = value)
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/bookmarks/BookMarksViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.bookmarks
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.github.yohannestz.satori.data.model.volume.BookMarkItem
5 | import com.github.yohannestz.satori.data.repository.BookMarkRepository
6 | import com.github.yohannestz.satori.ui.base.viewmodel.BaseViewModel
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.flow.update
9 | import kotlinx.coroutines.launch
10 |
11 | class BookMarksViewModel(
12 | private val bookMarkRepository: BookMarkRepository
13 | ) : BaseViewModel(), BookMarksEvent {
14 | override val mutableUiState: MutableStateFlow = MutableStateFlow(
15 | BookMarksUiState()
16 | )
17 |
18 | override fun onDeleteFromBookMarksClicked(item: BookMarkItem) {
19 | viewModelScope.launch {
20 | bookMarkRepository.deleteItemById(item.id)
21 | }
22 | }
23 |
24 | override fun refreshList() {
25 | mutableUiState.update { it.copy(nextPage = null, loadMore = true) }
26 | }
27 |
28 | override fun loadMore() {
29 | mutableUiState.value.run {
30 | if (canLoadMore) {
31 | mutableUiState.update {
32 | it.copy(loadMore = true)
33 | }
34 | }
35 | }
36 | }
37 |
38 | init {
39 | viewModelScope.launch {
40 | bookMarkRepository.getBookMarkedItems().collect { items ->
41 | mutableUiState.update {
42 | it.copy(bookMarks = items)
43 | }
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/bookmarks/composables/BookMarksGridItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.bookmarks.composables
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.layout.sizeIn
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.clip
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.text.style.TextAlign
17 | import androidx.compose.ui.text.style.TextOverflow
18 | import androidx.compose.ui.unit.dp
19 | import androidx.compose.ui.unit.sp
20 | import com.github.yohannestz.satori.R
21 | import com.github.yohannestz.satori.data.model.volume.BookMarkItem
22 | import com.github.yohannestz.satori.ui.composables.MEDIA_POSTER_SMALL_HEIGHT
23 | import com.github.yohannestz.satori.ui.composables.MEDIA_POSTER_SMALL_WIDTH
24 | import com.github.yohannestz.satori.ui.composables.PosterImage
25 |
26 | @Composable
27 | fun BookMarksGridItem(
28 | item: BookMarkItem,
29 | onClick: () -> Unit,
30 | onRemoveIconClicked: () -> Unit
31 | ) {
32 | Column(
33 | modifier = Modifier
34 | .padding(horizontal = 8.dp)
35 | .sizeIn(maxWidth = 300.dp, minWidth = 250.dp)
36 | .clip(RoundedCornerShape(8.dp))
37 | .clickable(onClick = onClick),
38 | horizontalAlignment = Alignment.CenterHorizontally
39 | ) {
40 | PosterImage(
41 | url = item.imageUrl?.replace("http://", "https://"),
42 | showShadow = false,
43 | modifier = Modifier.size(
44 | width = MEDIA_POSTER_SMALL_WIDTH.dp,
45 | height = MEDIA_POSTER_SMALL_HEIGHT.dp
46 | )
47 | )
48 | Text(
49 | text = item.title ?: "--",
50 | fontSize = 18.sp,
51 | overflow = TextOverflow.Ellipsis,
52 | maxLines = 1,
53 | textAlign = TextAlign.Center,
54 | modifier = Modifier.padding(bottom = 4.dp)
55 | )
56 |
57 | Text(
58 | text = if (item.authors != null) item.authors.split(",")
59 | .joinToString("\n") else stringResource(R.string.unknown),
60 | color = MaterialTheme.colorScheme.onSurfaceVariant,
61 | maxLines = 1,
62 | textAlign = TextAlign.Center,
63 | modifier = Modifier.padding(bottom = 8.dp)
64 | )
65 |
66 | Text(
67 | text = item.publishedDate ?: "",
68 | color = MaterialTheme.colorScheme.outline,
69 | maxLines = 1,
70 | textAlign = TextAlign.Center,
71 | modifier = Modifier.padding(bottom = 8.dp)
72 | )
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/bookmarks/composables/BookMarksListItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.bookmarks.composables
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.IntrinsicSize
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxHeight
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.heightIn
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.width
13 | import androidx.compose.material3.Card
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.text.style.TextOverflow
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.unit.sp
23 | import com.github.yohannestz.satori.R
24 | import com.github.yohannestz.satori.data.model.volume.BookMarkItem
25 | import com.github.yohannestz.satori.ui.composables.MEDIA_POSTER_SMALL_HEIGHT
26 | import com.github.yohannestz.satori.ui.composables.MEDIA_POSTER_SMALL_WIDTH
27 | import com.github.yohannestz.satori.ui.composables.PosterImage
28 |
29 | @Composable
30 | fun BookMarksListItem(
31 | modifier: Modifier = Modifier,
32 | item: BookMarkItem,
33 | onClick: () -> Unit,
34 | ) {
35 | Card(
36 | modifier = modifier
37 | .fillMaxWidth()
38 | .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
39 | .clickable(onClick = onClick),
40 | ) {
41 | Row(
42 | modifier = Modifier.height(IntrinsicSize.Max),
43 | verticalAlignment = Alignment.CenterVertically
44 | ) {
45 | PosterImage(
46 | url = item.imageUrl?.replace("http://", "https://"),
47 | showShadow = false,
48 | modifier = Modifier
49 | .fillMaxHeight()
50 | .width(MEDIA_POSTER_SMALL_WIDTH.dp)
51 | )
52 |
53 | Column(
54 | modifier = Modifier.heightIn(min = MEDIA_POSTER_SMALL_HEIGHT.dp),
55 | ) {
56 | Text(
57 | text = if (item.authors != null) item.authors.split(",")
58 | .joinToString("\n") else stringResource(R.string.unknown),
59 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
60 | color = MaterialTheme.colorScheme.onSurface,
61 | fontSize = MaterialTheme.typography.titleMedium.fontSize,
62 | lineHeight = 22.sp,
63 | overflow = TextOverflow.Ellipsis,
64 | maxLines = 2
65 | )
66 | Text(
67 | text = if (item.publisher != null) "${item.publisher} - ${item.publishedDate}" else item.publishedDate
68 | ?: stringResource(R.string.unknown),
69 | modifier = Modifier.padding(horizontal = 16.dp),
70 | color = MaterialTheme.colorScheme.onSurfaceVariant,
71 | fontSize = MaterialTheme.typography.bodyMedium.fontSize,
72 | overflow = TextOverflow.Ellipsis,
73 | maxLines = 2
74 | )
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/CommonIconButton.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.material3.Icon
4 | import androidx.compose.material3.IconButton
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.platform.LocalContext
7 | import androidx.compose.ui.res.painterResource
8 | import androidx.compose.ui.res.stringResource
9 | import com.github.yohannestz.satori.R
10 | import com.github.yohannestz.satori.utils.Extensions.openShareSheet
11 |
12 | fun singleClick(onClick: () -> Unit): () -> Unit {
13 | var latest = 0L
14 | return {
15 | val now = System.currentTimeMillis()
16 | if (now - latest >= 1000) {
17 | onClick()
18 | latest = now
19 | }
20 | }
21 | }
22 |
23 | @Composable
24 | fun BackIconButton(
25 | onClick: () -> Unit
26 | ) {
27 | IconButton(onClick = singleClick(onClick)) {
28 | Icon(
29 | painter = painterResource(R.drawable.ic_round_arrow_back_24),
30 | contentDescription = stringResource(R.string.action_back)
31 | )
32 | }
33 | }
34 |
35 | @Composable
36 | fun ShareIconButton(url: String) {
37 | val context = LocalContext.current
38 | IconButton(onClick = { context.openShareSheet(url) }) {
39 | Icon(
40 | painter = painterResource(R.drawable.ic_round_share_24),
41 | contentDescription = stringResource(R.string.share)
42 | )
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/CommonText.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.unit.dp
9 | import androidx.compose.ui.unit.sp
10 |
11 | @Composable
12 | fun InfoTitle(text: String) {
13 | Text(
14 | text = text,
15 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
16 | fontSize = 18.sp,
17 | fontWeight = FontWeight.Bold
18 | )
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/DefaultScaffoldWithMediumTopAppBar.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.RowScope
5 | import androidx.compose.foundation.layout.WindowInsets
6 | import androidx.compose.foundation.layout.systemBars
7 | import androidx.compose.material3.ExperimentalMaterial3Api
8 | import androidx.compose.material3.MediumTopAppBar
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBarColors
12 | import androidx.compose.material3.TopAppBarDefaults
13 | import androidx.compose.material3.TopAppBarScrollBehavior
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.text.font.FontWeight
17 | import androidx.compose.ui.unit.sp
18 |
19 | @OptIn(ExperimentalMaterial3Api::class)
20 | @Composable
21 | fun DefaultScaffoldWithMediumTopAppBar(
22 | title: String,
23 | modifier: Modifier = Modifier,
24 | floatActionButton: @Composable () -> Unit = {},
25 | navigationIcon: @Composable () -> Unit = {},
26 | actions: @Composable (RowScope.() -> Unit) = {},
27 | scrollBehavior: TopAppBarScrollBehavior,
28 | contentWindowInsets: WindowInsets = WindowInsets.systemBars,
29 | colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors(),
30 | content: @Composable (PaddingValues) -> Unit
31 | ) {
32 | Scaffold(
33 | modifier = modifier,
34 | topBar = {
35 | MediumTopAppBar(
36 | title = {
37 | Text(
38 | text = title,
39 | fontSize = 24.sp,
40 | fontWeight = FontWeight.W400,
41 | lineHeight = 32.sp
42 | )
43 | },
44 | navigationIcon = navigationIcon,
45 | actions = actions,
46 | scrollBehavior = scrollBehavior,
47 | colors = colors
48 | )
49 | },
50 | floatingActionButton = floatActionButton,
51 | contentWindowInsets = contentWindowInsets,
52 | content = content
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/DefaultScaffoldWithSmallTopAppBar.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.RowScope
5 | import androidx.compose.foundation.layout.WindowInsets
6 | import androidx.compose.foundation.layout.systemBars
7 | import androidx.compose.material3.ExperimentalMaterial3Api
8 | import androidx.compose.material3.Scaffold
9 | import androidx.compose.material3.Text
10 | import androidx.compose.material3.TopAppBar
11 | import androidx.compose.material3.TopAppBarScrollBehavior
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.text.font.FontWeight
15 | import androidx.compose.ui.unit.sp
16 |
17 | @OptIn(ExperimentalMaterial3Api::class)
18 | @Composable
19 | fun DefaultScaffoldWithSmallTopAppBar(
20 | title: String,
21 | modifier: Modifier = Modifier,
22 | floatingActionButton: @Composable () -> Unit = {},
23 | navigationIcon: @Composable () -> Unit = {},
24 | actions: @Composable (RowScope.() -> Unit) = {},
25 | scrollBehavior: TopAppBarScrollBehavior,
26 | contentWindowInsets: WindowInsets = WindowInsets.systemBars,
27 | content: @Composable (PaddingValues) -> Unit
28 | ) {
29 | Scaffold(
30 | modifier = modifier,
31 | topBar = {
32 | TopAppBar(
33 | title = {
34 | Text(
35 | text = title,
36 | fontSize = 22.sp,
37 | fontWeight = FontWeight.W400,
38 | lineHeight = 28.sp
39 | )
40 | },
41 | navigationIcon = navigationIcon,
42 | actions = actions,
43 | scrollBehavior = scrollBehavior
44 | )
45 | },
46 | floatingActionButton = floatingActionButton,
47 | contentWindowInsets = contentWindowInsets,
48 | content = content
49 | )
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/DefaultScaffoldWithTopAppBar.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.WindowInsets
5 | import androidx.compose.foundation.layout.systemBars
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.Scaffold
8 | import androidx.compose.material3.TopAppBarDefaults
9 | import androidx.compose.material3.rememberTopAppBarState
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.input.nestedscroll.nestedScroll
13 |
14 |
15 | @OptIn(ExperimentalMaterial3Api::class)
16 | @Composable
17 | fun DefaultScaffoldWithTopAppBar(
18 | title: String,
19 | navigateBack: () -> Unit,
20 | floatingActionButton: @Composable (() -> Unit) = {},
21 | contentWindowInsets: WindowInsets = WindowInsets.systemBars,
22 | content: @Composable (PaddingValues) -> Unit
23 | ) {
24 | val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
25 | rememberTopAppBarState()
26 | )
27 | Scaffold(
28 | modifier = Modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
29 | topBar = {
30 | DefaultTopAppBar(
31 | title = title,
32 | scrollBehavior = topAppBarScrollBehavior,
33 | navigateBack = navigateBack
34 | )
35 | },
36 | floatingActionButton = floatingActionButton,
37 | contentWindowInsets = contentWindowInsets,
38 | content = content
39 | )
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/DefaultTopAppBar.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.material3.ExperimentalMaterial3Api
4 | import androidx.compose.material3.Text
5 | import androidx.compose.material3.TopAppBar
6 | import androidx.compose.material3.TopAppBarScrollBehavior
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.tooling.preview.Preview
9 | import com.github.yohannestz.satori.ui.theme.SatoriTheme
10 |
11 |
12 | @OptIn(ExperimentalMaterial3Api::class)
13 | @Composable
14 | fun DefaultTopAppBar(
15 | title: String,
16 | scrollBehavior: TopAppBarScrollBehavior? = null,
17 | navigateBack: () -> Unit,
18 | ) {
19 | TopAppBar(
20 | title = { Text(text = title) },
21 | navigationIcon = {
22 | BackIconButton(onClick = navigateBack)
23 | },
24 | scrollBehavior = scrollBehavior
25 | )
26 | }
27 |
28 | @OptIn(ExperimentalMaterial3Api::class)
29 | @Preview
30 | @Composable
31 | fun DefaultTopAppBarPreview() {
32 | SatoriTheme {
33 | DefaultTopAppBar(
34 | title = "Satori",
35 | navigateBack = {}
36 | )
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/HorizontalListHeader.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.vector.ImageVector
15 | import androidx.compose.ui.res.vectorResource
16 | import androidx.compose.ui.text.font.FontWeight
17 | import androidx.compose.ui.unit.dp
18 | import androidx.compose.ui.unit.sp
19 | import com.github.yohannestz.satori.R
20 |
21 | @Composable
22 | fun HorizontalListHeader(text: String, onClick: () -> Unit) {
23 | Box(
24 | modifier = Modifier.clickable(onClick = onClick)
25 | ) {
26 | Row(
27 | modifier = Modifier
28 | .padding(horizontal = 20.dp, vertical = 16.dp)
29 | .fillMaxWidth(),
30 | horizontalArrangement = Arrangement.SpaceBetween,
31 | verticalAlignment = Alignment.CenterVertically
32 | ) {
33 | Text(text, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
34 | Icon(
35 | imageVector = ImageVector.vectorResource(R.drawable.ic_round_arrow_forward_24),
36 | contentDescription = text
37 | )
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/HorizontalPlaceHolder.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.clip
17 | import androidx.compose.ui.text.style.TextOverflow
18 | import androidx.compose.ui.unit.dp
19 | import androidx.compose.ui.unit.sp
20 | import com.github.yohannestz.satori.utils.Extensions.defaultPlaceholder
21 |
22 | @Composable
23 | fun HorizontalPlaceHolder() {
24 | Row(
25 | modifier = Modifier
26 | .fillMaxWidth()
27 | .height(MEDIA_POSTER_SMALL_HEIGHT.dp)
28 | ) {
29 | Box(
30 | modifier = Modifier
31 | .size(
32 | width = MEDIA_POSTER_SMALL_WIDTH.dp,
33 | height = MEDIA_POSTER_SMALL_HEIGHT.dp
34 | )
35 | .clip(RoundedCornerShape(8.dp))
36 | .defaultPlaceholder(visible = true)
37 | )
38 |
39 | Column(
40 | modifier = Modifier
41 | .padding(horizontal = 16.dp),
42 | verticalArrangement = Arrangement.SpaceEvenly
43 | ) {
44 | Text(
45 | text = "This is a placeholder text",
46 | modifier = Modifier.defaultPlaceholder(visible = true),
47 | fontSize = 17.sp,
48 | overflow = TextOverflow.Ellipsis,
49 | maxLines = 1
50 | )
51 |
52 | Spacer(modifier = Modifier.height(8.dp))
53 |
54 | Text(
55 | text = "This is a placeholder",
56 | modifier = Modifier.defaultPlaceholder(visible = true)
57 | )
58 |
59 | Spacer(modifier = Modifier.height(8.dp))
60 |
61 | Text(
62 | text = "Placeholder",
63 | modifier = Modifier.defaultPlaceholder(visible = true)
64 | )
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/LazyListState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 |
4 | import androidx.compose.foundation.lazy.LazyListState
5 | import androidx.compose.foundation.lazy.grid.LazyGridState
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.LaunchedEffect
8 | import androidx.compose.runtime.derivedStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.runtime.snapshotFlow
11 |
12 | /**
13 | * Extension function to load more items when the bottom is reached
14 | * @param buffer Tells how many items before it reaches the bottom of the list to call `onLoadMore`. This value should be >= 0
15 | * @param onLoadMore The code to execute when it reaches the bottom of the list
16 | * @author Manav Tamboli
17 | */
18 | @Composable
19 | fun LazyListState.OnBottomReached(
20 | buffer: Int = 0,
21 | onLoadMore: suspend () -> Unit
22 | ) {
23 | // Buffer must be positive.
24 | // Or our list will never reach the bottom.
25 | require(buffer >= 0) { "buffer cannot be negative, but was $buffer" }
26 |
27 | val shouldLoadMore = remember {
28 | derivedStateOf {
29 | val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
30 | ?: return@derivedStateOf true
31 |
32 | // subtract buffer from the total items
33 | lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - buffer
34 | }
35 | }
36 |
37 | LaunchedEffect(shouldLoadMore) {
38 | snapshotFlow { shouldLoadMore.value }
39 | .collect { if (it) onLoadMore() }
40 | }
41 | }
42 |
43 | /**
44 | * Extension function to load more items when the bottom is reached
45 | * @param buffer Tells how many items before it reaches the bottom of the list to call `onLoadMore`. This value should be >= 0
46 | * @param onLoadMore The code to execute when it reaches the bottom of the list
47 | * @author Manav Tamboli
48 | */
49 | @Composable
50 | fun LazyGridState.OnBottomReached(
51 | buffer: Int = 0,
52 | onLoadMore: suspend () -> Unit
53 | ) {
54 | // Buffer must be positive.
55 | // Or our list will never reach the bottom.
56 | require(buffer >= 0) { "buffer cannot be negative, but was $buffer" }
57 |
58 | val shouldLoadMore = remember {
59 | derivedStateOf {
60 | val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
61 | ?: return@derivedStateOf true
62 |
63 | // subtract buffer from the total items
64 | lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - buffer
65 | }
66 | }
67 |
68 | LaunchedEffect(shouldLoadMore) {
69 | snapshotFlow { shouldLoadMore.value }
70 | .collect { if (it) onLoadMore() }
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/PosterImage.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.foundation.layout.size
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.draw.clip
10 | import androidx.compose.ui.draw.shadow
11 | import androidx.compose.ui.graphics.painter.ColorPainter
12 | import androidx.compose.ui.layout.ContentScale
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import androidx.compose.ui.unit.dp
15 | import coil3.compose.AsyncImage
16 |
17 | const val MEDIA_POSTER_COMPACT_HEIGHT = 100
18 | const val MEDIA_POSTER_COMPACT_WIDTH = 100
19 |
20 | const val MEDIA_POSTER_SMALL_HEIGHT = 140
21 | const val MEDIA_POSTER_SMALL_WIDTH = 100
22 |
23 | const val MEDIA_POSTER_MEDIUM_HEIGHT = 156
24 | const val MEDIA_POSTER_MEDIUM_WIDTH = 110
25 |
26 | const val MEDIA_POSTER_BIG_HEIGHT = 213
27 | const val MEDIA_POSTER_BIG_WIDTH = 150
28 |
29 | @Composable
30 | fun PosterImage(
31 | url: String?,
32 | showShadow: Boolean = true,
33 | contentScale: ContentScale = ContentScale.Crop,
34 | modifier: Modifier
35 | ) {
36 | AsyncImage(
37 | model = url,
38 | contentDescription = "poster_image",
39 | contentScale = contentScale,
40 | placeholder = ColorPainter(MaterialTheme.colorScheme.outline),
41 | error = ColorPainter(MaterialTheme.colorScheme.outline),
42 | fallback = ColorPainter(MaterialTheme.colorScheme.outline),
43 | modifier = modifier
44 | .then(
45 | if (showShadow) Modifier
46 | .padding(start = 4.dp, top = 2.dp, end = 4.dp, bottom = 8.dp)
47 | .shadow(4.dp, shape = RoundedCornerShape(8.dp))
48 | else Modifier
49 | )
50 | .clip(RoundedCornerShape(8.dp))
51 | )
52 | }
53 |
54 | @Preview(showBackground = true)
55 | @Composable
56 | private fun PosterImagePreview() {
57 | PosterImage(
58 | url = "https://example.com/sample-poster.jpg",
59 | showShadow = true,
60 | modifier = Modifier
61 | .size(width = 120.dp, height = 180.dp)
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/ScoreIndicator.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.painterResource
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import androidx.compose.ui.unit.TextUnit
15 | import androidx.compose.ui.unit.dp
16 | import androidx.compose.ui.unit.sp
17 | import com.github.yohannestz.satori.R
18 | import com.github.yohannestz.satori.ui.theme.SatoriTheme
19 | import com.github.yohannestz.satori.utils.Extensions.toStringPositiveValueOrUnknown
20 |
21 | @Composable
22 | fun SmallScoreIndicator(
23 | score: Int?,
24 | modifier: Modifier = Modifier,
25 | fontSize: TextUnit = 14.sp,
26 | ) {
27 | Row(
28 | modifier = modifier,
29 | verticalAlignment = Alignment.CenterVertically
30 | ) {
31 | Icon(
32 | painter = painterResource(R.drawable.ic_round_star_24),
33 | contentDescription = stringResource(R.string.mean_score),
34 | tint = MaterialTheme.colorScheme.outline
35 | )
36 | Text(
37 | text = score.toStringPositiveValueOrUnknown(),
38 | modifier = Modifier.padding(horizontal = 4.dp),
39 | color = MaterialTheme.colorScheme.outline,
40 | fontSize = fontSize
41 | )
42 | }
43 | }
44 |
45 | @Composable
46 | fun SmallScoreIndicator(
47 | score: Float?,
48 | modifier: Modifier = Modifier,
49 | fontSize: TextUnit = 14.sp,
50 | ) {
51 | Row(
52 | modifier = modifier,
53 | verticalAlignment = Alignment.CenterVertically
54 | ) {
55 | Icon(
56 | painter = painterResource(R.drawable.ic_round_star_24),
57 | contentDescription = stringResource(R.string.mean_score),
58 | tint = MaterialTheme.colorScheme.outline
59 | )
60 | Text(
61 | text = score.toStringPositiveValueOrUnknown(),
62 | modifier = Modifier.padding(horizontal = 4.dp),
63 | color = MaterialTheme.colorScheme.outline,
64 | fontSize = fontSize
65 | )
66 | }
67 | }
68 |
69 | @Preview(showBackground = true)
70 | @Composable
71 | fun SmallScoreIndicatorPreview() {
72 | SatoriTheme {
73 | SmallScoreIndicator(score = 4.8f)
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/TopBannerView.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Surface
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Brush
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.graphics.painter.ColorPainter
17 | import androidx.compose.ui.layout.ContentScale
18 | import androidx.compose.ui.tooling.preview.Preview
19 | import androidx.compose.ui.unit.Dp
20 | import androidx.compose.ui.unit.dp
21 | import coil3.compose.AsyncImage
22 | import com.github.yohannestz.satori.ui.theme.SatoriTheme
23 | import com.github.yohannestz.satori.ui.theme.banner_shadow_color
24 |
25 | @Composable
26 | fun TopBannerView(
27 | imageUrl: String?,
28 | modifier: Modifier = Modifier,
29 | fallBackColor: Color? = null,
30 | height: Dp
31 | ) {
32 | Box(
33 | modifier = modifier
34 | .fillMaxWidth()
35 | .height(height)
36 | .padding(bottom = 16.dp),
37 | contentAlignment = Alignment.TopStart
38 | ) {
39 | if (imageUrl != null) {
40 | AsyncImage(
41 | model = imageUrl,
42 | contentDescription = "banner",
43 | placeholder = ColorPainter(MaterialTheme.colorScheme.outline),
44 | modifier = Modifier.fillMaxSize(),
45 | contentScale = ContentScale.Crop
46 | )
47 | } else {
48 | Box(
49 | modifier = Modifier
50 | .background(
51 | color = fallBackColor ?: MaterialTheme.colorScheme.outline
52 | )
53 | .fillMaxSize()
54 | )
55 | }
56 | Box(
57 | modifier = Modifier
58 | .fillMaxSize()
59 | .background(
60 | Brush.verticalGradient(
61 | listOf(banner_shadow_color, MaterialTheme.colorScheme.surface)
62 | )
63 | )
64 | )
65 | }
66 | }
67 |
68 | @Preview
69 | @Composable
70 | fun TopBannerViewPreview() {
71 | SatoriTheme {
72 | Surface {
73 | TopBannerView(
74 | imageUrl = "https://picsum.photos/200",
75 | fallBackColor = MaterialTheme.colorScheme.secondary,
76 | height = 250.dp
77 | )
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/preferences/PlainPreferenceView.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables.preferences
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.res.painterResource
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 |
22 | @Composable
23 | fun PlainPreferenceView(
24 | title: String,
25 | modifier: Modifier = Modifier,
26 | subtitle: String? = null,
27 | @DrawableRes icon: Int? = null,
28 | onClick: () -> Unit
29 | ) {
30 | Row(
31 | modifier = modifier
32 | .fillMaxWidth()
33 | .clickable(onClick = onClick),
34 | horizontalArrangement = Arrangement.SpaceBetween,
35 | verticalAlignment = Alignment.CenterVertically
36 | ) {
37 | Row(
38 | horizontalArrangement = Arrangement.Start,
39 | verticalAlignment = Alignment.CenterVertically
40 | ) {
41 | if (icon != null) {
42 | Icon(
43 | painter = painterResource(icon),
44 | contentDescription = "",
45 | modifier = Modifier.padding(16.dp),
46 | tint = MaterialTheme.colorScheme.primary
47 | )
48 | } else {
49 | Spacer(
50 | modifier = Modifier
51 | .padding(16.dp)
52 | .size(24.dp)
53 | )
54 | }
55 |
56 | Column(
57 | modifier = if (subtitle != null)
58 | Modifier.padding(16.dp)
59 | else Modifier.padding(horizontal = 16.dp)
60 | ) {
61 | Text(
62 | text = title,
63 | color = MaterialTheme.colorScheme.onSurface
64 | )
65 |
66 | if (subtitle != null) {
67 | Text(
68 | text = subtitle,
69 | color = MaterialTheme.colorScheme.onSurfaceVariant,
70 | fontSize = 13.sp
71 | )
72 | }
73 | }
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/composables/preferences/SwitchPreferenceView.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.composables.preferences
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Switch
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.res.painterResource
20 | import androidx.compose.ui.unit.dp
21 |
22 | @Composable
23 | fun SwitchPreferenceView(
24 | title: String,
25 | modifier: Modifier = Modifier,
26 | subtitle: String? = null,
27 | value: Boolean,
28 | @DrawableRes icon: Int? = null,
29 | onValueChange: (Boolean) -> Unit
30 | ) {
31 | Row(
32 | modifier = modifier
33 | .fillMaxWidth()
34 | .clickable {
35 | onValueChange(!value)
36 | },
37 | horizontalArrangement = Arrangement.SpaceBetween,
38 | verticalAlignment = Alignment.CenterVertically
39 | ) {
40 | Row(
41 | modifier = Modifier.weight(1f),
42 | horizontalArrangement = Arrangement.Start,
43 | verticalAlignment = Alignment.CenterVertically
44 | ) {
45 | if (icon != null) {
46 | Icon(
47 | painter = painterResource(icon),
48 | contentDescription = "",
49 | modifier = Modifier.padding(16.dp),
50 | tint = MaterialTheme.colorScheme.primary
51 | )
52 | } else {
53 | Spacer(
54 | modifier = Modifier
55 | .padding(16.dp)
56 | .size(24.dp)
57 | )
58 | }
59 |
60 | Column(
61 | modifier = if (subtitle != null)
62 | Modifier.padding(16.dp)
63 | else Modifier.padding(horizontal = 16.dp)
64 | ) {
65 | Text(
66 | text = title,
67 | color = MaterialTheme.colorScheme.onSurface,
68 | )
69 |
70 | if (subtitle != null) {
71 | Text(
72 | text = subtitle,
73 | color = MaterialTheme.colorScheme.onSurfaceVariant,
74 | style = MaterialTheme.typography.bodySmall
75 | )
76 | }
77 | }
78 | }
79 |
80 | Switch(
81 | checked = value,
82 | onCheckedChange = {
83 | onValueChange(it)
84 | },
85 | modifier = Modifier.padding(horizontal = 16.dp)
86 | )
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/details/VolumeDetailEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.details
2 |
3 | import com.github.yohannestz.satori.data.model.volume.VolumeDetail
4 | import com.github.yohannestz.satori.ui.base.event.UiEvent
5 |
6 | interface VolumeDetailEvent : UiEvent {
7 | fun onAddToBookMarkClicked(item: VolumeDetail?)
8 | fun onRemoveFromBookMarkClicked(item: VolumeDetail?)
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/details/VolumeDetailUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.details
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.github.yohannestz.satori.data.model.volume.VolumeDetail
5 | import com.github.yohannestz.satori.ui.base.state.UiState
6 |
7 | @Immutable
8 | data class VolumeDetailUiState(
9 | val volume: VolumeDetail? = null,
10 | val isBookMarked: Boolean = false,
11 | override val isLoading: Boolean = false,
12 | override val message: String? = null
13 | ) : UiState() {
14 | override fun setLoading(value: Boolean) = copy(isLoading = value)
15 | override fun setMessage(value: String?) = copy(message = value)
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/details/VolumeDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.details
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.github.yohannestz.satori.data.local.itembookmarks.toItemEntity
5 | import com.github.yohannestz.satori.data.model.volume.BookMarkItem
6 | import com.github.yohannestz.satori.data.model.volume.VolumeDetail
7 | import com.github.yohannestz.satori.data.repository.BookMarkRepository
8 | import com.github.yohannestz.satori.data.repository.BookRepository
9 | import com.github.yohannestz.satori.ui.base.viewmodel.BaseViewModel
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.update
13 | import kotlinx.coroutines.launch
14 |
15 | class VolumeDetailViewModel(
16 | volumeId: String,
17 | private val bookMarkRepository: BookMarkRepository,
18 | private val bookRepository: BookRepository,
19 | ) : BaseViewModel(), VolumeDetailEvent {
20 | override val mutableUiState: MutableStateFlow =
21 | MutableStateFlow(VolumeDetailUiState())
22 |
23 | init {
24 | viewModelScope.launch(Dispatchers.IO) {
25 | setLoading(true)
26 | val result = bookRepository.getVolume(volumeId)
27 |
28 | if (result.isSuccess) {
29 | mutableUiState.value = mutableUiState.value.copy(
30 | volume = result.getOrNull(),
31 | isLoading = false
32 | )
33 | } else {
34 | showMessage(result.exceptionOrNull()?.message ?: "Something went wrong")
35 | setLoading(false)
36 | }
37 | }
38 |
39 | viewModelScope.launch {
40 | val isBookMarked = bookMarkRepository.isItemBookmarked(volumeId)
41 |
42 | mutableUiState.update {
43 | it.copy(isBookMarked = isBookMarked)
44 | }
45 | }
46 | }
47 |
48 | override fun onAddToBookMarkClicked(item: VolumeDetail?) {
49 | if (item != null) {
50 | viewModelScope.launch {
51 | val bookMarkItem = BookMarkItem(
52 | id = item.id,
53 | etag = item.etag,
54 | title = item.volumeInfo.title,
55 | authors = item.volumeInfo.authors?.joinToString(),
56 | imageUrl = item.volumeInfo.imageLinks?.thumbnail,
57 | smallThumbnail = item.volumeInfo.imageLinks?.smallThumbnail,
58 | publisher = item.volumeInfo.publisher,
59 | publishedDate = item.volumeInfo.publishedDate
60 | )
61 | bookMarkRepository.addItem(bookMarkItem.toItemEntity())
62 | mutableUiState.update {
63 | it.copy(isBookMarked = true)
64 | }
65 | }
66 | }
67 | }
68 |
69 | override fun onRemoveFromBookMarkClicked(item: VolumeDetail?) {
70 | if (item != null) {
71 | viewModelScope.launch {
72 | bookMarkRepository.deleteItemById(item.id)
73 | mutableUiState.update {
74 | it.copy(isBookMarked = false)
75 | }
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/details/composables/InfoView.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.details.composables
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.text.selection.SelectionContainer
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import com.github.yohannestz.satori.R
14 |
15 | @Composable
16 | fun InfoView(
17 | title: String,
18 | info: String?,
19 | modifier: Modifier = Modifier
20 | ) {
21 | Row(
22 | modifier = Modifier
23 | .padding(horizontal = 16.dp, vertical = 8.dp)
24 | ) {
25 | Text(
26 | text = title,
27 | modifier = Modifier.weight(1f),
28 | color = MaterialTheme.colorScheme.onSurfaceVariant
29 | )
30 | Column(modifier = Modifier.weight(1.4f)) {
31 | SelectionContainer {
32 | Text(text = info ?: stringResource(R.string.unknown), modifier)
33 | }
34 | }
35 | }
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/details/composables/InfoViewWithContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.details.composables
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.foundation.text.selection.SelectionContainer
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.painterResource
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.text.LinkAnnotation
18 | import androidx.compose.ui.text.buildAnnotatedString
19 | import androidx.compose.ui.text.withLink
20 | import androidx.compose.ui.unit.dp
21 | import com.github.yohannestz.satori.R
22 |
23 | @Composable
24 | fun InfoViewWithContent(
25 | title: String,
26 | epubLink: String?,
27 | pdfLink: String?,
28 | modifier: Modifier = Modifier
29 | ) {
30 | if (epubLink != null || pdfLink != null) {
31 | Row(
32 | modifier = modifier
33 | .padding(horizontal = 16.dp, vertical = 8.dp)
34 | ) {
35 | Text(
36 | text = title,
37 | modifier = Modifier.weight(1f),
38 | color = MaterialTheme.colorScheme.onSurfaceVariant
39 | )
40 | Column(modifier = Modifier.weight(1.4f)) {
41 | SelectionContainer {
42 | Column {
43 | epubLink?.let {
44 | LinkRow(link = it, label = stringResource(R.string.epub))
45 | }
46 |
47 | pdfLink?.let {
48 | LinkRow(link = it, label = stringResource(R.string.pdf))
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 | }
56 |
57 | @Composable
58 | fun LinkRow(link: String, label: String) {
59 | Row {
60 | Text(buildAnnotatedString {
61 | withLink(LinkAnnotation.Url(url = link)) {
62 | append(label)
63 | }
64 | })
65 |
66 | Spacer(modifier = Modifier.width(35.dp))
67 |
68 | Icon(
69 | modifier = Modifier.size(24.dp),
70 | painter = painterResource(id = R.drawable.ic_link_outward_24),
71 | contentDescription = null
72 | )
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/home/HomeEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.home
2 |
3 | import com.github.yohannestz.satori.ui.base.event.UiEvent
4 |
5 | interface HomeEvent : UiEvent {
6 | fun initRequestChain()
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/home/HomeUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.home
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.github.yohannestz.satori.data.model.volume.Item
5 | import com.github.yohannestz.satori.ui.base.state.UiState
6 |
7 | @Immutable
8 | data class HomeUiState(
9 | val selfHelpBooks: List- = emptyList(),
10 | val historyBooks: List
- = emptyList(),
11 | val biographyBooks: List
- = emptyList(),
12 | val fictionBooks: List
- = emptyList(),
13 | override val isLoading: Boolean = true,
14 | override val message: String? = null
15 | ) : UiState() {
16 | override fun setLoading(value: Boolean) = copy(isLoading = value)
17 | override fun setMessage(value: String?) = copy(message = value)
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/home/composables/CategoriesCard.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.home.composables
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.height
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material3.Card
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.painterResource
15 | import androidx.compose.ui.text.style.TextOverflow
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.compose.ui.unit.dp
18 | import androidx.compose.ui.unit.sp
19 | import com.github.yohannestz.satori.R
20 |
21 | @Composable
22 | fun CategoriesCard(
23 | text: String,
24 | @DrawableRes icon: Int,
25 | modifier: Modifier = Modifier,
26 | onClick: () -> Unit
27 | ) {
28 |
29 | Card(
30 | onClick = onClick,
31 | modifier = modifier.padding(start = 8.dp)
32 | ) {
33 | Row(
34 | modifier = Modifier
35 | .padding(horizontal = 16.dp, vertical = 8.dp)
36 | .height(40.dp),
37 | verticalAlignment = Alignment.CenterVertically
38 | ) {
39 | Icon(
40 | painter = painterResource(icon),
41 | contentDescription = text,
42 | modifier = Modifier
43 | .padding(end = 8.dp)
44 | .size(18.dp)
45 | )
46 |
47 | Text(
48 | text = text,
49 | fontSize = 15.sp,
50 | overflow = TextOverflow.Ellipsis,
51 | maxLines = 2,
52 | lineHeight = 15.sp
53 | )
54 | }
55 | }
56 | }
57 |
58 | @Preview
59 | @Composable
60 | private fun CategoriesCardPreview() {
61 | CategoriesCard(
62 | text = "What a long text",
63 | icon = R.drawable.ic_round_collections_bookmark_24
64 | ) {}
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/home/composables/HorizontalBookItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.home.composables
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.layout.sizeIn
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.clip
15 | import androidx.compose.ui.text.style.TextOverflow
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.unit.sp
18 | import com.github.yohannestz.satori.data.model.volume.Item
19 | import com.github.yohannestz.satori.ui.composables.MEDIA_POSTER_SMALL_HEIGHT
20 | import com.github.yohannestz.satori.ui.composables.MEDIA_POSTER_SMALL_WIDTH
21 | import com.github.yohannestz.satori.ui.composables.PosterImage
22 |
23 | @Composable
24 | fun HorizontalBookItem(
25 | item: Item,
26 | onClick: () -> Unit
27 | ) {
28 | Row(
29 | modifier = Modifier
30 | .padding(horizontal = 8.dp)
31 | .sizeIn(maxWidth = 300.dp, minWidth = 250.dp)
32 | .clip(RoundedCornerShape(8.dp))
33 | .clickable(onClick = onClick)
34 | ) {
35 | PosterImage(
36 | url = item.volumeInfo.imageLinks?.thumbnail?.replace("http://", "https://")
37 | ?: item.volumeInfo.imageLinks?.smallThumbnail?.replace("http://", "https://")
38 | ?: "",
39 | showShadow = false,
40 | modifier = Modifier.size(
41 | width = MEDIA_POSTER_SMALL_WIDTH.dp,
42 | height = MEDIA_POSTER_SMALL_HEIGHT.dp
43 | )
44 | )
45 |
46 | Column(
47 | modifier = Modifier.padding(start = 16.dp)
48 | ) {
49 | Text(
50 | text = item.volumeInfo.title ?: "--",
51 | fontSize = 18.sp,
52 | overflow = TextOverflow.Ellipsis,
53 | maxLines = 2,
54 | modifier = Modifier.padding(bottom = 4.dp)
55 | )
56 |
57 | Text(
58 | text = item.volumeInfo.authors?.joinToString(", ") ?: "",
59 | color = MaterialTheme.colorScheme.onSurfaceVariant,
60 | modifier = Modifier.padding(bottom = 8.dp),
61 | maxLines = 2
62 | )
63 |
64 | Text(
65 | text = item.volumeInfo.publishedDate ?: "",
66 | color = MaterialTheme.colorScheme.outline,
67 | modifier = Modifier.padding(bottom = 8.dp),
68 | maxLines = 2
69 | )
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/latest/LatestEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.latest
2 |
3 | import com.github.yohannestz.satori.data.model.VolumeCategory
4 | import com.github.yohannestz.satori.data.model.volume.Item
5 | import com.github.yohannestz.satori.ui.base.event.PagedUiEvent
6 |
7 | interface LatestEvent : PagedUiEvent {
8 | fun refreshList()
9 | fun onItemSelected(item: Item)
10 | fun onCategorySelected(category: VolumeCategory)
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/latest/LatestUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.latest
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.mutableStateListOf
5 | import androidx.compose.runtime.snapshots.SnapshotStateList
6 | import com.github.yohannestz.satori.data.model.VolumeCategory
7 | import com.github.yohannestz.satori.data.model.volume.Item
8 | import com.github.yohannestz.satori.ui.base.state.PagedUiState
9 |
10 | @Stable
11 | data class LatestUiState(
12 | val categoryType: VolumeCategory?,
13 | val itemList: SnapshotStateList
- = mutableStateListOf(),
14 | val isLoadingMore: Boolean = false,
15 | val selectedItem: Item? = null,
16 | val totalItems: Int = 0,
17 | val noResult: Boolean = false,
18 | override val nextPage: Int? = null,
19 | override val loadMore: Boolean = true,
20 | override val isLoading: Boolean = true,
21 | override val message: String? = null
22 | ): PagedUiState() {
23 | override fun setLoading(value: Boolean) = copy(isLoading = value)
24 | override fun setMessage(value: String?) = copy(message = value)
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/latest/composable/LatestListItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.latest.composable
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.github.yohannestz.satori.data.model.volume.Item
5 | import com.github.yohannestz.satori.ui.composables.BaseListItem
6 |
7 | @Composable
8 | fun LatestListItem(
9 | item: Item,
10 | onClick: (Item) -> Unit
11 | ) {
12 | BaseListItem(
13 | title = item.volumeInfo.title ?: "--",
14 | subtitle = item.volumeInfo.authors?.joinToString(", ") ?: "--",
15 | bottomText = item.volumeInfo.publishedDate,
16 | imageOverlayText = item.volumeInfo.averageRating?.let { it.toString() } ?: null,
17 | item.volumeInfo.imageLinks?.thumbnail?.replace("http://", "https://")
18 | ?: item.volumeInfo.imageLinks?.smallThumbnail?.replace("http://", "https://")
19 | ?: "",
20 | onClick = { onClick(item) }
21 | )
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.main
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.github.yohannestz.satori.data.repository.PreferencesRepository
6 | import com.github.yohannestz.satori.ui.base.ThemeStyle
7 | import kotlinx.coroutines.flow.SharingStarted
8 | import kotlinx.coroutines.flow.stateIn
9 | import kotlinx.coroutines.launch
10 |
11 | class MainViewModel(
12 | private val preferencesRepository: PreferencesRepository
13 | ) : ViewModel() {
14 | val startTab = preferencesRepository.startTab
15 | val lastTab = preferencesRepository.lastTab
16 |
17 | fun saveLastTab(value: Int) = viewModelScope.launch {
18 | preferencesRepository.setLastTab(value)
19 | }
20 |
21 | val theme = preferencesRepository.theme
22 | .stateIn(viewModelScope, SharingStarted.Eagerly, ThemeStyle.FOLLOW_SYSTEM)
23 |
24 | val useBlackColors = preferencesRepository.useBlackColors
25 | val useDynamicColors = preferencesRepository.useDynamicColors
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/main/composables/MainBottomNavBar.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.main.composables
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.core.Animatable
5 | import androidx.compose.animation.core.AnimationVector1D
6 | import androidx.compose.animation.slideInVertically
7 | import androidx.compose.animation.slideOutVertically
8 | import androidx.compose.material3.NavigationBar
9 | import androidx.compose.material3.NavigationBarItem
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.rememberCoroutineScope
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.navigation.NavBackStackEntry
15 | import androidx.navigation.NavController
16 | import androidx.navigation.NavDestination.Companion.hasRoute
17 | import androidx.navigation.NavDestination.Companion.hierarchy
18 | import androidx.navigation.NavGraph.Companion.findStartDestination
19 | import com.github.yohannestz.satori.ui.base.BottomDestination
20 | import com.github.yohannestz.satori.ui.base.BottomDestination.Companion.Icon
21 | import kotlinx.coroutines.launch
22 |
23 | @Composable
24 | fun MainBottomNavBar(
25 | navController: NavController,
26 | navBackStackEntry: NavBackStackEntry?,
27 | isVisible: Boolean,
28 | onItemSelected: (Int) -> Unit,
29 | topBarOffsetY: Animatable,
30 | ) {
31 | val scope = rememberCoroutineScope()
32 |
33 | AnimatedVisibility(
34 | visible = isVisible,
35 | enter = slideInVertically(initialOffsetY = { it }),
36 | exit = slideOutVertically(targetOffsetY = { it })
37 | ) {
38 | NavigationBar {
39 | BottomDestination.values.forEachIndexed { index, dest ->
40 | val isSelected = navBackStackEntry?.destination?.hierarchy?.any {
41 | it.hasRoute(dest.route::class)
42 | } == true
43 | NavigationBarItem(
44 | icon = { dest.Icon(selected = isSelected) },
45 | label = { Text(text = stringResource(dest.title)) },
46 | selected = isSelected,
47 | onClick = {
48 | if (!isSelected) {
49 | scope.launch {
50 | topBarOffsetY.animateTo(0f)
51 | }
52 |
53 | onItemSelected(index)
54 | navController.navigate(dest.route) {
55 | popUpTo(navController.graph.findStartDestination().id) {
56 | saveState = true
57 | }
58 | launchSingleTop = true
59 | restoreState = true
60 | }
61 | }
62 | }
63 | )
64 | }
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/main/composables/MainTopAppBar.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.main.composables
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.slideInVertically
5 | import androidx.compose.animation.slideOutVertically
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxHeight
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.statusBarsPadding
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material3.Card
15 | import androidx.compose.material3.CardDefaults
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.res.painterResource
23 | import androidx.compose.ui.res.stringResource
24 | import androidx.compose.ui.unit.dp
25 | import androidx.lifecycle.compose.dropUnlessResumed
26 | import androidx.navigation.NavController
27 | import com.github.yohannestz.satori.R
28 | import com.github.yohannestz.satori.ui.base.navigation.Route
29 |
30 | @Composable
31 | fun MainTopAppBar(
32 | isVisible: Boolean,
33 | navController: NavController,
34 | modifier: Modifier = Modifier
35 | ) {
36 | AnimatedVisibility(
37 | visible = isVisible,
38 | enter = slideInVertically(initialOffsetY = { -it }),
39 | exit = slideOutVertically(targetOffsetY = { -it })
40 | ) {
41 | Card(
42 | onClick = dropUnlessResumed {
43 | navController.navigate(Route.Search)
44 | },
45 | modifier = modifier
46 | .statusBarsPadding()
47 | .fillMaxWidth()
48 | .height(56.dp)
49 | .padding(start = 16.dp, end = 16.dp, bottom = 4.dp, top = 4.dp),
50 | shape = RoundedCornerShape(50),
51 | colors = CardDefaults.cardColors(
52 | containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
53 | )
54 | ) {
55 | Row(
56 | modifier = Modifier
57 | .padding(horizontal = 16.dp)
58 | .fillMaxHeight(),
59 | horizontalArrangement = Arrangement.spacedBy(16.dp),
60 | verticalAlignment = Alignment.CenterVertically
61 | ) {
62 | Icon(
63 | painter = painterResource(R.drawable.ic_round_search_24),
64 | contentDescription = "search",
65 | tint = MaterialTheme.colorScheme.onSurface
66 | )
67 | Text(
68 | text = stringResource(R.string.search),
69 | modifier = Modifier.weight(1f),
70 | color = MaterialTheme.colorScheme.onSurfaceVariant
71 | )
72 | }
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/more/MoreEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.more
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/more/MoreViewViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.more
2 |
3 | class MoreViewViewModel {
4 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/more/composable/MoreItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.more.composable
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.res.painterResource
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 |
22 | @Composable
23 | fun MoreItem(
24 | title: String,
25 | subtitle: String? = null,
26 | modifier: Modifier = Modifier,
27 | @DrawableRes icon: Int? = null,
28 | onClick: () -> Unit,
29 | ) {
30 | Row(
31 | modifier = modifier
32 | .fillMaxWidth()
33 | .clickable(onClick = onClick),
34 | horizontalArrangement = Arrangement.Start,
35 | verticalAlignment = Alignment.CenterVertically
36 | ) {
37 | if (icon != null) {
38 | Icon(
39 | painter = painterResource(icon),
40 | contentDescription = "",
41 | modifier = Modifier.padding(16.dp),
42 | tint = MaterialTheme.colorScheme.primary
43 | )
44 | } else {
45 | Spacer(
46 | modifier = Modifier
47 | .padding(16.dp)
48 | .size(24.dp)
49 | )
50 | }
51 |
52 | Column(
53 | modifier = if (subtitle != null)
54 | Modifier.padding(16.dp)
55 | else Modifier.padding(horizontal = 16.dp)
56 | ) {
57 | Text(
58 | text = title,
59 | color = MaterialTheme.colorScheme.onSurface
60 | )
61 |
62 | if (subtitle != null) {
63 | Text(
64 | text = subtitle,
65 | color = MaterialTheme.colorScheme.onSurfaceVariant,
66 | fontSize = 13.sp
67 | )
68 | }
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/more/composable/SendFeedbackDialog.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.more.composable
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Text
6 | import androidx.compose.material3.TextButton
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.platform.LocalContext
9 | import androidx.compose.ui.res.stringResource
10 | import com.github.yohannestz.satori.R
11 | import com.github.yohannestz.satori.utils.DEVELOPER_EMAIL_ADDRESS
12 | import com.github.yohannestz.satori.utils.Extensions.openAction
13 | import com.github.yohannestz.satori.utils.Extensions.openEmail
14 | import com.github.yohannestz.satori.utils.GITHUB_ISSUES_URL
15 | import com.github.yohannestz.satori.utils.TELEGRAM_CHANNEL
16 |
17 | @Composable
18 | fun SendFeedbackDialog(
19 | onDismiss: () -> Unit
20 | ) {
21 | val context = LocalContext.current
22 |
23 | AlertDialog(
24 | onDismissRequest = onDismiss,
25 | confirmButton = {
26 | TextButton(onClick = onDismiss) {
27 | Text(text = stringResource(R.string.cancel))
28 | }
29 | },
30 | text = {
31 | Column {
32 | MoreItem(
33 | title = stringResource(R.string.github_issues),
34 | icon = R.drawable.ic_round_bug_report_24,
35 | onClick = {
36 | context.openAction(GITHUB_ISSUES_URL)
37 | }
38 | )
39 |
40 | MoreItem(
41 | title = stringResource(R.string.telegram_channel),
42 | icon = R.drawable.ic_telegram_icon,
43 | onClick = {
44 | context.openAction(TELEGRAM_CHANNEL)
45 | }
46 | )
47 |
48 | MoreItem(
49 | title = stringResource(R.string.email),
50 | icon = R.drawable.ic_round_attach_email_24,
51 | onClick = {
52 | context.openEmail(
53 | recipient = DEVELOPER_EMAIL_ADDRESS,
54 | subject = "In regards to Satori,",
55 | body = ""
56 | )
57 | }
58 | )
59 | }
60 | }
61 | )
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/search/SearchEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.search
2 |
3 | import com.github.yohannestz.satori.data.model.volume.SearchHistory
4 | import com.github.yohannestz.satori.ui.base.event.PagedUiEvent
5 |
6 | interface SearchEvent: PagedUiEvent {
7 | fun search(query: String)
8 | fun onSaveSearchHistory(query: String)
9 | fun onRemoveSearch(item: SearchHistory)
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/search/SearchUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.search
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.mutableStateListOf
5 | import androidx.compose.runtime.snapshots.SnapshotStateList
6 | import com.github.yohannestz.satori.data.model.volume.Item
7 | import com.github.yohannestz.satori.data.model.volume.SearchHistory
8 | import com.github.yohannestz.satori.ui.base.state.PagedUiState
9 |
10 | @Stable
11 | data class SearchUiState(
12 | val query: String = "",
13 | val searchHistoryList: List = emptyList(),
14 | val itemList: SnapshotStateList
- = mutableStateListOf(),
15 | val performSearch: Boolean = false,
16 | val noResult: Boolean = false,
17 | override val nextPage: Int? = null,
18 | override val loadMore: Boolean = false,
19 | override val isLoading: Boolean = false,
20 | override val message: String? = null
21 | ) : PagedUiState() {
22 | override fun setLoading(value: Boolean) = copy(isLoading = value)
23 | override fun setMessage(value: String?) = copy(message = value)
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/search/composable/NoResultsText.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.search.composable
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.unit.dp
12 | import com.github.yohannestz.satori.R
13 |
14 | @Composable
15 | fun NoResultsText() {
16 | Column(
17 | modifier = Modifier.fillMaxWidth(),
18 | horizontalAlignment = Alignment.CenterHorizontally
19 | ) {
20 | Text(
21 | text = stringResource(R.string.no_results),
22 | modifier = Modifier.padding(16.dp)
23 | )
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/search/composable/SearchHistoryItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.search.composable
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.res.painterResource
17 | import androidx.compose.ui.unit.dp
18 | import com.github.yohannestz.satori.R
19 | import com.github.yohannestz.satori.data.model.volume.SearchHistory
20 |
21 | @Composable
22 | fun SearchHistoryItem(
23 | item: SearchHistory,
24 | onClick: () -> Unit,
25 | onDelete: (SearchHistory) -> Unit,
26 | modifier: Modifier = Modifier
27 | ) {
28 | Row(
29 | modifier = modifier
30 | .fillMaxWidth()
31 | .clickable(onClick = onClick)
32 | .padding(
33 | horizontal = 16.dp,
34 | ),
35 | verticalAlignment = Alignment.CenterVertically
36 | ) {
37 |
38 | Icon(
39 | painter = painterResource(id = R.drawable.ic_history_24),
40 | contentDescription = item.query,
41 | tint = MaterialTheme.colorScheme.onSurfaceVariant,
42 | )
43 | Spacer(modifier = Modifier.width(8.dp))
44 | Text(
45 | text = item.query,
46 | color = MaterialTheme.colorScheme.onSurfaceVariant,
47 | )
48 | Spacer(modifier = Modifier.weight(1f))
49 | IconButton(
50 | onClick = { onDelete(item) },
51 | modifier = Modifier.padding(end = 8.dp)
52 | ) {
53 | Icon(
54 | painter = painterResource(id = R.drawable.ic_round_close_24),
55 | contentDescription = item.query,
56 | tint = MaterialTheme.colorScheme.onSurfaceVariant,
57 | )
58 | }
59 |
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/search/composable/SearchListItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.search.composable
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.github.yohannestz.satori.data.model.volume.Item
5 | import com.github.yohannestz.satori.ui.composables.BaseListItem
6 |
7 | @Composable
8 | fun SearchListItem(
9 | item: Item,
10 | onClick: (Item) -> Unit
11 | ) {
12 | BaseListItem(
13 | title = item.volumeInfo.title ?: "--",
14 | subtitle = item.volumeInfo.authors?.joinToString(", ") ?: "--",
15 | bottomText = item.volumeInfo.publishedDate,
16 | imageOverlayText = item.volumeInfo.averageRating?.let { it.toString() } ?: null,
17 | item.volumeInfo.imageLinks?.thumbnail?.replace("http://", "https://")
18 | ?: item.volumeInfo.imageLinks?.smallThumbnail?.replace("http://", "https://")
19 | ?: "",
20 | onClick = { onClick(item) }
21 | )
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/settings/SettingsEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.settings
2 |
3 | import com.github.yohannestz.satori.data.model.PrintType
4 | import com.github.yohannestz.satori.data.model.ViewMode
5 | import com.github.yohannestz.satori.ui.base.StartTab
6 | import com.github.yohannestz.satori.ui.base.ThemeStyle
7 | import com.github.yohannestz.satori.ui.base.event.UiEvent
8 |
9 | interface SettingsEvent : UiEvent {
10 | fun onThemeChanged(theme: ThemeStyle)
11 | fun onUseBlackColors(value: Boolean)
12 | fun onUseDynamicColors(value: Boolean)
13 | fun onViewModeChanged(value: ViewMode)
14 | fun onStartTabChanged(value: StartTab?)
15 | fun onOnlyShowFreeContentChanged(value: Boolean)
16 | fun onDefaultPrintTypeChanged(value: PrintType)
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/settings/SettingsUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.settings
2 |
3 | import com.github.yohannestz.satori.data.model.PrintType
4 | import com.github.yohannestz.satori.data.model.ViewMode
5 | import com.github.yohannestz.satori.ui.base.StartTab
6 | import com.github.yohannestz.satori.ui.base.ThemeStyle
7 | import com.github.yohannestz.satori.ui.base.state.UiState
8 |
9 | data class SettingsUiState(
10 | val theme: ThemeStyle = ThemeStyle.FOLLOW_SYSTEM,
11 | val useBlackColors: Boolean = false,
12 | val useDynamicColors: Boolean = false,
13 | val viewMode: ViewMode = ViewMode.LIST,
14 | val startTab: StartTab? = StartTab.LAST_USED,
15 | val onlyShowFreeContent: Boolean = false,
16 | val defaultPrintType: PrintType = PrintType.ALL,
17 | override val isLoading: Boolean = false,
18 | override val message: String? = null
19 | ) : UiState() {
20 | override fun setLoading(value: Boolean) = copy(isLoading = value)
21 | override fun setMessage(value: String?) = copy(message = value)
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.settings
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.github.yohannestz.satori.data.model.PrintType
5 | import com.github.yohannestz.satori.data.model.ViewMode
6 | import com.github.yohannestz.satori.data.repository.PreferencesRepository
7 | import com.github.yohannestz.satori.ui.base.StartTab
8 | import com.github.yohannestz.satori.ui.base.ThemeStyle
9 | import com.github.yohannestz.satori.ui.base.viewmodel.BaseViewModel
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.launchIn
12 | import kotlinx.coroutines.flow.onEach
13 | import kotlinx.coroutines.flow.update
14 | import kotlinx.coroutines.launch
15 |
16 | class SettingsViewModel(
17 | private val defaultPreferenceRepository: PreferencesRepository
18 | ) : BaseViewModel(), SettingsEvent {
19 |
20 | override val mutableUiState: MutableStateFlow =
21 | MutableStateFlow(SettingsUiState())
22 |
23 | override fun onThemeChanged(theme: ThemeStyle) {
24 | viewModelScope.launch {
25 | defaultPreferenceRepository.setTheme(theme)
26 | }
27 | }
28 |
29 | override fun onUseBlackColors(value: Boolean) {
30 | viewModelScope.launch {
31 | defaultPreferenceRepository.setUseBlackColors(value)
32 | }
33 | }
34 |
35 | override fun onUseDynamicColors(value: Boolean) {
36 | viewModelScope.launch {
37 | defaultPreferenceRepository.setUseDynamicColors(value)
38 | }
39 | }
40 |
41 | override fun onViewModeChanged(value: ViewMode) {
42 | viewModelScope.launch {
43 | defaultPreferenceRepository.setVolumeListViewMode(value)
44 | }
45 | }
46 |
47 | override fun onStartTabChanged(value: StartTab?) {
48 | viewModelScope.launch {
49 | defaultPreferenceRepository.setStartTab(value)
50 | }
51 | }
52 |
53 | override fun onOnlyShowFreeContentChanged(value: Boolean) {
54 | viewModelScope.launch {
55 | defaultPreferenceRepository.setOnlyShowFreeContent(value)
56 | }
57 | }
58 |
59 | override fun onDefaultPrintTypeChanged(value: PrintType) {
60 | viewModelScope.launch {
61 | defaultPreferenceRepository.setDefaultPrintType(value)
62 | }
63 | }
64 |
65 | init {
66 | defaultPreferenceRepository.theme
67 | .onEach { value ->
68 | mutableUiState.update { it.copy(theme = value) }
69 | }
70 | .launchIn(viewModelScope)
71 |
72 | defaultPreferenceRepository.useBlackColors
73 | .onEach { value ->
74 | mutableUiState.update { it.copy(useBlackColors = value) }
75 | }
76 | .launchIn(viewModelScope)
77 |
78 | defaultPreferenceRepository.useDynamicColors
79 | .onEach { value ->
80 | mutableUiState.update { it.copy(useDynamicColors = value) }
81 | }
82 | .launchIn(viewModelScope)
83 |
84 | defaultPreferenceRepository.volumeListViewMode
85 | .onEach { value ->
86 | mutableUiState.update { it.copy(viewMode = value) }
87 | }
88 | .launchIn(viewModelScope)
89 |
90 | defaultPreferenceRepository.startTab
91 | .onEach { value ->
92 | value.let { mutableUiState.update { it.copy(startTab = value) } }
93 | }
94 | .launchIn(viewModelScope)
95 |
96 | defaultPreferenceRepository.onlyShowFreeContent
97 | .onEach { value ->
98 | mutableUiState.update { it.copy(onlyShowFreeContent = value) }
99 | }
100 | .launchIn(viewModelScope)
101 |
102 | defaultPreferenceRepository.defaultPrintType
103 | .onEach { value ->
104 | mutableUiState.update { it.copy(defaultPrintType = value) }
105 | }
106 | .launchIn(viewModelScope)
107 | }
108 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/settings/composables/SettingsTitle.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.settings.composables
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.unit.dp
10 | import androidx.compose.ui.unit.sp
11 |
12 | @Composable
13 | fun SettingsTitle(text: String) {
14 | Text(
15 | text = text,
16 | modifier = Modifier
17 | .padding(start = 72.dp, top = 16.dp, end = 16.dp, bottom = 8.dp),
18 | color = MaterialTheme.colorScheme.secondary,
19 | fontSize = 13.sp,
20 | fontWeight = FontWeight.SemiBold
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val light_scrim = Color(0xE6FFFFFF)
6 | val dark_scrim = Color(0x801b1b1b)
7 | val banner_shadow_color = Color(0x55000000)
8 |
9 | val md_theme_light_primary = Color(0xFF4858AB)
10 | val md_theme_light_onPrimary = Color(0xFFFFFFFF)
11 | val md_theme_light_primaryContainer = Color(0xFFDEE0FF)
12 | val md_theme_light_onPrimaryContainer = Color(0xFF00105B)
13 | val md_theme_light_secondary = Color(0xFF4B57A9)
14 | val md_theme_light_onSecondary = Color(0xFFFFFFFF)
15 | val md_theme_light_secondaryContainer = Color(0xFFDFE0FF)
16 | val md_theme_light_onSecondaryContainer = Color(0xFF000D5F)
17 | val md_theme_light_tertiary = Color(0xFF4858AB)
18 | val md_theme_light_onTertiary = Color(0xFFFFFFFF)
19 | val md_theme_light_tertiaryContainer = Color(0xFFDEE1FF)
20 | val md_theme_light_onTertiaryContainer = Color(0xFF001159)
21 | val md_theme_light_error = Color(0xFFBA1A1A)
22 | val md_theme_light_errorContainer = Color(0xFFFFDAD6)
23 | val md_theme_light_onError = Color(0xFFFFFFFF)
24 | val md_theme_light_onErrorContainer = Color(0xFF410002)
25 | val md_theme_light_background = Color(0xFFFEFBFF)
26 | val md_theme_light_onBackground = Color(0xFF1B1B1F)
27 | val md_theme_light_surface = Color(0xFFFEFBFF)
28 | val md_theme_light_onSurface = Color(0xFF1B1B1F)
29 | val md_theme_light_surfaceVariant = Color(0xFFE3E1EC)
30 | val md_theme_light_onSurfaceVariant = Color(0xFF46464F)
31 | val md_theme_light_outline = Color(0xFF767680)
32 | val md_theme_light_inverseOnSurface = Color(0xFFF3F0F4)
33 | val md_theme_light_inverseSurface = Color(0xFF303034)
34 | val md_theme_light_inversePrimary = Color(0xFFBAC3FF)
35 | val md_theme_light_shadow = Color(0xFF000000)
36 | val md_theme_light_surfaceTint = Color(0xFF4858AB)
37 | val md_theme_light_outlineVariant = Color(0xFFC6C5D0)
38 | val md_theme_light_scrim = Color(0xFF000000)
39 |
40 | val md_theme_dark_primary = Color(0xFFBAC3FF)
41 | val md_theme_dark_onPrimary = Color(0xFF15267B)
42 | val md_theme_dark_primaryContainer = Color(0xFF2F3F92)
43 | val md_theme_dark_onPrimaryContainer = Color(0xFFDEE0FF)
44 | val md_theme_dark_secondary = Color(0xFFBBC3FF)
45 | val md_theme_dark_onSecondary = Color(0xFF192678)
46 | val md_theme_dark_secondaryContainer = Color(0xFF323F90)
47 | val md_theme_dark_onSecondaryContainer = Color(0xFFDFE0FF)
48 | val md_theme_dark_tertiary = Color(0xFFBAC3FF)
49 | val md_theme_dark_onTertiary = Color(0xFF14277A)
50 | val md_theme_dark_tertiaryContainer = Color(0xFF2F3F91)
51 | val md_theme_dark_onTertiaryContainer = Color(0xFFDEE1FF)
52 | val md_theme_dark_error = Color(0xFFFFB4AB)
53 | val md_theme_dark_errorContainer = Color(0xFF93000A)
54 | val md_theme_dark_onError = Color(0xFF690005)
55 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
56 | val md_theme_dark_background = Color(0xFF1B1B1F)
57 | val md_theme_dark_onBackground = Color(0xFFE4E1E6)
58 | val md_theme_dark_surface = Color(0xFF1B1B1F)
59 | val md_theme_dark_onSurface = Color(0xFFE4E1E6)
60 | val md_theme_dark_surfaceVariant = Color(0xFF46464F)
61 | val md_theme_dark_onSurfaceVariant = Color(0xFFC6C5D0)
62 | val md_theme_dark_outline = Color(0xFF90909A)
63 | val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F)
64 | val md_theme_dark_inverseSurface = Color(0xFFE4E1E6)
65 | val md_theme_dark_inversePrimary = Color(0xFF4858AB)
66 | val md_theme_dark_surfaceTint = Color(0xFFBAC3FF)
67 | val md_theme_dark_outlineVariant = Color(0xFF46464F)
68 | val md_theme_dark_scrim = Color(0xFF000000)
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/volumelist/VolumeListEvent.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.volumelist
2 |
3 | import com.github.yohannestz.satori.data.model.ViewMode
4 | import com.github.yohannestz.satori.ui.base.event.PagedUiEvent
5 |
6 | interface VolumeListEvent: PagedUiEvent {
7 | fun onViewModeChanged(viewMode: ViewMode)
8 | fun refreshList()
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/volumelist/VolumeListUiState.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.volumelist
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.mutableStateListOf
5 | import androidx.compose.runtime.snapshots.SnapshotStateList
6 | import com.github.yohannestz.satori.data.model.ViewMode
7 | import com.github.yohannestz.satori.data.model.VolumeCategory
8 | import com.github.yohannestz.satori.data.model.volume.Item
9 | import com.github.yohannestz.satori.ui.base.state.PagedUiState
10 |
11 | @Stable
12 | data class VolumeListUiState(
13 | val categoryType: VolumeCategory? = null,
14 | val viewMode: ViewMode = ViewMode.GRID,
15 | val itemList: SnapshotStateList
- = mutableStateListOf(),
16 | val isLoadingMore: Boolean = false,
17 | val selectedItem: Item? = null,
18 | val totalItems: Int = 0,
19 | override val nextPage: Int? = null,
20 | override val loadMore: Boolean = true,
21 | override val isLoading: Boolean = true,
22 | override val message: String? = null
23 | ) : PagedUiState() {
24 | override fun setLoading(value: Boolean) = copy(isLoading = value)
25 | override fun setMessage(value: String?) = copy(message = value)
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/ui/volumelist/composables/VolumeListItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.ui.volumelist.composables
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.github.yohannestz.satori.data.model.volume.Item
5 | import com.github.yohannestz.satori.ui.composables.BaseListItem
6 |
7 | @Composable
8 | fun VolumeListItem(
9 | item: Item,
10 | onClick: (Item) -> Unit
11 | ) {
12 | BaseListItem(
13 | title = item.volumeInfo.title ?: "--",
14 | subtitle = item.volumeInfo.authors?.joinToString(", ") ?: "--",
15 | bottomText = item.volumeInfo.publishedDate,
16 | imageOverlayText = item.volumeInfo.averageRating?.let { it.toString() } ?: null,
17 | item.volumeInfo.imageLinks?.thumbnail?.replace("http://", "https://")
18 | ?: item.volumeInfo.imageLinks?.smallThumbnail?.replace("http://", "https://")
19 | ?: "",
20 | onClick = { onClick(item) }
21 | )
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/yohannestz/satori/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori.utils
2 |
3 | const val GOOGLE_API_BASE_URL = "https://www.googleapis.com/"
4 | const val GOOGLE_BOOKS_URL = "https://books.google.com/"
5 | const val UNKNOWN_CHAR = "─"
6 |
7 | const val GITHUB_REPOSITORY_URL = "https://github.com/yohannestz/Satori"
8 | const val GITHUB_RELEASES_URL = "https://github.com/YohannesTz/Satori/releases"
9 | const val GITHUB_ISSUES_URL = "https://github.com/YohannesTz/Satori/issues"
10 | const val GITHUB_SLUG = "yohannestz/Satori"
11 | const val DEVELOPER_EMAIL_ADDRESS = "yohannes22ethiopia@gmail.com"
12 | const val TELEGRAM_CHANNEL = "https://t.me/brojects_and_resources"
13 |
14 | const val VOLUME_CATEGORY = "volumeCategory"
15 | const val VOLUME_LIST = "volumeList"
16 |
17 | const val VOLUME_DETAIL = "volumeDetail"
18 | const val MEDIA_DETAIL_ID = "id"
19 |
20 | const val DEFAULT_GRID_SPAN_COUNT = 2
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_telegram_icon.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_telegram_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/drawable-hdpi/ic_telegram_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_telegram_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/drawable-mdpi/ic_telegram_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_telegram_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/drawable-xhdpi/ic_telegram_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_telegram_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/drawable-xxhdpi/ic_telegram_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_book_spark_4.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_content_copy_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_expand_less_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_expand_more_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_github_icon.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_history_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_info_sided.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_link_outward_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_open_in_browser.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_outline_collections_bookmark_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_outline_fire_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_outline_home_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_arrow_forward_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_attach_email_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_bug_report_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_campaign_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_close_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_cloud_download_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_collections_bookmark_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_color_lens_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_delete_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_feedback_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_fire_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_format_list_bulleted_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_grid_view_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_home_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_info_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_more_horiz_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_power_settings_new_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_search_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_settings_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_share_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_star_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_round_view_list_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_satori_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_satori_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
14 |
18 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_star_filled_20.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_satori_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_satori_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_satori_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-hdpi/ic_satori_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_satori_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-hdpi/ic_satori_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_satori_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-mdpi/ic_satori_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_satori_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-mdpi/ic_satori_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_satori_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-xhdpi/ic_satori_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_satori_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-xhdpi/ic_satori_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_satori_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-xxhdpi/ic_satori_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_satori_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-xxhdpi/ic_satori_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_satori_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-xxxhdpi/ic_satori_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_satori_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/app/src/main/res/mipmap-xxxhdpi/ic_satori_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #03A9F4
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_satori_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #4858AB
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/github/yohannestz/satori/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.yohannestz.satori
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.jetbrains.kotlin.android) apply false
5 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.defaults.buildfeatures.buildconfig=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Sep 28 17:58:48 EAT 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/screenshots/1.png
--------------------------------------------------------------------------------
/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/screenshots/2.png
--------------------------------------------------------------------------------
/screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/screenshots/3.png
--------------------------------------------------------------------------------
/screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/screenshots/4.png
--------------------------------------------------------------------------------
/screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/screenshots/5.png
--------------------------------------------------------------------------------
/screenshots/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YohannesTz/Satori/cc0197102c21c8a7d12597d7073659152c34ed85/screenshots/banner.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Satori"
23 | include(":app")
24 |
--------------------------------------------------------------------------------