mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-05 19:52:34 +02:00
Merge pull request #1018 from KotlinGeekDev/fullscreen-video-behavior
Change tap to fullscreen video behavior for livestreams.
This commit is contained in:
@@ -252,6 +252,7 @@ dependencies {
|
|||||||
|
|
||||||
// Language picker and Theme chooser
|
// Language picker and Theme chooser
|
||||||
implementation libs.androidx.appcompat
|
implementation libs.androidx.appcompat
|
||||||
|
implementation libs.androidx.window.core.android
|
||||||
|
|
||||||
// Local model for language identification
|
// Local model for language identification
|
||||||
playImplementation libs.google.mlkit.language.id
|
playImplementation libs.google.mlkit.language.id
|
||||||
|
@@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package androidx.compose.material3.adaptive
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.toComposeRect
|
||||||
|
import androidx.window.layout.FoldingFeature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the [Posture] for a given list of [FoldingFeature]s. This methods converts framework
|
||||||
|
* folding info into the Material-opinionated posture info.
|
||||||
|
*/
|
||||||
|
fun calculatePosture(foldingFeatures: List<FoldingFeature>): Posture {
|
||||||
|
var isTableTop = false
|
||||||
|
val hingeList = mutableListOf<HingeInfo>()
|
||||||
|
@Suppress("ListIterator")
|
||||||
|
foldingFeatures.forEach {
|
||||||
|
if (it.orientation == FoldingFeature.Orientation.HORIZONTAL &&
|
||||||
|
it.state == FoldingFeature.State.HALF_OPENED
|
||||||
|
) {
|
||||||
|
isTableTop = true
|
||||||
|
}
|
||||||
|
hingeList.add(
|
||||||
|
HingeInfo(
|
||||||
|
bounds = it.bounds.toComposeRect(),
|
||||||
|
isFlat = it.state == FoldingFeature.State.FLAT,
|
||||||
|
isVertical = it.orientation == FoldingFeature.Orientation.VERTICAL,
|
||||||
|
isSeparating = it.isSeparating,
|
||||||
|
isOccluding = it.occlusionType == FoldingFeature.OcclusionType.FULL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Posture(isTableTop, hingeList)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posture info that can help make layout adaptation decisions. For example when
|
||||||
|
* [Posture.separatingVerticalHingeBounds] is not empty, the layout may want to avoid putting any
|
||||||
|
* content over those hinge area. We suggest to use [calculatePosture] to retrieve instances of this
|
||||||
|
* class in applications, unless you have a strong need of customization that cannot be fulfilled by
|
||||||
|
* the default implementation.
|
||||||
|
*
|
||||||
|
* Note that the hinge bounds will be represent as [Rect] with window coordinates, instead of layout
|
||||||
|
* coordinate.
|
||||||
|
*
|
||||||
|
* @constructor create an instance of [Posture]
|
||||||
|
* @property isTabletop `true` if the current window is considered as in the table top mode, i.e.
|
||||||
|
* there is one half-opened horizontal hinge in the middle of the current window. When
|
||||||
|
* this is `true` it usually means it's hard for users to interact with the window area
|
||||||
|
* around the hinge and developers may consider separating the layout along the hinge and
|
||||||
|
* show software keyboard or other controls in the bottom half of the window.
|
||||||
|
* @property hingeList a list of all hinges that are relevant to the posture.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
class Posture(
|
||||||
|
val isTabletop: Boolean = false,
|
||||||
|
val hingeList: List<HingeInfo> = emptyList(),
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Posture) return false
|
||||||
|
if (isTabletop != other.isTabletop) return false
|
||||||
|
if (hingeList != other.hingeList) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = isTabletop.hashCode()
|
||||||
|
result = 31 * result + hingeList.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
@Suppress("ListIterator")
|
||||||
|
return "Posture(isTabletop=$isTabletop, " +
|
||||||
|
"hinges=[${hingeList.joinToString(", ")}])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of vertical hinge bounds that are separating.
|
||||||
|
*/
|
||||||
|
val Posture.separatingVerticalHingeBounds get() = hingeList.getBounds { isVertical && isSeparating }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of vertical hinge bounds that are occluding.
|
||||||
|
*/
|
||||||
|
val Posture.occludingVerticalHingeBounds get() = hingeList.getBounds { isVertical && isOccluding }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all vertical hinge bounds.
|
||||||
|
*/
|
||||||
|
val Posture.allVerticalHingeBounds get() = hingeList.getBounds { isVertical }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of horizontal hinge bounds that are separating.
|
||||||
|
*/
|
||||||
|
val Posture.separatingHorizontalHingeBounds
|
||||||
|
get() = hingeList.getBounds { !isVertical && isSeparating }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of horizontal hinge bounds that are occluding.
|
||||||
|
*/
|
||||||
|
val Posture.occludingHorizontalHingeBounds
|
||||||
|
get() = hingeList.getBounds { !isVertical && isOccluding }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all horizontal hinge bounds.
|
||||||
|
*/
|
||||||
|
val Posture.allHorizontalHingeBounds
|
||||||
|
get() = hingeList.getBounds { !isVertical }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that contains the info of a hinge relevant to a [Posture].
|
||||||
|
*
|
||||||
|
* @param bounds the bounds of the hinge in the relevant viewport.
|
||||||
|
* @param isFlat `true` if the hinge is fully open and the relevant window space presented to the
|
||||||
|
* user is flat.
|
||||||
|
* @param isVertical `true` if the hinge is a vertical one, i.e., it separates the viewport into
|
||||||
|
* left and right; `false` if the hinge is horizontal, i.e., it separates the viewport
|
||||||
|
* into top and bottom.
|
||||||
|
* @param isSeparating `true` if the hinge creates two logical display areas.
|
||||||
|
* @param isOccluding `true` if the hinge conceals part of the display.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
class HingeInfo(
|
||||||
|
val bounds: Rect,
|
||||||
|
val isFlat: Boolean,
|
||||||
|
val isVertical: Boolean,
|
||||||
|
val isSeparating: Boolean,
|
||||||
|
val isOccluding: Boolean,
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is HingeInfo) return false
|
||||||
|
if (bounds != other.bounds) return false
|
||||||
|
if (isFlat != other.isFlat) return false
|
||||||
|
if (isVertical != other.isVertical) return false
|
||||||
|
if (isSeparating != other.isSeparating) return false
|
||||||
|
if (isOccluding != other.isOccluding) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = bounds.hashCode()
|
||||||
|
result = 31 * result + isFlat.hashCode()
|
||||||
|
result = 31 * result + isVertical.hashCode()
|
||||||
|
result = 31 * result + isSeparating.hashCode()
|
||||||
|
result = 31 * result + isOccluding.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
"HingeInfo(bounds=$bounds, " +
|
||||||
|
"isFlat=$isFlat, " +
|
||||||
|
"isVertical=$isVertical, " +
|
||||||
|
"isSeparating=$isSeparating, " +
|
||||||
|
"isOccluding=$isOccluding)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun List<HingeInfo>.getBounds(predicate: HingeInfo.() -> Boolean): List<Rect> =
|
||||||
|
@Suppress("ListIterator")
|
||||||
|
mapNotNull { if (it.predicate()) it.bounds else null }
|
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package androidx.compose.material3.adaptive
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
import androidx.window.layout.FoldingFeature
|
||||||
|
import androidx.window.layout.WindowInfoTracker
|
||||||
|
import androidx.window.layout.WindowMetricsCalculator
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun currentWindowAdaptiveInfo(): WindowAdaptiveInfo {
|
||||||
|
val windowSize =
|
||||||
|
with(LocalDensity.current) {
|
||||||
|
currentWindowSize().toSize().toDpSize()
|
||||||
|
}
|
||||||
|
return WindowAdaptiveInfo(
|
||||||
|
WindowSizeClass.compute(windowSize.width.value, windowSize.height.value),
|
||||||
|
calculatePosture(collectFoldingFeaturesAsState().value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns and automatically update the current window size from [WindowMetricsCalculator].
|
||||||
|
*
|
||||||
|
* @return an [IntSize] that represents the current window size.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun currentWindowSize(): IntSize {
|
||||||
|
// Observe view configuration changes and recalculate the size class on each change. We can't
|
||||||
|
// use Activity#onConfigurationChanged as this will sometimes fail to be called on different
|
||||||
|
// API levels, hence why this function needs to be @Composable so we can observe the
|
||||||
|
// ComposeView's configuration changes.
|
||||||
|
LocalConfiguration.current
|
||||||
|
val windowBounds =
|
||||||
|
WindowMetricsCalculator
|
||||||
|
.getOrCreate()
|
||||||
|
.computeCurrentWindowMetrics(LocalContext.current)
|
||||||
|
.bounds
|
||||||
|
return IntSize(windowBounds.width(), windowBounds.height())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects the current window folding features from [WindowInfoTracker] in to a [State].
|
||||||
|
*
|
||||||
|
* @return a [State] of a [FoldingFeature] list.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun collectFoldingFeaturesAsState(): State<List<FoldingFeature>> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return remember(context) {
|
||||||
|
if (context is Activity) {
|
||||||
|
// TODO(b/284347941) remove the instance check after the test bug is fixed.
|
||||||
|
WindowInfoTracker
|
||||||
|
.getOrCreate(context)
|
||||||
|
.windowLayoutInfo(context)
|
||||||
|
} else {
|
||||||
|
WindowInfoTracker
|
||||||
|
.getOrCreate(context)
|
||||||
|
.windowLayoutInfo(context)
|
||||||
|
}.map { it.displayFeatures.filterIsInstance<FoldingFeature>() }
|
||||||
|
}.collectAsState(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class collects window info that affects adaptation decisions. An adaptive layout is supposed
|
||||||
|
* to use the info from this class to decide how the layout is supposed to be adapted.
|
||||||
|
*
|
||||||
|
* @constructor create an instance of [WindowAdaptiveInfo]
|
||||||
|
* @param windowSizeClass [WindowSizeClass] of the current window.
|
||||||
|
* @param windowPosture [Posture] of the current window.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
class WindowAdaptiveInfo(
|
||||||
|
val windowSizeClass: WindowSizeClass,
|
||||||
|
val windowPosture: Posture,
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is WindowAdaptiveInfo) return false
|
||||||
|
if (windowSizeClass != other.windowSizeClass) return false
|
||||||
|
if (windowPosture != other.windowPosture) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = windowSizeClass.hashCode()
|
||||||
|
result = 31 * result + windowPosture.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "WindowAdaptiveInfo(windowSizeClass=$windowSizeClass, windowPosture=$windowPosture)"
|
||||||
|
}
|
@@ -350,7 +350,7 @@ fun VideoViewInner(
|
|||||||
defaultToStart: Boolean = false,
|
defaultToStart: Boolean = false,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumb: VideoThumb? = null,
|
thumb: VideoThumb? = null,
|
||||||
showControls: Boolean = false,
|
showControls: Boolean = true,
|
||||||
isFiniteHeight: Boolean,
|
isFiniteHeight: Boolean,
|
||||||
borderModifier: Modifier,
|
borderModifier: Modifier,
|
||||||
waveform: ImmutableList<Int>? = null,
|
waveform: ImmutableList<Int>? = null,
|
||||||
@@ -375,7 +375,7 @@ fun VideoViewInner(
|
|||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
controller = controller,
|
controller = controller,
|
||||||
thumbData = thumb,
|
thumbData = thumb,
|
||||||
hideControls = showControls,
|
showControls = showControls,
|
||||||
isFiniteHeight = isFiniteHeight,
|
isFiniteHeight = isFiniteHeight,
|
||||||
nostrUriCallback = nostrUriCallback,
|
nostrUriCallback = nostrUriCallback,
|
||||||
waveform = waveform,
|
waveform = waveform,
|
||||||
@@ -723,7 +723,7 @@ private fun RenderVideoPlayer(
|
|||||||
mimeType: String?,
|
mimeType: String?,
|
||||||
controller: MediaController,
|
controller: MediaController,
|
||||||
thumbData: VideoThumb?,
|
thumbData: VideoThumb?,
|
||||||
hideControls: Boolean = false,
|
showControls: Boolean = true,
|
||||||
isFiniteHeight: Boolean,
|
isFiniteHeight: Boolean,
|
||||||
nostrUriCallback: String?,
|
nostrUriCallback: String?,
|
||||||
waveform: ImmutableList<Int>? = null,
|
waveform: ImmutableList<Int>? = null,
|
||||||
@@ -754,7 +754,7 @@ private fun RenderVideoPlayer(
|
|||||||
setBackgroundColor(Color.Transparent.toArgb())
|
setBackgroundColor(Color.Transparent.toArgb())
|
||||||
setShutterBackgroundColor(Color.Transparent.toArgb())
|
setShutterBackgroundColor(Color.Transparent.toArgb())
|
||||||
controllerAutoShow = false
|
controllerAutoShow = false
|
||||||
useController = !hideControls
|
useController = showControls
|
||||||
thumbData?.thumb?.let { defaultArtwork = it }
|
thumbData?.thumb?.let { defaultArtwork = it }
|
||||||
hideController()
|
hideController()
|
||||||
resizeMode =
|
resizeMode =
|
||||||
@@ -763,7 +763,7 @@ private fun RenderVideoPlayer(
|
|||||||
} else {
|
} else {
|
||||||
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||||
}
|
}
|
||||||
if (!hideControls) {
|
if (showControls) {
|
||||||
onDialog?.let { innerOnDialog ->
|
onDialog?.let { innerOnDialog ->
|
||||||
setFullscreenButtonClickListener {
|
setFullscreenButtonClickListener {
|
||||||
controller.pause()
|
controller.pause()
|
||||||
@@ -783,7 +783,7 @@ private fun RenderVideoPlayer(
|
|||||||
|
|
||||||
waveform?.let { Waveform(it, controller, remember { Modifier.align(Alignment.Center) }) }
|
waveform?.let { Waveform(it, controller, remember { Modifier.align(Alignment.Center) }) }
|
||||||
|
|
||||||
if (!hideControls) {
|
if (showControls) {
|
||||||
val startingMuteState = remember(controller) { controller.volume < 0.001 }
|
val startingMuteState = remember(controller) { controller.volume < 0.001 }
|
||||||
|
|
||||||
MuteButton(
|
MuteButton(
|
||||||
|
@@ -40,6 +40,7 @@ import androidx.compose.material3.DropdownMenuItem
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
@@ -82,6 +83,8 @@ import com.vitorpamplona.amethyst.service.BlurHashRequester
|
|||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
|
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
|
||||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.util.DeviceUtils
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.getActivity
|
||||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
||||||
@@ -118,6 +121,12 @@ fun ZoomableContentView(
|
|||||||
) {
|
) {
|
||||||
var dialogOpen by remember(content) { mutableStateOf(false) }
|
var dialogOpen by remember(content) { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val activity = LocalView.current.context.getActivity()
|
||||||
|
val currentWindowSize = currentWindowAdaptiveInfo().windowSizeClass
|
||||||
|
|
||||||
|
val isLandscapeMode = DeviceUtils.isLandscapeMetric(LocalContext.current)
|
||||||
|
val isFoldableOrLarge = DeviceUtils.windowIsLarge(windowSize = currentWindowSize, isInLandscapeMode = isLandscapeMode)
|
||||||
|
|
||||||
val contentScale =
|
val contentScale =
|
||||||
if (isFiniteHeight) {
|
if (isFiniteHeight) {
|
||||||
ContentScale.Fit
|
ContentScale.Fit
|
||||||
@@ -149,7 +158,12 @@ fun ZoomableContentView(
|
|||||||
roundedCorner = roundedCorner,
|
roundedCorner = roundedCorner,
|
||||||
isFiniteHeight = isFiniteHeight,
|
isFiniteHeight = isFiniteHeight,
|
||||||
nostrUriCallback = content.uri,
|
nostrUriCallback = content.uri,
|
||||||
onDialog = { dialogOpen = true },
|
onDialog = {
|
||||||
|
dialogOpen = true
|
||||||
|
if (!isFoldableOrLarge) {
|
||||||
|
DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity)
|
||||||
|
}
|
||||||
|
},
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -181,7 +195,15 @@ fun ZoomableContentView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false }, accountViewModel)
|
ZoomableImageDialog(
|
||||||
|
content,
|
||||||
|
images,
|
||||||
|
onDismiss = {
|
||||||
|
dialogOpen = false
|
||||||
|
if (!isFoldableOrLarge) DeviceUtils.changeDeviceOrientation(isLandscapeMode, activity)
|
||||||
|
},
|
||||||
|
accountViewModel,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.components.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.window.core.layout.WindowHeightSizeClass
|
||||||
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
|
import androidx.window.core.layout.WindowWidthSizeClass
|
||||||
|
|
||||||
|
object DeviceUtils {
|
||||||
|
/**
|
||||||
|
* Tries to determine if the device is
|
||||||
|
* in landscape mode, by using the [android.util.DisplayMetrics] API.
|
||||||
|
*
|
||||||
|
* Credits: NewPipe devs
|
||||||
|
*/
|
||||||
|
fun isLandscapeMetric(context: Context): Boolean = context.resources.displayMetrics.heightPixels < context.resources.displayMetrics.widthPixels
|
||||||
|
|
||||||
|
fun changeDeviceOrientation(
|
||||||
|
isInLandscape: Boolean,
|
||||||
|
currentActivity: Activity,
|
||||||
|
) {
|
||||||
|
val newOrientation =
|
||||||
|
if (isInLandscape) {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
} else {
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
}
|
||||||
|
currentActivity.requestedOrientation = newOrientation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method looks at the window in which the app resides,
|
||||||
|
* and determines if it is large, while making sure not to be affected
|
||||||
|
* by configuration changes(such as screen rotation),
|
||||||
|
* as the device display metrics can be affected as well.
|
||||||
|
*
|
||||||
|
* It could be used as an approximation of the type of device(as is the case here),
|
||||||
|
* though one ought to be careful about multi-window situations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun windowIsLarge(
|
||||||
|
isInLandscapeMode: Boolean,
|
||||||
|
windowSize: WindowSizeClass,
|
||||||
|
): Boolean =
|
||||||
|
remember(windowSize) {
|
||||||
|
if (isInLandscapeMode) {
|
||||||
|
when (windowSize.windowHeightSizeClass) {
|
||||||
|
WindowHeightSizeClass.COMPACT -> false
|
||||||
|
WindowHeightSizeClass.MEDIUM -> true
|
||||||
|
WindowHeightSizeClass.EXPANDED -> true
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (windowSize.windowWidthSizeClass) {
|
||||||
|
WindowWidthSizeClass.EXPANDED -> true
|
||||||
|
WindowWidthSizeClass.MEDIUM -> true
|
||||||
|
WindowWidthSizeClass.COMPACT -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -49,6 +49,7 @@ zelory = "3.0.1"
|
|||||||
zoomable = "1.6.1"
|
zoomable = "1.6.1"
|
||||||
zxing = "3.5.3"
|
zxing = "3.5.3"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
|
windowCoreAndroid = "1.3.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
|
abedElazizShe-image-compressor = { group = "com.github.AbedElazizShe", name = "LightCompressor", version.ref = "lightcompressor" }
|
||||||
@@ -120,6 +121,7 @@ zelory-video-compressor = { group = "id.zelory", name = "compressor", version.re
|
|||||||
zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" }
|
zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" }
|
||||||
zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
|
zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
|
||||||
zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
zxing-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxingAndroidEmbedded" }
|
||||||
|
androidx-window-core-android = { group = "androidx.window", name = "window-core-android", version.ref = "windowCoreAndroid" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
Reference in New Issue
Block a user