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 +}