mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-07-13 00:13:17 +00:00
Compare commits
2 commits
master
...
reposearch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a2b447a2 |
||
|
|
d1a5d0a39f |
6 changed files with 180 additions and 48 deletions
|
|
@ -16,7 +16,6 @@ import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileNa
|
|||
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.SerialName
|
||||
|
|
@ -26,7 +25,7 @@ import java.nio.file.AtomicMoveNotSupportedException
|
|||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Comes with the app, always available in the app, non removable.
|
||||
|
|
@ -136,17 +135,15 @@ object RepositoryManager {
|
|||
suspend fun parseRepository(url: String): Repository? {
|
||||
return safeAsync {
|
||||
// Take manifestVersion and such into account later
|
||||
app.get(convertRawGitUrl(url)).parsedSafe<Repository>()
|
||||
app.get(convertRawGitUrl(url), cacheTime = 5, cacheUnit = TimeUnit.MINUTES).parsedSafe<Repository>()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> {
|
||||
// Take manifestVersion and such into account later
|
||||
return try {
|
||||
val response = app.get(convertRawGitUrl(pluginUrls))
|
||||
// Normal parsed function not working?
|
||||
// return response.parsedSafe()
|
||||
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
|
||||
app.get(convertRawGitUrl(pluginUrls), cacheTime = 5, cacheUnit = TimeUnit.MINUTES)
|
||||
.parsed<Array<SitePlugin>>().toList()
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
emptyList()
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ import android.content.Context
|
|||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
|
|
@ -26,6 +28,7 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager
|
|||
import com.lagradost.cloudstream3.ui.BaseFragment
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.setRecycledViewPool
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setSystemBarsPadding
|
||||
|
|
@ -43,6 +46,7 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
) {
|
||||
|
||||
private val extensionViewModel: ExtensionsViewModel by activityViewModels()
|
||||
private val pluginViewModel: PluginsViewModel by activityViewModels()
|
||||
|
||||
private fun View.setLayoutWidth(weight: Int) {
|
||||
val param = LinearLayout.LayoutParams(
|
||||
|
|
@ -126,7 +130,10 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
ioSafe {
|
||||
RepositoryManager.removeRepository(uiContext.applicationContext, repo)
|
||||
RepositoryManager.removeRepository(
|
||||
uiContext.applicationContext,
|
||||
repo
|
||||
)
|
||||
extensionViewModel.loadStats()
|
||||
extensionViewModel.loadRepositories()
|
||||
}
|
||||
|
|
@ -145,10 +152,11 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
})
|
||||
}
|
||||
|
||||
observe(extensionViewModel.repositories) {
|
||||
binding.repoRecyclerView.isVisible = it.isNotEmpty()
|
||||
binding.blankRepoScreen.isVisible = it.isEmpty()
|
||||
(binding.repoRecyclerView.adapter as? RepoAdapter)?.submitList(it.toList())
|
||||
observe(extensionViewModel.repositories) { repos ->
|
||||
binding.repoRecyclerView.isVisible = repos.isNotEmpty()
|
||||
binding.blankRepoScreen.isVisible = repos.isEmpty()
|
||||
(binding.repoRecyclerView.adapter as? RepoAdapter)?.submitList(repos.toList())
|
||||
pluginViewModel.updatePluginList(binding.root.context, repos.map { it.url })
|
||||
}
|
||||
|
||||
observeNullable(extensionViewModel.pluginStats) { value ->
|
||||
|
|
@ -185,6 +193,70 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
)
|
||||
}
|
||||
|
||||
binding.pluginRecyclerView.apply {
|
||||
setLinearListLayout(
|
||||
isHorizontal = false,
|
||||
nextDown = FOCUS_SELF,
|
||||
nextRight = FOCUS_SELF,
|
||||
)
|
||||
setRecycledViewPool(PluginAdapter.sharedPool)
|
||||
adapter =
|
||||
PluginAdapter {
|
||||
val urls = extensionViewModel.repositories.value?.map { repo -> repo.url }
|
||||
?: emptyList()
|
||||
pluginViewModel.handlePluginAction(activity, urls, it, false)
|
||||
}
|
||||
}
|
||||
|
||||
observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) ->
|
||||
(binding.pluginRecyclerView.adapter as? PluginAdapter)?.submitList(list)
|
||||
if (scrollToTop) {
|
||||
binding.pluginRecyclerView.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
binding.settingsToolbar.apply {
|
||||
val searchItem = menu?.findItem(R.id.search_button)
|
||||
val searchView = searchItem?.actionView as? SearchView
|
||||
|
||||
searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionCollapse(p0: MenuItem): Boolean {
|
||||
binding.pluginRecyclerView.isVisible = false
|
||||
binding.repoRecyclerView.isVisible = true
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(p0: MenuItem): Boolean {
|
||||
binding.pluginRecyclerView.isVisible = true
|
||||
binding.repoRecyclerView.isVisible = false
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// Don't go back if active query
|
||||
setNavigationOnClickListener {
|
||||
if (searchView?.isIconified == false) {
|
||||
searchView.isIconified = true
|
||||
} else {
|
||||
dispatchBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
pluginViewModel.search(query)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
pluginViewModel.search(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
val addRepositoryClick = View.OnClickListener {
|
||||
val ctx = context ?: return@OnClickListener
|
||||
val binding = AddRepoInputBinding.inflate(LayoutInflater.from(ctx), null, false)
|
||||
|
|
@ -199,7 +271,10 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
)?.text?.toString()?.let { copiedText ->
|
||||
if (copiedText.contains(RepoAdapter.SHAREABLE_REPO_SEPARATOR)) {
|
||||
// text is of format <repository name> : <repository url>
|
||||
val (name, url) = copiedText.split(RepoAdapter.SHAREABLE_REPO_SEPARATOR, limit = 2)
|
||||
val (name, url) = copiedText.split(
|
||||
RepoAdapter.SHAREABLE_REPO_SEPARATOR,
|
||||
limit = 2
|
||||
)
|
||||
binding.repoUrlInput.setText(url.trim())
|
||||
binding.repoNameInput.setText(name.trim())
|
||||
} else {
|
||||
|
|
@ -227,7 +302,7 @@ class ExtensionsFragment : BaseFragment<FragmentExtensionsBinding>(
|
|||
|
||||
val fixedName = if (!name.isNullOrBlank()) name
|
||||
else repository.name
|
||||
val newRepo = RepositoryData(repository.iconUrl,fixedName, url)
|
||||
val newRepo = RepositoryData(repository.iconUrl, fixedName, url)
|
||||
RepositoryManager.addRepository(newRepo)
|
||||
extensionViewModel.loadStats()
|
||||
extensionViewModel.loadRepositories()
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import android.os.Bundle
|
|||
import android.view.View
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.lagradost.cloudstream3.AllLanguagesName
|
||||
import com.lagradost.cloudstream3.BuildConfig
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.ui.BaseFragment
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
|
||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||
|
|
@ -34,8 +34,7 @@ const val PLUGINS_BUNDLE_LOCAL = "isLocal"
|
|||
class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
||||
BaseFragment.BindingCreator.Inflate(FragmentPluginsBinding::inflate)
|
||||
) {
|
||||
|
||||
private val pluginViewModel: PluginsViewModel by activityViewModels()
|
||||
private lateinit var pluginViewModel: PluginsViewModel
|
||||
|
||||
override fun onDestroyView() {
|
||||
pluginViewModel.clear() // clear for the next observe
|
||||
|
|
@ -47,6 +46,8 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
|||
}
|
||||
|
||||
override fun onBindingCreated(binding: FragmentPluginsBinding) {
|
||||
pluginViewModel = ViewModelProvider(this)[PluginsViewModel::class.java]
|
||||
|
||||
// Since the ViewModel is getting reused the tvTypes must be cleared between uses
|
||||
pluginViewModel.tvTypes.clear()
|
||||
pluginViewModel.selectedLanguages = listOf()
|
||||
|
|
@ -130,9 +131,6 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
|||
dispatchBackPressed()
|
||||
}
|
||||
}
|
||||
searchView?.setOnQueryTextFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) pluginViewModel.search(null)
|
||||
}
|
||||
|
||||
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
|
|
@ -161,7 +159,7 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
|||
setRecycledViewPool(PluginAdapter.sharedPool)
|
||||
adapter =
|
||||
PluginAdapter {
|
||||
pluginViewModel.handlePluginAction(activity, url, it, isLocal)
|
||||
pluginViewModel.handlePluginAction(activity, listOf(url), it, isLocal)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +183,7 @@ class PluginsFragment : BaseFragment<FragmentPluginsBinding>(
|
|||
|
||||
binding.tvtypesChipsScroll.root.isVisible = false
|
||||
} else {
|
||||
pluginViewModel.updatePluginList(context, url)
|
||||
pluginViewModel.updatePluginList(context, listOf(url))
|
||||
binding.tvtypesChipsScroll.root.isVisible = true
|
||||
// not needed for users but may be useful for devs
|
||||
downloadAllButton?.isVisible = BuildConfig.DEBUG
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ class PluginsViewModel : ViewModel() {
|
|||
),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
viewModel?.updatePluginListPrivate(activity, repositoryUrl)
|
||||
viewModel?.updatePluginListPrivate(activity, listOf(repositoryUrl))
|
||||
} else if (list.isNotEmpty()) {
|
||||
showToast(R.string.download_failed, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
|
@ -157,11 +157,11 @@ class PluginsViewModel : ViewModel() {
|
|||
* */
|
||||
fun handlePluginAction(
|
||||
activity: Activity?,
|
||||
repositoryUrl: String,
|
||||
repositoryUrls: List<String>,
|
||||
plugin: Plugin,
|
||||
isLocal: Boolean
|
||||
) = ioSafe {
|
||||
Log.i(TAG, "handlePluginAction = $repositoryUrl, $plugin, $isLocal")
|
||||
Log.i(TAG, "handlePluginAction = ${repositoryUrls}, $plugin, $isLocal")
|
||||
|
||||
if (activity == null) return@ioSafe
|
||||
val (repo, metadata) = plugin
|
||||
|
|
@ -198,15 +198,18 @@ class PluginsViewModel : ViewModel() {
|
|||
if (isLocal)
|
||||
updatePluginListLocal()
|
||||
else
|
||||
updatePluginListPrivate(activity, repositoryUrl)
|
||||
updatePluginListPrivate(activity, repositoryUrls)
|
||||
}
|
||||
|
||||
private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) {
|
||||
private suspend fun updatePluginListPrivate(context: Context, repositoryUrls: List<String>) {
|
||||
val isAdult = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getStringSet(context.getString(R.string.prefer_media_type_key), emptySet())
|
||||
?.contains(TvType.NSFW.ordinal.toString()) == true
|
||||
|
||||
val plugins = getPlugins(repositoryUrl)
|
||||
val plugins = repositoryUrls.flatMap { repositoryUrl ->
|
||||
getPlugins(repositoryUrl)
|
||||
}
|
||||
|
||||
val list = plugins.filter {
|
||||
// Show all non-nsfw plugins or all if nsfw is enabled
|
||||
it.second.tvTypes?.contains(TvType.NSFW.name) != true || isAdult
|
||||
|
|
@ -241,16 +244,28 @@ class PluginsViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private fun List<PluginViewData>.sortByQuery(query: String?): List<PluginViewData> {
|
||||
return if (query == null) {
|
||||
return if (query.isNullOrBlank()) {
|
||||
// Return list to base state if no query
|
||||
this.sortedBy { it.plugin.second.name }
|
||||
} else {
|
||||
this.sortedBy {
|
||||
-Levenshtein.partialRatio(
|
||||
this.mapNotNull {
|
||||
// Try matching name
|
||||
val score = Levenshtein.partialRatio(
|
||||
it.plugin.second.name.lowercase(),
|
||||
query.lowercase()
|
||||
)
|
||||
}
|
||||
).takeIf { score -> score > 80 } ?:
|
||||
// Fallback to description, but limit characters to reduce lag
|
||||
it.plugin.second.description?.lowercase()?.take(64)
|
||||
?.let { description ->
|
||||
Levenshtein.partialRatio(
|
||||
description,
|
||||
query.lowercase()
|
||||
)
|
||||
}?.takeIf { score -> score > 80 } ?: return@mapNotNull null
|
||||
it to score
|
||||
}.sortedBy {
|
||||
-it.second
|
||||
}.map { it.first }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,16 +282,17 @@ class PluginsViewModel : ViewModel() {
|
|||
)
|
||||
}
|
||||
|
||||
fun updatePluginList(context: Context?, repositoryUrl: String) = viewModelScope.launchSafe {
|
||||
if (context == null) return@launchSafe
|
||||
Log.i(TAG, "updatePluginList = $repositoryUrl")
|
||||
updatePluginListPrivate(context, repositoryUrl)
|
||||
}
|
||||
fun updatePluginList(context: Context?, repositoryUrls: List<String>) =
|
||||
viewModelScope.launchSafe {
|
||||
if (context == null) return@launchSafe
|
||||
Log.i(TAG, "updatePluginList = $repositoryUrls")
|
||||
updatePluginListPrivate(context, repositoryUrls)
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
currentQuery = query
|
||||
_filteredPlugins.postValue(
|
||||
true to (filteredPlugins.value?.second?.sortByQuery(query) ?: emptyList())
|
||||
true to plugins.filterTvTypes().filterLang().sortByQuery(query)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,20 +4,39 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/extensions_root"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/standard_toolbar" />
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/settings_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:paddingTop="@dimen/navbar_height"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:menu="@menu/repository_search"
|
||||
app:navigationIconTint="?attr/iconColor"
|
||||
app:titleTextColor="?attr/textColor"
|
||||
tools:title="Overlord" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<!-- <include layout="@layout/standard_toolbar" />-->
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:clipToPadding="false"
|
||||
android:id="@+id/repo_recycler_view"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="80dp"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:clipToPadding="false"
|
||||
android:nextFocusUp="@id/settings_toolbar"
|
||||
android:nextFocusDown="@id/plugin_storage_appbar"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
|
|
@ -27,11 +46,25 @@
|
|||
tools:listitem="@layout/repository_item"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/blank_repo_screen"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:visibility="gone"
|
||||
android:id="@+id/plugin_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:clipToPadding="false"
|
||||
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusUp="@id/tvtypes_chips"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/repository_item" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/blank_repo_screen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
tools:visibility="gone">
|
||||
|
|
@ -64,14 +97,14 @@
|
|||
android:layout_height="80dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/primaryGrayBackground"
|
||||
android:baselineAligned="false"
|
||||
android:focusable="true"
|
||||
android:foreground="@drawable/outline_drawable"
|
||||
android:nextFocusRight="@id/add_repo_button_imageview"
|
||||
android:nextFocusUp="@id/repo_recycler_view"
|
||||
android:orientation="horizontal"
|
||||
android:padding="10dp"
|
||||
android:visibility="visible"
|
||||
android:baselineAligned="false">
|
||||
android:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
|
|
|
|||
13
app/src/main/res/menu/repository_search.xml
Normal file
13
app/src/main/res/menu/repository_search.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/search_button"
|
||||
android:title="@string/title_search"
|
||||
android:icon="@drawable/search_icon"
|
||||
android:searchIcon="@drawable/search_icon"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="always|collapseActionView"
|
||||
tools:ignore="AppCompatResource" />
|
||||
</menu>
|
||||
Loading…
Add table
Add a link
Reference in a new issue