forked from recloudstream/cloudstream
resume watching and stuff
This commit is contained in:
parent
7cfb19c39b
commit
15d85422ca
21 changed files with 553 additions and 192 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,7 +83,9 @@ 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>(
|
||||
val movieEpisode =
|
||||
if (!it.type.isMovieType()) null
|
||||
else context.getKey<VideoDownloadHelper.DownloadEpisodeCached>(
|
||||
DOWNLOAD_EPISODE_CACHE,
|
||||
getFolderName(it.id.toString(), it.id.toString())
|
||||
)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 ->
|
||||
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)
|
||||
}
|
||||
|
||||
if (isDownloadedFile) {
|
||||
ctx.setLastWatched(uriData.parentId, uriData.id, uriData.episode, uriData.season, true)
|
||||
} else
|
||||
viewModel.reloadEpisodes(ctx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,16 +52,14 @@ 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)
|
||||
|
||||
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
|
||||
|
@ -75,7 +78,7 @@ class SearchViewModel : ViewModel() {
|
|||
_searchResponse.postValue(Resource.Success(list))
|
||||
}
|
||||
|
||||
fun quickSearch(query: String) = viewModelScope.launch {
|
||||
return@launch
|
||||
fun quickSearch(query: String) {
|
||||
return
|
||||
}
|
||||
}
|
|
@ -71,18 +71,18 @@ object AppUtils {
|
|||
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?) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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_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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue