Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,24 @@
plugins {
id(ThunderbirdPlugins.Library.android)
}
dependencies {
implementation(projects.legacy.core)
api(projects.core.ui.theme.manager)
api(libs.androidx.appcompat)
api(libs.androidx.activity)
api(libs.android.material)
api(libs.androidx.navigation.fragment)
api(libs.androidx.navigation.ui)
api(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.biometric)
implementation(libs.kotlinx.coroutines.core)
}
android {
namespace = "com.fsck.k9.ui.base"
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<!--
This component is disabled by default. It will be enabled programmatically by SystemLocaleManager if necessary.
-->
<receiver
android:name=".locale.LocaleBroadcastReceiver"
android:exported="false"
android:enabled="false"
>
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,99 @@
package com.fsck.k9.ui.base
import android.content.res.Resources
import com.fsck.k9.ui.base.extensions.currentLocale
import com.fsck.k9.ui.base.locale.SystemLocaleChangeListener
import com.fsck.k9.ui.base.locale.SystemLocaleManager
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import net.thunderbird.core.preference.display.coreSettings.DisplayCoreSettingsPreferenceManager
import net.thunderbird.core.preference.update
/**
* Manages app language changes.
*
* - Sets the default locale when the app language is changed.
* - Notifies listeners when the app language has changed.
*/
class AppLanguageManager(
private val systemLocaleManager: SystemLocaleManager,
private val coroutineScope: CoroutineScope = MainScope(),
private val displayCoreSettingsPreferenceManager: DisplayCoreSettingsPreferenceManager,
) {
private var currentOverrideLocale: Locale? = null
private val _overrideLocale = MutableSharedFlow<Locale?>(replay = 1)
private val _appLocale = MutableSharedFlow<Locale>(replay = 1)
val overrideLocale: Flow<Locale?> = _overrideLocale
val appLocale: Flow<Locale> = _appLocale
private val systemLocaleListener = SystemLocaleChangeListener {
coroutineScope.launch {
_appLocale.emit(systemLocale)
}
}
fun init() {
setLocale(getAppLanguage())
}
fun getOverrideLocale(): Locale? = currentOverrideLocale
fun getAppLanguage(): String {
return displayCoreSettingsPreferenceManager.getConfig().appLanguage
}
fun setAppLanguage(appLanguage: String) {
if (appLanguage == getAppLanguage()) {
return
}
displayCoreSettingsPreferenceManager.update { displayCoreSettings ->
displayCoreSettings.copy(appLanguage = appLanguage)
}
setLocale(appLanguage)
}
fun applyOverrideLocale() {
currentOverrideLocale?.let { overrideLocale ->
Locale.setDefault(overrideLocale)
}
}
private fun setLocale(appLanguage: String) {
val overrideLocale = getOverrideLocaleForLanguage(appLanguage)
currentOverrideLocale = overrideLocale
val locale = overrideLocale ?: systemLocale
Locale.setDefault(locale)
if (overrideLocale == null) {
systemLocaleManager.addListener(systemLocaleListener)
} else {
systemLocaleManager.removeListener(systemLocaleListener)
}
coroutineScope.launch {
_overrideLocale.emit(overrideLocale)
_appLocale.emit(locale)
}
}
private fun getOverrideLocaleForLanguage(appLanguage: String): Locale? {
return if (appLanguage.isEmpty()) {
null
} else if (appLanguage.length == 5 && appLanguage[2] == '_') {
// language is in the form: en_US
val language = appLanguage.substring(0, 2)
val country = appLanguage.substring(3)
Locale(language, country)
} else {
Locale(appLanguage)
}
}
private val systemLocale get() = Resources.getSystem().configuration.currentLocale
}

View file

@ -0,0 +1,118 @@
package com.fsck.k9.ui.base
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat.Type.displayCutout
import androidx.core.view.WindowInsetsCompat.Type.navigationBars
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.lifecycle.asLiveData
import com.fsck.k9.controller.push.PushController
import java.util.Locale
import net.thunderbird.core.ui.theme.manager.ThemeManager
import org.koin.android.ext.android.inject
abstract class K9Activity(private val themeType: ThemeType) : AppCompatActivity() {
constructor() : this(ThemeType.DEFAULT)
private val pushController: PushController by inject()
protected val themeManager: ThemeManager by inject()
private val appLanguageManager: AppLanguageManager by inject()
private var overrideLocaleOnLaunch: Locale? = null
override fun attachBaseContext(baseContext: Context) {
overrideLocaleOnLaunch = appLanguageManager.getOverrideLocale()
val newBaseContext = overrideLocaleOnLaunch?.let { locale ->
LocaleContextWrapper(baseContext, locale)
} ?: baseContext
super.attachBaseContext(newBaseContext)
}
override fun onCreate(savedInstanceState: Bundle?) {
initializeTheme()
initializePushController()
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setLayoutDirection()
listenForAppLanguageChanges()
}
// On Android 12+ the layout direction doesn't seem to be updated when recreating the activity. This is a problem
// when switching from an LTR to an RTL language (or the other way around) using the language picker in the app.
private fun setLayoutDirection() {
if (Build.VERSION.SDK_INT >= 31) {
window.decorView.layoutDirection = resources.configuration.layoutDirection
}
}
private fun listenForAppLanguageChanges() {
appLanguageManager.overrideLocale.asLiveData().observe(this) { overrideLocale ->
if (overrideLocale != overrideLocaleOnLaunch) {
recreateCompat()
}
}
}
private fun initializeTheme() {
val theme = when (themeType) {
ThemeType.DEFAULT -> themeManager.appThemeResourceId
ThemeType.DIALOG -> themeManager.translucentDialogThemeResourceId
}
setTheme(theme)
}
private fun initializePushController() {
pushController.init()
}
protected fun setLayout(@LayoutRes layoutResId: Int) {
setContentView(layoutResId)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
?: error("K9 layouts must provide a toolbar with id='toolbar'.")
setSupportActionBar(toolbar)
ViewCompat.setOnApplyWindowInsetsListener(toolbar) { v, windowsInsets ->
val insets = windowsInsets.getInsets(systemBars() or displayCutout())
v.setPadding(insets.left, insets.top, insets.right, 0)
windowsInsets
}
}
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
val newView = super.onCreateView(parent, name, context, attrs)
if (newView != null) initializeInsets(newView)
return newView
}
private fun initializeInsets(view: View) {
ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowsInsets ->
val insets = windowsInsets.getInsets(displayCutout() or navigationBars())
v.setPadding(insets.left, 0, insets.right, insets.bottom)
windowsInsets
}
}
protected fun recreateCompat() {
ActivityCompat.recreate(this)
}
}
enum class ThemeType {
DEFAULT,
DIALOG,
}

