mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	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,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) | ||||
|  |  | |||
|  | @ -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() | ||||
|                 } | ||||
|             } | ||||
|  | @ -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)) | ||||
|     } | ||||
| } | ||||
|  | @ -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 -> | ||||
|                     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) | ||||
|                 } | ||||
|             } | ||||
|  |  | |||
|  | @ -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() | ||||
|  | @ -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( | ||||
|  |  | |||
|  | @ -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,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 | ||||
|     } | ||||
| } | ||||
|  | @ -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?) { | ||||
|  |  | |||
|  | @ -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_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" | ||||
|  |  | |||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue