diff --git a/app/src/main/java/org/fedorahosted/freeotp/token/TokenLayout.kt b/app/src/main/java/org/fedorahosted/freeotp/token/TokenLayout.kt
index b13392c..a08ac89 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/token/TokenLayout.kt
+++ b/app/src/main/java/org/fedorahosted/freeotp/token/TokenLayout.kt
@@ -84,22 +84,24 @@ class TokenLayout : MaterialCardView, View.OnClickListener, Runnable {
mImage.setTokenImage(token)
// Set the labels.
- mLabel.text = token.label
+ val category = token.category?.trim()?.takeIf { it.isNotEmpty() }
+ val accountLabel = if (category == null) {
+ token.label
+ } else {
+ "${token.label} ($category)"
+ }
+ mLabel.text = accountLabel
mIssuer.text = token.issuer
mCode.text = mPlaceholder
if (mIssuer.text.isEmpty()) {
- mIssuer.text = token.label
+ mIssuer.text = accountLabel
mLabel.visibility = View.GONE
} else {
mLabel.visibility = View.VISIBLE
}
- if (token.category.isNullOrBlank()) {
- mCategory.visibility = View.GONE
- } else {
- mCategory.visibility = View.VISIBLE
- mCategory.text = token.category
- }
+ mCategory.visibility = View.GONE
+ mCategory.text = ""
}
private fun animate(view: View, anim: Int, animate: Boolean) {
diff --git a/app/src/main/java/org/fedorahosted/freeotp/ui/MainActivity.kt b/app/src/main/java/org/fedorahosted/freeotp/ui/MainActivity.kt
index 66db5b9..6520778 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/ui/MainActivity.kt
+++ b/app/src/main/java/org/fedorahosted/freeotp/ui/MainActivity.kt
@@ -65,6 +65,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.tabs.TabLayout
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
@@ -96,6 +97,9 @@ class MainActivity : AppCompatActivity() {
private var menu: Menu? = null
private var lastSessionEndTimestamp = 0L;
private var categories: List = emptyList()
+ private var hasUncategorizedTokens = false
+ private var selectedCategoryFilter: String? = null
+ private var updatingCategoryTabs = false
private val tokenListObserver: AdapterDataObserver = object: AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
@@ -181,6 +185,15 @@ class MainActivity : AppCompatActivity() {
launch {
viewModel.getAllCategories().collect {
categories = it
+ updateCategoryTabs()
+ invalidateOptionsMenu()
+ }
+ }
+
+ launch {
+ viewModel.hasUncategorizedTokens().collect {
+ hasUncategorizedTokens = it
+ updateCategoryTabs()
invalidateOptionsMenu()
}
}
@@ -203,6 +216,20 @@ class MainActivity : AppCompatActivity() {
})
+ binding.categoryTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabSelected(tab: TabLayout.Tab) {
+ if (!updatingCategoryTabs) {
+ selectCategoryFilter(tab.tag as? String, updateTabs = false)
+ }
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab) {
+ }
+
+ override fun onTabReselected(tab: TabLayout.Tab) {
+ }
+ })
+
binding.addTokenFab.setOnClickListener {
startActivity(Intent(this, ScanTokenActivity::class.java))
}
@@ -245,14 +272,19 @@ class MainActivity : AppCompatActivity() {
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val filterItem = menu.findItem(R.id.action_filter)
val filterMenu = filterItem?.subMenu ?: return super.onPrepareOptionsMenu(menu)
+ filterItem.isVisible = categories.isNotEmpty() || hasUncategorizedTokens
filterMenu.clear()
- filterMenu.add(Menu.NONE, R.id.action_filter_all, Menu.NONE, R.string.all_categories)
- filterMenu.add(Menu.NONE, R.id.action_filter_uncategorized, Menu.NONE, R.string.uncategorized)
+ if (categories.isNotEmpty()) {
+ filterMenu.add(Menu.NONE, R.id.action_filter_all, Menu.NONE, R.string.all_categories)
+ }
+ if (hasUncategorizedTokens) {
+ filterMenu.add(Menu.NONE, R.id.action_filter_uncategorized, Menu.NONE, R.string.uncategorized)
+ }
for (category in categories) {
filterMenu.add(Menu.NONE, Menu.NONE, Menu.NONE, category).setOnMenuItemClickListener {
- viewModel.setCategoryFilter(category)
+ selectCategoryFilter(category)
true
}
}
@@ -273,12 +305,12 @@ class MainActivity : AppCompatActivity() {
}
R.id.action_filter_all -> {
- viewModel.setCategoryFilter(null)
+ selectCategoryFilter(null)
return true
}
R.id.action_filter_uncategorized -> {
- viewModel.setCategoryFilter(MainViewModel.CATEGORY_UNCATEGORIZED)
+ selectCategoryFilter(MainViewModel.CATEGORY_UNCATEGORIZED)
return true
}
@@ -478,6 +510,74 @@ class MainActivity : AppCompatActivity() {
this.menu?.findItem(R.id.require_authentication)?.isChecked = settings.requireAuthentication
}
+ private fun selectCategoryFilter(category: String?, updateTabs: Boolean = true) {
+ selectedCategoryFilter = category
+ viewModel.setCategoryFilter(category)
+
+ if (updateTabs) {
+ selectCategoryTab(category)
+ }
+ }
+
+ private fun updateCategoryTabs() {
+ normalizeCategoryFilter()
+
+ updatingCategoryTabs = true
+ binding.categoryTabs.removeAllTabs()
+ if (categories.isEmpty() && !hasUncategorizedTokens) {
+ binding.categoryTabs.visibility = View.GONE
+ updatingCategoryTabs = false
+ return
+ }
+
+ binding.categoryTabs.visibility = View.VISIBLE
+ if (categories.isNotEmpty()) {
+ addCategoryTab(getString(R.string.category_tab_all), null)
+ }
+ if (hasUncategorizedTokens) {
+ addCategoryTab(getString(R.string.category_tab_none), MainViewModel.CATEGORY_UNCATEGORIZED)
+ }
+ categories.forEach { category ->
+ addCategoryTab(category, category)
+ }
+ selectCategoryTab(selectedCategoryFilter)
+ updatingCategoryTabs = false
+ }
+
+ private fun normalizeCategoryFilter() {
+ val normalizedCategory = when {
+ categories.isEmpty() && !hasUncategorizedTokens -> null
+ categories.isEmpty() && hasUncategorizedTokens -> MainViewModel.CATEGORY_UNCATEGORIZED
+ selectedCategoryFilter == MainViewModel.CATEGORY_UNCATEGORIZED && !hasUncategorizedTokens -> null
+ selectedCategoryFilter != null &&
+ selectedCategoryFilter != MainViewModel.CATEGORY_UNCATEGORIZED &&
+ !categories.contains(selectedCategoryFilter) -> null
+ else -> selectedCategoryFilter
+ }
+
+ if (selectedCategoryFilter != normalizedCategory) {
+ selectedCategoryFilter = normalizedCategory
+ viewModel.setCategoryFilter(normalizedCategory)
+ }
+ }
+
+ private fun addCategoryTab(title: String, category: String?) {
+ val tab = binding.categoryTabs.newTab()
+ .setText(title)
+ tab.tag = category
+ binding.categoryTabs.addTab(tab, category == selectedCategoryFilter)
+ }
+
+ private fun selectCategoryTab(category: String?) {
+ for (index in 0 until binding.categoryTabs.tabCount) {
+ val tab = binding.categoryTabs.getTabAt(index) ?: continue
+ if (tab.tag == category) {
+ tab.select()
+ return
+ }
+ }
+ }
+
private fun verifyAuthentication(isEnabling: Boolean = false, isDisabling: Boolean = false) {
val executor = ContextCompat.getMainExecutor(this)
val biometricPrompt = BiometricPrompt(this, executor,
diff --git a/app/src/main/java/org/fedorahosted/freeotp/ui/MainViewModel.kt b/app/src/main/java/org/fedorahosted/freeotp/ui/MainViewModel.kt
index fea1603..2ad18d9 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/ui/MainViewModel.kt
+++ b/app/src/main/java/org/fedorahosted/freeotp/ui/MainViewModel.kt
@@ -56,6 +56,8 @@ class MainViewModel @Inject constructor(
fun getAllCategories(): Flow> = otpTokenDatabase.otpTokenDao().getAllCategories()
+ fun hasUncategorizedTokens(): Flow = otpTokenDatabase.otpTokenDao().hasUncategorizedTokens()
+
fun getTokenList(): Flow> {
return combine(authState, tokenSearchQuery, categoryFilter, otpTokenDatabase.otpTokenDao().getAll()) {auth, searchQuery, category, tokens ->
if (auth != AuthState.AUTHENTICATED) {
diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml
index ff01516..ff783ea 100644
--- a/app/src/main/res/layout/main.xml
+++ b/app/src/main/res/layout/main.xml
@@ -39,6 +39,12 @@
android:layout_marginEnd="@dimen/space_2"
android:iconifiedByDefault="true"/>
+
Launch with screenshot enabled
Token details
Share to other apps
+ Alles
+ Geen
diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml
index 9f132e8..1fbd31e 100644
--- a/app/src/main/res/values-ar-rSA/strings.xml
+++ b/app/src/main/res/values-ar-rSA/strings.xml
@@ -65,4 +65,6 @@
إطلاق مع تمكين لقطة الشاشة
تفاصيل الرمز
Share to other apps
+ الكل
+ لا شيء
diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml
index 3b97310..39272cf 100644
--- a/app/src/main/res/values-ca-rES/strings.xml
+++ b/app/src/main/res/values-ca-rES/strings.xml
@@ -65,4 +65,6 @@
Inicia amb la captura de pantalla activada
Detalls de l\'OTP
Envia a una altra app
+ Tot
+ Cap
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index 7dd134e..bd710ca 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -65,4 +65,6 @@
Spustit s povoleným snímkem obrazovky
Detaily tokenu
Share to other apps
+ Vše
+ Žádné
diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml
index 30313a9..9cd83f8 100644
--- a/app/src/main/res/values-de-rDE/strings.xml
+++ b/app/src/main/res/values-de-rDE/strings.xml
@@ -65,4 +65,6 @@
Starte mit aktiviertem Screenshot
Token-Details
Mit anderen Apps teilen
+ Alle
+ Keine
diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml
index e464721..c1c5b49 100644
--- a/app/src/main/res/values-es-rES/strings.xml
+++ b/app/src/main/res/values-es-rES/strings.xml
@@ -65,4 +65,6 @@
Iniciar con captura de pantalla activada
Detalles del token
Enviar a otras aplicaciones
+ Todo
+ Ninguna
diff --git a/app/src/main/res/values-eu-rES/strings.xml b/app/src/main/res/values-eu-rES/strings.xml
index f6221ba..6fc3a0e 100644
--- a/app/src/main/res/values-eu-rES/strings.xml
+++ b/app/src/main/res/values-eu-rES/strings.xml
@@ -65,4 +65,6 @@
Abiarazi pantaila-argazkiak gaituz
Token xehetasunak
Partekatu beste aplikazioekin
+ Denak
+ Bat ere ez
diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml
index de84eba..d2f8c88 100644
--- a/app/src/main/res/values-fr-rFR/strings.xml
+++ b/app/src/main/res/values-fr-rFR/strings.xml
@@ -65,4 +65,6 @@
Lancer avec capture d\'écran activée
Détails du jeton
Partager avec d\'autres applications
+ Tout
+ Aucune
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index 7c05bc0..e5d7407 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -65,4 +65,6 @@
Launch with screenshot enabled
Token details
Share to other apps
+ Mind
+ Nincs
diff --git a/app/src/main/res/values-id-rID/strings.xml b/app/src/main/res/values-id-rID/strings.xml
index 298ca39..5cb222f 100644
--- a/app/src/main/res/values-id-rID/strings.xml
+++ b/app/src/main/res/values-id-rID/strings.xml
@@ -76,4 +76,6 @@
Luncurkan dengan tangkapan layar aktif
Detail Token
Bagikan ke aplikasi lain
+ Semua
+ Tidak ada
diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml
index 02a7a19..145c0da 100644
--- a/app/src/main/res/values-it-rIT/strings.xml
+++ b/app/src/main/res/values-it-rIT/strings.xml
@@ -65,4 +65,6 @@
Usa con gli screenshot abilitati
Dettagli del token
Condividi con altre app
+ Tutti
+ Nessuna
diff --git a/app/src/main/res/values-iw-rHE/strings.xml b/app/src/main/res/values-iw-rHE/strings.xml
index c035d77..909827e 100644
--- a/app/src/main/res/values-iw-rHE/strings.xml
+++ b/app/src/main/res/values-iw-rHE/strings.xml
@@ -65,4 +65,6 @@
הפעלה כאשר צילום מסך מופעל
פרטי אסימון
שיתוף עם יישומים אחרים
+ הכל
+ ללא
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index 3b7f587..17c71ec 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -65,4 +65,6 @@
スクリーンショットを有効にして起動
トークンの詳細
他のアプリと共有する
+ すべて
+ なし
diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml
index b6297f3..43ee3c2 100644
--- a/app/src/main/res/values-pl-rPL/strings.xml
+++ b/app/src/main/res/values-pl-rPL/strings.xml
@@ -65,4 +65,6 @@
Uruchom z włączonym zrzutem ekranu
Szczegóły tokenu
Udostępnij innym aplikacjom
+ Wszystkie
+ Brak
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 9908301..a98ec1c 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -65,4 +65,6 @@
Iniciar com captura de tela ativada
Detalhes do token
Compartilhar com outros apps
+ Todos
+ Nenhuma
diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml
index 178c0ca..cf81ef9 100644
--- a/app/src/main/res/values-ru-rRU/strings.xml
+++ b/app/src/main/res/values-ru-rRU/strings.xml
@@ -65,4 +65,6 @@
Запуск с включённым режимом скриншота
Детали токена
Поделиться с другими приложениями
+ Все
+ Нет
diff --git a/app/src/main/res/values-si-rLK/strings.xml b/app/src/main/res/values-si-rLK/strings.xml
index 3042f4c..3cb3c2c 100644
--- a/app/src/main/res/values-si-rLK/strings.xml
+++ b/app/src/main/res/values-si-rLK/strings.xml
@@ -65,4 +65,6 @@
තිර රුවක් සබල කර දියත් කරන්න
ටෝකන් විස්තර
Share to other apps
+ සියල්ල
+ නැත
diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml
index 1e48a5b..ce24ba1 100644
--- a/app/src/main/res/values-sv-rSE/strings.xml
+++ b/app/src/main/res/values-sv-rSE/strings.xml
@@ -65,4 +65,6 @@
Starta med skärmdump aktiverad
Tokendetaljer
Dela med andra appar
+ Alla
+ Ingen
diff --git a/app/src/main/res/values-ta-rIN/strings.xml b/app/src/main/res/values-ta-rIN/strings.xml
index 5d70b50..5bc80a7 100644
--- a/app/src/main/res/values-ta-rIN/strings.xml
+++ b/app/src/main/res/values-ta-rIN/strings.xml
@@ -65,4 +65,6 @@
Launch with screenshot enabled
Token details
Share to other apps
+ அனைத்தும்
+ எதுவுமில்லை
diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml
index 79c4669..3e3b26b 100644
--- a/app/src/main/res/values-tr-rTR/strings.xml
+++ b/app/src/main/res/values-tr-rTR/strings.xml
@@ -65,4 +65,6 @@
Ekran görüntüsü aktifken başlat
Token ayrıntıları
Diğer uygulamalarla paylaş
+ Tümü
+ Yok
diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml
index 2ec9fe7..0f12380 100644
--- a/app/src/main/res/values-uk-rUA/strings.xml
+++ b/app/src/main/res/values-uk-rUA/strings.xml
@@ -65,4 +65,6 @@
Запустити з дозволеним знімком екрана
Деталі ключа
Share to other apps
+ Усі
+ Немає
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 08df180..5583d88 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -65,4 +65,6 @@
启动时允许屏幕截图
令牌详细信息
分享到其他应用
+ 全部
+ 无
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 506a565..ee059e8 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -65,4 +65,6 @@
啟用螢幕截圖啟動
權杖詳細資訊
分享給其他應用程式
+ 全部
+ 無
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 450cf29..c15e313 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -80,4 +80,6 @@
Category
All categories
Uncategorized
+ All
+ None
diff --git a/token-data/src/main/java/org/fedorahosted/freeotp/data/OtpTokenDao.kt b/token-data/src/main/java/org/fedorahosted/freeotp/data/OtpTokenDao.kt
index 15f3fc8..08cf96b 100644
--- a/token-data/src/main/java/org/fedorahosted/freeotp/data/OtpTokenDao.kt
+++ b/token-data/src/main/java/org/fedorahosted/freeotp/data/OtpTokenDao.kt
@@ -29,6 +29,9 @@ interface OtpTokenDao {
@Query("select distinct category from otp_tokens where category is not null and category != ''")
fun getAllCategories(): Flow>
+ @Query("select exists(select 1 from otp_tokens where category is null or category = '')")
+ fun hasUncategorizedTokens(): Flow
+
@Query("delete from otp_tokens where id = :id")
suspend fun deleteById(id: Long)
@@ -73,4 +76,4 @@ interface OtpTokenDao {
}
}
}
-}
\ No newline at end of file
+}