View file

@ -0,0 +1,19 @@
package com.fsck.k9.ui.base
import com.fsck.k9.ui.base.locale.SystemLocaleManager
import net.thunderbird.core.ui.theme.manager.ThemeManager
import org.koin.core.qualifier.named
import org.koin.dsl.module
val uiBaseModule = module {
single {
ThemeManager(
context = get(),
themeProvider = get(),
generalSettingsManager = get(),
appCoroutineScope = get(named("AppCoroutineScope")),
)
}
single { AppLanguageManager(systemLocaleManager = get(), displayCoreSettingsPreferenceManager = get()) }
single { SystemLocaleManager(context = get()) }
}

View file

@ -0,0 +1,17 @@
package com.fsck.k9.ui.base
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import com.fsck.k9.ui.base.extensions.currentLocale
import java.util.Locale
/**
* In combination with `AppCompatActivity` this will override the locale in the configuration.
*/
internal class LocaleContextWrapper(baseContext: Context, private val locale: Locale) : ContextWrapper(baseContext) {
override fun createConfigurationContext(overrideConfiguration: Configuration): Context {
overrideConfiguration.currentLocale = locale
return super.createConfigurationContext(overrideConfiguration)
}
}

View file

@ -0,0 +1,17 @@
package com.fsck.k9.ui.base.bundle
import android.os.Bundle
fun <T : Enum<T>> Bundle.putEnum(key: String, value: T) {
putString(key, value.name)
}
inline fun <reified T : Enum<T>> Bundle.getEnum(key: String, defaultValue: T): T {
val value = getString(key) ?: return defaultValue
return enumValueOf(value)
}
inline fun <reified T : Enum<T>> Bundle.getEnum(key: String): T {
val value = getString(key) ?: error("Missing enum value for key '$key'")
return enumValueOf(value)
}

View file

@ -0,0 +1,41 @@
package com.fsck.k9.ui.base.extensions
import android.content.res.Configuration
import android.os.Build
import android.os.LocaleList
import androidx.annotation.RequiresApi
import java.util.Locale
@Suppress("DEPRECATION")
var Configuration.currentLocale: Locale
get() {
return if (Build.VERSION.SDK_INT >= 24) {
locales[0]
} else {
locale
}
}
set(value) {
if (Build.VERSION.SDK_INT >= 24) {
setLocales(createLocaleList(value, locales))
} else {
setLocale(value)
}
}
@RequiresApi(24)
private fun createLocaleList(topLocale: Locale, otherLocales: LocaleList): LocaleList {
if (!otherLocales.isEmpty && otherLocales[0] == topLocale) {
return otherLocales
}
val locales = mutableListOf(topLocale)
for (index in 0 until otherLocales.size()) {
val currentLocale = otherLocales[index]
if (currentLocale != topLocale) {
locales.add(currentLocale)
}
}
return LocaleList(*locales.toTypedArray())
}

View file

@ -0,0 +1,27 @@
package com.fsck.k9.ui.base.extensions
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentTransaction
inline fun FragmentActivity.fragmentTransaction(crossinline block: FragmentTransaction.() -> Unit) {
with(supportFragmentManager.beginTransaction()) {
block()
commit()
}
}
inline fun FragmentActivity.fragmentTransactionWithBackStack(
name: String? = null,
crossinline block: FragmentTransaction.() -> Unit,
) {
fragmentTransaction {
block()
addToBackStack(name)
}
}
fun <T : Fragment> T.withArguments(vararg argumentPairs: Pair<String, Any?>) = apply {
arguments = bundleOf(*argumentPairs)
}

View file

@ -0,0 +1,11 @@
package com.fsck.k9.ui.base.extensions
import androidx.annotation.IdRes
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
fun FragmentActivity.findNavController(@IdRes containerIdRes: Int): NavController {
val navHostFragment = supportFragmentManager.findFragmentById(containerIdRes) as NavHostFragment
return navHostFragment.navController
}

View file

@ -0,0 +1,8 @@
package com.fsck.k9.ui.base.livedata
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, observer: (T) -> Unit) {
this.observe(owner) { observer(it!!) }
}

View file

@ -0,0 +1,56 @@
package com.fsck.k9.ui.base.loader
import androidx.lifecycle.LiveData
import androidx.lifecycle.liveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.thunderbird.core.logging.legacy.Log
const val LOADING_INDICATOR_DELAY = 500L
/**
* Load data in an I/O thread. Updates the returned [LiveData] with the current loading state.
*
* If loading takes longer than [LOADING_INDICATOR_DELAY] the [LoaderState.Loading] state will be emitted so the UI can
* display a loading indicator. We use a delay so fast loads won't flash a loading indicator.
* If an exception is thrown during loading the [LoaderState.Error] state is emitted.
* If the data was loaded successfully [LoaderState.Data] will be emitted containing the data.
*/
fun <T> liveDataLoader(block: CoroutineScope.() -> T): LiveData<LoaderState<T>> = liveData {
coroutineScope {
val job = launch {
delay(LOADING_INDICATOR_DELAY)
// Emit loading state if loading took longer than configured delay. If the data was loaded faster than that,
// this coroutine will have been canceled before the next line is executed.
emit(LoaderState.Loading)
}
val finalState = try {
val data = withContext(Dispatchers.IO) {
block()
}
LoaderState.Data(data)
} catch (e: Exception) {
Log.e(e, "Error loading data")
LoaderState.Error
}
// Cancel job that emits Loading state
job.cancelAndJoin()
emit(finalState)
}
}
sealed class LoaderState<out T> {
object Loading : LoaderState<Nothing>()
object Error : LoaderState<Nothing>()
class Data<T>(val data: T) : LoaderState<T>()
}

