WIP adding open with specified extension

This commit is contained in:
Blatzar 2022-12-28 20:26:59 +01:00
parent 2a2a0a26e7
commit 8cdc31ffcd
11 changed files with 203 additions and 69 deletions

View file

@ -10,10 +10,10 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.metaproviders.SyncIdName
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*

View file

@ -3,27 +3,24 @@ package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil
enum class SyncIdName {
AniList,
MyAnimeList,
Trakt,
Imdb
}
object SyncRedirector { object SyncRedirector {
val syncApis = SyncApis val syncApis = SyncApis
private val syncIds = private val syncIds =
listOf( listOf(
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""), SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
SyncIdName.AniList to Regex("""anilist\.co\/anime\/(\d+)""") SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
) )
suspend fun redirect(url: String, providerApi: MainAPI): String { suspend fun redirect(
url: String,
providerApi: MainAPI
): String {
// Deprecated since providers should do this instead!
// Tries built in ID -> ProviderUrl // Tries built in ID -> ProviderUrl
/*
for (api in syncApis) { for (api in syncApis) {
if (url.contains(api.mainUrl)) { if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) { val otherApi = when (api.name) {
@ -42,8 +39,10 @@ object SyncRedirector {
// } // }
} }
} }
*/
// Tries provider solution // Tries provider solution
// This goes through all sync ids and finds supported id by said provider
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) -> return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
if (providerApi.supportedSyncNames.contains(syncName)) { if (providerApi.supportedSyncNames.contains(syncName)) {
syncRegex.find(url)?.value?.let { syncRegex.find(url)?.value?.let {

View file

@ -3,9 +3,18 @@ package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ui.library.LibraryItem import com.lagradost.cloudstream3.ui.library.LibraryItem
enum class SyncIdName {
Anilist,
MyAnimeList,
Trakt,
Imdb
}
interface SyncAPI : OAuth2API { interface SyncAPI : OAuth2API {
val mainUrl: String val mainUrl: String
val syncIdName: SyncIdName
/** /**
-1 -> None -1 -> None
0 -> Watching 0 -> Watching

View file

@ -12,6 +12,7 @@ class SyncRepo(private val repo: SyncAPI) {
val icon = repo.icon val icon = repo.icon
val mainUrl = repo.mainUrl val mainUrl = repo.mainUrl
val requiresLogin = repo.requiresLogin val requiresLogin = repo.requiresLogin
val syncIdName = repo.syncIdName
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> { suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
return safeApiCall { repo.score(id, status) } return safeApiCall { repo.score(id, status) }
@ -37,5 +38,7 @@ class SyncRepo(private val repo: SyncAPI) {
return normalSafeApiCall { repo.loginInfo() != null } ?: false return normalSafeApiCall { repo.loginInfo() != null } ?: false
} }
fun getIdFromUrl(url: String): String = repo.getIdFromUrl(url) fun getIdFromUrl(url: String): String? = normalSafeApiCall {
repo.getIdFromUrl(url)
}
} }

View file

@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.LibraryItem import com.lagradost.cloudstream3.ui.library.LibraryItem
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
@ -29,6 +30,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val icon = R.drawable.ic_anilist_icon override val icon = R.drawable.ic_anilist_icon
override val requiresLogin = false override val requiresLogin = false
override val createAccountUrl = "$mainUrl/signup" override val createAccountUrl = "$mainUrl/signup"
override val syncIdName = SyncIdName.Anilist
override fun loginInfo(): AuthAPI.LoginInfo? { override fun loginInfo(): AuthAPI.LoginInfo? {
// context.getUser(true)?. // context.getUser(true)?.
@ -603,6 +605,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
?: this.media.synonyms.firstOrNull() ?: this.media.synonyms.firstOrNull()
?: "", ?: "",
"https://anilist.co/anime/${this.media.id}/", "https://anilist.co/anime/${this.media.id}/",
this.media.id.toString(),
listName?.lowercase()?.capitalize() ?: return null, listName?.lowercase()?.capitalize() ?: return null,
this.progress, this.progress,
this.media.episodes, this.media.episodes,

View file

@ -13,6 +13,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.LibraryItem import com.lagradost.cloudstream3.ui.library.LibraryItem
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
@ -35,6 +36,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val apiUrl = "https://api.myanimelist.net" val apiUrl = "https://api.myanimelist.net"
override val icon = R.drawable.mal_logo override val icon = R.drawable.mal_logo
override val requiresLogin = false override val requiresLogin = false
override val syncIdName = SyncIdName.MyAnimeList
override val createAccountUrl = "$mainUrl/register.php" override val createAccountUrl = "$mainUrl/register.php"
@ -388,6 +390,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return LibraryItem( return LibraryItem(
this.node.title, this.node.title,
"https://myanimelist.net/anime/${this.node.id}/", "https://myanimelist.net/anime/${this.node.id}/",
this.node.id.toString(),
this.list_status?.status?.lowercase()?.capitalize()?.replace("_", " ") ?: "NONE", this.list_status?.status?.lowercase()?.capitalize()?.replace("_", " ") ?: "NONE",
this.list_status?.num_episodes_watched, this.list_status?.num_episodes_watched,
this.node.num_episodes, this.node.num_episodes,
@ -395,7 +398,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
"MAL", "MAL",
TvType.Anime, TvType.Anime,
this.node.main_picture?.large ?: this.node.main_picture?.medium, this.node.main_picture?.large ?: this.node.main_picture?.medium,
null, null, null null,
null,
) )
} }
} }

View file

@ -9,15 +9,35 @@ import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_library.* import kotlinx.android.synthetic.main.fragment_library.*
const val LIBRARY_FOLDER = "library_folder"
enum class LibraryOpenerType {
Provider,
Browser,
}
/** Used to store how the user wants to open said poster */
class LibraryOpener(
val openType: LibraryOpenerType,
val data: String?,
)
class LibraryFragment : Fragment() { class LibraryFragment : Fragment() {
companion object { companion object {
@ -27,8 +47,7 @@ class LibraryFragment : Fragment() {
private val libraryViewModel: LibraryViewModel by activityViewModels() private val libraryViewModel: LibraryViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
savedInstanceState: Bundle?
): View? { ): View? {
return inflater.inflate(R.layout.fragment_library, container, false) return inflater.inflate(R.layout.fragment_library, container, false)
} }
@ -38,11 +57,13 @@ class LibraryFragment : Fragment() {
context?.fixPaddingStatusbar(library_root) context?.fixPaddingStatusbar(library_root)
sort_fab?.setOnClickListener { sort_fab?.setOnClickListener {
val methods = libraryViewModel.sortingMethods val methods = libraryViewModel.sortingMethods.map {
.map { txt(it.stringRes).asString(context ?: view.context) } txt(it.stringRes).asString(
context ?: view.context
)
}
activity?.showBottomDialog( activity?.showBottomDialog(methods,
methods,
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod),
"Sort by", "Sort by",
false, false,
@ -50,8 +71,7 @@ class LibraryFragment : Fragment() {
{ {
val method = libraryViewModel.sortingMethods[it] val method = libraryViewModel.sortingMethods[it]
libraryViewModel.sort(method) libraryViewModel.sort(method)
} })
)
} }
main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
@ -72,18 +92,75 @@ class LibraryFragment : Fragment() {
val items = libraryViewModel.availableApiNames val items = libraryViewModel.availableApiNames
val currentItem = libraryViewModel.currentApiName.value val currentItem = libraryViewModel.currentApiName.value
activity?.showBottomDialog( activity?.showBottomDialog(items,
items,
items.indexOf(currentItem), items.indexOf(currentItem),
"Select library", "Select library",
false, false,
{} {}) {
) {
val selectedItem = items.getOrNull(it) ?: return@showBottomDialog val selectedItem = items.getOrNull(it) ?: return@showBottomDialog
libraryViewModel.switchList(selectedItem) libraryViewModel.switchList(selectedItem)
} }
} }
/**
* Shows a plugin selection dialogue and saves the response
**/
fun showPluginSelectionDialog(key: String, syncId: SyncIdName) {
val availableProviders = allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name }
val baseOptions = listOf(LibraryOpenerType.Browser.name)
val items = baseOptions + availableProviders
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, key)
val selectedIndex =
when {
savedSelection == null -> -1
// If provider
savedSelection.openType == LibraryOpenerType.Provider
&& savedSelection.data != null -> {
availableProviders.indexOf(savedSelection.data).takeIf { it != -1 }
?.plus(baseOptions.size) ?: -1
}
// Else base option
else -> baseOptions.indexOf(savedSelection.openType.name)
}
activity?.showBottomDialog(
items,
selectedIndex,
"Open with",
true,
{},
) {
val savedData = if (it < baseOptions.size) {
LibraryOpener(
LibraryOpenerType.valueOf(baseOptions[it]),
null
)
} else {
LibraryOpener(
LibraryOpenerType.Provider,
items[it]
)
}
setKey(
LIBRARY_FOLDER,
key,
savedData,
)
}
}
provider_selector?.setOnClickListener {
val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener
showPluginSelectionDialog(syncName.name, syncName)
}
viewpager?.setPageTransformer(LibraryScrollTransformer()) viewpager?.setPageTransformer(LibraryScrollTransformer())
viewpager?.adapter = viewpager?.adapter =
viewpager.adapter ?: ViewpagerAdapter(emptyList(), { isScrollingDown: Boolean -> viewpager.adapter ?: ViewpagerAdapter(emptyList(), { isScrollingDown: Boolean ->
@ -92,10 +169,27 @@ class LibraryFragment : Fragment() {
} else { } else {
sort_fab?.extend() sort_fab?.extend()
} }
}) { searchClickCallback -> }) callback@{ searchClickCallback ->
// To prevent future accidents
debugAssert({
searchClickCallback.card !is LibraryItem
}, {
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
})
val syncId = (searchClickCallback.card as LibraryItem).syncId
println("SEARCH CLICK $searchClickCallback") println("SEARCH CLICK $searchClickCallback")
if (searchClickCallback.action == SEARCH_ACTION_LOAD) { when (searchClickCallback.action) {
activity?.loadSearchResult(searchClickCallback.card) SEARCH_ACTION_SHOW_METADATA -> {
val syncName =
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
showPluginSelectionDialog(syncId, syncName)
}
SEARCH_ACTION_LOAD -> {
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, syncId)
activity?.loadSearchResult(searchClickCallback.card)
}
} }
} }
viewpager?.offscreenPageLimit = 2 viewpager?.offscreenPageLimit = 2

View file

@ -27,7 +27,10 @@ class LibraryViewModel : ViewModel() {
val currentApiName: LiveData<String> = _currentApiName val currentApiName: LiveData<String> = _currentApiName
private val availableSyncApis = SyncApis.filter { it.hasAccount() } private val availableSyncApis = SyncApis.filter { it.hasAccount() }
private var currentSyncApi = availableSyncApis.firstOrNull()
// TODO REMEMBER SELECTION
var currentSyncApi = availableSyncApis.firstOrNull()
private set
val availableApiNames: List<String> = availableSyncApis.map { it.name } val availableApiNames: List<String> = availableSyncApis.map { it.name }

View file

@ -42,6 +42,7 @@ data class Page(
data class LibraryItem( data class LibraryItem(
override val name: String, override val name: String,
override val url: String, override val url: String,
val syncId: String,
val listName: String, val listName: String,
val episodesCompleted: Int?, val episodesCompleted: Int?,
val episodesTotal: Int?, val episodesTotal: Int?,
@ -51,8 +52,8 @@ data class LibraryItem(
override var type: TvType?, override var type: TvType?,
override var posterUrl: String?, override var posterUrl: String?,
override var posterHeaders: Map<String, String>?, override var posterHeaders: Map<String, String>?,
override var id: Int?,
override var quality: SearchQuality?, override var quality: SearchQuality?,
override var id: Int? = null,
) : SearchResponse ) : SearchResponse
@ -90,7 +91,6 @@ class ViewpagerAdapter(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
println("DOWN ${(scrollY - oldScrollY)}")
val diff = scrollY - oldScrollY val diff = scrollY - oldScrollY
if (diff == 0) return@setOnScrollChangeListener if (diff == 0) return@setOnScrollChangeListener

View file

@ -15,7 +15,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.search_result_compact.view.* import kotlinx.android.synthetic.main.search_result_compact.view.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** Click */
const val SEARCH_ACTION_LOAD = 0 const val SEARCH_ACTION_LOAD = 0
/** Long press */
const val SEARCH_ACTION_SHOW_METADATA = 1 const val SEARCH_ACTION_SHOW_METADATA = 1
const val SEARCH_ACTION_PLAY_FILE = 2 const val SEARCH_ACTION_PLAY_FILE = 2
const val SEARCH_ACTION_FOCUSED = 4 const val SEARCH_ACTION_FOCUSED = 4

View file

@ -13,52 +13,69 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/primaryGrayBackground"> android:background="?attr/primaryGrayBackground">
<FrameLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="wrap_content"
android:layout_margin="10dp" android:orientation="horizontal">
android:background="@drawable/search_background"
android:visibility="visible"
app:layout_scrollFlags="scroll|enterAlways">
<androidx.appcompat.widget.SearchView
android:id="@+id/main_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
android:inputType="text"
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusRight="@id/search_filter"
android:paddingStart="-10dp"
app:iconifiedByDefault="false"
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView>
<ImageView <ImageView
android:id="@+id/list_selector" android:id="@+id/provider_selector"
android:layout_width="25dp" android:layout_width="25dp"
android:layout_height="25dp" android:layout_height="25dp"
android:layout_gravity="end|center_vertical" android:layout_gravity="end|center_vertical"
android:layout_margin="10dp" android:layout_marginStart="10dp"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des" android:contentDescription="@string/change_providers_img_des"
android:nextFocusLeft="@id/main_search" android:src="@drawable/ic_baseline_extension_24"
android:nextFocusRight="@id/main_search"
android:src="@drawable/ic_baseline_filter_list_24"
app:tint="?attr/textColor" /> app:tint="?attr/textColor" />
</FrameLayout> <FrameLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/search_background"
android:visibility="visible"
app:layout_scrollFlags="scroll|enterAlways">
<androidx.appcompat.widget.SearchView
android:id="@+id/main_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
android:inputType="text"
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusRight="@id/search_filter"
android:paddingStart="-10dp"
app:iconifiedByDefault="false"
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView>
<ImageView
android:id="@+id/list_selector"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_margin="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:nextFocusLeft="@id/main_search"
android:nextFocusRight="@id/main_search"
android:src="@drawable/ic_baseline_filter_list_24"
app:tint="?attr/textColor" />
</FrameLayout>
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>