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/gradle.xml
/.idea/deploymentTargetDropDown.xml
/.idea/misc.xml
.DS_Store
/build
/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
@ -21,12 +21,19 @@ Home | Library | Movie | Season | Episode
- Completely native interface
- Supported media items: movies, series, seasons, episodes
- Direct play only, (no transcoding)
- Video codes: H.263, H.264, H.265, VP8, VP9, AV1
- Support depends on Android device
- 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 provided by ExoPlayer FFmpeg extension
- Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB
- SSA/ASS has limited styling support see [this issue](https://github.com/google/ExoPlayer/issues/8435)
- ExoPlayer
- Video codes: H.263, H.264, H.265, VP8, VP9, AV1
- Support depends on Android device
- 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 provided by ExoPlayer FFmpeg extension
- 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
- 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.
# 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
# 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
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import android.widget.ImageButton
import android.widget.TextView
import androidx.activity.viewModels
import androidx.core.view.updatePadding
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 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 timber.log.Timber
@AndroidEntryPoint
class PlayerActivity : AppCompatActivity() {
private lateinit var binding: ActivityPlayerBinding
private val viewModel: PlayerActivityViewModel by viewModels()
private val args: PlayerActivityArgs by navArgs()
private lateinit var playerView: StyledPlayerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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)
playerView = findViewById(R.id.video_view)
binding.playerView.player = viewModel.player
viewModel.player.observe(this, {
playerView.player = it
val playerControls = binding.playerView.findViewById<View>(R.id.player_controls)
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, {
@ -37,21 +149,19 @@ class PlayerActivity : AppCompatActivity() {
}
})
if (viewModel.player.value == null) {
viewModel.initializePlayer(args.items)
}
viewModel.initializePlayer(args.items)
hideSystemUI()
}
override fun onPause() {
super.onPause()
viewModel.playWhenReady = viewModel.player.value?.playWhenReady == true
playerView.player?.playWhenReady = false
viewModel.playWhenReady = viewModel.player.playWhenReady == true
viewModel.player.playWhenReady = false
}
override fun onResume() {
super.onResume()
viewModel.player.value?.playWhenReady = viewModel.playWhenReady
viewModel.player.playWhenReady = viewModel.playWhenReady
hideSystemUI()
}
@ -63,6 +173,23 @@ class PlayerActivity : AppCompatActivity() {
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?

View file

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

View file

@ -6,6 +6,7 @@ import java.util.*
@Parcelize
data class PlayerItem(
val name: String?,
val itemId: UUID,
val mediaSourceId: String,
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)
for (intro in intros) {
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
}
}
@ -102,6 +102,7 @@ constructor(
if (episode.locationType == LocationType.VIRTUAL) continue
playerItems.add(
PlayerItem(
episode.name,
episode.id,
episode.mediaSources?.get(0)?.id!!,
playbackPosition

View file

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

View file

@ -12,6 +12,8 @@ import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel
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 kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -23,38 +25,70 @@ import javax.inject.Inject
class PlayerActivityViewModel
@Inject
constructor(
private val application: Application,
application: Application,
private val jellyfinRepository: JellyfinRepository
) : ViewModel(), Player.Listener {
private var _player = MutableLiveData<SimpleExoPlayer>()
var player: LiveData<SimpleExoPlayer> = _player
val player: BasePlayer
private val _navigateBack = MutableLiveData<Boolean>()
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
private var currentWindow = 0
private var playbackPosition: Long = 0
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(
items: Array<PlayerItem>
) {
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()
this.items = items
player.addListener(this)
viewModelScope.launch {
@ -76,16 +110,15 @@ constructor(
}
player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition)
player.playWhenReady = playWhenReady
player.prepare()
_player.value = player
player.play()
}
pollPosition(player)
}
private fun releasePlayer() {
_player.value?.let { player ->
player.let { player ->
runBlocking {
try {
jellyfinRepository.postPlaybackStop(
@ -98,17 +131,14 @@ constructor(
}
}
if (player.value != null) {
playWhenReady = player.value!!.playWhenReady
playbackPosition = player.value!!.currentPosition
currentWindow = player.value!!.currentWindowIndex
player.value!!.removeListener(this)
player.value!!.release()
_player.value = null
}
playWhenReady = player.playWhenReady
playbackPosition = player.currentPosition
currentWindow = player.currentWindowIndex
player.removeListener(this)
player.release()
}
private fun pollPosition(player: SimpleExoPlayer) {
private fun pollPosition(player: BasePlayer) {
val handler = Handler(Looper.getMainLooper())
val runnable = object : Runnable {
override fun run() {
@ -135,6 +165,11 @@ constructor(
Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
viewModelScope.launch {
try {
for (item in items) {
if (item.itemId.toString() == player.currentMediaItem?.mediaId ?: "") {
_currentItemTitle.value = item.name
}
}
jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId))
} catch (e: Exception) {
Timber.e(e)
@ -153,6 +188,23 @@ constructor(
}
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 -> {
stateString = "ExoPlayer.STATE_ENDED -"
@ -167,4 +219,10 @@ constructor(
Timber.d("Clearing Player ViewModel")
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"?>
<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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PlayerActivity">
<com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/video_view"
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
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="red">#EB5757</color>
<color name="yellow">#F2C94C</color>
<color name="player_background">#AA000000</color>
</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">
<item>null</item>
<item>ab</item>
<item>aa</item>
<item>af</item>
<item>ak</item>
<item>sq</item>
<item>am</item>
<item>ar</item>
<item>an</item>
<item>hy</item>
<item>as</item>
<item>av</item>
<item>ae</item>
<item>ay</item>
<item>az</item>
<item>bm</item>
<item>ba</item>
<item>eu</item>
<item>be</item>
<item>bn</item>
<item>bh</item>
<item>bi</item>
<item>nb</item>
<item>bs</item>
<item>br</item>
<item>bg</item>
<item>my</item>
<item>ca</item>
<item>km</item>
<item>ch</item>
<item>ce</item>
<item>ny</item>
<item>zh</item>
<item>cu</item>
<item>cv</item>
<item>kw</item>
<item>co</item>
<item>cr</item>
<item>hr</item>
<item>cs</item>
<item>da</item>
<item>dv</item>
<item>nl</item>
<item>dz</item>
<item>en</item>
<item>eo</item>
<item>et</item>
<item>ee</item>
<item>fo</item>
<item>fj</item>
<item>fi</item>
<item>fr</item>
<item>ff</item>
<item>gd</item>
<item>gl</item>
<item>lg</item>
<item>ka</item>
<item>de</item>
<item>el</item>
<item>gn</item>
<item>gu</item>
<item>ht</item>
<item>ha</item>
<item>he</item>
<item>hz</item>
<item>hi</item>
<item>ho</item>
<item>hu</item>
<item>is</item>
<item>io</item>
<item>ig</item>
<item>id</item>
<item>ia</item>
<item>ie</item>
<item>iu</item>
<item>ik</item>
<item>ga</item>
<item>it</item>
<item>ja</item>
<item>jv</item>
<item>kl</item>
<item>kn</item>
<item>kr</item>
<item>ks</item>
<item>kk</item>
<item>ki</item>
<item>rw</item>
<item>ky</item>
<item>kv</item>
<item>kg</item>
<item>ko</item>
<item>kj</item>
<item>ku</item>
<item>lo</item>
<item>la</item>
<item>lv</item>
<item>li</item>
<item>ln</item>
<item>lt</item>
<item>lu</item>
<item>lb</item>
<item>mk</item>
<item>mg</item>
<item>ms</item>
<item>ml</item>
<item>mt</item>
<item>gv</item>
<item>mi</item>
<item>mr</item>
<item>mh</item>
<item>mn</item>
<item>na</item>
<item>nv</item>
<item>nd</item>
<item>nr</item>
<item>ng</item>
<item>ne</item>
<item>se</item>
<item>no</item>
<item>nn</item>
<item>oc</item>
<item>oj</item>
<item>or</item>
<item>om</item>
<item>os</item>
<item>pi</item>
<item>pa</item>
<item>fa</item>
<item>pl</item>
<item>pt</item>
<item>ps</item>
<item>qu</item>
<item>ro</item>
<item>rm</item>
<item>rn</item>
<item>ru</item>
<item>sm</item>
<item>sg</item>
<item>sa</item>
<item>sc</item>
<item>sr</item>
<item>sn</item>
<item>ii</item>
<item>sd</item>
<item>si</item>
<item>sk</item>
<item>sl</item>
<item>so</item>
<item>st</item>
<item>es</item>
<item>su</item>
<item>sw</item>
<item>ss</item>
<item>sv</item>
<item>tl</item>
<item>ty</item>
<item>tg</item>
<item>ta</item>
<item>tt</item>
<item>te</item>
<item>th</item>
<item>bo</item>
<item>ti</item>
<item>to</item>
<item>ts</item>
<item>tn</item>
<item>tr</item>
<item>tk</item>
<item>tw</item>
<item>ug</item>
<item>uk</item>
<item>ur</item>
<item>uz</item>
<item>ve</item>
<item>vi</item>
<item>vo</item>
<item>wa</item>
<item>cy</item>
<item>fy</item>
<item>wo</item>
<item>xh</item>
<item>yi</item>
<item>yo</item>
<item>za</item>
<item>zu</item>
<item>abk</item>
<item>aar</item>
<item>afr</item>
<item>aka</item>
<item>sqi</item>
<item>amh</item>
<item>ara</item>
<item>arg</item>
<item>hye</item>
<item>asm</item>
<item>ava</item>
<item>ave</item>
<item>aym</item>
<item>aze</item>
<item>bam</item>
<item>bak</item>
<item>eus</item>
<item>bel</item>
<item>ben</item>
<item>bih</item>
<item>bis</item>
<item>nob</item>
<item>bos</item>
<item>bre</item>
<item>bul</item>
<item>bur</item>
<item>cat</item>
<item>khm</item>
<item>cha</item>
<item>che</item>
<item>nya</item>
<item>chi</item>
<item>chu</item>
<item>chv</item>
<item>cor</item>
<item>cos</item>
<item>cre</item>
<item>hrv</item>
<item>cze</item>
<item>dan</item>
<item>div</item>
<item>dut</item>
<item>dzo</item>
<item>eng</item>
<item>epo</item>
<item>est</item>
<item>ewe</item>
<item>fao</item>
<item>fij</item>
<item>fin</item>
<item>fre</item>
<item>ful</item>
<item>gla</item>
<item>glg</item>
<item>lug</item>
<item>geo</item>
<item>ger</item>
<item>gre</item>
<item>grn</item>
<item>guj</item>
<item>hat</item>
<item>hau</item>
<item>heb</item>
<item>her</item>
<item>hin</item>
<item>hmo</item>
<item>hun</item>
<item>ice</item>
<item>ido</item>
<item>ibo</item>
<item>ind</item>
<item>ina</item>
<item>ile</item>
<item>iku</item>
<item>ipk</item>
<item>gle</item>
<item>ita</item>
<item>jpn</item>
<item>jav</item>
<item>kal</item>
<item>kan</item>
<item>kau</item>
<item>kas</item>
<item>kaz</item>
<item>kik</item>
<item>kin</item>
<item>kir</item>
<item>kom</item>
<item>kon</item>
<item>kor</item>
<item>kua</item>
<item>kur</item>
<item>lao</item>
<item>lat</item>
<item>lav</item>
<item>lim</item>
<item>lin</item>
<item>lit</item>
<item>lub</item>
<item>ltz</item>
<item>mac</item>
<item>mlg</item>
<item>may</item>
<item>mal</item>
<item>mlt</item>
<item>glv</item>
<item>mao</item>
<item>mar</item>
<item>mah</item>
<item>mon</item>
<item>nau</item>
<item>nav</item>
<item>nde</item>
<item>nbl</item>
<item>ndo</item>
<item>nep</item>
<item>sme</item>
<item>nor</item>
<item>nno</item>
<item>oci</item>
<item>oji</item>
<item>ori</item>
<item>orm</item>
<item>oss</item>
<item>pli</item>
<item>pan</item>
<item>per</item>
<item>pol</item>
<item>por</item>
<item>pus</item>
<item>que</item>
<item>rum</item>
<item>roh</item>
<item>run</item>
<item>rus</item>
<item>smo</item>
<item>sag</item>
<item>san</item>
<item>srd</item>
<item>srp</item>
<item>sna</item>
<item>iii</item>
<item>snd</item>
<item>sin</item>
<item>slo</item>
<item>slv</item>
<item>som</item>
<item>sot</item>
<item>spa</item>
<item>sun</item>
<item>swa</item>
<item>ssw</item>
<item>swe</item>
<item>tgl</item>
<item>tah</item>
<item>tgk</item>
<item>tam</item>
<item>tat</item>
<item>tel</item>
<item>tha</item>
<item>tib</item>
<item>tir</item>
<item>ton</item>
<item>tso</item>
<item>tsn</item>
<item>tur</item>
<item>tuk</item>
<item>twi</item>
<item>uig</item>
<item>ukr</item>
<item>urd</item>
<item>uzb</item>
<item>ven</item>
<item>vie</item>
<item>vol</item>
<item>wln</item>
<item>wel</item>
<item>fry</item>
<item>wol</item>
<item>xho</item>
<item>yid</item>
<item>yor</item>
<item>zha</item>
<item>zul</item>
</string-array>
</resources>

View file

@ -56,4 +56,10 @@
<string name="privacy_policy">Privacy policy</string>
<string name="app_info">App info</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>

View file

@ -39,6 +39,18 @@
app:useSimpleSummaryProvider="true" />
</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">
<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")