Option to change app language #282

pull/300/head
Koitharu 3 years ago
parent 6f37d95c24
commit 9cb5971182
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -7,7 +7,7 @@
<option name="testRunner" value="GRADLE" /> <option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Embedded JDK" /> <option name="gradleJvm" value="jbr-17" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />

@ -90,6 +90,7 @@ dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' 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.core:core-ktx:1.9.0'
implementation 'androidx.activity:activity-ktx:1.6.1' implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.5' implementation 'androidx.fragment:fragment-ktx:1.5.5'

@ -50,6 +50,7 @@ class KotatsuApp : Application(), Configuration.Provider {
enableStrictMode() enableStrictMode()
} }
AppCompatDelegate.setDefaultNightMode(settings.theme) AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setApplicationLocales(settings.appLocales)
setupActivityLifecycleCallbacks() setupActivityLifecycleCallbacks()
processLifecycleScope.launch(Dispatchers.Default) { processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers() setupDatabaseObservers()

@ -7,6 +7,7 @@ import android.provider.Settings
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -79,6 +80,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getInt(KEY_GRID_SIZE, 100) get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) } 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<String> val readerPageSwitch: Set<String>
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS) 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_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_SHELF_SECTIONS = "shelf_sections_2" const val KEY_SHELF_SECTIONS = "shelf_sections_2"
const val KEY_PREFETCH_CONTENT = "prefetch_content" const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

@ -1,27 +1,38 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.LocaleManagerCompat
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.TwoStatePreference import androidx.preference.TwoStatePreference
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.*
import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.parsers.util.names 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.protect.ProtectSetupActivity
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
import org.koitharu.kotatsu.settings.utils.SliderPreference 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.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.toList
import java.util.Date
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class AppearanceSettingsFragment : class AppearanceSettingsFragment :
@ -52,7 +63,7 @@ class AppearanceSettingsFragment :
entries = entryValues.map { value -> entries = entryValues.map { value ->
val formattedDate = settings.getDateFormat(value.toString()).format(now) val formattedDate = settings.getDateFormat(value.toString()).format(now)
if (value == "") { if (value == "") {
"${context.getString(R.string.system_default)} ($formattedDate)" getString(R.string.default_s, formattedDate)
} else { } else {
formattedDate formattedDate
} }
@ -62,6 +73,20 @@ class AppearanceSettingsFragment :
} }
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP) findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty() ?.isChecked = !settings.appPassword.isNullOrEmpty()
findPreference<ActivityListPreference>(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<ActivityListPreference> {
val locale = AppCompatDelegate.getApplicationLocales().get(0)
locale?.getDisplayName(locale)?.toTitleCase(locale) ?: getString(R.string.automatic)
}
setDefaultValueCompat("")
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -79,16 +104,23 @@ class AppearanceSettingsFragment :
AppSettings.KEY_THEME -> { AppSettings.KEY_THEME -> {
AppCompatDelegate.setDefaultNightMode(settings.theme) AppCompatDelegate.setDefaultNightMode(settings.theme)
} }
AppSettings.KEY_DYNAMIC_THEME -> { AppSettings.KEY_DYNAMIC_THEME -> {
postRestart() postRestart()
} }
AppSettings.KEY_THEME_AMOLED -> { AppSettings.KEY_THEME_AMOLED -> {
postRestart() postRestart()
} }
AppSettings.KEY_APP_PASSWORD -> { AppSettings.KEY_APP_PASSWORD -> {
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP) findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty() ?.isChecked = !settings.appPassword.isNullOrEmpty()
} }
AppSettings.KEY_APP_LOCALE -> {
AppCompatDelegate.setApplicationLocales(settings.appLocales)
}
} }
} }
@ -104,6 +136,7 @@ class AppearanceSettingsFragment :
} }
true true
} }
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
} }
@ -113,4 +146,45 @@ class AppearanceSettingsFragment :
activityRecreationHandle.recreateAll() 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<Locale> {
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)
}
}
}
}
} }

@ -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()
}
}
}

@ -8,6 +8,7 @@ import android.content.OperationApplicationException
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SyncResult import android.content.SyncResult
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.content.res.Resources
import android.database.SQLException import android.database.SQLException
import android.graphics.Color import android.graphics.Color
import android.net.ConnectivityManager import android.net.ConnectivityManager
@ -20,6 +21,7 @@ import android.view.Window
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IntegerRes import androidx.annotation.IntegerRes
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
@ -34,8 +36,12 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okio.IOException import okio.IOException
import org.json.JSONException import org.json.JSONException
import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.InternalResourceHelper import org.koitharu.kotatsu.utils.InternalResourceHelper
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import kotlin.math.roundToLong import kotlin.math.roundToLong
val Context.activityManager: ActivityManager? val Context.activityManager: ActivityManager?
@ -146,3 +152,23 @@ fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.make
view.width, view.width,
view.height, 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())
}

@ -401,4 +401,5 @@
<string name="source_disabled">Source disabled</string> <string name="source_disabled">Source disabled</string>
<string name="prefetch_content">Content preloading</string> <string name="prefetch_content">Content preloading</string>
<string name="mark_as_current">Mark as current</string> <string name="mark_as_current">Mark as current</string>
<string name="language">Language</string>
</resources> </resources>

@ -14,16 +14,16 @@
<locale android:name="in" /> <locale android:name="in" />
<locale android:name="it" /> <locale android:name="it" />
<locale android:name="ja" /> <locale android:name="ja" />
<locale android:name="nb-rNO" /> <locale android:name="nb-NO" />
<locale android:name="pl" /> <locale android:name="pl" />
<locale android:name="pt" /> <locale android:name="pt" />
<locale android:name="pt-rBR" /> <locale android:name="pt-BR" />
<locale android:name="ru" /> <locale android:name="ru" />
<locale android:name="si" /> <locale android:name="si" />
<locale android:name="sr" /> <locale android:name="sr" />
<locale android:name="sv" /> <locale android:name="sv" />
<locale android:name="tr" /> <locale android:name="tr" />
<locale android:name="uk" /> <locale android:name="uk" />
<locale android:name="zh-rCN" /> <locale android:name="zh-CN" />
<locale android:name="zh-rTW" /> <locale android:name="zh-TW" />
</locale-config> </locale-config>

@ -24,6 +24,10 @@
android:summary="@string/black_dark_theme_summary" android:summary="@string/black_dark_theme_summary"
android:title="@string/black_dark_theme" /> android:title="@string/black_dark_theme" />
<org.koitharu.kotatsu.settings.utils.ActivityListPreference
android:key="app_locale"
android:title="@string/language" />
<ListPreference <ListPreference
android:key="date_format" android:key="date_format"
android:title="@string/date_format" /> android:title="@string/date_format" />
@ -56,4 +60,4 @@
android:summary="@string/protect_application_summary" android:summary="@string/protect_application_summary"
android:title="@string/protect_application" /> android:title="@string/protect_application" />
</PreferenceScreen> </PreferenceScreen>

Loading…
Cancel
Save