Compare commits
42 commits
89d5a332d1
...
e009b86153
Author | SHA1 | Date | |
---|---|---|---|
|
e009b86153 | ||
|
d669dcb618 | ||
|
ff5bce074e | ||
|
d85684317b | ||
|
3cc52938f2 | ||
|
3ff71ae489 | ||
|
b511b26aa1 | ||
|
059b17af9a | ||
|
c293c906d4 | ||
|
b5d31a6c72 | ||
|
5609f7368d | ||
|
d70253140d | ||
|
8482df9733 | ||
|
21ae815223 | ||
|
c79342523b | ||
|
7adcc50d75 | ||
|
0ace01f5f8 | ||
|
6dded2e726 | ||
|
ba580f8769 | ||
|
4baa7bc046 | ||
|
633ee6b8c4 | ||
|
062781a43d | ||
|
ccc6788a02 | ||
|
db79b50629 | ||
|
45d4b88738 | ||
|
eabe738136 | ||
|
48d8b18bae | ||
|
15c1ac9593 | ||
|
307ce957c2 | ||
|
1267f9809d | ||
|
e00156cd1c | ||
|
ea1163d25d | ||
|
0c94c3c7dc | ||
|
03023d8c9f | ||
|
785db44744 | ||
|
2dd65705af | ||
|
2bfe4388ea | ||
|
44fe7dac35 | ||
|
544e6432f5 | ||
|
36891e7682 | ||
|
32c6d22035 | ||
|
ba03ef4e9f |
33 changed files with 605 additions and 425 deletions
1
.github/workflows/build.yaml
vendored
1
.github/workflows/build.yaml
vendored
|
@ -25,6 +25,7 @@ jobs:
|
|||
assemble:
|
||||
name: Assemble
|
||||
runs-on: ubuntu-22.04
|
||||
if: startsWith(github.event.head_commit.message, 'build:')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -41,3 +41,4 @@ fastlane/report.xml
|
|||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
push.sh
|
||||
|
|
|
@ -16,35 +16,9 @@
|
|||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 14,
|
||||
"versionName": "0.10.4-0.14.2",
|
||||
"outputFile": "ananas-v0.10.4-0.14.2-Ananas-armeabi-v7a.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "x86_64"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 14,
|
||||
"versionName": "0.10.4-0.14.2",
|
||||
"outputFile": "ananas-v0.10.4-0.14.2-Ananas-x86_64.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "x86"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 14,
|
||||
"versionName": "0.10.4-0.14.2",
|
||||
"outputFile": "ananas-v0.10.4-0.14.2-Ananas-x86.apk"
|
||||
"versionCode": 16,
|
||||
"versionName": "0.10.6-0.14.2",
|
||||
"outputFile": "ananas-v0.10.6-0.14.2-Ananas-armeabi-v7a.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
|
@ -55,9 +29,35 @@
|
|||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 14,
|
||||
"versionName": "0.10.4-0.14.2",
|
||||
"outputFile": "ananas-v0.10.4-0.14.2-Ananas-arm64-v8a.apk"
|
||||
"versionCode": 16,
|
||||
"versionName": "0.10.6-0.14.2",
|
||||
"outputFile": "ananas-v0.10.6-0.14.2-Ananas-arm64-v8a.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "x86_64"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 16,
|
||||
"versionName": "0.10.6-0.14.2",
|
||||
"outputFile": "ananas-v0.10.6-0.14.2-Ananas-x86_64.apk"
|
||||
},
|
||||
{
|
||||
"type": "ONE_OF_MANY",
|
||||
"filters": [
|
||||
{
|
||||
"filterType": "ABI",
|
||||
"value": "x86"
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"versionCode": 16,
|
||||
"versionName": "0.10.6-0.14.2",
|
||||
"outputFile": "ananas-v0.10.6-0.14.2-Ananas-x86.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
|
@ -66,20 +66,20 @@
|
|||
"minApi": 28,
|
||||
"maxApi": 30,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/1/ananas-v0.10.4-0.14.2-Ananas-armeabi-v7a.dm",
|
||||
"baselineProfiles/1/ananas-v0.10.4-0.14.2-Ananas-x86_64.dm",
|
||||
"baselineProfiles/1/ananas-v0.10.4-0.14.2-Ananas-x86.dm",
|
||||
"baselineProfiles/1/ananas-v0.10.4-0.14.2-Ananas-arm64-v8a.dm"
|
||||
"baselineProfiles/1/ananas-v0.10.6-0.14.2-Ananas-armeabi-v7a.dm",
|
||||
"baselineProfiles/1/ananas-v0.10.6-0.14.2-Ananas-arm64-v8a.dm",
|
||||
"baselineProfiles/1/ananas-v0.10.6-0.14.2-Ananas-x86_64.dm",
|
||||
"baselineProfiles/1/ananas-v0.10.6-0.14.2-Ananas-x86.dm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"minApi": 31,
|
||||
"maxApi": 2147483647,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/0/ananas-v0.10.4-0.14.2-Ananas-armeabi-v7a.dm",
|
||||
"baselineProfiles/0/ananas-v0.10.4-0.14.2-Ananas-x86_64.dm",
|
||||
"baselineProfiles/0/ananas-v0.10.4-0.14.2-Ananas-x86.dm",
|
||||
"baselineProfiles/0/ananas-v0.10.4-0.14.2-Ananas-arm64-v8a.dm"
|
||||
"baselineProfiles/0/ananas-v0.10.6-0.14.2-Ananas-armeabi-v7a.dm",
|
||||
"baselineProfiles/0/ananas-v0.10.6-0.14.2-Ananas-arm64-v8a.dm",
|
||||
"baselineProfiles/0/ananas-v0.10.6-0.14.2-Ananas-x86_64.dm",
|
||||
"baselineProfiles/0/ananas-v0.10.6-0.14.2-Ananas-x86.dm"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
@ -25,6 +25,8 @@ android {
|
|||
testInstrumentationRunner = "com.nomadics9.ananas.HiltTestRunner"
|
||||
buildConfigField( "String", "DEFAULT_SERVER_ADDRESS", "\" \"")
|
||||
buildConfigField( "String", "REQUEST_SERVER_ADDRESS", "\" \"")
|
||||
buildConfigField("String", "FORGET_PASSWORD_ADDRESS", "\" \"")
|
||||
buildConfigField("String", "UPDATE_ADDRESS", "\" \"")
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
|
@ -68,6 +70,8 @@ android {
|
|||
isDefault = false
|
||||
buildConfigField( "String", "DEFAULT_SERVER_ADDRESS", "\"https://askar.tv\"")
|
||||
buildConfigField( "String", "REQUEST_SERVER_ADDRESS", "\"https://r.askar.tv\"")
|
||||
buildConfigField("String", "FORGET_PASSWORD_ADDRESS", "\"https://user.askar.tv/my/account\"")
|
||||
buildConfigField("String", "UPDATE_ADDRESS", "\"https://fs.nmd.mov/p/ananas.apk\"")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,6 +134,7 @@ dependencies {
|
|||
implementation(libs.material)
|
||||
implementation(libs.media3.ffmpeg.decoder)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.markwon)
|
||||
|
||||
coreLibraryDesugaring(libs.android.desugar.jdk)
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import com.nomadics9.ananas.core.R as CoreR
|
||||
import com.nomadics9.ananas.models.VideoQuality
|
||||
|
||||
var isControlsLocked: Boolean = false
|
||||
|
||||
|
@ -86,12 +87,10 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
|
||||
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val changeQualityButton: ImageButton = findViewById(R.id.btnChangeQuality)
|
||||
changeQualityButton.setOnClickListener {
|
||||
showQualitySelectionDialog()
|
||||
}
|
||||
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
binding.playerView.player = viewModel.player
|
||||
|
@ -356,8 +355,7 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
|
||||
if (appPreferences.playerTrickplay) {
|
||||
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
||||
previewScrubListener =
|
||||
PreviewScrubListener(
|
||||
previewScrubListener = PreviewScrubListener(
|
||||
imagePreview,
|
||||
timeBar,
|
||||
viewModel.player,
|
||||
|
@ -439,50 +437,32 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
|
||||
try {
|
||||
enterPictureInPictureMode(pipParams())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
} catch (_: IllegalArgumentException) { }
|
||||
}
|
||||
|
||||
private var selectedIndex = 1 // Default to "Original" (index 1)
|
||||
private fun showQualitySelectionDialog() {
|
||||
val height = viewModel.getOriginalHeight()
|
||||
val originalResolution = viewModel.getOriginalResolution() ?: 0
|
||||
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList()
|
||||
val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList()
|
||||
|
||||
// Map entries to values
|
||||
val qualityMap = qualityEntries.zip(qualityValues).toMap()
|
||||
val qualities = qualityEntries.toMutableList()
|
||||
val closestQuality = VideoQuality.entries
|
||||
.filter { it != VideoQuality.Auto && it != VideoQuality.Original }
|
||||
.minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution) }
|
||||
|
||||
val qualities: List<String> =
|
||||
when (height) {
|
||||
0 -> qualityEntries
|
||||
in 1001..1999 ->
|
||||
listOf(
|
||||
qualityEntries[0],
|
||||
"${qualityEntries[1]} (1080p)",
|
||||
qualityEntries[2],
|
||||
qualityEntries[3],
|
||||
qualityEntries[4],
|
||||
qualityEntries[5],
|
||||
)
|
||||
in 2000..3000 ->
|
||||
listOf(
|
||||
qualityEntries[0],
|
||||
"${qualityEntries[1]} (4K)",
|
||||
qualityEntries[2],
|
||||
qualityEntries[3],
|
||||
qualityEntries[4],
|
||||
qualityEntries[5],
|
||||
)
|
||||
else -> qualityEntries
|
||||
if (closestQuality != null) {
|
||||
qualities[1] = "${qualities[1]} (${closestQuality})"
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle("Select Video Quality")
|
||||
.setItems(qualities.toTypedArray()) { _, which ->
|
||||
val selectedQualityEntry = qualities[which]
|
||||
val selectedQualityValue =
|
||||
qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry
|
||||
.setTitle(CoreR.string.select_quality)
|
||||
.setSingleChoiceItems(qualities.toTypedArray(), selectedIndex) { dialog, which ->
|
||||
selectedIndex = which
|
||||
val selectedQualityValue = qualityValues[which]
|
||||
viewModel.changeVideoQuality(selectedQualityValue)
|
||||
}.show()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
|
|
|
@ -172,11 +172,11 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
}else if (!appPreferences.downloadQualityDefault) {
|
||||
createPickQualityDialog()
|
||||
} else {
|
||||
download()
|
||||
startDownload()
|
||||
}
|
||||
}
|
||||
|
||||
private fun download(){
|
||||
private fun startDownload(){
|
||||
binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent)
|
||||
binding.itemActions.progressDownload.isIndeterminate = true
|
||||
binding.itemActions.progressDownload.isVisible = true
|
||||
|
@ -413,8 +413,8 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
private fun createPickQualityDialog() {
|
||||
val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_entries)
|
||||
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_values)
|
||||
val qualityEntries = resources.getStringArray(CoreR.array.download_quality_entries)
|
||||
val qualityValues = resources.getStringArray(CoreR.array.download_quality_values)
|
||||
val quality = appPreferences.downloadQuality
|
||||
val currentQualityIndex = qualityValues.indexOf(quality)
|
||||
var selectedQuality = quality
|
||||
|
@ -428,7 +428,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
builder.setPositiveButton("Download") { dialog, _ ->
|
||||
appPreferences.downloadQuality = selectedQuality
|
||||
dialog.dismiss()
|
||||
download()
|
||||
startDownload()
|
||||
}
|
||||
builder.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.nomadics9.ananas.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Html.fromHtml
|
||||
import android.view.LayoutInflater
|
||||
|
@ -17,11 +19,13 @@ import androidx.navigation.fragment.findNavController
|
|||
import androidx.navigation.fragment.navArgs
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import com.nomadics9.ananas.AppPreferences
|
||||
import com.nomadics9.ananas.BuildConfig
|
||||
import com.nomadics9.ananas.adapters.UserLoginListAdapter
|
||||
import com.nomadics9.ananas.database.ServerDatabaseDao
|
||||
import com.nomadics9.ananas.databinding.FragmentLoginBinding
|
||||
import com.nomadics9.ananas.viewmodels.LoginEvent
|
||||
import com.nomadics9.ananas.viewmodels.LoginViewModel
|
||||
import io.noties.markwon.Markwon
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -78,6 +82,17 @@ class LoginFragment : Fragment() {
|
|||
(binding.editTextPassword as AppCompatEditText).requestFocus()
|
||||
}
|
||||
|
||||
|
||||
if (BuildConfig.FLAVOR == "Ananas") {
|
||||
binding.buttonForgetPassword.setOnClickListener {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(BuildConfig.FORGET_PASSWORD_ADDRESS))
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
binding.buttonForgetPassword.isVisible = true
|
||||
} else {
|
||||
binding.buttonForgetPassword.isVisible = false
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.uiState.collect { uiState ->
|
||||
|
@ -143,7 +158,21 @@ class LoginFragment : Fragment() {
|
|||
binding.editTextPasswordLayout.isEnabled = true
|
||||
|
||||
uiState.disclaimer?.let { disclaimer ->
|
||||
binding.loginDisclaimer.text = fromHtml(disclaimer, 0)
|
||||
if (BuildConfig.FLAVOR == "Ananas") {
|
||||
val lines = disclaimer.lines()
|
||||
val lineToRemoveIndex = 3
|
||||
val filteredLines = lines.toMutableList().apply {
|
||||
if (size > lineToRemoveIndex) {
|
||||
removeAt(lineToRemoveIndex)
|
||||
}
|
||||
}
|
||||
val filteredDisclaimer = filteredLines.joinToString("\n")
|
||||
val markwon = Markwon.create(requireContext())
|
||||
markwon.setMarkdown(binding.loginDisclaimer, filteredDisclaimer)
|
||||
} else {
|
||||
val markwon = Markwon.create(requireContext())
|
||||
markwon.setMarkdown(binding.loginDisclaimer, disclaimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -209,11 +209,11 @@ class MovieFragment : Fragment() {
|
|||
} else if (!appPreferences.downloadQualityDefault) {
|
||||
createPickQualityDialog()
|
||||
} else {
|
||||
download()
|
||||
startDownload()
|
||||
}
|
||||
}
|
||||
|
||||
private fun download() {
|
||||
private fun startDownload() {
|
||||
binding.itemActions.downloadButton.setIconResource(android.R.color.transparent)
|
||||
binding.itemActions.progressDownload.isIndeterminate = true
|
||||
binding.itemActions.progressDownload.isVisible = true
|
||||
|
@ -506,8 +506,8 @@ class MovieFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun createPickQualityDialog() {
|
||||
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries)
|
||||
val qualityValues = resources.getStringArray(CoreR.array.quality_values)
|
||||
val qualityEntries = resources.getStringArray(CoreR.array.download_quality_entries)
|
||||
val qualityValues = resources.getStringArray(CoreR.array.download_quality_values)
|
||||
val quality = appPreferences.downloadQuality
|
||||
val currentQualityIndex = qualityValues.indexOf(quality)
|
||||
var selectedQuality = quality
|
||||
|
@ -520,7 +520,7 @@ class MovieFragment : Fragment() {
|
|||
}
|
||||
builder.setPositiveButton("Download") { dialog, _ ->
|
||||
appPreferences.downloadQuality = selectedQuality
|
||||
download()
|
||||
startDownload()
|
||||
dialog.dismiss()
|
||||
}
|
||||
builder.setNegativeButton("Cancel") { dialog, _ ->
|
||||
|
|
|
@ -226,8 +226,8 @@ class SeasonFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun createPickQualityDialog(onQualitySelected: () -> Unit) {
|
||||
val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_entries)
|
||||
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_values)
|
||||
val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.download_quality_entries)
|
||||
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.download_quality_values)
|
||||
val quality = appPreferences.downloadQuality
|
||||
val currentQualityIndex = qualityValues.indexOf(quality)
|
||||
|
||||
|
|
|
@ -3,12 +3,23 @@ package com.nomadics9.ananas.fragments
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import com.nomadics9.ananas.AppPreferences
|
||||
import com.nomadics9.ananas.BuildConfig
|
||||
import com.nomadics9.ananas.utils.restart
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import com.nomadics9.ananas.core.R as CoreR
|
||||
|
||||
|
@ -17,6 +28,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
private val updateUrl = BuildConfig.UPDATE_ADDRESS
|
||||
private var isUpdateAvailable: Boolean = false
|
||||
private var newLastModifiedDate: Date? = null
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(CoreR.xml.fragment_settings, rootKey)
|
||||
|
||||
|
@ -27,13 +42,21 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
findPreference<Preference>("switchUser")?.setOnPreferenceClickListener {
|
||||
val serverId = appPreferences.currentServer!!
|
||||
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToUsersFragment(serverId))
|
||||
findNavController().navigate(
|
||||
TwoPaneSettingsFragmentDirections.actionNavigationSettingsToUsersFragment(
|
||||
serverId
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>("switchAddress")?.setOnPreferenceClickListener {
|
||||
val serverId = appPreferences.currentServer!!
|
||||
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToServerAddressesFragment(serverId))
|
||||
findNavController().navigate(
|
||||
TwoPaneSettingsFragmentDirections.actionNavigationSettingsToServerAddressesFragment(
|
||||
serverId
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -51,14 +74,102 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
true
|
||||
}
|
||||
|
||||
|
||||
findPreference<Preference>("appInfo")?.setOnPreferenceClickListener {
|
||||
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionSettingsFragmentToAboutLibraries())
|
||||
if (isUpdateAvailable && newLastModifiedDate != null) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(updateUrl))
|
||||
startActivity(intent)
|
||||
storeDate(newLastModifiedDate!!)
|
||||
true
|
||||
} else {
|
||||
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionSettingsFragmentToAboutLibraries())
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
findPreference<Preference>("requests")?.setOnPreferenceClickListener {
|
||||
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToRequestsWebFragment())
|
||||
true
|
||||
}
|
||||
|
||||
// Check for updates when the settings screen is opened
|
||||
checkForUpdates()
|
||||
}
|
||||
|
||||
private fun checkForUpdates() {
|
||||
lifecycleScope.launch {
|
||||
val lastModifiedDate = fetchLastModifiedDate(updateUrl)
|
||||
if (lastModifiedDate != null) {
|
||||
Timber.d("Fetched Last-Modified date: $lastModifiedDate")
|
||||
val storedDate = getStoredDate()
|
||||
Timber.d("Stored date: $storedDate")
|
||||
if (storedDate == Date(0L) || lastModifiedDate.after(storedDate)) {
|
||||
Timber.d("Update available")
|
||||
isUpdateAvailable = true
|
||||
newLastModifiedDate = lastModifiedDate
|
||||
showUpdateAvailable()
|
||||
} else {
|
||||
Timber.d("No update available")
|
||||
isUpdateAvailable = false
|
||||
}
|
||||
} else {
|
||||
Timber.d("Failed to fetch Last-Modified date")
|
||||
isUpdateAvailable = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchLastModifiedDate(urlString: String): Date? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
var urlConnection: HttpURLConnection? = null
|
||||
try {
|
||||
val url = URL(urlString)
|
||||
urlConnection = url.openConnection() as HttpURLConnection
|
||||
urlConnection.requestMethod = "HEAD"
|
||||
val lastModified = urlConnection.getHeaderField("Last-Modified")
|
||||
if (lastModified != null) {
|
||||
val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US)
|
||||
dateFormat.parse(lastModified)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error fetching Last-Modified date")
|
||||
null
|
||||
} finally {
|
||||
urlConnection?.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStoredDate(): Date {
|
||||
val sharedPreferences = preferenceManager.sharedPreferences
|
||||
val storedDateString = sharedPreferences?.getString("stored_date", null)
|
||||
Timber.d("Retrieved stored date string: $storedDateString")
|
||||
return if (storedDateString != null) {
|
||||
try {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).parse(storedDateString) ?: Date(0)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error parsing stored date string")
|
||||
Date(0)
|
||||
}
|
||||
} else {
|
||||
Date(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeDate(date: Date) {
|
||||
val dateString = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).format(date)
|
||||
preferenceManager.sharedPreferences?.edit()?.putString("stored_date", dateString)?.apply()
|
||||
Timber.d("Stored new date: $dateString")
|
||||
}
|
||||
|
||||
private fun showUpdateAvailable() {
|
||||
val appInfoPreference = findPreference<Preference>("appInfo")
|
||||
appInfoPreference?.let {
|
||||
it.summary = "Update available!"
|
||||
it.icon = ResourcesCompat.getDrawable(resources, CoreR.drawable.ic_download, null) // Ensure this drawable exists
|
||||
Timber.d("Update available UI shown")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!--TODO: Content Desc to Strings-->
|
||||
<ImageButton
|
||||
android:id="@+id/btnChangeQuality"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -80,7 +81,7 @@
|
|||
android:background="@drawable/transparent_circle_background"
|
||||
android:contentDescription="Quality"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_quality"
|
||||
android:src="@drawable/ic_monitor_play"
|
||||
android:layout_gravity="end"
|
||||
app:tint="@android:color/white"
|
||||
/>
|
||||
|
|
|
@ -141,10 +141,24 @@
|
|||
android:visibility="invisible" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_forget_password"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/forget_password" />
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/login_disclaimer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_margin="24dp"
|
||||
android:textSize="16sp"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import org.gradle.api.JavaVersion
|
||||
|
||||
object Versions {
|
||||
const val appCode = 14
|
||||
const val appName = "0.10.4-0.14.2"
|
||||
const val appCode = 16
|
||||
const val appName = "0.10.6-0.14.2"
|
||||
|
||||
const val compileSdk = 34
|
||||
const val buildTools = "34.0.0"
|
||||
|
|
|
@ -79,15 +79,8 @@ class DownloaderImpl(
|
|||
),
|
||||
)
|
||||
}
|
||||
val qualityPreference = appPreferences.downloadQuality!!
|
||||
Timber.d("Quality preference: $qualityPreference")
|
||||
return if (qualityPreference != "Original") {
|
||||
Timber.d("Handling Transcoding download for item: ${item.id}")
|
||||
handleTranscodeDownload(item, source, storageIndex, trickplayInfo, segments, path, qualityPreference)
|
||||
} else {
|
||||
Timber.d("Handling original download for item: ${item.id}")
|
||||
downloadOriginalItem(item, source, storageIndex, trickplayInfo, segments, path)
|
||||
}
|
||||
handleDownload(item, source, storageIndex, trickplayInfo, segments, path)
|
||||
return Pair(-1, null)
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId }
|
||||
|
@ -108,79 +101,7 @@ class DownloaderImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTranscodeDownload(
|
||||
item: FindroidItem,
|
||||
source: FindroidSource,
|
||||
storageIndex: Int,
|
||||
trickplayInfo: FindroidTrickplayInfo?,
|
||||
segments: List<FindroidSegment>?,
|
||||
path: Uri,
|
||||
quality: String,
|
||||
): Pair<Long, UiText?> {
|
||||
val transcodingUrl = getTranscodedUrl(item.id, quality)
|
||||
when (item) {
|
||||
is FindroidMovie -> {
|
||||
database.insertMovie(item.toFindroidMovieDto(appPreferences.currentServer!!))
|
||||
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
||||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||
downloadExternalMediaStreams(item, source, storageIndex)
|
||||
downloadEmbeddedMediaStreams(item, source, storageIndex)
|
||||
if (trickplayInfo != null) {
|
||||
downloadTrickplayData(item.id, source.id, trickplayInfo)
|
||||
}
|
||||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(transcodingUrl)
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
}
|
||||
|
||||
is FindroidEpisode -> {
|
||||
database.insertShow(
|
||||
jellyfinRepository
|
||||
.getShow(item.seriesId)
|
||||
.toFindroidShowDto(appPreferences.currentServer!!),
|
||||
)
|
||||
database.insertSeason(
|
||||
jellyfinRepository.getSeason(item.seasonId).toFindroidSeasonDto(),
|
||||
)
|
||||
database.insertEpisode(item.toFindroidEpisodeDto(appPreferences.currentServer!!))
|
||||
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
||||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||
downloadExternalMediaStreams(item, source, storageIndex)
|
||||
downloadEmbeddedMediaStreams(item, source, storageIndex)
|
||||
if (trickplayInfo != null) {
|
||||
downloadTrickplayData(item.id, source.id, trickplayInfo)
|
||||
}
|
||||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(transcodingUrl)
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
}
|
||||
}
|
||||
return Pair(-1, null)
|
||||
}
|
||||
|
||||
private suspend fun downloadOriginalItem(
|
||||
private suspend fun handleDownload(
|
||||
item: FindroidItem,
|
||||
source: FindroidSource,
|
||||
storageIndex: Int,
|
||||
|
@ -200,6 +121,22 @@ class DownloaderImpl(
|
|||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
if (appPreferences.downloadQuality != VideoQuality.Original.toString()) {
|
||||
downloadEmbeddedMediaStreams(item, source, storageIndex)
|
||||
val transcodingUrl =
|
||||
getTranscodedUrl(item.id, appPreferences.downloadQuality!!)
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(transcodingUrl)
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
} else {
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(source.path.toUri())
|
||||
|
@ -212,6 +149,7 @@ class DownloaderImpl(
|
|||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
}
|
||||
}
|
||||
|
||||
is FindroidEpisode -> {
|
||||
database.insertShow(
|
||||
|
@ -232,6 +170,22 @@ class DownloaderImpl(
|
|||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
if (appPreferences.downloadQuality != VideoQuality.Original.toString()) {
|
||||
downloadEmbeddedMediaStreams(item, source, storageIndex)
|
||||
val transcodingUrl =
|
||||
getTranscodedUrl(item.id, appPreferences.downloadQuality!!)
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(transcodingUrl)
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
} else {
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(source.path.toUri())
|
||||
|
@ -245,6 +199,7 @@ class DownloaderImpl(
|
|||
return Pair(downloadId, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pair(-1, null)
|
||||
}
|
||||
|
||||
|
@ -483,8 +438,8 @@ class DownloaderImpl(
|
|||
mediaSourceId,
|
||||
playSessionId,
|
||||
VideoQuality.getBitrate(videoQuality),
|
||||
VideoQuality.getQualityInt(videoQuality),
|
||||
"mkv",
|
||||
VideoQuality.getHeight(videoQuality),
|
||||
)
|
||||
|
||||
return downloadUrl.toUri()
|
||||
|
|
35
core/src/main/res/drawable/ic_monitor_play.xml
Normal file
35
core/src/main/res/drawable/ic_monitor_play.xml
Normal file
|
@ -0,0 +1,35 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:pathData="M10,7.75a0.75,0.75 0,0 1,1.142 -0.638l3.664,2.249a0.75,0.75 0,0 1,0 1.278l-3.664,2.25a0.75,0.75 0,0 1,-1.142 -0.64z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,17v4"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,21h8"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M4,3L20,3A2,2 0,0 1,22 5L22,15A2,2 0,0 1,20 17L4,17A2,2 0,0 1,2 15L2,5A2,2 0,0 1,4 3z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -1,2 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="add_server_error_outdated">اصدار الخادم قديم: %1$s. الرجاء تحديث الخادم</string>
|
||||
<string name="add_server_error_not_jellyfin">ليس خادم جيلي فن:%1$s</string>
|
||||
<string name="add_server_error_version">اصدار الخادم غير مدعوم: %1$s. الرجاء تحديث الخادم</string>
|
||||
<string name="add_server_error_slow">رد الخادم بطيء: %1$s</string>
|
||||
<string name="login">تسجيل دخول</string>
|
||||
<string name="login_error_wrong_username_password">اسم المستخدم او الكلمه السريه غير صحيحه</string>
|
||||
<string name="select_server">اختر الخادم</string>
|
||||
<string name="edit_text_server_address_hint">عنوان الخادم</string>
|
||||
<string name="edit_text_username_hint">اسم المستخدم</string>
|
||||
<string name="edit_text_password_hint">الكلمه السريه</string>
|
||||
<string name="button_connect">اتصل</string>
|
||||
<string name="button_login">تسجيل دخول</string>
|
||||
<string name="remove_server">حذف الخادم</string>
|
||||
<string name="add_server">اضافه خادم</string>
|
||||
<string name="add_server_error_not_found">الخادم غير موجود</string>
|
||||
<string name="add_server_error_empty_address">عنوان الخادم فاضي</string>
|
||||
<string name="add_server_error_no_id">الخادم غير معرف بالid , يبدو انه هناك خلل في الخادم</string>
|
||||
</resources>
|
|
@ -26,12 +26,12 @@
|
|||
<item>opensles</item>
|
||||
</string-array>
|
||||
<string-array name="quality_entries">
|
||||
<item>Auto</item>
|
||||
<item>Original</item>
|
||||
<item>1080p - 8Mbps</item>
|
||||
<item>720p - 2Mbps</item>
|
||||
<item>480p - 1Mbps</item>
|
||||
<item>360p - 800Kbps</item>
|
||||
<item>@string/quality_auto</item>
|
||||
<item>@string/quality_original</item>
|
||||
<item>@string/quality_1080p</item>
|
||||
<item>@string/quality_720p</item>
|
||||
<item>@string/quality_480p</item>
|
||||
<item>@string/quality_360p</item>
|
||||
</string-array>
|
||||
<string-array name="quality_values">
|
||||
<item>Auto</item>
|
||||
|
@ -41,4 +41,22 @@
|
|||
<item>480p</item>
|
||||
<item>360p</item>
|
||||
</string-array>
|
||||
<string-array name="download_quality_entries">
|
||||
<item>@string/quality_original</item>
|
||||
<item>@string/quality_1080p</item>
|
||||
<item>@string/quality_720p</item>
|
||||
<item>@string/quality_480p</item>
|
||||
<item>@string/quality_360p</item>
|
||||
</string-array>
|
||||
<string-array name="download_quality_values">
|
||||
<item>Original</item>
|
||||
<item>1080p</item>
|
||||
<item>720p</item>
|
||||
<item>480p</item>
|
||||
<item>360p</item>
|
||||
</string-array>
|
||||
<string-array name="codecs">
|
||||
<item>h264</item>
|
||||
<item>hevc</item>
|
||||
</string-array>
|
||||
</resources>
|
|
@ -11,6 +11,7 @@
|
|||
<string name="add_server_error_not_found">Server not found</string>
|
||||
<string name="add_server_error_no_id">Server has no id, something seems to be wrong with the server</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="forget_password">Forget Password?</string>
|
||||
<string name="login_error_wrong_username_password">Wrong username or password</string>
|
||||
<string name="select_server">Select server</string>
|
||||
<string name="edit_text_server_address_hint">Server address</string>
|
||||
|
@ -139,6 +140,7 @@
|
|||
<string name="settings_request_timeout">Request timeout (ms)</string>
|
||||
<string name="settings_connect_timeout">Connect timeout (ms)</string>
|
||||
<string name="settings_socket_timeout">Socket timeout (ms)</string>
|
||||
<string name="settings_quality_codec">Transcoding codec</string>
|
||||
<string name="users">Users</string>
|
||||
<string name="add_user">Add user</string>
|
||||
<string name="pref_player_mpv_hwdec">Hardware decoding</string>
|
||||
|
@ -193,6 +195,15 @@
|
|||
<string name="unmark_as_played">Unmark as played</string>
|
||||
<string name="add_to_favorites">Add to favorites</string>
|
||||
<string name="remove_from_favorites">Remove from favorites</string>
|
||||
<string name="quality_default">Default to selected download quality</string>
|
||||
<string name="download_quality">Download Quality</string>
|
||||
<string name="select_quality">Select Video Quality</string>
|
||||
<string name="quality_auto">Auto</string>
|
||||
<string name="quality_original">Original</string>
|
||||
<string name="quality_1080p">1080p - 8Mbps</string>
|
||||
<string name="quality_720p">720p - 3Mbps</string>
|
||||
<string name="quality_480p">480p - 1.5Mbps</string>
|
||||
<string name="quality_360p">360p - 0.8Mbps</string>
|
||||
<string name="alaskarTV_requests">AlaskarTV Requests</string>
|
||||
<string name="pref_player_trickplay_gesture">Trick Play in seek gesture</string>
|
||||
<string name="pref_player_trickplay_gesture_summary">Requires \'Seek gesture\' and \'Trick Play\'</string>
|
||||
|
|
|
@ -75,7 +75,8 @@
|
|||
<Preference
|
||||
app:key="appInfo"
|
||||
app:icon="@drawable/ic_logo"
|
||||
app:title="@string/app_info" />
|
||||
app:title="@string/app_info"
|
||||
app:summary="" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
|
|
|
@ -11,15 +11,13 @@
|
|||
app:title="@string/download_roaming" />
|
||||
<ListPreference
|
||||
android:key="pref_downloads_quality"
|
||||
android:title="Download Quality"
|
||||
android:defaultValue="Original"
|
||||
android:entries="@array/quality_entries"
|
||||
android:entryValues="@array/quality_values"
|
||||
android:title="@string/download_quality"
|
||||
android:defaultValue="@string/quality_original"
|
||||
android:entries="@array/download_quality_entries"
|
||||
android:entryValues="@array/download_quality_values"
|
||||
android:summary="%s" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
app:key="pref_downloads_quality_default"
|
||||
app:summary="Default to picked Download Quality" />
|
||||
|
||||
app:summary="@string/quality_default" />
|
||||
</PreferenceScreen>
|
|
@ -20,4 +20,11 @@
|
|||
android:defaultValue="true"
|
||||
app:key="pref_auto_offline"
|
||||
app:title="@string/turn_on_offline_mode_automatically" />
|
||||
<ListPreference
|
||||
app:defaultValue="hevc"
|
||||
app:key="pref_network_codec"
|
||||
app:title="@string/settings_quality_codec"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:entries="@array/codecs"
|
||||
app:entryValues="@array/codecs" />
|
||||
</PreferenceScreen>
|
|
@ -2,24 +2,30 @@ package com.nomadics9.ananas.models
|
|||
|
||||
enum class VideoQuality(
|
||||
val bitrate: Int,
|
||||
val qualityString: String,
|
||||
val qualityInt: Int,
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val isOriginalQuality: Boolean,
|
||||
) {
|
||||
PAuto(1, "Auto", 1080),
|
||||
POriginal(1000000000, "Original", 1080),
|
||||
P1080(8000000, "1080p", 1080),
|
||||
P720(2000000, "720p", 720),
|
||||
P480(1000000, "480p", 480),
|
||||
P360(700000, "360p", 360),
|
||||
;
|
||||
Auto(10000000, 1080, 1920, false),
|
||||
Original(1000000000, 1080, 1920, true),
|
||||
P3840(12000000,3840, 2160, false), // Here for future proofing and to calculate original resolution only
|
||||
P1080(8000000, 1080, 1920, false),
|
||||
P720(3000000, 720, 1280, false),
|
||||
P480(1500000, 480, 854, false),
|
||||
P360(800000, 360, 640, false);
|
||||
|
||||
override fun toString(): String = when (this) {
|
||||
Auto -> "Auto"
|
||||
Original -> "Original"
|
||||
P3840 -> "4K"
|
||||
else -> "${height}p"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromString(quality: String): VideoQuality? = entries.find { it.qualityString == quality }
|
||||
|
||||
fun fromString(quality: String): VideoQuality? = entries.find { it.toString() == quality }
|
||||
fun getBitrate(quality: VideoQuality): Int = quality.bitrate
|
||||
|
||||
fun getQualityString(quality: VideoQuality): String = quality.qualityString
|
||||
|
||||
fun getQualityInt(quality: VideoQuality): Int = quality.qualityInt
|
||||
fun getHeight(quality: VideoQuality): Int = quality.height
|
||||
fun getWidth(quality: VideoQuality): Int = quality.width
|
||||
fun getIsOriginalQuality(quality: VideoQuality): Boolean = quality.isOriginalQuality
|
||||
}
|
||||
}
|
|
@ -88,40 +88,21 @@ interface JellyfinRepository {
|
|||
offline: Boolean = false,
|
||||
): List<FindroidEpisode>
|
||||
|
||||
suspend fun getMediaSources(
|
||||
itemId: UUID,
|
||||
includePath: Boolean = false,
|
||||
): List<FindroidSource>
|
||||
suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List<FindroidSource>
|
||||
|
||||
suspend fun getStreamUrl(
|
||||
itemId: UUID,
|
||||
mediaSourceId: String,
|
||||
playSessionId: String? = null,
|
||||
): String
|
||||
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String
|
||||
|
||||
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
|
||||
|
||||
suspend fun getTrickplayData(
|
||||
itemId: UUID,
|
||||
width: Int,
|
||||
index: Int,
|
||||
): ByteArray?
|
||||
suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray?
|
||||
|
||||
suspend fun postCapabilities()
|
||||
|
||||
suspend fun postPlaybackStart(itemId: UUID)
|
||||
|
||||
suspend fun postPlaybackStop(
|
||||
itemId: UUID,
|
||||
positionTicks: Long,
|
||||
playedPercentage: Int,
|
||||
)
|
||||
suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int)
|
||||
|
||||
suspend fun postPlaybackProgress(
|
||||
itemId: UUID,
|
||||
positionTicks: Long,
|
||||
isPaused: Boolean,
|
||||
)
|
||||
suspend fun postPlaybackProgress(itemId: UUID, positionTicks: Long, isPaused: Boolean)
|
||||
|
||||
suspend fun markAsFavorite(itemId: UUID)
|
||||
|
||||
|
@ -155,8 +136,8 @@ interface JellyfinRepository {
|
|||
mediaSourceId: String,
|
||||
playSessionId: String,
|
||||
videoBitrate: Int,
|
||||
maxHeight: Int,
|
||||
container: String,
|
||||
maxHeight: Int,
|
||||
): String
|
||||
|
||||
suspend fun getTranscodedVideoStream(
|
||||
|
@ -175,4 +156,6 @@ interface JellyfinRepository {
|
|||
): Response<PlaybackInfoResponse>
|
||||
|
||||
suspend fun stopEncodingProcess(playSessionId: String)
|
||||
|
||||
suspend fun getAccessToken(): String?
|
||||
}
|
||||
|
|
|
@ -384,7 +384,7 @@ class JellyfinRepositoryImpl(
|
|||
playSessionId: String?,
|
||||
): String =
|
||||
withContext(Dispatchers.IO) {
|
||||
// val deviceId = getDeviceId()
|
||||
val deviceId = getDeviceId()
|
||||
try {
|
||||
val url =
|
||||
if (playSessionId != null) {
|
||||
|
@ -393,7 +393,7 @@ class JellyfinRepositoryImpl(
|
|||
static = true,
|
||||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
// deviceId = deviceId,
|
||||
deviceId = deviceId,
|
||||
context = EncodingContext.STREAMING,
|
||||
)
|
||||
} else {
|
||||
|
@ -401,7 +401,7 @@ class JellyfinRepositoryImpl(
|
|||
itemId,
|
||||
static = true,
|
||||
mediaSourceId = mediaSourceId,
|
||||
// deviceId = deviceId,
|
||||
deviceId = deviceId,
|
||||
)
|
||||
}
|
||||
url
|
||||
|
@ -752,8 +752,8 @@ class JellyfinRepositoryImpl(
|
|||
mediaSourceId: String,
|
||||
playSessionId: String,
|
||||
videoBitrate: Int,
|
||||
maxHeight: Int,
|
||||
container: String,
|
||||
maxHeight: Int,
|
||||
): String {
|
||||
val url =
|
||||
jellyfinApi.videosApi.getVideoStreamByContainerUrl(
|
||||
|
@ -764,9 +764,9 @@ class JellyfinRepositoryImpl(
|
|||
playSessionId = playSessionId,
|
||||
videoBitRate = videoBitrate,
|
||||
maxHeight = maxHeight,
|
||||
audioBitRate = 128000,
|
||||
videoCodec = "hevc",
|
||||
audioCodec = "aac",
|
||||
audioBitRate = 328000,
|
||||
videoCodec = appPreferences.transcodeCodec,
|
||||
audioCodec = "aac,ac3,eac3",
|
||||
container = container,
|
||||
startTimeTicks = 0,
|
||||
copyTimestamps = true,
|
||||
|
@ -782,7 +782,7 @@ class JellyfinRepositoryImpl(
|
|||
playSessionId: String,
|
||||
videoBitrate: Int,
|
||||
): String {
|
||||
val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto)
|
||||
val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.Auto)
|
||||
val url: String
|
||||
try {
|
||||
url =
|
||||
|
@ -795,9 +795,9 @@ class JellyfinRepositoryImpl(
|
|||
playSessionId = playSessionId,
|
||||
videoBitRate = videoBitrate,
|
||||
enableAdaptiveBitrateStreaming = false,
|
||||
audioBitRate = 128000,
|
||||
videoCodec = "hevc",
|
||||
audioCodec = "aac",
|
||||
audioBitRate = 328000,
|
||||
videoCodec = appPreferences.transcodeCodec,
|
||||
audioCodec = "aac,ac3,eac3",
|
||||
startTimeTicks = 0,
|
||||
copyTimestamps = true,
|
||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||
|
@ -813,8 +813,8 @@ class JellyfinRepositoryImpl(
|
|||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
enableAdaptiveBitrateStreaming = true,
|
||||
videoCodec = "hevc",
|
||||
audioCodec = "aac",
|
||||
videoCodec = appPreferences.transcodeCodec,
|
||||
audioCodec = "aac,ac3,eac3",
|
||||
startTimeTicks = 0,
|
||||
copyTimestamps = true,
|
||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||
|
@ -839,4 +839,8 @@ class JellyfinRepositoryImpl(
|
|||
playSessionId = playSessionId,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAccessToken(): String? {
|
||||
return jellyfinApi.api.accessToken
|
||||
}
|
||||
}
|
||||
|
|
|
@ -336,8 +336,8 @@ class JellyfinRepositoryOfflineImpl(
|
|||
mediaSourceId: String,
|
||||
playSessionId: String,
|
||||
videoBitrate: Int,
|
||||
maxHeight: Int,
|
||||
container: String,
|
||||
maxHeight: Int,
|
||||
): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
@ -364,4 +364,8 @@ class JellyfinRepositoryOfflineImpl(
|
|||
override suspend fun stopEncodingProcess(playSessionId: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getAccessToken(): String? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ libmpv = "0.3.0"
|
|||
material = "1.12.0"
|
||||
media3-ffmpeg-decoder = "1.3.1+2"
|
||||
timber = "5.0.1"
|
||||
markwon = "4.6.2"
|
||||
|
||||
[libraries]
|
||||
aboutlibraries-core = { group = "com.mikepenz", name = "aboutlibraries-core", version.ref = "aboutlibraries" }
|
||||
|
@ -99,6 +100,7 @@ material = { group = "com.google.android.material", name = "material", version.r
|
|||
media3-ffmpeg-decoder = { group = "org.jellyfin.media3", name = "media3-ffmpeg-decoder", version.ref = "media3-ffmpeg-decoder" }
|
||||
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
markwon = { group = "io.noties.markwon", name = "core", version.ref = "markwon" }
|
||||
|
||||
[plugins]
|
||||
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
|
||||
|
|
1
lint.xml
1
lint.xml
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
|
||||
</lint>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.nomadics9.ananas
|
||||
|
||||
import androidx.media3.common.MimeTypes
|
||||
|
||||
public fun setSubtitlesMimeTypes(codec: String): String {
|
||||
return when (codec) {
|
||||
"subrip" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"webvtt" -> MimeTypes.TEXT_VTT
|
||||
"ssa" -> MimeTypes.TEXT_SSA
|
||||
"pgs" -> MimeTypes.APPLICATION_PGS
|
||||
"ass" -> MimeTypes.TEXT_SSA
|
||||
"srt" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"vtt" -> MimeTypes.TEXT_VTT
|
||||
"ttml" -> MimeTypes.APPLICATION_TTML
|
||||
"dfxp" -> MimeTypes.APPLICATION_TTML
|
||||
"stl" -> MimeTypes.APPLICATION_TTML
|
||||
"sbv" -> MimeTypes.APPLICATION_SUBRIP
|
||||
else -> MimeTypes.TEXT_UNKNOWN
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import com.nomadics9.ananas.models.VideoQuality
|
|||
import com.nomadics9.ananas.mpv.MPVPlayer
|
||||
import com.nomadics9.ananas.player.video.R
|
||||
import com.nomadics9.ananas.repository.JellyfinRepository
|
||||
import com.nomadics9.ananas.setSubtitlesMimeTypes
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -57,12 +58,11 @@ class PlayerActivityViewModel
|
|||
private val application: Application,
|
||||
private val jellyfinRepository: JellyfinRepository,
|
||||
private val appPreferences: AppPreferences,
|
||||
private val jellyfinApi: JellyfinApi,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(),
|
||||
Player.Listener {
|
||||
val player: Player
|
||||
private var originalHeight: Int = 0
|
||||
private var originalResolution: Int? = null
|
||||
|
||||
private val _uiState =
|
||||
MutableStateFlow(
|
||||
|
@ -191,6 +191,21 @@ class PlayerActivityViewModel
|
|||
).setSubtitleConfigurations(mediaSubtitles)
|
||||
.build()
|
||||
mediaItems.add(mediaItem)
|
||||
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(state: Int) {
|
||||
if (state == Player.STATE_READY) {
|
||||
val videoSize = player.videoSize
|
||||
val initialHeight = videoSize.height
|
||||
val initialWidth = videoSize.width
|
||||
|
||||
originalResolution = initialHeight * initialWidth
|
||||
Timber.d("Initial video size: $initialWidth x $initialHeight")
|
||||
|
||||
player.removeListener(this)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
|
@ -538,110 +553,76 @@ class PlayerActivityViewModel
|
|||
val currentPosition = player.currentPosition
|
||||
|
||||
viewModelScope.launch {
|
||||
val videoQuality = VideoQuality.fromString(quality)!!
|
||||
try {
|
||||
val deviceProfile =
|
||||
jellyfinRepository.buildDeviceProfile(
|
||||
VideoQuality.getBitrate(videoQuality),
|
||||
"ts",
|
||||
EncodingContext.STREAMING,
|
||||
)
|
||||
|
||||
val playbackInfo =
|
||||
jellyfinRepository.getPostedPlaybackInfo(
|
||||
currentItem.itemId,
|
||||
true,
|
||||
deviceProfile,
|
||||
VideoQuality.getBitrate(videoQuality),
|
||||
)
|
||||
val videoQuality = VideoQuality.fromString(quality)!!
|
||||
val deviceProfile = jellyfinRepository.buildDeviceProfile(VideoQuality.getBitrate(videoQuality), "mkv", EncodingContext.STREAMING)
|
||||
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,VideoQuality.getBitrate(videoQuality))
|
||||
val playSessionId = playbackInfo.content.playSessionId
|
||||
if (playSessionId != null) {
|
||||
jellyfinRepository.stopEncodingProcess(playSessionId)
|
||||
}
|
||||
val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true)
|
||||
|
||||
val externalSubtitles =
|
||||
currentItem.externalSubtitles.map { externalSubtitle ->
|
||||
MediaItem.SubtitleConfiguration
|
||||
.Builder(externalSubtitle.uri)
|
||||
// TODO: can maybe tidy the sub stuff up
|
||||
val externalSubtitles = currentItem.externalSubtitles.map { externalSubtitle ->
|
||||
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
|
||||
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
|
||||
.setLanguage(externalSubtitle.language.ifBlank { "Unknown" })
|
||||
.setMimeType(externalSubtitle.mimeType)
|
||||
.build()
|
||||
}
|
||||
|
||||
val embeddedSubtitles =
|
||||
mediaSources[currentMediaItemIndex]
|
||||
.mediaStreams
|
||||
val embeddedSubtitles = mediaSources[currentMediaItemIndex].mediaStreams
|
||||
.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null }
|
||||
.map { mediaStream ->
|
||||
val test = mediaStream.codec
|
||||
Timber.d("Deliver: %s", test)
|
||||
var deliveryUrl = mediaStream.path
|
||||
Timber.d("Deliverurl: %s", deliveryUrl)
|
||||
// Not sure if still needed
|
||||
if (mediaStream.codec == "webvtt") {
|
||||
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")
|
||||
}
|
||||
MediaItem.SubtitleConfiguration
|
||||
.Builder(Uri.parse(deliveryUrl))
|
||||
.setMimeType(
|
||||
when (mediaStream.codec) {
|
||||
"subrip" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"webvtt" -> MimeTypes.TEXT_VTT
|
||||
"ssa" -> MimeTypes.TEXT_SSA
|
||||
"pgs" -> MimeTypes.APPLICATION_PGS
|
||||
"ass" -> MimeTypes.TEXT_SSA // ASS is a subtitle format that is essentially an extension of SSA
|
||||
"srt" -> MimeTypes.APPLICATION_SUBRIP // SRT is another common name for SubRip
|
||||
"vtt" -> MimeTypes.TEXT_VTT // VTT is a common extension for WebVTT
|
||||
"ttml" -> MimeTypes.APPLICATION_TTML // TTML (Timed Text Markup Language)
|
||||
"dfxp" -> MimeTypes.APPLICATION_TTML // DFXP is a profile of TTML
|
||||
"stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format)
|
||||
"sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip
|
||||
else -> MimeTypes.TEXT_UNKNOWN
|
||||
},
|
||||
).setLanguage(mediaStream.language.ifBlank { "Unknown" })
|
||||
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")}
|
||||
MediaItem.SubtitleConfiguration.Builder(Uri.parse(deliveryUrl))
|
||||
.setMimeType(setSubtitlesMimeTypes(mediaStream.codec))
|
||||
.setLanguage(mediaStream.language.ifBlank { "Unknown" })
|
||||
.setLabel("Embedded")
|
||||
.build()
|
||||
}.toMutableList()
|
||||
}
|
||||
.toMutableList()
|
||||
|
||||
val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) }
|
||||
|
||||
val url =
|
||||
if (VideoQuality.getQualityString(videoQuality) == "Original") {
|
||||
val allSubtitles =
|
||||
if (VideoQuality.getIsOriginalQuality(videoQuality)) {
|
||||
externalSubtitles
|
||||
}else {
|
||||
embeddedSubtitles.apply { addAll(externalSubtitles) }
|
||||
}
|
||||
|
||||
val url = if (VideoQuality.getIsOriginalQuality(videoQuality)){
|
||||
jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId)
|
||||
} else {
|
||||
val mediaSourceId = mediaSources[currentMediaItemIndex].id
|
||||
val deviceId = jellyfinApi.api.deviceInfo.id
|
||||
Timber.d("deviceid = %s", deviceId)
|
||||
val url =
|
||||
jellyfinRepository.getTranscodedVideoStream(
|
||||
currentItem.itemId,
|
||||
deviceId,
|
||||
mediaSourceId,
|
||||
playSessionId!!,
|
||||
VideoQuality.getBitrate(videoQuality),
|
||||
)
|
||||
val deviceId = jellyfinRepository.getDeviceId()
|
||||
val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, VideoQuality.getBitrate(videoQuality))
|
||||
val uriBuilder = url.toUri().buildUpon()
|
||||
val apiKey = jellyfinApi.api.accessToken
|
||||
val apiKey = jellyfinRepository.getAccessToken()
|
||||
uriBuilder.appendQueryParameter("api_key",apiKey )
|
||||
val newUri = uriBuilder.build()
|
||||
newUri.toString()
|
||||
}
|
||||
|
||||
|
||||
|
||||
Timber.e("URI IS %s", url)
|
||||
val mediaItemBuilder =
|
||||
MediaItem
|
||||
.Builder()
|
||||
val mediaItemBuilder = MediaItem.Builder()
|
||||
.setMediaId(currentItem.itemId.toString())
|
||||
.setUri(url)
|
||||
.setSubtitleConfigurations(allSubtitles)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata
|
||||
.Builder()
|
||||
MediaMetadata.Builder()
|
||||
.setTitle(currentItem.name)
|
||||
.build(),
|
||||
)
|
||||
|
||||
|
||||
player.pause()
|
||||
player.setMediaItem(mediaItemBuilder.build())
|
||||
player.prepare()
|
||||
|
@ -649,15 +630,8 @@ class PlayerActivityViewModel
|
|||
playWhenReady = true
|
||||
player.play()
|
||||
|
||||
val originalHeight =
|
||||
mediaSources[currentMediaItemIndex]
|
||||
.mediaStreams
|
||||
.filter { it.type == MediaStreamType.VIDEO }
|
||||
.map { mediaStream -> mediaStream.height }
|
||||
.first() ?: 1080
|
||||
|
||||
// Store the original height
|
||||
this@PlayerActivityViewModel.originalHeight = originalHeight
|
||||
|
||||
|
||||
//isQualityChangeInProgress = true
|
||||
} catch (e: Exception) {
|
||||
|
@ -666,8 +640,11 @@ class PlayerActivityViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun getOriginalHeight(): Int = originalHeight
|
||||
fun getOriginalResolution(): Int? {
|
||||
return originalResolution
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sealed interface PlayerEvents {
|
||||
data object NavigateBack : PlayerEvents
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.nomadics9.ananas.models.PlayerChapter
|
|||
import com.nomadics9.ananas.models.PlayerItem
|
||||
import com.nomadics9.ananas.models.TrickplayInfo
|
||||
import com.nomadics9.ananas.repository.JellyfinRepository
|
||||
import com.nomadics9.ananas.setSubtitlesMimeTypes
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -136,6 +137,7 @@ class PlayerViewModel @Inject internal constructor(
|
|||
} else {
|
||||
mediaSources[mediaSourceIndex]
|
||||
}
|
||||
// Embedded Sub externally for offline playback
|
||||
val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) {
|
||||
mediaSource.mediaStreams
|
||||
.filter { mediaStream ->
|
||||
|
@ -146,13 +148,7 @@ class PlayerViewModel @Inject internal constructor(
|
|||
mediaStream.title,
|
||||
mediaStream.language,
|
||||
Uri.parse(mediaStream.path!!),
|
||||
when (mediaStream.codec) {
|
||||
"subrip" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"webvtt" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"pgs" -> MimeTypes.APPLICATION_PGS
|
||||
"ass" -> MimeTypes.TEXT_SSA
|
||||
else -> MimeTypes.TEXT_UNKNOWN
|
||||
},
|
||||
setSubtitlesMimeTypes(mediaStream.codec),
|
||||
)
|
||||
}
|
||||
}else {
|
||||
|
@ -165,13 +161,7 @@ class PlayerViewModel @Inject internal constructor(
|
|||
mediaStream.title,
|
||||
mediaStream.language,
|
||||
Uri.parse(mediaStream.path!!),
|
||||
when (mediaStream.codec) {
|
||||
"subrip" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"webvtt" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"pgs" -> MimeTypes.APPLICATION_PGS
|
||||
"ass" -> MimeTypes.TEXT_SSA
|
||||
else -> MimeTypes.TEXT_UNKNOWN
|
||||
},
|
||||
setSubtitlesMimeTypes(mediaStream.codec)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,6 +121,11 @@ constructor(
|
|||
Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT.toString(),
|
||||
)!!.toLongOrNull() ?: Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT
|
||||
|
||||
val transcodeCodec get() = sharedPreferences.getString(
|
||||
Constants.PREF_NETWORK_CODEC,
|
||||
Constants.NETWORK_DEFAULT_CODEC,
|
||||
)
|
||||
|
||||
// Cache
|
||||
val imageCache get() = sharedPreferences.getBoolean(
|
||||
Constants.PREF_IMAGE_CACHE,
|
||||
|
|
|
@ -43,6 +43,7 @@ object Constants {
|
|||
const val PREF_NETWORK_REQUEST_TIMEOUT = "pref_network_request_timeout"
|
||||
const val PREF_NETWORK_CONNECT_TIMEOUT = "pref_network_connect_timeout"
|
||||
const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout"
|
||||
const val PREF_NETWORK_CODEC = "pref_network_codec"
|
||||
const val PREF_DOWNLOADS_MOBILE_DATA = "pref_downloads_mobile_data"
|
||||
const val PREF_DOWNLOADS_ROAMING = "pref_downloads_roaming"
|
||||
const val PREF_DOWNLOADS_QUALITY = "pref_downloads_quality"
|
||||
|
@ -63,6 +64,7 @@ object Constants {
|
|||
const val NETWORK_DEFAULT_REQUEST_TIMEOUT = 30_000L
|
||||
const val NETWORK_DEFAULT_CONNECT_TIMEOUT = 6_000L
|
||||
const val NETWORK_DEFAULT_SOCKET_TIMEOUT = 10_000L
|
||||
const val NETWORK_DEFAULT_CODEC = "h264"
|
||||
|
||||
// sorting
|
||||
// This values must correspond to a SortString from [SortBy]
|
||||
|
|
1
version
Normal file
1
version
Normal file
|
@ -0,0 +1 @@
|
|||
0.10.6
|
Loading…
Reference in a new issue