mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	subtitle download and other sub settings
This commit is contained in:
		
							parent
							
								
									440a9810be
								
							
						
					
					
						commit
						10f25eaefe
					
				
					 11 changed files with 547 additions and 229 deletions
				
			
		|  | @ -8,6 +8,7 @@ import com.lagradost.cloudstream3.R | |||
| import com.lagradost.cloudstream3.ui.player.PlayerFragment | ||||
| import com.lagradost.cloudstream3.ui.player.UriData | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.getNameFull | ||||
| import com.lagradost.cloudstream3.utils.DataStore.getKey | ||||
| import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadHelper | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager | ||||
|  | @ -69,7 +70,10 @@ object DownloadButtonSetup { | |||
|                     val info = | ||||
|                         VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(act, click.data.id) | ||||
|                             ?: return | ||||
| 
 | ||||
|                     val keyInfo = act.getKey<VideoDownloadManager.DownloadedFileInfo>( | ||||
|                         VideoDownloadManager.KEY_DOWNLOAD_INFO, | ||||
|                         click.data.id.toString() | ||||
|                     ) ?: return | ||||
|                     (act as FragmentActivity).supportFragmentManager.beginTransaction() | ||||
|                         .setCustomAnimations( | ||||
|                             R.anim.enter_anim, | ||||
|  | @ -82,6 +86,8 @@ object DownloadButtonSetup { | |||
|                             PlayerFragment.newInstance( | ||||
|                                 UriData( | ||||
|                                     info.path.toString(), | ||||
|                                     keyInfo.relativePath, | ||||
|                                     keyInfo.displayName, | ||||
|                                     click.data.id, | ||||
|                                     headerName ?: "null", | ||||
|                                     if (click.data.episode <= 0) null else click.data.episode, | ||||
|  |  | |||
|  | @ -66,6 +66,7 @@ import com.lagradost.cloudstream3.ui.result.ResultViewModel | |||
| import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle | ||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment | ||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.fromSaveToStyle | ||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 | ||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.getFocusRequest | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.getVideoContentUri | ||||
|  | @ -78,6 +79,7 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey | |||
| import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos | ||||
| import com.lagradost.cloudstream3.utils.ExtractorLink | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog | ||||
| import com.lagradost.cloudstream3.utils.SubtitleHelper | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | ||||
|  | @ -86,7 +88,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage | |||
| import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.toPx | ||||
| import com.lagradost.cloudstream3.utils.VIDEO_PLAYER_BRIGHTNESS | ||||
| import com.lagradost.cloudstream3.utils.getId | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager.getId | ||||
| import kotlinx.android.synthetic.main.fragment_player.* | ||||
| import kotlinx.android.synthetic.main.player_custom_layout.* | ||||
| import kotlinx.coroutines.* | ||||
|  | @ -140,6 +143,8 @@ data class PlayerData( | |||
| 
 | ||||
| data class UriData( | ||||
|     val uri: String, | ||||
|     val relativePath: String, | ||||
|     val displayName: String, | ||||
|     val id: Int?, | ||||
|     val name: String, | ||||
|     val episode: Int?, | ||||
|  | @ -280,7 +285,8 @@ class PlayerFragment : Fragment() { | |||
|         fadeAnimation.fillAfter = true | ||||
| 
 | ||||
|         subView?.let { sView -> | ||||
|             val move = if (isShowing) -((bottom_player_bar?.height?.toFloat() ?: 0f) + 10.toPx) else -subStyle.elevation.toPx.toFloat() | ||||
|             val move = if (isShowing) -((bottom_player_bar?.height?.toFloat() | ||||
|                 ?: 0f) + 10.toPx) else -subStyle.elevation.toPx.toFloat() | ||||
|             ObjectAnimator.ofFloat(sView, "translationY", move).apply { | ||||
|                 duration = 200 | ||||
|                 start() | ||||
|  | @ -810,13 +816,40 @@ class PlayerFragment : Fragment() { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun setPreferredSubLanguage(lang: String?) { | ||||
|         //val textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT) ?: return@setOnClickListener | ||||
|         val realLang = if (lang.isNullOrBlank()) "" else lang | ||||
|         preferredSubtitles = | ||||
|             if (realLang.length == 2) SubtitleHelper.fromTwoLettersToLanguage(realLang) ?: realLang else realLang | ||||
| 
 | ||||
|         if (!this::exoPlayer.isInitialized) return | ||||
|         (exoPlayer?.trackSelector as DefaultTrackSelector?)?.let { trackSelector -> | ||||
|             if (lang.isNullOrBlank()) { | ||||
|                 trackSelector.setParameters( | ||||
|                     trackSelector.buildUponParameters() | ||||
|                         .setPreferredTextLanguage(realLang) | ||||
|                     //.setRendererDisabled(textRendererIndex, true) | ||||
|                 ) | ||||
|             } else { | ||||
|                 trackSelector.setParameters( | ||||
|                     trackSelector.buildUponParameters() | ||||
|                         .setPreferredTextLanguage(realLang) | ||||
|                     //.setRendererDisabled(textRendererIndex, false) | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("SetTextI18n") | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         context?.let { ctx -> | ||||
|             setPreferredSubLanguage(ctx.getAutoSelectLanguageISO639_1()) | ||||
|         } | ||||
| 
 | ||||
|         subView = player_view.findViewById(R.id.exo_subtitles) | ||||
|         subView?.let { sView -> | ||||
|             (sView.parent as ViewGroup?) ?.removeView(sView) | ||||
|             (sView.parent as ViewGroup?)?.removeView(sView) | ||||
|             subtitle_holder.addView(sView) | ||||
|         } | ||||
| 
 | ||||
|  | @ -860,7 +893,7 @@ class PlayerFragment : Fragment() { | |||
|                             epData.index, | ||||
|                             episodes, | ||||
|                             links, | ||||
|                             getSubs() ?: ArrayList(), | ||||
|                             context?.getSubs(supportsDownloadedFiles = false) ?: emptyList(), | ||||
|                             index, | ||||
|                             exoPlayer.currentPosition | ||||
|                         ) | ||||
|  | @ -929,7 +962,12 @@ class PlayerFragment : Fragment() { | |||
|         } | ||||
| 
 | ||||
|         sources_btt.visibility = | ||||
|             if (isDownloadedFile) GONE else VISIBLE | ||||
|             if (isDownloadedFile) | ||||
|                 if (context?.getSubs()?.isNullOrEmpty() != false) | ||||
|                     GONE else VISIBLE | ||||
|             else VISIBLE | ||||
| 
 | ||||
| 
 | ||||
|         player_media_route_button.visibility = | ||||
|             if (isDownloadedFile) GONE else VISIBLE | ||||
|         if (savedInstanceState != null) { | ||||
|  | @ -1154,136 +1192,97 @@ class PlayerFragment : Fragment() { | |||
|         } | ||||
| 
 | ||||
|         sources_btt.setOnClickListener { | ||||
|             lateinit var dialog: AlertDialog | ||||
|             getUrls()?.let { it1 -> | ||||
|                 sortUrls(it1).let { sources -> | ||||
|                     val isPlaying = exoPlayer.isPlaying | ||||
|                     exoPlayer.pause() | ||||
|                     val currentSubtitles = activeSubtitles | ||||
|             val isPlaying = exoPlayer.isPlaying | ||||
|             exoPlayer.pause() | ||||
|             val currentSubtitles = activeSubtitles | ||||
| 
 | ||||
|                     val sourceBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) | ||||
|                         .setView(R.layout.player_select_source_and_subs) | ||||
|             val sourceBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack) | ||||
|                 .setView(R.layout.player_select_source_and_subs) | ||||
| 
 | ||||
|                     val sourceDialog = sourceBuilder.create() | ||||
|                     sourceDialog.show() | ||||
|                     //  bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) | ||||
|                     val providerList = sourceDialog.findViewById<ListView>(R.id.sort_providers)!! | ||||
|                     val subtitleList = sourceDialog.findViewById<ListView>(R.id.sort_subtitles)!! | ||||
|                     val applyButton = sourceDialog.findViewById<MaterialButton>(R.id.apply_btt)!! | ||||
|                     val cancelButton = sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!! | ||||
|                     val subsSettings = sourceDialog.findViewById<View>(R.id.subs_settings)!! | ||||
|             val sourceDialog = sourceBuilder.create() | ||||
|             sourceDialog.show() | ||||
|             //  bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet) | ||||
|             val providerList = sourceDialog.findViewById<ListView>(R.id.sort_providers)!! | ||||
|             val subtitleList = sourceDialog.findViewById<ListView>(R.id.sort_subtitles)!! | ||||
|             val applyButton = sourceDialog.findViewById<MaterialButton>(R.id.apply_btt)!! | ||||
|             val cancelButton = sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!! | ||||
|             val subsSettings = sourceDialog.findViewById<View>(R.id.subs_settings)!! | ||||
| 
 | ||||
|                     subsSettings.setOnClickListener { | ||||
|                         SubtitlesFragment.push(activity) | ||||
|                         sourceDialog.dismiss() | ||||
|                     } | ||||
|             subsSettings.setOnClickListener { | ||||
|                 SubtitlesFragment.push(activity) | ||||
|                 sourceDialog.dismiss() | ||||
|             } | ||||
|             var sourceIndex = 0 | ||||
|             var startSource = 0 | ||||
|             var sources: List<ExtractorLink> = emptyList() | ||||
| 
 | ||||
|                     val startSource = sources.indexOf(getCurrentUrl()) | ||||
|                     var sourceIndex = startSource | ||||
|                     val startSubtitle = currentSubtitles.indexOf(preferredSubtitles) + 1 | ||||
|                     var subtitleIndex = startSubtitle | ||||
|             val nonSortedUrls = getUrls() | ||||
|             if (nonSortedUrls.isNullOrEmpty()) { | ||||
|                 sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.visibility = GONE | ||||
|             } else { | ||||
|                 sources = sortUrls(nonSortedUrls) | ||||
|                 startSource = sources.indexOf(getCurrentUrl()) | ||||
|                 sourceIndex = startSource | ||||
| 
 | ||||
|                     if (currentSubtitles.isEmpty()) { | ||||
|                         sourceDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE | ||||
|                     } else { | ||||
|                         val subsArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice) | ||||
|                         subsArrayAdapter.add("No Subtitles") | ||||
|                         subsArrayAdapter.addAll(currentSubtitles) | ||||
|                 val sourcesArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice) | ||||
|                 sourcesArrayAdapter.addAll(sources.map { it.name }) | ||||
| 
 | ||||
|                         subtitleList.adapter = subsArrayAdapter | ||||
|                         subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE | ||||
|                 providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE | ||||
|                 providerList.adapter = sourcesArrayAdapter | ||||
|                 providerList.setSelection(sourceIndex) | ||||
|                 providerList.setItemChecked(sourceIndex, true) | ||||
| 
 | ||||
|                         subtitleList.setSelection(subtitleIndex) | ||||
|                         subtitleList.setItemChecked(subtitleIndex, true) | ||||
| 
 | ||||
|                         subtitleList.setOnItemClickListener { _, _, which, _ -> | ||||
|                             subtitleIndex = which | ||||
|                             subtitleList.setItemChecked(which, true) | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     val sourcesArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice) | ||||
|                     sourcesArrayAdapter.addAll(sources.map { it.name }) | ||||
| 
 | ||||
|                     providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE | ||||
|                     providerList.adapter = sourcesArrayAdapter | ||||
|                     providerList.setSelection(sourceIndex) | ||||
|                     providerList.setItemChecked(sourceIndex, true) | ||||
| 
 | ||||
|                     providerList.setOnItemClickListener { _, _, which, _ -> | ||||
|                         sourceIndex = which | ||||
|                         providerList.setItemChecked(which, true) | ||||
|                     } | ||||
| 
 | ||||
|                     sourceDialog.setOnDismissListener { | ||||
|                         activity?.hideSystemUI() | ||||
|                     } | ||||
| 
 | ||||
|                     cancelButton.setOnClickListener { | ||||
|                         sourceDialog.dismiss() | ||||
|                     } | ||||
| 
 | ||||
|                     applyButton.setOnClickListener { | ||||
|                         if (sourceIndex != startSource) { | ||||
|                             playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0 | ||||
|                             setMirrorId(sources[sourceIndex].getId()) | ||||
|                             initPlayer(getCurrentUrl()) | ||||
|                         } else { | ||||
|                             if (isPlaying) { | ||||
|                                 // exoPlayer.play() | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         if (subtitleIndex != startSubtitle) { | ||||
|                             val textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT) ?: return@setOnClickListener | ||||
|                             (exoPlayer.trackSelector as DefaultTrackSelector?)?.let { trackSelector -> | ||||
|                                 if (subtitleIndex <= 0) { | ||||
|                                     preferredSubtitles = "" | ||||
|                                     trackSelector.setParameters( | ||||
|                                         trackSelector.buildUponParameters() | ||||
|                                             .setPreferredTextLanguage("") | ||||
|                                             .setRendererDisabled(textRendererIndex, true) | ||||
|                                     ) | ||||
|                                 } else { | ||||
|                                     val currentPreferredSub = currentSubtitles[subtitleIndex - 1] | ||||
|                                     preferredSubtitles = currentPreferredSub | ||||
|                                     trackSelector.setParameters( | ||||
|                                         trackSelector.buildUponParameters() | ||||
|                                             .setPreferredTextLanguage(currentPreferredSub) | ||||
|                                             .setRendererDisabled(textRendererIndex, false) | ||||
|                                     ) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         sourceDialog.dismiss() | ||||
|                     } | ||||
|                     /* | ||||
| 
 | ||||
|                      */ | ||||
|                     /* | ||||
| 
 | ||||
|                     val sourcesText = sources.map { it.name } | ||||
|                     val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) | ||||
|                         builder.setTitle("Pick source") | ||||
|                         builder.setOnDismissListener { | ||||
|                             activity?.hideSystemUI() | ||||
|                         } | ||||
|                         builder.setSingleChoiceItems( | ||||
|                             sourcesText.toTypedArray(), | ||||
|                             sources.indexOf(getCurrentUrl()) | ||||
|                         ) { _, which -> | ||||
|                             //val speed = speedsText[which] | ||||
|                             //Toast.makeText(requireContext(), "$speed selected.", Toast.LENGTH_SHORT).show() | ||||
|                             playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0 | ||||
|                             setMirrorId(sources[which].getId()) | ||||
|                             initPlayer(getCurrentUrl()) | ||||
| 
 | ||||
|                             dialog.dismiss() | ||||
|                             activity?.hideSystemUI() | ||||
|                         } | ||||
|                         dialog = builder.create() | ||||
|                         dialog.show()*/ | ||||
|                 providerList.setOnItemClickListener { _, _, which, _ -> | ||||
|                     sourceIndex = which | ||||
|                     providerList.setItemChecked(which, true) | ||||
|                 } | ||||
| 
 | ||||
|                 sourceDialog.setOnDismissListener { | ||||
|                     activity?.hideSystemUI() | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             val startIndexFromMap = currentSubtitles.map { it.removeSuffix(" ") }.indexOf(preferredSubtitles.removeSuffix(" ")) + 1 | ||||
|             var subtitleIndex = startIndexFromMap | ||||
| 
 | ||||
|             if (currentSubtitles.isEmpty()) { | ||||
|                 sourceDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE | ||||
|             } else { | ||||
|                 val subsArrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice) | ||||
|                 subsArrayAdapter.add("No Subtitles") | ||||
|                 subsArrayAdapter.addAll(currentSubtitles) | ||||
| 
 | ||||
|                 subtitleList.adapter = subsArrayAdapter | ||||
|                 subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE | ||||
| 
 | ||||
|                 subtitleList.setSelection(subtitleIndex) | ||||
|                 subtitleList.setItemChecked(subtitleIndex, true) | ||||
| 
 | ||||
|                 subtitleList.setOnItemClickListener { _, _, which, _ -> | ||||
|                     subtitleIndex = which | ||||
|                     subtitleList.setItemChecked(which, true) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             cancelButton.setOnClickListener { | ||||
|                 sourceDialog.dismiss() | ||||
|             } | ||||
| 
 | ||||
|             applyButton.setOnClickListener { | ||||
|                 if (sourceIndex != startSource) { | ||||
|                     playbackPosition = if (this::exoPlayer.isInitialized) exoPlayer.currentPosition else 0 | ||||
|                     setMirrorId(sources[sourceIndex].getId()) | ||||
|                     initPlayer(getCurrentUrl()) | ||||
|                 } else { | ||||
|                     if (isPlaying) { | ||||
|                         // exoPlayer.play() | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 if (subtitleIndex != startIndexFromMap) { | ||||
|                     setPreferredSubLanguage(if (subtitleIndex <= 0) null else currentSubtitles[subtitleIndex - 1]) | ||||
|                 } | ||||
|                 sourceDialog.dismiss() | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -1347,9 +1346,25 @@ class PlayerFragment : Fragment() { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getSubs(): List<SubtitleFile>? { | ||||
|     private fun Context.getSubs(supportsDownloadedFiles: Boolean = true): List<SubtitleFile>? { | ||||
|         return try { | ||||
|             allEpisodesSubs[getEpisode()?.id] | ||||
|             if (isDownloadedFile) { | ||||
|                 if (!supportsDownloadedFiles) return null | ||||
|                 val list = ArrayList<SubtitleFile>() | ||||
|                 VideoDownloadManager.getFolder(this, uriData.relativePath)?.forEach { file -> | ||||
|                     val name = uriData.displayName.removeSuffix(".mp4") | ||||
|                     if (file.first != uriData.displayName && file.first.startsWith(name)) { | ||||
|                         val realName = file.first.removePrefix(name) | ||||
|                             .removeSuffix(".vtt") | ||||
|                             .removeSuffix(".srt") | ||||
|                             .removeSuffix(".txt") | ||||
|                         list.add(SubtitleFile(realName.ifBlank { "Default" }, file.second.toString())) | ||||
|                     } | ||||
|                 } | ||||
|                 return list | ||||
|             } else { | ||||
|                 allEpisodesSubs[getEpisode()?.id] | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|         } | ||||
|  | @ -1599,28 +1614,26 @@ class PlayerFragment : Fragment() { | |||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             val subs = getSubs() | ||||
|             if (subs != null) { | ||||
|                 val subItems = ArrayList<MediaItem.Subtitle>() | ||||
|                 val subItemsId = ArrayList<String>() | ||||
|             val subs = context?.getSubs() ?: emptyList() | ||||
|             val subItems = ArrayList<MediaItem.Subtitle>() | ||||
|             val subItemsId = ArrayList<String>() | ||||
| 
 | ||||
|                 for (sub in sortSubs(subs)) { | ||||
|                     val langId = sub.lang //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang | ||||
|                     subItemsId.add(langId) | ||||
|                     subItems.add( | ||||
|                         MediaItem.Subtitle( | ||||
|                             Uri.parse(sub.url), | ||||
|                             sub.url.toSubtitleMimeType(), | ||||
|                             langId, | ||||
|                             C.SELECTION_FLAG_DEFAULT | ||||
|                         ) | ||||
|             for (sub in sortSubs(subs)) { | ||||
|                 val langId = sub.lang //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang | ||||
|                 subItemsId.add(langId) | ||||
|                 subItems.add( | ||||
|                     MediaItem.Subtitle( | ||||
|                         Uri.parse(sub.url), | ||||
|                         sub.url.toSubtitleMimeType(), | ||||
|                         langId, | ||||
|                         C.SELECTION_FLAG_DEFAULT | ||||
|                     ) | ||||
|                 } | ||||
| 
 | ||||
|                 activeSubtitles = subItemsId | ||||
|                 mediaItemBuilder.setSubtitles(subItems) | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             activeSubtitles = subItemsId | ||||
|             mediaItemBuilder.setSubtitles(subItems) | ||||
| 
 | ||||
| //might add https://github.com/ed828a/Aihua/blob/1896f46888b5a954b367e83f40b845ce174a2328/app/src/main/java/com/dew/aihua/player/playerUI/VideoPlayer.kt#L287 toggle caps | ||||
| 
 | ||||
|             val mediaItem = mediaItemBuilder.build() | ||||
|  | @ -1825,6 +1838,16 @@ class PlayerFragment : Fragment() { | |||
|             }) | ||||
|         } catch (e: java.lang.IllegalStateException) { | ||||
|             println("Warning: Illegal state exception in PlayerFragment") | ||||
|         } finally { | ||||
|             setPreferredSubLanguage( | ||||
|                 if(isDownloadedFile) { | ||||
|                     if(activeSubtitles.isNotEmpty()) { | ||||
|                         activeSubtitles.first() | ||||
|                     } else null | ||||
|                 } else { | ||||
|                     preferredSubtitles | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,9 +24,6 @@ import androidx.fragment.app.Fragment | |||
| import androidx.lifecycle.ViewModelProvider | ||||
| import androidx.recyclerview.widget.GridLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.bumptech.glide.Glide | ||||
| import com.bumptech.glide.load.model.GlideUrl | ||||
| import com.bumptech.glide.request.RequestOptions.bitmapTransform | ||||
| import com.google.android.gms.cast.framework.CastButtonFactory | ||||
| import com.google.android.gms.cast.framework.CastContext | ||||
| import com.google.android.gms.cast.framework.CastState | ||||
|  | @ -52,6 +49,8 @@ import com.lagradost.cloudstream3.ui.download.EasyDownloadButton | |||
| import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick | ||||
| import com.lagradost.cloudstream3.ui.player.PlayerData | ||||
| import com.lagradost.cloudstream3.ui.player.PlayerFragment | ||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment | ||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 | ||||
| import com.lagradost.cloudstream3.utils.* | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable | ||||
|  | @ -64,13 +63,11 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos | |||
| import com.lagradost.cloudstream3.utils.UIHelper.setImage | ||||
| 
 | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename | ||||
| import jp.wasabeef.glide.transformations.BlurTransformation | ||||
| import kotlinx.android.synthetic.main.fragment_result.* | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.withContext | ||||
| import java.io.File | ||||
| import java.util.* | ||||
| import kotlin.collections.ArrayList | ||||
| import kotlin.collections.HashMap | ||||
| 
 | ||||
|  | @ -239,7 +236,7 @@ class ResultFragment : Fragment() { | |||
|     var startAction: Int? = null | ||||
| 
 | ||||
|     private fun lateFixDownloadButton(show: Boolean) { | ||||
|         if(!show || currentType?.isMovieType() == false) { | ||||
|         if (!show || currentType?.isMovieType() == false) { | ||||
|             result_movie_parent.visibility = GONE | ||||
|             result_episodes_text.visibility = VISIBLE | ||||
|             result_episodes.visibility = VISIBLE | ||||
|  | @ -381,7 +378,11 @@ class ResultFragment : Fragment() { | |||
|                 return false | ||||
|             } | ||||
| 
 | ||||
|             fun aquireSingeExtractorLink(links: List<ExtractorLink>, title: String, callback: (ExtractorLink) -> Unit) { | ||||
|             fun acquireSingeExtractorLink( | ||||
|                 links: List<ExtractorLink>, | ||||
|                 title: String, | ||||
|                 callback: (ExtractorLink) -> Unit | ||||
|             ) { | ||||
|                 val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) | ||||
| 
 | ||||
|                 builder.setTitle(title) | ||||
|  | @ -392,8 +393,8 @@ class ResultFragment : Fragment() { | |||
|                 builder.create().show() | ||||
|             } | ||||
| 
 | ||||
|             fun aquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) { | ||||
|                 aquireSingeExtractorLink(currentLinks ?: return, title, callback) | ||||
|             fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) { | ||||
|                 acquireSingeExtractorLink(currentLinks ?: return, title, callback) | ||||
|             } | ||||
| 
 | ||||
|             fun startChromecast(startIndex: Int) { | ||||
|  | @ -412,7 +413,7 @@ class ResultFragment : Fragment() { | |||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             fun startDownload(links: List<ExtractorLink>) { | ||||
|             fun startDownload(links: List<ExtractorLink>, subs: List<SubtitleFile>?) { | ||||
|                 val isMovie = currentIsMovie ?: return | ||||
|                 val titleName = sanitizeFilename(currentHeaderName ?: return) | ||||
| 
 | ||||
|  | @ -481,6 +482,36 @@ class ResultFragment : Fragment() { | |||
|                         meta, | ||||
|                         links | ||||
|                     ) | ||||
|                     // 1. Checks if the lang should be downloaded | ||||
|                     // 2. Makes it into the download format | ||||
|                     // 3. Downloads it as a .vtt file | ||||
|                     val downloadList = ctx.getDownloadSubsLanguageISO639_1() | ||||
|                     main { | ||||
|                         subs?.let { subsList -> | ||||
|                             subsList.filter { downloadList.contains(SubtitleHelper.fromLanguageToTwoLetters(it.lang)) } | ||||
|                                 .map { ExtractorSubtitleLink(it.lang, it.url, "") } | ||||
|                                 .forEach { link -> | ||||
|                                     val epName = meta.name ?: "Episode ${meta.episode}" | ||||
|                                     val fileName = | ||||
|                                         sanitizeFilename(epName + if (downloadList.size > 1) " ${link.name}" else "") | ||||
|                                     val topFolder = "$folder" | ||||
| 
 | ||||
|                                     withContext(Dispatchers.IO) { | ||||
|                                         VideoDownloadManager.downloadThing( | ||||
|                                             ctx, | ||||
|                                             link, | ||||
|                                             fileName, | ||||
|                                             topFolder, | ||||
|                                             "vtt", | ||||
|                                             false, | ||||
|                                             null | ||||
|                                         ) { | ||||
|                                             // no notification | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|  | @ -547,10 +578,10 @@ class ResultFragment : Fragment() { | |||
|                     dialog.show() | ||||
|                 } | ||||
|                 ACTION_COPY_LINK -> { | ||||
|                     aquireSingeExtractorLink("Copy Link") { link -> | ||||
|                     acquireSingeExtractorLink("Copy Link") { link -> | ||||
|                         val serviceClipboard = | ||||
|                             (requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?) | ||||
|                                 ?: return@aquireSingeExtractorLink | ||||
|                                 ?: return@acquireSingeExtractorLink | ||||
|                         val clip = ClipData.newPlainText(link.name, link.url) | ||||
|                         serviceClipboard.setPrimaryClip(clip) | ||||
|                         Toast.makeText(requireContext(), "Text Copied", Toast.LENGTH_SHORT).show() | ||||
|  | @ -558,7 +589,7 @@ class ResultFragment : Fragment() { | |||
|                 } | ||||
| 
 | ||||
|                 ACTION_PLAY_EPISODE_IN_BROWSER -> { | ||||
|                     aquireSingeExtractorLink("Play in Browser") { link -> | ||||
|                     acquireSingeExtractorLink("Play in Browser") { link -> | ||||
|                         val i = Intent(ACTION_VIEW) | ||||
|                         i.data = Uri.parse(link.url) | ||||
|                         startActivity(i) | ||||
|  | @ -566,7 +597,7 @@ class ResultFragment : Fragment() { | |||
|                 } | ||||
| 
 | ||||
|                 ACTION_CHROME_CAST_MIRROR -> { | ||||
|                     aquireSingeExtractorLink("Cast Mirror") { link -> | ||||
|                     acquireSingeExtractorLink("Cast Mirror") { link -> | ||||
|                         val mirrorIndex = currentLinks?.indexOf(link) ?: -1 | ||||
|                         startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex) | ||||
|                     } | ||||
|  | @ -657,15 +688,15 @@ class ResultFragment : Fragment() { | |||
|                 } | ||||
| 
 | ||||
|                 ACTION_DOWNLOAD_EPISODE -> { | ||||
|                     startDownload(currentLinks ?: return@main) | ||||
|                     startDownload(currentLinks ?: return@main, currentSubs) | ||||
|                 } | ||||
| 
 | ||||
|                 ACTION_DOWNLOAD_MIRROR -> { | ||||
|                     aquireSingeExtractorLink( | ||||
|                     acquireSingeExtractorLink( | ||||
|                         (currentLinks ?: return@main).filter { !it.isM3u8 }, | ||||
|                         "Download Mirror" | ||||
|                     ) { link -> | ||||
|                         startDownload(listOf(link)) | ||||
|                         startDownload(listOf(link), currentSubs) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | @ -704,7 +735,7 @@ class ResultFragment : Fragment() { | |||
|         } | ||||
| 
 | ||||
|         observe(viewModel.episodes) { episodeList -> | ||||
|             lateFixDownloadButton( episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this | ||||
|             lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this | ||||
| 
 | ||||
|             when (startAction) { | ||||
|                 START_ACTION_RESUME_LATEST -> { | ||||
|  |  | |||
|  | @ -26,12 +26,17 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey | |||
| import com.lagradost.cloudstream3.utils.DataStore.setKey | ||||
| import com.lagradost.cloudstream3.utils.Event | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog | ||||
| import com.lagradost.cloudstream3.utils.SubtitleHelper | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage | ||||
| import kotlinx.android.synthetic.main.subtitle_settings.* | ||||
| 
 | ||||
| const val SUBTITLE_KEY = "subtitle_settings" | ||||
| const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" | ||||
| const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" | ||||
| 
 | ||||
| data class SaveCaptionStyle( | ||||
|     var foregroundColor: Int, | ||||
|  | @ -111,6 +116,14 @@ class SubtitlesFragment : Fragment() { | |||
|             val metrics: DisplayMetrics = Resources.getSystem().displayMetrics | ||||
|             return TypedValue.applyDimension(unit, size, metrics).toInt() | ||||
|         } | ||||
| 
 | ||||
|         fun Context.getDownloadSubsLanguageISO639_1(): List<String> { | ||||
|             return getKey(SUBTITLE_DOWNLOAD_KEY) ?: listOf("en") | ||||
|         } | ||||
| 
 | ||||
|         fun Context.getAutoSelectLanguageISO639_1(): String { | ||||
|             return getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun onColorSelected(stuff: Pair<Int, Int>) { | ||||
|  | @ -296,10 +309,57 @@ class SubtitlesFragment : Fragment() { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         subs_font.setOnLongClickListener { | ||||
|         subs_font.setOnLongClickListener { textView -> | ||||
|             state.typeface = null | ||||
|             it.context.updateState() | ||||
|             Toast.makeText(it.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show() | ||||
|             textView.context.updateState() | ||||
|             Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show() | ||||
|             return@setOnLongClickListener true | ||||
|         } | ||||
| 
 | ||||
|         subs_auto_select_language.setOnClickListener { textView -> | ||||
|             val langMap = arrayListOf( | ||||
|                 SubtitleHelper.Language639("None", "None", "", "", "", "", ""), | ||||
|             ) | ||||
|             langMap.addAll(SubtitleHelper.languages) | ||||
| 
 | ||||
|             val lang639_1 = langMap.map { it.ISO_639_1 } | ||||
|             textView.context.showDialog( | ||||
|                 langMap.map { it.languageName }, | ||||
|                 lang639_1.indexOf(textView.context.getAutoSelectLanguageISO639_1()), | ||||
|                 (textView as TextView).text.toString(), | ||||
|                 true, | ||||
|                 dismissCallback | ||||
|             ) { index -> | ||||
|                 textView.context.setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index]) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         subs_auto_select_language.setOnLongClickListener { textView -> | ||||
|             textView.context.setKey(SUBTITLE_AUTO_SELECT_KEY, "en") | ||||
|             Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show() | ||||
|             return@setOnLongClickListener true | ||||
|         } | ||||
| 
 | ||||
|         subs_download_languages.setOnClickListener { textView -> | ||||
|             val langMap = SubtitleHelper.languages | ||||
|             val lang639_1 = langMap.map { it.ISO_639_1 } | ||||
|             val keys = textView.context.getDownloadSubsLanguageISO639_1() | ||||
|             val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 } | ||||
| 
 | ||||
|             textView.context.showMultiDialog( | ||||
|                 langMap.map { it.languageName }, | ||||
|                 keyMap, | ||||
|                 (textView as TextView).text.toString(), | ||||
|                 dismissCallback | ||||
|             ) { indexList -> | ||||
|                 textView.context.setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList()) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         subs_download_languages.setOnLongClickListener { textView -> | ||||
|             textView.context.setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en")) | ||||
| 
 | ||||
|             Toast.makeText(textView.context, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT).show() | ||||
|             return@setOnLongClickListener true | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,15 +6,17 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall | |||
| data class ExtractorLink( | ||||
|     val source: String, | ||||
|     val name: String, | ||||
|     val url: String, | ||||
|     val referer: String, | ||||
|     override val url: String, | ||||
|     override val referer: String, | ||||
|     val quality: Int, | ||||
|     val isM3u8: Boolean = false, | ||||
| ) | ||||
| ) : VideoDownloadManager.IDownloadableMinimum | ||||
| 
 | ||||
| fun ExtractorLink.getId(): Int { | ||||
|     return url.hashCode() | ||||
| } | ||||
| data class ExtractorSubtitleLink( | ||||
|     val name: String, | ||||
|     override val url: String, | ||||
|     override val referer: String, | ||||
| ) : VideoDownloadManager.IDownloadableMinimum | ||||
| 
 | ||||
| enum class Qualities(var value: Int) { | ||||
|     Unknown(0), | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import android.content.Context | |||
| import android.view.View | ||||
| import android.widget.* | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.core.util.forEach | ||||
| import androidx.core.view.marginLeft | ||||
| import androidx.core.view.marginRight | ||||
| import androidx.core.view.marginTop | ||||
|  | @ -15,20 +16,22 @@ object SingleSelectionHelper { | |||
|     fun Context.showDialog( | ||||
|         dialog: Dialog, | ||||
|         items: List<String>, | ||||
|         selectedIndex: Int, | ||||
|         selectedIndex: List<Int>, | ||||
|         name: String, | ||||
|         showApply: Boolean, | ||||
|         callback: (Int) -> Unit, | ||||
|         isMultiSelect: Boolean, | ||||
|         callback: (List<Int>) -> Unit, | ||||
|         dismissCallback: () -> Unit | ||||
|     ) { | ||||
|         val realShowApply = showApply || isMultiSelect | ||||
|         val listView = dialog.findViewById<ListView>(R.id.listview1)!! | ||||
|         val textView = dialog.findViewById<TextView>(R.id.text1)!! | ||||
|         val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!! | ||||
|         val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!! | ||||
|         val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!! | ||||
| 
 | ||||
|         applyHolder.visibility = if (showApply) View.VISIBLE else View.GONE | ||||
|         if (!showApply) { | ||||
|         applyHolder.visibility = if (realShowApply) View.VISIBLE else View.GONE | ||||
|         if (!realShowApply) { | ||||
|             val params = listView.layoutParams as LinearLayout.LayoutParams | ||||
|             params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0) | ||||
|             listView.layoutParams = params | ||||
|  | @ -40,29 +43,45 @@ object SingleSelectionHelper { | |||
|         arrayAdapter.addAll(items) | ||||
| 
 | ||||
|         listView.adapter = arrayAdapter | ||||
|         listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE | ||||
|         if (isMultiSelect) { | ||||
|             listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE | ||||
|         } else { | ||||
|             listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE | ||||
|         } | ||||
| 
 | ||||
|         listView.setSelection(selectedIndex) | ||||
|         listView.setItemChecked(selectedIndex, true) | ||||
|         for (select in selectedIndex) { | ||||
|             listView.setItemChecked(select, true) | ||||
|         } | ||||
| 
 | ||||
|         var currentIndex = selectedIndex | ||||
|         selectedIndex.minOrNull()?.let { | ||||
|             listView.setSelection(it) | ||||
|         } | ||||
| 
 | ||||
|         //  var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1 | ||||
| 
 | ||||
|         dialog.setOnDismissListener { | ||||
|             dismissCallback.invoke() | ||||
|         } | ||||
| 
 | ||||
|         listView.setOnItemClickListener { _, _, which, _ -> | ||||
|             if (showApply) { | ||||
|                 currentIndex = which | ||||
|                 listView.setItemChecked(which, true) | ||||
|             //  lastSelectedIndex = which | ||||
|             if (realShowApply) { | ||||
|                 if (!isMultiSelect) { | ||||
|                     listView.setItemChecked(which, true) | ||||
|                 } | ||||
|             } else { | ||||
|                 callback.invoke(which) | ||||
|                 callback.invoke(listOf(which)) | ||||
|                 dialog.dismiss() | ||||
|             } | ||||
|         } | ||||
|         if (showApply) { | ||||
|         if (realShowApply) { | ||||
|             applyButton.setOnClickListener { | ||||
|                 callback.invoke(currentIndex) | ||||
|                 val list = ArrayList<Int>() | ||||
|                 for (index in 0 until listView.count) { | ||||
|                     if (listView.checkedItemPositions[index]) | ||||
|                         list.add(index) | ||||
|                 } | ||||
|                 callback.invoke(list) | ||||
|                 dialog.dismiss() | ||||
|             } | ||||
|             cancelButton.setOnClickListener { | ||||
|  | @ -71,6 +90,21 @@ object SingleSelectionHelper { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun Context.showMultiDialog( | ||||
|         items: List<String>, | ||||
|         selectedIndex: List<Int>, | ||||
|         name: String, | ||||
|         dismissCallback: () -> Unit, | ||||
|         callback: (List<Int>) -> Unit, | ||||
|     ) { | ||||
|         val builder = | ||||
|             AlertDialog.Builder(this, R.style.AlertDialogCustom).setView(R.layout.bottom_selection_dialog) | ||||
| 
 | ||||
|         val dialog = builder.create() | ||||
|         dialog.show() | ||||
|         showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback) | ||||
|     } | ||||
| 
 | ||||
|     fun Context.showDialog( | ||||
|         items: List<String>, | ||||
|         selectedIndex: Int, | ||||
|  | @ -84,7 +118,16 @@ object SingleSelectionHelper { | |||
| 
 | ||||
|         val dialog = builder.create() | ||||
|         dialog.show() | ||||
|         showDialog(dialog, items, selectedIndex, name, showApply, callback, dismissCallback) | ||||
|         showDialog( | ||||
|             dialog, | ||||
|             items, | ||||
|             listOf(selectedIndex), | ||||
|             name, | ||||
|             showApply, | ||||
|             false, | ||||
|             { if (it.isNotEmpty()) callback.invoke(it.first()) }, | ||||
|             dismissCallback | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     fun Context.showBottomDialog( | ||||
|  | @ -100,6 +143,15 @@ object SingleSelectionHelper { | |||
|         builder.setContentView(R.layout.bottom_selection_dialog) | ||||
| 
 | ||||
|         builder.show() | ||||
|         showDialog(builder, items, selectedIndex, name, showApply, callback, dismissCallback) | ||||
|         showDialog( | ||||
|             builder, | ||||
|             items, | ||||
|             listOf(selectedIndex), | ||||
|             name, | ||||
|             showApply, | ||||
|             false, | ||||
|             { callback.invoke(it.first()) }, | ||||
|             dismissCallback | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -207,7 +207,7 @@ object SubtitleHelper { | |||
|         Language639("Malayalam", "മലയാളം", "ml", "mal", "mal", "mal", ""), | ||||
|         Language639("Maltese", "Malti", "mt", "mlt", "mlt", "mlt", ""), | ||||
|         Language639("Māori", "te reo Māori", "mi", "mri", "", "mri", ""), | ||||
|         Language639("Marathi (Marāṭhī)", "मराठी", "mr", "mar", "mar", "mar", ""), | ||||
|         Language639("Marathi", "मराठी", "mr", "mar", "mar", "mar", ""), | ||||
|         Language639("Marshallese", "Kajin M̧ajeļ", "mh", "mah", "mah", "mah", ""), | ||||
|         Language639("Mongolian", "Монгол хэл", "mn", "mon", "mon", "mon", ""), | ||||
|         Language639("Nauruan", "Dorerin Naoero", "na", "nau", "nau", "nau", ""), | ||||
|  | @ -238,7 +238,7 @@ object SubtitleHelper { | |||
|         Language639("Reunion Creole", "Kréol Rénioné", "rc", "rcf", "rcf", "rcf", ""), | ||||
|         Language639("Romanian", "limba română", "ro", "ron", "", "ron", ""), | ||||
|         Language639("Russian", "Русский", "ru", "rus", "rus", "rus", ""), | ||||
|         Language639("Sanskrit (Saṁskṛta)", "संस्कृतम्", "sa", "san", "san", "san", ""), | ||||
|         Language639("Sanskrit", "संस्कृतम्", "sa", "san", "san", "san", ""), | ||||
|         Language639("Sardinian", "sardu", "sc", "srd", "srd", "srd", ""), | ||||
|         Language639("Sindhi", "सिन्धी, سنڌي، سندھی", "sd", "snd", "snd", "snd", ""), | ||||
|         Language639("Northern Sami", "Davvisámegiella", "se", "sme", "sme", "sme", ""), | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import android.net.Uri | |||
| import android.os.Build | ||||
| import android.os.Environment | ||||
| import android.provider.MediaStore | ||||
| import android.webkit.MimeTypeMap | ||||
| import android.widget.Toast | ||||
| import androidx.annotation.DrawableRes | ||||
| import androidx.annotation.RequiresApi | ||||
|  | @ -27,6 +28,7 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey | |||
| import com.lagradost.cloudstream3.utils.DataStore.removeKey | ||||
| import com.lagradost.cloudstream3.utils.DataStore.setKey | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute | ||||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager.getExistingDownloadUriOrNullQ | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.withContext | ||||
|  | @ -35,6 +37,7 @@ import java.lang.Thread.sleep | |||
| import java.net.URL | ||||
| import java.net.URLConnection | ||||
| import java.util.* | ||||
| import kotlin.collections.ArrayList | ||||
| 
 | ||||
| const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" | ||||
| const val DOWNLOAD_CHANNEL_NAME = "Downloads" | ||||
|  | @ -85,6 +88,15 @@ object VideoDownloadManager { | |||
|         Stop, | ||||
|     } | ||||
| 
 | ||||
|     interface IDownloadableMinimum { | ||||
|         val url: String | ||||
|         val referer: String | ||||
|     } | ||||
| 
 | ||||
|     fun VideoDownloadManager.IDownloadableMinimum.getId(): Int { | ||||
|         return url.hashCode() | ||||
|     } | ||||
| 
 | ||||
|     data class DownloadEpisodeMetadata( | ||||
|         val id: Int, | ||||
|         val mainName: String, | ||||
|  | @ -126,7 +138,7 @@ object VideoDownloadManager { | |||
| 
 | ||||
|     private const val SUCCESS_DOWNLOAD_DONE = 1 | ||||
|     private const val SUCCESS_STOPPED = 2 | ||||
|     private const val ERROR_DELETING_FILE = -1 | ||||
|     private const val ERROR_DELETING_FILE = 3 // will not download the next one, but is still classified as an error | ||||
|     private const val ERROR_CREATE_FILE = -2 | ||||
|     private const val ERROR_OPEN_FILE = -3 | ||||
|     private const val ERROR_TOO_SMALL_CONNECTION = -4 | ||||
|  | @ -191,7 +203,7 @@ object VideoDownloadManager { | |||
|                 cachedBitmaps[url] = bitmap | ||||
|             } | ||||
|             return null | ||||
|         } catch (e : Exception) { | ||||
|         } catch (e: Exception) { | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
|  | @ -361,6 +373,69 @@ object VideoDownloadManager { | |||
|         return tempName.replace("  ", " ").trim(' ') | ||||
|     } | ||||
| 
 | ||||
|     @RequiresApi(Build.VERSION_CODES.Q) | ||||
|     private fun ContentResolver.getExistingFolderStartName(relativePath: String): List<Pair<String, Uri>>? { | ||||
|         try { | ||||
|             val projection = arrayOf( | ||||
|                 MediaStore.MediaColumns._ID, | ||||
|                 MediaStore.MediaColumns.DISPLAY_NAME,   // unused (for verification use only) | ||||
|                 //MediaStore.MediaColumns.RELATIVE_PATH,  // unused (for verification use only) | ||||
|             ) | ||||
| 
 | ||||
|             val selection = | ||||
|                 "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath'" | ||||
| 
 | ||||
|             val result = this.query( | ||||
|                 MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), | ||||
|                 projection, selection, null, null | ||||
|             ) | ||||
|             val list = ArrayList<Pair<String, Uri>>() | ||||
| 
 | ||||
|             result.use { c -> | ||||
|                 if (c != null && c.count >= 1) { | ||||
|                     c.moveToFirst() | ||||
|                     while (true) { | ||||
|                         val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) | ||||
|                         val name = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) | ||||
|                         val uri = ContentUris.withAppendedId( | ||||
|                             MediaStore.Downloads.EXTERNAL_CONTENT_URI, id | ||||
|                         ) | ||||
|                         list.add(Pair(name, uri)) | ||||
|                         if (c.isLast) { | ||||
|                             break | ||||
|                         } | ||||
|                         c.moveToNext() | ||||
|                     } | ||||
| 
 | ||||
|                     /* | ||||
|                     val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) | ||||
|                     val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/ | ||||
| 
 | ||||
|                 } | ||||
|             } | ||||
|             return list | ||||
|         } catch (e: Exception) { | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun getFolder(context: Context, relativePath: String): List<Pair<String, Uri>>? { | ||||
|         if (isScopedStorage()) { | ||||
|             return context.contentResolver?.getExistingFolderStartName(relativePath) | ||||
|         } else { | ||||
|             val normalPath = | ||||
|                 "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace( | ||||
|                     '/', | ||||
|                     File.separatorChar | ||||
|                 ) | ||||
|             val folder = File(normalPath) | ||||
|             if (folder.isDirectory) { | ||||
|                 return folder.listFiles().map { Pair(it.name, it.toUri()) } | ||||
|             } | ||||
|             return null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @RequiresApi(Build.VERSION_CODES.Q) | ||||
|     private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? { | ||||
|         try { | ||||
|  | @ -412,18 +487,24 @@ object VideoDownloadManager { | |||
|         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q | ||||
|     } | ||||
| 
 | ||||
|     private fun downloadSingleEpisode( | ||||
|         context: Context, | ||||
|         source: String?, | ||||
|         folder: String?, | ||||
|         ep: DownloadEpisodeMetadata, | ||||
|         link: ExtractorLink, | ||||
|         tryResume: Boolean = false, | ||||
|     ): Int { | ||||
|         val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}") | ||||
|     data class CreateNotificationMetadata( | ||||
|         val type: DownloadType, | ||||
|         val bytesDownloaded: Long, | ||||
|         val bytesTotal: Long, | ||||
|     ) | ||||
| 
 | ||||
|     fun downloadThing( | ||||
|         context: Context, | ||||
|         link: IDownloadableMinimum, | ||||
|         name: String, | ||||
|         folder: String?, | ||||
|         extension: String, | ||||
|         tryResume: Boolean, | ||||
|         parentId: Int?, | ||||
|         createNotificationCallback: (CreateNotificationMetadata) -> Unit | ||||
|     ): Int { | ||||
|         val relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar) | ||||
|         val displayName = "$name.mp4" | ||||
|         val displayName = "$name.$extension" | ||||
| 
 | ||||
|         val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName" | ||||
|         var resume = tryResume | ||||
|  | @ -440,7 +521,9 @@ object VideoDownloadManager { | |||
|             } else { | ||||
|                 if (!File(normalPath).delete()) return ERROR_DELETING_FILE | ||||
|             } | ||||
|             downloadDeleteEvent.invoke(ep.id) | ||||
|             parentId?.let { | ||||
|                 downloadDeleteEvent.invoke(parentId) | ||||
|             } | ||||
|             return SUCCESS_STOPPED | ||||
|         } | ||||
| 
 | ||||
|  | @ -468,11 +551,18 @@ object VideoDownloadManager { | |||
|             } else { | ||||
|                 val contentUri = | ||||
|                     MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI | ||||
| 
 | ||||
|                 //val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) | ||||
|                 val currentMimeType = when (extension) { | ||||
|                     "vtt" -> "text/vtt" | ||||
|                     "mp4" -> "video/mp4" | ||||
|                     "srt" -> "text/plain" | ||||
|                     else -> null | ||||
|                 } | ||||
|                 val newFile = ContentValues().apply { | ||||
|                     put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) | ||||
|                     put(MediaStore.MediaColumns.TITLE, name) | ||||
|                     put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4") | ||||
|                     if (currentMimeType != null) | ||||
|                         put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType) | ||||
|                     put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) | ||||
|                 } | ||||
| 
 | ||||
|  | @ -539,9 +629,11 @@ object VideoDownloadManager { | |||
|         } | ||||
|         val bytesTotal = contentLength + resumeLength | ||||
| 
 | ||||
|         if (bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG | ||||
|         if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG | ||||
| 
 | ||||
|         context.setKey(KEY_DOWNLOAD_INFO, ep.id.toString(), DownloadedFileInfo(bytesTotal, relativePath, displayName)) | ||||
|         parentId?.let { | ||||
|             context.setKey(KEY_DOWNLOAD_INFO, it.toString(), DownloadedFileInfo(bytesTotal, relativePath, displayName)) | ||||
|         } | ||||
| 
 | ||||
|         // Could use connection.contentType for mime types when creating the file, | ||||
|         // however file is already created and players don't go of file type | ||||
|  | @ -573,15 +665,18 @@ object VideoDownloadManager { | |||
|                 else -> DownloadType.IsDownloading | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 downloadStatus[ep.id] = type | ||||
|                 downloadStatusEvent.invoke(Pair(ep.id, type)) | ||||
|                 downloadProgressEvent.invoke(Triple(ep.id, bytesDownloaded, bytesTotal)) | ||||
|             } catch (e: Exception) { | ||||
|                 // IDK MIGHT ERROR | ||||
|             parentId?.let { id -> | ||||
|                 try { | ||||
|                     downloadStatus[id] = type | ||||
|                     downloadStatusEvent.invoke(Pair(id, type)) | ||||
|                     downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) | ||||
|                 } catch (e: Exception) { | ||||
|                     // IDK MIGHT ERROR | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             createNotification( | ||||
|             createNotificationCallback.invoke(CreateNotificationMetadata(type, bytesDownloaded, bytesTotal)) | ||||
|             /*createNotification( | ||||
|                 context, | ||||
|                 source, | ||||
|                 link.name, | ||||
|  | @ -589,12 +684,11 @@ object VideoDownloadManager { | |||
|                 type, | ||||
|                 bytesDownloaded, | ||||
|                 bytesTotal | ||||
|             ) | ||||
|             )*/ | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         val downloadEventListener = { event: Pair<Int, DownloadActionType> -> | ||||
|             if (event.first == ep.id) { | ||||
|             if (event.first == parentId) { | ||||
|                 when (event.second) { | ||||
|                     DownloadActionType.Pause -> { | ||||
|                         isPaused = true; updateNotification() | ||||
|  | @ -611,7 +705,8 @@ object VideoDownloadManager { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         downloadEvent += downloadEventListener | ||||
|         if (parentId != null) | ||||
|             downloadEvent += downloadEventListener | ||||
| 
 | ||||
|         // UPDATE DOWNLOAD NOTIFICATION | ||||
|         val notificationCoroutine = main { | ||||
|  | @ -625,7 +720,6 @@ object VideoDownloadManager { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         val id = ep.id | ||||
|         // THE REAL READ | ||||
|         try { | ||||
|             while (true) { | ||||
|  | @ -655,13 +749,16 @@ object VideoDownloadManager { | |||
|         notificationCoroutine.cancel() | ||||
| 
 | ||||
|         try { | ||||
|             downloadEvent -= downloadEventListener | ||||
|             if (parentId != null) | ||||
|                 downloadEvent -= downloadEventListener | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             downloadStatus.remove(ep.id) | ||||
|             parentId?.let { | ||||
|                 downloadStatus.remove(it) | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             // IDK MIGHT ERROR | ||||
|         } | ||||
|  | @ -669,15 +766,15 @@ object VideoDownloadManager { | |||
|         // RETURN MESSAGE | ||||
|         return when { | ||||
|             isFailed -> { | ||||
|                 downloadProgressEvent.invoke(Triple(id, 0, 0)) | ||||
|                 parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } | ||||
|                 ERROR_CONNECTION_ERROR | ||||
|             } | ||||
|             isStopped -> { | ||||
|                 downloadProgressEvent.invoke(Triple(id, 0, 0)) | ||||
|                 parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } | ||||
|                 deleteFile() | ||||
|             } | ||||
|             else -> { | ||||
|                 downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) | ||||
|                 parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) } | ||||
|                 isDone = true | ||||
|                 updateNotification() | ||||
|                 SUCCESS_DOWNLOAD_DONE | ||||
|  | @ -685,6 +782,29 @@ object VideoDownloadManager { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun downloadSingleEpisode( | ||||
|         context: Context, | ||||
|         source: String?, | ||||
|         folder: String?, | ||||
|         ep: DownloadEpisodeMetadata, | ||||
|         link: ExtractorLink, | ||||
|         tryResume: Boolean = false, | ||||
|     ): Int { | ||||
|         val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}") | ||||
| 
 | ||||
|         return downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> | ||||
|             createNotification( | ||||
|                 context, | ||||
|                 source, | ||||
|                 link.name, | ||||
|                 ep, | ||||
|                 meta.type, | ||||
|                 meta.bytesDownloaded, | ||||
|                 meta.bytesTotal | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun downloadCheck(context: Context) { | ||||
|         if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { | ||||
|             val pkg = downloadQueue.removeFirst() | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ | |||
|             android:baselineAligned="false"> | ||||
| 
 | ||||
|         <LinearLayout | ||||
|                 android:id="@+id/sort_sources_holder" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:orientation="vertical" | ||||
|  |  | |||
|  | @ -12,8 +12,8 @@ | |||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content"> | ||||
|         <TextView | ||||
|                 android:paddingStart="?android:attr/listPreferredItemPaddingStart" | ||||
|                 android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" | ||||
|                 android:paddingStart="20dp" | ||||
|                 android:paddingEnd="20dp" | ||||
|                 android:layout_marginTop="20dp" | ||||
|                 android:layout_marginBottom="10dp" | ||||
|                 android:textStyle="bold" | ||||
|  | @ -78,6 +78,26 @@ | |||
|                 android:text="@string/subs_subtitle_elevation" | ||||
|                 style="@style/SettingsItem" | ||||
|         /> | ||||
|         <TextView | ||||
|                 android:id="@+id/subs_auto_select_language" | ||||
|                 android:text="@string/subs_auto_select_language" | ||||
|                 style="@style/SettingsItem" | ||||
|         /> | ||||
|         <TextView | ||||
|                 android:id="@+id/subs_download_languages" | ||||
|                 android:text="@string/subs_download_languages" | ||||
|                 style="@style/SettingsItem" | ||||
|         /> | ||||
|         <TextView | ||||
|                 android:gravity="center" | ||||
|                 android:text="@string/subs_hold_to_reset_to_default" | ||||
|                 android:textSize="14sp" | ||||
| 
 | ||||
|                 android:textColor="?attr/textColor" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_rowWeight="1" | ||||
|                 android:layout_height="wrap_content"> | ||||
|         </TextView> | ||||
| 
 | ||||
|         <LinearLayout | ||||
|                 android:orientation="horizontal" | ||||
|  |  | |||
|  | @ -88,4 +88,7 @@ | |||
|     <string name="manual_check_update_key">manual_check_update</string> | ||||
| 
 | ||||
|     <string name="prerelease_commit_hash">unknown_prerelease</string> | ||||
|     <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> | ||||
| </resources> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue