diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 331236c0..ce800f4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -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?, @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 4041a36f..15527bc8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 7a5ef0ff..7d3cf3f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -52,6 +52,7 @@ class DownloadViewModel : ViewModel() { // parentId : downloadsCount val totalDownloads = HashMap() + // 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(it) } + val cached = withContext(Dispatchers.IO) { // wont fetch useless keys + totalDownloads.entries.filter { it.value > 0 }.mapNotNull { + context.getKey( + 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( - DOWNLOAD_EPISODE_CACHE, - getFolderName(it.id.toString(), it.id.toString()) - ) + val movieEpisode = + if (!it.type.isMovieType()) null + else context.getKey( + 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) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index 2f30c9ca..96900901 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -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) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 83397c22..c0430f38 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -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(HOMEPAGE_API)) + homeViewModel.loadAndCancel(context?.getKey(HOMEPAGE_API)) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index fbbfd1d3..d327b5f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -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>() val bookmarks: LiveData> = _bookmarks + private val _resumeWatching = MutableLiveData>() + val resumeWatching: LiveData> = _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() + + val resumeWatchingResult = withContext(Dispatchers.IO) { + resumeWatching.map { resume -> + val data = context.getKey( + 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) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt index e6898c79..bb5c149e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt @@ -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) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 0f8486a2..510f7439 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -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) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index 46cedc33..8395ee20 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -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> = MutableLiveData() private val _episodes: MutableLiveData> = MutableLiveData() + private val episodeById: MutableLiveData> = MutableLiveData() // lookup by ID to get Index + private val _publicEpisodes: MutableLiveData> = MutableLiveData() private val _publicEpisodesCount: MutableLiveData = MutableLiveData() // before the sorting private val _rangeOptions: MutableLiveData> = 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, selection: Int?) { _episodes.postValue(list) + val set = HashMap() + + 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() for ((index, i) in dataList.withIndex()) { episodes.add( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index 54c6ce9b..02e47c43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -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) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 7eda47f6..f6c5a04c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -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 } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index d2534bde..2a8bc5e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -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) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt new file mode 100644 index 00000000..b8b7bb2a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -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 + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index 0d959a02..f5b893d7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -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> = MutableLiveData() val currentSearch: LiveData> 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() @@ -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() - val nestedList = currentList.map { it.data }.filterIsInstance>>().map { it.value } + val nestedList = + currentList.map { it.data }.filterIsInstance>>().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 } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 13eebd83..c7cd932b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -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?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 15ff14b0..19b958ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -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" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 5371ba0d..02a4d46c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -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 { + 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) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index 002428c7..cad52921 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -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, + ) } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 2d4a3a61..5d2cea8f 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -9,14 +9,30 @@ android:id="@+id/home_root" tools:context=".ui.home.HomeFragment"> - - + android:layout_width="match_parent" + android:layout_height="match_parent"> + + + + + + - + /> - + /> - + android:layout_height="wrap_content" + /> + + + + + + + + + + + - - + + + + + +