View file

@ -0,0 +1,50 @@
package com.fsck.k9.ui.base.loader
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
/**
* Use to observe the [LiveData] returned by [liveDataLoader].
*
* Works with separate views for the [LoaderState.Loading], [LoaderState.Error], and [LoaderState.Data] states. The
* view associated with the current state is made visible and the others are hidden. For the [LoaderState.Data] state
* the [displayData] function is also called.
*/
fun <T> LiveData<LoaderState<T>>.observeLoading(
owner: LifecycleOwner,
loadingView: View,
errorView: View,
dataView: View,
displayData: (T) -> Unit,
) {
observe(owner, LoaderStateObserver(loadingView, errorView, dataView, displayData))
}
private class LoaderStateObserver<T>(
private val loadingView: View,
private val errorView: View,
private val dataView: View,
private val displayData: (T) -> Unit,
) : Observer<LoaderState<T>> {
private val allViews = setOf(loadingView, errorView, dataView)
override fun onChanged(value: LoaderState<T>) {
when (value) {
is LoaderState.Loading -> loadingView.show()
is LoaderState.Error -> errorView.show()
is LoaderState.Data -> {
dataView.show()
displayData(value.data)
}
}
}
private fun View.show() {
for (view in allViews) {
view.isVisible = view === this
}
}
}

View file

@ -0,0 +1,17 @@
package com.fsck.k9.ui.base.locale
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class LocaleBroadcastReceiver : BroadcastReceiver(), KoinComponent {
private val systemLocaleManager: SystemLocaleManager by inject()
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == Intent.ACTION_LOCALE_CHANGED) {
systemLocaleManager.notifyListeners()
}
}
}

View file

@ -0,0 +1,68 @@
package com.fsck.k9.ui.base.locale
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import java.util.concurrent.CopyOnWriteArraySet
import net.thunderbird.core.logging.legacy.Log
class SystemLocaleManager(context: Context) {
private val packageManager = context.packageManager
private val componentName = ComponentName(context, LocaleBroadcastReceiver::class.java)
private val listeners = CopyOnWriteArraySet<SystemLocaleChangeListener>()
@Synchronized
fun addListener(listener: SystemLocaleChangeListener) {
if (listeners.isEmpty()) {
enableReceiver()
}
listeners.add(listener)
}
@Synchronized
fun removeListener(listener: SystemLocaleChangeListener) {
listeners.remove(listener)
if (listeners.isEmpty()) {
disableReceiver()
}
}
internal fun notifyListeners() {
for (listener in listeners) {
listener.onSystemLocaleChanged()
}
}
private fun enableReceiver() {
Log.v("Enable LocaleBroadcastReceiver")
try {
packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP,
)
} catch (e: Exception) {
Log.e(e, "Error enabling LocaleBroadcastReceiver")
}
}
private fun disableReceiver() {
Log.v("Disable LocaleBroadcastReceiver")
try {
packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP,
)
} catch (e: Exception) {
Log.e(e, "Error disabling LocaleBroadcastReceiver")
}
}
}
fun interface SystemLocaleChangeListener {
fun onSystemLocaleChanged()
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Don't use a this layout when using a subtitle. See https://issuetracker.google.com/issues/135865267 -->
<com.google.android.material.appbar.MaterialToolbar
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">حسنًا</string>
<string name="cancel_action">إلغ</string>
<string name="star_button_description">نجمة</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cancel_action">Encaboxar</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Oldu</string>
<string name="cancel_action">Ləğv et</string>
<string name="star_button_description">Ulduz</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Добра</string>
<string name="cancel_action">Скасаваць</string>
<string name="star_button_description">Выбранае</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">ОК</string>
<string name="cancel_action">Отказ</string>
<string name="star_button_description">Звезда</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="okay_action">আচ্ছা</string>
<string name="cancel_action">ঠিকাছে</string>
<string name="star_button_description">তারা</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Nullañ</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="okay_action">U redu</string>
<string name="cancel_action">Otkaži</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">D\'acord</string>
<string name="cancel_action">Cancel·la</string>
<string name="star_button_description">Destaca</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="okay_action">Vai</string>
<string name="cancel_action">Abbandunà</string>
<string name="star_button_description">Aghjunghje una stella</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Zrušit</string>
<string name="star_button_description">Hvězdička</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Iawn</string>
<string name="cancel_action">Diddymu</string>
<string name="star_button_description">Enw hygyrch ar gyfer y botwm seren</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">O.k.</string>
<string name="cancel_action">Annullér</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Abbrechen</string>
<string name="star_button_description">Wichtigkeit</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Ακύρωση</string>
<string name="star_button_description">Αστέρι</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="okay_action">OK</string>
<string name="cancel_action">Cancel</string>
<string name="star_button_description">Star</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Bone</string>
<string name="cancel_action">Nuligi</string>
<string name="star_button_description">Steleto</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Aceptar</string>
<string name="cancel_action">Cancelar</string>
<string name="star_button_description">Destacar</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Ok</string>
<string name="cancel_action">Tühista</string>
<string name="star_button_description">Märgi tähega</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Ados</string>
<string name="cancel_action">Utzi</string>
<string name="star_button_description">Nabarmendu</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">خب</string>
<string name="cancel_action">لغو</string>
<string name="star_button_description">ستاره</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Peruuta</string>
<string name="star_button_description">Tähti</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Daccord</string>
<string name="cancel_action">Annuler</string>
<string name="star_button_description">Ajouter une étoile</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Annulearje</string>
<string name="star_button_description">Stjer</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="star_button_description">Réalta</string>
<string name="okay_action">Ceart go leor</string>
<string name="cancel_action">Cealaigh</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Ceart ma-thà</string>
<string name="cancel_action">Sguir dheth</string>
<string name="star_button_description">Rionnag</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Aceptar</string>
<string name="cancel_action">Cancelar</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="okay_action">ठीक है</string>
<string name="cancel_action">कैंसिल करें</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">U redu</string>
<string name="cancel_action">Otkaži</string>
<string name="star_button_description">Označi zvjezdicom</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Mégse</string>
<string name="star_button_description">Csillag</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Լաւ</string>
<string name="cancel_action">Չեղարկել</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Oke</string>
<string name="cancel_action">Batalkan</string>
<string name="star_button_description">Bintang</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Í lagi</string>
<string name="cancel_action">Hætta við</string>
<string name="star_button_description">Stjörnumerkja</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Annulla</string>
<string name="star_button_description">Stella</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">אישור</string>
<string name="cancel_action">ביטול</string>
<string name="star_button_description">כוכב</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">キャンセル</string>
<string name="star_button_description">スターを付ける</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">ᲝᲙ</string>
<string name="cancel_action">გაუქმება</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cancel_action">Sefsex</string>
<string name="star_button_description">Rnu itri</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="okay_action">OK</string>
<string name="cancel_action">Бас тарту</string>
<string name="star_button_description">Жұлдызша</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">확인</string>
<string name="cancel_action">취소</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Gerai</string>
<string name="cancel_action">Atsisakyti</string>
<string name="star_button_description">Pažymėti žvaigždute</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Piekrist</string>
<string name="cancel_action">Atcelt</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">ശരി</string>
<string name="cancel_action">റദ്ദാക്കുക</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Avbryt</string>
<string name="star_button_description">Stjernemerk</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Annuleren</string>
<string name="star_button_description">Ster</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Greitt</string>
<string name="cancel_action">Avbryt</string>
<string name="star_button_description">Stjerne</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Anuluj</string>
<string name="star_button_description">Gwiazdka</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Cancelar</string>
<string name="star_button_description">Estrela</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Cancelar</string>
<string name="star_button_description">Estrela</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Renunță</string>
<string name="star_button_description">Stea</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">ОК</string>
<string name="cancel_action">Отмена</string>
<string name="star_button_description">Избранное</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Zrušiť</string>
<string name="star_button_description">Hviezdička</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">V redu</string>
<string name="cancel_action">Prekliči</string>
<string name="star_button_description">Zvezdica</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Anuloje</string>
<string name="star_button_description">Yll</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">У реду</string>
<string name="cancel_action">Одустани</string>
<string name="star_button_description">Звездица</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Avbryt</string>
<string name="star_button_description">Stjärna</string>
</resources>

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">சரி</string>
<string name="cancel_action">வேண்டாம்</string>
<string name="star_button_description">விண்மீன்</string>
</resources>

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Tamam</string>
<string name="cancel_action">İptal</string>
<string name="star_button_description">Yıldız</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">Гаразд</string>
<string name="cancel_action">Скасувати</string>
<string name="star_button_description">Позначити зірочкою</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">OK</string>
<string name="cancel_action">Hủy bỏ</string>
<string name="star_button_description">Sao</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">确定</string>
<string name="cancel_action">取消</string>
<string name="star_button_description">星标</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--Used to confirm acceptance of dialog boxes, warnings, errors, etc.-->
<string name="okay_action">確定</string>
<string name="cancel_action">取消</string>
<string name="star_button_description">加上星號</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="toolbarTitleMarginVertical">8dp</dimen>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Used to confirm acceptance of dialog boxes, warnings, errors, etc. -->
<string name="okay_action">OK</string>
<!-- Text of the cancel button in dialogs -->
<string name="cancel_action">Cancel</string>
<!-- Accessible name for the star button -->
<string name="star_button_description">Star</string>
</resources>

View file

@ -0,0 +1,19 @@
plugins {
id(ThunderbirdPlugins.Library.android)
}
android {
namespace = "app.k9mail.legacy.ui.folder"
}
dependencies {
implementation(projects.core.ui.legacy.designsystem)
implementation(projects.core.android.account)
implementation(projects.legacy.mailstore)
implementation(projects.legacy.message)
implementation(projects.feature.mail.account.api)
implementation(projects.feature.mail.folder.api)
implementation(libs.androidx.lifecycle.livedata.ktx)
}

View file

@ -0,0 +1,114 @@
package app.k9mail.legacy.ui.folder
import app.k9mail.legacy.mailstore.FolderSettingsChangedListener
import app.k9mail.legacy.mailstore.FolderTypeMapper
import app.k9mail.legacy.mailstore.MessageStoreManager
import app.k9mail.legacy.message.controller.MessagingControllerRegistry
import app.k9mail.legacy.message.controller.SimpleMessagingListener
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import net.thunderbird.core.android.account.AccountManager
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.feature.mail.folder.api.Folder
import net.thunderbird.feature.mail.folder.api.FolderType
import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
import com.fsck.k9.mail.FolderType as LegacyFolderType
class DefaultDisplayFolderRepository(
private val accountManager: AccountManager,
private val messagingController: MessagingControllerRegistry,
private val messageStoreManager: MessageStoreManager,
private val outboxFolderManager: OutboxFolderManager,
private val coroutineContext: CoroutineContext = Dispatchers.IO,
) : DisplayFolderRepository {
private val sortForDisplay =
compareByDescending<DisplayFolder> { it.folder.type == FolderType.INBOX }
.thenByDescending { it.folder.type == FolderType.OUTBOX }
.thenByDescending { it.folder.type != FolderType.REGULAR }
.thenByDescending { it.isInTopGroup }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.folder.name }
private fun getDisplayFolders(
account: LegacyAccount,
outboxFolderId: Long,
includeHiddenFolders: Boolean,
): List<DisplayFolder> {
val messageStore = messageStoreManager.getMessageStore(account.uuid)
return messageStore.getDisplayFolders(
includeHiddenFolders = includeHiddenFolders,
outboxFolderId = outboxFolderId,
) { folder ->
DisplayFolder(
folder = Folder(
id = folder.id,
name = folder.name,
type = folder.takeIf { it.id == outboxFolderId }?.type?.toFolderType()
?: FolderTypeMapper.folderTypeOf(account, folder.id),
isLocalOnly = folder.isLocalOnly,
),
isInTopGroup = folder.isInTopGroup,
unreadMessageCount = folder.unreadMessageCount,
starredMessageCount = folder.starredMessageCount,
pathDelimiter = account.folderPathDelimiter,
)
}.sortedWith(sortForDisplay)
}
override fun getDisplayFoldersFlow(
account: LegacyAccount,
includeHiddenFolders: Boolean,
): Flow<List<DisplayFolder>> {
val messageStore = messageStoreManager.getMessageStore(account.uuid)
return callbackFlow {
val outboxFolderId = outboxFolderManager.getOutboxFolderId(account.id)
send(getDisplayFolders(account, outboxFolderId, includeHiddenFolders))
val folderStatusChangedListener = object : SimpleMessagingListener() {
override fun folderStatusChanged(statusChangedAccount: LegacyAccount, folderId: Long) {
if (statusChangedAccount.uuid == account.uuid) {
trySendBlocking(getDisplayFolders(account, outboxFolderId, includeHiddenFolders))
}
}
}
messagingController.addListener(folderStatusChangedListener)
val folderSettingsChangedListener = FolderSettingsChangedListener {
trySendBlocking(getDisplayFolders(account, outboxFolderId, includeHiddenFolders))
}
messageStore.addFolderSettingsChangedListener(folderSettingsChangedListener)
awaitClose {
messagingController.removeListener(folderStatusChangedListener)
messageStore.removeFolderSettingsChangedListener(folderSettingsChangedListener)
}
}.buffer(capacity = Channel.CONFLATED)
.distinctUntilChanged()
.flowOn(coroutineContext)
}
override fun getDisplayFoldersFlow(accountUuid: String): Flow<List<DisplayFolder>> {
val account = accountManager.getAccount(accountUuid) ?: error("Account not found: $accountUuid")
return getDisplayFoldersFlow(account, includeHiddenFolders = false)
}
private fun LegacyFolderType.toFolderType(): FolderType =
when (this) {
LegacyFolderType.REGULAR -> FolderType.REGULAR
LegacyFolderType.INBOX -> FolderType.INBOX
LegacyFolderType.OUTBOX -> FolderType.OUTBOX
LegacyFolderType.DRAFTS -> FolderType.DRAFTS
LegacyFolderType.SENT -> FolderType.SENT
LegacyFolderType.TRASH -> FolderType.TRASH
LegacyFolderType.SPAM -> FolderType.SPAM
LegacyFolderType.ARCHIVE -> FolderType.ARCHIVE
}
}

