added custom subtitles

This commit is contained in:
Blatzar 2021-12-27 22:56:47 +01:00
parent 914238597e
commit 4ec287d1e9
4 changed files with 246 additions and 152 deletions

View file

@ -29,6 +29,7 @@ import android.view.animation.Animation
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.* import android.widget.*
import android.widget.Toast.LENGTH_SHORT import android.widget.Toast.LENGTH_SHORT
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.blue import androidx.core.graphics.blue
import androidx.core.graphics.green import androidx.core.graphics.green
@ -47,7 +48,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.TIME_UNSET import com.google.android.exoplayer2.C.TIME_UNSET
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import com.google.android.exoplayer2.source.*
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.SubtitleView import com.google.android.exoplayer2.ui.SubtitleView
@ -63,6 +64,7 @@ import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState import com.google.android.gms.cast.framework.CastState
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.MainActivity.Companion.canEnterPipMode import com.lagradost.cloudstream3.MainActivity.Companion.canEnterPipMode
import com.lagradost.cloudstream3.MainActivity.Companion.getCastSession import com.lagradost.cloudstream3.MainActivity.Companion.getCastSession
@ -100,6 +102,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager.getId
import kotlinx.android.synthetic.main.fragment_player.* import kotlinx.android.synthetic.main.fragment_player.*
import kotlinx.android.synthetic.main.player_custom_layout.* import kotlinx.android.synthetic.main.player_custom_layout.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import okhttp3.internal.format
import java.io.File import java.io.File
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
@ -766,6 +769,32 @@ class PlayerFragment : Fragment() {
safeReleasePlayer() safeReleasePlayer()
} }
// Open file picker
private val subsPathPicker =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
// It lies, it can be null if file manager quits.
if (uri == null) return@registerForActivityResult
val context = context ?: AcraApplication.context ?: return@registerForActivityResult
// RW perms for the path
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromUri(context, uri)
println("Selected URI path: $uri - Full path: ${file.filePath}")
// DO NOT REMOVE THE FILE EXTENSION FROM NAME, IT'S NEEDED FOR MIME TYPES
val name = file.name ?: uri.toString()
viewModel.loadSubtitleFile(uri, name, getEpisode()?.id)
setPreferredSubLanguage(name)
showToast(
activity,
format(context.getString(R.string.player_loaded_subtitles), name),
1000
)
}
private class SettingsContentObserver(handler: Handler?, val activity: Activity) : private class SettingsContentObserver(handler: Handler?, val activity: Activity) :
ContentObserver(handler) { ContentObserver(handler) {
private val audioManager = activity.getSystemService(AUDIO_SERVICE) as? AudioManager private val audioManager = activity.getSystemService(AUDIO_SERVICE) as? AudioManager
@ -1231,12 +1260,30 @@ class PlayerFragment : Fragment() {
sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!! sourceDialog.findViewById<MaterialButton>(R.id.cancel_btt)!!
val subsSettings = sourceDialog.findViewById<View>(R.id.subs_settings)!! val subsSettings = sourceDialog.findViewById<View>(R.id.subs_settings)!!
val subtitleLoadButton =
sourceDialog.findViewById<MaterialButton>(R.id.load_btt)!!
subtitleLoadButton.setOnClickListener {
// "vtt" -> "text/vtt"
// "srt" -> "application/x-subrip"// "text/plain"
subsPathPicker.launch(
arrayOf(
"text/vtt",
"application/x-subrip",
"text/plain",
"text/str",
"application/octet-stream"
)
)
}
subsSettings.setOnClickListener { subsSettings.setOnClickListener {
autoHide() autoHide()
saveArguments() saveArguments()
SubtitlesFragment.push(activity) SubtitlesFragment.push(activity)
sourceDialog.dismissSafe(activity) sourceDialog.dismissSafe(activity)
} }
var sourceIndex = 0 var sourceIndex = 0
var startSource = 0 var startSource = 0
var sources: List<ExtractorLink> = emptyList() var sources: List<ExtractorLink> = emptyList()
@ -2116,40 +2163,7 @@ class PlayerFragment : Fragment() {
mediaItemBuilder.setUri(uriPrimary) mediaItemBuilder.setUri(uriPrimary)
} }
val subs = context?.getSubs() ?: emptyList() fun getDataSourceFactory(isOnline: Boolean): DataSource.Factory {
val subItems = ArrayList<MediaItem.SubtitleConfiguration>()
val subItemsId = ArrayList<String>()
for (sub in sortSubs(subs)) {
val langId =
sub.lang.trimEnd() //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang
subItemsId.add(langId)
subItems.add(
MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
.setMimeType(sub.url.toSubtitleMimeType())
.setLanguage("_$langId")
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.build()
)
}
activeSubtitles = subItemsId
mediaItemBuilder.setSubtitleConfigurations(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()
val trackSelector = DefaultTrackSelector(requireContext())
// Disable subtitles
trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(requireContext())
// .setRendererDisabled(C.TRACK_TYPE_VIDEO, true)
.setRendererDisabled(C.TRACK_TYPE_TEXT, true)
.setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT)
.clearSelectionOverrides()
.build()
fun getDataSourceFactory(): DataSource.Factory {
return if (isOnline) { return if (isOnline) {
DefaultHttpDataSource.Factory().apply { DefaultHttpDataSource.Factory().apply {
setUserAgent(USER_AGENT) setUserAgent(USER_AGENT)
@ -2174,6 +2188,41 @@ class PlayerFragment : Fragment() {
} }
} }
val subs = context?.getSubs() ?: emptyList()
val subItemsId = ArrayList<String>()
val subSources = sortSubs(subs).map { sub ->
// The url can look like .../document/4294 when the name is EnglishSDH.srt
val subtitleMimeType =
if (sub.url.startsWith("content")) sub.lang.toSubtitleMimeType() else sub.url.toSubtitleMimeType()
val langId =
sub.lang.trimEnd() //SubtitleHelper.fromLanguageToTwoLetters(it.lang) ?: it.lang
subItemsId.add(langId)
val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url))
.setMimeType(subtitleMimeType)
.setLanguage("_$langId")
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.build()
SingleSampleMediaSource.Factory(getDataSourceFactory(!sub.url.startsWith("content")))
.createMediaSource(subConfig, TIME_UNSET)
}
activeSubtitles = subItemsId
// mediaItemBuilder.setSubtitleConfigurations(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()
val trackSelector = DefaultTrackSelector(requireContext())
// Disable subtitles
trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(requireContext())
// .setRendererDisabled(C.TRACK_TYPE_VIDEO, true)
.setRendererDisabled(C.TRACK_TYPE_TEXT, true)
.setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT)
.clearSelectionOverrides()
.build()
normalSafeApiCall { normalSafeApiCall {
val databaseProvider = StandaloneDatabaseProvider(requireContext()) val databaseProvider = StandaloneDatabaseProvider(requireContext())
simpleCache = SimpleCache( simpleCache = SimpleCache(
@ -2187,18 +2236,23 @@ class PlayerFragment : Fragment() {
val cacheFactory = CacheDataSource.Factory().apply { val cacheFactory = CacheDataSource.Factory().apply {
simpleCache?.let { setCache(it) } simpleCache?.let { setCache(it) }
setUpstreamDataSourceFactory(getDataSourceFactory()) setUpstreamDataSourceFactory(getDataSourceFactory(isOnline))
} }
val exoPlayerBuilder = val exoPlayerBuilder =
ExoPlayer.Builder(requireContext()) ExoPlayer.Builder(requireContext())
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
val videoMediaSource =
DefaultMediaSourceFactory(cacheFactory).createMediaSource(mediaItem)
exoPlayer = exoPlayerBuilder.build().apply { exoPlayer = exoPlayerBuilder.build().apply {
playWhenReady = isPlayerPlaying playWhenReady = isPlayerPlaying
seekTo(currentWindow, playbackPosition) seekTo(currentWindow, playbackPosition)
setMediaSource( setMediaSource(
DefaultMediaSourceFactory(cacheFactory).createMediaSource(mediaItem), MergingMediaSource(
videoMediaSource, *subSources.toTypedArray()
),
playbackPosition playbackPosition
) )
prepare() prepare()

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -57,7 +58,8 @@ class ResultViewModel : ViewModel() {
private val _resultResponse: MutableLiveData<Resource<Any?>> = MutableLiveData() private val _resultResponse: MutableLiveData<Resource<Any?>> = MutableLiveData()
private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData() private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData()
private val episodeById: MutableLiveData<HashMap<Int, Int>> = MutableLiveData() // lookup by ID to get Index private val episodeById: MutableLiveData<HashMap<Int, Int>> =
MutableLiveData() // lookup by ID to get Index
private val _publicEpisodes: MutableLiveData<Resource<List<ResultEpisode>>> = MutableLiveData() private val _publicEpisodes: MutableLiveData<Resource<List<ResultEpisode>>> = MutableLiveData()
private val _publicEpisodesCount: MutableLiveData<Int> = MutableLiveData() // before the sorting private val _publicEpisodesCount: MutableLiveData<Int> = MutableLiveData() // before the sorting
@ -83,7 +85,8 @@ class ResultViewModel : ViewModel() {
private val _dubSubSelections: MutableLiveData<Set<DubStatus>> = MutableLiveData() private val _dubSubSelections: MutableLiveData<Set<DubStatus>> = MutableLiveData()
val dubSubEpisodes: LiveData<Map<DubStatus, List<ResultEpisode>>?> get() = _dubSubEpisodes val dubSubEpisodes: LiveData<Map<DubStatus, List<ResultEpisode>>?> get() = _dubSubEpisodes
private val _dubSubEpisodes: MutableLiveData<Map<DubStatus, List<ResultEpisode>>?> = MutableLiveData() private val _dubSubEpisodes: MutableLiveData<Map<DubStatus, List<ResultEpisode>>?> =
MutableLiveData()
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData() private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
val watchStatus: LiveData<WatchType> get() = _watchStatus val watchStatus: LiveData<WatchType> get() = _watchStatus
@ -291,7 +294,14 @@ class ResultViewModel : ViewModel() {
_apiName.postValue(apiName) _apiName.postValue(apiName)
val api = getApiFromNameNull(apiName) val api = getApiFromNameNull(apiName)
if (api == null) { if (api == null) {
_resultResponse.postValue(Resource.Failure(false, null, null, "This provider does not exist")) _resultResponse.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
return@launch return@launch
} }
repo = APIRepository(api) repo = APIRepository(api)
@ -335,7 +345,8 @@ class ResultViewModel : ViewModel() {
_dubStatus.postValue(dubStatus) _dubStatus.postValue(dubStatus)
_dubSubSelections.postValue(d.episodes.keys) _dubSubSelections.postValue(d.episodes.keys)
val fillerEpisodes = if (showFillers) safeApiCall { getFillerEpisodes(d.name) } else null val fillerEpisodes =
if (showFillers) safeApiCall { getFillerEpisodes(d.name) } else null
var idIndex = 0 var idIndex = 0
val res = d.episodes.map { ep -> val res = d.episodes.map { ep ->
@ -464,6 +475,20 @@ class ResultViewModel : ViewModel() {
return loadEpisode(episode.id, episode.data, isCasting) return loadEpisode(episode.id, episode.data, isCasting)
} }
fun loadSubtitleFile(uri: Uri, name: String, id: Int?) {
if (id == null) return
val hashMap: HashMap<String, SubtitleFile> = _allEpisodesSubs.value?.get(id) ?: hashMapOf()
hashMap[name] = SubtitleFile(
name,
uri.toString()
)
_allEpisodesSubs.value.apply {
this?.set(id, hashMap)
}?.let {
_allEpisodesSubs.postValue(it)
}
}
private suspend fun loadEpisode( private suspend fun loadEpisode(
id: Int, id: Int,
data: String, data: String,

View file

@ -1,137 +1,150 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:orientation="vertical" android:layout_height="match_parent"
android:layout_width="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@null" android:background="@null"
android:layout_height="match_parent"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_marginBottom="60dp" android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_marginBottom="60dp"
android:layout_height="match_parent" android:baselineAligned="false"
android:baselineAligned="false"> android:orientation="horizontal">
<LinearLayout <LinearLayout
android:id="@+id/sort_sources_holder" android:id="@+id/sort_sources_holder"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_weight="50"
android:layout_weight="50"> android:orientation="vertical">
<TextView <TextView
android:layout_marginTop="10dp" android:layout_width="match_parent"
android:paddingTop="10dp" android:layout_height="wrap_content"
android:paddingBottom="10dp" android:layout_rowWeight="1"
android:layout_marginTop="10dp"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingTop="10dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingBottom="10dp"
android:text="@string/pick_source"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold" />
<ListView
android:id="@+id/sort_providers"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1"
android:background="?attr/primaryBlackBackground"
android:nextFocusLeft="@id/sort_subtitles"
android:nextFocusRight="@id/apply_btt"
tools:listitem="@layout/sort_bottom_single_choice" />
</LinearLayout>
<LinearLayout
android:id="@+id/sort_subtitles_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="50"
android:orientation="vertical">
<LinearLayout
android:id="@+id/subs_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_rowWeight="1"
android:layout_weight="1"
android:nextFocusRight="@id/sort_providers"
android:nextFocusDown="@id/sort_subtitles"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textStyle="bold" android:text="@string/pick_subtitle"
android:text="@string/pick_source"
android:textSize="20sp"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:layout_width="match_parent" android:textSize="20sp"
android:layout_rowWeight="1" android:textStyle="bold" />
android:layout_height="wrap_content">
</TextView>
<ListView
android:nextFocusRight="@id/apply_btt"
android:nextFocusLeft="@id/sort_subtitles"
android:id="@+id/sort_providers"
android:background="?attr/primaryBlackBackground"
android:requiresFadingEdge="vertical"
tools:listitem="@layout/sort_bottom_single_choice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/sort_subtitles_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="50">
<FrameLayout
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:id="@+id/subs_settings"
android:orientation="horizontal"
android:layout_marginTop="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:nextFocusRight="@id/sort_providers"
android:nextFocusDown="@id/sort_subtitles"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:textStyle="bold"
android:text="@string/pick_subtitle"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_rowWeight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</TextView>
<ImageView <ImageView
android:visibility="gone" android:layout_width="25dp"
android:layout_marginTop="0dp" android:layout_height="wrap_content"
android:layout_marginEnd="10dp" android:layout_gravity="end|center_vertical"
android:layout_gravity="end|center_vertical" android:layout_marginTop="0dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_outline_settings_24" android:contentDescription="@string/home_change_provider_img_des"
android:layout_width="25dp" android:src="@drawable/ic_outline_settings_24"
android:layout_height="wrap_content" android:visibility="gone" />
android:contentDescription="@string/home_change_provider_img_des"> </LinearLayout>
</ImageView>
</FrameLayout>
<ListView <ListView
android:nextFocusRight="@id/cancel_btt" android:id="@+id/sort_subtitles"
android:nextFocusLeft="@id/sort_providers" android:layout_width="match_parent"
android:id="@+id/sort_subtitles" android:layout_height="match_parent"
android:background="?attr/primaryBlackBackground" android:layout_rowWeight="1"
android:requiresFadingEdge="vertical" android:background="?attr/primaryBlackBackground"
tools:listitem="@layout/sort_bottom_single_choice" android:nextFocusLeft="@id/sort_providers"
android:layout_width="match_parent" android:nextFocusRight="@id/cancel_btt"
android:layout_rowWeight="1" tools:listitem="@layout/sort_bottom_single_choice" />
android:layout_height="match_parent">
</ListView>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_gravity="bottom" android:layout_height="60dp"
android:gravity="bottom|end" android:layout_gravity="bottom"
android:layout_marginTop="-60dp" android:layout_marginTop="-60dp"
android:layout_width="match_parent" android:orientation="horizontal">
android:layout_height="60dp">
<com.google.android.material.button.MaterialButton <!-- Kinda hack because the gravity won't behave correctly-->
android:nextFocusRight="@id/cancel_btt" <LinearLayout
android:nextFocusLeft="@id/cancel_btt" android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start">
android:id="@+id/apply_btt" <com.google.android.material.button.MaterialButton
android:id="@+id/load_btt"
app:icon="@drawable/ic_baseline_subtitles_24"
style="@style/WhiteButton" style="@style/WhiteButton"
android:layout_gravity="center_vertical|end"
android:text="@string/sort_apply"
android:layout_width="wrap_content" android:layout_width="wrap_content"
/> android:nextFocusLeft="@id/cancel_btt"
<com.google.android.material.button.MaterialButton
android:nextFocusRight="@id/apply_btt" android:nextFocusRight="@id/apply_btt"
android:nextFocusLeft="@id/apply_btt" android:text="@string/player_load_subtitles" />
</LinearLayout>
android:id="@+id/cancel_btt" <com.google.android.material.button.MaterialButton
style="@style/BlackButton" android:id="@+id/apply_btt"
android:layout_gravity="center_vertical|end" style="@style/WhiteButton"
android:text="@string/sort_cancel" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_gravity="center_vertical|end"
/> android:nextFocusLeft="@id/cancel_btt"
android:nextFocusRight="@id/cancel_btt"
android:text="@string/sort_apply" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_btt"
style="@style/BlackButton"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|end"
android:nextFocusLeft="@id/apply_btt"
android:nextFocusRight="@id/apply_btt"
android:text="@string/sort_cancel" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -368,4 +368,6 @@
<string name="subtitles_example_text">The quick brown fox jumps over the lazy dog</string> <string name="subtitles_example_text">The quick brown fox jumps over the lazy dog</string>
<string name="tab_recommended">Recommended</string> <string name="tab_recommended">Recommended</string>
<string name="player_loaded_subtitles">Loaded %s</string>
<string name="player_load_subtitles">Load from file</string>
</resources> </resources>