resume watching and stuff

This commit is contained in:
LagradOst 2021-08-25 17:28:25 +02:00
parent 7cfb19c39b
commit 15d85422ca
21 changed files with 553 additions and 192 deletions

View file

@ -234,11 +234,10 @@ class HomePageList(
interface SearchResponse {
val name: String
val url: String // PUBLIC URL FOR OPEN IN APP
val url: String
val apiName: String
val type: TvType
val posterUrl: String?
val year: Int?
val id: Int?
}
@ -249,7 +248,7 @@ data class AnimeSearchResponse(
override val type: TvType,
override val posterUrl: String?,
override val year: Int?,
val year: Int?,
val otherName: String?,
val dubStatus: EnumSet<DubStatus>?,
@ -265,7 +264,7 @@ data class MovieSearchResponse(
override val type: TvType,
override val posterUrl: String?,
override val year: Int?,
val year: Int?,
override val id: Int? = null,
) : SearchResponse
@ -276,7 +275,7 @@ data class TvSeriesSearchResponse(
override val type: TvType,
override val posterUrl: String?,
override val year: Int?,
val year: Int?,
val episodes: Int?,
override val id: Int? = null,
) : SearchResponse

View file

@ -88,6 +88,7 @@ object DownloadButtonSetup {
info.path.toString(),
keyInfo.relativePath,
keyInfo.displayName,
click.data.parentId,
click.data.id,
headerName ?: "null",
if (click.data.episode <= 0) null else click.data.episode,

View file

@ -52,6 +52,7 @@ class DownloadViewModel : ViewModel() {
// parentId : downloadsCount
val totalDownloads = HashMap<Int, Int>()
// Gets all children downloads
withContext(Dispatchers.IO) {
for (c in children) {
@ -67,9 +68,13 @@ class DownloadViewModel : ViewModel() {
}
}
val cached = withContext(Dispatchers.IO) {
val headers = context.getKeys(DOWNLOAD_HEADER_CACHE)
headers.mapNotNull { context.getKey<VideoDownloadHelper.DownloadHeaderCached>(it) }
val cached = withContext(Dispatchers.IO) { // wont fetch useless keys
totalDownloads.entries.filter { it.value > 0 }.mapNotNull {
context.getKey<VideoDownloadHelper.DownloadHeaderCached>(
DOWNLOAD_HEADER_CACHE,
it.key.toString()
)
}
}
val visual = withContext(Dispatchers.IO) {
@ -78,10 +83,12 @@ class DownloadViewModel : ViewModel() {
val bytes = totalBytesUsedByChild[it.id] ?: 0
val currentBytes = currentBytesUsedByChild[it.id] ?: 0
if (bytes <= 0 || downloads <= 0) return@mapNotNull null
val movieEpisode = if (!it.type.isMovieType()) null else context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(
DOWNLOAD_EPISODE_CACHE,
getFolderName(it.id.toString(), it.id.toString())
)
val movieEpisode =
if (!it.type.isMovieType()) null
else context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(
DOWNLOAD_EPISODE_CACHE,
getFolderName(it.id.toString(), it.id.toString())
)
VisualDownloadHeaderCached(
0,
downloads,
@ -90,7 +97,9 @@ class DownloadViewModel : ViewModel() {
it,
movieEpisode
)
}
}.sortedBy {
(it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0)
} // episode sorting by episode, lowest to highest
}
val stat = StatFs(Environment.getExternalStorageDirectory().path)

View file

@ -13,6 +13,7 @@ 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 com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.home_result_grid.view.*
@ -44,60 +45,9 @@ class HomeChildItemAdapter(
class CardViewHolder
constructor(itemView: View, private val clickCallback: (SearchClickCallback) -> Unit) :
RecyclerView.ViewHolder(itemView) {
val cardView: ImageView = itemView.imageView
private val cardText: TextView = itemView.imageText
private val textType: TextView? = itemView.text_type
// val search_result_lang: ImageView? = itemView.search_result_lang
private val textIsDub: View? = itemView.text_is_dub
private val textIsSub: View? = itemView.text_is_sub
//val cardTextExtra: TextView? = itemView.imageTextExtra
//val imageTextProvider: TextView? = itemView.imageTextProvider
private val bg: CardView = itemView.backgroundCard
fun bind(card: SearchResponse) {
textType?.text = when (card.type) {
TvType.Anime -> "Anime"
TvType.Movie -> "Movie"
TvType.AnimeMovie -> "Movie"
TvType.ONA -> "ONA"
TvType.TvSeries -> "TV"
TvType.Cartoon -> "Cartoon"
}
// search_result_lang?.visibility = View.GONE
textIsDub?.visibility = View.GONE
textIsSub?.visibility = View.GONE
cardText.text = card.name
//imageTextProvider.text = card.apiName
cardView.setImage(card.posterUrl)
bg.setOnClickListener {
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_LOAD, it, card))
}
bg.setOnLongClickListener {
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_SHOW_METADATA, it, card))
return@setOnLongClickListener true
}
when (card) {
is AnimeSearchResponse -> {
if (card.dubStatus?.size == 1) {
//search_result_lang?.visibility = View.VISIBLE
if (card.dubStatus.contains(DubStatus.Dubbed)) {
textIsDub?.visibility = View.VISIBLE
//search_result_lang?.setColorFilter(ContextCompat.getColor(activity, R.color.dubColor))
} else if (card.dubStatus.contains(DubStatus.Subbed)) {
//search_result_lang?.setColorFilter(ContextCompat.getColor(activity, R.color.subColor))
textIsSub?.visibility = View.VISIBLE
}
}
}
}
SearchResultBuilder.bind(clickCallback, card, itemView)
}
}
}

View file

@ -24,13 +24,13 @@ 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.*
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.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.HOMEPAGE_API
@ -66,7 +66,7 @@ class HomeFragment : Fragment() {
recycle.adapter = SearchAdapter(item.list, recycle) { callback ->
handleSearchClickCallback(this, callback)
if (callback.action == SEARCH_ACTION_LOAD) {
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
bottomSheetDialogBuilder.dismiss()
}
}
@ -121,7 +121,7 @@ class HomeFragment : Fragment() {
while (random?.posterUrl == null) {
try {
random = home.items.random().list.random()
} catch (e : Exception) {
} catch (e: Exception) {
// probs Collection is empty.
}
@ -175,7 +175,7 @@ class HomeFragment : Fragment() {
val validAPIs = apis.filter { api -> api.hasMainPage }
view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api -> Pair(index, api.name) }) {
homeViewModel.load(validAPIs[itemId])
homeViewModel.loadAndCancel(validAPIs[itemId])
}
}
@ -196,6 +196,7 @@ class HomeFragment : Fragment() {
private fun reloadStored() {
context?.let { ctx ->
homeViewModel.loadResumeWatching(ctx)
homeViewModel.loadStoredData(ctx, WatchType.fromInternalId(ctx.getKey(HOME_BOOKMARK_VALUE)))
}
}
@ -235,6 +236,7 @@ class HomeFragment : Fragment() {
}
home_change_api.setOnClickListener(apiChangeClickListener)
home_change_api_loading.setOnClickListener(apiChangeClickListener)
observe(homeViewModel.apiName) {
context?.setKey(HOMEPAGE_API, it)
@ -326,6 +328,21 @@ class HomeFragment : Fragment() {
}
}
observe(homeViewModel.resumeWatching) { resumeWatching ->
home_watch_holder.visibility = if (resumeWatching.isNotEmpty()) View.VISIBLE else View.GONE
(home_watch_child_recyclerview?.adapter as HomeChildItemAdapter?)?.cardList = resumeWatching
home_watch_child_recyclerview?.adapter?.notifyDataSetChanged()
home_watch_child_more_info.setOnClickListener {
activity?.loadHomepageList(
HomePageList(
home_watch_parent_item_title?.text?.toString() ?: getString(R.string.continue_watching),
resumeWatching
)
)
}
}
home_bookmarked_child_recyclerview.adapter = HomeChildItemAdapter(ArrayList()) { callback ->
if (callback.action == SEARCH_ACTION_SHOW_METADATA) {
val id = callback.card.id
@ -342,12 +359,43 @@ class HomeFragment : Fragment() {
}
}
home_watch_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(1, R.string.action_open_watching),
Pair(0, R.string.action_remove_watching)
)
) {
if (itemId == 1) {
handleSearchClickCallback(
activity,
SearchClickCallback(SEARCH_ACTION_LOAD, callback.view, callback.card)
)
reloadStored()
}
if (itemId == 0) {
val card = callback.card
if(card is DataStoreHelper.ResumeWatchingResult) {
context?.removeLastWatched(card.parentId)
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))
homeViewModel.loadAndCancel(context?.getKey<String>(HOMEPAGE_API))
}
}

View file

@ -13,10 +13,18 @@ import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -38,6 +46,44 @@ class HomeViewModel : ViewModel() {
private val _bookmarks = MutableLiveData<List<SearchResponse>>()
val bookmarks: LiveData<List<SearchResponse>> = _bookmarks
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
fun loadResumeWatching(context: Context) = viewModelScope.launch {
val resumeWatching = withContext(Dispatchers.IO) {
context.getAllResumeStateIds().mapNotNull { id ->
context.getLastWatched(id)
}.sortedBy { -it.updateTime }
}
// val resumeWatchingResult = ArrayList<DataStoreHelper.ResumeWatchingResult>()
val resumeWatchingResult = withContext(Dispatchers.IO) {
resumeWatching.map { resume ->
val data = context.getKey<VideoDownloadHelper.DownloadHeaderCached>(
DOWNLOAD_HEADER_CACHE,
resume.parentId.toString()
) ?: return@map null
val watchPos = context.getViewPos(resume.episodeId)
DataStoreHelper.ResumeWatchingResult(
data.name,
data.url,
data.apiName,
data.type,
data.poster,
watchPos,
resume.episodeId,
resume.parentId,
resume.episode,
resume.season,
resume.isFromDownload
)
}.filterNotNull()
}
_resumeWatching.postValue(resumeWatchingResult)
}
fun loadStoredData(context: Context, preferredWatchStatus: WatchType?) = viewModelScope.launch {
val watchStatusIds = withContext(Dispatchers.IO) {
context.getAllWatchStateIds().map { id ->
@ -78,19 +124,26 @@ class HomeViewModel : ViewModel() {
_bookmarks.postValue(list)
}
fun load(api: MainAPI?) = viewModelScope.launch {
var onGoingLoad: Job? = null
fun loadAndCancel(api: MainAPI?) {
onGoingLoad?.cancel()
onGoingLoad = load(api)
}
private fun load(api: MainAPI?) = viewModelScope.launch {
repo = if (api?.hasMainPage == true) {
APIRepository(api)
} else {
autoloadRepo()
}
_apiName.postValue(repo?.name)
_page.postValue(Resource.Loading())
_page.postValue(repo?.getMainPage())
}
fun load(preferredApiName: String?) = viewModelScope.launch {
fun loadAndCancel(preferredApiName: String?) = viewModelScope.launch {
val api = getApiFromNameNull(preferredApiName)
load(api)
loadAndCancel(api)
}
}

View file

@ -77,6 +77,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.setLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -146,6 +147,7 @@ data class UriData(
val uri: String,
val relativePath: String,
val displayName: String,
val parentId: Int?,
val id: Int?,
val name: String,
val episode: Int?,
@ -652,12 +654,24 @@ class PlayerFragment : Fragment() {
if (this::exoPlayer.isInitialized) {
if (exoPlayer.duration > 0 && exoPlayer.currentPosition > 0) {
context?.let { ctx ->
ctx.setViewPos(
if (isDownloadedFile) uriData.id else getEpisode()?.id,
exoPlayer.currentPosition,
exoPlayer.duration
)
if (!isDownloadedFile)
if (this::viewModel.isInitialized) {
viewModel.setViewPos(
ctx,
if (isDownloadedFile) uriData.id else getEpisode()?.id,
exoPlayer.currentPosition,
exoPlayer.duration
)
} else {
ctx.setViewPos(
if (isDownloadedFile) uriData.id else getEpisode()?.id,
exoPlayer.currentPosition,
exoPlayer.duration
)
}
if (isDownloadedFile) {
ctx.setLastWatched(uriData.parentId, uriData.id, uriData.episode, uriData.season, true)
} else
viewModel.reloadEpisodes(ctx)
}
}

View file

@ -72,6 +72,9 @@ const val MAX_SYNO_LENGH = 300
const val START_ACTION_NORMAL = 0
const val START_ACTION_RESUME_LATEST = 1
const val START_ACTION_LOAD_EP = 2
const val START_VALUE_NORMAL = 0
data class ResultEpisode(
val name: String?,
@ -140,12 +143,13 @@ fun ResultEpisode.getWatchProgress(): Float {
class ResultFragment : Fragment() {
companion object {
fun newInstance(url: String, apiName: String, startAction: Int = 0) =
fun newInstance(url: String, apiName: String, startAction: Int = 0, startValue : Int = 0) =
ResultFragment().apply {
arguments = Bundle().apply {
putString("url", url)
putString("apiName", apiName)
putInt("startAction", startAction)
putInt("startValue", startValue)
}
}
}
@ -231,6 +235,7 @@ class ResultFragment : Fragment() {
}
var startAction: Int? = null
var startValue: Int? = null
private fun lateFixDownloadButton(show: Boolean) {
if (!show || currentType?.isMovieType() == false) {
@ -267,6 +272,7 @@ class ResultFragment : Fragment() {
url = arguments?.getString("url")
val apiName = arguments?.getString("apiName") ?: return
startAction = arguments?.getInt("startAction") ?: START_ACTION_NORMAL
startValue = arguments?.getInt("startValue") ?: START_VALUE_NORMAL
val api = getApiFromName(apiName)
if (media_route_button != null) {
@ -443,7 +449,8 @@ class ResultFragment : Fragment() {
// SET VISUAL KEYS
ctx.setKey(
DOWNLOAD_HEADER_CACHE, parentId.toString(),
DOWNLOAD_HEADER_CACHE,
parentId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
url ?: return@let,
@ -749,13 +756,21 @@ class ResultFragment : Fragment() {
continue
}
handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep))
startAction = null
break
}
}
START_ACTION_LOAD_EP -> {
for (ep in episodeList) {
if (ep.id == startValue) { // watched too much
handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep))
break
}
}
}
else -> {
}
}
startAction = null
}
observe(viewModel.allEpisodes) {

View file

@ -9,18 +9,25 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.Exception
const val EPISODE_RANGE_SIZE = 50
const val EPISODE_RANGE_OVERLOAD = 60
@ -30,6 +37,8 @@ class ResultViewModel : ViewModel() {
private val _resultResponse: MutableLiveData<Resource<Any?>> = MutableLiveData()
private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData()
private val episodeById: MutableLiveData<HashMap<Int, Int>> = MutableLiveData() // lookup by ID to get Index
private val _publicEpisodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData()
private val _publicEpisodesCount: MutableLiveData<Int> = MutableLiveData() // before the sorting
private val _rangeOptions: MutableLiveData<List<String>> = MutableLiveData()
@ -96,7 +105,7 @@ class ResultViewModel : ViewModel() {
}
val seasons = seasonTypes.toList().map { it.first }
seasonSelections.postValue(seasons)
if(seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS
if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS
_publicEpisodes.postValue(ArrayList())
return
}
@ -141,7 +150,7 @@ class ResultViewModel : ViewModel() {
selectedRangeInt.postValue(realRange)
selectedRange.postValue(rangeList[realRange])
} else {
val allRange ="1-${currentList.size}"
val allRange = "1-${currentList.size}"
_rangeOptions.postValue(listOf(allRange))
selectedRangeInt.postValue(0)
selectedRange.postValue(allRange)
@ -160,6 +169,11 @@ class ResultViewModel : ViewModel() {
private fun updateEpisodes(context: Context, localId: Int?, list: List<ResultEpisode>, selection: Int?) {
_episodes.postValue(list)
val set = HashMap<Int, Int>()
list.withIndex().forEach { set[it.value.id] = it.index }
episodeById.postValue(set)
filterEpisodes(
context,
list,
@ -176,6 +190,40 @@ class ResultViewModel : ViewModel() {
updateEpisodes(context, null, copy, selectedSeason.value)
}
fun setViewPos(context: Context?, episodeId: Int?, pos: Long, dur: Long) {
try {
if (context == null || episodeId == null) return
context.setViewPos(episodeId, pos, dur)
var index = episodeById.value?.get(episodeId) ?: return
var startPos = pos
var startDur = dur
val episodeList = (episodes.value ?: return)
var episode = episodeList[index]
val parentId = id.value ?: return
while (true) {
if (startDur > 0L && (startPos * 100 / startDur) > 95) {
index++
if (episodeList.size <= index) { // last episode
context.removeLastWatched(parentId)
return
}
episode = episodeList[index]
startPos = episode.position
startDur = episode.duration
continue
} else {
context.setLastWatched(parentId, episode.id, episode.episode, episode.season)
return
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun load(context: Context, url: String, apiName: String) = viewModelScope.launch {
_resultResponse.postValue(Resource.Loading(url))
@ -195,14 +243,28 @@ class ResultViewModel : ViewModel() {
id.postValue(mainId)
loadWatchStatus(context, mainId)
context.setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
url,
d.type,
d.name,
d.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
when (d) {
is AnimeLoadResponse -> {
val isDub = d.dubEpisodes != null && d.dubEpisodes.size > 0
val isDub = d.dubEpisodes != null && d.dubEpisodes.isNotEmpty()
dubStatus.postValue(if (isDub) DubStatus.Dubbed else DubStatus.Subbed)
val dataList = (if (isDub) d.dubEpisodes else d.subEpisodes)
if (dataList != null) {
if (dataList != null) { // TODO dub and sub at the same time
val episodes = ArrayList<ResultEpisode>()
for ((index, i) in dataList.withIndex()) {
episodes.add(

View file

@ -28,6 +28,7 @@ import kotlin.math.roundToInt
const val SEARCH_ACTION_LOAD = 0
const val SEARCH_ACTION_SHOW_METADATA = 1
const val SEARCH_ACTION_PLAY_FILE = 2
class SearchClickCallback(val action: Int, val view: View, val card: SearchResponse)
@ -66,16 +67,7 @@ class SearchAdapter(
) :
RecyclerView.ViewHolder(itemView) {
val cardView: ImageView = itemView.imageView
private val cardText: TextView = itemView.imageText
private val textType: TextView? = itemView.text_type
// val search_result_lang: ImageView? = itemView.search_result_lang
private val textIsDub: View? = itemView.text_is_dub
private val textIsSub: View? = itemView.text_is_sub
//val cardTextExtra: TextView? = itemView.imageTextExtra
//val imageTextProvider: TextView? = itemView.imageTextProvider
private val bg: CardView = itemView.backgroundCard
private val compactView = itemView.context.getGridIsCompact()
private val coverHeight: Int = if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt()
@ -89,47 +81,7 @@ class SearchAdapter(
}
}
textType?.text = when (card.type) {
TvType.Anime -> "Anime"
TvType.Movie -> "Movie"
TvType.AnimeMovie -> "Movie"
TvType.ONA -> "ONA"
TvType.TvSeries -> "TV"
TvType.Cartoon -> "Cartoon"
}
// search_result_lang?.visibility = View.GONE
textIsDub?.visibility = View.GONE
textIsSub?.visibility = View.GONE
cardText.text = card.name
//imageTextProvider.text = card.apiName
cardView.setImage(card.posterUrl)
bg.setOnClickListener {
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_LOAD, it, card))
}
bg.setOnLongClickListener {
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_SHOW_METADATA, it, card))
return@setOnLongClickListener true
}
when (card) {
is AnimeSearchResponse -> {
if (card.dubStatus?.size == 1) {
//search_result_lang?.visibility = View.VISIBLE
if (card.dubStatus.contains(DubStatus.Dubbed)) {
textIsDub?.visibility = View.VISIBLE
//search_result_lang?.setColorFilter(ContextCompat.getColor(activity, R.color.dubColor))
} else if (card.dubStatus.contains(DubStatus.Subbed)) {
//search_result_lang?.setColorFilter(ContextCompat.getColor(activity, R.color.subColor))
textIsSub?.visibility = View.VISIBLE
}
}
}
}
SearchResultBuilder.bind(clickCallback, card, itemView)
}
}
}

View file

@ -305,7 +305,7 @@ class SearchFragment : Fragment() {
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
searchViewModel.search(query)
searchViewModel.searchAndCancel(query)
return true
}

View file

@ -3,7 +3,14 @@ package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.widget.Toast
import com.lagradost.cloudstream3.MainActivity.Companion.showToast
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
object SearchHelper {
fun handleSearchClickCallback(activity: Activity?, callback: SearchClickCallback) {
@ -12,6 +19,32 @@ object SearchHelper {
SEARCH_ACTION_LOAD -> {
activity.loadSearchResult(card)
}
SEARCH_ACTION_PLAY_FILE -> {
if (card is DataStoreHelper.ResumeWatchingResult && card.id != null) {
if (card.isFromDownload) {
handleDownloadClick(
activity, card.name, DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE,
VideoDownloadHelper.DownloadEpisodeCached(
card.name,
card.posterUrl,
card.episode ?: 0,
card.season,
card.id!!,
card.parentId ?: return,
null,
null,
System.currentTimeMillis()
)
)
)
} else {
activity.loadSearchResult(card, START_ACTION_LOAD_EP, card.id!!)
}
} else {
handleSearchClickCallback(activity, SearchClickCallback(SEARCH_ACTION_LOAD,callback.view,callback.card))
}
}
SEARCH_ACTION_SHOW_METADATA -> {
showToast(activity, callback.card.name, Toast.LENGTH_SHORT)
}

View file

@ -0,0 +1,85 @@
package com.lagradost.cloudstream3.ui.search
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.lagradost.cloudstream3.AnimeSearchResponse
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.isMovieType
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.home_result_grid.view.*
object SearchResultBuilder {
fun bind(
clickCallback: (SearchClickCallback) -> Unit,
card: SearchResponse,
itemView: View
) {
val cardView: ImageView = itemView.imageView
val cardText: TextView = itemView.imageText
val textIsDub: View? = itemView.text_is_dub
val textIsSub: View? = itemView.text_is_sub
val bg: CardView = itemView.backgroundCard
val bar: ProgressBar? = itemView.watchProgress
val playImg: ImageView? = itemView.search_item_download_play
// Do logic
bar?.visibility = View.GONE
playImg?.visibility = View.GONE
textIsDub?.visibility = View.GONE
textIsSub?.visibility = View.GONE
cardText.text = card.name
//imageTextProvider.text = card.apiName
cardView.setImage(card.posterUrl)
bg.setOnClickListener {
clickCallback.invoke(SearchClickCallback(if(card is DataStoreHelper.ResumeWatchingResult) SEARCH_ACTION_PLAY_FILE else SEARCH_ACTION_LOAD, it, card))
}
bg.setOnLongClickListener {
clickCallback.invoke(SearchClickCallback(SEARCH_ACTION_SHOW_METADATA, it, card))
return@setOnLongClickListener true
}
when (card) {
is DataStoreHelper.ResumeWatchingResult -> {
val pos = card.watchPos?.fixVisual()
if (pos != null) {
bar?.max = (pos.duration / 1000).toInt()
bar?.progress = (pos.position / 1000).toInt()
bar?.visibility = View.VISIBLE
}
playImg?.visibility = View.VISIBLE
if (!card.type.isMovieType()) {
cardText.text = getNameFull(card.name, card.episode, card.season)
}
}
is AnimeSearchResponse -> {
if (card.dubStatus?.size == 1) {
//search_result_lang?.visibility = View.VISIBLE
if (card.dubStatus.contains(DubStatus.Dubbed)) {
textIsDub?.visibility = View.VISIBLE
//search_result_lang?.setColorFilter(ContextCompat.getColor(activity, R.color.dubColor))
} else if (card.dubStatus.contains(DubStatus.Subbed)) {
//search_result_lang?.setColorFilter(ContextCompat.getColor(activity, R.color.subColor))
textIsSub?.visibility = View.VISIBLE
}
}
}
}
}
}

View file

@ -9,6 +9,7 @@ import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.APIRepository.Companion.providersActive
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
data class OnGoingSearch(
@ -23,20 +24,24 @@ class SearchViewModel : ViewModel() {
private val _currentSearch: MutableLiveData<ArrayList<OnGoingSearch>> = MutableLiveData()
val currentSearch: LiveData<ArrayList<OnGoingSearch>> get() = _currentSearch
var searchCounter = 0
private val repos = apis.map { APIRepository(it) }
private fun clearSearch() {
_searchResponse.postValue(Resource.Success(ArrayList()))
}
fun search(query: String) = viewModelScope.launch {
searchCounter++
var onGoingSearch : Job? = null
fun searchAndCancel(query: String) {
onGoingSearch?.cancel()
onGoingSearch = search(query)
}
private fun search(query: String) = viewModelScope.launch {
if (query.length <= 1) {
clearSearch()
return@launch
}
val localSearchCounter = searchCounter
_searchResponse.postValue(Resource.Loading())
val currentList = ArrayList<OnGoingSearch>()
@ -47,35 +52,33 @@ class SearchViewModel : ViewModel() {
(providersActive.size == 0 || providersActive.contains(a.name))
}.map { a ->
currentList.add(OnGoingSearch(a.name, a.search(query)))
if (localSearchCounter == searchCounter) {
_currentSearch.postValue(currentList)
}
_currentSearch.postValue(currentList)
}
_currentSearch.postValue(currentList)
if (localSearchCounter != searchCounter) return@launch
val list = ArrayList<SearchResponse>()
val nestedList = currentList.map { it.data }.filterIsInstance<Resource.Success<List<SearchResponse>>>().map { it.value }
val nestedList =
currentList.map { it.data }.filterIsInstance<Resource.Success<List<SearchResponse>>>().map { it.value }
// I do it this way to move the relevant search results to the top
var index = 0
while (true) {
var added = 0
for (sublist in nestedList) {
if(sublist.size > index) {
if (sublist.size > index) {
list.add(sublist[index])
added++
}
}
if(added == 0) break
if (added == 0) break
index++
}
_searchResponse.postValue(Resource.Success(list))
}
fun quickSearch(query: String) = viewModelScope.launch {
return@launch
fun quickSearch(query: String) {
return
}
}

View file

@ -50,39 +50,39 @@ object AppUtils {
* | Episode 2
* **/
fun getNameFull(name: String?, episode: Int?, season: Int?): String {
val rEpisode = if(episode == 0) null else episode
val rSeason = if(season == 0) null else season
val rEpisode = if (episode == 0) null else episode
val rSeason = if (season == 0) null else season
if (name != null) {
return if(rEpisode != null && rSeason != null) {
return if (rEpisode != null && rSeason != null) {
"S${rSeason}:E${rEpisode} $name"
} else if(rEpisode != null) {
} else if (rEpisode != null) {
"Episode $rEpisode. $name"
} else {
name
}
} else {
if(rEpisode != null && rSeason != null) {
if (rEpisode != null && rSeason != null) {
return "Season $rSeason - Episode $rEpisode"
} else if(rSeason == null) {
} else if (rSeason == null) {
return "Episode $rEpisode"
}
}
return ""
}
fun AppCompatActivity.loadResult(url: String, apiName: String, startAction: Int = 0) {
fun AppCompatActivity.loadResult(url: String, apiName: String, startAction: Int = 0, startValue: Int = 0) {
this.runOnUiThread {
viewModelStore.clear()
this.supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_anim, R.anim.exit_anim, R.anim.pop_enter, R.anim.pop_exit)
.add(R.id.homeRoot, ResultFragment.newInstance(url, apiName, startAction))
.add(R.id.homeRoot, ResultFragment.newInstance(url, apiName, startAction, startValue))
.commit()
}
}
fun Activity?.loadSearchResult(card: SearchResponse, startAction: Int = 0) {
(this as AppCompatActivity?)?.loadResult(card.url, card.apiName, startAction)
fun Activity?.loadSearchResult(card: SearchResponse, startAction: Int = 0, startValue: Int = 0) {
(this as AppCompatActivity?)?.loadResult(card.url, card.apiName, startAction, startValue)
}
fun Activity.requestLocalAudioFocus(focusRequest: AudioFocusRequest?) {

View file

@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
//const val WATCH_HEADER_CACHE = "watch_header_cache"
const val DOWNLOAD_EPISODE_CACHE = "download_episode_cache"
const val VIDEO_PLAYER_BRIGHTNESS = "video_player_alpha"
const val HOMEPAGE_API = "home_api_used"

View file

@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey
const val VIDEO_POS_DUR = "video_pos_dur"
const val RESULT_WATCH_STATE = "result_watch_state"
const val RESULT_WATCH_STATE_DATA = "result_watch_state_data"
const val RESULT_RESUME_WATCHING = "result_resume_watching"
const val RESULT_SEASON = "result_season"
object DataStoreHelper {
@ -35,7 +36,23 @@ object DataStoreHelper {
override val apiName: String,
override val type: TvType,
override val posterUrl: String?,
override val year: Int?,
val year: Int?,
) : SearchResponse
data class ResumeWatchingResult(
override val name: String,
override val url: String,
override val apiName: String,
override val type: TvType,
override val posterUrl: String?,
val watchPos: PosDur?,
override val id: Int?,
val parentId: Int?,
val episode: Int?,
val season: Int?,
val isFromDownload: Boolean,
) : SearchResponse
var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
@ -47,6 +64,48 @@ object DataStoreHelper {
}
}
fun Context.getAllResumeStateIds(): List<Int> {
val folder = "$currentAccount/$RESULT_RESUME_WATCHING"
return getKeys(folder).mapNotNull {
it.removePrefix("$folder/").toIntOrNull()
}
}
fun Context.setLastWatched(
parentId: Int?,
episodeId: Int?,
episode: Int?,
season: Int?,
isFromDownload: Boolean = false
) {
if (parentId == null || episodeId == null) return
setKey(
"$currentAccount/$RESULT_RESUME_WATCHING",
parentId.toString(),
VideoDownloadHelper.ResumeWatching(
parentId,
episodeId,
episode,
season,
System.currentTimeMillis(),
isFromDownload
)
)
}
fun Context.removeLastWatched(parentId: Int?) {
if (parentId == null) return
removeKey("$currentAccount/$RESULT_RESUME_WATCHING", parentId.toString())
}
fun Context.getLastWatched(id: Int?): VideoDownloadHelper.ResumeWatching? {
if (id == null) return null
return getKey(
"$currentAccount/$RESULT_RESUME_WATCHING",
id.toString(),
)
}
fun Context.setBookmarkedData(id: Int?, data: BookmarkedData) {
if (id == null) return
setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data)

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
@ -25,4 +26,13 @@ object VideoDownloadHelper {
val id: Int,
val cacheTime: Long,
)
data class ResumeWatching(
val parentId: Int,
val episodeId: Int,
val episode: Int?,
val season: Int?,
val updateTime : Long,
val isFromDownload: Boolean,
)
}

View file

@ -9,14 +9,30 @@
android:id="@+id/home_root"
tools:context=".ui.home.HomeFragment">
<ProgressBar
android:visibility="visible"
tools:visibility="gone"
<FrameLayout
android:id="@+id/home_loading"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp">
</ProgressBar>
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_gravity="center"
android:visibility="visible"
tools:visibility="visible"
android:layout_width="50dp"
android:layout_height="50dp">
</ProgressBar>
<ImageView
android:id="@+id/home_change_api_loading"
android:layout_margin="10dp"
android:layout_gravity="end"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_outline_settings_24"
android:layout_width="25dp"
android:layout_height="25dp"
android:contentDescription="@string/home_change_provider">
</ImageView>
</FrameLayout>
<LinearLayout
tools:visibility="gone"
android:id="@+id/home_loading_error"
@ -33,8 +49,7 @@
android:id="@+id/home_reload_connectionerror"
android:layout_width="wrap_content"
android:minWidth="200dp"
>
</com.google.android.material.button.MaterialButton>
/>
<com.google.android.material.button.MaterialButton
android:layout_gravity="center"
style="@style/BlackButton"
@ -44,8 +59,7 @@
android:id="@+id/home_reload_connection_open_in_browser"
android:layout_width="wrap_content"
android:minWidth="200dp"
>
</com.google.android.material.button.MaterialButton>
/>
<TextView
android:layout_margin="5dp"
android:gravity="center"
@ -53,8 +67,8 @@
android:id="@+id/result_error_text"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</TextView>
android:layout_height="wrap_content"
/>
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/home_loaded"
@ -156,6 +170,51 @@
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/home_watch_holder"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:id="@+id/home_watch_child_more_info"
android:padding="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_gravity="center_vertical"
android:id="@+id/home_watch_parent_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/textColor"
android:gravity="center_vertical"
android:textSize="18sp"
android:textStyle="bold"
android:text="@string/continue_watching"
/>
<ImageView
android:layout_marginEnd="5dp"
android:layout_gravity="end|center_vertical"
android:src="@drawable/ic_baseline_arrow_forward_24"
android:layout_width="30dp"
android:layout_height="match_parent"
android:contentDescription="@string/home_more_info">
</ImageView>
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:id="@+id/home_watch_child_recyclerview"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:listitem="@layout/home_result_grid"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/home_bookmarked_holder"
android:orientation="vertical"

View file

@ -46,22 +46,26 @@
android:paddingEnd="5dp"
android:ellipsize="end"
/>
<TextView
android:text="Movie"
android:visibility="gone"
android:id="@+id/text_type"
android:textColor="@color/textColor"
android:paddingRight="10dp"
android:paddingLeft="10dp"
android:paddingTop="4dp"
android:layout_marginBottom="5dp"
android:layout_gravity="start"
android:paddingBottom="8dp"
android:minWidth="50dp"
android:gravity="center"
android:background="@drawable/type_bg_color"
android:layout_width="wrap_content" android:layout_height="wrap_content">
</TextView>
<ImageView
android:id="@+id/search_item_download_play"
android:layout_gravity="center"
android:src="@drawable/play_button"
android:layout_width="60dp"
android:layout_height="60dp">
</ImageView>
<androidx.core.widget.ContentLoadingProgressBar
android:layout_marginBottom="-1.5dp"
android:id="@+id/watchProgress"
android:progressTint="@color/colorPrimary"
android:progressBackgroundTint="@color/colorPrimary"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
tools:progress="50"
android:layout_gravity="bottom"
android:layout_height="5dp">
</androidx.core.widget.ContentLoadingProgressBar>
<!--<View
android:id="@+id/search_result_lang"
android:layout_gravity="bottom"

View file

@ -91,4 +91,8 @@
<string name="subs_auto_select_language">Auto Select Language</string>
<string name="subs_download_languages">Download Languages</string>
<string name="subs_hold_to_reset_to_default">Hold to reset to default</string>
<string name="continue_watching">Continue Watching</string>
<string name="action_remove_watching">Remove</string>
<string name="action_open_watching">More Info</string>
</resources>