View file

@ -0,0 +1,12 @@
package app.k9mail.legacy.ui.folder
import net.thunderbird.feature.mail.folder.api.Folder
import net.thunderbird.feature.mail.folder.api.FolderPathDelimiter
data class DisplayFolder(
val folder: Folder,
val isInTopGroup: Boolean,
val unreadMessageCount: Int,
val starredMessageCount: Int,
val pathDelimiter: FolderPathDelimiter,
)

View file

@ -0,0 +1,11 @@
package app.k9mail.legacy.ui.folder
import kotlinx.coroutines.flow.Flow
import net.thunderbird.core.android.account.LegacyAccount
interface DisplayFolderRepository {
fun getDisplayFoldersFlow(account: LegacyAccount, includeHiddenFolders: Boolean): Flow<List<DisplayFolder>>
fun getDisplayFoldersFlow(accountUuid: String): Flow<List<DisplayFolder>>
}

View file

@ -0,0 +1,17 @@
package app.k9mail.legacy.ui.folder
import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons
import net.thunderbird.feature.mail.folder.api.FolderType
class FolderIconProvider {
fun getFolderIcon(type: FolderType): Int = when (type) {
FolderType.INBOX -> Icons.Outlined.Inbox
FolderType.OUTBOX -> Icons.Outlined.Outbox
FolderType.SENT -> Icons.Outlined.Send
FolderType.TRASH -> Icons.Outlined.Delete
FolderType.DRAFTS -> Icons.Outlined.Draft
FolderType.ARCHIVE -> Icons.Outlined.Archive
FolderType.SPAM -> Icons.Outlined.Report
FolderType.REGULAR -> Icons.Outlined.Folder
}
}

