From fa94dba71a7221fbb3eafe34b72b5cfff19a0a6e Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Thu, 11 Aug 2022 20:56:07 +0200 Subject: [PATCH] Add new experimental picture viewer (#1829) * Add KeyHandler * Add PictureViewerFragment to replace PhotoPlayerActivity * Address review feedback --- app/src/main/AndroidManifest.xml | 2 + .../org/jellyfin/androidtv/di/AppModule.kt | 2 + .../androidtv/preference/UserPreferences.kt | 5 + .../ui/itemdetail/PhotoPlayerActivity.java | 12 ++ .../ui/picture/PictureViewerActivity.kt | 65 +++++++ .../ui/picture/PictureViewerFragment.kt | 159 ++++++++++++++++++ .../ui/picture/PictureViewerViewModel.kt | 103 ++++++++++++ .../screen/DeveloperPreferencesScreen.kt | 7 + .../org/jellyfin/androidtv/util/KeyHandler.kt | 127 ++++++++++++++ app/src/main/res/drawable/button_bar_back.xml | 6 + .../drawable/button_icon_tint_animated.xml | 6 + app/src/main/res/drawable/ica_play_pause.xml | 110 ++++++++++++ .../res/layout/fragment_picture_viewer.xml | 72 ++++++++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 4 + 15 files changed, 681 insertions(+) create mode 100644 app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerActivity.kt create mode 100644 app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerFragment.kt create mode 100644 app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerViewModel.kt create mode 100644 app/src/main/java/org/jellyfin/androidtv/util/KeyHandler.kt create mode 100644 app/src/main/res/drawable/button_bar_back.xml create mode 100644 app/src/main/res/drawable/button_icon_tint_animated.xml create mode 100644 app/src/main/res/drawable/ica_play_pause.xml create mode 100644 app/src/main/res/layout/fragment_picture_viewer.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d5f72a4ec..21cafbb232 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -179,6 +179,8 @@ android:name=".ui.itemdetail.PhotoPlayerActivity" android:screenOrientation="landscape" /> + + diff --git a/app/src/main/java/org/jellyfin/androidtv/di/AppModule.kt b/app/src/main/java/org/jellyfin/androidtv/di/AppModule.kt index b0cbabb6bf..c68cba3233 100644 --- a/app/src/main/java/org/jellyfin/androidtv/di/AppModule.kt +++ b/app/src/main/java/org/jellyfin/androidtv/di/AppModule.kt @@ -11,6 +11,7 @@ import org.jellyfin.androidtv.data.repository.NotificationsRepositoryImpl import org.jellyfin.androidtv.data.repository.UserViewsRepository import org.jellyfin.androidtv.data.repository.UserViewsRepositoryImpl import org.jellyfin.androidtv.data.service.BackgroundService +import org.jellyfin.androidtv.ui.picture.PictureViewerViewModel import org.jellyfin.androidtv.ui.playback.MediaManager import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer import org.jellyfin.androidtv.ui.playback.nextup.NextUpViewModel @@ -93,6 +94,7 @@ val appModule = module { viewModel { UserLoginViewModel(get(), get(), get()) } viewModel { ServerAddViewModel(get()) } viewModel { NextUpViewModel(get(), get(), get(), get()) } + viewModel { PictureViewerViewModel(get()) } single { BackgroundService(get(), get(), get(), get()) } diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt index d860baee7f..cbb6005f38 100644 --- a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt +++ b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt @@ -153,6 +153,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore( */ var playbackRewriteEnabled = booleanPreference("playback_new", false) + /** + * Use PictureViewer rewrite + */ + var pictureViewerRewriteEnabled = booleanPreference("picture_viewer_new", false) + /** * When to show the clock. */ diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/PhotoPlayerActivity.java b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/PhotoPlayerActivity.java index e4714a06e3..cdc517f9de 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/PhotoPlayerActivity.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/itemdetail/PhotoPlayerActivity.java @@ -3,6 +3,7 @@ import static org.koin.java.KoinJavaComponent.inject; import android.animation.Animator; +import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; @@ -34,7 +35,9 @@ import org.jellyfin.androidtv.R; import org.jellyfin.androidtv.databinding.ActivityPhotoPlayerBinding; +import org.jellyfin.androidtv.preference.UserPreferences; import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem; +import org.jellyfin.androidtv.ui.picture.PictureViewerActivity; import org.jellyfin.androidtv.ui.playback.MediaManager; import org.jellyfin.androidtv.ui.presentation.PositionableListRowPresenter; import org.jellyfin.androidtv.util.ImageHelper; @@ -71,11 +74,20 @@ public class PhotoPlayerActivity extends FragmentActivity { Handler handler; private Lazy imageHelper = inject(ImageHelper.class); private Lazy mediaManager = inject(MediaManager.class); + private Lazy userPreferences = inject(UserPreferences.class); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + boolean pictureViewerRewriteEnabled = userPreferences.getValue().get(UserPreferences.Companion.getPictureViewerRewriteEnabled()); + if (pictureViewerRewriteEnabled) { + Intent intent = PictureViewerActivity.Companion.createIntent(this, ModelCompat.asSdk(mediaManager.getValue().getCurrentMediaItem().getBaseItem()), getIntent().getBooleanExtra("Play", false)); + startActivity(intent); + finishAfterTransition(); + return; + } + ActivityPhotoPlayerBinding binding = ActivityPhotoPlayerBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerActivity.kt b/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerActivity.kt new file mode 100644 index 0000000000..a1bfe699b0 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerActivity.kt @@ -0,0 +1,65 @@ +package org.jellyfin.androidtv.ui.picture + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.add +import androidx.fragment.app.commit +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import org.jellyfin.androidtv.R +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.serializer.toUUIDOrNull +import org.koin.androidx.viewmodel.ext.android.viewModel + +class PictureViewerActivity : FragmentActivity(R.layout.fragment_content_view) { + companion object { + const val EXTRA_ITEM_ID = "item_id" + const val EXTRA_AUTO_PLAY = "auto_play" + + fun createIntent(context: Context, item: BaseItemDto, autoPlay: Boolean): Intent { + require(item.type == BaseItemKind.PHOTO) { "Expected item of type PHOTO but got ${item.type}" } + + return Intent(context, PictureViewerActivity::class.java).apply { + putExtra(EXTRA_ITEM_ID, item.id.toString()) + putExtra(EXTRA_AUTO_PLAY, autoPlay) + } + } + } + + private val pictureViewerViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Load requested item in viewmodel + lifecycleScope.launch { + val itemId = requireNotNull(intent.extras?.getString(EXTRA_ITEM_ID)?.toUUIDOrNull()) + pictureViewerViewModel.loadItem(itemId) + + val autoPlay = intent.extras?.getBoolean(EXTRA_AUTO_PLAY) == true + if (autoPlay) pictureViewerViewModel.startPresentation() + } + + // Show fragment + supportFragmentManager.commit { + add(R.id.content_view) + } + } + + // Forward key events to fragments + private fun onKeyEvent(keyCode: Int, event: KeyEvent?): Boolean = supportFragmentManager.fragments + .filter { it.isVisible } + .filterIsInstance() + .any { it.onKey(currentFocus, keyCode, event) } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean = + onKeyEvent(keyCode, event) || super.onKeyDown(keyCode, event) + + override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean = + onKeyEvent(keyCode, event) || super.onKeyUp(keyCode, event) +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerFragment.kt b/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerFragment.kt new file mode 100644 index 0000000000..5ed0f779cf --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerFragment.kt @@ -0,0 +1,159 @@ +package org.jellyfin.androidtv.ui.picture + +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.databinding.FragmentPictureViewerBinding +import org.jellyfin.androidtv.ui.AsyncImageView +import org.jellyfin.androidtv.util.createKeyHandler +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.imageApi +import org.jellyfin.sdk.api.client.extensions.libraryApi +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.ImageType +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +class PictureViewerFragment : Fragment(), View.OnKeyListener { + companion object { + /** + * The download API is used by jellyfin-web when loading images. Unfortunately with our + * current code it doesn't work for the app. Larger files (gifs, large photos etc.) can + * cause the app to go out of memory. This is mostly caught by Glide but it ends up + * never displaying the picture in those cases. + * + * This toggle is left in the code in case we migrate to a different image processor that + * potentially fixes the issue. + */ + const val USE_DOWNLOAD_API = false + } + + private val pictureViewerViewModel by sharedViewModel() + private val api by inject() + private lateinit var binding: FragmentPictureViewerBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentPictureViewerBinding.inflate(inflater, container, false) + binding.actionPrevious.setOnClickListener { pictureViewerViewModel.showPrevious() } + binding.actionNext.setOnClickListener { pictureViewerViewModel.showNext() } + binding.actionPlayPause.setOnClickListener { pictureViewerViewModel.togglePresentation() } + binding.root.setOnClickListener { toggleActions() } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + lifecycleScope.launch { + pictureViewerViewModel.currentItem.filterNotNull().collect { item -> + binding.itemSwitcher.getNextView().load(item) + binding.itemSwitcher.showNextView() + } + } + + lifecycleScope.launch { + pictureViewerViewModel.presentationActive.collect { active -> + binding.actionPlayPause.isActivated = active + } + } + } + + private val keyHandler = createKeyHandler { + keyDown(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PAUSE) + .body { pictureViewerViewModel.togglePresentation() } + + keyDown(KeyEvent.KEYCODE_DPAD_LEFT) + .condition { !binding.actions.isVisible } + .body { pictureViewerViewModel.showPrevious() } + + keyDown(KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_PREVIOUS) + .body { pictureViewerViewModel.showPrevious() } + + keyDown(KeyEvent.KEYCODE_DPAD_RIGHT) + .condition { !binding.actions.isVisible } + .body { pictureViewerViewModel.showNext() } + + keyDown(KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_NEXT) + .body { pictureViewerViewModel.showNext() } + + keyDown( + KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_DPAD_CENTER, + ) + .condition { !binding.actions.isVisible } + .body { showActions() } + + keyDown(KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_BACK) + .condition { binding.actions.isVisible } + .body { hideActions() } + } + + override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean = keyHandler.onKey(event) + + private var focusedActionView: View? = null + fun showActions(): Boolean { + if (binding.actions.isVisible) return false + + binding.actions.isVisible = true + if (focusedActionView?.requestFocus() != true) binding.actionPlayPause.requestFocus() + binding.actions.startAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_in)) + return true + } + + fun hideActions(): Boolean { + if (!binding.actions.isVisible) return false + binding.actions.startAnimation(AnimationUtils.loadAnimation(context, R.anim.fade_out).apply { + setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation?) = Unit + + override fun onAnimationEnd(animation: Animation?) { + focusedActionView = binding.actions.findFocus() + binding.actions.isGone = true + } + + override fun onAnimationRepeat(animation: Animation?) = Unit + }) + }) + return true + } + + fun toggleActions(): Boolean { + return if (binding.actions.isVisible) hideActions() + else showActions() + } + + private fun AsyncImageView.load(item: BaseItemDto) { + val url = when (USE_DOWNLOAD_API) { + true -> api.libraryApi.getDownloadUrl( + itemId = item.id, + includeCredentials = true, // TODO send authentication via header + ) + false -> api.imageApi.getItemImageUrl( + itemId = item.id, + imageType = ImageType.PRIMARY, + tag = item.imageTags?.get(ImageType.PRIMARY), + // Ask the server to downscale the image to avoid the app going out of memory + // unfortunately this can be a bit slow for larger files + maxWidth = context.resources.displayMetrics.widthPixels, + maxHeight = context.resources.displayMetrics.heightPixels, + ) + } + + load( + url = url, + blurHash = item.imageBlurHashes?.get(ImageType.PRIMARY)?.get(item.imageTags?.get(ImageType.PRIMARY)), + aspectRatio = item.primaryImageAspectRatio ?: 1.0, + ) + } +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerViewModel.kt b/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerViewModel.kt new file mode 100644 index 0000000000..056456417d --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/ui/picture/PictureViewerViewModel.kt @@ -0,0 +1,103 @@ +package org.jellyfin.androidtv.ui.picture + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.itemsApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi +import org.jellyfin.sdk.model.api.BaseItemDto +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ItemFields +import org.jellyfin.sdk.model.api.SortOrder +import java.util.UUID +import kotlin.time.Duration.Companion.seconds + +class PictureViewerViewModel(private val api: ApiClient) : ViewModel() { + private var album: List = emptyList() + private var albumIndex = -1 + + private val _currentItem = MutableStateFlow(null) + val currentItem = _currentItem.asStateFlow() + + suspend fun loadItem(id: UUID) { + // Load requested item + val itemResponse by api.userLibraryApi.getItem(itemId = id) + _currentItem.value = itemResponse + + val albumResponse by api.itemsApi.getItemsByUserId( + parentId = itemResponse.parentId, + includeItemTypes = setOf(BaseItemKind.PHOTO), + fields = setOf(ItemFields.PRIMARY_IMAGE_ASPECT_RATIO), + sortBy = listOf(ItemFields.SORT_NAME.name), // TODO: Order should be consistent with the stdgridview the user comes from, which allows to change the order + sortOrder = listOf(SortOrder.ASCENDING), + ) + album = albumResponse.items.orEmpty() + albumIndex = album.indexOfFirst { it.id == id } + } + + // Album actions + + fun showNext() { + albumIndex++ + if (albumIndex == album.size) albumIndex = 0 + + _currentItem.value = album[albumIndex] + restartPresentation() + } + + fun showPrevious() { + albumIndex-- + if (albumIndex == -1) albumIndex = album.size - 1 + + _currentItem.value = album[albumIndex] + restartPresentation() + } + + // Presentation + + private var presentationJob: Job? = null + private val _presentationActive = MutableStateFlow(false) + val presentationActive = _presentationActive.asStateFlow() + var presentationDelay = 8.seconds + + fun createPresentationJob() = viewModelScope.launch(Dispatchers.IO) { + while (isActive) { + delay(presentationDelay) + showNext() + } + } + + fun startPresentation() { + if (presentationActive.value) return + _presentationActive.value = true + + presentationJob = createPresentationJob() + } + + fun restartPresentation() { + if (!presentationActive.value) return + + presentationJob?.cancel() + presentationJob = createPresentationJob() + } + + fun stopPresentation() { + if (!presentationActive.value) return + + presentationJob?.cancel() + presentationJob = null + _presentationActive.value = false + } + + fun togglePresentation() { + if (presentationActive.value) stopPresentation() + else startPresentation() + } +} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/DeveloperPreferencesScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/DeveloperPreferencesScreen.kt index a3fca01a13..46bb14c82d 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/DeveloperPreferencesScreen.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/DeveloperPreferencesScreen.kt @@ -31,6 +31,13 @@ class DeveloperPreferencesScreen : OptionsFragment() { bind(userPreferences, UserPreferences.playbackRewriteEnabled) } + + checkbox { + title = getString(R.string.enable_picture_viewer_title) + setContent(R.string.enable_playback_module_description) + + bind(userPreferences, UserPreferences.pictureViewerRewriteEnabled) + } } } } diff --git a/app/src/main/java/org/jellyfin/androidtv/util/KeyHandler.kt b/app/src/main/java/org/jellyfin/androidtv/util/KeyHandler.kt new file mode 100644 index 0000000000..6816a75f22 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/util/KeyHandler.kt @@ -0,0 +1,127 @@ +package org.jellyfin.androidtv.util + +import android.view.KeyEvent +import timber.log.Timber + +@DslMarker +annotation class KeyHandlerDsl + +class KeyHandler( + private val actions: Collection +) { + fun onKey(event: KeyEvent?): Boolean { + if (event == null) return false + + val action = actions + .filter { it.keys.contains(event.keyCode) } + .filter { it.type == event.action } + .filter { it.conditions.isEmpty() || it.conditions.all { predicate -> predicate.invoke() } } + .firstOrNull() ?: return false + + Timber.d("Key press detected: code=${event.keyCode} type=${event.action} action=$action") + + action.body() + return true + } +} + +data class KeyHandlerAction( + val type: Int, + val keys: Collection, + val conditions: Collection<() -> Boolean>, + val body: () -> Unit +) + +class KeyHandlerBuilder { + private val actions = mutableListOf() + + fun addAction(action: KeyHandlerAction) { + actions.add(action) + } + + fun build(): KeyHandler = KeyHandler(actions) + + @KeyHandlerDsl + fun keyDown(vararg keys: Int) = KeyHandlerActionBuilder(this).apply { + setType(KeyEvent.ACTION_DOWN) + for (key in keys) addKey(key) + } + + @KeyHandlerDsl + fun keyDown(vararg keys: Int, action: () -> Unit) = KeyHandlerActionBuilder(this).apply { + setType(KeyEvent.ACTION_DOWN) + for (key in keys) addKey(key) + setBody(action) + }.build() + + @KeyHandlerDsl + fun keyUp(vararg keys: Int) = KeyHandlerActionBuilder(this).apply { + setType(KeyEvent.ACTION_UP) + for (key in keys) addKey(key) + } + + @KeyHandlerDsl + fun keyUp(vararg keys: Int, action: () -> Unit) = KeyHandlerActionBuilder(this).apply { + setType(KeyEvent.ACTION_UP) + for (key in keys) addKey(key) + setBody(action) + }.build() +} + +class KeyHandlerActionBuilder( + private val context: KeyHandlerBuilder +) { + private var type = KeyEvent.ACTION_DOWN + private val keys = mutableListOf() + private val conditions = mutableListOf<() -> Boolean>() + private var body: (() -> Unit)? = null + + fun setType(type: Int) { + require(type == KeyEvent.ACTION_DOWN || type == KeyEvent.ACTION_UP) { + "Type must be KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP" + } + + this.type = type + } + + fun addKey(key: Int) { + keys.add(key) + } + + fun addCondition(condition: () -> Boolean) { + conditions.add(condition) + } + + fun setBody(body: () -> Unit) { + this.body = body + } + + fun build() { + require(keys.isNotEmpty()) { "Keys should contain at least 1 key" } + requireNotNull(body) { "Body must be set" } + + val action = KeyHandlerAction(type, keys, conditions, body!!) + context.addAction(action) + } + + @KeyHandlerDsl + fun condition(condition: () -> Boolean) = apply { + addCondition(condition) + } + + @KeyHandlerDsl + fun condition(condition: () -> Boolean, action: () -> Unit) = apply { + addCondition(condition) + setBody(action) + }.build() + + @KeyHandlerDsl + fun body(action: () -> Unit) = apply { + setBody(action) + }.build() +} + +@KeyHandlerDsl +inline fun createKeyHandler(body: KeyHandlerBuilder.() -> Unit) = KeyHandlerBuilder().apply { + body() +}.build() diff --git a/app/src/main/res/drawable/button_bar_back.xml b/app/src/main/res/drawable/button_bar_back.xml new file mode 100644 index 0000000000..342a62a3f7 --- /dev/null +++ b/app/src/main/res/drawable/button_bar_back.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/button_icon_tint_animated.xml b/app/src/main/res/drawable/button_icon_tint_animated.xml new file mode 100644 index 0000000000..2edb39595c --- /dev/null +++ b/app/src/main/res/drawable/button_icon_tint_animated.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/ica_play_pause.xml b/app/src/main/res/drawable/ica_play_pause.xml new file mode 100644 index 0000000000..a6f473a193 --- /dev/null +++ b/app/src/main/res/drawable/ica_play_pause.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_picture_viewer.xml b/app/src/main/res/layout/fragment_picture_viewer.xml new file mode 100644 index 0000000000..5f5e083a19 --- /dev/null +++ b/app/src/main/res/layout/fragment_picture_viewer.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be4b608802..4759576de9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -494,4 +494,5 @@ Logs will not be included in crash reports Oops! Something went wrong, a crash report was sent to your Jellyfin server. The setup of this server has not been completed. Open Jellyfin in a web browser to finish setup before signing in. + Enable new picture viewer diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3ec4ca5d6e..4a8cda1053 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -64,6 +64,10 @@ @null + +