Skip to content

Add data transfer tools, lyrics improvements, and navigation UX updates#992

Merged
theovilardo merged 2 commits intomasterfrom
feat/data-transfer-lyrics-speed-nav-ux
Feb 5, 2026
Merged

Add data transfer tools, lyrics improvements, and navigation UX updates#992
theovilardo merged 2 commits intomasterfrom
feat/data-transfer-lyrics-speed-nav-ux

Conversation

@lostf1sh
Copy link
Copy Markdown
Collaborator

@lostf1sh lostf1sh commented Feb 5, 2026

Context

This PR bundles a set of quality-of-life and reliability improvements across data transfer, lyrics, navigation, and library UX.
The main goals were:

  • make app data portable (export/import, selective restore)
  • improve lyrics reliability (embedded-first behavior + metadata write-back)
  • reduce perceived latency in lyrics search/fetch
  • improve navigation ergonomics (double-tap search, icon animation, compact-tab looping)

What Changed

1) Data Transfer (Export/Import)

  • Added a backup manager with selective section support:
    • PREFERENCES
    • FAVORITES
    • LYRICS
    • SEARCH_HISTORY
    • TRANSITIONS
  • Added export/import payload model and serialization pipeline.
  • Added bulk DAO methods required for full section dump/restore.
  • Added ViewModel actions and UI wiring (dialogs + document pickers).

Key files:

  • app/src/main/java/com/theveloper/pixelplay/data/backup/AppDataBackupManager.kt
  • app/src/main/java/com/theveloper/pixelplay/data/preferences/PreferenceBackupEntry.kt
  • app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SettingsViewModel.kt
  • app/src/main/java/com/theveloper/pixelplay/presentation/screens/SettingsCategoryScreen.kt
  • DAO updates in FavoritesDao.kt, LyricsDao.kt, SearchHistoryDao.kt, TransitionDao.kt

2) Lyrics Reliability + Metadata Behavior

  • Embedded lyrics are now prioritized consistently before remote fetch paths.
  • Added broader embedded tag support:
    • LYRICS
    • UNSYNCEDLYRICS
  • When remote/manual lyrics are accepted, the app attempts to write them back into file metadata automatically.
  • Added user-facing string for embedded-lyrics short-circuit notification.

Key files:

  • app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/LyricsStateHolder.kt
  • app/src/main/java/com/theveloper/pixelplay/data/media/AudioMetadataReader.kt
  • app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt
  • app/src/main/res/values/strings.xml

3) Lyrics Search Performance

  • Refactored remote lyrics search strategy from sequential fallback to parallel-first-hit logic.
  • Added runSearchStrategiesFast(...) helper to run multiple query forms concurrently and return on first non-empty response batch.
  • Applied to:
    • API fetch path
    • remote search path
    • manual query search path

Expected impact:

  • lower time-to-first-result for lyrics search/fetch in common network conditions
  • fewer user-visible stalls during strategy fallback

Key file:

  • app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt

4) Navigation and Interaction UX

  • Added global haptics preference and app-wide enable/disable propagation.
  • Added bottom-nav selected-icon animation.
  • Added Search nav icon double-tap behavior to quickly activate search.

Key files:

  • app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt
  • app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt
  • app/src/main/java/com/theveloper/pixelplay/MainActivity.kt
  • app/src/main/java/com/theveloper/pixelplay/presentation/components/scoped/CustomNavigationBarItem.kt
  • app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt
  • app/src/main/java/com/theveloper/pixelplay/presentation/screens/SearchScreen.kt

5) Library UX Improvements

  • Restored AI playlist generation action in Library playlist controls.
  • Added infinite sliding behavior for compact-pill library navigation mode.

Key files:

  • app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt
  • app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LibraryActionRow.kt

Validation

Executed:

  • ./gradlew :app:compileDebugKotlin -x lint
  • ./gradlew :app:installDebug -x lint

Notes:

  • Existing unit test suite in repository has pre-existing failures unrelated to this change set; this PR validation focused on compile/install and runtime verification paths for touched features.

Risk / Tradeoffs

  • Lyrics API strategies now run concurrently; this improves responsiveness but can increase short burst request count versus strict sequential fallback.
  • Backup/import touches multiple persistence layers; restore actions are intentionally explicit and section-scoped to reduce accidental overwrite scope.

Scope

  • No DB schema migration introduced.
  • No API contract changes exposed externally.

Copilot AI review requested due to automatic review settings February 5, 2026 21:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a comprehensive data backup/restore system with selectable sections, implements global haptics control, improves lyrics handling with embedded lyrics prioritization and parallel API search, restores AI playlist functionality, and enhances navigation UX with infinite tab sliding and animated bottom nav icons.