View file

@ -0,0 +1,34 @@
package app.k9mail.legacy.ui.folder
import android.content.res.Resources
import net.thunderbird.feature.mail.folder.api.Folder
import net.thunderbird.feature.mail.folder.api.FolderType
import net.thunderbird.feature.mail.folder.api.RemoteFolder
class FolderNameFormatter(private val resources: Resources) {
fun displayName(folder: Folder): String {
return if (folder.isLocalOnly) {
localFolderDisplayName(folder)
} else {
remoteFolderDisplayName(folder)
}
}
private fun localFolderDisplayName(folder: Folder) = when (folder.type) {
FolderType.OUTBOX -> resources.getString(R.string.special_mailbox_name_outbox)
FolderType.DRAFTS -> resources.getString(R.string.special_mailbox_name_drafts)
FolderType.SENT -> resources.getString(R.string.special_mailbox_name_sent)
FolderType.TRASH -> resources.getString(R.string.special_mailbox_name_trash)
else -> folder.name
}
private fun remoteFolderDisplayName(folder: Folder) = when (folder.type) {
FolderType.INBOX -> resources.getString(R.string.special_mailbox_name_inbox)
else -> folder.name
}
fun displayName(folder: RemoteFolder) = when (folder.type) {
FolderType.INBOX -> resources.getString(R.string.special_mailbox_name_inbox)
else -> folder.name
}
}

View file

@ -0,0 +1,14 @@
package app.k9mail.legacy.ui.folder
import org.koin.dsl.module
val uiFolderModule = module {
single<DisplayFolderRepository> {
DefaultDisplayFolderRepository(
accountManager = get(),
messagingController = get(),
messageStoreManager = get(),
outboxFolderManager = get(),
)
}
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="special_mailbox_name_outbox">الصادر</string>
<string name="special_mailbox_name_drafts">المسودات</string>
<string name="special_mailbox_name_sent">المُرسَل</string>
<string name="special_mailbox_name_trash">المهملات</string>
<string name="special_mailbox_name_inbox">الوارد</string>
</resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="special_mailbox_name_outbox">Адпраўленыя</string>
<string name="special_mailbox_name_drafts">Чарнавікі</string>
<string name="special_mailbox_name_sent">Адпраўлена</string>
<string name="special_mailbox_name_trash">Сметніца</string>
<string name="special_mailbox_name_inbox">Атрыманыя</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="special_mailbox_name_outbox">Изходящи</string>
<string name="special_mailbox_name_drafts">Чернови</string>
<string name="special_mailbox_name_sent">Изпратени</string>
<string name="special_mailbox_name_trash">Кошче</string>
<string name="special_mailbox_name_inbox">Входящи</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="special_mailbox_name_sent">পাঠানো হয়েছে</string>
<string name="special_mailbox_name_inbox">ইনবক্স</string>
<string name="special_mailbox_name_trash">ঝুড়ি</string>
<string name="special_mailbox_name_outbox">আউটবক্স</string>
<string name="special_mailbox_name_drafts">খসড়া</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more