Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/src/main/java/com/theveloper/pixelplay/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import com.theveloper.pixelplay.data.github.GitHubAnnouncementPropertiesService
import com.theveloper.pixelplay.data.github.PlayStoreAnnouncementRemoteConfig
import com.theveloper.pixelplay.data.preferences.AppThemeMode
import com.theveloper.pixelplay.data.preferences.NavBarStyle
import com.theveloper.pixelplay.data.preferences.sanitizeNavBarCornerRadius
import com.theveloper.pixelplay.data.preferences.ThemePreferencesRepository
import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository
import com.theveloper.pixelplay.data.service.MusicService
Expand Down Expand Up @@ -637,7 +638,8 @@ class MainActivity : ComponentActivity() {

val navBarStyle by playerViewModel.navBarStyle.collectAsStateWithLifecycle()
val navBarCompactMode by playerViewModel.navBarCompactMode.collectAsStateWithLifecycle()
val navBarCornerRadius by playerViewModel.navBarCornerRadius.collectAsStateWithLifecycle()
val navBarCornerRadiusRaw by playerViewModel.navBarCornerRadius.collectAsStateWithLifecycle()
val navBarCornerRadius = sanitizeNavBarCornerRadius(navBarCornerRadiusRaw)
val hapticsEnabled by playerViewModel.hapticsEnabled.collectAsStateWithLifecycle()
val rootView = LocalView.current
val platformHapticFeedback = LocalHapticFeedback.current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ object AppThemeMode {
const val DARK = "dark"
}

const val MIN_NAV_BAR_CORNER_RADIUS = 0
const val MAX_NAV_BAR_CORNER_RADIUS = 60

internal fun sanitizeNavBarCornerRadius(radius: Int): Int =
radius.coerceIn(MIN_NAV_BAR_CORNER_RADIUS, MAX_NAV_BAR_CORNER_RADIUS)

/**
* Album art quality settings for developer options.
* Controls maximum resolution for album artwork in player view.
Expand Down Expand Up @@ -1223,12 +1229,12 @@ constructor(

val navBarCornerRadiusFlow: Flow<Int> =
dataStore.data.map { preferences ->
preferences[PreferencesKeys.NAV_BAR_CORNER_RADIUS] ?: 32
sanitizeNavBarCornerRadius(preferences[PreferencesKeys.NAV_BAR_CORNER_RADIUS] ?: 32)
}

suspend fun setNavBarCornerRadius(radius: Int) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.NAV_BAR_CORNER_RADIUS] = radius
preferences[PreferencesKeys.NAV_BAR_CORNER_RADIUS] = sanitizeNavBarCornerRadius(radius)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.theveloper.pixelplay.data.model.Song
import com.theveloper.pixelplay.data.preferences.sanitizeNavBarCornerRadius
import com.theveloper.pixelplay.presentation.components.scoped.PlayerAlbumNavigationEffect
import com.theveloper.pixelplay.presentation.components.scoped.PlayerArtistNavigationEffect
import com.theveloper.pixelplay.presentation.components.scoped.PlayerSheetPredictiveBackHandler
Expand Down Expand Up @@ -204,7 +205,7 @@ fun UnifiedPlayerSheetV2(
val prewarmFullPlayer = rememberPrewarmFullPlayer(infrequentPlayerState.currentSong?.id)

val playerConfig by playerViewModel.playerConfigSlice.collectAsStateWithLifecycle()
val navBarCornerRadius = playerConfig.navBarCornerRadius
val navBarCornerRadius = sanitizeNavBarCornerRadius(playerConfig.navBarCornerRadius)
val navBarStyle = playerConfig.navBarStyle
val carouselStyle = playerConfig.carouselStyle
val fullPlayerLoadingTweaks = playerConfig.fullPlayerLoadingTweaks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import androidx.compose.ui.unit.sp
import androidx.media3.common.util.UnstableApi
import coil.size.Size
import com.theveloper.pixelplay.R
import com.theveloper.pixelplay.data.preferences.sanitizeNavBarCornerRadius
import com.theveloper.pixelplay.presentation.components.OptimizedAlbumArt
import com.theveloper.pixelplay.presentation.components.WavyMusicSlider
import com.theveloper.pixelplay.presentation.components.player.AnimatedPlaybackControls
Expand All @@ -74,7 +75,8 @@ fun ExternalPlayerOverlay(
val playbackPosition by playerViewModel.currentPlaybackPosition.collectAsStateWithLifecycle()
val remotePosition by playerViewModel.remotePosition.collectAsStateWithLifecycle()
val isRemotePlaybackActive by playerViewModel.isRemotePlaybackActive.collectAsStateWithLifecycle()
val navBarCornerRadius by playerViewModel.navBarCornerRadius.collectAsStateWithLifecycle()
val navBarCornerRadiusRaw by playerViewModel.navBarCornerRadius.collectAsStateWithLifecycle()
val navBarCornerRadius = sanitizeNavBarCornerRadius(navBarCornerRadiusRaw)
val currentSong = stablePlayerState.currentSong

var sheetVisible by remember { mutableStateOf(true) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import com.theveloper.pixelplay.R
import com.theveloper.pixelplay.data.model.Song
import com.theveloper.pixelplay.data.preferences.CollagePattern
import com.theveloper.pixelplay.data.stats.PlaybackStatsRepository
import com.theveloper.pixelplay.presentation.components.AlbumArtCollage
import com.theveloper.pixelplay.presentation.components.BetaInfoBottomSheet
import com.theveloper.pixelplay.presentation.components.Beta05CleanInstallDisclaimerDialog
Expand Down Expand Up @@ -229,7 +228,7 @@ fun HomeScreen(
val scope = rememberCoroutineScope()
LocalContext.current

val weeklyStats by statsViewModel.weeklyOverview.collectAsStateWithLifecycle()
val homeStatsOverview by statsViewModel.homeOverview.collectAsStateWithLifecycle()

val listState = rememberLazyListState()
val density = LocalDensity.current
Expand Down Expand Up @@ -407,13 +406,13 @@ fun HomeScreen(
}
}

if (weeklyStats.hasListeningActivity()) {
if (homeStatsOverview != null) {
item(
key = "listening_stats_preview",
contentType = "listening_stats_preview"
) {
StatsOverviewCard(
summary = weeklyStats,
summary = homeStatsOverview,
onClick = { navController.navigateSafely(Screen.Stats.route) }
)
}
Expand Down Expand Up @@ -726,12 +725,3 @@ private fun rememberYourMixTitleStyle(): TextStyle {
)
}
}

private fun PlaybackStatsRepository.PlaybackStatsSummary?.hasListeningActivity(): Boolean {
val summary = this ?: return false
return summary.totalDurationMs > 0L ||
summary.totalPlayCount > 0 ||
summary.uniqueSongs > 0 ||
summary.activeDays > 0 ||
summary.totalSessions > 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.theveloper.pixelplay.R
import com.theveloper.pixelplay.data.preferences.MAX_NAV_BAR_CORNER_RADIUS
import com.theveloper.pixelplay.data.preferences.MIN_NAV_BAR_CORNER_RADIUS
import com.theveloper.pixelplay.presentation.viewmodel.SettingsViewModel
import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape
import com.theveloper.pixelplay.data.preferences.NavBarStyle
Expand Down Expand Up @@ -100,14 +102,17 @@ fun NavBarCornerRadiusContent(
isFullWidth: Boolean,
isCompactMode: Boolean = false
) {
var sliderValue by remember { mutableFloatStateOf(initialRadius) }
fun Float.safeRadius(): Float =
coerceIn(MIN_NAV_BAR_CORNER_RADIUS.toFloat(), MAX_NAV_BAR_CORNER_RADIUS.toFloat())

var sliderValue by remember { mutableFloatStateOf(initialRadius.safeRadius()) }
var hasBeenAdjusted by remember { mutableStateOf(sliderValue != DEFAULT_NAV_BAR_CORNER_RADIUS) }

val haptic = LocalHapticFeedback.current

// Sync if initial value changes externally (though unlikely in this flow, good practice)
LaunchedEffect(initialRadius) {
sliderValue = initialRadius
sliderValue = initialRadius.safeRadius()
}

// Update hasBeenAdjusted when sliderValue changes relative to default
Expand Down Expand Up @@ -136,7 +141,7 @@ fun NavBarCornerRadiusContent(
actions = {
Button(
onClick = {
onRadiusChange(sliderValue.toInt())
onRadiusChange(sliderValue.toInt().coerceIn(MIN_NAV_BAR_CORNER_RADIUS, MAX_NAV_BAR_CORNER_RADIUS))
onDone()
},
colors = ButtonDefaults.buttonColors(
Expand Down Expand Up @@ -233,7 +238,7 @@ fun NavBarCornerRadiusContent(
if (hasBeenAdjusted) {
FilledTonalButton(
onClick = {
sliderValue = DEFAULT_NAV_BAR_CORNER_RADIUS
sliderValue = DEFAULT_NAV_BAR_CORNER_RADIUS.safeRadius()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
},
colors = ButtonDefaults.filledTonalButtonColors(
Expand Down Expand Up @@ -275,10 +280,10 @@ fun NavBarCornerRadiusContent(
if (it.toInt() != sliderValue.toInt()) {
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
}
sliderValue = it
sliderValue = it.safeRadius()
}
},
valueRange = 0f..60f,
valueRange = MIN_NAV_BAR_CORNER_RADIUS.toFloat()..MAX_NAV_BAR_CORNER_RADIUS.toFloat(),
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class StatsViewModel @Inject constructor(
private val _weeklyOverview = MutableStateFlow<PlaybackStatsSummary?>(null)
val weeklyOverview: StateFlow<PlaybackStatsSummary?> = _weeklyOverview.asStateFlow()

private val _homeOverview = MutableStateFlow<PlaybackStatsSummary?>(null)
val homeOverview: StateFlow<PlaybackStatsSummary?> = _homeOverview.asStateFlow()

@Volatile
private var cachedSongs: List<Song>? = null

Expand All @@ -50,6 +53,7 @@ class StatsViewModel @Inject constructor(
showLoading = true,
updateWeeklyOverview = true
)
refreshHomeOverview()
}

fun onRangeSelected(range: StatsTimeRange) {
Expand Down Expand Up @@ -79,6 +83,28 @@ class StatsViewModel @Inject constructor(
}
}

fun refreshHomeOverview() {
viewModelScope.launch {
runCatching {
withContext(Dispatchers.IO) {
val songs = loadSongs()
for (range in HomeOverviewRanges) {
val summary = playbackStatsRepository.loadSummary(range, songs)
if (summary.hasListeningActivity()) {
return@withContext summary
}
}
null
}
}.onSuccess { summary ->
_homeOverview.value = summary
}.onFailure { throwable ->
Timber.e(throwable, "Failed to load home stats overview")
_homeOverview.value = null
}
}
}

private fun refreshRange(
range: StatsTimeRange,
showLoading: Boolean = true,
Expand Down Expand Up @@ -127,6 +153,7 @@ class StatsViewModel @Inject constructor(
if (selectedRange != StatsTimeRange.WEEK) {
refreshWeeklyOverview()
}
refreshHomeOverview()
}
}
}
Expand All @@ -148,4 +175,21 @@ class StatsViewModel @Inject constructor(
cachedSongs = songs
return songs
}

private fun PlaybackStatsSummary.hasListeningActivity(): Boolean {
return totalDurationMs > 0L ||
totalPlayCount > 0 ||
uniqueSongs > 0 ||
activeDays > 0 ||
totalSessions > 0
}

private companion object {
val HomeOverviewRanges = listOf(
StatsTimeRange.WEEK,
StatsTimeRange.MONTH,
StatsTimeRange.YEAR,
StatsTimeRange.ALL
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import org.junit.jupiter.api.extension.ExtensionContext
class MainCoroutineExtension(private val testDispatcher: TestDispatcher = StandardTestDispatcher()) :
BeforeEachCallback, AfterEachCallback {

override fun beforeEach(context: ExtensionContext?) {
override fun beforeEach(context: ExtensionContext) {
Dispatchers.setMain(testDispatcher)
}

override fun afterEach(context: ExtensionContext?) {
override fun afterEach(context: ExtensionContext) {
Dispatchers.resetMain()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,38 @@ class UserPreferencesRepositoryTest {
tempDir.toFile().deleteRecursively()
}
}

@Test
fun `navBarCornerRadiusFlow clamps values outside the supported UI range`() = runTest {
val tempDir = Files.createTempDirectory("user-preferences-repository-test")
try {
val repository = UserPreferencesRepository(
dataStore = PreferenceDataStoreFactory.create(
scope = backgroundScope,
produceFile = { tempDir.resolve("settings.preferences_pb").toFile() }
),
json = Json
)

repository.setNavBarCornerRadius(-1)
assertEquals(MIN_NAV_BAR_CORNER_RADIUS, repository.navBarCornerRadiusFlow.first())

repository.setNavBarCornerRadius(999)
assertEquals(MAX_NAV_BAR_CORNER_RADIUS, repository.navBarCornerRadiusFlow.first())

repository.importPreferencesFromBackup(
entries = listOf(
PreferenceBackupEntry(
key = "nav_bar_corner_radius",
type = "int",
intValue = -1
)
),
clearExisting = false
)
assertEquals(MIN_NAV_BAR_CORNER_RADIUS, repository.navBarCornerRadiusFlow.first())
} finally {
tempDir.toFile().deleteRecursively()
}
}
}
Loading