Merge pull request #34 from jarnedemeulemeester/develop

Version 0.2.0
This commit is contained in:
Jarne Demeulemeester 2021-09-20 11:24:40 +02:00 committed by GitHub
commit ac54e40555
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 2778 additions and 462 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@
/.idea/discord.xml /.idea/discord.xml
/.idea/gradle.xml /.idea/gradle.xml
/.idea/deploymentTargetDropDown.xml /.idea/deploymentTargetDropDown.xml
/.idea/misc.xml
.DS_Store .DS_Store
/build /build
/captures /captures

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="app/src/main/res/layout/fragment_home.xml" value="0.1736111111111111" />
<entry key="app/src/main/res/layout/fragment_library.xml" value="0.1736111111111111" />
<entry key="app/src/main/res/layout/fragment_season.xml" value="0.20471014492753623" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View file

@ -1,4 +1,4 @@
![Findroid banner](images/banner.svg) ![Findroid banner](images/findroid-banner.png)
# Findroid # Findroid
@ -21,12 +21,19 @@ Home | Library | Movie | Season | Episode
- Completely native interface - Completely native interface
- Supported media items: movies, series, seasons, episodes - Supported media items: movies, series, seasons, episodes
- Direct play only, (no transcoding) - Direct play only, (no transcoding)
- Video codes: H.263, H.264, H.265, VP8, VP9, AV1 - ExoPlayer
- Support depends on Android device - Video codes: H.263, H.264, H.265, VP8, VP9, AV1
- Audio codes: Vorbis, Opus, FLAC, ALAC, PCM µ-law, PCM A-law, MP1, MP2, MP3, AMR-NB, AMR-WB, AAC, AC-3, E-AC-3, DTS, DTS-HD, TrueHD - Support depends on Android device
- Support provided by ExoPlayer FFmpeg extension - Audio codes: Vorbis, Opus, FLAC, ALAC, PCM µ-law, PCM A-law, MP1, MP2, MP3, AMR-NB, AMR-WB, AAC, AC-3, E-AC-3, DTS, DTS-HD, TrueHD
- Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB - Support provided by ExoPlayer FFmpeg extension
- SSA/ASS has limited styling support see [this issue](https://github.com/google/ExoPlayer/issues/8435) - Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB
- SSA/ASS has limited styling support see [this issue](https://github.com/google/ExoPlayer/issues/8435)
- **NEW** MPV Player
- Should play everything, including SSA/ASS subs with proper styling!
- Optionally force software decoding when hardware decoding has issues.
- Issues:
- Can only play one item at a time, doesn't transistion to the next episode
## Planned features ## Planned features
- Websocket connection (Syncplay) - Websocket connection (Syncplay)

View file

@ -1,113 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'kotlin-kapt'
id 'androidx.navigation.safeargs.kotlin'
id 'dagger.hilt.android.plugin'
id "com.mikepenz.aboutlibraries.plugin"
}
android {
compileSdkVersion 31
buildToolsVersion "31.0.0"
defaultConfig {
applicationId "dev.jdtech.jellyfin"
minSdkVersion 24
targetSdkVersion 31
versionCode 3
versionName "0.1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
dataBinding true
viewBinding true
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
implementation 'androidx.appcompat:appcompat:1.3.1'
// Material
implementation 'com.google.android.material:material:1.4.0'
// ConstraintLayout
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
// Navigation
def navigation_version = "2.3.5"
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
// RecyclerView
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
// Room
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// Preference
def preference_version = "1.1.1"
implementation "androidx.preference:preference-ktx:$preference_version"
// Jellyfin
def jellyfin_version = "1.0.2"
implementation "org.jellyfin.sdk:jellyfin-platform-android:$jellyfin_version"
// Glide
def glide_version = "4.12.0"
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
// Hilt
def hilt_version = "2.38.1"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
// ExoPlayer
def exoplayer_version = "2.15.0"
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
implementation files('libs/extension-ffmpeg-release.aar')
// Timber
def timber_version = "5.0.1"
implementation "com.jakewharton.timber:timber:$timber_version"
def about_libraries_version = "8.9.1"
implementation "com.mikepenz:aboutlibraries-core:$about_libraries_version"
implementation "com.mikepenz:aboutlibraries:$about_libraries_version"
// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}

123
app/build.gradle.kts Normal file
View file

@ -0,0 +1,123 @@
plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-parcelize")
id("kotlin-kapt")
id("androidx.navigation.safeargs.kotlin")
id("dagger.hilt.android.plugin")
id("com.mikepenz.aboutlibraries.plugin")
}
android {
compileSdk = 31
buildToolsVersion = "31.0.0"
defaultConfig {
applicationId = "dev.jdtech.jellyfin"
minSdk = 24
targetSdk = 31
versionCode = 4
versionName = "0.2.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("debug") {
applicationIdSuffix = ".debug"
}
create("staging") {
initWith(getByName("release"))
applicationIdSuffix = ".staging"
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
dataBinding = true
viewBinding = true
}
}
dependencies {
implementation("androidx.core:core-ktx:1.6.0")
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
implementation("androidx.appcompat:appcompat:1.3.1")
// Material
implementation("com.google.android.material:material:1.4.0")
// ConstraintLayout
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
// Navigation
val navigationVersion = "2.3.5"
implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion")
// RecyclerView
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.recyclerview:recyclerview-selection:1.1.0")
// Room
val roomVersion = "2.3.0"
implementation("androidx.room:room-runtime:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
// Preference
val preferenceVersion = "1.1.1"
implementation("androidx.preference:preference-ktx:$preferenceVersion")
// Jellyfin
val jellyfinVersion = "1.0.3"
implementation("org.jellyfin.sdk:jellyfin-platform-android:$jellyfinVersion")
// Glide
val glideVersion = "4.12.0"
implementation("com.github.bumptech.glide:glide:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
// Hilt
val hiltVersion = "2.38.1"
implementation("com.google.dagger:hilt-android:$hiltVersion")
kapt("com.google.dagger:hilt-compiler:$hiltVersion")
// ExoPlayer
val exoplayerVersion = "2.15.0"
implementation("com.google.android.exoplayer:exoplayer-core:$exoplayerVersion")
implementation("com.google.android.exoplayer:exoplayer-ui:$exoplayerVersion")
implementation(files("libs/extension-ffmpeg-release.aar"))
// MPV
implementation(files("libs/libmpv.aar"))
// Timber
val timberVersion = "5.0.1"
implementation("com.jakewharton.timber:timber:$timberVersion")
val aboutLibrariesVersion = "8.9.1"
implementation("com.mikepenz:aboutlibraries-core:$aboutLibrariesVersion")
implementation("com.mikepenz:aboutlibraries:$aboutLibrariesVersion")
// Testing
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
}

BIN
app/libs/libmpv.aar Executable file

Binary file not shown.

View file

@ -1,6 +1,6 @@
# Add project specific ProGuard rules here. # Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the # You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle. # proguardFiles setting in build.gradle.kts.
# #
# For more details, see # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html

11
app/src/main/assets/mpv.conf Executable file
View file

@ -0,0 +1,11 @@
### hwdec: try to use hardware decoding
# hwdec=mediacodec-copy
# hwdec-codecs="h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1"
### tls: allow self signed certificate
# tls-verify=no
# tls-ca-file=""
### sub: scale subtitles with video
# sub-scale-with-window=no
# sub-use-margins=no

Binary file not shown.

View file

@ -1,34 +1,146 @@
package dev.jdtech.jellyfin package dev.jdtech.jellyfin
import android.os.Build
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageButton
import android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.updatePadding
import androidx.navigation.navArgs import androidx.navigation.navArgs
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.trackselection.MappingTrackSelector
import com.google.android.exoplayer2.ui.TrackSelectionDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
import dev.jdtech.jellyfin.mpv.MPVPlayer
import dev.jdtech.jellyfin.mpv.TrackType
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
import timber.log.Timber import timber.log.Timber
@AndroidEntryPoint @AndroidEntryPoint
class PlayerActivity : AppCompatActivity() { class PlayerActivity : AppCompatActivity() {
private lateinit var binding: ActivityPlayerBinding
private val viewModel: PlayerActivityViewModel by viewModels() private val viewModel: PlayerActivityViewModel by viewModels()
private val args: PlayerActivityArgs by navArgs() private val args: PlayerActivityArgs by navArgs()
private lateinit var playerView: StyledPlayerView
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Timber.d("Creating player activity") Timber.d("Creating player activity")
setContentView(R.layout.activity_player) binding = ActivityPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
playerView = findViewById(R.id.video_view) binding.playerView.player = viewModel.player
viewModel.player.observe(this, { val playerControls = binding.playerView.findViewById<View>(R.id.player_controls)
playerView.player = it
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
binding.playerView.findViewById<View>(R.id.player_controls)
.setOnApplyWindowInsetsListener { _, windowInsets ->
val cutout = windowInsets.displayCutout
playerControls.updatePadding(
left = cutout?.safeInsetLeft ?: 0,
top = cutout?.safeInsetTop ?: 0,
right = cutout?.safeInsetRight ?: 0,
bottom = cutout?.safeInsetBottom ?: 0,
)
return@setOnApplyWindowInsetsListener windowInsets
}
}
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
onBackPressed()
}
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
viewModel.currentItemTitle.observe(this, { title ->
videoNameTextView.text = title
})
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
val subtitleButton = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle)
audioButton.isEnabled = false
audioButton.imageAlpha = 75
subtitleButton.isEnabled = false
subtitleButton.imageAlpha = 75
audioButton.setOnClickListener {
when (viewModel.player) {
is MPVPlayer -> {
TrackSelectionDialogFragment(TrackType.AUDIO, viewModel).show(
supportFragmentManager,
"trackselectiondialog"
)
}
is SimpleExoPlayer -> {
val mappedTrackInfo =
viewModel.trackSelector.currentMappedTrackInfo ?: return@setOnClickListener
var audioRenderer: Int? = null
for (i in 0 until mappedTrackInfo.rendererCount) {
if (isRendererType(mappedTrackInfo, i, C.TRACK_TYPE_AUDIO)) {
audioRenderer = i
}
}
if (audioRenderer == null) return@setOnClickListener
val trackSelectionDialogBuilder = TrackSelectionDialogBuilder(
this, resources.getString(R.string.select_audio_track),
viewModel.trackSelector, audioRenderer
)
val trackSelectionDialog = trackSelectionDialogBuilder.build()
trackSelectionDialog.show()
}
}
}
subtitleButton.setOnClickListener {
when (viewModel.player) {
is MPVPlayer -> {
TrackSelectionDialogFragment(TrackType.SUBTITLE, viewModel).show(
supportFragmentManager,
"trackselectiondialog"
)
}
is SimpleExoPlayer -> {
val mappedTrackInfo =
viewModel.trackSelector.currentMappedTrackInfo ?: return@setOnClickListener
var subtitleRenderer: Int? = null
for (i in 0 until mappedTrackInfo.rendererCount) {
if (isRendererType(mappedTrackInfo, i, C.TRACK_TYPE_TEXT)) {
subtitleRenderer = i
}
}
if (subtitleRenderer == null) return@setOnClickListener
val trackSelectionDialogBuilder = TrackSelectionDialogBuilder(
this, resources.getString(R.string.select_subtile_track),
viewModel.trackSelector, subtitleRenderer
)
val trackSelectionDialog = trackSelectionDialogBuilder.build()
trackSelectionDialog.show()
}
}
}
viewModel.fileLoaded.observe(this, {
if (it) {
audioButton.isEnabled = true
audioButton.imageAlpha = 255
subtitleButton.isEnabled = true
subtitleButton.imageAlpha = 255
}
}) })
viewModel.navigateBack.observe(this, { viewModel.navigateBack.observe(this, {
@ -37,21 +149,19 @@ class PlayerActivity : AppCompatActivity() {
} }
}) })
if (viewModel.player.value == null) { viewModel.initializePlayer(args.items)
viewModel.initializePlayer(args.items)
}
hideSystemUI() hideSystemUI()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
viewModel.playWhenReady = viewModel.player.value?.playWhenReady == true viewModel.playWhenReady = viewModel.player.playWhenReady == true
playerView.player?.playWhenReady = false viewModel.player.playWhenReady = false
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.player.value?.playWhenReady = viewModel.playWhenReady viewModel.player.playWhenReady = viewModel.playWhenReady
hideSystemUI() hideSystemUI()
} }
@ -63,6 +173,23 @@ class PlayerActivity : AppCompatActivity() {
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
private fun isRendererType(
mappedTrackInfo: MappingTrackSelector.MappedTrackInfo,
rendererIndex: Int,
type: Int
): Boolean {
val trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex)
if (trackGroupArray.length == 0) {
return false
}
val trackType = mappedTrackInfo.getRendererType(rendererIndex)
return type == trackType
} }
} }

View file

@ -0,0 +1,73 @@
package dev.jdtech.jellyfin.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import dev.jdtech.jellyfin.mpv.TrackType
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
import java.lang.IllegalStateException
class TrackSelectionDialogFragment(
private val type: String,
private val viewModel: PlayerActivityViewModel
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val trackNames: List<String>
when (type) {
TrackType.AUDIO -> {
trackNames = viewModel.currentAudioTracks.map { track ->
if (track.title.isEmpty()) {
"${track.lang} - ${track.codec}"
} else {
"${track.title} - ${track.lang} - ${track.codec}"
}
}
return activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
builder.setTitle("Select audio track")
.setSingleChoiceItems(
trackNames.toTypedArray(),
viewModel.currentAudioTracks.indexOfFirst { it.selected }) { _, which ->
viewModel.switchToTrack(
TrackType.AUDIO,
viewModel.currentAudioTracks[which]
)
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
TrackType.SUBTITLE -> {
trackNames = viewModel.currentSubtitleTracks.map { track ->
if (track.title.isEmpty()) {
"${track.lang} - ${track.codec}"
} else {
"${track.title} - ${track.lang} - ${track.codec}"
}
}
return activity?.let { activity ->
val builder = AlertDialog.Builder(activity)
builder.setTitle("Select subtitle track")
.setSingleChoiceItems(
trackNames.toTypedArray(),
viewModel.currentSubtitleTracks.indexOfFirst { it.selected }) { _, which ->
viewModel.switchToTrack(
TrackType.SUBTITLE,
viewModel.currentSubtitleTracks[which]
)
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
else -> {
trackNames = listOf()
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setTitle("Select ? track")
.setMessage("Unknown track type")
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
}
}

View file

@ -10,10 +10,6 @@ import dev.jdtech.jellyfin.R
class InitializingFragment : Fragment() { class InitializingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?

View file

@ -75,7 +75,7 @@ class MediaInfoFragment : Fragment() {
} else { } else {
binding.originalTitle.visibility = View.GONE binding.originalTitle.visibility = View.GONE
} }
if (item.trailerCount != null && item.trailerCount!! < 1) { if (item.remoteTrailers.isNullOrEmpty()) {
binding.trailerButton.visibility = View.GONE binding.trailerButton.visibility = View.GONE
} }
binding.communityRating.visibility = when (item.communityRating != null) { binding.communityRating.visibility = when (item.communityRating != null) {
@ -147,6 +147,7 @@ class MediaInfoFragment : Fragment() {
} }
binding.trailerButton.setOnClickListener { binding.trailerButton.setOnClickListener {
if (viewModel.item.value?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener
val intent = Intent( val intent = Intent(
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
Uri.parse(viewModel.item.value?.remoteTrailers?.get(0)?.url) Uri.parse(viewModel.item.value?.remoteTrailers?.get(0)?.url)

View file

@ -6,6 +6,7 @@ import java.util.*
@Parcelize @Parcelize
data class PlayerItem( data class PlayerItem(
val name: String?,
val itemId: UUID, val itemId: UUID,
val mediaSourceId: String, val mediaSourceId: String,
val playbackPosition: Long val playbackPosition: Long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
package dev.jdtech.jellyfin.mpv;
import androidx.annotation.StringDef;
@StringDef({
TrackType.VIDEO,
TrackType.AUDIO,
TrackType.SUBTITLE,
})
public @interface TrackType {
String VIDEO = "video";
String AUDIO = "audio";
String SUBTITLE = "sub";
}

View file

@ -86,7 +86,7 @@ constructor(
val intros = jellyfinRepository.getIntros(startEpisode.id) val intros = jellyfinRepository.getIntros(startEpisode.id)
for (intro in intros) { for (intro in intros) {
if (intro.mediaSources.isNullOrEmpty()) continue if (intro.mediaSources.isNullOrEmpty()) continue
playerItems.add(PlayerItem(intro.id, intro.mediaSources?.get(0)?.id!!, 0)) playerItems.add(PlayerItem(intro.name, intro.id, intro.mediaSources?.get(0)?.id!!, 0))
introsCount += 1 introsCount += 1
} }
} }
@ -102,6 +102,7 @@ constructor(
if (episode.locationType == LocationType.VIRTUAL) continue if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add( playerItems.add(
PlayerItem( PlayerItem(
episode.name,
episode.id, episode.id,
episode.mediaSources?.get(0)?.id!!, episode.mediaSources?.get(0)?.id!!,
playbackPosition playbackPosition

View file

@ -202,7 +202,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
val intros = jellyfinRepository.getIntros(series.id) val intros = jellyfinRepository.getIntros(series.id)
for (intro in intros) { for (intro in intros) {
if (intro.mediaSources.isNullOrEmpty()) continue if (intro.mediaSources.isNullOrEmpty()) continue
playerItems.add(PlayerItem(intro.id, intro.mediaSources?.get(0)?.id!!, 0)) playerItems.add(PlayerItem(intro.name, intro.id, intro.mediaSources?.get(0)?.id!!, 0))
introsCount += 1 introsCount += 1
} }
} }
@ -211,6 +211,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
"Movie" -> { "Movie" -> {
playerItems.add( playerItems.add(
PlayerItem( PlayerItem(
series.name,
series.id, series.id,
series.mediaSources?.get(mediaSourceIndex ?: 0)?.id!!, series.mediaSources?.get(mediaSourceIndex ?: 0)?.id!!,
playbackPosition playbackPosition
@ -231,6 +232,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
if (episode.locationType == LocationType.VIRTUAL) continue if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add( playerItems.add(
PlayerItem( PlayerItem(
episode.name,
episode.id, episode.id,
episode.mediaSources?.get(0)?.id!!, episode.mediaSources?.get(0)?.id!!,
0 0
@ -250,6 +252,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
if (episode.locationType == LocationType.VIRTUAL) continue if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add( playerItems.add(
PlayerItem( PlayerItem(
episode.name,
episode.id, episode.id,
episode.mediaSources?.get(0)?.id!!, episode.mediaSources?.get(0)?.id!!,
0 0

View file

@ -12,6 +12,8 @@ import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.mpv.MPVPlayer
import dev.jdtech.jellyfin.mpv.TrackType
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -23,38 +25,70 @@ import javax.inject.Inject
class PlayerActivityViewModel class PlayerActivityViewModel
@Inject @Inject
constructor( constructor(
private val application: Application, application: Application,
private val jellyfinRepository: JellyfinRepository private val jellyfinRepository: JellyfinRepository
) : ViewModel(), Player.Listener { ) : ViewModel(), Player.Listener {
private var _player = MutableLiveData<SimpleExoPlayer>() val player: BasePlayer
var player: LiveData<SimpleExoPlayer> = _player
private val _navigateBack = MutableLiveData<Boolean>() private val _navigateBack = MutableLiveData<Boolean>()
val navigateBack: LiveData<Boolean> = _navigateBack val navigateBack: LiveData<Boolean> = _navigateBack
private val _currentItemTitle = MutableLiveData<String>()
val currentItemTitle: LiveData<String> = _currentItemTitle
var currentAudioTracks: MutableList<MPVPlayer.Companion.Track> = mutableListOf()
var currentSubtitleTracks: MutableList<MPVPlayer.Companion.Track> = mutableListOf()
private val _fileLoaded = MutableLiveData(false)
val fileLoaded: LiveData<Boolean> = _fileLoaded
private var items: Array<PlayerItem> = arrayOf()
val trackSelector = DefaultTrackSelector(application)
var playWhenReady = true var playWhenReady = true
private var currentWindow = 0 private var currentWindow = 0
private var playbackPosition: Long = 0 private var playbackPosition: Long = 0
private val sp = PreferenceManager.getDefaultSharedPreferences(application) private val sp = PreferenceManager.getDefaultSharedPreferences(application)
init {
val useMpv = sp.getBoolean("mpv_player", false)
val preferredAudioLanguage = sp.getString("audio_language", null) ?: ""
val preferredSubtitleLanguage = sp.getString("subtitle_language", null) ?: ""
if (useMpv) {
val preferredLanguages = mapOf(
TrackType.AUDIO to preferredAudioLanguage,
TrackType.SUBTITLE to preferredSubtitleLanguage
)
player = MPVPlayer(
application,
false,
preferredLanguages,
sp.getBoolean("mpv_disable_hwdec", false)
)
} else {
val renderersFactory =
DefaultRenderersFactory(application).setExtensionRendererMode(
DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
)
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setTunnelingEnabled(true)
.setPreferredAudioLanguage(preferredAudioLanguage)
.setPreferredTextLanguage(preferredSubtitleLanguage)
)
player = SimpleExoPlayer.Builder(application, renderersFactory)
.setTrackSelector(trackSelector)
.build()
}
}
fun initializePlayer( fun initializePlayer(
items: Array<PlayerItem> items: Array<PlayerItem>
) { ) {
this.items = items
val renderersFactory =
DefaultRenderersFactory(application).setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
val trackSelector = DefaultTrackSelector(application)
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setTunnelingEnabled(true)
.setPreferredAudioLanguage(sp.getString("audio_language", null))
.setPreferredTextLanguage(sp.getString("subtitle_language", null))
)
val player = SimpleExoPlayer.Builder(application, renderersFactory)
.setTrackSelector(trackSelector)
.build()
player.addListener(this) player.addListener(this)
viewModelScope.launch { viewModelScope.launch {
@ -76,16 +110,15 @@ constructor(
} }
player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition) player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition)
player.playWhenReady = playWhenReady
player.prepare() player.prepare()
_player.value = player player.play()
} }
pollPosition(player) pollPosition(player)
} }
private fun releasePlayer() { private fun releasePlayer() {
_player.value?.let { player -> player.let { player ->
runBlocking { runBlocking {
try { try {
jellyfinRepository.postPlaybackStop( jellyfinRepository.postPlaybackStop(
@ -98,17 +131,14 @@ constructor(
} }
} }
if (player.value != null) { playWhenReady = player.playWhenReady
playWhenReady = player.value!!.playWhenReady playbackPosition = player.currentPosition
playbackPosition = player.value!!.currentPosition currentWindow = player.currentWindowIndex
currentWindow = player.value!!.currentWindowIndex player.removeListener(this)
player.value!!.removeListener(this) player.release()
player.value!!.release()
_player.value = null
}
} }
private fun pollPosition(player: SimpleExoPlayer) { private fun pollPosition(player: BasePlayer) {
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
val runnable = object : Runnable { val runnable = object : Runnable {
override fun run() { override fun run() {
@ -135,6 +165,11 @@ constructor(
Timber.d("Playing MediaItem: ${mediaItem?.mediaId}") Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
viewModelScope.launch { viewModelScope.launch {
try { try {
for (item in items) {
if (item.itemId.toString() == player.currentMediaItem?.mediaId ?: "") {
_currentItemTitle.value = item.name
}
}
jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId)) jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
@ -153,6 +188,23 @@ constructor(
} }
ExoPlayer.STATE_READY -> { ExoPlayer.STATE_READY -> {
stateString = "ExoPlayer.STATE_READY -" stateString = "ExoPlayer.STATE_READY -"
currentAudioTracks.clear()
currentSubtitleTracks.clear()
when (player) {
is MPVPlayer -> {
player.currentTracks.forEach {
when (it.type) {
TrackType.AUDIO -> {
currentAudioTracks.add(it)
}
TrackType.SUBTITLE -> {
currentSubtitleTracks.add(it)
}
}
}
}
}
_fileLoaded.value = true
} }
ExoPlayer.STATE_ENDED -> { ExoPlayer.STATE_ENDED -> {
stateString = "ExoPlayer.STATE_ENDED -" stateString = "ExoPlayer.STATE_ENDED -"
@ -167,4 +219,10 @@ constructor(
Timber.d("Clearing Player ViewModel") Timber.d("Clearing Player ViewModel")
releasePlayer() releasePlayer()
} }
fun switchToTrack(trackType: String, track: MPVPlayer.Companion.Track) {
if (player is MPVPlayer) {
player.selectTrack(trackType, isExternal = false, index = track.ffIndex)
}
}
} }

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,12L5,12"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M12,19l-7,-7l7,-7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM19,18L5,18L5,6h14v12zM7,15h3c0.55,0 1,-0.45 1,-1v-1L9.5,13v0.5h-2v-3h2v0.5L11,11v-1c0,-0.55 -0.45,-1 -1,-1L7,9c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1zM14,15h3c0.55,0 1,-0.45 1,-1v-1h-1.5v0.5h-2v-3h2v0.5L18,11v-1c0,-0.55 -0.45,-1 -1,-1h-3c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1z"
android:fillColor="@color/white"/>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13,19l9,-7l-9,-7l0,14z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M2,19l9,-7l-9,-7l0,14z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,4h4v16h-4z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M14,4h4v16h-4z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11,19l-9,-7l9,-7l0,14z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M22,19l-9,-7l9,-7l0,14z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,20l-10,-8l10,-8l0,16z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M5,19L5,5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5,4l10,8l-10,8l0,-16z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M19,5L19,19"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,2L18,2A2,2 0,0 1,20 4L20,20A2,2 0,0 1,18 22L6,22A2,2 0,0 1,4 20L4,4A2,2 0,0 1,6 2z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M12,14m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M12,6L12.01,6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/white">
<item android:id="@android:id/mask">
<shape android:shape="oval">
<solid android:color="@color/white"/>
</shape>
</item>
<item>
<shape>
<solid android:color="@android:color/transparent"/>
</shape>
</item>
</ripple>

View file

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".PlayerActivity"> tools:context=".PlayerActivity">
<com.google.android.exoplayer2.ui.StyledPlayerView <com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view" android:id="@+id/player_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black" android:background="@color/black"
app:show_subtitle_button="true" /> app:show_buffering="always" />
</merge> </FrameLayout>

View file

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_controls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/player_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@id/extra_buttons"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_arrow_left" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
<TextView
android:id="@+id/video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
android:textColor="@color/white"
tools:text="The Dawn of Despair" />
</LinearLayout>
<LinearLayout
android:id="@+id/extra_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/btn_audio_track"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_speaker" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
<ImageButton
android:id="@+id/btn_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_closed_caption" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageButton
android:id="@+id/exo_prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_skip_back" />
<ImageButton
android:id="@+id/exo_rew"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_rewind" />
<ImageButton
android:id="@+id/exo_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_background"
android:backgroundTint="@color/white"
android:padding="16dp"
android:src="@drawable/ic_play"
app:tint="@color/black" />
<ImageButton
android:id="@+id/exo_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_background"
android:backgroundTint="@color/white"
android:padding="16dp"
android:src="@drawable/ic_pause"
android:visibility="gone"
app:tint="@color/black" />
<ImageButton
android:id="@+id/exo_ffwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_fast_forward" />
<ImageButton
android:id="@+id/exo_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_skip_forward" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="8dp">
<TextView
android:id="@+id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="00:00" />
<Space
android:layout_width="8dp"
android:layout_height="0dp" />
<View
android:layout_width="4dp"
android:layout_height="1dp"
android:background="@color/white" />
<Space
android:layout_width="8dp"
android:layout_height="0dp" />
<TextView
android:id="@+id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="24:21" />
</LinearLayout>
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@+id/exo_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:played_color="@color/primary" />
</LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,59 @@
<resources>
<string name="app_name">Findroid</string>
<string name="app_description">Aplicación nativa de terceros para Jellyfin</string>
<string name="jellyfin_banner">Emblema Jellyfin</string>
<string name="add_server">Agregar servidor</string>
<string name="login">Acceso</string>
<string name="select_server">Seleccionar servidor</string>
<string name="edit_text_server_address_hint">Dirección del servidor</string>
<string name="edit_text_username_hint">Usuario</string>
<string name="edit_text_password_hint">Contraseña</string>
<string name="button_connect">Conectar</string>
<string name="button_login">Acceder</string>
<string name="server_icon">Icono del servidor</string>
<string name="remove_server">Quitar servidor</string>
<string name="remove_server_dialog_text">¿Está seguro de quitar el servidor %1$s</string>
<string name="remove">Quitar</string>
<string name="cancel">Cancelar</string>
<string name="title_home">Inicio</string>
<string name="title_media">Mis contenidos</string>
<string name="title_favorite">Favoritos</string>
<string name="title_settings">Configuración</string>
<string name="view_all">Ver todo</string>
<string name="error_loading_data">Error al cargar datos</string>
<string name="retry">Reintentar</string>
<string name="genres">Generos</string>
<string name="director">Director</string>
<string name="writers">Escritores</string>
<string name="cast_amp_crew">Reparto y equipo</string>
<string name="seasons">Temporadas</string>
<string name="play_button_description">Reproducir contenido</string>
<string name="trailer_button_description">Ver el trailer</string>
<string name="check_button_description">Marcar como visto o No visto</string>
<string name="favorite_button_description">Favorito</string>
<string name="episode_watched_indicator">Indicador de episodio visto</string>
<string name="episode_name">%1$d. %2$s</string>
<string name="episode_name_extended">S%1$d:E%2$d - %3$s</string>
<string name="next_up">Siguiente</string>
<string name="continue_watching">Continuar viendo</string>
<string name="series_poster">Poster serie</string>
<string name="no_favorites">No tienes favoritos</string>
<string name="search">Buscar</string>
<string name="no_search_results">Búsqueda sin resultados</string>
<string name="settings_category_language">Idioma</string>
<string name="settings_preferred_audio_language">Idioma de audio preferido</string>
<string name="settings_preferred_subtitle_language">Idioma de subtitulo preferido</string>
<string name="initializing">Iniciando…</string>
<string name="settings_category_servers">Servidores</string>
<string name="manage_servers">Administrar servidores</string>
<string name="settings_category_appearance">Apariencia</string>
<string name="theme">Tema</string>
<string name="error_preparing_player_items">Error preparando elementos.</string>
<string name="view_details">Ver detalles</string>
<string name="view_details_underlined"><u>Ver detalles</u></string>
<string name="about">Acerca</string>
<string name="privacy_policy">Política de privacidad</string>
<string name="app_info">Información de la App</string>
<string name="unknown_error">Error desconocido</string>
<string name="latest_library">Últimos %1$s</string>
</resources>

View file

@ -0,0 +1,59 @@
<resources>
<string name="app_name">Findroid</string>
<string name="app_description">Aplicación nativa de terceros para Jellyfin</string>
<string name="jellyfin_banner">Emblema Jellyfin</string>
<string name="add_server">Agregar servidor</string>
<string name="login">Acceso</string>
<string name="select_server">Seleccionar servidor</string>
<string name="edit_text_server_address_hint">Dirección del servidor</string>
<string name="edit_text_username_hint">Usuario</string>
<string name="edit_text_password_hint">Contraseña</string>
<string name="button_connect">Conectar</string>
<string name="button_login">Acceder</string>
<string name="server_icon">Icono del servidor</string>
<string name="remove_server">Quitar servidor</string>
<string name="remove_server_dialog_text">¿Está seguro de quitar el servidor %1$s</string>
<string name="remove">Quitar</string>
<string name="cancel">Cancelar</string>
<string name="title_home">Inicio</string>
<string name="title_media">Mis contenidos</string>
<string name="title_favorite">Favoritos</string>
<string name="title_settings">Configuración</string>
<string name="view_all">Ver todo</string>
<string name="error_loading_data">Error al cargar datos</string>
<string name="retry">Reintentar</string>
<string name="genres">Generos</string>
<string name="director">Director</string>
<string name="writers">Escritores</string>
<string name="cast_amp_crew">Reparto y equipo</string>
<string name="seasons">Temporadas</string>
<string name="play_button_description">Reproducir contenido</string>
<string name="trailer_button_description">Ver el trailer</string>
<string name="check_button_description">Marcar cono visto o No visto</string>
<string name="favorite_button_description">Favorito</string>
<string name="episode_watched_indicator">Indicador de episodio visto</string>
<string name="episode_name">%1$d. %2$s</string>
<string name="episode_name_extended">S%1$d:E%2$d - %3$s</string>
<string name="next_up">Siguiente</string>
<string name="continue_watching">Continar viendo</string>
<string name="latest_library">Últimos %1$s</string>
<string name="series_poster">Poster serie</string>
<string name="no_favorites">No tienes favoritos</string>
<string name="search">Buscar</string>
<string name="no_search_results">Búsqueda sin resultados</string>
<string name="settings_category_language">Idioma</string>
<string name="settings_preferred_audio_language">Idioma de audio preferido</string>
<string name="settings_preferred_subtitle_language">Idioma de subtitulo preferido</string>
<string name="initializing">Iniciando…</string>
<string name="settings_category_servers">Servidores</string>
<string name="manage_servers">Administrar servidores</string>
<string name="settings_category_appearance">Apariencia</string>
<string name="theme">Tema</string>
<string name="error_preparing_player_items">Error preparando elementos.</string>
<string name="view_details">Ver detalles</string>
<string name="view_details_underlined"><u>Ver detalles</u></string>
<string name="about">Acerca</string>
<string name="privacy_policy">Política de privacidad</string>
<string name="app_info">Información de la App</string>
<string name="unknown_error">Error desconocido</string>
</resources>

View file

@ -0,0 +1,59 @@
<resources>
<string name="app_name">Findroid</string>
<string name="app_description">Aplicación nativa de terceros para Jellyfin</string>
<string name="jellyfin_banner">Emblema Jellyfin</string>
<string name="add_server">Agregar servidor</string>
<string name="login">Acceso</string>
<string name="select_server">Seleccionar servidor</string>
<string name="edit_text_server_address_hint">Dirección del servidor</string>
<string name="edit_text_username_hint">Usuario</string>
<string name="edit_text_password_hint">Contraseña</string>
<string name="button_connect">Conectar</string>
<string name="button_login">Acceder</string>
<string name="server_icon">Icono del servidor</string>
<string name="remove_server">Quitar servidor</string>
<string name="remove_server_dialog_text">¿Está seguro de quitar el servidor %1$s</string>
<string name="remove">Quitar</string>
<string name="cancel">Cancelar</string>
<string name="title_home">Inicio</string>
<string name="title_media">Mis medios</string>
<string name="title_favorite">Favoritos</string>
<string name="title_settings">Ajustes</string>
<string name="view_all">Ver todo</string>
<string name="error_loading_data">Error al cargar datos</string>
<string name="retry">Reintentar</string>
<string name="genres">Géneros</string>
<string name="director">Director</string>
<string name="writers">Escritores</string>
<string name="cast_amp_crew">Reparto y equipo</string>
<string name="seasons">Temporadas</string>
<string name="play_button_description">Reproducir contenido</string>
<string name="trailer_button_description">Ver el adelanto</string>
<string name="check_button_description">Marcar como visto o No visto</string>
<string name="favorite_button_description">Favorito</string>
<string name="episode_watched_indicator">Indicador de episodio visto</string>
<string name="episode_name">%1$d. %2$s</string>
<string name="episode_name_extended">S%1$d:E%2$d - %3$s</string>
<string name="next_up">Siguiente</string>
<string name="continue_watching">Continar viendo</string>
<string name="latest_library">Últimos %1$s</string>
<string name="series_poster">Poster serie</string>
<string name="no_favorites">No tienes favoritos</string>
<string name="search">Buscar</string>
<string name="no_search_results">Búsqueda sin resultados</string>
<string name="settings_category_language">Idioma</string>
<string name="settings_preferred_audio_language">Idioma de audio preferido</string>
<string name="settings_preferred_subtitle_language">Idioma de subtitulo preferido</string>
<string name="initializing">Iniciando…</string>
<string name="settings_category_servers">Servidores</string>
<string name="manage_servers">Administrar servidores</string>
<string name="settings_category_appearance">Apariencia</string>
<string name="theme">Tema</string>
<string name="error_preparing_player_items">Error preparando elementos.</string>
<string name="view_details">Ver detalles</string>
<string name="view_details_underlined"><u>Ver detalles</u></string>
<string name="about">Acerca</string>
<string name="privacy_policy">Política de privacidad</string>
<string name="app_info">Información de aplicación</string>
<string name="unknown_error">Error desconocido</string>
</resources>

View file

@ -15,4 +15,5 @@
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="red">#EB5757</color> <color name="red">#EB5757</color>
<color name="yellow">#F2C94C</color> <color name="yellow">#F2C94C</color>
<color name="player_background">#AA000000</color>
</resources> </resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<drawable name="exo_styled_controls_play">@drawable/ic_play</drawable>
<drawable name="exo_styled_controls_pause">@drawable/ic_pause</drawable>
</resources>

View file

@ -190,189 +190,189 @@
<string-array name="languages_values"> <string-array name="languages_values">
<item>null</item> <item>null</item>
<item>ab</item> <item>abk</item>
<item>aa</item> <item>aar</item>
<item>af</item> <item>afr</item>
<item>ak</item> <item>aka</item>
<item>sq</item> <item>sqi</item>
<item>am</item> <item>amh</item>
<item>ar</item> <item>ara</item>
<item>an</item> <item>arg</item>
<item>hy</item> <item>hye</item>
<item>as</item> <item>asm</item>
<item>av</item> <item>ava</item>
<item>ae</item> <item>ave</item>
<item>ay</item> <item>aym</item>
<item>az</item> <item>aze</item>
<item>bm</item> <item>bam</item>
<item>ba</item> <item>bak</item>
<item>eu</item> <item>eus</item>
<item>be</item> <item>bel</item>
<item>bn</item> <item>ben</item>
<item>bh</item> <item>bih</item>
<item>bi</item> <item>bis</item>
<item>nb</item> <item>nob</item>
<item>bs</item> <item>bos</item>
<item>br</item> <item>bre</item>
<item>bg</item> <item>bul</item>
<item>my</item> <item>bur</item>
<item>ca</item> <item>cat</item>
<item>km</item> <item>khm</item>
<item>ch</item> <item>cha</item>
<item>ce</item> <item>che</item>
<item>ny</item> <item>nya</item>
<item>zh</item> <item>chi</item>
<item>cu</item> <item>chu</item>
<item>cv</item> <item>chv</item>
<item>kw</item> <item>cor</item>
<item>co</item> <item>cos</item>
<item>cr</item> <item>cre</item>
<item>hr</item> <item>hrv</item>
<item>cs</item> <item>cze</item>
<item>da</item> <item>dan</item>
<item>dv</item> <item>div</item>
<item>nl</item> <item>dut</item>
<item>dz</item> <item>dzo</item>
<item>en</item> <item>eng</item>
<item>eo</item> <item>epo</item>
<item>et</item> <item>est</item>
<item>ee</item> <item>ewe</item>
<item>fo</item> <item>fao</item>
<item>fj</item> <item>fij</item>
<item>fi</item> <item>fin</item>
<item>fr</item> <item>fre</item>
<item>ff</item> <item>ful</item>
<item>gd</item> <item>gla</item>
<item>gl</item> <item>glg</item>
<item>lg</item> <item>lug</item>
<item>ka</item> <item>geo</item>
<item>de</item> <item>ger</item>
<item>el</item> <item>gre</item>
<item>gn</item> <item>grn</item>
<item>gu</item> <item>guj</item>
<item>ht</item> <item>hat</item>
<item>ha</item> <item>hau</item>
<item>he</item> <item>heb</item>
<item>hz</item> <item>her</item>
<item>hi</item> <item>hin</item>
<item>ho</item> <item>hmo</item>
<item>hu</item> <item>hun</item>
<item>is</item> <item>ice</item>
<item>io</item> <item>ido</item>
<item>ig</item> <item>ibo</item>
<item>id</item> <item>ind</item>
<item>ia</item> <item>ina</item>
<item>ie</item> <item>ile</item>
<item>iu</item> <item>iku</item>
<item>ik</item> <item>ipk</item>
<item>ga</item> <item>gle</item>
<item>it</item> <item>ita</item>
<item>ja</item> <item>jpn</item>
<item>jv</item> <item>jav</item>
<item>kl</item> <item>kal</item>
<item>kn</item> <item>kan</item>
<item>kr</item> <item>kau</item>
<item>ks</item> <item>kas</item>
<item>kk</item> <item>kaz</item>
<item>ki</item> <item>kik</item>
<item>rw</item> <item>kin</item>
<item>ky</item> <item>kir</item>
<item>kv</item> <item>kom</item>
<item>kg</item> <item>kon</item>
<item>ko</item> <item>kor</item>
<item>kj</item> <item>kua</item>
<item>ku</item> <item>kur</item>
<item>lo</item> <item>lao</item>
<item>la</item> <item>lat</item>
<item>lv</item> <item>lav</item>
<item>li</item> <item>lim</item>
<item>ln</item> <item>lin</item>
<item>lt</item> <item>lit</item>
<item>lu</item> <item>lub</item>
<item>lb</item> <item>ltz</item>
<item>mk</item> <item>mac</item>
<item>mg</item> <item>mlg</item>
<item>ms</item> <item>may</item>
<item>ml</item> <item>mal</item>
<item>mt</item> <item>mlt</item>
<item>gv</item> <item>glv</item>
<item>mi</item> <item>mao</item>
<item>mr</item> <item>mar</item>
<item>mh</item> <item>mah</item>
<item>mn</item> <item>mon</item>
<item>na</item> <item>nau</item>
<item>nv</item> <item>nav</item>
<item>nd</item> <item>nde</item>
<item>nr</item> <item>nbl</item>
<item>ng</item> <item>ndo</item>
<item>ne</item> <item>nep</item>
<item>se</item> <item>sme</item>
<item>no</item> <item>nor</item>
<item>nn</item> <item>nno</item>
<item>oc</item> <item>oci</item>
<item>oj</item> <item>oji</item>
<item>or</item> <item>ori</item>
<item>om</item> <item>orm</item>
<item>os</item> <item>oss</item>
<item>pi</item> <item>pli</item>
<item>pa</item> <item>pan</item>
<item>fa</item> <item>per</item>
<item>pl</item> <item>pol</item>
<item>pt</item> <item>por</item>
<item>ps</item> <item>pus</item>
<item>qu</item> <item>que</item>
<item>ro</item> <item>rum</item>
<item>rm</item> <item>roh</item>
<item>rn</item> <item>run</item>
<item>ru</item> <item>rus</item>
<item>sm</item> <item>smo</item>
<item>sg</item> <item>sag</item>
<item>sa</item> <item>san</item>
<item>sc</item> <item>srd</item>
<item>sr</item> <item>srp</item>
<item>sn</item> <item>sna</item>
<item>ii</item> <item>iii</item>
<item>sd</item> <item>snd</item>
<item>si</item> <item>sin</item>
<item>sk</item> <item>slo</item>
<item>sl</item> <item>slv</item>
<item>so</item> <item>som</item>
<item>st</item> <item>sot</item>
<item>es</item> <item>spa</item>
<item>su</item> <item>sun</item>
<item>sw</item> <item>swa</item>
<item>ss</item> <item>ssw</item>
<item>sv</item> <item>swe</item>
<item>tl</item> <item>tgl</item>
<item>ty</item> <item>tah</item>
<item>tg</item> <item>tgk</item>
<item>ta</item> <item>tam</item>
<item>tt</item> <item>tat</item>
<item>te</item> <item>tel</item>
<item>th</item> <item>tha</item>
<item>bo</item> <item>tib</item>
<item>ti</item> <item>tir</item>
<item>to</item> <item>ton</item>
<item>ts</item> <item>tso</item>
<item>tn</item> <item>tsn</item>
<item>tr</item> <item>tur</item>
<item>tk</item> <item>tuk</item>
<item>tw</item> <item>twi</item>
<item>ug</item> <item>uig</item>
<item>uk</item> <item>ukr</item>
<item>ur</item> <item>urd</item>
<item>uz</item> <item>uzb</item>
<item>ve</item> <item>ven</item>
<item>vi</item> <item>vie</item>
<item>vo</item> <item>vol</item>
<item>wa</item> <item>wln</item>
<item>cy</item> <item>wel</item>
<item>fy</item> <item>fry</item>
<item>wo</item> <item>wol</item>
<item>xh</item> <item>xho</item>
<item>yi</item> <item>yid</item>
<item>yo</item> <item>yor</item>
<item>za</item> <item>zha</item>
<item>zu</item> <item>zul</item>
</string-array> </string-array>
</resources> </resources>

View file

@ -56,4 +56,10 @@
<string name="privacy_policy">Privacy policy</string> <string name="privacy_policy">Privacy policy</string>
<string name="app_info">App info</string> <string name="app_info">App info</string>
<string name="unknown_error">Unknown error</string> <string name="unknown_error">Unknown error</string>
<string name="select_audio_track">Select audio track</string>
<string name="select_subtile_track">Select subtitle track</string>
<string name="mpv_player">MPV Player</string>
<string name="mpv_player_summary">Use the experimental MPV Player to play videos. MPV has support for more video, audio and subtitle codecs.</string>
<string name="force_software_decoding">Force software decoding</string>
<string name="force_software_decoding_summary">Disable hardware decoding and use software decoding. Can be useful if hardware decoding gives weird artifacts.</string>
</resources> </resources>

View file

@ -39,6 +39,18 @@
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="Player">
<SwitchPreference
app:key="mpv_player"
app:title="@string/mpv_player"
app:summary="@string/mpv_player_summary"/>
<SwitchPreference
app:key="mpv_disable_hwdec"
app:dependency="mpv_player"
app:title="@string/force_software_decoding"
app:summary="@string/force_software_decoding_summary"/>
</PreferenceCategory>
<PreferenceCategory app:title="@string/about"> <PreferenceCategory app:title="@string/about">
<Preference <Preference

View file

@ -1,37 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.5.30"
repositories {
google()
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
def nav_version = "2.3.5"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
def hilt_version = "2.38.1"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
def about_libraries_version = "8.9.1"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

37
build.gradle.kts Normal file
View file

@ -0,0 +1,37 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
val kotlinVersion = "1.5.30"
repositories {
google()
mavenCentral()
maven {
url = uri("https://plugins.gradle.org/m2/")
}
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle.kts files
val navVersion = "2.3.5"
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion")
val hiltVersion = "2.38.1"
classpath("com.google.dagger:hilt-android-gradle-plugin:$hiltVersion")
val aboutLibrariesVersion = "8.9.1"
classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$aboutLibrariesVersion")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.create<Delete>("clean") {
delete(rootProject.buildDir)
}

View file

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 1536 512" xmlns="http://www.w3.org/2000/svg">
<style>
.text {
fill: #000;
}
@media (prefers-color-scheme: dark) {
.text {
fill: #fff;
}
}
</style>
<defs>
<linearGradient id="linear-gradient-6" x1="110.25" x2="496.14" y1="213.3" y2="436.09" gradientTransform="matrix(.6218 0 0 .6218 100.51 103.53)" gradientUnits="userSpaceOnUse">
<stop stop-color="#3ddc84" offset="0"/>
<stop stop-color="#00A4DC" offset="1"/>
</linearGradient>
</defs>
<title>banner-dark</title>
<g id="banner-dark">
<path id="logo" d="m307.94 275.03 15.785-27.34c2.192-3.7916-3.4954-7.0797-5.6875-3.288l-15.985 27.686c-12.223-5.5788-25.952-8.6852-40.6-8.6852-14.648 0-28.375 3.1109-40.598 8.6852l-15.983-27.686c-2.1884-3.7952-7.8773-0.51167-5.6889 3.2836l15.787 27.345c-27.107 14.743-45.647 42.185-48.359 74.607h189.69c-2.715-32.422-21.254-59.863-48.359-74.607m-46.458-252.33c-61.813 0-260.7 360.64-230.39 421.55 30.305 60.91 430.79 60.208 460.79 0 30.004-60.208-168.58-421.55-230.39-421.55zm151.02 368.77c-19.668 39.436-282.07 39.938-301.94 0-19.869-39.938 110.48-276.25 150.92-276.25s170.69 236.72 151.02 276.25z" fill="url(#linear-gradient-6)" stroke-width=".14885"/>
<g class="text" style="font-variation-settings:'wght' 400" aria-label="Findroid">
<path d="m569.69 357.11q-3.7333 0-6.1333-2.4-2.1333-2.4-2.1333-5.6v-170.67q0-3.2 2.4-5.6t5.6-2.4h97.067q3.4667 0 5.6 2.4 2.4 2.1333 2.4 5.6 0 3.2-2.4 5.6-2.1333 2.1333-5.6 2.1333h-89.867l1.0667-1.6v70.933l-1.3333-2.4h78.133q3.4667 0 5.6 2.4 2.4 2.1333 2.4 5.3333 0 3.4667-2.4 5.6-2.1333 2.1333-5.6 2.1333h-78.667l1.8667-2.1333v82.667q0 3.2-2.4 5.6-2.1333 2.4-5.6 2.4z"/>
<path d="m723.83 349.11q0 3.2-2.4 5.6t-5.6 2.4q-3.4667 0-5.8667-2.4-2.1333-2.4-2.1333-5.6v-122.67q0-3.2 2.1333-5.6 2.4-2.4 5.8667-2.4t5.6 2.4q2.4 2.4 2.4 5.6zm-8-148.53q-5.6 0-8.5333-2.4-2.6667-2.6667-2.6667-7.4667v-2.6667q0-4.8 2.9333-7.2 3.2-2.6667 8.5333-2.6667 5.0667 0 7.7333 2.6667 2.9333 2.4 2.9333 7.2v2.6667q0 4.8-2.9333 7.4667-2.6667 2.4-8 2.4z"/>
<path d="m829.69 217.65q17.6 0 28 7.2 10.667 6.9333 15.2 19.2 4.8 12 4.8 26.667v78.4q0 3.2-2.4 5.6t-5.6 2.4q-3.7333 0-5.8667-2.4t-2.1333-5.6v-77.6q0-10.667-3.4667-19.467t-11.467-14.133q-7.7333-5.3333-20.533-5.3333-11.467 0-21.867 5.3333-10.133 5.3333-16.533 14.133-6.4 8.8-6.4 19.467v77.6q0 3.2-2.4 5.6-2.4 2.4-5.6 2.4-3.7333 0-5.8667-2.4-2.1333-2.4-2.1333-5.6v-119.47q0-3.2 2.1333-5.6 2.4-2.4 5.8667-2.4t5.6 2.4q2.4 2.4 2.4 5.6v22.4l-6.1333 9.6q0.53333-8.5333 5.3333-16.267 5.0667-8 12.8-14.133 7.7334-6.4 17.067-9.8667 9.6-3.7333 19.2-3.7333z"/>
<path d="m1030.2 159.78q3.4667 0 5.6 2.4 2.4 2.1333 2.4 5.6v181.33q0 3.2-2.4 5.6t-5.6 2.4q-3.7333 0-5.8666-2.4-2.1334-2.4-2.1334-5.6v-31.733l4.5334-3.7333q0 7.4667-4 15.733-4 8-11.467 14.933-7.2 6.9333-17.067 11.2-9.6 4.2667-21.067 4.2667-17.6 0-32-9.3333-14.133-9.3334-22.4-25.333-8.2667-16-8.2667-36.533 0-20.267 8.2667-36.267 8.2667-16.267 22.4-25.333 14.133-9.3333 31.733-9.3333 11.2 0 21.067 4 9.8667 4 17.333 10.933 7.7333 6.9333 12 16 4.5333 8.8 4.5333 18.4l-5.6-4v-95.2q0-3.2 2.1334-5.6 2.1333-2.4 5.8666-2.4zm-55.467 185.07q14.133 0 25.067-7.2 10.933-7.4667 17.067-20 6.4-12.8 6.4-29.067 0-16-6.4-28.533-6.1334-12.8-17.067-20-10.933-7.4667-25.067-7.4667-13.867 0-25.067 7.4667-10.933 7.2-17.333 20-6.1334 12.533-6.1334 28.533t6.1334 28.8q6.4 12.8 17.333 20.267 11.2 7.2 25.067 7.2z"/>
<path d="m1088.1 357.11q-3.7333 0-5.8667-2.4-2.1333-2.4-2.1333-5.6v-119.47q0-3.2 2.1333-5.6 2.4-2.4 5.8667-2.4t5.6 2.4q2.4 2.4 2.4 5.6v40l-4 0.8q0.8-9.3333 4.5333-18.4 4-9.3334 10.667-17.067t15.733-12.533q9.3333-4.8 20.8-4.8 4.8 0 9.3333 2.1333 4.5334 1.8667 4.5334 6.4 0 4-2.1334 6.1333-2.1333 2.1333-5.0666 2.1333-2.4 0-5.3334-1.3333-2.6666-1.3333-7.2-1.3333-7.4667 0-14.933 4.5333-7.4667 4.2667-13.6 11.733-6.1334 7.4667-9.8667 16.8-3.4667 9.0667-3.4667 18.4v65.867q0 3.2-2.4 5.6t-5.6 2.4z"/>
<path d="m1302.8 288.85q0 20.267-9.0667 36.533-8.8 16-24 25.333-15.2 9.0667-34.4 9.0667-18.933 0-34.4-9.0667-15.2-9.3334-24.267-25.333-8.8-16.267-8.8-36.533 0-20.533 8.8-36.533 9.0667-16 24.267-25.333 15.467-9.3333 34.4-9.3333 19.2 0 34.4 9.3333 15.2 9.3334 24 25.333 9.0667 16 9.0667 36.533zm-16 0q0-16.267-6.6667-28.8-6.6666-12.8-18.4-20-11.467-7.4667-26.4-7.4667-14.667 0-26.4 7.4667-11.467 7.2-18.4 20-6.6667 12.533-6.6667 28.8 0 16.267 6.6667 28.8 6.9334 12.533 18.4 20 11.733 7.2 26.4 7.2 14.933 0 26.4-7.2 11.733-7.4667 18.4-20 6.6667-12.533 6.6667-28.8z"/>
<path d="m1351 349.11q0 3.2-2.4 5.6t-5.6 2.4q-3.4667 0-5.8667-2.4-2.1333-2.4-2.1333-5.6v-122.67q0-3.2 2.1333-5.6 2.4-2.4 5.8667-2.4 3.4666 0 5.6 2.4 2.4 2.4 2.4 5.6zm-8-148.53q-5.6 0-8.5334-2.4-2.6666-2.6667-2.6666-7.4667v-2.6667q0-4.8 2.9333-7.2 3.2-2.6667 8.5333-2.6667 5.0667 0 7.7334 2.6667 2.9333 2.4 2.9333 7.2v2.6667q0 4.8-2.9333 7.4667-2.6667 2.4-8 2.4z"/>
<path d="m1503.3 159.78q3.4667 0 5.6 2.4 2.4 2.1333 2.4 5.6v181.33q0 3.2-2.4 5.6t-5.6 2.4q-3.7333 0-5.8667-2.4-2.1333-2.4-2.1333-5.6v-31.733l4.5333-3.7333q0 7.4667-4 15.733-4 8-11.467 14.933-7.2 6.9333-17.067 11.2-9.6001 4.2667-21.067 4.2667-17.6 0-32-9.3333-14.133-9.3334-22.4-25.333-8.2667-16-8.2667-36.533 0-20.267 8.2667-36.267 8.2667-16.267 22.4-25.333 14.133-9.3333 31.733-9.3333 11.2 0 21.067 4 9.8667 4 17.333 10.933 7.7334 6.9333 12 16 4.5333 8.8 4.5333 18.4l-5.6-4v-95.2q0-3.2 2.1333-5.6 2.1334-2.4 5.8667-2.4zm-55.467 185.07q14.133 0 25.067-7.2 10.933-7.4667 17.067-20 6.4001-12.8 6.4001-29.067 0-16-6.4001-28.533-6.1333-12.8-17.067-20-10.933-7.4667-25.067-7.4667-13.867 0-25.067 7.4667-10.933 7.2-17.333 20-6.1333 12.533-6.1333 28.533t6.1333 28.8q6.4 12.8 17.333 20.267 11.2 7.2 25.067 7.2z"/>
</g>
<g class="text" style="font-variation-settings:'wght' 400" aria-label="For Jellyfin">
<path d="m1093.4 453.74q-1.1947 0-1.9627-0.768-0.6826-0.768-0.6826-1.792v-54.613q0-1.024 0.768-1.792t1.792-0.768h31.061q1.1093 0 1.792 0.768 0.768 0.68267 0.768 1.792 0 1.024-0.768 1.792-0.6827 0.68266-1.792 0.68266h-28.757l0.3413-0.512v22.699l-0.4267-0.768h25.003q1.1093 0 1.792 0.768 0.768 0.68267 0.768 1.7067 0 1.1093-0.768 1.792-0.6827 0.68266-1.792 0.68266h-25.173l0.5973-0.68266v26.453q0 1.024-0.768 1.792-0.6827 0.768-1.792 0.768z"/>
<path d="m1176.8 431.9q0 6.4853-2.9013 11.691-2.816 5.12-7.68 8.1067-4.864 2.9013-11.008 2.9013-6.0587 0-11.008-2.9013-4.864-2.9867-7.7653-8.1067-2.816-5.2053-2.816-11.691 0-6.5707 2.816-11.691 2.9013-5.12 7.7653-8.1067 4.9493-2.9867 11.008-2.9867 6.144 0 11.008 2.9867 4.864 2.9867 7.68 8.1067 2.9013 5.12 2.9013 11.691zm-5.12 0q0-5.2053-2.1333-9.216-2.1333-4.096-5.888-6.4-3.6693-2.3893-8.448-2.3893-4.6933 0-8.448 2.3893-3.6693 2.304-5.888 6.4-2.1333 4.0107-2.1333 9.216 0 5.2053 2.1333 9.216 2.2187 4.0107 5.888 6.4 3.7547 2.304 8.448 2.304 4.7787 0 8.448-2.304 3.7547-2.3893 5.888-6.4 2.1333-4.0107 2.1333-9.216z"/>
<path d="m1189.9 453.74q-1.1947 0-1.8774-0.768-0.6826-0.768-0.6826-1.792v-38.229q0-1.024 0.6826-1.792 0.768-0.768 1.8774-0.768 1.1093 0 1.792 0.768 0.768 0.768 0.768 1.792v12.8l-1.28 0.256q0.256-2.9867 1.4506-5.888 1.28-2.9867 3.4134-5.4613 2.1333-2.4747 5.0346-4.0107 2.9867-1.536 6.656-1.536 1.536 0 2.9867 0.68267 1.4507 0.59733 1.4507 2.048 0 1.28-0.6827 1.9627-0.6827 0.68267-1.6213 0.68267-0.768 0-1.7067-0.42667-0.8533-0.42666-2.304-0.42666-2.3893 0-4.7787 1.4507-2.3893 1.3653-4.352 3.7547-1.9626 2.3893-3.1573 5.376-1.1093 2.9013-1.1093 5.888v21.077q0 1.024-0.768 1.792t-1.792 0.768z"/>
<path d="m1258.9 454.59q-5.376 0-9.6427-2.9867-4.2667-2.9867-6.4853-7.8507-0.4267-0.768-0.4267-1.3653 0-1.1093 0.8533-1.7067 0.8534-0.68266 1.7067-0.68266t1.3653 0.42666q0.5974 0.42667 1.024 1.024 1.6214 3.584 4.6934 5.8027 3.072 2.2187 6.912 2.2187 3.9253 0 6.912-1.6213 2.9866-1.7067 4.608-4.6933 1.7066-2.9867 1.7066-6.8267v-39.765q0-1.024 0.768-1.792 0.8534-0.768 1.9627-0.768 1.1947 0 1.8773 0.768 0.768 0.768 0.768 1.792v39.765q0 5.2907-2.3893 9.472-2.3893 4.096-6.5707 6.4853-4.1813 2.304-9.6426 2.304z"/>
<path d="m1311.6 454.59q-6.5706 0-11.605-2.816t-7.8507-7.8507q-2.816-5.0347-2.816-11.776 0-7.2533 2.816-12.373 2.9014-5.12 7.424-7.8507 4.608-2.816 9.728-2.816 3.7547 0 7.2534 1.3653 3.584 1.28 6.3146 3.9253 2.7307 2.56 4.4374 6.3147 1.7066 3.7547 1.792 8.704 0 1.024-0.768 1.792-0.768 0.68266-1.792 0.68266h-34.219l-1.024-4.608h33.621l-1.1093 1.024v-1.7067q-0.4267-4.0107-2.6453-6.8267-2.2187-2.816-5.376-4.2667-3.072-1.4507-6.4854-1.4507-2.56 0-5.2906 1.024-2.6454 1.024-4.864 3.2427-2.1334 2.1333-3.4987 5.5467-1.3653 3.328-1.3653 7.936 0 5.0347 2.048 9.1307 2.048 4.096 5.888 6.4853 3.84 2.3893 9.3013 2.3893 2.9013 0 5.2907-0.85334 2.3893-0.85333 4.1813-2.2187 1.8773-1.4507 3.072-2.9867 0.9387-0.768 1.792-0.768 0.9387 0 1.536 0.68267 0.6827 0.68267 0.6827 1.536 0 1.024-0.8534 1.792-2.56 3.072-6.656 5.376-4.096 2.2187-8.96 2.2187z"/>
<path d="m1345.4 451.18q0 1.024-0.768 1.792t-1.792 0.768q-1.1094 0-1.8774-0.768-0.6826-0.768-0.6826-1.792v-58.027q0-1.024 0.768-1.792t1.792-0.768q1.1093 0 1.792 0.768 0.768 0.768 0.768 1.792z"/>
<path d="m1365 451.18q0 1.024-0.768 1.792t-1.792 0.768q-1.1093 0-1.8773-0.768-0.6827-0.768-0.6827-1.792v-58.027q0-1.024 0.768-1.792t1.792-0.768q1.1093 0 1.792 0.768 0.768 0.768 0.768 1.792z"/>
<path d="m1410.2 409.37q1.1093 0 1.792 0.768 0.768 0.768 0.768 1.792v37.632q0 6.912-2.7307 11.605-2.7307 4.7787-7.3387 7.168-4.608 2.4747-10.496 2.4747-3.6693 0-6.8266-0.85334-3.072-0.76799-5.0347-2.048-1.024-0.59734-1.536-1.4507t-0.085-1.792q0.4266-1.1947 1.28-1.6213 0.9386-0.34134 1.8773 0.0853 1.4507 0.768 4.1813 1.8773 2.7307 1.1093 6.2294 1.1093 4.6933 0 8.1066-1.9627 3.4987-1.9627 5.376-5.7173 1.8774-3.6693 1.8774-8.7893v-6.144l0.5973 2.048q-1.28 2.6453-3.6693 4.6933-2.304 2.048-5.376 3.2427-2.9867 1.1093-6.4 1.1093-5.12 0-8.5334-2.048-3.328-2.1333-4.9493-5.8027t-1.6213-8.6187v-26.197q0-1.024 0.6826-1.792 0.6827-0.768 1.8774-0.768 1.1093 0 1.792 0.768 0.768 0.768 0.768 1.792v25.429q0 5.9733 2.56 9.216 2.6453 3.2427 8.5333 3.2427 3.6693 0 6.7413-1.7067 3.072-1.792 5.0347-4.608 1.9627-2.9013 1.9627-6.144v-25.429q0-1.024 0.6826-1.792 0.768-0.768 1.8774-0.768z"/>
<path d="m1441.9 390.94q1.28 0 2.7306 0.256 1.536 0.256 2.6454 0.93867 1.1093 0.59733 1.1093 1.8773 0 0.93867-0.6827 1.7067-0.6826 0.68266-1.536 0.68266-0.8533 0-2.1333-0.42666-1.28-0.512-2.7307-0.512-1.792 0-3.072 0.85333-1.28 0.768-1.9626 2.304-0.6827 1.4507-0.6827 3.584v48.981q0 1.024-0.768 1.792-0.6827 0.768-1.792 0.768t-1.8773-0.768q-0.6827-0.768-0.6827-1.792v-48.981q0-5.4613 3.1574-8.3627 3.2426-2.9013 8.2773-2.9013zm3.2426 20.053q1.024 0 1.7067 0.68266 0.6827 0.68267 0.6827 1.7067t-0.6827 1.7067q-0.6827 0.68267-1.7067 0.68267h-20.907q-0.9387 0-1.7067-0.68267-0.6826-0.768-0.6826-1.7067 0-1.1093 0.6826-1.7067 0.768-0.68266 1.7067-0.68266zm16.043 40.192q0 1.024-0.768 1.792t-1.792 0.768q-1.1093 0-1.8773-0.768-0.6827-0.768-0.6827-1.792v-39.253q0-1.024 0.6827-1.792 0.768-0.768 1.8773-0.768t1.792 0.768q0.768 0.768 0.768 1.792zm-2.56-47.531q-1.792 0-2.7307-0.768-0.8533-0.85333-0.8533-2.3893v-0.85333q0-1.536 0.9387-2.304 1.024-0.85333 2.7306-0.85333 1.6214 0 2.4747 0.85333 0.9387 0.768 0.9387 2.304v0.85333q0 1.536-0.9387 2.3893-0.8533 0.768-2.56 0.768z"/>
<path d="m1495.1 409.11q5.632 0 8.96 2.304 3.4133 2.2187 4.864 6.144 1.5359 3.84 1.5359 8.5333v25.088q0 1.024-0.768 1.792-0.7679 0.768-1.7919 0.768-1.1947 0-1.8774-0.768-0.6826-0.768-0.6826-1.792v-24.832q0-3.4133-1.1094-6.2293-1.1093-2.816-3.6693-4.5227-2.4747-1.7067-6.5707-1.7067-3.6693 0-6.9973 1.7067-3.2427 1.7067-5.2907 4.5227t-2.048 6.2293v24.832q0 1.024-0.768 1.792t-1.792 0.768q-1.1946 0-1.8773-0.768t-0.6827-1.792v-38.229q0-1.024 0.6827-1.792 0.768-0.768 1.8773-0.768 1.1094 0 1.792 0.768 0.768 0.768 0.768 1.792v7.168l-1.9626 3.072q0.1706-2.7307 1.7066-5.2053 1.6214-2.56 4.096-4.5227 2.4747-2.048 5.4614-3.1573 3.072-1.1947 6.144-1.1947z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
images/findroid-banner.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -1,2 +0,0 @@
rootProject.name = "Jellyfin"
include ':app'

1
settings.gradle.kts Normal file
View file

@ -0,0 +1 @@
include(":app")