Changes:

  • Implemented export/import backup flow supporting preferences, favorites, lyrics, search history, and transition rules with selectable sections
  • Added global haptics toggle with app-wide application through view hierarchy
  • Improved lyrics system by prioritizing embedded lyrics, persisting fetched lyrics to file metadata, and parallelizing API search strategies for faster results
  • Restored AI playlist generation action in library playlist controls
  • Added compact library infinite tab sliding behavior with proper modulo arithmetic for seamless cycling
  • Implemented animated bottom nav icons with scale effects and search double-tap handling to activate search bar

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
strings.xml Added message string for embedded lyrics availability notification
SettingsViewModel.kt Added haptics preference, backup manager integration, and export/import methods with progress tracking
PlayerViewModel.kt Exposed haptics enabled state flow and search nav double-tap event emission
LyricsStateHolder.kt Prioritized embedded lyrics in manual fetch, added metadata persistence, and song update event flows
SettingsCategoryScreen.kt Added backup/restore UI with section selection dialogs and activity result launchers
SearchScreen.kt Integrated double-tap event handling to activate search bar
LibraryScreen.kt Implemented infinite pager logic for compact navigation mode with helper functions
LibraryActionRow.kt Restored AI playlist button alongside import M3U button
CustomNavigationBarItem.kt Changed onClick to always trigger, added icon scale animation for selection
PlayerInternalNavigationBar.kt Added double-tap detection logic for search icon with timestamp tracking
LyricsRepositoryImpl.kt Parallelized search strategies using channels for first non-empty result, added UNSYNCEDLYRICS tag support
UserPreferencesRepository.kt Added haptics preference and backup export/import methods for all preference types
PreferenceBackupEntry.kt New data class for preference backup representation
AudioMetadataReader.kt Added lyrics field extraction from LYRICS and UNSYNCEDLYRICS tags
TransitionDao.kt Added batch insert, get all, and clear all methods for backup support
SearchHistoryDao.kt Added batch insert and get all methods for backup support
LyricsDao.kt Added batch insert and get all methods for backup support
FavoritesDao.kt Added batch insert, get all, and clear all methods for backup support
AppDataBackupManager.kt New manager class implementing JSON-based export/import for selected data sections
MainActivity.kt Applied haptics setting to root view hierarchy in LaunchedEffect
Comments suppressed due to low confidence (1)

app/src/main/java/com/theveloper/pixelplay/presentation/components/PlayerInternalNavigationBar.kt:117

  • The onClick lambda is no longer wrapped in a remember call, which means it will be recreated on every recomposition. This could cause unnecessary recompositions of child components that depend on this lambda. The previous implementation used remember(navController, item.screen.route) to memoize the callback. Consider wrapping this lambda in a remember block with appropriate keys to avoid unnecessary recompositions.
            val onClickLambda: () -> Unit = {
                val isSearchTab = item.screen.route == Screen.Search.route
                val isAlreadySelected = currentRoute == item.screen.route

                if (isSearchTab && isAlreadySelected) {
                    val now = SystemClock.elapsedRealtime()
                    if (now - lastSearchTapTimestamp <= 350L) {
                        onSearchIconDoubleTap()
                        lastSearchTapTimestamp = 0L
                    } else {
                        lastSearchTapTimestamp = now
                    }
                } else if (!isAlreadySelected) {
                    navController.navigate(item.screen.route) {
                        popUpTo(navController.graph.id) { inclusive = true; saveState = false }
                        launchSingleTop = true
                        restoreState = false
                    }
                }
            }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +899 to +907
