mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-01 09:09:29 +00:00
Merge branch 'cmp-rewrite' into simkl
This commit is contained in:
commit
db64bada04
18 changed files with 326 additions and 19 deletions
18
androidApp/src/playstore/AndroidManifest.xml
Normal file
18
androidApp/src/playstore/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE"
|
||||
tools:node="remove" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
|
||||
tools:node="remove" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name="com.nuvio.app.features.player.PlayerNowPlayingService"
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -10,4 +10,5 @@ actual object AppFeaturePolicy {
|
|||
actual val heroTrailerPlaybackSupported: Boolean = true
|
||||
actual val inAppUpdaterEnabled: Boolean = true
|
||||
actual val imdbRatingLogoEnabled: Boolean = true
|
||||
actual val mediaPlaybackForegroundServiceEnabled: Boolean = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import android.os.Build
|
||||
|
||||
internal actual fun currentDeviceClientMetadata(): DeviceClientMetadata {
|
||||
val deviceName = formatDeviceName(
|
||||
manufacturer = Build.MANUFACTURER.orEmpty(),
|
||||
model = Build.MODEL.orEmpty(),
|
||||
fallback = "Android device",
|
||||
)
|
||||
val osVersion = Build.VERSION.RELEASE.orEmpty()
|
||||
.trim()
|
||||
.ifBlank { Build.VERSION.SDK_INT.toString() }
|
||||
|
||||
return DeviceClientMetadata(
|
||||
deviceName = deviceName,
|
||||
platform = "Android $osVersion",
|
||||
)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import android.text.SpannableString
|
|||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
|
|
@ -39,6 +40,7 @@ import androidx.media3.common.MimeTypes
|
|||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.Cue
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
|
@ -254,6 +256,9 @@ private fun ExoPlayerSurface(
|
|||
var selectedExternalSubtitleMimeType by remember(playerSourceKey) { mutableStateOf<String?>(null) }
|
||||
val latestSubtitleDelayMs = rememberUpdatedState(subtitleDelayMs)
|
||||
val latestExternalSubtitleMimeType = rememberUpdatedState(selectedExternalSubtitleMimeType)
|
||||
var playerViewRef by remember { mutableStateOf<PlayerView?>(null) }
|
||||
var videoAspectRatio by remember(playerSourceKey) { mutableStateOf(0f) }
|
||||
val latestVideoAspectRatio = rememberUpdatedState(videoAspectRatio)
|
||||
var decoderPriorityOverride by remember(playerSourceKey) { mutableStateOf<Int?>(null) }
|
||||
var fallbackStartPositionMs by remember(playerSourceKey) { mutableStateOf<Long?>(null) }
|
||||
val effectiveDecoderPriority = decoderPriorityOverride ?: playerSettings.decoderPriority
|
||||
|
|
@ -342,6 +347,9 @@ private fun ExoPlayerSurface(
|
|||
shouldNormalizeCuePositionProvider = {
|
||||
latestExternalSubtitleMimeType.value == MimeTypes.TEXT_VTT
|
||||
},
|
||||
videoBoundsFractionProvider = {
|
||||
playerViewRef?.videoBoundsFraction(latestVideoAspectRatio.value)
|
||||
},
|
||||
)
|
||||
.setExtensionRendererMode(effectiveDecoderPriority)
|
||||
.setEnableDecoderFallback(true)
|
||||
|
|
@ -449,7 +457,6 @@ private fun ExoPlayerSurface(
|
|||
|
||||
val pendingSubtitleTrackIndex = remember { mutableListOf<Int>() }
|
||||
val pendingAudioTrackSelection = remember { mutableListOf<TrackSelectionSnapshot>() }
|
||||
var playerViewRef by remember { mutableStateOf<PlayerView?>(null) }
|
||||
var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) }
|
||||
var subtitleSelectionJob by remember { mutableStateOf<Job?>(null) }
|
||||
val isInPip = rememberIsInPictureInPicture()
|
||||
|
|
@ -580,10 +587,6 @@ private fun ExoPlayerSurface(
|
|||
dispatchExoPlayerSnapshot()
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) {
|
||||
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
Log.i(
|
||||
PLAYER_DIAGNOSTIC_TAG,
|
||||
|
|
@ -608,6 +611,13 @@ private fun ExoPlayerSurface(
|
|||
dispatchExoPlayerSnapshot()
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||
if (videoSize.width > 0 && videoSize.height > 0) {
|
||||
videoAspectRatio = videoSize.width.toFloat() / videoSize.height.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: androidx.media3.common.Tracks) {
|
||||
Log.d(TAG, "onTracksChanged: ${tracks.groups.size} groups total")
|
||||
exoPlayer.logCurrentTracks("onTracksChanged")
|
||||
|
|
@ -1794,11 +1804,50 @@ private fun ExoPlayer.logCurrentTracks(context: String) {
|
|||
Log.d(TAG, "--- end logCurrentTracks ---")
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
private fun PlayerView.videoBoundsFraction(aspectRatio: Float): RectF? {
|
||||
val subtitleView = this.subtitleView ?: return null
|
||||
val viewWidth = subtitleView.width.toFloat()
|
||||
val viewHeight = subtitleView.height.toFloat()
|
||||
if (viewWidth <= 0f || viewHeight <= 0f) return null
|
||||
|
||||
if (aspectRatio > 0f) {
|
||||
val parentRatio = viewWidth / viewHeight
|
||||
return if (parentRatio > aspectRatio) {
|
||||
val fitW = viewHeight * aspectRatio
|
||||
val leftPx = (viewWidth - fitW) / 2f
|
||||
RectF(leftPx / viewWidth, 0f, (leftPx + fitW) / viewWidth, 1f)
|
||||
} else {
|
||||
val fitH = viewWidth / aspectRatio
|
||||
val topPx = (viewHeight - fitH) / 2f
|
||||
RectF(0f, topPx / viewHeight, 1f, (topPx + fitH) / viewHeight)
|
||||
}
|
||||
}
|
||||
|
||||
val contentFrame = getTag(androidx.media3.ui.R.id.exo_content_frame) as? AspectRatioFrameLayout
|
||||
?: findViewById<AspectRatioFrameLayout>(androidx.media3.ui.R.id.exo_content_frame)
|
||||
?.also { setTag(androidx.media3.ui.R.id.exo_content_frame, it) }
|
||||
?: return null
|
||||
val frameWidth = contentFrame.width.toFloat()
|
||||
val frameHeight = contentFrame.height.toFloat()
|
||||
if (frameWidth <= 0f || frameHeight <= 0f) return null
|
||||
if (frameWidth > viewWidth || frameHeight > viewHeight) return null
|
||||
val left = contentFrame.x / viewWidth
|
||||
val top = contentFrame.y / viewHeight
|
||||
return RectF(
|
||||
left,
|
||||
top,
|
||||
left + frameWidth / viewWidth,
|
||||
top + frameHeight / viewHeight,
|
||||
)
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
private class SubtitleOffsetRenderersFactory(
|
||||
context: Context,
|
||||
private val subtitleDelayUsProvider: () -> Long,
|
||||
private val shouldNormalizeCuePositionProvider: () -> Boolean,
|
||||
private val videoBoundsFractionProvider: () -> RectF?,
|
||||
) : DefaultRenderersFactory(context) {
|
||||
override fun buildTextRenderers(
|
||||
context: Context,
|
||||
|
|
@ -1810,6 +1859,7 @@ private class SubtitleOffsetRenderersFactory(
|
|||
val normalizingOutput = CueNormalizingTextOutput(
|
||||
delegate = output,
|
||||
shouldNormalizeCuePositionProvider = shouldNormalizeCuePositionProvider,
|
||||
videoBoundsFractionProvider = videoBoundsFractionProvider,
|
||||
)
|
||||
val startIndex = out.size
|
||||
super.buildTextRenderers(context, normalizingOutput, outputLooper, extensionRendererMode, out)
|
||||
|
|
@ -1825,6 +1875,7 @@ private class SubtitleOffsetRenderersFactory(
|
|||
private class CueNormalizingTextOutput(
|
||||
private val delegate: TextOutput,
|
||||
private val shouldNormalizeCuePositionProvider: () -> Boolean,
|
||||
private val videoBoundsFractionProvider: () -> RectF?,
|
||||
) : TextOutput {
|
||||
override fun onCues(cueGroup: CueGroup) {
|
||||
val processed = cueGroup.cues.map(::processCue)
|
||||
|
|
@ -1841,9 +1892,36 @@ private class CueNormalizingTextOutput(
|
|||
if (shouldNormalizeCuePositionProvider()) {
|
||||
processed = normalizeCuePosition(processed)
|
||||
}
|
||||
if (processed.bitmap != null) {
|
||||
val bounds = videoBoundsFractionProvider()
|
||||
if (bounds != null && bounds.width() > 0f && bounds.height() > 0f) {
|
||||
val isIdentity = bounds.left == 0f && bounds.top == 0f
|
||||
&& bounds.width() == 1f && bounds.height() == 1f
|
||||
if (!isIdentity) {
|
||||
processed = remapBitmapCueToVideoBounds(processed, bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
return processed
|
||||
}
|
||||
|
||||
private fun remapBitmapCueToVideoBounds(cue: Cue, bounds: RectF): Cue {
|
||||
val builder = cue.buildUpon()
|
||||
if (cue.position != Cue.DIMEN_UNSET) {
|
||||
builder.setPosition(bounds.left + cue.position * bounds.width())
|
||||
}
|
||||
if (cue.size != Cue.DIMEN_UNSET) {
|
||||
builder.setSize(cue.size * bounds.width())
|
||||
}
|
||||
if (cue.lineType == Cue.LINE_TYPE_FRACTION && cue.line != Cue.DIMEN_UNSET) {
|
||||
builder.setLine(bounds.top + cue.line * bounds.height(), Cue.LINE_TYPE_FRACTION)
|
||||
}
|
||||
if (cue.bitmapHeight != Cue.DIMEN_UNSET) {
|
||||
builder.setBitmapHeight(cue.bitmapHeight * bounds.height())
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun normalizeCuePosition(cue: Cue): Cue {
|
||||
if (cue.bitmap != null || cue.verticalType != Cue.TYPE_UNSET || cue.line == Cue.DIMEN_UNSET) {
|
||||
return cue
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import android.os.Looper
|
|||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import com.nuvio.app.R
|
||||
import com.nuvio.app.core.build.AppFeaturePolicy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.lang.ref.WeakReference
|
||||
import java.net.HttpURLConnection
|
||||
|
|
@ -106,7 +107,9 @@ internal class AndroidPlayerNowPlayingController(
|
|||
get() = !released && metadata != null
|
||||
|
||||
init {
|
||||
createNotificationChannel(appContext)
|
||||
if (AppFeaturePolicy.mediaPlaybackForegroundServiceEnabled) {
|
||||
createNotificationChannel(appContext)
|
||||
}
|
||||
AndroidNowPlayingActionDispatcher.register(this)
|
||||
}
|
||||
|
||||
|
|
@ -288,6 +291,7 @@ internal class AndroidPlayerNowPlayingController(
|
|||
}
|
||||
|
||||
private fun publishNotification() {
|
||||
if (!AppFeaturePolicy.mediaPlaybackForegroundServiceEnabled) return
|
||||
val currentMetadata = metadata ?: return
|
||||
val notification = buildNotification(
|
||||
context = appContext,
|
||||
|
|
@ -377,6 +381,7 @@ class PlayerNowPlayingService : Service() {
|
|||
|
||||
companion object {
|
||||
internal fun publish(context: Context, notification: Notification) {
|
||||
if (!AppFeaturePolicy.mediaPlaybackForegroundServiceEnabled) return
|
||||
PlayerNowPlayingServiceState.notification = notification
|
||||
val intent = Intent(context, PlayerNowPlayingService::class.java)
|
||||
.setAction(ACTION_START_FOREGROUND)
|
||||
|
|
@ -394,6 +399,7 @@ class PlayerNowPlayingService : Service() {
|
|||
}
|
||||
|
||||
internal fun hide(context: Context) {
|
||||
if (!AppFeaturePolicy.mediaPlaybackForegroundServiceEnabled) return
|
||||
PlayerNowPlayingServiceState.notification = null
|
||||
runCatching { context.stopService(Intent(context, PlayerNowPlayingService::class.java)) }
|
||||
context.getSystemService(NotificationManager::class.java)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ package com.nuvio.app.core.build
|
|||
actual object AppFeaturePolicy {
|
||||
actual val pluginsEnabled: Boolean = false
|
||||
actual val supportersContributorsPageEnabled: Boolean = true
|
||||
actual val accountDeletionEnabled: Boolean = false
|
||||
actual val accountDeletionEnabled: Boolean = true
|
||||
actual val personalMediaAddonCopyEnabled: Boolean = false
|
||||
actual val p2pEnabled: Boolean = true
|
||||
actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.EXTERNAL
|
||||
actual val heroTrailerPlaybackSupported: Boolean = false
|
||||
actual val inAppUpdaterEnabled: Boolean = false
|
||||
actual val imdbRatingLogoEnabled: Boolean = false
|
||||
actual val mediaPlaybackForegroundServiceEnabled: Boolean = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -434,6 +434,7 @@
|
|||
<string name="compose_settings_page_playback">Playback</string>
|
||||
<string name="compose_settings_page_plugins">Plugins</string>
|
||||
<string name="compose_settings_page_poster_customization">Poster Card Style</string>
|
||||
<string name="compose_settings_page_privacy_policy">Privacy Policy</string>
|
||||
<string name="compose_settings_page_root">Settings</string>
|
||||
<string name="compose_settings_page_streams">Streams</string>
|
||||
<string name="settings_appearance_section_streams">STREAMS</string>
|
||||
|
|
@ -457,6 +458,7 @@
|
|||
<string name="compose_settings_root_general_section">GENERAL</string>
|
||||
<string name="compose_settings_root_integrations_description">Manage available integrations</string>
|
||||
<string name="compose_settings_root_notifications_description">Manage episode release alerts and send a test notification.</string>
|
||||
<string name="compose_settings_root_privacy_policy_description">Learn how Nuvio handles your data</string>
|
||||
<string name="compose_settings_root_streams_description">Stream result display and badge URL rules.</string>
|
||||
<string name="compose_settings_root_switch_profile_description">Change to a different profile.</string>
|
||||
<string name="compose_settings_root_switch_profile_title">Switch Profile</string>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ import coil3.svg.SvgDecoder
|
|||
import com.nuvio.app.core.build.AppFeaturePolicy
|
||||
import com.nuvio.app.core.auth.AuthRepository
|
||||
import com.nuvio.app.core.auth.AuthState
|
||||
import com.nuvio.app.core.auth.DeviceSessionRegistration
|
||||
import com.nuvio.app.core.deeplink.AppDeepLink
|
||||
import com.nuvio.app.core.deeplink.AppDeepLinkRepository
|
||||
import com.nuvio.app.core.network.NetworkCondition
|
||||
|
|
@ -498,6 +499,11 @@ fun App(
|
|||
NetworkStatusRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(authState) {
|
||||
if (!ownsAppRuntime) return@LaunchedEffect
|
||||
DeviceSessionRegistration.registerIfAuthenticated(force = true)
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
profileState.activeProfile?.profileIndex,
|
||||
profileState.activeProfile?.name,
|
||||
|
|
@ -1063,6 +1069,7 @@ private fun MainAppContent(
|
|||
if (!ownsAppRuntime) return@LaunchedEffect
|
||||
AppForegroundMonitor.events().collect {
|
||||
NetworkStatusRepository.requestForegroundRefresh()
|
||||
DeviceSessionRegistration.registerIfAuthenticated()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.core.build.AppVersionConfig
|
||||
import com.nuvio.app.core.network.SupabaseProvider
|
||||
import com.nuvio.app.core.sync.SyncClientIdentity
|
||||
import io.github.jan.supabase.postgrest.postgrest
|
||||
import io.github.jan.supabase.postgrest.rpc
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.TimeMark
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
private const val CLIENT_NAME = "Nuvio Mobile"
|
||||
private val REGISTRATION_INTERVAL = 15.minutes
|
||||
|
||||
internal data class DeviceClientMetadata(
|
||||
val deviceName: String,
|
||||
val platform: String,
|
||||
)
|
||||
|
||||
internal expect fun currentDeviceClientMetadata(): DeviceClientMetadata
|
||||
|
||||
object DeviceSessionRegistration {
|
||||
private val log = Logger.withTag("DeviceSessionRegistration")
|
||||
private val registrationMutex = Mutex()
|
||||
private var lastRegistration: TimeMark? = null
|
||||
|
||||
suspend fun registerIfAuthenticated(force: Boolean = false): Boolean {
|
||||
val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return false
|
||||
if (authState.isAnonymous) return false
|
||||
|
||||
return registrationMutex.withLock {
|
||||
if (!force && lastRegistration?.elapsedNow()?.let { it < REGISTRATION_INTERVAL } == true) {
|
||||
return@withLock true
|
||||
}
|
||||
|
||||
val metadata = currentDeviceClientMetadata()
|
||||
val params = buildDeviceRegistrationParams(
|
||||
installationId = SyncClientIdentity.currentClientId(),
|
||||
clientVersion = AppVersionConfig.VERSION_NAME,
|
||||
metadata = metadata,
|
||||
)
|
||||
|
||||
try {
|
||||
SupabaseProvider.client.postgrest.rpc("register_current_device", params)
|
||||
lastRegistration = TimeSource.Monotonic.markNow()
|
||||
true
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
log.w(error) { "Device session registration failed" }
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildDeviceRegistrationParams(
|
||||
installationId: String,
|
||||
clientVersion: String,
|
||||
metadata: DeviceClientMetadata,
|
||||
): JsonObject = buildJsonObject {
|
||||
put("p_installation_id", installationId)
|
||||
put("p_client_name", CLIENT_NAME)
|
||||
put("p_client_version", clientVersion)
|
||||
put("p_platform", metadata.platform)
|
||||
put("p_device_name", metadata.deviceName)
|
||||
}
|
||||
|
||||
internal fun formatDeviceName(
|
||||
manufacturer: String,
|
||||
model: String,
|
||||
fallback: String,
|
||||
): String {
|
||||
val normalizedManufacturer = manufacturer.trim()
|
||||
val normalizedModel = model.trim()
|
||||
|
||||
return when {
|
||||
normalizedModel.isBlank() -> fallback
|
||||
normalizedManufacturer.isBlank() -> normalizedModel
|
||||
normalizedModel.startsWith(normalizedManufacturer, ignoreCase = true) -> normalizedModel
|
||||
else -> "$normalizedManufacturer $normalizedModel"
|
||||
}
|
||||
}
|
||||
|
|
@ -15,4 +15,5 @@ expect object AppFeaturePolicy {
|
|||
val heroTrailerPlaybackSupported: Boolean
|
||||
val inAppUpdaterEnabled: Boolean
|
||||
val imdbRatingLogoEnabled: Boolean
|
||||
val mediaPlaybackForegroundServiceEnabled: Boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
|
||||
@Composable
|
||||
fun rememberCardDepthStyleUiState(): CardDepthStyleUiState {
|
||||
CardDepthStyleRepository.ensureLoaded()
|
||||
|
|
@ -77,20 +79,31 @@ fun Modifier.cardDepthVisual(
|
|||
drawContent()
|
||||
val sheenHeight = size.height * 0.22f
|
||||
if (sheenHeight > 0f) {
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = sheen),
|
||||
Color.Transparent,
|
||||
val outline = shape.createOutline(size, layoutDirection, this)
|
||||
val shapePath = outline.toPath()
|
||||
clipPath(shapePath) {
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = sheen),
|
||||
Color.Transparent,
|
||||
),
|
||||
startY = 0f,
|
||||
endY = sheenHeight,
|
||||
),
|
||||
startY = 0f,
|
||||
endY = sheenHeight,
|
||||
),
|
||||
size = Size(size.width, sheenHeight),
|
||||
)
|
||||
size = Size(size.width, sheenHeight),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withEdge
|
||||
}
|
||||
}
|
||||
|
||||
private fun androidx.compose.ui.graphics.Outline.toPath(): androidx.compose.ui.graphics.Path = when (this) {
|
||||
is androidx.compose.ui.graphics.Outline.Rectangle -> androidx.compose.ui.graphics.Path().apply { addRect(rect) }
|
||||
is androidx.compose.ui.graphics.Outline.Rounded -> androidx.compose.ui.graphics.Path().apply { addRoundRect(roundRect) }
|
||||
is androidx.compose.ui.graphics.Outline.Generic -> path
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ import androidx.compose.material.icons.rounded.Notifications
|
|||
import androidx.compose.material.icons.rounded.Palette
|
||||
import androidx.compose.material.icons.rounded.People
|
||||
import androidx.compose.material.icons.rounded.PlayArrow
|
||||
import androidx.compose.material.icons.rounded.Policy
|
||||
import androidx.compose.material.icons.rounded.Tune
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.nuvio.app.core.build.AppVersionConfig
|
||||
|
|
@ -33,6 +35,7 @@ import nuvio.composeapp.generated.resources.compose_settings_page_integrations
|
|||
import nuvio.composeapp.generated.resources.compose_settings_page_licenses_attributions
|
||||
import nuvio.composeapp.generated.resources.compose_settings_page_notifications
|
||||
import nuvio.composeapp.generated.resources.compose_settings_page_playback
|
||||
import nuvio.composeapp.generated.resources.compose_settings_page_privacy_policy
|
||||
import nuvio.composeapp.generated.resources.compose_settings_page_supporters_contributors
|
||||
import nuvio.composeapp.generated.resources.compose_settings_root_account_description
|
||||
import nuvio.composeapp.generated.resources.compose_settings_root_appearance_description
|
||||
|
|
@ -44,6 +47,7 @@ import nuvio.composeapp.generated.resources.compose_settings_root_downloads_titl
|
|||
import nuvio.composeapp.generated.resources.compose_settings_root_general_section
|
||||
import nuvio.composeapp.generated.resources.compose_settings_root_integrations_description
|
||||
import nuvio.composeapp.generated.resources.compose_settings_root_notifications_description
|
||||
import nuvio.composeapp.generated.resources.compose_settings_root_privacy_policy_description
|
||||
import nuvio.composeapp.generated.resources.compose_settings_root_switch_profile_description
|
||||
import nuvio.composeapp.generated.resources.compose_settings_root_switch_profile_title
|
||||
import nuvio.composeapp.generated.resources.compose_settings_root_tracking_description
|
||||
|
|
@ -60,6 +64,8 @@ import nuvio.composeapp.generated.resources.about_supporters_contributors_subtit
|
|||
import nuvio.composeapp.generated.resources.about_licenses_attributions_subtitle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
private const val PRIVACY_POLICY_URL = "https://nuvio.tv/privacy-policy"
|
||||
|
||||
internal fun LazyListScope.settingsRootContent(
|
||||
isTablet: Boolean,
|
||||
onPlaybackClick: () -> Unit,
|
||||
|
|
@ -178,6 +184,7 @@ internal fun LazyListScope.settingsRootContent(
|
|||
}
|
||||
if (showAboutSection) {
|
||||
item {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
SettingsSection(
|
||||
title = stringResource(Res.string.compose_settings_root_about_section),
|
||||
isTablet = isTablet,
|
||||
|
|
@ -193,6 +200,14 @@ internal fun LazyListScope.settingsRootContent(
|
|||
)
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
}
|
||||
SettingsNavigationRow(
|
||||
title = stringResource(Res.string.compose_settings_page_privacy_policy),
|
||||
description = stringResource(Res.string.compose_settings_root_privacy_policy_description),
|
||||
icon = Icons.Rounded.Policy,
|
||||
isTablet = isTablet,
|
||||
onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) },
|
||||
)
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsNavigationRow(
|
||||
title = stringResource(Res.string.compose_settings_page_licenses_attributions),
|
||||
description = stringResource(Res.string.about_licenses_attributions_subtitle),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DeviceSessionRegistrationTest {
|
||||
@Test
|
||||
fun buildsOfficialClientRegistrationPayload() {
|
||||
val params = buildDeviceRegistrationParams(
|
||||
installationId = "nuvio-mobile-installation",
|
||||
clientVersion = "1.2.3",
|
||||
metadata = DeviceClientMetadata(
|
||||
deviceName = "Google Pixel 9",
|
||||
platform = "Android 16",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals("nuvio-mobile-installation", params.getValue("p_installation_id").toString().trim('"'))
|
||||
assertEquals("Nuvio Mobile", params.getValue("p_client_name").toString().trim('"'))
|
||||
assertEquals("1.2.3", params.getValue("p_client_version").toString().trim('"'))
|
||||
assertEquals("Android 16", params.getValue("p_platform").toString().trim('"'))
|
||||
assertEquals("Google Pixel 9", params.getValue("p_device_name").toString().trim('"'))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combinesManufacturerAndModelWithoutRepeatingManufacturer() {
|
||||
assertEquals(
|
||||
"Samsung SM-S918B",
|
||||
formatDeviceName("Samsung", "SM-S918B", "Android device"),
|
||||
)
|
||||
assertEquals(
|
||||
"Google Pixel 9",
|
||||
formatDeviceName("Google", "Google Pixel 9", "Android device"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,4 +10,5 @@ actual object AppFeaturePolicy {
|
|||
actual val heroTrailerPlaybackSupported: Boolean = false
|
||||
actual val inAppUpdaterEnabled: Boolean = false
|
||||
actual val imdbRatingLogoEnabled: Boolean = true
|
||||
actual val mediaPlaybackForegroundServiceEnabled: Boolean = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ actual object AppFeaturePolicy {
|
|||
actual val heroTrailerPlaybackSupported: Boolean = false
|
||||
actual val inAppUpdaterEnabled: Boolean = false
|
||||
actual val imdbRatingLogoEnabled: Boolean = false
|
||||
actual val mediaPlaybackForegroundServiceEnabled: Boolean = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ actual object AppFeaturePolicy {
|
|||
actual val heroTrailerPlaybackSupported: Boolean = false
|
||||
actual val inAppUpdaterEnabled: Boolean = false
|
||||
actual val imdbRatingLogoEnabled: Boolean = true
|
||||
actual val mediaPlaybackForegroundServiceEnabled: Boolean = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import platform.UIKit.UIDevice
|
||||
|
||||
internal actual fun currentDeviceClientMetadata(): DeviceClientMetadata {
|
||||
val device = UIDevice.currentDevice
|
||||
val deviceName = device.name
|
||||
.trim()
|
||||
.ifBlank { device.localizedModel.trim() }
|
||||
.ifBlank { "Apple device" }
|
||||
val platform = "${device.systemName()} ${device.systemVersion}".trim()
|
||||
|
||||
return DeviceClientMetadata(
|
||||
deviceName = deviceName,
|
||||
platform = platform,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
CURRENT_PROJECT_VERSION=100
|
||||
MARKETING_VERSION=0.3.1
|
||||
CURRENT_PROJECT_VERSION=101
|
||||
MARKETING_VERSION=0.3.2
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue