diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 4cfb21deb9..77eb379ce9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -152,6 +152,8 @@ object PreferenceKeys { const val automaticExtUpdates = "automatic_ext_updates" + const val installedExtensionsOrder = "installed_extensions_order" + const val autoHideHopper = "autohide_hopper" const val hopperLongPress = "hopper_long_press" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 1673a22993..eace5f51e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.updater.AutoUpdaterJob +import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet import eu.kanade.tachiyomi.ui.reader.settings.OrientationType import eu.kanade.tachiyomi.ui.reader.settings.PageLayout @@ -297,6 +298,8 @@ class PreferencesHelper(val context: Context) { fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true) + fun installedExtensionsOrder() = flowPrefs.getInt(Keys.installedExtensionsOrder, InstalledExtensionsOrder.Name.value) + fun collapsedCategories() = rxPrefs.getStringSet("collapsed_categories", mutableSetOf()) fun collapsedDynamicCategories() = flowPrefs.getStringSet("collapsed_dynamic_categories", mutableSetOf()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstalledExtensionsOrder.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstalledExtensionsOrder.kt new file mode 100644 index 0000000000..fc4bb250ea --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstalledExtensionsOrder.kt @@ -0,0 +1,18 @@ +package eu.kanade.tachiyomi.extension.model + +import androidx.annotation.StringRes +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper + +enum class InstalledExtensionsOrder(val value: Int, @StringRes val nameRes: Int) { + Name(0, R.string.name), + RecentlyUpdated(1, R.string.recently_updated), + RecentlyInstalled(2, R.string.recently_installed), + Language(3, R.string.language), + ; + + companion object { + fun fromValue(preference: Int) = values().find { it.value == preference } ?: Name + fun fromPreference(pref: PreferencesHelper) = fromValue(pref.installedExtensionsOrder().get()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt index 2f2c77108e..2b1828b3de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt @@ -1,8 +1,11 @@ package eu.kanade.tachiyomi.ui.extension +import android.widget.TextView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.extension.ExtensionAdapter.OnButtonClickListener +import uy.kohesive.injekt.injectLazy /** * Adapter that holds the catalogue cards. @@ -12,6 +15,10 @@ import eu.kanade.tachiyomi.ui.extension.ExtensionAdapter.OnButtonClickListener class ExtensionAdapter(val listener: OnButtonClickListener) : FlexibleAdapter>(null, listener, true) { + val preferences: PreferencesHelper by injectLazy() + + var installedSortOrder = preferences.installedExtensionsOrder().get() + init { setDisplayHeadersAtStartUp(true) } @@ -25,5 +32,6 @@ class ExtensionAdapter(val listener: OnButtonClickListener) : fun onButtonClick(position: Int) fun onCancelClick(position: Int) fun onUpdateAllClicked(position: Int) + fun onExtSortClicked(view: TextView, position: Int) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt index 5a26ba60d9..a373b3c9a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt @@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionsChangedListener import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep +import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager @@ -24,7 +25,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -181,11 +181,25 @@ class ExtensionBottomPresenter( val items = mutableListOf() - val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName } + val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.name } + val sortOrder = InstalledExtensionsOrder.fromPreference(preferences) val installedSorted = installed .filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) } - .sortedWith(compareBy({ !it.isObsolete }, { it.pkgName })) - val untrustedSorted = untrusted.sortedBy { it.pkgName } + .sortedWith( + compareBy( + { !it.isObsolete }, + { + when (sortOrder) { + InstalledExtensionsOrder.Name -> it.name + InstalledExtensionsOrder.RecentlyUpdated -> Long.MAX_VALUE - extensionUpdateDate(it.pkgName) + InstalledExtensionsOrder.RecentlyInstalled -> Long.MAX_VALUE - extensionInstallDate(it.pkgName) + InstalledExtensionsOrder.Language -> it.lang + } + }, + { it.name } + ) + ) + val untrustedSorted = untrusted.sortedBy { it.name } val availableSorted = available // Filter out already installed extensions and disabled languages .filter { avail -> @@ -194,7 +208,7 @@ class ExtensionBottomPresenter( (avail.lang in activeLangs || avail.lang == "all") && (showNsfwExtensions || !avail.isNsfw) } - .sortedBy { it.pkgName } + .sortedBy { it.name } if (updatesSorted.isNotEmpty()) { val header = ExtensionGroupItem( @@ -211,7 +225,7 @@ class ExtensionBottomPresenter( } } if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) { - val header = ExtensionGroupItem(context.getString(R.string.installed), installedSorted.size + untrustedSorted.size) + val header = ExtensionGroupItem(context.getString(R.string.installed), installedSorted.size + untrustedSorted.size, installedSorting = preferences.installedExtensionsOrder().get()) items += installedSorted.map { extension -> ExtensionItem(extension, header, currentDownloads[extension.pkgName]) } @@ -237,8 +251,25 @@ class ExtensionBottomPresenter( return items } + private fun extensionInstallDate(pkgName: String): Long { + val context = bottomSheet.context + return try { + context.packageManager.getPackageInfo(pkgName, 0).firstInstallTime + } catch (e: java.lang.Exception) { + 0 + } + } + + private fun extensionUpdateDate(pkgName: String): Long { + val context = bottomSheet.context + return try { + context.packageManager.getPackageInfo(pkgName, 0).lastUpdateTime + } catch (e: java.lang.Exception) { + 0 + } + } + fun getExtensionUpdateCount(): Int = preferences.extensionUpdatesCount().getOrDefault() - fun getAutoCheckPref() = preferences.automaticExtUpdates() @Synchronized private fun updateInstallStep( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt index 687362ff82..e47868c14c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -18,6 +19,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.databinding.ExtensionsBottomSheetBinding import eu.kanade.tachiyomi.databinding.RecyclerWithScrollerBinding import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.ui.extension.details.ExtensionDetailsController import eu.kanade.tachiyomi.ui.migration.MangaAdapter import eu.kanade.tachiyomi.ui.migration.MangaItem @@ -30,6 +32,7 @@ import eu.kanade.tachiyomi.util.view.collapse import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets import eu.kanade.tachiyomi.util.view.expand import eu.kanade.tachiyomi.util.view.isExpanded +import eu.kanade.tachiyomi.util.view.popupMenu import eu.kanade.tachiyomi.util.view.smoothScrollToTop import eu.kanade.tachiyomi.util.view.updatePaddingRelative import eu.kanade.tachiyomi.util.view.withFadeTransaction @@ -217,6 +220,18 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At } } + override fun onExtSortClicked(view: TextView, position: Int) { + view.popupMenu( + InstalledExtensionsOrder.values().map { it.value to it.nameRes }, + presenter.preferences.installedExtensionsOrder().get() + ) { + presenter.preferences.installedExtensionsOrder().set(itemId) + extAdapter?.installedSortOrder = itemId + view.setText(InstalledExtensionsOrder.fromValue(itemId).nameRes) + presenter.refreshExtensions() + } + } + fun updateAllExtensions(position: Int) { val header = (extAdapter?.getSectionHeader(position)) as? ExtensionGroupItem ?: return val items = extAdapter?.getSectionItemPositions(header) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt index 4c16428d90..ac2b427d9d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt @@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.databinding.ExtensionCardHeaderBinding +import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter>) : @@ -19,6 +20,9 @@ class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter= Build.VERSION_CODES.S binding.extButton.isEnabled = item.canUpdate == true + binding.extSort.isVisible = item.installedSorting != null + binding.extSort.setText(InstalledExtensionsOrder.fromValue(item.installedSorting ?: 0).nameRes) + binding.extSort.post { + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt index 10cefb51ba..1f1c89581e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt @@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.R * @param name The header name. * @param size The number of items in the group. */ -data class ExtensionGroupItem(val name: String, val size: Int, var canUpdate: Boolean? = null) : AbstractHeaderItem() { +data class ExtensionGroupItem(val name: String, val size: Int, var canUpdate: Boolean? = null, var installedSorting: Int? = null) : AbstractHeaderItem() { /** * Returns the layout resource of this item. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt index 3706c5f04f..dd20a81447 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.extension import android.content.res.ColorStateList import android.graphics.Color import android.view.View +import androidx.core.view.isGone import androidx.core.view.isVisible import coil.clear import coil.load @@ -14,6 +15,8 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding +import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder +import eu.kanade.tachiyomi.util.system.timeSpanFromNow import eu.kanade.tachiyomi.util.view.resetStrokeColor import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -41,7 +44,34 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : // Set source name binding.extTitle.text = extension.name - binding.version.text = extension.versionName + + val infoText = mutableListOf(extension.versionName) + if (extension is Extension.Installed) { + when (InstalledExtensionsOrder.fromValue(adapter.installedSortOrder)) { + InstalledExtensionsOrder.RecentlyUpdated -> { + binding.date.isVisible = true + binding.date.text = itemView.context.getString( + R.string.updated_, + extensionUpdateDate(extension.pkgName).timeSpanFromNow + ) + infoText.add("") + } + InstalledExtensionsOrder.RecentlyInstalled -> { + binding.date.isVisible = true + binding.date.text = itemView.context.getString( + R.string.installed_, + extensionInstallDate(extension.pkgName).timeSpanFromNow + ) + infoText.add("") + } + else -> binding.date.isVisible = false + } + } else { + binding.date.isVisible = false + } + binding.lang.isVisible = binding.date.isGone + + binding.version.text = infoText.joinToString(" • ") binding.lang.text = LocaleHelper.getDisplayName(extension.lang) binding.warning.text = when { extension is Extension.Untrusted -> itemView.context.getString(R.string.untrusted) @@ -113,4 +143,22 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : setText(R.string.install) } } + + private fun extensionInstallDate(pkgName: String): Long { + val context = itemView.context + return try { + context.packageManager.getPackageInfo(pkgName, 0).firstInstallTime + } catch (e: java.lang.Exception) { + 0 + } + } + + private fun extensionUpdateDate(pkgName: String): Long { + val context = itemView.context + return try { + context.packageManager.getPackageInfo(pkgName, 0).lastUpdateTime + } catch (e: java.lang.Exception) { + 0 + } + } } diff --git a/app/src/main/res/layout/extension_card_header.xml b/app/src/main/res/layout/extension_card_header.xml index 416b0be1ac..3ec6a5a904 100644 --- a/app/src/main/res/layout/extension_card_header.xml +++ b/app/src/main/res/layout/extension_card_header.xml @@ -33,7 +33,7 @@ android:textAllCaps="false" android:textColor="@color/accent_text_btn_color_selector" android:visibility="gone" - tools:visibility="visible" + tools:visibility="gone" app:layout_constraintBaseline_toBaselineOf="@id/title" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintEnd_toEndOf="parent" @@ -43,6 +43,34 @@ app:rippleColor="@color/fullRippleColor" android:text="@string/update_all" /> + + diff --git a/app/src/main/res/layout/extension_card_item.xml b/app/src/main/res/layout/extension_card_item.xml index 5a14c3702f..ba7135d143 100644 --- a/app/src/main/res/layout/extension_card_item.xml +++ b/app/src/main/res/layout/extension_card_item.xml @@ -49,7 +49,7 @@ android:maxLines="1" android:textAppearance="@style/TextAppearance.Regular.SubHeading" android:textSize="14sp" - app:layout_constraintBottom_toTopOf="@id/lang" + app:layout_constraintBottom_toTopOf="@id/version" app:layout_constraintEnd_toStartOf="@id/button_layout" app:layout_constraintStart_toEndOf="@id/source_image" app:layout_constraintTop_toTopOf="parent" @@ -63,8 +63,13 @@ android:layout_height="wrap_content" android:maxLines="1" android:textSize="12sp" + android:layout_marginEnd="4dp" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintVertical_bias="0.0" + app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/source_image" + app:layout_constraintEnd_toStartOf="@id/version" app:layout_constraintTop_toBottomOf="@+id/ext_title" tools:text="English" tools:visibility="visible" /> @@ -74,13 +79,28 @@ style="@style/TextAppearance.Regular.Body1.Secondary" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="4dp" android:maxLines="1" + android:ellipsize="middle" android:textSize="12sp" - app:layout_constraintBaseline_toBaselineOf="@id/lang" + app:layout_constraintTop_toBottomOf="@+id/ext_title" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/lang" - tools:text="Version" /> + app:layout_constraintEnd_toStartOf="@id/date" + tools:text="Version • " /> + Version: %1$s Language: %1$s 18+ + Installed %1$s Unofficial MIUI Optimization must be disabled to install extensions. May contain NSFW (18+) content @@ -963,6 +964,9 @@ Move to top Move to %1$s Moved to %1$s + Name + Recently updated + Recently installed Never Newest Next