mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2025-01-23 13:11:14 +01:00
Added Source Migration to browse section
Don't ask me how I did it.
This commit is contained in:
parent
90a6543334
commit
d6b07b7f40
@ -1,19 +1,33 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
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.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.migration.MangaItem
|
||||
import eu.kanade.tachiyomi.ui.migration.SelectionHeader
|
||||
import eu.kanade.tachiyomi.ui.migration.SourceItem
|
||||
import eu.kanade.tachiyomi.util.lang.combineLatest
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.executeOnIO
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@ -32,23 +46,64 @@ class ExtensionBottomPresenter(
|
||||
|
||||
private var extensions = emptyList<ExtensionItem>()
|
||||
|
||||
var sourceItems = emptyList<SourceItem>()
|
||||
private set
|
||||
|
||||
var mangaItems = hashMapOf<Long, List<MangaItem>>()
|
||||
private set
|
||||
|
||||
private var currentDownloads = hashMapOf<String, InstallStep>()
|
||||
|
||||
private val sourceManager: SourceManager = Injekt.get()
|
||||
|
||||
private var selectedSource: Long? = null
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
|
||||
fun onCreate() {
|
||||
scope.launch {
|
||||
extensionManager.findAvailableExtensionsAsync()
|
||||
extensions = toItems(
|
||||
Triple(
|
||||
extensionManager.installedExtensions,
|
||||
extensionManager.untrustedExtensions,
|
||||
extensionManager.availableExtensions
|
||||
val extensionJob = async {
|
||||
extensionManager.findAvailableExtensionsAsync()
|
||||
extensions = toItems(
|
||||
Triple(
|
||||
extensionManager.installedExtensions,
|
||||
extensionManager.untrustedExtensions,
|
||||
extensionManager.availableExtensions
|
||||
)
|
||||
)
|
||||
)
|
||||
withContext(Dispatchers.Main) { bottomSheet.setExtensions(extensions) }
|
||||
extensionManager.setListener(this@ExtensionBottomPresenter)
|
||||
withContext(Dispatchers.Main) { bottomSheet.setExtensions(extensions) }
|
||||
extensionManager.setListener(this@ExtensionBottomPresenter)
|
||||
}
|
||||
val migrationJob = async {
|
||||
val favs = db.getFavoriteMangas().executeOnIO()
|
||||
sourceItems = findSourcesWithManga(favs)
|
||||
mangaItems = HashMap(sourceItems.associate {
|
||||
it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(favs, it.source.id)
|
||||
})
|
||||
withContext(Dispatchers.Main) {
|
||||
if (selectedSource != null) {
|
||||
bottomSheet.setMigrationManga(mangaItems[selectedSource])
|
||||
}
|
||||
else {
|
||||
bottomSheet.setMigrationSources(sourceItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
listOf(migrationJob, extensionJob).awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
||||
val header = SelectionHeader()
|
||||
return library.map { it.source }.toSet()
|
||||
.mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null }
|
||||
.sortedBy { it.name }
|
||||
.map { SourceItem(it, header) }
|
||||
}
|
||||
|
||||
private fun libraryToMigrationItem(library: List<Manga>, sourceId: Long): List<MangaItem> {
|
||||
return library.filter { it.source == sourceId }.map(::MangaItem)
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
extensionManager.removeListener(this)
|
||||
}
|
||||
@ -66,6 +121,24 @@ class ExtensionBottomPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshMigrations() {
|
||||
scope.launch {
|
||||
val favs = db.getFavoriteMangas().executeOnIO()
|
||||
sourceItems = findSourcesWithManga(favs)
|
||||
mangaItems = HashMap(sourceItems.associate {
|
||||
it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(favs, it.source.id)
|
||||
})
|
||||
withContext(Dispatchers.Main) {
|
||||
if (selectedSource != null) {
|
||||
bottomSheet.setMigrationManga(mangaItems[selectedSource])
|
||||
}
|
||||
else {
|
||||
bottomSheet.setMigrationSources(sourceItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun extensionsUpdated() {
|
||||
refreshExtensions()
|
||||
}
|
||||
@ -166,4 +239,18 @@ class ExtensionBottomPresenter(
|
||||
fun trustSignature(signatureHash: String) {
|
||||
extensionManager.trustSignature(signatureHash)
|
||||
}
|
||||
|
||||
fun setSelectedSource(source: Source) {
|
||||
selectedSource = source.id
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Main) { bottomSheet.setMigrationManga(mangaItems[source.id]) }
|
||||
}
|
||||
}
|
||||
|
||||
fun deselectSource() {
|
||||
selectedSource = null
|
||||
scope.launch {
|
||||
withContext(Dispatchers.Main) { bottomSheet.setMigrationSources(sourceItems) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,33 @@ package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.get
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.migration.MangaAdapter
|
||||
import eu.kanade.tachiyomi.ui.migration.MangaItem
|
||||
import eu.kanade.tachiyomi.ui.migration.SourceAdapter
|
||||
import eu.kanade.tachiyomi.ui.migration.SourceItem
|
||||
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
|
||||
import eu.kanade.tachiyomi.ui.recents.RecentMangaHolder
|
||||
import eu.kanade.tachiyomi.ui.source.SourceController
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.await
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import eu.kanade.tachiyomi.util.view.collapse
|
||||
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
|
||||
import eu.kanade.tachiyomi.util.view.expand
|
||||
@ -19,15 +36,25 @@ import eu.kanade.tachiyomi.util.view.isExpanded
|
||||
import eu.kanade.tachiyomi.util.view.updateLayoutParams
|
||||
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
|
||||
import eu.kanade.tachiyomi.util.view.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import kotlinx.android.synthetic.main.extensions_bottom_sheet.view.*
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import kotlinx.android.synthetic.main.migration_controller.*
|
||||
import kotlinx.android.synthetic.main.recents_controller.*
|
||||
import kotlinx.android.synthetic.main.recycler_with_scroller.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
LinearLayout(context, attrs),
|
||||
ExtensionAdapter.OnButtonClickListener,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
ExtensionTrustDialog.Listener {
|
||||
ExtensionTrustDialog.Listener,
|
||||
SourceAdapter.OnAllClickListener {
|
||||
|
||||
var sheetBehavior: BottomSheetBehavior<*>? = null
|
||||
|
||||
@ -37,28 +64,79 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
* Adapter containing the list of extensions
|
||||
*/
|
||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||
private var migAdapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||
|
||||
val presenter = ExtensionBottomPresenter(this)
|
||||
|
||||
private var extensions: List<ExtensionItem> = emptyList()
|
||||
var canExpand = false
|
||||
|
||||
lateinit var controller: SourceController
|
||||
|
||||
private val pAdapter = FlexibleAdapter<IFlexible<*>>(null, this, true)
|
||||
|
||||
val extensionFrameLayout =
|
||||
inflate(context, R.layout.recycler_with_scroller, null) as FrameLayout
|
||||
val migrationFrameLayout =
|
||||
inflate(context, R.layout.recycler_with_scroller, null) as FrameLayout
|
||||
|
||||
fun onCreate(controller: SourceController) {
|
||||
// Initialize adapter, scroll listener and recycler views
|
||||
adapter = ExtensionAdapter(this)
|
||||
migAdapter = ExtensionAdapter(this)
|
||||
sheetBehavior = BottomSheetBehavior.from(this)
|
||||
// Create recycler and set adapter.
|
||||
ext_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
|
||||
ext_recycler.adapter = adapter
|
||||
ext_recycler.setHasFixedSize(true)
|
||||
ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(context))
|
||||
adapter?.fastScroller = fast_scroller
|
||||
val extRecyler = extensionFrameLayout.recycler
|
||||
val migRecyler = migrationFrameLayout.recycler
|
||||
|
||||
extRecyler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
|
||||
extRecyler.adapter = adapter
|
||||
extRecyler.setHasFixedSize(true)
|
||||
extRecyler.addItemDecoration(ExtensionDividerItemDecoration(context))
|
||||
|
||||
migRecyler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
|
||||
migRecyler.setHasFixedSize(true)
|
||||
|
||||
adapter?.fastScroller = extensionFrameLayout.fast_scroller
|
||||
this.controller = controller
|
||||
ext_recycler.doOnApplyWindowInsets { view, _, _ ->
|
||||
val bottomBar = (this@ExtensionBottomSheet.controller.activity as? MainActivity)?.bottom_nav
|
||||
view.updatePaddingRelative(bottom = bottomBar?.height ?: 0)
|
||||
pager.doOnApplyWindowInsets { _, _, _ ->
|
||||
val bottomBar =
|
||||
(this@ExtensionBottomSheet.controller.activity as? MainActivity)?.bottom_nav
|
||||
extRecyler.updatePaddingRelative(bottom = bottomBar?.height ?: 0)
|
||||
migRecyler.updatePaddingRelative(bottom = bottomBar?.height ?: 0)
|
||||
}
|
||||
pager.adapter = TabbedSheetAdapter()
|
||||
tabs.setupWithViewPager(pager)
|
||||
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
if (canExpand) {
|
||||
this@ExtensionBottomSheet.sheetBehavior?.expand()
|
||||
}
|
||||
this@ExtensionBottomSheet.controller.updateTitleAndMenu()
|
||||
when (tab?.position) {
|
||||
0 -> extensionFrameLayout
|
||||
else -> migrationFrameLayout
|
||||
}.recycler?.isNestedScrollingEnabled = true
|
||||
when (tab?.position) {
|
||||
0 -> extensionFrameLayout
|
||||
else -> migrationFrameLayout
|
||||
}.recycler?.requestLayout()
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
||||
when (tab?.position) {
|
||||
0 -> extensionFrameLayout
|
||||
else -> migrationFrameLayout
|
||||
}.recycler?.isNestedScrollingEnabled = false
|
||||
}
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
this@ExtensionBottomSheet.sheetBehavior?.expand()
|
||||
when (tab?.position) {
|
||||
0 -> extensionFrameLayout
|
||||
else -> migrationFrameLayout
|
||||
}.recycler?.isNestedScrollingEnabled = true
|
||||
}
|
||||
})
|
||||
presenter.onCreate()
|
||||
updateExtTitle()
|
||||
|
||||
@ -66,12 +144,6 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
val array = context.obtainStyledAttributes(attrsArray)
|
||||
val headerHeight = array.getDimensionPixelSize(0, 0)
|
||||
array.recycle()
|
||||
ext_recycler_layout.doOnApplyWindowInsets { v, windowInsets, _ ->
|
||||
v.updateLayoutParams<MarginLayoutParams> {
|
||||
topMargin = windowInsets.systemWindowInsetTop + headerHeight -
|
||||
(sheet_layout.height)
|
||||
}
|
||||
}
|
||||
sheet_layout.setOnClickListener {
|
||||
if (!sheetBehavior.isExpanded()) {
|
||||
sheetBehavior?.expand()
|
||||
@ -92,18 +164,8 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
|
||||
fun updateExtTitle() {
|
||||
val extCount = presenter.getExtensionUpdateCount()
|
||||
title_text.text = if (extCount == 0) context.getString(R.string.extensions)
|
||||
else resources.getQuantityString(
|
||||
R.plurals.extension_updates_available,
|
||||
extCount,
|
||||
extCount
|
||||
)
|
||||
|
||||
title_text.setTextColor(
|
||||
context.getResourceColor(
|
||||
if (extCount == 0) R.attr.actionBarTintColor else R.attr.colorAccent
|
||||
)
|
||||
)
|
||||
if (extCount > 0) tabs.getTabAt(0)?.orCreateBadge?.number = extCount
|
||||
else tabs.getTabAt(0)?.removeBadge()
|
||||
}
|
||||
|
||||
override fun onButtonClick(position: Int) {
|
||||
@ -126,23 +188,55 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
|
||||
if (extension is Extension.Installed) {
|
||||
openDetails(extension)
|
||||
} else if (extension is Extension.Untrusted) {
|
||||
openTrustDialog(extension)
|
||||
}
|
||||
when (tabs.selectedTabPosition) {
|
||||
0 -> {
|
||||
val extension =
|
||||
(adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
|
||||
if (extension is Extension.Installed) {
|
||||
openDetails(extension)
|
||||
} else if (extension is Extension.Untrusted) {
|
||||
openTrustDialog(extension)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val item = migAdapter?.getItem(position) ?: return false
|
||||
|
||||
if (item is MangaItem) {
|
||||
PreMigrationController.navigateToMigration(
|
||||
Injekt.get<PreferencesHelper>().skipPreMigration().getOrDefault(),
|
||||
controller.router,
|
||||
listOf(item.manga.id!!)
|
||||
)
|
||||
} else if (item is SourceItem) {
|
||||
presenter.setSelectedSource(item.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
||||
if (extension is Extension.Installed || extension is Extension.Untrusted) {
|
||||
uninstallExtension(extension.pkgName)
|
||||
if (tabs.selectedTabPosition == 0) {
|
||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
||||
if (extension is Extension.Installed || extension is Extension.Untrusted) {
|
||||
uninstallExtension(extension.pkgName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAllClick(position: Int) {
|
||||
val item = migAdapter?.getItem(position) as? SourceItem ?: return
|
||||
|
||||
val sourceMangas =
|
||||
presenter.mangaItems[item.source.id]?.mapNotNull { it.manga.id }?.toList()
|
||||
?: emptyList()
|
||||
PreMigrationController.navigateToMigration(
|
||||
Injekt.get<PreferencesHelper>().skipPreMigration().getOrDefault(),
|
||||
controller.router,
|
||||
sourceMangas
|
||||
)
|
||||
}
|
||||
|
||||
private fun openDetails(extension: Extension.Installed) {
|
||||
val controller = ExtensionDetailsController(extension.pkgName)
|
||||
this.controller.router.pushController(controller.withFadeTransaction())
|
||||
@ -159,8 +253,28 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
drawExtensions()
|
||||
}
|
||||
|
||||
fun setMigrationSources(sources: List<SourceItem>) {
|
||||
val migRecyler = migrationFrameLayout.recycler
|
||||
if (migAdapter !is SourceAdapter) {
|
||||
migAdapter = SourceAdapter(this)
|
||||
migRecyler.adapter = migAdapter
|
||||
migAdapter?.fastScroller = migrationFrameLayout.fast_scroller
|
||||
}
|
||||
migAdapter?.updateDataSet(sources)
|
||||
}
|
||||
|
||||
fun setMigrationManga(manga: List<MangaItem>?) {
|
||||
val migRecyler = migrationFrameLayout.recycler
|
||||
if (migAdapter !is MangaAdapter) {
|
||||
migAdapter = MangaAdapter(this)
|
||||
migRecyler.adapter = migAdapter
|
||||
migAdapter?.fastScroller = migrationFrameLayout.fast_scroller
|
||||
}
|
||||
migAdapter?.updateDataSet(manga)
|
||||
}
|
||||
|
||||
fun drawExtensions() {
|
||||
if (!controller.extQuery.isBlank()) {
|
||||
if (controller.extQuery.isNotBlank()) {
|
||||
adapter?.updateDataSet(
|
||||
extensions.filter {
|
||||
it.extension.name.contains(controller.extQuery, ignoreCase = true)
|
||||
@ -172,6 +286,14 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
updateExtTitle()
|
||||
}
|
||||
|
||||
fun canGoBack(): Boolean {
|
||||
if (tabs.selectedTabPosition == 1 && migAdapter is MangaAdapter) {
|
||||
presenter.deselectSource()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun downloadUpdate(item: ExtensionItem) {
|
||||
adapter?.updateItem(item, item.installStep)
|
||||
}
|
||||
@ -183,4 +305,25 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
override fun uninstallExtension(pkgName: String) {
|
||||
presenter.uninstallExtension(pkgName)
|
||||
}
|
||||
|
||||
private inner class TabbedSheetAdapter : ViewPagerAdapter() {
|
||||
|
||||
override fun createView(container: ViewGroup, position: Int): View {
|
||||
return when (position) {
|
||||
0 -> extensionFrameLayout
|
||||
else -> migrationFrameLayout
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return context.getString(when (position) {
|
||||
0 -> R.string.extensions
|
||||
else -> R.string.migration
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.ui.migration
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
|
||||
class MangaAdapter(controller: MigrationController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller) {
|
||||
class MangaAdapter(listener: Any) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, listener) {
|
||||
|
||||
private var items: List<IFlexible<*>>? = null
|
||||
|
||||
|
@ -8,8 +8,8 @@ import eu.davidea.flexibleadapter.items.IFlexible
|
||||
*
|
||||
* @param controller instance of [MigrationController].
|
||||
*/
|
||||
class SourceAdapter(val controller: MigrationController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
class SourceAdapter(val allClickListener: OnAllClickListener) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, allClickListener, true) {
|
||||
|
||||
private var items: List<IFlexible<*>>? = null
|
||||
|
||||
@ -17,11 +17,6 @@ class SourceAdapter(val controller: MigrationController) :
|
||||
setDisplayHeadersAtStartUp(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for auto item clicks.
|
||||
*/
|
||||
val allClickListener: OnAllClickListener? = controller
|
||||
|
||||
/**
|
||||
* Listener which should be called when user clicks select.
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.source
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.Activity
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@ -10,6 +11,8 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
@ -29,12 +32,15 @@ import eu.kanade.tachiyomi.ui.extension.SettingsExtensionsController
|
||||
import eu.kanade.tachiyomi.ui.main.BottomSheetController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsBrowseController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
||||
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.source.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.getBottomGestureInsets
|
||||
import eu.kanade.tachiyomi.util.system.spToPx
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.view.collapse
|
||||
import eu.kanade.tachiyomi.util.view.expand
|
||||
import eu.kanade.tachiyomi.util.view.isCollapsed
|
||||
@ -48,17 +54,20 @@ import eu.kanade.tachiyomi.util.view.updatePaddingRelative
|
||||
import eu.kanade.tachiyomi.util.view.withFadeTransaction
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.extensions_bottom_sheet.*
|
||||
import kotlinx.android.synthetic.main.extensions_bottom_sheet.ext_bottom_sheet
|
||||
import kotlinx.android.synthetic.main.extensions_bottom_sheet.sheet_layout
|
||||
import kotlinx.android.synthetic.main.extensions_bottom_sheet.view.*
|
||||
import kotlinx.android.synthetic.main.filter_bottom_sheet.*
|
||||
import kotlinx.android.synthetic.main.library_list_controller.*
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import kotlinx.android.synthetic.main.recycler_with_scroller.view.*
|
||||
import kotlinx.android.synthetic.main.rounded_category_hopper.*
|
||||
import kotlinx.android.synthetic.main.source_controller.*
|
||||
import kotlinx.android.synthetic.main.source_controller.shadow2
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* This controller shows and manages the different catalogues enabled by the user.
|
||||
@ -101,7 +110,12 @@ class SourceController :
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return if (showingExtensions)
|
||||
view?.context?.getString(R.string.extensions)
|
||||
view?.context?.getString(
|
||||
when (ext_bottom_sheet.tabs.selectedTabPosition) {
|
||||
0 -> R.string.extensions
|
||||
else -> R.string.source_migration
|
||||
}
|
||||
)
|
||||
else view?.context?.getString(R.string.sources)
|
||||
}
|
||||
|
||||
@ -144,6 +158,7 @@ class SourceController :
|
||||
|
||||
recycler?.post {
|
||||
setBottomPadding()
|
||||
setBottomSheetTabs(if (ext_bottom_sheet?.sheetBehavior.isCollapsed()) 0f else 1f)
|
||||
}
|
||||
recycler.addOnScrollListener(
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
@ -163,12 +178,10 @@ class SourceController :
|
||||
override fun onSlide(bottomSheet: View, progress: Float) {
|
||||
val recycler = recycler ?: return
|
||||
shadow2?.alpha = (1 - max(0f, progress)) * 0.25f
|
||||
activity?.appbar?.elevation = max(
|
||||
progress * 15f,
|
||||
activity?.appbar?.elevation = min(
|
||||
(1f - progress) * 15f,
|
||||
if (recycler.canScrollVertically(-1)) 15f else 0f
|
||||
)
|
||||
|
||||
sheet_layout?.alpha = 1 - progress
|
||||
activity?.appbar?.y = max(activity!!.appbar.y, -headerHeight * (1 - progress))
|
||||
val oldShow = showingExtensions
|
||||
showingExtensions = progress > 0.92f
|
||||
@ -176,9 +189,7 @@ class SourceController :
|
||||
setTitle()
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
val bottomBar = activity?.bottom_nav ?: return
|
||||
val pad = bottomBar.translationY - bottomBar.height
|
||||
ext_bottom_sheet.updatePaddingRelative(bottom = (pad * (1 - max(progress, 0f))).toInt())
|
||||
setBottomSheetTabs(max(0f, progress))
|
||||
}
|
||||
|
||||
override fun onStateChanged(p0: View, state: Int) {
|
||||
@ -189,8 +200,6 @@ class SourceController :
|
||||
if (state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
state == BottomSheetBehavior.STATE_COLLAPSED
|
||||
) {
|
||||
sheet_layout?.alpha =
|
||||
if (state == BottomSheetBehavior.STATE_COLLAPSED) 1f else 0f
|
||||
showingExtensions = state == BottomSheetBehavior.STATE_EXPANDED
|
||||
setTitle()
|
||||
if (state == BottomSheetBehavior.STATE_EXPANDED)
|
||||
@ -203,6 +212,7 @@ class SourceController :
|
||||
RetainViewMode.RETAIN_DETACH else RetainViewMode.RELEASE_DETACH
|
||||
sheet_layout.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED
|
||||
sheet_layout.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED
|
||||
setBottomSheetTabs(if (state == BottomSheetBehavior.STATE_COLLAPSED) 0f else 1f)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -212,21 +222,73 @@ class SourceController :
|
||||
}
|
||||
}
|
||||
|
||||
fun setBottomPadding() {
|
||||
fun updateTitleAndMenu() {
|
||||
setTitle()
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
fun setBottomSheetTabs(progress: Float) {
|
||||
val bottomSheet = ext_bottom_sheet ?: return
|
||||
val bottomBar = activity?.bottom_nav ?: return
|
||||
ext_bottom_sheet.updatePaddingRelative(
|
||||
bottom =
|
||||
if (ext_bottom_sheet.sheetBehavior.isExpanded()) 0 else
|
||||
max(
|
||||
(-bottomBar.translationY + bottomBar.height).toInt(),
|
||||
this@SourceController.view?.rootWindowInsets?.getBottomGestureInsets()
|
||||
?: 0
|
||||
)
|
||||
ext_bottom_sheet.tabs.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = ((activity?.appbar?.height?.minus(9f.dpToPx) ?: 0f) * progress).toInt()
|
||||
}
|
||||
val selectedColor = ColorUtils.setAlphaComponent(
|
||||
ContextCompat.getColor(ext_bottom_sheet.tabs.context, R.color.colorAccent),
|
||||
(progress * 255).toInt()
|
||||
)
|
||||
val unselectedColor = ColorUtils.setAlphaComponent(
|
||||
bottomSheet.context.getResourceColor(R.attr.colorOnBackground),
|
||||
153
|
||||
)
|
||||
ext_bottom_sheet.sheet_layout.elevation = progress * 5
|
||||
ext_bottom_sheet.pager.alpha = progress * 10
|
||||
ext_bottom_sheet.tabs.setSelectedTabIndicatorColor(selectedColor)
|
||||
ext_bottom_sheet.tabs.setTabTextColors(
|
||||
ColorUtils.blendARGB(
|
||||
bottomSheet.context.getResourceColor(R.attr.actionBarTintColor),
|
||||
unselectedColor,
|
||||
progress),
|
||||
ColorUtils.blendARGB(
|
||||
bottomSheet.context.getResourceColor(R.attr.actionBarTintColor),
|
||||
selectedColor,
|
||||
progress)
|
||||
)
|
||||
val pad = bottomBar.translationY - bottomBar.height
|
||||
val padding = (max(
|
||||
(-pad).toInt(),
|
||||
this@SourceController.view?.rootWindowInsets?.getBottomGestureInsets() ?: 0
|
||||
) * (1f - progress)).toInt()
|
||||
ext_bottom_sheet.updatePaddingRelative(
|
||||
bottom = padding
|
||||
)
|
||||
ext_bottom_sheet.sheet_layout.backgroundTintList = ColorStateList.valueOf(
|
||||
ColorUtils.blendARGB(
|
||||
bottomSheet.context.getResourceColor(R.attr.colorPrimaryVariant),
|
||||
bottomSheet.context.getResourceColor(R.attr.colorSecondary),
|
||||
progress)
|
||||
)
|
||||
}
|
||||
|
||||
fun setBottomPadding() {
|
||||
val bottomBar = activity?.bottom_nav ?: return
|
||||
ext_bottom_sheet ?: return
|
||||
val pad = bottomBar.translationY - bottomBar.height
|
||||
val padding = max(
|
||||
(-pad).toInt(),
|
||||
if (ext_bottom_sheet.sheetBehavior.isExpanded()) 0 else
|
||||
this@SourceController.view?.rootWindowInsets?.getBottomGestureInsets()
|
||||
?: 0
|
||||
)
|
||||
ext_bottom_sheet.updatePaddingRelative(
|
||||
bottom = padding
|
||||
)
|
||||
shadow2.translationY = pad
|
||||
ext_bottom_sheet.sheetBehavior?.peekHeight = 48.spToPx + ext_bottom_sheet.paddingBottom
|
||||
ext_bottom_sheet.fast_scroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
ext_bottom_sheet.sheetBehavior?.peekHeight = 60.dpToPx + padding
|
||||
ext_bottom_sheet.extensionFrameLayout.fast_scroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = -pad.toInt()
|
||||
}
|
||||
ext_bottom_sheet.migrationFrameLayout.fast_scroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = -pad.toInt()
|
||||
}
|
||||
}
|
||||
@ -247,7 +309,9 @@ class SourceController :
|
||||
|
||||
override fun handleSheetBack(): Boolean {
|
||||
if (!ext_bottom_sheet.sheetBehavior.isCollapsed()) {
|
||||
ext_bottom_sheet.sheetBehavior?.collapse()
|
||||
if (ext_bottom_sheet.canGoBack()) {
|
||||
ext_bottom_sheet.sheetBehavior?.collapse()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -265,6 +329,23 @@ class SourceController :
|
||||
ext_bottom_sheet.presenter.refreshExtensions()
|
||||
}
|
||||
if (!type.isEnter) {
|
||||
ext_bottom_sheet.canExpand = false
|
||||
activity?.appbar?.elevation =
|
||||
when {
|
||||
ext_bottom_sheet.sheetBehavior.isExpanded() -> 0f
|
||||
recycler.canScrollVertically(-1) -> 15f
|
||||
else -> 0f
|
||||
}
|
||||
} else {
|
||||
ext_bottom_sheet.presenter.refreshMigrations()
|
||||
}
|
||||
setBottomPadding()
|
||||
}
|
||||
|
||||
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeEnded(handler, type)
|
||||
if (type.isEnter) {
|
||||
ext_bottom_sheet.canExpand = true
|
||||
setBottomPadding()
|
||||
}
|
||||
}
|
||||
@ -272,6 +353,8 @@ class SourceController :
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
super.onActivityResumed(activity)
|
||||
ext_bottom_sheet?.presenter?.refreshExtensions()
|
||||
ext_bottom_sheet?.presenter?.refreshMigrations()
|
||||
setBottomPadding()
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
@ -351,21 +434,25 @@ class SourceController :
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (onRoot) (activity as? MainActivity)?.setDismissIcon(showingExtensions)
|
||||
if (showingExtensions) {
|
||||
// Inflate menu
|
||||
inflater.inflate(R.menu.extension_main, menu)
|
||||
if (ext_bottom_sheet.tabs.selectedTabPosition == 0) {
|
||||
// Inflate menu
|
||||
inflater.inflate(R.menu.extension_main, menu)
|
||||
|
||||
// Initialize search option.
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
// Initialize search option.
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
// Change hint to show global search.
|
||||
searchView.queryHint = view?.context?.getString(R.string.search_extensions)
|
||||
// Change hint to show global search.
|
||||
searchView.queryHint = view?.context?.getString(R.string.search_extensions)
|
||||
|
||||
// Create query listener which opens the global search view.
|
||||
setOnQueryTextChangeListener(searchView) {
|
||||
extQuery = it ?: ""
|
||||
ext_bottom_sheet.drawExtensions()
|
||||
true
|
||||
// Create query listener which opens the global search view.
|
||||
setOnQueryTextChangeListener(searchView) {
|
||||
extQuery = it ?: ""
|
||||
ext_bottom_sheet.drawExtensions()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
inflater.inflate(R.menu.migration_main, menu)
|
||||
}
|
||||
} else {
|
||||
// Inflate menu
|
||||
@ -410,6 +497,12 @@ class SourceController :
|
||||
).pushChangeHandler(FadeChangeHandler())
|
||||
)
|
||||
}
|
||||
R.id.action_migration_guide -> {
|
||||
activity?.openInBrowser(HELP_URL)
|
||||
}
|
||||
R.id.action_sources_settings -> {
|
||||
router.pushController(SettingsBrowseController().withFadeTransaction())
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
@ -437,4 +530,8 @@ class SourceController :
|
||||
|
||||
@Parcelize
|
||||
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long) : Parcelable
|
||||
|
||||
companion object {
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/actionBarTintColor"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,10.9c-0.61,0 -1.1,0.49 -1.1,1.1s0.49,1.1 1.1,1.1c0.61,0 1.1,-0.49 1.1,-1.1s-0.49,-1.1 -1.1,-1.1zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM14.19,14.19L6,18l3.81,-8.19L18,6l-3.81,8.19z"/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<!-- drawable/pin.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/actionBarTintColor"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
@ -9,7 +9,7 @@
|
||||
android:background="@drawable/bottom_sheet_rounded_background"
|
||||
android:backgroundTint="?android:attr/colorBackground"
|
||||
android:orientation="vertical"
|
||||
app:behavior_peekHeight="48sp"
|
||||
app:behavior_peekHeight="60sp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<LinearLayout
|
||||
@ -19,6 +19,7 @@
|
||||
android:background="@drawable/bottom_sheet_rounded_background"
|
||||
android:backgroundTint="?attr/colorPrimaryVariant"
|
||||
android:orientation="vertical"
|
||||
android:elevation="5dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@ -37,41 +38,25 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title_text"
|
||||
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
style="@style/Theme.Widget.Tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="center"
|
||||
android:textColor="?actionBarTintColor"
|
||||
android:textSize="18sp"
|
||||
tools:text="Extensions" />
|
||||
android:background="@android:color/transparent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/menu"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tabTextColor="@color/tabs_selector_background"
|
||||
app:tabIndicatorColor="?attr/colorAccent"
|
||||
app:tabRippleColor="@color/rippleColor"
|
||||
app:tabGravity="fill"
|
||||
app:tabMode="fixed" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/ext_recycler_layout"
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/ext_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/extension_card_header" />
|
||||
|
||||
<eu.kanade.tachiyomi.ui.base.MaterialFastScroll
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:fastScrollerBubbleEnabled="false" />
|
||||
|
||||
</FrameLayout>
|
||||
android:layout_height="match_parent" />
|
||||
</eu.kanade.tachiyomi.ui.extension.ExtensionBottomSheet>
|
24
app/src/main/res/layout/recycler_with_scroller.xml
Normal file
24
app/src/main/res/layout/recycler_with_scroller.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/recycler_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
tools:listitem="@layout/extension_card_header" />
|
||||
|
||||
<eu.kanade.tachiyomi.ui.base.MaterialFastScroll
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:fastScrollerBubbleEnabled="false" />
|
||||
|
||||
</FrameLayout>
|
15
app/src/main/res/menu/migration_main.xml
Normal file
15
app/src/main/res/menu/migration_main.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_sources_settings"
|
||||
android:icon="@drawable/ic_tune_24dp"
|
||||
android:title="@string/options"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_migration_guide"
|
||||
android:title="@string/help"
|
||||
android:icon="@drawable/ic_help_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
</menu>
|
@ -620,6 +620,7 @@
|
||||
<string name="migration_sources_changed">Migration sources changed</string>
|
||||
<string name="you_can_migrate_in_library">You can also migrate by selecting manga in your
|
||||
library</string>
|
||||
<string name="source_migration_guide">Source migration guide</string>
|
||||
|
||||
<!-- About section -->
|
||||
<string name="version">Version</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user