├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── nsh07 │ │ └── wikireader │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── org │ │ │ └── nsh07 │ │ │ └── wikireader │ │ │ ├── MainActivity.kt │ │ │ ├── WikiReaderApplication.kt │ │ │ ├── data │ │ │ ├── AppContainer.kt │ │ │ ├── FeedApiResponse.kt │ │ │ ├── LanguageData.kt │ │ │ ├── PreferencesRepository.kt │ │ │ ├── WRStatus.kt │ │ │ ├── WikiApiPageData.kt │ │ │ ├── WikiApiSearchResults.kt │ │ │ ├── WikipediaRepository.kt │ │ │ └── misc.kt │ │ │ ├── network │ │ │ ├── HostSelectionInterceptor.java │ │ │ ├── NetworkException.kt │ │ │ └── WikipediaApiService.kt │ │ │ ├── parser │ │ │ ├── cleanUpWikitext.kt │ │ │ ├── parseWikitable.kt │ │ │ └── wikitextToAnnotatedString.kt │ │ │ └── ui │ │ │ ├── AppNavigationDrawer.kt │ │ │ ├── AppScreen.kt │ │ │ ├── aboutScreen │ │ │ ├── AboutScreen.kt │ │ │ └── AboutTopAppBar.kt │ │ │ ├── homeScreen │ │ │ ├── AppFab.kt │ │ │ ├── AppHomeScreen.kt │ │ │ ├── AppSearchBar.kt │ │ │ ├── ArticleFeed.kt │ │ │ ├── ArticleLanguageBottomSheet.kt │ │ │ ├── ArticleViewsGraph.kt │ │ │ ├── AsyncWikitable.kt │ │ │ ├── EquationImage.kt │ │ │ ├── ExpandableSection.kt │ │ │ ├── Gallery.kt │ │ │ ├── ImageWithCaption.kt │ │ │ └── ParsedBodyText.kt │ │ │ ├── image │ │ │ ├── FeedImage.kt │ │ │ ├── FullScreenImage.kt │ │ │ ├── FullScreenImageTopBar.kt │ │ │ ├── ImageCard.kt │ │ │ └── PageImage.kt │ │ │ ├── savedArticlesScreen │ │ │ ├── DeleteArticleDialog.kt │ │ │ ├── LanguageFilterOption.kt │ │ │ ├── SavedArticlesScreen.kt │ │ │ └── SavedArticlesTopBar.kt │ │ │ ├── settingsScreen │ │ │ ├── ColorSchemePickerDialog.kt │ │ │ ├── LanguageBottomSheet.kt │ │ │ ├── LanguageSearchBar.kt │ │ │ ├── ResetSettingsDialog.kt │ │ │ ├── SettingsScreen.kt │ │ │ ├── SettingsTopBar.kt │ │ │ └── ThemeDialog.kt │ │ │ ├── shimmer │ │ │ ├── AnimatedShimmer.kt │ │ │ └── FeedLoader.kt │ │ │ ├── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ │ └── viewModel │ │ │ ├── UiState.kt │ │ │ └── UiViewModel.kt │ └── res │ │ ├── drawable-night-v34 │ │ └── splash_logo.xml │ │ ├── drawable-night │ │ └── splash_logo.xml │ │ ├── drawable-v34 │ │ └── splash_logo.xml │ │ ├── drawable │ │ ├── brightness_auto.xml │ │ ├── code.xml │ │ ├── colors.xml │ │ ├── contrast.xml │ │ ├── dark_mode.xml │ │ ├── data_saver_on.xml │ │ ├── delete.xml │ │ ├── download.xml │ │ ├── download_done.xml │ │ ├── error.xml │ │ ├── expand_all.xml │ │ ├── feed.xml │ │ ├── filled_download_done.xml │ │ ├── filled_home.xml │ │ ├── filled_info.xml │ │ ├── filled_settings.xml │ │ ├── format_size.xml │ │ ├── function.xml │ │ ├── gavel.xml │ │ ├── github.xml │ │ ├── heart.xml │ │ ├── history.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ ├── light_mode.xml │ │ ├── menu.xml │ │ ├── menu_open.xml │ │ ├── more_horiz.xml │ │ ├── north_west.xml │ │ ├── open_in_browser.xml │ │ ├── open_in_full.xml │ │ ├── outline_home.xml │ │ ├── outline_info.xml │ │ ├── outline_settings.xml │ │ ├── palette.xml │ │ ├── reset_settings.xml │ │ ├── save.xml │ │ ├── serif.xml │ │ ├── share.xml │ │ ├── share_filled.xml │ │ ├── shuffle.xml │ │ ├── splash_logo.xml │ │ ├── texture.xml │ │ ├── translate.xml │ │ ├── update.xml │ │ ├── upward.xml │ │ ├── wikimedia_logo_black.xml │ │ └── wikipedia_s_w.xml │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── resources.properties │ │ ├── values-ar │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-et │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-hi │ │ └── strings.xml │ │ ├── values-hu │ │ └── strings.xml │ │ ├── values-iw │ │ └── strings.xml │ │ ├── values-night-v34 │ │ └── splash.xml │ │ ├── values-night │ │ ├── splash.xml │ │ └── themes.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-uk │ │ └── strings.xml │ │ ├── values-v34 │ │ └── splash.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── splash.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── org │ └── nsh07 │ └── wikireader │ └── MiscKtTest.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── ar │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── de-DE │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── en-US │ ├── changelogs │ │ ├── 12.txt │ │ ├── 13.txt │ │ ├── 14.txt │ │ ├── 15.txt │ │ ├── 16.txt │ │ ├── 17.txt │ │ ├── 18.txt │ │ ├── 19.txt │ │ ├── 20.txt │ │ ├── 21.txt │ │ ├── 22.txt │ │ ├── 23.txt │ │ ├── 24.txt │ │ ├── 27.txt │ │ ├── 32.txt │ │ ├── 33.txt │ │ ├── 34.txt │ │ ├── 37.txt │ │ ├── 38.txt │ │ └── 39.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ └── 8.png │ ├── short_description.txt │ └── title.txt │ ├── es-ES │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── et │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── fr-FR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── hi-IN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── iw-IL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pt-BR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ru-RU │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── uk │ ├── changelogs │ │ ├── 12.txt │ │ ├── 13.txt │ │ ├── 14.txt │ │ ├── 15.txt │ │ ├── 16.txt │ │ ├── 17.txt │ │ ├── 18.txt │ │ ├── 19.txt │ │ ├── 20.txt │ │ ├── 21.txt │ │ ├── 22.txt │ │ ├── 23.txt │ │ ├── 24.txt │ │ ├── 27.txt │ │ ├── 32.txt │ │ ├── 33.txt │ │ ├── 34.txt │ │ └── 37.txt │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── zh-CN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ └── zh-TW │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── renovate.json ├── settings.gradle.kts └── weblate └── screenshots ├── about.jpg ├── fab_menu.jpg ├── feed_1.jpg ├── feed_2.jpg ├── feed_3.jpg ├── image_top_bar.jpg ├── language_bottom_sheet.jpg ├── navigation_drawer.jpg ├── reset_settings_dialog.jpg ├── saved_articles.jpg ├── saved_articles_delete_all_dialog.jpg ├── saved_articles_delete_dialog.jpg ├── search_delete_all_dialog.jpg ├── search_delete_dialog.jpg ├── search_history.jpg ├── search_suggestions.jpg ├── settings_bottom.jpg └── settings_top.jpg /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: nsh07 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve this app 4 | title: "[BUG] Enter your title here" 5 | labels: bug 6 | assignees: nsh07 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ### To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ### Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ### Screenshots 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ### Additional context 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this app 4 | title: "[FEATURE] Enter your title here" 5 | labels: enhancement 6 | assignees: nsh07 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ### Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ### Additional context 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.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 | /.kotlin 17 | api-reference.json 18 | .idea -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | release -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | alias(libs.plugins.jetbrains.compose.compiler) 5 | alias(libs.plugins.jetbrains.kotlin.serialization) 6 | } 7 | 8 | android { 9 | namespace = "org.nsh07.wikireader" 10 | compileSdk = 36 11 | 12 | defaultConfig { 13 | applicationId = "org.nsh07.wikireader" 14 | minSdk = 26 15 | targetSdk = 36 16 | versionCode = 39 17 | versionName = "2.2.1" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | androidResources { 24 | generateLocaleConfig = true 25 | } 26 | } 27 | 28 | buildTypes { 29 | release { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | signingConfig = signingConfigs.getByName("debug") 36 | } 37 | } 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_17 40 | targetCompatibility = JavaVersion.VERSION_17 41 | } 42 | kotlinOptions { 43 | jvmTarget = "17" 44 | } 45 | buildFeatures { 46 | compose = true 47 | buildConfig = true 48 | } 49 | packaging { 50 | resources { 51 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 52 | } 53 | } 54 | dependenciesInfo { 55 | includeInApk = false 56 | includeInBundle = false 57 | } 58 | } 59 | 60 | dependencies { 61 | implementation(platform(libs.androidx.compose.bom)) 62 | implementation(libs.androidx.activity.compose) 63 | implementation(libs.androidx.adaptive) 64 | implementation(libs.androidx.core.ktx) 65 | implementation(libs.androidx.material.icons.core) 66 | implementation(libs.androidx.core.splashscreen) 67 | implementation(libs.androidx.lifecycle.runtime.ktx) 68 | implementation(libs.androidx.lifecycle.viewmodel.compose) 69 | implementation(libs.androidx.material3) 70 | implementation(libs.androidx.navigation.compose) 71 | implementation(libs.androidx.ui) 72 | implementation(libs.androidx.ui.graphics) 73 | implementation(libs.androidx.ui.tooling.preview) 74 | 75 | implementation(libs.androidx.datastore.preferences) 76 | implementation(libs.coil3.coil.gif) 77 | implementation(libs.coil3.coil.svg) 78 | implementation(libs.coil3.compose) 79 | implementation(libs.coil3.network.okhttp) 80 | implementation(libs.ehsannarmani.compose.charts) 81 | implementation(libs.kotlinx.serialization.json) 82 | implementation(libs.material.kolor) 83 | implementation(libs.okhttp) 84 | implementation(libs.retrofit2.converter.scalars) 85 | implementation(libs.retrofit2.kotlinx.serialization.converter) 86 | implementation(libs.retrofit2.retrofit) 87 | implementation(libs.latex2unicode.x.x3) 88 | 89 | testImplementation(libs.junit) 90 | androidTestImplementation(libs.androidx.junit) 91 | androidTestImplementation(libs.androidx.espresso.core) 92 | androidTestImplementation(platform(libs.androidx.compose.bom)) 93 | androidTestImplementation(libs.androidx.ui.test.junit4) 94 | debugImplementation(libs.androidx.ui.tooling) 95 | debugImplementation(libs.androidx.ui.test.manifest) 96 | } -------------------------------------------------------------------------------- /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 org 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/org/nsh07/wikireader/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader 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("org.nsh07.wikireader", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsh07/WikiReader/a53cad35470eb6f319467d7b8929ade87a870341/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.activity.viewModels 8 | import androidx.compose.foundation.isSystemInDarkTheme 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.material3.MaterialTheme.colorScheme 11 | import androidx.compose.material3.MaterialTheme.typography 12 | import androidx.compose.runtime.CompositionLocalProvider 13 | import androidx.compose.runtime.collectAsState 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalLayoutDirection 17 | import androidx.compose.ui.unit.LayoutDirection 18 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 19 | import org.nsh07.wikireader.data.isRtl 20 | import org.nsh07.wikireader.data.toColor 21 | import org.nsh07.wikireader.ui.AppScreen 22 | import org.nsh07.wikireader.ui.theme.WikiReaderTheme 23 | import org.nsh07.wikireader.ui.viewModel.UiViewModel 24 | 25 | class MainActivity : ComponentActivity() { 26 | 27 | val viewModel: UiViewModel by viewModels(factoryProducer = { UiViewModel.Factory }) 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | viewModel.startAnimDuration() 32 | installSplashScreen().setKeepOnScreenCondition { 33 | !viewModel.isReady || !viewModel.isAnimDurationComplete 34 | } 35 | viewModel.setFilesDir(filesDir.path) 36 | viewModel.migrateArticles() 37 | enableEdgeToEdge() 38 | 39 | setContent { 40 | val preferencesState by viewModel.preferencesState.collectAsState() 41 | 42 | val darkTheme = when (preferencesState.theme) { 43 | "dark" -> true 44 | "light" -> false 45 | else -> isSystemInDarkTheme() 46 | } 47 | 48 | val seed = preferencesState.colorScheme.toColor() 49 | 50 | WikiReaderTheme( 51 | darkTheme = darkTheme, 52 | seedColor = seed, 53 | blackTheme = preferencesState.blackTheme 54 | ) { 55 | viewModel.setCompositionLocals( 56 | cs = colorScheme, 57 | tg = typography 58 | ) 59 | CompositionLocalProvider( 60 | LocalLayoutDirection provides 61 | if (isRtl(preferencesState.lang)) LayoutDirection.Rtl 62 | else LayoutDirection.Ltr 63 | ) { 64 | AppScreen( 65 | viewModel = viewModel, 66 | preferencesState = preferencesState, 67 | modifier = Modifier.fillMaxSize() 68 | ) 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/WikiReaderApplication.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader 2 | 3 | import android.app.Application 4 | import org.nsh07.wikireader.data.AppContainer 5 | import org.nsh07.wikireader.data.DefaultAppContainer 6 | 7 | class WikiReaderApplication : Application() { 8 | lateinit var container: AppContainer 9 | override fun onCreate() { 10 | super.onCreate() 11 | container = DefaultAppContainer(this) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/data/AppContainer.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.data 2 | 3 | import android.content.Context 4 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.serialization.json.Json 7 | import okhttp3.MediaType.Companion.toMediaType 8 | import okhttp3.OkHttpClient 9 | import org.nsh07.wikireader.network.HostSelectionInterceptor 10 | import org.nsh07.wikireader.network.WikipediaApiService 11 | import retrofit2.Retrofit 12 | import retrofit2.converter.scalars.ScalarsConverterFactory 13 | 14 | interface AppContainer { 15 | val interceptor: HostSelectionInterceptor 16 | val wikipediaRepository: WikipediaRepository 17 | val appPreferencesRepository: AppPreferencesRepository 18 | } 19 | 20 | class DefaultAppContainer(context: Context) : AppContainer { 21 | private val baseUrl = "https://en.wikipedia.org" 22 | private val json = Json { ignoreUnknownKeys = true } 23 | 24 | override val interceptor = HostSelectionInterceptor() 25 | 26 | private val okHttpClient by lazy { 27 | OkHttpClient.Builder() 28 | .addInterceptor(interceptor) 29 | .build() 30 | } 31 | 32 | private val wikipediaRetrofit = Retrofit.Builder() 33 | .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) 34 | .baseUrl(baseUrl) 35 | .client(okHttpClient) 36 | .build() 37 | 38 | private val wikipediaPageRetrofit = Retrofit.Builder() 39 | .addConverterFactory(ScalarsConverterFactory.create()) 40 | .baseUrl(baseUrl) 41 | .client(okHttpClient) 42 | .build() 43 | 44 | private val wikipediaRetrofitService: WikipediaApiService by lazy { 45 | wikipediaRetrofit.create(WikipediaApiService::class.java) 46 | } 47 | 48 | private val wikipediaPageRetrofitService: WikipediaApiService by lazy { 49 | wikipediaPageRetrofit.create(WikipediaApiService::class.java) 50 | } 51 | 52 | override val wikipediaRepository: WikipediaRepository by lazy { 53 | NetworkWikipediaRepository( 54 | wikipediaRetrofitService, 55 | wikipediaPageRetrofitService, 56 | Dispatchers.IO 57 | ) 58 | } 59 | 60 | override val appPreferencesRepository: AppPreferencesRepository by lazy { 61 | AppPreferencesRepository(context, Dispatchers.IO) 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/data/FeedApiResponse.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class FeedApiResponse( 8 | val tfa: FeedApiTFA? = null, 9 | @SerialName("mostread") val mostRead: FeedApiMostRead? = null, 10 | val image: FeedApiImage? = null, 11 | val news: List? = null, 12 | @SerialName("onthisday") val onThisDay: List? = null, 13 | ) 14 | 15 | @Serializable 16 | data class FeedApiTFA( 17 | val titles: Titles? = null, 18 | val thumbnail: Image? = null, 19 | @SerialName("originalimage") val originalImage: Image? = null, 20 | val lang: String? = null, 21 | val description: String? = null, 22 | val extract: String? = null, 23 | val timestamp: String? = null 24 | ) 25 | 26 | @Serializable 27 | data class FeedApiMostRead( 28 | val date: String? = null, 29 | val articles: List? = null 30 | ) 31 | 32 | @Serializable 33 | data class FeedApiImage( 34 | val title: String? = null, 35 | val thumbnail: Image? = null, 36 | val image: Image? = null, 37 | val artist: Artist? = null, 38 | val credit: Credit? = null, 39 | val description: Description? = null, 40 | @SerialName("file_page") val filePage: String? = null 41 | ) 42 | 43 | @Serializable 44 | data class FeedApiNews( 45 | val links: List
? = null, 46 | val story: String? = null 47 | ) 48 | 49 | @Serializable 50 | data class FeedApiOTD( 51 | val text: String? = null, 52 | val pages: List
? = null, 53 | val year: Int? = null 54 | ) 55 | 56 | @Serializable 57 | data class Titles( 58 | val canonical: String? = null, 59 | val normalized: String? = null 60 | ) 61 | 62 | @Serializable 63 | data class Image( 64 | val source: String? = null, 65 | val width: Int? = null, 66 | val height: Int? = null 67 | ) 68 | 69 | @Serializable 70 | data class MostReadArticle( 71 | val views: Int? = null, 72 | val rank: Int? = null, 73 | @SerialName("view_history") val viewHistory: List? = null, 74 | val titles: Titles? = null, 75 | val thumbnail: Image? = null, 76 | @SerialName("originalimage") val originalImage: Image? = null, 77 | val lang: String? = null, 78 | val description: String? = null, 79 | val extract: String? = null 80 | ) 81 | 82 | @Serializable 83 | data class Article( 84 | val titles: Titles? = null, 85 | val thumbnail: Image? = null, 86 | @SerialName("originalimage") val originalImage: Image? = null, 87 | val lang: String? = null, 88 | val description: String? = null, 89 | val extract: String? = null 90 | ) 91 | 92 | @Serializable 93 | data class ViewHistory( 94 | val date: String? = null, 95 | val views: Int? = null 96 | ) 97 | 98 | @Serializable 99 | data class Artist( 100 | val text: String? = null, 101 | val name: String? = null 102 | ) 103 | 104 | @Serializable 105 | data class Credit( 106 | val text: String? = null 107 | ) 108 | 109 | @Serializable 110 | data class Description( 111 | val text: String? = null, 112 | val lang: String? = null 113 | ) -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/data/PreferencesRepository.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.data 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.intPreferencesKey 9 | import androidx.datastore.preferences.core.stringPreferencesKey 10 | import androidx.datastore.preferences.core.stringSetPreferencesKey 11 | import androidx.datastore.preferences.preferencesDataStore 12 | import kotlinx.coroutines.CoroutineDispatcher 13 | import kotlinx.coroutines.flow.first 14 | import kotlinx.coroutines.withContext 15 | 16 | interface PreferencesRepository { 17 | suspend fun saveStringPreference(key: String, value: String): String 18 | suspend fun saveIntPreference(key: String, value: Int): Int 19 | suspend fun saveBooleanPreference(key: String, value: Boolean): Boolean 20 | suspend fun saveHistory(history: Set) 21 | 22 | suspend fun readStringPreference(key: String): String? 23 | suspend fun readIntPreference(key: String): Int? 24 | suspend fun readBooleanPreference(key: String): Boolean? 25 | suspend fun readHistory(): Set? 26 | 27 | suspend fun resetSettings() 28 | } 29 | 30 | class AppPreferencesRepository( 31 | private val context: Context, 32 | private val ioDispatcher: CoroutineDispatcher 33 | ) : PreferencesRepository { 34 | private val Context.dataStore: DataStore by preferencesDataStore(name = "preferences") 35 | 36 | /** 37 | * Saves a preference key-value pair into the app's [DataStore] 38 | * 39 | * @param key The key of the key-value pair 40 | * @param value The value of the key-value pair 41 | * 42 | * @return a [String] with the same value as [value] 43 | */ 44 | override suspend fun saveStringPreference(key: String, value: String): String = 45 | withContext(ioDispatcher) { 46 | val dataStoreKey = stringPreferencesKey(key) 47 | context.dataStore.edit { preferences -> 48 | preferences[dataStoreKey] = value 49 | } 50 | value 51 | } 52 | 53 | override suspend fun saveIntPreference(key: String, value: Int): Int = 54 | withContext(ioDispatcher) { 55 | val dataStoreKey = intPreferencesKey(key) 56 | context.dataStore.edit { preferences -> 57 | preferences[dataStoreKey] = value 58 | } 59 | value 60 | } 61 | 62 | override suspend fun saveBooleanPreference(key: String, value: Boolean): Boolean = 63 | withContext(ioDispatcher) { 64 | val dataStoreKey = booleanPreferencesKey(key) 65 | context.dataStore.edit { preferences -> 66 | preferences[dataStoreKey] = value 67 | } 68 | value 69 | } 70 | 71 | override suspend fun saveHistory(history: Set) = 72 | withContext(ioDispatcher) { 73 | val dataStoreKey = stringSetPreferencesKey("history") 74 | context.dataStore.edit { preferences -> 75 | preferences[dataStoreKey] = history 76 | } 77 | Unit 78 | } 79 | 80 | /** 81 | * Reads the preference value for a given key in the app's [DataStore] 82 | * 83 | * @param key The key of the required associated value 84 | * 85 | * @return a [String] with the value corresponding to the [key] 86 | */ 87 | override suspend fun readStringPreference(key: String): String? = 88 | withContext(ioDispatcher) { 89 | val dataStoreKey = stringPreferencesKey(key) 90 | context.dataStore.data.first()[dataStoreKey] 91 | } 92 | 93 | override suspend fun readIntPreference(key: String): Int? = 94 | withContext(ioDispatcher) { 95 | val dataStoreKey = intPreferencesKey(key) 96 | context.dataStore.data.first()[dataStoreKey] 97 | } 98 | 99 | override suspend fun readBooleanPreference(key: String): Boolean? = 100 | withContext(ioDispatcher) { 101 | val dataStoreKey = booleanPreferencesKey(key) 102 | context.dataStore.data.first()[dataStoreKey] 103 | } 104 | 105 | override suspend fun readHistory(): Set? = 106 | withContext(ioDispatcher) { 107 | val dataStoreKey = stringSetPreferencesKey("history") 108 | context.dataStore.data.first()[dataStoreKey] 109 | } 110 | 111 | override suspend fun resetSettings() = 112 | withContext(ioDispatcher) { 113 | context.dataStore.edit { preferences -> 114 | preferences.clear() 115 | } 116 | Unit 117 | } 118 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/data/WRStatus.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.data 2 | 3 | enum class WRStatus { 4 | SUCCESS, NETWORK_ERROR, IO_ERROR, NO_SEARCH_RESULT, UNINITIALIZED, 5 | FEED_LOADED, FEED_NETWORK_ERROR, OTHER 6 | } 7 | 8 | enum class SavedStatus { 9 | NOT_SAVED, SAVING, SAVED 10 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/data/WikiApiPageData.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class WikiApiPageData( 8 | val query: WikiApiQuery? = null 9 | ) 10 | 11 | @Serializable 12 | data class WikiApiQuery( 13 | val pages: List 14 | ) 15 | 16 | @Serializable 17 | data class WikiApiPage( 18 | val title: String, 19 | val extract: String? = null, 20 | @SerialName(value = "pageid") val pageId: Int? = null, 21 | @SerialName(value = "original") val photo: WikiPhoto? = null, 22 | @SerialName(value = "terms") val photoDesc: WikiPhotoDesc? = null, 23 | @SerialName(value = "langlinks") val langs: List? = null 24 | ) 25 | 26 | @Serializable 27 | data class WikiPhoto( 28 | val source: String, 29 | val width: Int, 30 | val height: Int 31 | ) 32 | 33 | @Serializable 34 | data class WikiPhotoDesc( 35 | val label: List? = null, 36 | val description: List? = null 37 | ) 38 | 39 | @Serializable 40 | data class WikiLang( 41 | val lang: String, 42 | val title: String 43 | ) 44 | -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/data/WikiApiSearchResults.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class WikiApiSearchResults( 8 | val query: WikiSearchResultsQuery = WikiSearchResultsQuery() 9 | ) 10 | 11 | @Serializable 12 | data class WikiSearchResultsQuery( 13 | val pages: List = emptyList() 14 | ) 15 | 16 | @Serializable 17 | data class WikiSearchResult( 18 | val ns: Int = 0, 19 | val title: String, 20 | @SerialName("titlesnippet") val titleSnippet: String, 21 | @SerialName("pageid") val pageId: Int, 22 | val snippet: String, 23 | val index: Int, 24 | @SerialName("redirecttitle") val redirectTitle: String? = null, 25 | val thumbnail: WikiPhoto? = null 26 | ) 27 | 28 | @Serializable 29 | data class WikiApiPrefixSearchResults( 30 | val query: WikiPrefixSearchResultsQuery = WikiPrefixSearchResultsQuery() 31 | ) 32 | 33 | @Serializable 34 | data class WikiPrefixSearchResultsQuery( 35 | val pages: List = emptyList() 36 | ) 37 | 38 | @Serializable 39 | data class WikiPrefixSearchResult( 40 | @SerialName("pageid") val pageId: Int, 41 | val ns: Int = 0, 42 | val title: String, 43 | val index: Int, 44 | val thumbnail: WikiPhoto? = null, 45 | val terms: WikiPrefixSearchPageTerms? = null 46 | ) 47 | 48 | @Serializable 49 | data class WikiPrefixSearchPageTerms( 50 | val description: List 51 | ) -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/data/WikipediaRepository.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.data 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.withContext 5 | import org.nsh07.wikireader.network.WikipediaApiService 6 | import java.time.LocalDate 7 | import java.time.format.DateTimeFormatter 8 | 9 | interface WikipediaRepository { 10 | suspend fun getPrefixSearchResults(query: String): WikiApiPrefixSearchResults 11 | suspend fun getSearchResults(query: String): WikiApiSearchResults 12 | suspend fun getPageData(query: String): WikiApiPageData 13 | suspend fun getPageContent(title: String): String 14 | suspend fun getRandomResult(): WikiApiPageData 15 | suspend fun getFeed( 16 | date: String = LocalDate.now() 17 | .format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) 18 | ): FeedApiResponse 19 | } 20 | 21 | class NetworkWikipediaRepository( 22 | private val wikipediaApiService: WikipediaApiService, 23 | private val wikipediaPageApiService: WikipediaApiService, 24 | private val ioDispatcher: CoroutineDispatcher 25 | ) : WikipediaRepository { 26 | override suspend fun getPrefixSearchResults(query: String): WikiApiPrefixSearchResults = 27 | withContext(ioDispatcher) { 28 | wikipediaApiService.getPrefixSearchResults(query) 29 | } 30 | 31 | override suspend fun getSearchResults(query: String): WikiApiSearchResults = 32 | withContext(ioDispatcher) { 33 | wikipediaApiService.getSearchResults(query) 34 | } 35 | override suspend fun getPageData(query: String): WikiApiPageData = 36 | withContext(ioDispatcher) { 37 | wikipediaApiService.getPageData(query) 38 | } 39 | 40 | override suspend fun getPageContent(title: String): String = 41 | withContext(ioDispatcher) { 42 | wikipediaPageApiService.getPageContent(title) 43 | } 44 | 45 | override suspend fun getRandomResult(): WikiApiPageData = 46 | withContext(ioDispatcher) { 47 | wikipediaApiService.getRandomResult() 48 | } 49 | 50 | override suspend fun getFeed( 51 | date: String 52 | ): FeedApiResponse = 53 | withContext(ioDispatcher) { 54 | wikipediaApiService.getFeed(date) 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/data/misc.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.data 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | /** 6 | * Extension function for [String] to convert it to a [Color] 7 | * 8 | * The base string must be of the format produced by [Color.toString], 9 | * i.e, the color black with 100% opacity in sRGB would be represented by: 10 | * 11 | * Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1) 12 | */ 13 | fun String.toColor(): Color { 14 | // Sample string: Color(0.0, 0.0, 0.0, 1.0, sRGB IEC61966-2.1) 15 | val comma1 = this.indexOf(',') 16 | val comma2 = this.indexOf(',', comma1 + 1) 17 | val comma3 = this.indexOf(',', comma2 + 1) 18 | val comma4 = this.indexOf(',', comma3 + 1) 19 | 20 | val r = this.substringAfter('(').substringBefore(',').toFloat() 21 | val g = this.slice(comma1 + 1..comma2 - 1).toFloat() 22 | val b = this.slice(comma2 + 1..comma3 - 1).toFloat() 23 | val a = this.slice(comma3 + 1..comma4 - 1).toFloat() 24 | return Color(r, g, b, a) 25 | } 26 | 27 | /** 28 | * Function to parse a string returned by the Wikipedia API and convert it into a [List] of [String]s 29 | * 30 | * @param text The [String] returned by the Wikipedia API 31 | * 32 | * @return List of the format: 33 | * 34 | * {intro text, heading1, body1, heading2, body2, ...} 35 | * 36 | * Note that subheadings are *not* parsed, and are left as is (surrounded by three or more '=' signs 37 | * on either side) 38 | */ 39 | fun parseSections(text: String): List { 40 | val out = text.split("\n==(?!=)|(?= 1 shl 30 -> "%.1f GB".format(bytes / (1 shl 30)) 78 | bytes >= 1 shl 20 -> "%.1f MB".format(bytes / (1 shl 20)) 79 | bytes >= 1 shl 10 -> "%.0f kB".format(bytes / (1 shl 10)) 80 | else -> "$bytes bytes" 81 | } 82 | 83 | /** 84 | * Converts a Wikipedia URL language code (e.g. "en" in en.wikipedia.org for the English Wikipedia) 85 | * into its corresponding language name (e.g. "English" for en) 86 | */ 87 | fun langCodeToName(langCode: String): String { 88 | return try { 89 | LanguageData.langNames[LanguageData.langCodes.binarySearch(langCode)] 90 | } catch (_: Exception) { 91 | langCode 92 | } 93 | } 94 | 95 | /** 96 | * Converts a Wikipedia URL language code (e.g. "en" in en.wikipedia.org for the English Wikipedia) 97 | * into its corresponding Wikipedia name (e.g. "English Wikipedia" for en) 98 | */ 99 | fun langCodeToWikiName(langCode: String): String { 100 | return try { 101 | LanguageData.wikipediaNames[LanguageData.langCodes.binarySearch(langCode)] 102 | } catch (_: Exception) { 103 | langCode 104 | } 105 | } 106 | 107 | /** 108 | * Checks whether a Wikipedia language code corresponds to an RTL language. 109 | * 110 | * @param langCode The language code to check 111 | * @return True if the language is RTL, false otherwise 112 | */ 113 | fun isRtl(langCode: String): Boolean { 114 | return langCode in LanguageData.rtlLangCodes 115 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/network/HostSelectionInterceptor.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Code taken from https://gist.github.com/swankjesse/8571a8207a5815cca1fb 4 | */ 5 | 6 | package org.nsh07.wikireader.network; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | import java.io.IOException; 11 | 12 | import okhttp3.HttpUrl; 13 | import okhttp3.Interceptor; 14 | import okhttp3.Request; 15 | 16 | /** 17 | * An interceptor that allows runtime changes to the URL hostname. 18 | */ 19 | public final class HostSelectionInterceptor implements Interceptor { 20 | private volatile String host; 21 | 22 | public void setHost(String host) { 23 | this.host = host; 24 | } 25 | 26 | @NonNull 27 | @Override 28 | public okhttp3.Response intercept(Chain chain) throws IOException { 29 | Request request = chain.request(); 30 | String host = this.host; 31 | if (host != null) { 32 | HttpUrl newUrl = request.url().newBuilder() 33 | .host(host) 34 | .build(); 35 | request = request.newBuilder() 36 | .url(newUrl) 37 | .build(); 38 | } 39 | return chain.proceed(request); 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/network/NetworkException.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.network 2 | 3 | class NetworkException: Exception() { 4 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/network/WikipediaApiService.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.network 2 | 3 | import org.nsh07.wikireader.data.FeedApiResponse 4 | import org.nsh07.wikireader.data.WikiApiPageData 5 | import org.nsh07.wikireader.data.WikiApiPrefixSearchResults 6 | import org.nsh07.wikireader.data.WikiApiSearchResults 7 | import retrofit2.http.GET 8 | import retrofit2.http.Path 9 | import retrofit2.http.Query 10 | 11 | private const val API_QUERY = 12 | "w/api.php?format=json&action=query&prop=pageimages|pageterms|langlinks&piprop=original&pilicense=any&lllimit=max&redirects=1&formatversion=2&maxage=900&smaxage=900" 13 | 14 | private const val CONTENT_QUERY = 15 | "wiki/{title}?action=raw&maxage=900&smaxage=900" 16 | 17 | private const val FEED_QUERY = 18 | "api/rest_v1/feed/featured/{date}" 19 | 20 | private const val PREFIX_SEARCH_QUERY = 21 | "w/api.php?action=query&generator=prefixsearch&prop=pageimages|pageterms&piprop=thumbnail&pithumbsize=128&wbptterms=description&gpslimit=20&format=json&formatversion=2&maxage=900&smaxage=900" 22 | 23 | private const val RANDOM_QUERY = 24 | "w/api.php?format=json&action=query&prop=pageimages|pageterms|langlinks&piprop=original&pilicense=any&lllimit=max&generator=random&redirects=1&formatversion=2&grnnamespace=0" 25 | 26 | private const val SEARCH_QUERY = 27 | "w/api.php?action=query&generator=search&prop=pageimages&piprop=thumbnail&pithumbsize=128&gsrnamespace=0&gsrlimit=20&gsrprop=snippet|titlesnippet|redirecttitle|extensiondata&format=json&formatversion=2&maxage=900&smaxage=900" 28 | 29 | interface WikipediaApiService { 30 | @GET(PREFIX_SEARCH_QUERY) 31 | suspend fun getPrefixSearchResults(@Query("gpssearch") query: String): WikiApiPrefixSearchResults 32 | 33 | @GET(SEARCH_QUERY) 34 | suspend fun getSearchResults(@Query("gsrsearch") query: String): WikiApiSearchResults 35 | 36 | @GET(API_QUERY) 37 | suspend fun getPageData(@Query("titles") query: String): WikiApiPageData 38 | 39 | @GET(CONTENT_QUERY) 40 | suspend fun getPageContent(@Path("title", encoded = true) title: String): String 41 | 42 | @GET(RANDOM_QUERY) 43 | suspend fun getRandomResult(): WikiApiPageData 44 | 45 | @GET(FEED_QUERY) 46 | suspend fun getFeed( 47 | @Path("date", encoded = true) date: String, 48 | ): FeedApiResponse 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/parser/cleanUpWikitext.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.parser 2 | 3 | /** 4 | * Remove parts of a Wikitext section that are not to be rendered 5 | * 6 | * @param wikitext Source Wikitext to clean up 7 | */ 8 | fun cleanUpWikitext(wikitext: String): String { 9 | return wikitext 10 | .replace( 11 | ".+?|".toRegex(RegexOption.DOT_MATCHES_ALL), 12 | "" 13 | ) 14 | .replace( 15 | "\\{\\{nobility table header.*?\\}\\}" 16 | .toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL)), 17 | "{| class=\"wikitable\"\n" 18 | ) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/aboutScreen/AboutTopAppBar.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.aboutScreen 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.material3.IconButtonDefaults 10 | import androidx.compose.material3.LargeFlexibleTopAppBar 11 | import androidx.compose.material3.Text 12 | import androidx.compose.material3.TopAppBarScrollBehavior 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.res.stringResource 15 | import org.nsh07.wikireader.R 16 | import org.nsh07.wikireader.ui.theme.CustomTopBarColors.topBarColors 17 | 18 | @Composable 19 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) 20 | fun AboutTopAppBar(scrollBehavior: TopAppBarScrollBehavior, onBack: () -> Unit) { 21 | LargeFlexibleTopAppBar( 22 | title = { Text(stringResource(R.string.about)) }, 23 | subtitle = { Text(stringResource(R.string.app_name)) }, 24 | navigationIcon = { 25 | IconButton( 26 | shapes = IconButtonDefaults.shapes(), 27 | onClick = onBack 28 | ) { 29 | Icon( 30 | Icons.AutoMirrored.Outlined.ArrowBack, 31 | contentDescription = stringResource(R.string.back) 32 | ) 33 | } 34 | }, 35 | colors = topBarColors, 36 | scrollBehavior = scrollBehavior 37 | ) 38 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/homeScreen/ArticleLanguageBottomSheet.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.homeScreen 2 | 3 | import android.util.Log 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 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.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.foundation.lazy.rememberLazyListState 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.HorizontalDivider 14 | import androidx.compose.material3.ListItem 15 | import androidx.compose.material3.ListItemDefaults 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.ModalBottomSheet 18 | import androidx.compose.material3.Text 19 | import androidx.compose.material3.rememberModalBottomSheetState 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.LaunchedEffect 22 | import androidx.compose.runtime.rememberCoroutineScope 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.res.stringResource 26 | import androidx.compose.ui.unit.dp 27 | import kotlinx.coroutines.launch 28 | import org.nsh07.wikireader.R 29 | import org.nsh07.wikireader.data.WikiLang 30 | import org.nsh07.wikireader.data.langCodeToName 31 | import org.nsh07.wikireader.ui.settingsScreen.LanguageSearchBar 32 | 33 | @OptIn(ExperimentalMaterial3Api::class) 34 | @Composable 35 | fun ArticleLanguageBottomSheet( 36 | langs: List, 37 | searchStr: String, 38 | searchQuery: String, 39 | setShowSheet: (Boolean) -> Unit, 40 | setLang: (String) -> Unit, 41 | loadPage: (String) -> Unit, 42 | setSearchStr: (String) -> Unit, 43 | modifier: Modifier = Modifier 44 | ) { 45 | val listState = rememberLazyListState() 46 | val scope = rememberCoroutineScope() 47 | val bottomSheetState = rememberModalBottomSheetState() 48 | 49 | ModalBottomSheet( 50 | onDismissRequest = { 51 | setShowSheet(false) 52 | setSearchStr("") 53 | }, 54 | sheetState = bottomSheetState, 55 | modifier = modifier 56 | ) { 57 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 58 | Text( 59 | text = stringResource(R.string.chooseWikipediaLanguage), 60 | style = MaterialTheme.typography.labelLarge 61 | ) 62 | LanguageSearchBar( 63 | searchStr = searchStr, 64 | setSearchStr = setSearchStr, 65 | modifier = Modifier 66 | .fillMaxWidth() 67 | .padding(16.dp) 68 | ) 69 | HorizontalDivider() 70 | LazyColumn(state = listState) { 71 | items(langs, key = { it.lang }) { 72 | val langName: String? = try { 73 | langCodeToName(it.lang) 74 | } catch (_: Exception) { 75 | Log.e("Language", "Language not found: ${it.lang}") 76 | null 77 | } 78 | if (langName != null && langName.contains(searchQuery, ignoreCase = true)) 79 | ListItem( 80 | headlineContent = { Text(langName) }, 81 | supportingContent = { Text(it.title) }, 82 | colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow), 83 | modifier = Modifier 84 | .clickable(onClick = { 85 | setLang(it.lang) 86 | loadPage(it.title) 87 | scope 88 | .launch { bottomSheetState.hide() } 89 | .invokeOnCompletion { 90 | if (!bottomSheetState.isVisible) { 91 | setShowSheet(false) 92 | setSearchStr("") 93 | } 94 | } 95 | }) 96 | ) 97 | } 98 | } 99 | Spacer(Modifier.weight(1f)) 100 | } 101 | } 102 | LaunchedEffect(searchQuery) { 103 | listState.scrollToItem(0) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/homeScreen/ArticleViewsGraph.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.homeScreen 2 | 3 | import androidx.compose.animation.core.FastOutSlowInEasing 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.SolidColor 10 | import ir.ehsannarmani.compose_charts.LineChart 11 | import ir.ehsannarmani.compose_charts.models.DividerProperties 12 | import ir.ehsannarmani.compose_charts.models.DotProperties 13 | import ir.ehsannarmani.compose_charts.models.GridProperties 14 | import ir.ehsannarmani.compose_charts.models.HorizontalIndicatorProperties 15 | import ir.ehsannarmani.compose_charts.models.LabelHelperProperties 16 | import ir.ehsannarmani.compose_charts.models.LabelProperties 17 | import ir.ehsannarmani.compose_charts.models.Line 18 | import ir.ehsannarmani.compose_charts.models.PopupProperties 19 | 20 | @Composable 21 | fun ArticleViewsGraph(viewCounts: List, modifier: Modifier = Modifier) { 22 | val colorScheme = MaterialTheme.colorScheme 23 | LineChart( 24 | modifier = modifier, 25 | data = remember { 26 | listOf( 27 | Line( 28 | label = "", 29 | values = viewCounts.map { it.toDouble() }, 30 | color = SolidColor(colorScheme.primary), 31 | strokeAnimationSpec = tween(1500, easing = FastOutSlowInEasing), 32 | curvedEdges = true 33 | ) 34 | ) 35 | }, 36 | labelHelperProperties = LabelHelperProperties(enabled = false), 37 | gridProperties = GridProperties(enabled = false), 38 | popupProperties = PopupProperties(enabled = false), 39 | labelProperties = LabelProperties(enabled = false), 40 | indicatorProperties = HorizontalIndicatorProperties(enabled = false), 41 | dotsProperties = DotProperties(enabled = false), 42 | dividerProperties = DividerProperties(enabled = false) 43 | ) 44 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/homeScreen/EquationImage.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.homeScreen 2 | 3 | import android.content.Context 4 | import androidx.compose.foundation.horizontalScroll 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.ColorFilter 11 | import androidx.compose.ui.graphics.ColorMatrix 12 | import androidx.compose.ui.res.painterResource 13 | import androidx.compose.ui.unit.dp 14 | import coil3.compose.AsyncImage 15 | import coil3.request.ImageRequest 16 | import coil3.size.Size 17 | import org.nsh07.wikireader.R 18 | import org.nsh07.wikireader.ui.theme.ColorConstants.colorMatrixInvert 19 | 20 | @Composable 21 | fun EquationImage( 22 | context: Context, 23 | dpi: Float, 24 | latex: String, 25 | fontSize: Int, 26 | darkTheme: Boolean 27 | ) { 28 | Box(modifier = Modifier.horizontalScroll(rememberScrollState())) { 29 | AsyncImage( 30 | model = ImageRequest.Builder(context) 31 | .data( 32 | "https://latex.codecogs.com/png.image?\\dpi{${ 33 | (dpi * 160 * (fontSize / 16.0)).toInt() 34 | }}${latex}" 35 | ) 36 | .size(Size.ORIGINAL) 37 | .build(), 38 | placeholder = painterResource(R.drawable.more_horiz), 39 | error = painterResource(R.drawable.error), 40 | contentDescription = null, 41 | colorFilter = if (darkTheme) // Invert colors in dark theme 42 | ColorFilter.colorMatrix(ColorMatrix(colorMatrixInvert)) 43 | else null, 44 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) 45 | ) 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/homeScreen/ImageWithCaption.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.homeScreen 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.widthIn 7 | import androidx.compose.material3.MaterialTheme.colorScheme 8 | import androidx.compose.material3.MaterialTheme.shapes 9 | import androidx.compose.material3.MaterialTheme.typography 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.graphics.ColorFilter 16 | import androidx.compose.ui.graphics.ColorMatrix 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import coil3.ImageLoader 21 | import org.nsh07.wikireader.parser.toWikitextAnnotatedString 22 | import org.nsh07.wikireader.ui.image.FeedImage 23 | import org.nsh07.wikireader.ui.theme.ColorConstants.colorMatrixInvert 24 | 25 | @Composable 26 | fun ImageWithCaption( 27 | text: String, 28 | fontSize: Int, 29 | darkTheme: Boolean, 30 | background: Boolean, 31 | imageLoader: ImageLoader, 32 | onLinkClick: (String) -> Unit, 33 | onClick: (String, String) -> Unit, 34 | modifier: Modifier = Modifier 35 | ) { 36 | val uriLow = remember(text) { 37 | "https://commons.wikimedia.org/wiki/Special:FilePath/${ 38 | text.substringAfter(':').substringBefore('|').substringBefore("]]") 39 | }?width=720" 40 | } 41 | val uriHigh = remember(text) { 42 | "https://commons.wikimedia.org/wiki/Special:FilePath/${ 43 | text.substringAfter(':').substringBefore('|').substringBefore("]]") 44 | }" 45 | } 46 | val description = remember(text) { text.substringAfter('|', "").substringBefore('|') } 47 | val invert = remember { text.contains("|invert") } 48 | 49 | FeedImage( 50 | source = uriLow, 51 | description = description, 52 | imageLoader = imageLoader, 53 | loadingIndicator = false, 54 | colorFilter = if (invert && darkTheme && !background) // Invert colors in dark theme 55 | ColorFilter.colorMatrix(ColorMatrix(colorMatrixInvert)) 56 | else null, 57 | background = background, 58 | modifier = modifier 59 | .padding(horizontal = 16.dp) 60 | .padding(top = 8.dp) 61 | .clip(shapes.large) 62 | .animateContentSize() 63 | .widthIn(max = 512.dp) 64 | .clickable(onClick = { onClick(uriHigh, description) }) 65 | ) 66 | Text( 67 | description.toWikitextAnnotatedString( 68 | colorScheme = colorScheme, 69 | fontSize = fontSize - 2, 70 | loadPage = onLinkClick, 71 | typography = typography 72 | ), 73 | fontSize = (fontSize - 2).sp, 74 | lineHeight = (24 * ((fontSize - 2) / 16.0)).toInt().sp, 75 | textAlign = TextAlign.Center, 76 | color = colorScheme.onSurfaceVariant, 77 | modifier = Modifier 78 | .padding(horizontal = 16.dp) 79 | .padding(vertical = 8.dp) 80 | ) 81 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/homeScreen/ParsedBodyText.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.homeScreen 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.MaterialTheme.typography 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.platform.LocalDensity 14 | import androidx.compose.ui.text.AnnotatedString 15 | import androidx.compose.ui.text.font.FontFamily 16 | import androidx.compose.ui.text.style.Hyphens 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import coil3.ImageLoader 20 | import com.github.tomtung.latex2unicode.LaTeX2Unicode 21 | import kotlin.text.Typography.nbsp 22 | 23 | @Composable 24 | fun ParsedBodyText( 25 | body: List, 26 | fontSize: Int, 27 | fontFamily: FontFamily, 28 | imageLoader: ImageLoader, 29 | background: Boolean, 30 | renderMath: Boolean, 31 | darkTheme: Boolean, 32 | dataSaver: Boolean, 33 | onLinkClick: (String) -> Unit, 34 | onGalleryImageClick: (String, String) -> Unit, 35 | modifier: Modifier = Modifier 36 | ) { 37 | val context = LocalContext.current 38 | val dpi = LocalDensity.current.density 39 | 40 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { 41 | body.forEach { 42 | if (it.startsWith("[[File:")) { 43 | if (!dataSaver) { 44 | ImageWithCaption( 45 | text = it.toString(), 46 | fontSize = fontSize, 47 | imageLoader = imageLoader, 48 | onLinkClick = onLinkClick, 49 | onClick = onGalleryImageClick, 50 | darkTheme = darkTheme, 51 | background = background 52 | ) 53 | } 54 | } else if (it.startsWith("') }, 71 | fontSize = fontSize, 72 | darkTheme = darkTheme 73 | ) 74 | } else { 75 | Text( 76 | text = LaTeX2Unicode.convert(it.toString()) 77 | .replace(' ', nbsp).substringAfter('>'), 78 | fontFamily = FontFamily.Serif, 79 | fontSize = (fontSize + 4).sp, 80 | lineHeight = (24 * (fontSize / 16.0) + 4).toInt().sp, 81 | modifier = Modifier 82 | .padding(horizontal = 16.dp) 83 | ) 84 | } 85 | } else if (it.startsWith("{|")) { 86 | AsyncWikitable( 87 | text = it.toString(), 88 | fontSize = fontSize, 89 | onLinkClick = onLinkClick 90 | ) 91 | } else { 92 | Text( 93 | text = it, 94 | style = typography.bodyLarge.copy(hyphens = Hyphens.Auto), 95 | fontSize = fontSize.sp, 96 | fontFamily = fontFamily, 97 | lineHeight = (24 * (fontSize / 16.0)).toInt().sp, 98 | modifier = Modifier 99 | .fillMaxWidth() 100 | .padding(horizontal = 16.dp) 101 | ) 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/image/FeedImage.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.aspectRatio 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.material3.CircularWavyProgressIndicator 10 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.LoadingIndicator 13 | import androidx.compose.material3.MaterialTheme.colorScheme 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.collectAsState 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.ColorFilter 21 | import androidx.compose.ui.layout.ContentScale 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.res.stringResource 25 | import coil3.ImageLoader 26 | import coil3.compose.AsyncImagePainter 27 | import coil3.compose.rememberAsyncImagePainter 28 | import coil3.request.ImageRequest 29 | import coil3.request.crossfade 30 | import org.nsh07.wikireader.R 31 | 32 | @OptIn(ExperimentalMaterial3ExpressiveApi::class) 33 | @Composable 34 | fun FeedImage( 35 | source: String?, 36 | modifier: Modifier = Modifier, 37 | description: String? = null, 38 | width: Int? = null, 39 | height: Int? = null, 40 | imageLoader: ImageLoader, 41 | loadingIndicator: Boolean, 42 | background: Boolean, 43 | colorFilter: ColorFilter? = null, 44 | contentScale: ContentScale = ContentScale.Crop 45 | ) { 46 | val context = LocalContext.current 47 | val painter = rememberAsyncImagePainter( 48 | model = ImageRequest.Builder(context) 49 | .data(source) 50 | .crossfade(true) 51 | .build(), 52 | imageLoader = imageLoader, 53 | contentScale = contentScale, 54 | ) 55 | 56 | val painterState by painter.state.collectAsState() 57 | 58 | if (painterState is AsyncImagePainter.State.Success) { 59 | Image( 60 | painter = painter, 61 | contentDescription = description, 62 | contentScale = contentScale, 63 | colorFilter = colorFilter, 64 | modifier = 65 | if (width != null && height != null) 66 | modifier 67 | .fillMaxWidth() 68 | .aspectRatio(width.toFloat() / height.toFloat()) 69 | .background(if (background) Color.White else Color.Transparent) 70 | else 71 | modifier 72 | .fillMaxSize() 73 | .background(if (background) Color.White else Color.Transparent) 74 | ) 75 | } else if (painterState is AsyncImagePainter.State.Loading) { 76 | Box( 77 | contentAlignment = Alignment.Center, 78 | modifier = 79 | if (width != null && height != null) 80 | modifier 81 | .fillMaxWidth() 82 | .aspectRatio(width.toFloat() / height.toFloat()) 83 | else 84 | modifier 85 | .fillMaxSize() 86 | ) { 87 | if (loadingIndicator) LoadingIndicator() 88 | else CircularWavyProgressIndicator() 89 | } 90 | } else { 91 | Box( 92 | contentAlignment = Alignment.Center, 93 | modifier = 94 | if (width != null && height != null) 95 | modifier 96 | .fillMaxWidth() 97 | .aspectRatio(width.toFloat() / height.toFloat()) 98 | else 99 | modifier.fillMaxSize() 100 | ) { 101 | Icon( 102 | painterResource(R.drawable.error), 103 | contentDescription = stringResource(R.string.errorLoadingImage), 104 | tint = colorScheme.error 105 | ) 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/image/ImageCard.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.image 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.widthIn 8 | import androidx.compose.material3.ElevatedCard 9 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.MaterialTheme.motionScheme 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.layout.ContentScale 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import coil3.ImageLoader 20 | import org.nsh07.wikireader.data.WikiPhoto 21 | import org.nsh07.wikireader.data.WikiPhotoDesc 22 | 23 | /** 24 | * Composable for displaying a Wikipedia image with its associated text 25 | * 26 | * Displays an [androidx.compose.foundation.Image] composable fetched from a URI with a 27 | * [androidx.compose.material3.CircularWavyProgressIndicator] while the image is loading, with the 28 | * image title and description at the bottom of a card. 29 | * 30 | * @param photo A (nullable) WikiPhoto object. The image url and aspect ratio are provided by this 31 | * object 32 | * @param photoDesc A WikiPhotoDesc object that provides the image title and description 33 | */ 34 | 35 | @OptIn(ExperimentalMaterial3ExpressiveApi::class) 36 | @Composable 37 | fun ImageCard( 38 | photo: WikiPhoto?, 39 | photoDesc: WikiPhotoDesc, 40 | title: String, 41 | imageLoader: ImageLoader, 42 | showPhoto: Boolean, 43 | background: Boolean, 44 | onClick: () -> Unit, 45 | modifier: Modifier = Modifier 46 | ) { 47 | val labelBottomPadding = 48 | if (photoDesc.description == null) 16.dp 49 | else 8.dp 50 | ElevatedCard( 51 | onClick = onClick, 52 | modifier = modifier 53 | .padding(horizontal = 16.dp) 54 | .widthIn(max = 512.dp) 55 | .fillMaxWidth() 56 | ) { 57 | Column( 58 | horizontalAlignment = Alignment.CenterHorizontally, 59 | modifier = Modifier 60 | .animateContentSize(motionScheme.defaultSpatialSpec()) 61 | ) { 62 | if (photo != null && showPhoto) { 63 | PageImage( 64 | photo = photo, 65 | photoDesc = photoDesc, 66 | contentScale = ContentScale.Crop, 67 | imageLoader = imageLoader, 68 | background = background, 69 | modifier = Modifier.fillMaxWidth() 70 | ) 71 | } 72 | Text( 73 | text = photoDesc.label?.get(0) ?: title, 74 | style = MaterialTheme.typography.titleLarge, 75 | textAlign = TextAlign.Center, 76 | modifier = Modifier 77 | .padding(horizontal = 16.dp) 78 | .padding(top = 16.dp, bottom = labelBottomPadding) 79 | .fillMaxWidth() 80 | ) 81 | if (photoDesc.description != null) { 82 | Text( 83 | text = photoDesc.description[0], 84 | style = MaterialTheme.typography.bodyLarge, 85 | textAlign = TextAlign.Center, 86 | modifier = Modifier 87 | .padding(horizontal = 16.dp) 88 | .padding(bottom = 16.dp) 89 | .fillMaxWidth() 90 | ) 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/savedArticlesScreen/DeleteArticleDialog.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.savedArticlesScreen 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.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.wrapContentHeight 9 | import androidx.compose.foundation.layout.wrapContentWidth 10 | import androidx.compose.material3.AlertDialogDefaults 11 | import androidx.compose.material3.BasicAlertDialog 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Surface 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.TextButton 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.unit.dp 25 | import org.nsh07.wikireader.R 26 | import org.nsh07.wikireader.data.WRStatus 27 | 28 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) 29 | @Composable 30 | fun DeleteArticleDialog( 31 | articleFileName: String?, 32 | showSnackbar: (String) -> Unit, 33 | setShowDeleteDialog: (Boolean) -> Unit, 34 | deleteArticle: (String) -> WRStatus, 35 | deleteAll: () -> WRStatus 36 | ) { 37 | val articleName: String? = articleFileName?.substringBeforeLast('.')?.substringBeforeLast('.') 38 | val context = LocalContext.current 39 | BasicAlertDialog( 40 | onDismissRequest = { setShowDeleteDialog(false) } 41 | ) { 42 | Surface( 43 | modifier = Modifier 44 | .wrapContentWidth() 45 | .wrapContentHeight(), 46 | shape = MaterialTheme.shapes.extraLarge, 47 | tonalElevation = AlertDialogDefaults.TonalElevation 48 | ) { 49 | Column(modifier = Modifier.padding(24.dp)) { 50 | Text( 51 | text = if (articleName != null) stringResource(R.string.deleteSavedArticle) 52 | else stringResource(R.string.deleteAllArticles), 53 | style = MaterialTheme.typography.headlineSmall 54 | ) 55 | Spacer(modifier = Modifier.padding(16.dp)) 56 | Text( 57 | text = 58 | if (articleName != null) 59 | stringResource(R.string.deleteSavedArticleDesc, articleName) 60 | else 61 | stringResource(R.string.deleteAllArticlesDesc), 62 | style = MaterialTheme.typography.bodyMedium 63 | ) 64 | Spacer(modifier = Modifier.height(24.dp)) 65 | Row(modifier = Modifier.align(Alignment.End)) { 66 | TextButton( 67 | shapes = ButtonDefaults.shapes(), 68 | onClick = { setShowDeleteDialog(false) }) { 69 | Text(text = stringResource(R.string.cancel)) 70 | } 71 | TextButton(shapes = ButtonDefaults.shapes(), onClick = { 72 | setShowDeleteDialog(false) 73 | val status = if (articleFileName != null) deleteArticle(articleFileName) 74 | else deleteAll() 75 | if (status == WRStatus.SUCCESS) 76 | showSnackbar(context.getString(R.string.articleDeleted)) 77 | else 78 | showSnackbar( 79 | context.getString( 80 | R.string.unableToDeleteArticle, 81 | status.name 82 | ) 83 | ) 84 | } 85 | ) { 86 | Text(text = stringResource(R.string.delete)) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/savedArticlesScreen/LanguageFilterOption.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.savedArticlesScreen 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | 7 | class LanguageFilterOption( 8 | val option: String, 9 | val langCode: String, 10 | initialSelection: Boolean = false 11 | ) { 12 | var selected by mutableStateOf(initialSelection) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/savedArticlesScreen/SavedArticlesTopBar.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.savedArticlesScreen 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.material3.IconButtonDefaults 10 | import androidx.compose.material3.LargeFlexibleTopAppBar 11 | import androidx.compose.material3.Text 12 | import androidx.compose.material3.TopAppBarScrollBehavior 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.res.painterResource 15 | import androidx.compose.ui.res.stringResource 16 | import org.nsh07.wikireader.R 17 | import org.nsh07.wikireader.ui.theme.CustomTopBarColors.topBarColors 18 | 19 | @Composable 20 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) 21 | fun SavedArticlesTopBar( 22 | articlesInfo: String, 23 | scrollBehavior: TopAppBarScrollBehavior, 24 | deleteEnabled: Boolean, 25 | onBack: () -> Unit, 26 | onDeleteAll: () -> Unit 27 | ) { 28 | LargeFlexibleTopAppBar( 29 | title = { Text(stringResource(R.string.savedArticles)) }, 30 | subtitle = { Text(articlesInfo) }, 31 | navigationIcon = { 32 | IconButton( 33 | shapes = IconButtonDefaults.shapes(), 34 | onClick = onBack 35 | ) { 36 | Icon( 37 | Icons.AutoMirrored.Outlined.ArrowBack, 38 | contentDescription = stringResource(R.string.back) 39 | ) 40 | } 41 | }, 42 | actions = { 43 | IconButton( 44 | enabled = deleteEnabled, 45 | shapes = IconButtonDefaults.shapes(), 46 | onClick = onDeleteAll 47 | ) { 48 | Icon( 49 | painterResource(R.drawable.delete), 50 | contentDescription = stringResource(R.string.deleteAll) 51 | ) 52 | } 53 | }, 54 | colors = topBarColors, 55 | scrollBehavior = scrollBehavior 56 | ) 57 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/settingsScreen/LanguageSearchBar.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.settingsScreen 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.Search 5 | import androidx.compose.material3.DockedSearchBar 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.SearchBarDefaults 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import org.nsh07.wikireader.R 14 | 15 | @OptIn(ExperimentalMaterial3Api::class) 16 | @Composable 17 | fun LanguageSearchBar( 18 | searchStr: String, 19 | setSearchStr: (String) -> Unit, 20 | modifier: Modifier = Modifier 21 | ) { 22 | DockedSearchBar( 23 | inputField = { 24 | SearchBarDefaults.InputField( 25 | query = searchStr, 26 | onQueryChange = setSearchStr, 27 | onSearch = {}, 28 | expanded = false, 29 | onExpandedChange = {}, 30 | placeholder = { Text(stringResource(R.string.searchLanguages)) }, 31 | leadingIcon = { 32 | Icon( 33 | Icons.Outlined.Search, 34 | contentDescription = stringResource(R.string.search) 35 | ) 36 | }, 37 | ) 38 | }, 39 | expanded = false, 40 | onExpandedChange = {}, 41 | modifier = modifier 42 | ) {} 43 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/settingsScreen/ResetSettingsDialog.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.settingsScreen 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.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.wrapContentHeight 9 | import androidx.compose.foundation.layout.wrapContentWidth 10 | import androidx.compose.material3.AlertDialogDefaults 11 | import androidx.compose.material3.BasicAlertDialog 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Surface 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.TextButton 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.unit.dp 25 | import org.nsh07.wikireader.R 26 | 27 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) 28 | @Composable 29 | fun ResetSettingsDialog( 30 | onResetSettings: () -> Unit, 31 | setShowResetSettingsDialog: (Boolean) -> Unit, 32 | showSnackbar: (String) -> Unit 33 | ) { 34 | val context = LocalContext.current 35 | BasicAlertDialog( 36 | onDismissRequest = { setShowResetSettingsDialog(false) } 37 | ) { 38 | Surface( 39 | modifier = Modifier 40 | .wrapContentWidth() 41 | .wrapContentHeight(), 42 | shape = MaterialTheme.shapes.extraLarge, 43 | tonalElevation = AlertDialogDefaults.TonalElevation 44 | ) { 45 | Column(modifier = Modifier.padding(24.dp)) { 46 | Text( 47 | text = stringResource(R.string.resetSettingsDialog), 48 | style = MaterialTheme.typography.headlineSmall 49 | ) 50 | Spacer(modifier = Modifier.padding(16.dp)) 51 | Text( 52 | text = stringResource(R.string.resetSettingsDialogDesc), 53 | style = MaterialTheme.typography.bodyMedium 54 | ) 55 | Spacer(modifier = Modifier.height(24.dp)) 56 | Row(modifier = Modifier.align(Alignment.End)) { 57 | TextButton( 58 | shapes = ButtonDefaults.shapes(), 59 | onClick = { setShowResetSettingsDialog(false) }) { 60 | Text(text = stringResource(R.string.cancel)) 61 | } 62 | TextButton(shapes = ButtonDefaults.shapes(), onClick = { 63 | setShowResetSettingsDialog(false) 64 | onResetSettings() 65 | showSnackbar(context.getString(R.string.settingsRestored)) 66 | } 67 | ) { 68 | Text(text = stringResource(R.string.reset)) 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/settingsScreen/SettingsTopBar.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.settingsScreen 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.material3.IconButtonDefaults 10 | import androidx.compose.material3.LargeFlexibleTopAppBar 11 | import androidx.compose.material3.Text 12 | import androidx.compose.material3.TopAppBarScrollBehavior 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.res.painterResource 15 | import androidx.compose.ui.res.stringResource 16 | import org.nsh07.wikireader.R 17 | import org.nsh07.wikireader.ui.theme.CustomTopBarColors.topBarColors 18 | 19 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) 20 | @Composable 21 | fun SettingsTopBar( 22 | scrollBehavior: TopAppBarScrollBehavior, 23 | onBack: () -> Unit, 24 | onResetSettings: () -> Unit 25 | ) { 26 | LargeFlexibleTopAppBar( 27 | title = { Text(stringResource(R.string.settings)) }, 28 | navigationIcon = { 29 | IconButton(shapes = IconButtonDefaults.shapes(), onClick = onBack) { 30 | Icon( 31 | Icons.AutoMirrored.Outlined.ArrowBack, 32 | contentDescription = stringResource(R.string.back) 33 | ) 34 | } 35 | }, 36 | actions = { 37 | IconButton(shapes = IconButtonDefaults.shapes(), onClick = onResetSettings) { 38 | Icon( 39 | painterResource(R.drawable.reset_settings), 40 | contentDescription = stringResource(R.string.resetSettingsIconDesc) 41 | ) 42 | } 43 | }, 44 | colors = topBarColors, 45 | scrollBehavior = scrollBehavior 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/shimmer/AnimatedShimmer.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.shimmer 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.RepeatMode 5 | import androidx.compose.animation.core.animateFloat 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.rememberInfiniteTransition 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.geometry.Offset 12 | import androidx.compose.ui.graphics.Brush 13 | 14 | @Composable 15 | fun AnimatedShimmer(content: @Composable (Brush) -> Unit) { 16 | val colorScheme = MaterialTheme.colorScheme 17 | val shimmerColors = listOf( 18 | colorScheme.surfaceContainer, 19 | colorScheme.surfaceContainerHighest, 20 | colorScheme.surfaceContainer 21 | ) 22 | 23 | val transition = rememberInfiniteTransition() 24 | val translateAnimation = transition.animateFloat( 25 | initialValue = 0f, 26 | targetValue = 5000f, 27 | animationSpec = infiniteRepeatable( 28 | animation = tween( 29 | durationMillis = 2500, 30 | easing = LinearEasing 31 | ), 32 | repeatMode = RepeatMode.Restart 33 | ) 34 | ) 35 | 36 | val brush = Brush.linearGradient( 37 | colors = shimmerColors, 38 | start = Offset(x = translateAnimation.value - 500, y = translateAnimation.value - 500), 39 | end = Offset(x = translateAnimation.value, y = translateAnimation.value) 40 | ) 41 | 42 | content(brush) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/shimmer/FeedLoader.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.shimmer 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.PaddingValues 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.lazy.LazyColumn 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.graphics.Brush 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | import org.nsh07.wikireader.ui.theme.WikiReaderTheme 22 | 23 | @Composable 24 | fun FeedLoader(brush: Brush, insets: PaddingValues, modifier: Modifier = Modifier) { 25 | val xl = MaterialTheme.shapes.extraLarge 26 | val l = MaterialTheme.shapes.large 27 | LazyColumn( 28 | verticalArrangement = Arrangement.spacedBy(16.dp), 29 | contentPadding = insets, 30 | modifier = modifier 31 | ) { 32 | item { 33 | Spacer( 34 | Modifier 35 | .padding(top = 16.dp) 36 | .padding(horizontal = 16.dp) 37 | .size(256.dp, 56.dp) 38 | .clip(xl) 39 | .background(brush) 40 | ) 41 | } 42 | item { 43 | Spacer( 44 | Modifier 45 | .fillMaxWidth() 46 | .height(256.dp) 47 | .padding(horizontal = 16.dp) 48 | .clip(l) 49 | .background(brush) 50 | ) 51 | } 52 | item { 53 | Spacer( 54 | Modifier 55 | .padding(horizontal = 16.dp) 56 | .size(200.dp, 52.dp) 57 | .clip(xl) 58 | .background(brush) 59 | ) 60 | } 61 | item { 62 | Spacer( 63 | Modifier 64 | .fillMaxWidth() 65 | .height(400.dp) 66 | .padding(horizontal = 16.dp) 67 | .clip(l) 68 | .background(brush) 69 | ) 70 | } 71 | item { 72 | Spacer( 73 | Modifier 74 | .padding(horizontal = 16.dp) 75 | .size(200.dp, 52.dp) 76 | .clip(xl) 77 | .background(brush) 78 | ) 79 | } 80 | item { 81 | Spacer( 82 | Modifier 83 | .fillMaxWidth() 84 | .height(256.dp) 85 | .padding(horizontal = 16.dp) 86 | .clip(l) 87 | .background(brush) 88 | ) 89 | } 90 | items(2) { 91 | Spacer( 92 | Modifier 93 | .padding(horizontal = 16.dp) 94 | .size(200.dp, 52.dp) 95 | .clip(xl) 96 | .background(brush) 97 | ) 98 | Row { 99 | Spacer( 100 | Modifier 101 | .weight(8f) 102 | .height(256.dp) 103 | .padding(start = 16.dp) 104 | .padding(end = 8.dp) 105 | .clip(xl) 106 | .background(brush) 107 | ) 108 | Spacer( 109 | Modifier 110 | .weight(2f) 111 | .height(256.dp) 112 | .padding(end = 16.dp) 113 | .clip(xl) 114 | .background(brush) 115 | ) 116 | } 117 | } 118 | item { 119 | Spacer(Modifier.height(156.dp)) 120 | } 121 | } 122 | } 123 | 124 | @Preview 125 | @Composable 126 | fun FeedShimmerPreview() { 127 | WikiReaderTheme { 128 | Surface { 129 | AnimatedShimmer { 130 | FeedLoader(it, PaddingValues(bottom = 16.dp)) 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.theme 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.material3.MaterialTheme.colorScheme 5 | import androidx.compose.material3.TopAppBarColors 6 | import androidx.compose.material3.TopAppBarDefaults 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.graphics.Color 10 | 11 | val Purple80 = Color(0xFFD0BCFF) 12 | val PurpleGrey80 = Color(0xFFCCC2DC) 13 | val Pink80 = Color(0xFFEFB8C8) 14 | 15 | val Purple40 = Color(0xFF6650a4) 16 | val PurpleGrey40 = Color(0xFF625b71) 17 | val Pink40 = Color(0xFF7D5260) 18 | 19 | object CustomTopBarColors { 20 | var black = false 21 | 22 | @OptIn(ExperimentalMaterial3Api::class) 23 | val topBarColors: TopAppBarColors 24 | @Composable get() { 25 | return if (!black) TopAppBarDefaults.topAppBarColors( 26 | containerColor = colorScheme.surfaceContainer, 27 | scrolledContainerColor = colorScheme.surfaceContainer 28 | ) else TopAppBarDefaults.topAppBarColors() 29 | } 30 | } 31 | 32 | object ColorConstants { 33 | val colorMatrixInvert: FloatArray 34 | @Composable get() = remember { 35 | floatArrayOf( 36 | -1f, 0f, 0f, 0f, 255f, // Red 37 | 0f, -1f, 0f, 0f, 255f, // Green 38 | 0f, 0f, -1f, 0f, 255f, // Blue 39 | 0f, 0f, 0f, 1f, 0f // Alpha 40 | ) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 5 | import androidx.compose.ui.unit.dp 6 | 7 | @ExperimentalMaterial3ExpressiveApi 8 | object ExpressiveListItemShapes { 9 | val topListItemShape = 10 | RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 4.dp, bottomEnd = 4.dp) 11 | val middleListItemShape = RoundedCornerShape(4.dp) 12 | val bottomListItemShape = 13 | RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp, bottomStart = 16.dp, bottomEnd = 16.dp) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.ColorScheme 7 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 8 | import androidx.compose.material3.MaterialExpressiveTheme 9 | import androidx.compose.material3.darkColorScheme 10 | import androidx.compose.material3.dynamicDarkColorScheme 11 | import androidx.compose.material3.dynamicLightColorScheme 12 | import androidx.compose.material3.lightColorScheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.SideEffect 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.luminance 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.platform.LocalView 19 | import androidx.core.view.WindowCompat 20 | import com.materialkolor.rememberDynamicColorScheme 21 | 22 | private val DarkColorScheme = darkColorScheme( 23 | primary = Purple80, 24 | secondary = PurpleGrey80, 25 | tertiary = Pink80 26 | ) 27 | 28 | private val LightColorScheme = lightColorScheme( 29 | primary = Purple40, 30 | secondary = PurpleGrey40, 31 | tertiary = Pink40 32 | 33 | /* Other default colors to override 34 | background = Color(0xFFFFFBFE), 35 | surface = Color(0xFFFFFBFE), 36 | onPrimary = Color.White, 37 | onSecondary = Color.White, 38 | onTertiary = Color.White, 39 | onBackground = Color(0xFF1C1B1F), 40 | onSurface = Color(0xFF1C1B1F), 41 | */ 42 | ) 43 | 44 | @OptIn(ExperimentalMaterial3ExpressiveApi::class) 45 | @Composable 46 | fun WikiReaderTheme( 47 | darkTheme: Boolean = isSystemInDarkTheme(), 48 | // Dynamic color is available on Android 12+ 49 | seedColor: Color = Color.White, 50 | dynamicColor: Boolean = true, 51 | blackTheme: Boolean = false, 52 | content: @Composable () -> Unit 53 | ) { 54 | val colorScheme = when { 55 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 56 | val context = LocalContext.current 57 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 58 | } 59 | 60 | darkTheme -> DarkColorScheme 61 | else -> LightColorScheme 62 | } 63 | val view = LocalView.current 64 | if (!view.isInEditMode) { 65 | SideEffect { 66 | val window = (view.context as Activity).window 67 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme 68 | } 69 | } 70 | CustomTopBarColors.black = blackTheme 71 | 72 | val dynamicColorScheme = rememberDynamicColorScheme( 73 | seedColor = when (seedColor) { 74 | Color.White -> colorScheme.primary 75 | else -> seedColor 76 | }, 77 | isDark = darkTheme, 78 | isAmoled = blackTheme 79 | ) 80 | 81 | MaterialExpressiveTheme( 82 | colorScheme = dynamicColorScheme, 83 | typography = Typography, 84 | content = content 85 | ) 86 | } 87 | 88 | @Composable 89 | fun ColorScheme.isDark() = this.background.luminance() < 0.5 90 | -------------------------------------------------------------------------------- /app/src/main/java/org/nsh07/wikireader/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.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/org/nsh07/wikireader/ui/viewModel/UiState.kt: -------------------------------------------------------------------------------- 1 | package org.nsh07.wikireader.ui.viewModel 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.focus.FocusRequester 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.text.AnnotatedString 7 | import org.nsh07.wikireader.data.FeedApiImage 8 | import org.nsh07.wikireader.data.FeedApiNews 9 | import org.nsh07.wikireader.data.FeedApiOTD 10 | import org.nsh07.wikireader.data.FeedApiTFA 11 | import org.nsh07.wikireader.data.MostReadArticle 12 | import org.nsh07.wikireader.data.SavedStatus 13 | import org.nsh07.wikireader.data.WRStatus 14 | import org.nsh07.wikireader.data.WikiLang 15 | import org.nsh07.wikireader.data.WikiPhoto 16 | import org.nsh07.wikireader.data.WikiPhotoDesc 17 | import org.nsh07.wikireader.data.WikiPrefixSearchResult 18 | import org.nsh07.wikireader.data.WikiSearchResult 19 | import org.nsh07.wikireader.ui.savedArticlesScreen.LanguageFilterOption 20 | 21 | @Immutable 22 | data class AppSearchBarState( 23 | val prefixSearchResults: List? = emptyList(), 24 | val searchResults: List? = emptyList(), 25 | val history: Set = setOf(), 26 | val focusRequester: FocusRequester = FocusRequester() 27 | ) 28 | 29 | @Immutable 30 | data class HomeScreenState( 31 | val title: String = "", 32 | val extract: List> = emptyList(), 33 | val sections: List> = emptyList(), 34 | val photo: WikiPhoto? = null, 35 | val photoDesc: WikiPhotoDesc? = null, 36 | val langs: List? = null, 37 | val currentLang: String? = null, 38 | val pageId: Int? = null, 39 | val status: WRStatus = WRStatus.UNINITIALIZED, 40 | val savedStatus: SavedStatus = SavedStatus.NOT_SAVED, 41 | val isLoading: Boolean = false, 42 | val loadingProgress: Float? = null, 43 | val backStackSize: Int = 0 44 | ) 45 | 46 | @Immutable 47 | data class PreferencesState( 48 | val theme: String = "auto", 49 | val lang: String = "en", 50 | val fontStyle: String = "sans", 51 | val colorScheme: String = Color.White.toString(), 52 | val fontSize: Int = 16, 53 | val blackTheme: Boolean = false, 54 | val dataSaver: Boolean = false, 55 | val feedEnabled: Boolean = true, 56 | val expandedSections: Boolean = false, 57 | val imageBackground: Boolean = false, 58 | val immersiveMode: Boolean = true, 59 | val renderMath: Boolean = true, 60 | val searchHistory: Boolean = true 61 | ) 62 | 63 | @Immutable 64 | data class FeedState( 65 | val tfa: FeedApiTFA? = null, 66 | val mostReadArticles: List? = null, 67 | val image: FeedApiImage? = null, 68 | val news: List? = null, 69 | val onThisDay: List? = null, 70 | val sections: List> = emptyList() 71 | ) 72 | 73 | @Immutable 74 | data class SavedArticlesState( 75 | val isLoading: Boolean = false, 76 | val savedArticles: List = emptyList(), 77 | val languageFilters: List = emptyList(), 78 | val articlesSize: Long = 0L 79 | ) 80 | 81 | enum class FeedSection { 82 | TFA, MOST_READ, IMAGE, NEWS, ON_THIS_DAY 83 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-night-v34/splash_logo.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 16 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/splash_logo.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 16 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v34/splash_logo.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 16 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/brightness_auto.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/code.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/colors.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/contrast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dark_mode.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/data_saver_on.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/delete.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/download.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/download_done.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/expand_all.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/feed.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/filled_download_done.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/filled_home.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/filled_info.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/filled_settings.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/format_size.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/function.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gavel.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/github.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/heart.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/history.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/light_mode.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/menu.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/menu_open.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/more_horiz.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/north_west.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/open_in_browser.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/open_in_full.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_home.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_info.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_settings.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/palette.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 16 | 17 | 20 | 21 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/reset_settings.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/save.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/serif.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_filled.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shuffle.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_logo.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 17 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 40 | 41 | 42 | 43 | 44 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/texture.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/translate.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/update.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/upward.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/wikimedia_logo_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 12 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/wikipedia_s_w.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en -------------------------------------------------------------------------------- /app/src/main/res/values-night-v34/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |