bookmarks working

This commit is contained in:
LagradOst 2021-07-31 01:41:54 +02:00
parent a467060486
commit 5523d6539a
14 changed files with 233 additions and 84 deletions

View file

@ -13,8 +13,8 @@ android {
applicationId "com.lagradost.cloudstream3"
minSdkVersion 21
targetSdkVersion 30
versionCode 8
versionName "1.1.6"
versionCode 9
versionName "1.1.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View file

@ -56,7 +56,7 @@ object APIHolder {
}
fun LoadResponse.getId(): Int {
return url.replace(getApiFromName(apiName).mainUrl, "").hashCode()
return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode()
}
fun Activity.getApiSettings(): HashSet<String> {
@ -189,6 +189,7 @@ interface SearchResponse {
val type: TvType
val posterUrl: String?
val year: Int?
val id: Int?
}
data class AnimeSearchResponse(
@ -204,6 +205,7 @@ data class AnimeSearchResponse(
val dubStatus: EnumSet<DubStatus>?,
val dubEpisodes: Int?,
val subEpisodes: Int?,
override val id: Int? = null,
) : SearchResponse
data class MovieSearchResponse(
@ -214,6 +216,7 @@ data class MovieSearchResponse(
override val posterUrl: String?,
override val year: Int?,
override val id: Int? = null,
) : SearchResponse
data class TvSeriesSearchResponse(
@ -225,6 +228,7 @@ data class TvSeriesSearchResponse(
override val posterUrl: String?,
override val year: Int?,
val episodes: Int?,
override val id: Int? = null,
) : SearchResponse
interface LoadResponse {

View file

@ -13,7 +13,6 @@ import androidx.navigation.NavOptions
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
@ -31,6 +30,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.Event
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_result.*
@ -59,6 +59,8 @@ class MainActivity : AppCompatActivity() {
var isInPlayer: Boolean = false
var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false
val backEvent = Event<Boolean>()
lateinit var navOptions: NavOptions
}
@ -110,8 +112,10 @@ class MainActivity : AppCompatActivity() {
.setCustomAnimations(R.anim.enter_anim, R.anim.exit_anim, R.anim.pop_enter, R.anim.pop_exit)
.remove(currentFragment)
.commitAllowingStateLoss()
backEvent.invoke(true)
return true
}
backEvent.invoke(false)
return false
}

View file

@ -10,11 +10,14 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import kotlinx.android.synthetic.main.home_result_grid.view.*
class HomeChildItemAdapter(
var cardList: List<Any>,
private val clickCallback: (SearchResponse) -> Unit
var cardList: List<SearchResponse>,
private val clickCallback: (SearchClickCallback) -> Unit
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -38,7 +41,7 @@ class HomeChildItemAdapter(
}
class CardViewHolder
constructor(itemView: View, private val clickCallback: (SearchResponse) -> Unit) :
constructor(itemView: View, private val clickCallback: (SearchClickCallback) -> Unit) :
RecyclerView.ViewHolder(itemView) {
val cardView: ImageView = itemView.imageView
private val cardText: TextView = itemView.imageText
@ -52,9 +55,7 @@ class HomeChildItemAdapter(
//val imageTextProvider: TextView? = itemView.imageTextProvider
private val bg: CardView = itemView.backgroundCard
fun bind(card: Any) {
if (card is SearchResponse) { // GENERIC
fun bind(card: SearchResponse) {
textType?.text = when (card.type) {
TvType.Anime -> "Anime"
TvType.Movie -> "Movie"
@ -81,8 +82,12 @@ class HomeChildItemAdapter(
}
bg.setOnClickListener {
clickCallback.invoke(card)
// (activity as AppCompatActivity).loadResult(card.url, card.slug, card.apiName)
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_LOAD, it, card))
}
bg.setOnLongClickListener {
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_SHOW_METADATA, it, card))
return@setOnLongClickListener true
}
when (card) {
@ -102,4 +107,3 @@ class HomeChildItemAdapter(
}
}
}
}

View file

@ -17,26 +17,33 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.AnimeSearchResponse
import com.lagradost.cloudstream3.HomePageResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.MainActivity.Companion.backEvent
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.HOMEPAGE_API
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import kotlinx.android.synthetic.main.fragment_home.*
const val HOME_BOOKMARK_VALUE = "home_bookmarked_last"
class HomeFragment : Fragment() {
private lateinit var homeViewModel: HomeViewModel
@ -142,6 +149,28 @@ class HomeFragment : Fragment() {
fixGrid()
}
override fun onResume() {
backEvent += ::handleBack
super.onResume()
}
override fun onStop() {
backEvent -= ::handleBack
super.onStop()
}
private fun reloadStored() {
context?.let { ctx ->
homeViewModel.loadStoredData(ctx, WatchType.fromInternalId(ctx.getKey(HOME_BOOKMARK_VALUE)))
}
}
private fun handleBack(poppedFragment: Boolean) {
if (poppedFragment) {
reloadStored()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fixGrid()
@ -225,9 +254,7 @@ class HomeFragment : Fragment() {
}
}
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = ParentItemAdapter(listOf(), { card ->
activity.loadSearchResult(card)
}, { item ->
fun loadHomepageList(item: HomePageList) {
val bottomSheetDialogBuilder = BottomSheetDialog(view.context)
bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded)
val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!!
@ -242,9 +269,11 @@ class HomeFragment : Fragment() {
// Span settings
recycle.spanCount = currentSpan
recycle.adapter = SearchAdapter(item.list, recycle) { card ->
recycle.adapter = SearchAdapter(item.list, recycle) { callback ->
handleSearchClickCallback(activity, callback)
if (callback.action == SEARCH_ACTION_LOAD) {
bottomSheetDialogBuilder.dismiss()
activity.loadSearchResult(card)
}
}
val spanListener = { span: Int ->
@ -261,13 +290,66 @@ class HomeFragment : Fragment() {
(recycle.adapter as SearchAdapter).notifyDataSetChanged()
bottomSheetDialogBuilder.show()
}
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = ParentItemAdapter(listOf(), { callback ->
handleSearchClickCallback(activity, callback)
}, { item ->
loadHomepageList(item)
})
observe(homeViewModel.availableWatchStatusTypes) { availableWatchStatusTypes ->
context?.setKey(HOME_BOOKMARK_VALUE, availableWatchStatusTypes.first.internalId)
home_bookmark_select?.setOnClickListener {
it.popupMenuNoIcons(availableWatchStatusTypes.second.map { type ->
Pair(
type.internalId,
type.stringRes
)
}) {
homeViewModel.loadStoredData(it.context, WatchType.fromInternalId(this.itemId))
}
}
home_bookmarked_parent_item_title?.text = getString(availableWatchStatusTypes.first.stringRes)
}
observe(homeViewModel.bookmarks) { bookmarks ->
home_bookmarked_holder.visibility = if (bookmarks.isNotEmpty()) View.VISIBLE else View.GONE
(home_bookmarked_child_recyclerview?.adapter as HomeChildItemAdapter?)?.cardList = bookmarks
home_bookmarked_child_recyclerview?.adapter?.notifyDataSetChanged()
home_bookmarked_child_more_info.setOnClickListener {
loadHomepageList(
HomePageList(
home_bookmarked_parent_item_title?.text?.toString() ?: getString(R.string.error_bookmarks_text),
bookmarks
)
)
}
}
home_bookmarked_child_recyclerview.adapter = HomeChildItemAdapter(ArrayList()) { callback ->
if (callback.action == SEARCH_ACTION_SHOW_METADATA) {
val id = callback.card.id
if (id != null) {
callback.view.popupMenuNoIcons(listOf(Pair(0, R.string.action_remove_from_bookmarks))) {
if (itemId == 0) {
activity?.setResultWatchState(id, WatchType.NONE.internalId)
reloadStored()
}
}
}
} else {
handleSearchClickCallback(activity, callback)
}
}
context?.fixPaddingStatusbar(home_root)
home_master_recycler.adapter = adapter
home_master_recycler.layoutManager = GridLayoutManager(context, 1)
reloadStored()
homeViewModel.load(context?.getKey<String>(HOMEPAGE_API))
}
}

View file

@ -9,11 +9,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import kotlinx.android.synthetic.main.homepage_parent.view.*
class ParentItemAdapter(
var items: List<HomePageList>,
private val clickCallback: (SearchResponse) -> Unit,
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomePageList) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, i: Int): ParentViewHolder {
@ -38,7 +39,7 @@ class ParentItemAdapter(
class ParentViewHolder
constructor(
itemView: View,
private val clickCallback: (SearchResponse) -> Unit,
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomePageList) -> Unit
) :
RecyclerView.ViewHolder(itemView) {

View file

@ -33,15 +33,17 @@ class HomeViewModel : ViewModel() {
return APIRepository(apis.first { it.hasMainPage })
}
private val availableWatchStatusTypes = MutableLiveData<Pair<WatchType, List<WatchType>>>()
private val bookmarks = MutableLiveData<List<SearchResponse>>()
private val _availableWatchStatusTypes = MutableLiveData<Pair<WatchType, List<WatchType>>>()
val availableWatchStatusTypes: LiveData<Pair<WatchType, List<WatchType>>> = _availableWatchStatusTypes
private val _bookmarks = MutableLiveData<List<SearchResponse>>()
val bookmarks: LiveData<List<SearchResponse>> = _bookmarks
fun loadStoredData(context: Context, preferredWatchStatus: WatchType?) = viewModelScope.launch {
val watchStatusIds = withContext(Dispatchers.IO) {
context.getAllWatchStateIds().map { id ->
Pair(id, context.getResultWatchState(id))
}
}
}.distinctBy { it.first }
val length = WatchType.values().size
val currentWatchTypes = HashSet<WatchType>()
@ -52,21 +54,28 @@ class HomeViewModel : ViewModel() {
}
}
currentWatchTypes.remove(WatchType.NONE)
if (currentWatchTypes.size <= 0) {
bookmarks.postValue(ArrayList())
_bookmarks.postValue(ArrayList())
return@launch
}
val watchStatus = preferredWatchStatus ?: currentWatchTypes.first()
availableWatchStatusTypes.postValue(
val watchPrefNotNull = preferredWatchStatus ?: currentWatchTypes.first()
val watchStatus =
if (currentWatchTypes.contains(watchPrefNotNull)) watchPrefNotNull else currentWatchTypes.first()
_availableWatchStatusTypes.postValue(
Pair(
watchStatus,
currentWatchTypes.sortedBy { it.internalId }.toList()
)
)
val list = withContext(Dispatchers.IO) {
watchStatusIds.map { context.getBookmarkedData(it.first) }
watchStatusIds.filter { it.second == watchStatus }
.mapNotNull { context.getBookmarkedData(it.first) }
.sortedBy { -it.latestUpdatedTime }
}
_bookmarks.postValue(list)
}
fun load(api: MainAPI?) = viewModelScope.launch {

View file

@ -117,7 +117,7 @@ class EpisodeAdapter(
class EpisodeCardViewHolder
constructor(
itemView: View,
val hasDownloadSupport: Boolean,
private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
@ -129,8 +129,8 @@ class EpisodeAdapter(
private val episodeProgress: ContentLoadingProgressBar? = itemView.episode_progress
private val episodePoster: ImageView? = itemView.episode_poster
val episodeDownloadBar: ContentLoadingProgressBar = itemView.result_episode_progress_downloaded
val episodeDownloadImage: ImageView = itemView.result_episode_download
private val episodeDownloadBar: ContentLoadingProgressBar = itemView.result_episode_progress_downloaded
private val episodeDownloadImage: ImageView = itemView.result_episode_download
private val episodeHolder = itemView.episode_holder

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -24,12 +25,16 @@ import kotlinx.android.synthetic.main.search_result_compact.view.imageView
import kotlinx.android.synthetic.main.search_result_grid.view.*
import kotlin.math.roundToInt
const val SEARCH_ACTION_LOAD = 0
const val SEARCH_ACTION_SHOW_METADATA = 1
class SearchClickCallback(val action: Int, val view: View, val card: SearchResponse)
class SearchAdapter(
var cardList: List<SearchResponse>,
private val resView: AutofitRecyclerView,
private val clickCallback: (SearchResponse) -> Unit,
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val clickCallback: (SearchClickCallback) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = parent.context.getGridFormatId()
@ -54,7 +59,11 @@ class SearchAdapter(
}
class CardViewHolder
constructor(itemView: View, private val clickCallback: (SearchResponse) -> Unit, resView: AutofitRecyclerView) :
constructor(
itemView: View,
private val clickCallback: (SearchClickCallback) -> Unit,
resView: AutofitRecyclerView
) :
RecyclerView.ViewHolder(itemView) {
val cardView: ImageView = itemView.imageView
private val cardText: TextView = itemView.imageText
@ -105,7 +114,12 @@ class SearchAdapter(
}
bg.setOnClickListener {
clickCallback.invoke(card)
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_LOAD, it, card))
}
bg.setOnLongClickListener {
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_SHOW_METADATA, it, card))
return@setOnLongClickListener true
}
when (card) {
@ -121,7 +135,6 @@ class SearchAdapter(
}
}
}
}
}
}

View file

@ -9,7 +9,6 @@ import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
@ -24,11 +23,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import kotlinx.android.synthetic.main.fragment_search.*
class SearchFragment : Fragment() {
private lateinit var searchViewModel: SearchViewModel
override fun onCreateView(
@ -77,8 +74,8 @@ class SearchFragment : Fragment() {
SearchAdapter(
ArrayList(),
cardSpace,
) { card ->
activity.loadSearchResult(card)
) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
}
}

View file

@ -0,0 +1,21 @@
package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.widget.Toast
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
object SearchHelper {
fun handleSearchClickCallback(activity: Activity?, callback: SearchClickCallback) {
val card = callback.card
when (callback.action) {
SEARCH_ACTION_LOAD -> {
activity.loadSearchResult(card)
}
SEARCH_ACTION_SHOW_METADATA -> {
activity?.let { act ->
Toast.makeText(act, callback.card.name, Toast.LENGTH_SHORT).show()
}
}
}
}
}

View file

@ -6,6 +6,7 @@ import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
const val VIDEO_POS_DUR = "video_pos_dur"
@ -27,7 +28,7 @@ object DataStoreHelper {
}
data class BookmarkedData(
val parentId: Int,
override val id: Int?,
val bookmarkedTime: Long,
val latestUpdatedTime: Long,
override val name: String,
@ -42,17 +43,19 @@ object DataStoreHelper {
fun Context.getAllWatchStateIds(): List<Int> {
val folder = "$currentAccount/$RESULT_WATCH_STATE"
return getKeys(folder).mapNotNull { it.removePrefix(folder).toIntOrNull() }
return getKeys(folder).mapNotNull {
it.removePrefix("$folder/").toIntOrNull()
}
}
fun Context.setBookmarkedData(id: Int?, data: BookmarkedData) {
if (id == null) return
setKey("$currentAccount/$RESULT_WATCH_STATE", id.toString(), data)
setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data)
}
fun Context.getBookmarkedData(id: Int?): BookmarkedData? {
if (id == null) return null
return getKey("$currentAccount/$RESULT_WATCH_STATE", id.toString())
return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
}
fun Context.setViewPos(id: Int?, pos: Long, dur: Long) {
@ -66,7 +69,13 @@ object DataStoreHelper {
fun Context.setResultWatchState(id: Int?, status: Int) {
if (id == null) return
setKey("$currentAccount/$RESULT_WATCH_STATE", id.toString(), status)
val folder = "$currentAccount/$RESULT_WATCH_STATE"
if (status == WatchType.NONE.internalId) {
removeKey(folder, id.toString())
removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
} else {
setKey(folder, id.toString(), status)
}
}
fun Context.getResultWatchState(id: Int): WatchType {

View file

@ -207,6 +207,8 @@
<LinearLayout
android:id="@+id/home_bookmarked_holder"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -217,6 +219,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/home_bookmark_select"
android:src="@drawable/ic_baseline_filter_list_24"
android:layout_width="24dp"
android:layout_height="24dp"
@ -232,7 +235,7 @@
android:gravity="center_vertical"
android:textSize="18sp"
android:textStyle="bold"
android:text="Bookmarked"
tools:text="Bookmarked"
/>
<ImageView
android:layout_marginEnd="5dp"

View file

@ -64,4 +64,6 @@
<string name="background_blur">Background blur</string>
<string name="background_shadow">Background Shadow</string>
<string name="filter_bookmarks">Filter Bookmarks</string>
<string name="error_bookmarks_text">Bookmarks</string>
<string name="action_remove_from_bookmarks">Remove</string>
</resources>