val sectionLabels = remember {
mapOf(
BackupSection.PREFERENCES to "Preferences and playlists",
BackupSection.FAVORITES to "Favorites",
BackupSection.LYRICS to "Lyrics",
BackupSection.SEARCH_HISTORY to "Search history",
BackupSection.TRANSITIONS to "Transition rules"
)
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backup section labels map is recreated on every recomposition of the AlertDialog. Consider moving this map outside the composable function as a constant or using remember without keys to ensure it's only created once.

Copilot uses AI. Check for mistakes.
Comment on lines +333 to +345
runCatching {
songMetadataEditor.editSongMetadata(
songId = songId,
newTitle = song.title,
newArtist = song.artist,
newAlbum = song.album,
newGenre = song.genre ?: "",
newLyrics = normalizedLyrics,
newTrackNumber = song.trackNumber,
coverArtUpdate = coverArtUpdate
)
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The persistLyricsToFileMetadataIfPossible method silently ignores all errors when writing lyrics to file metadata (using runCatching without error handling). While this might be intentional to prevent blocking the lyrics flow, it makes debugging difficult when metadata writes fail. Consider at least logging failures so users and developers can diagnose issues with file permissions or unsupported file formats.

Copilot uses AI. Check for mistakes.
.fillMaxHeight()
.clickable(
onClick = { if (!selected) onClick() else null },
onClick = onClick,
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The navigation bar's onClick handler was changed from preventing clicks when already selected to always calling onClick. This changes the behavior for all tabs, not just the search tab. For tabs other than search, this means clicking an already-selected tab will now trigger navigation logic unnecessarily. The previous behavior of only navigating when not selected was more efficient. Consider preserving the old behavior for non-search tabs by checking isAlreadySelected for all tabs and only allowing re-clicks for search with double-tap logic.

Suggested change
onClick = onClick,
onClick = {
if (!selected) {
onClick()
}
},

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +160
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
) { uri ->
if (uri != null) {
settingsViewModel.exportAppData(uri, exportSections)
}
}

val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
if (uri != null) {
settingsViewModel.importAppData(uri, importSections)
}
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The export data UI disables the button when a transfer is in progress, but the import launcher could still be invoked before the export completes if the user rapidly switches between dialogs. Consider also checking isDataTransferInProgress before opening the file picker dialogs, or add mutual exclusion between export and import operations to prevent race conditions.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +113
userPreferencesRepository.importPreferencesFromBackup(
entries = it,
clearExisting = true
)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The importPreferencesFromBackup method uses clearExisting = true by default, which means importing any preferences will wipe all existing preferences, not just the ones being imported. This could be unexpected behavior if a user imports a partial backup. Consider either warning users about this in the UI, or implementing selective import that only overwrites preferences that exist in the backup file.

Copilot uses AI. Check for mistakes.
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
var lastSearchTapTimestamp by remember { mutableStateOf(0L) }
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The double-tap state variable lastSearchTapTimestamp is declared inside the forEach loop, which means each navigation item will have its own independent timestamp state. This will cause the double-tap detection to not work correctly when there are multiple navigation items, as the state will be recreated for each item in the loop. The variable should be declared outside the forEach loop to maintain a single shared state for double-tap detection across all navigation items.

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +128
val channel = Channel<RemoteSearchBatch>(capacity = strategies.size)
val jobs = strategies.map { strategy ->
launch {
val responses = runCatching { strategy.request() }
.getOrNull()
?.toList()
.orEmpty()
channel.trySend(
RemoteSearchBatch(
strategyName = strategy.name,
responses = responses
)
)
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parallel lyrics search implementation has a potential race condition. When multiple strategies return results simultaneously, the channel.trySend() calls may fail silently (since trySend returns a result that's not being checked), and the repeat loop will block indefinitely on channel.receive() waiting for results that were never successfully sent. This can cause the search to hang. Consider using channel.send() instead to suspend and ensure all results are delivered, or check the result of trySend() and handle failures appropriately.

Copilot uses AI. Check for mistakes.
Comment on lines +962 to +970
val sectionLabels = remember {
mapOf(
BackupSection.PREFERENCES to "Preferences and playlists",
BackupSection.FAVORITES to "Favorites",
BackupSection.LYRICS to "Lyrics",
BackupSection.SEARCH_HISTORY to "Search history",
BackupSection.TRANSITIONS to "Transition rules"
)
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same section labels map is duplicated in both the export and import dialogs (lines 899-907 and 962-970). This violates the DRY principle and makes maintenance harder. Consider extracting this map to a constant at the file or class level to avoid duplication.

Copilot uses AI. Check for mistakes.
if (showExportDataDialog) {
val sectionLabels = remember {
mapOf(
BackupSection.PREFERENCES to "Preferences and playlists",
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backup section descriptions mention "Preferences and playlists" but the backup manager does not appear to export/import playlist data. The PREFERENCES section only handles user preferences from DataStore. If playlists are stored separately (possibly in a different DAO or repository), they should either be included in the backup or the label should be updated to remove the mention of playlists to avoid misleading users.

Copilot uses AI. Check for mistakes.

LaunchedEffect(hapticsEnabled, rootView) {
rootView.isHapticFeedbackEnabled = hapticsEnabled
rootView.rootView?.isHapticFeedbackEnabled = hapticsEnabled
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting rootView.rootView?.isHapticFeedbackEnabled appears to be redundant since rootView should already be the root view in most cases, and accessing .rootView on it would return the same view. This could also potentially cause confusion or unexpected behavior if the view hierarchy is complex. Consider either removing this line or adding a comment explaining why both are needed.

Suggested change
rootView.rootView?.isHapticFeedbackEnabled = hapticsEnabled

Copilot uses AI. Check for mistakes.
@theovilardo theovilardo merged commit 9f0e4b1 into master Feb 5, 2026
@lostf1sh lostf1sh deleted the feat/data-transfer-lyrics-speed-nav-ux branch February 25, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants