Compare commits
17 Commits
master
...
release-0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3da856ec23 | ||
|
|
8148850206 | ||
|
|
eb676a4ff5 | ||
|
|
7a478a77a4 | ||
|
|
9723da673e | ||
|
|
72e45a7b47 | ||
|
|
4e82f10f18 | ||
|
|
c1b103e385 | ||
|
|
0fc8daf45e | ||
|
|
bd98065766 | ||
|
|
4d68e3d31e | ||
|
|
9c33bb34ff | ||
|
|
9f7891f129 | ||
|
|
11e5b9526d | ||
|
|
a0fb27491c | ||
|
|
b1babfe477 | ||
|
|
ed3b92df08 |
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.jellyfin.androidtv">
|
||||
package="org.jellyfin.androidtv"
|
||||
android:installLocation="auto">
|
||||
|
||||
<!-- Android TV Integration -->
|
||||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
|
||||
@@ -88,6 +89,9 @@
|
||||
<meta-data
|
||||
android:name="org.jellyfin.androidtv.SessionInitializer"
|
||||
android:value="androidx.startup" />
|
||||
<meta-data
|
||||
android:name="org.jellyfin.androidtv.ProcessLifecycleInitializer"
|
||||
android:value="androidx.startup" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
|
||||
@@ -2,8 +2,6 @@ package org.jellyfin.androidtv
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.BackoffPolicy
|
||||
@@ -15,16 +13,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.acra.ACRA
|
||||
import org.jellyfin.androidtv.auth.repository.SessionRepository
|
||||
import org.jellyfin.androidtv.data.eventhandling.SocketHandler
|
||||
import org.jellyfin.androidtv.data.repository.NotificationsRepository
|
||||
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
|
||||
import org.jellyfin.androidtv.telemetry.TelemetryService
|
||||
import org.jellyfin.androidtv.util.AutoBitrate
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.getKoin
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("unused")
|
||||
@@ -37,28 +31,6 @@ class JellyfinApplication : Application() {
|
||||
|
||||
val notificationsRepository by inject<NotificationsRepository>()
|
||||
notificationsRepository.addDefaultNotifications()
|
||||
|
||||
// Register application lifecycle events
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
/**
|
||||
* Called by the Process Lifecycle when the app is created. It is called after [onCreate].
|
||||
*/
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
// Register activity lifecycle callbacks
|
||||
getKoin().getAll<ActivityLifecycleCallbacks>().forEach(::registerActivityLifecycleCallbacks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the Process Lifecycle when the app is activated in the foreground (activity opened).
|
||||
*/
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
Timber.i("Process lifecycle started")
|
||||
|
||||
owner.lifecycleScope.launch {
|
||||
get<SessionRepository>().restoreSession()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.jellyfin.androidtv
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.startup.AppInitializer
|
||||
import androidx.startup.Initializer
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.auth.repository.SessionRepository
|
||||
import org.jellyfin.androidtv.di.KoinInitializer
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("unused")
|
||||
class ProcessLifecycleInitializer : Initializer<Unit> {
|
||||
override fun create(context: Context) {
|
||||
val koin = AppInitializer.getInstance(context)
|
||||
.initializeComponent(KoinInitializer::class.java)
|
||||
.koin
|
||||
|
||||
// Register application lifecycle events
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
/**
|
||||
* Called by the Process Lifecycle when the app is created. It is called after [onCreate].
|
||||
*/
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
// Register activity lifecycle callbacks
|
||||
val callbacks = koin.getAll<Application.ActivityLifecycleCallbacks>()
|
||||
Timber.i("Registering ${callbacks.size} ActivityLifecycleCallbacks")
|
||||
val app = context.applicationContext as Application
|
||||
callbacks.forEach { callback -> app.registerActivityLifecycleCallbacks(callback) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the Process Lifecycle when the app is activated in the foreground (activity opened).
|
||||
*/
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
Timber.i("Process lifecycle started")
|
||||
|
||||
owner.lifecycleScope.launch {
|
||||
koin.get<SessionRepository>().restoreSession()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun dependencies() = listOf(KoinInitializer::class.java)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.jellyfin.androidtv.auth.store
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -47,8 +48,15 @@ class AuthenticationStore(
|
||||
// No store found
|
||||
if (!storePath.exists()) return emptyMap()
|
||||
|
||||
// Parse JSON document
|
||||
val root = try {
|
||||
json.parseToJsonElement(storePath.readText()).jsonObject
|
||||
} catch (e: SerializationException) {
|
||||
Timber.e(e, "Unable to read JSON")
|
||||
JsonObject(emptyMap())
|
||||
}
|
||||
|
||||
// Check for version
|
||||
val root = json.parseToJsonElement(storePath.readText()).jsonObject
|
||||
return when (root["version"]?.jsonPrimitive?.intOrNull) {
|
||||
1 -> json.decodeFromJsonElement<Map<UUID, AuthenticationStoreServer>>(root["servers"]!!)
|
||||
null -> {
|
||||
|
||||
@@ -12,6 +12,7 @@ interface UserViewsRepository {
|
||||
|
||||
fun isSupported(collectionType: String?): Boolean
|
||||
fun allowViewSelection(collectionType: String?): Boolean
|
||||
fun allowGridView(collectionType: String?): Boolean
|
||||
}
|
||||
|
||||
class UserViewsRepositoryImpl(
|
||||
@@ -26,7 +27,8 @@ class UserViewsRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun isSupported(collectionType: String?) = collectionType !in unsupportedCollectionTypes
|
||||
override fun allowViewSelection(collectionType: String?) = collectionType != CollectionType.Music
|
||||
override fun allowViewSelection(collectionType: String?) = collectionType !in disallowViewSelectionCollectionTypes
|
||||
override fun allowGridView(collectionType: String?) = collectionType !in disallowGridViewCollectionTypes
|
||||
|
||||
private companion object {
|
||||
private val unsupportedCollectionTypes = arrayOf(
|
||||
@@ -34,5 +36,16 @@ class UserViewsRepositoryImpl(
|
||||
CollectionType.Games,
|
||||
CollectionType.Folders
|
||||
)
|
||||
|
||||
private val disallowViewSelectionCollectionTypes = arrayOf(
|
||||
CollectionType.livetv,
|
||||
CollectionType.Music,
|
||||
CollectionType.Photos,
|
||||
)
|
||||
|
||||
private val disallowGridViewCollectionTypes = arrayOf(
|
||||
CollectionType.livetv,
|
||||
CollectionType.Music
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ val appModule = module {
|
||||
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get()) }
|
||||
|
||||
viewModel { StartupViewModel(get(), get(), get(), get()) }
|
||||
viewModel { UserLoginViewModel(get(), get(), get()) }
|
||||
viewModel { UserLoginViewModel(get(), get(), get(), get(defaultDeviceInfo)) }
|
||||
viewModel { ServerAddViewModel(get()) }
|
||||
viewModel { NextUpViewModel(get(), get(), get(), get()) }
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.jellyfin.androidtv.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
@@ -22,6 +23,7 @@ class TextUnderButton @JvmOverloads constructor(
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
descendantFocusability = FOCUS_BLOCK_DESCENDANTS
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) defaultFocusHighlightEnabled = false
|
||||
}
|
||||
|
||||
fun setLabel(text: String?) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.jellyfin.androidtv.ui.card
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
@@ -25,6 +26,7 @@ class DefaultCardView @JvmOverloads constructor(
|
||||
init {
|
||||
isFocusable = true
|
||||
descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) defaultFocusHighlightEnabled = false
|
||||
}
|
||||
|
||||
val binding = ViewCardDefaultBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
package org.jellyfin.androidtv.ui.card
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.FrameLayout
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.toSpannable
|
||||
import org.jellyfin.androidtv.databinding.ViewCardMediaInfoBinding
|
||||
import org.jellyfin.androidtv.util.Utils
|
||||
import org.jellyfin.apiclient.model.entities.MediaStream
|
||||
import org.jellyfin.apiclient.model.entities.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.MediaStream
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
class MediaInfoCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -30,13 +22,13 @@ class MediaInfoCardView @JvmOverloads constructor(
|
||||
fun setMediaStream(mediaStream: MediaStream) {
|
||||
binding.title.text = mediaStream.type.toString()
|
||||
binding.entries.text = SpannableStringBuilder().apply {
|
||||
if (mediaStream.type != MediaStreamType.Video) mediaStream.language?.let { addRow("Language", it) }
|
||||
if (mediaStream.type != MediaStreamType.VIDEO) mediaStream.language?.let { addRow("Language", it) }
|
||||
mediaStream.codec?.let { addRow("Codec", it) }
|
||||
mediaStream.profile?.let { addRow("Profile", it) }
|
||||
mediaStream.level?.let { addRow("Level", it.toString()) }
|
||||
mediaStream.channelLayout?.let { addRow("Layout", it) }
|
||||
|
||||
if (mediaStream.type == MediaStreamType.Video) {
|
||||
if (mediaStream.type == MediaStreamType.VIDEO) {
|
||||
if (mediaStream.width != null && mediaStream.height != null) addRow("Resolution", "${mediaStream.width}x${mediaStream.height}")
|
||||
if (mediaStream.isAnamorphic == true) addRow("Anamorphic")
|
||||
if (mediaStream.isInterlaced) addRow("Interlaced")
|
||||
|
||||
@@ -756,7 +756,7 @@ public class FullDetailsActivity extends BaseActivity implements RecordingIndica
|
||||
HeaderItem header = new HeaderItem("Media Details"+(ms.getContainer() != null ? " (" +ms.getContainer()+")" : ""));
|
||||
ArrayObjectAdapter infoAdapter = new ArrayObjectAdapter(new InfoCardPresenter());
|
||||
for (MediaStream stream : ms.getMediaStreams()) {
|
||||
infoAdapter.add(stream);
|
||||
infoAdapter.add(ModelCompat.asSdk(stream));
|
||||
}
|
||||
|
||||
adapter.add(new ListRow(header, infoAdapter));
|
||||
|
||||
@@ -410,7 +410,7 @@ public class PlaybackController implements PlaybackControllerNotifiable {
|
||||
newPos = getRealTimeProgress();
|
||||
// live tv
|
||||
} else if (hasInitializedVideoManager()) {
|
||||
if (!isPlaying() && mSeekPosition != -1) {
|
||||
if (currentSkipPos != 0 || (!isPlaying() && mSeekPosition != -1)) {
|
||||
newPos = mSeekPosition;
|
||||
// use seekPosition until playback starts
|
||||
} else if (isPlaying()) {
|
||||
@@ -1270,9 +1270,7 @@ public class PlaybackController implements PlaybackControllerNotifiable {
|
||||
|
||||
public void skip(int msec) {
|
||||
if (hasInitializedVideoManager() && (isPlaying() || isPaused()) && spinnerOff && mVideoManager.getCurrentPosition() > 0) { //guard against skipping before playback has truly begun
|
||||
pause();
|
||||
mHandler.removeCallbacks(skipRunnable);
|
||||
stopReportLoop();
|
||||
refreshCurrentPosition();
|
||||
currentSkipPos = Utils.getSafeSeekPosition((currentSkipPos == 0 ? mCurrentPosition : currentSkipPos) + msec, getDuration());
|
||||
|
||||
|
||||
@@ -529,7 +529,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
|
||||
}
|
||||
|
||||
public boolean setExoPlayerTrack(int index, @Nullable MediaStreamType streamType, @Nullable List<MediaStream> allStreams) {
|
||||
if (!nativeMode || !isInitialized() || allStreams == null || streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
|
||||
if (!nativeMode || !isInitialized() || allStreams == null || allStreams.isEmpty() || streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
|
||||
return false;
|
||||
|
||||
int chosenTrackType = streamType == MediaStreamType.Subtitle ? C.TRACK_TYPE_TEXT : C.TRACK_TYPE_AUDIO;
|
||||
|
||||
@@ -9,9 +9,11 @@ import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.data.repository.UserViewsRepository
|
||||
import org.jellyfin.androidtv.ui.browsing.DisplayPreferencesScreen
|
||||
import org.jellyfin.androidtv.ui.livetv.GuideOptionsScreen
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.link
|
||||
import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen
|
||||
import org.jellyfin.apiclient.model.entities.CollectionType
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class LibrariesPreferencesScreen : OptionsFragment() {
|
||||
@@ -37,11 +39,20 @@ class LibrariesPreferencesScreen : OptionsFragment() {
|
||||
|
||||
link {
|
||||
title = it.name
|
||||
|
||||
if (userViewsRepository.allowGridView(it.collectionType)) {
|
||||
icon = R.drawable.ic_folder
|
||||
withFragment<DisplayPreferencesScreen>(bundleOf(
|
||||
DisplayPreferencesScreen.ARG_ALLOW_VIEW_SELECTION to allowViewSelection,
|
||||
DisplayPreferencesScreen.ARG_PREFERENCES_ID to it.displayPreferencesId,
|
||||
))
|
||||
} else if (it.collectionType == CollectionType.livetv) {
|
||||
icon = R.drawable.ic_guide
|
||||
withFragment<GuideOptionsScreen>()
|
||||
} else {
|
||||
icon = R.drawable.ic_folder
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.view.ViewGroup;
|
||||
import androidx.leanback.widget.Presenter;
|
||||
|
||||
import org.jellyfin.androidtv.ui.card.MediaInfoCardView;
|
||||
import org.jellyfin.apiclient.model.entities.MediaStream;
|
||||
import org.jellyfin.sdk.model.api.MediaStream;
|
||||
|
||||
public class InfoCardPresenter extends Presenter {
|
||||
class ViewHolder extends Presenter.ViewHolder {
|
||||
@@ -15,7 +15,6 @@ public class InfoCardPresenter extends Presenter {
|
||||
public ViewHolder(View view) {
|
||||
super(view);
|
||||
mInfoCardView = (MediaInfoCardView) view;
|
||||
|
||||
}
|
||||
|
||||
public void setItem(MediaStream ms) {
|
||||
@@ -44,6 +43,5 @@ public class InfoCardPresenter extends Presenter {
|
||||
|
||||
@Override
|
||||
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
|
||||
//Log.d(TAG, "onUnbindViewHolder");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.jellyfin.androidtv.ui.shared
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.constant.AppTheme
|
||||
@@ -14,22 +15,38 @@ class AppThemeCallbacks(
|
||||
private var lastPreferencesTheme: AppTheme? = null
|
||||
|
||||
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) activity.applyThemeOnCreated()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) activity.applyThemeOnCreated()
|
||||
}
|
||||
|
||||
override fun onActivityPreResumed(activity: Activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) activity.applyThemeOnResume()
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) activity.applyThemeOnResume()
|
||||
}
|
||||
|
||||
private fun Activity.applyThemeOnCreated() {
|
||||
userPreferences[UserPreferences.appTheme].let {
|
||||
Timber.i("Applying theme: %s", it)
|
||||
activity.setTheme(ThemeManager.getTheme(activity, it))
|
||||
when (activity) {
|
||||
setTheme(ThemeManager.getTheme(this, it))
|
||||
when (this) {
|
||||
is PreferencesActivity -> lastPreferencesTheme = it
|
||||
else -> lastTheme = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPreResumed(activity: Activity) {
|
||||
val lastThemeForActivity = if (activity is PreferencesActivity) lastPreferencesTheme else lastTheme
|
||||
private fun Activity.applyThemeOnResume() {
|
||||
val lastThemeForActivity = if (this is PreferencesActivity) lastPreferencesTheme else lastTheme
|
||||
userPreferences[UserPreferences.appTheme].let {
|
||||
if (lastThemeForActivity != null && lastThemeForActivity != it) {
|
||||
Timber.i("Recreating activity to apply new theme: %s -> %s", lastThemeForActivity, it)
|
||||
activity.recreate()
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.jellyfin.androidtv.ui.shared
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import org.jellyfin.androidtv.auth.repository.SessionRepository
|
||||
import org.jellyfin.androidtv.ui.preference.PreferencesActivity
|
||||
@@ -26,14 +27,22 @@ class AuthenticatedUserCallbacks(
|
||||
}
|
||||
|
||||
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
val name = activity::class.qualifiedName
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) activity.checkAuthentication()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) activity.checkAuthentication()
|
||||
}
|
||||
|
||||
private fun Activity.checkAuthentication() {
|
||||
val name = this::class.qualifiedName
|
||||
|
||||
if (name in ignoredClassNames) {
|
||||
Timber.i("Activity $name is ignored")
|
||||
} else if (sessionRepository.currentSession.value == null) {
|
||||
Timber.w("Activity $name started without a session, bouncing to StartupActivity")
|
||||
activity.startActivity(Intent(activity, StartupActivity::class.java))
|
||||
activity.finish()
|
||||
startActivity(Intent(this, StartupActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,11 @@ import org.jellyfin.androidtv.auth.model.UnknownQuickConnectState
|
||||
import org.jellyfin.androidtv.auth.model.User
|
||||
import org.jellyfin.androidtv.auth.repository.AuthenticationRepository
|
||||
import org.jellyfin.androidtv.auth.repository.ServerRepository
|
||||
import org.jellyfin.androidtv.util.sdk.forUser
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -33,6 +35,7 @@ class UserLoginViewModel(
|
||||
jellyfin: Jellyfin,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val authenticationRepository: AuthenticationRepository,
|
||||
private val defaultDeviceInfo: DeviceInfo,
|
||||
) : ViewModel() {
|
||||
private val _loginState = MutableStateFlow<LoginState?>(null)
|
||||
val loginState = _loginState.asStateFlow()
|
||||
@@ -74,6 +77,7 @@ class UserLoginViewModel(
|
||||
_quickConnectState.emit(UnknownQuickConnectState)
|
||||
quickConnectSecret = null
|
||||
quickConnectApi.baseUrl = server.address
|
||||
quickConnectApi.deviceInfo = defaultDeviceInfo.forUser(UUID.randomUUID())
|
||||
|
||||
try {
|
||||
val response by quickConnectApi.quickConnectApi.initiate()
|
||||
|
||||
@@ -36,14 +36,22 @@ fun BaseItemDto?.canPlay() = this != null
|
||||
&& (!isFolderItem || childCount == null || childCount > 0)
|
||||
|
||||
fun BaseItemDto.getFullName(context: Context): String? = when (baseItemType) {
|
||||
BaseItemType.Episode -> listOfNotNull(
|
||||
seriesName,
|
||||
parentIndexNumber?.let { context.getString(R.string.lbl_season_number, it) },
|
||||
indexNumber?.let { start ->
|
||||
indexNumberEnd?.let { end -> context.getString(R.string.lbl_episode_range, start, end) }
|
||||
?: context.getString(R.string.lbl_episode_number, start)
|
||||
BaseItemType.Episode -> buildList {
|
||||
add(seriesName)
|
||||
|
||||
if (parentIndexNumber == 0) {
|
||||
add(context.getString(R.string.episode_name_special))
|
||||
} else {
|
||||
if (parentIndexNumber != null)
|
||||
add(context.getString(R.string.lbl_season_number, parentIndexNumber))
|
||||
|
||||
if (indexNumber != null && indexNumberEnd != null)
|
||||
add(context.getString(R.string.lbl_episode_range, indexNumber, indexNumberEnd))
|
||||
else if (indexNumber != null)
|
||||
add(context.getString(R.string.lbl_episode_number, indexNumber))
|
||||
|
||||
}
|
||||
).filter { it.isNotEmpty() }.joinToString(" ")
|
||||
}.filterNot { it.isNullOrBlank() }.joinToString(" ")
|
||||
// we actually want the artist name if available
|
||||
BaseItemType.Audio,
|
||||
BaseItemType.MusicAlbum -> listOfNotNull(albumArtist, name)
|
||||
|
||||
@@ -444,11 +444,13 @@ fun LegacyMediaStream.asSdk(): ModernMediaStream = ModernMediaStream(
|
||||
isAnamorphic = this.isAnamorphic,
|
||||
)
|
||||
|
||||
fun LegacyMediaStreamType.asSdk(): ModernMediaStreamType = when (this) {
|
||||
fun LegacyMediaStreamType?.asSdk(): ModernMediaStreamType = when (this) {
|
||||
LegacyMediaStreamType.Audio -> ModernMediaStreamType.AUDIO
|
||||
LegacyMediaStreamType.Video -> ModernMediaStreamType.VIDEO
|
||||
LegacyMediaStreamType.Subtitle -> ModernMediaStreamType.SUBTITLE
|
||||
LegacyMediaStreamType.EmbeddedImage -> ModernMediaStreamType.EMBEDDED_IMAGE
|
||||
// Note: The apiclient doesn't have the DATA member and defaults to "null"
|
||||
null -> ModernMediaStreamType.DATA
|
||||
}
|
||||
|
||||
fun LegacySubtitleDeliveryMethod.asSdk(): ModernSubtitleDeliveryMethod = when (this) {
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
android:layout_height="150sp"
|
||||
android:id="@+id/programImage"
|
||||
android:layout_gravity="left|top"
|
||||
android:layout_margin="15sp" />
|
||||
android:layout_margin="20sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -495,4 +495,6 @@
|
||||
<string name="pref_crash_report_logs_disabled">Logs will not be included in crash reports</string>
|
||||
<string name="crash_report_toast">Oops! Something went wrong, a crash report was sent to your Jellyfin server.</string>
|
||||
<string name="server_setup_incomplete">The setup of this server has not been completed. Open Jellyfin in a web browser to finish setup before signing in.</string>
|
||||
<string name="enable_picture_viewer_title">Enable new picture viewer</string>
|
||||
<string name="episode_name_special">special</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,7 +24,7 @@ glide = "4.13.2"
|
||||
gson = "2.8.9"
|
||||
jellyfin-apiclient = "v0.7.10"
|
||||
jellyfin-exoplayer-ffmpegextension = "2.18.1+1"
|
||||
jellyfin-sdk = "1.3.4"
|
||||
jellyfin-sdk = "1.3.7"
|
||||
junit = "4.13.2"
|
||||
kenburnsview = "1.0.7"
|
||||
koin = "3.2.0"
|
||||
|
||||
Reference in New Issue
Block a user