├── .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 | Satori banner 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 | screenshot 16 | screenshot 17 | screenshot 18 | screenshot 19 | screenshot 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 |