diff --git a/.idea/gradle.xml b/.idea/gradle.xml index a0de2a152..ae388c2a5 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -7,7 +7,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 9657b7c14..6621f9067 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,6 +90,7 @@ dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + implementation "androidx.appcompat:appcompat:1.6.0" implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.activity:activity-ktx:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.5.5' diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index d80e4ad21..fdae94568 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -50,6 +50,7 @@ class KotatsuApp : Application(), Configuration.Provider { enableStrictMode() } AppCompatDelegate.setDefaultNightMode(settings.theme) + AppCompatDelegate.setApplicationLocales(settings.appLocales) setupActivityLifecycleCallbacks() processLifecycleScope.launch(Dispatchers.Default) { setupDatabaseObservers() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 86e2aa20d..396d9157f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -7,6 +7,7 @@ import android.provider.Settings import androidx.appcompat.app.AppCompatDelegate import androidx.collection.arraySetOf import androidx.core.content.edit +import androidx.core.os.LocaleListCompat import androidx.preference.PreferenceManager import com.google.android.material.color.DynamicColors import dagger.hilt.android.qualifiers.ApplicationContext @@ -79,6 +80,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getInt(KEY_GRID_SIZE, 100) set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) } + var appLocales: LocaleListCompat + get() { + val raw = prefs.getString(KEY_APP_LOCALE, null) + return LocaleListCompat.forLanguageTags(raw) + } + set(value) { + prefs.edit { + putString(KEY_APP_LOCALE, value.toLanguageTags()) + } + } + val readerPageSwitch: Set get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS) @@ -358,6 +370,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_WEBTOON_ZOOM = "webtoon_zoom" const val KEY_SHELF_SECTIONS = "shelf_sections_2" const val KEY_PREFETCH_CONTENT = "prefetch_content" + const val KEY_APP_LOCALE = "app_locale" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt index e71bb1576..c4103e685 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppearanceSettingsFragment.kt @@ -1,27 +1,38 @@ package org.koitharu.kotatsu.settings +import android.content.Context import android.content.Intent import android.content.SharedPreferences +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.Settings import android.view.View import androidx.appcompat.app.AppCompatDelegate +import androidx.core.app.LocaleManagerCompat import androidx.core.view.postDelayed import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.TwoStatePreference import com.google.android.material.color.DynamicColors import dagger.hilt.android.AndroidEntryPoint -import java.util.* -import javax.inject.Inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.parsers.util.names +import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity +import org.koitharu.kotatsu.settings.utils.ActivityListPreference import org.koitharu.kotatsu.settings.utils.SliderPreference +import org.koitharu.kotatsu.utils.ext.getLocalesConfig +import org.koitharu.kotatsu.utils.ext.map import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat +import org.koitharu.kotatsu.utils.ext.toList +import java.util.Date +import java.util.Locale +import javax.inject.Inject @AndroidEntryPoint class AppearanceSettingsFragment : @@ -52,7 +63,7 @@ class AppearanceSettingsFragment : entries = entryValues.map { value -> val formattedDate = settings.getDateFormat(value.toString()).format(now) if (value == "") { - "${context.getString(R.string.system_default)} ($formattedDate)" + getString(R.string.default_s, formattedDate) } else { formattedDate } @@ -62,6 +73,20 @@ class AppearanceSettingsFragment : } findPreference(AppSettings.KEY_PROTECT_APP) ?.isChecked = !settings.appPassword.isNullOrEmpty() + findPreference(AppSettings.KEY_APP_LOCALE)?.run { + initLocalePicker(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + activityIntent = Intent( + Settings.ACTION_APP_LOCALE_SETTINGS, + Uri.fromParts("package", context.packageName, null), + ) + } + summaryProvider = Preference.SummaryProvider { + val locale = AppCompatDelegate.getApplicationLocales().get(0) + locale?.getDisplayName(locale)?.toTitleCase(locale) ?: getString(R.string.automatic) + } + setDefaultValueCompat("") + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -79,16 +104,23 @@ class AppearanceSettingsFragment : AppSettings.KEY_THEME -> { AppCompatDelegate.setDefaultNightMode(settings.theme) } + AppSettings.KEY_DYNAMIC_THEME -> { postRestart() } + AppSettings.KEY_THEME_AMOLED -> { postRestart() } + AppSettings.KEY_APP_PASSWORD -> { findPreference(AppSettings.KEY_PROTECT_APP) ?.isChecked = !settings.appPassword.isNullOrEmpty() } + + AppSettings.KEY_APP_LOCALE -> { + AppCompatDelegate.setApplicationLocales(settings.appLocales) + } } } @@ -104,6 +136,7 @@ class AppearanceSettingsFragment : } true } + else -> super.onPreferenceTreeClick(preference) } } @@ -113,4 +146,45 @@ class AppearanceSettingsFragment : activityRecreationHandle.recreateAll() } } + + private fun initLocalePicker(preference: ListPreference) { + val locales = resources.getLocalesConfig() + .toList() + .sortedWith(LocaleComparator(preference.context)) + preference.entries = Array(locales.size + 1) { i -> + if (i == 0) { + getString(R.string.automatic) + } else { + val lc = locales[i - 1] + lc.getDisplayName(lc).toTitleCase(lc) + } + } + preference.entryValues = Array(locales.size + 1) { i -> + if (i == 0) { + "" + } else { + locales[i - 1].toLanguageTag() + } + } + } + + private class LocaleComparator(context: Context) : Comparator { + + private val deviceLocales = LocaleManagerCompat.getSystemLocales(context) + .map { it.language } + + override fun compare(a: Locale, b: Locale): Int { + return if (a === b) { + 0 + } else { + val indexA = deviceLocales.indexOf(a.language) + val indexB = deviceLocales.indexOf(b.language) + if (indexA == -1 && indexB == -1) { + compareValues(a.language, b.language) + } else { + -2 - (indexA - indexB) + } + } + } + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt b/app/src/main/java/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt new file mode 100644 index 000000000..0de795a3b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/utils/ActivityListPreference.kt @@ -0,0 +1,38 @@ +package org.koitharu.kotatsu.settings.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.util.AttributeSet +import androidx.preference.ListPreference +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug + +class ActivityListPreference : ListPreference { + + var activityIntent: Intent? = null + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + override fun onClick() { + val intent = activityIntent + if (intent == null) { + super.onClick() + return + } + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + e.printStackTraceDebug() + super.onClick() + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index c5e8a34a8..251604b12 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -8,6 +8,7 @@ import android.content.OperationApplicationException import android.content.SharedPreferences import android.content.SyncResult import android.content.pm.ResolveInfo +import android.content.res.Resources import android.database.SQLException import android.graphics.Color import android.net.ConnectivityManager @@ -20,6 +21,7 @@ import android.view.Window import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IntegerRes import androidx.core.app.ActivityOptionsCompat +import androidx.core.os.LocaleListCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import androidx.work.CoroutineWorker @@ -34,8 +36,12 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import okio.IOException import org.json.JSONException +import org.jsoup.internal.StringUtil.StringJoiner import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.utils.InternalResourceHelper +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException import kotlin.math.roundToLong val Context.activityManager: ActivityManager? @@ -146,3 +152,23 @@ fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.make view.width, view.height, ) + +fun Resources.getLocalesConfig(): LocaleListCompat { + val tagsList = StringJoiner(",") + try { + val xpp: XmlPullParser = getXml(R.xml.locales) + while (xpp.eventType != XmlPullParser.END_DOCUMENT) { + if (xpp.eventType == XmlPullParser.START_TAG) { + if (xpp.name == "locale") { + tagsList.add(xpp.getAttributeValue(0)) + } + } + xpp.next() + } + } catch (e: XmlPullParserException) { + e.printStackTraceDebug() + } catch (e: IOException) { + e.printStackTraceDebug() + } + return LocaleListCompat.forLanguageTags(tagsList.complete()) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 99e8a3d69..6ff653519 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -401,4 +401,5 @@ Source disabled Content preloading Mark as current + Language diff --git a/app/src/main/res/xml/locales.xml b/app/src/main/res/xml/locales.xml index 0e10b2cc8..3f6f313ef 100644 --- a/app/src/main/res/xml/locales.xml +++ b/app/src/main/res/xml/locales.xml @@ -14,16 +14,16 @@ - + - + - - + + diff --git a/app/src/main/res/xml/pref_appearance.xml b/app/src/main/res/xml/pref_appearance.xml index 0f472b72e..29c7fcbd7 100644 --- a/app/src/main/res/xml/pref_appearance.xml +++ b/app/src/main/res/xml/pref_appearance.xml @@ -24,6 +24,10 @@ android:summary="@string/black_dark_theme_summary" android:title="@string/black_dark_theme" /> + + @@ -56,4 +60,4 @@ android:summary="@string/protect_application_summary" android:title="@string/protect_application" /> - \ No newline at end of file +