├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── resources.properties
│ │ ├── values-lg
│ │ │ └── strings.xml
│ │ ├── values-th
│ │ │ └── strings.xml
│ │ ├── xml
│ │ │ ├── file_paths.xml
│ │ │ ├── backup_rules.xml
│ │ │ └── data_extraction_rules.xml
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── ic_launcher_background.xml
│ │ │ └── themes.xml
│ │ ├── values-v31
│ │ │ └── themes.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── mipmap-anydpi-v31
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── drawable
│ │ │ ├── ic_song.xml
│ │ │ ├── ic_notification.xml
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── drawable-v31
│ │ │ ├── ic_song.xml
│ │ │ ├── ic_notification.xml
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── values-hi
│ │ │ └── strings.xml
│ │ └── values-ko
│ │ │ └── strings.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ └── pl
│ │ │ └── lambada
│ │ │ └── songsync
│ │ │ ├── util
│ │ │ ├── Exceptions.kt
│ │ │ ├── ext
│ │ │ │ ├── IntExt.kt
│ │ │ │ ├── StringExt.kt
│ │ │ │ ├── ContextExt.kt
│ │ │ │ ├── FileExt.kt
│ │ │ │ └── ComposeExt.kt
│ │ │ ├── ScreenState.kt
│ │ │ ├── networking
│ │ │ │ └── Ktor.kt
│ │ │ ├── ui
│ │ │ │ ├── MotionConstants.kt
│ │ │ │ └── AnimationSpecs.kt
│ │ │ ├── ResourceState.kt
│ │ │ ├── MiscelaneousUtils.kt
│ │ │ └── DataStorePreferences.kt
│ │ │ ├── services
│ │ │ └── NotificationListener.kt
│ │ │ ├── domain
│ │ │ └── model
│ │ │ │ ├── lyrics_providers
│ │ │ │ ├── spotify
│ │ │ │ │ ├── SpotifySyncedLyricsApi.kt
│ │ │ │ │ ├── AccessToken.kt
│ │ │ │ │ ├── WebPlayerToken.kt
│ │ │ │ │ └── SpotifyApi.kt
│ │ │ │ ├── others
│ │ │ │ │ ├── LRCLib.kt
│ │ │ │ │ ├── Netease.kt
│ │ │ │ │ ├── QQMusic.kt
│ │ │ │ │ ├── Musixmatch.kt
│ │ │ │ │ └── Apple.kt
│ │ │ │ └── PaxMusicFormat.kt
│ │ │ │ ├── Release.kt
│ │ │ │ ├── Song.kt
│ │ │ │ ├── Sort.kt
│ │ │ │ └── SongInfo.kt
│ │ │ ├── ui
│ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Type.kt
│ │ │ │ └── Theme.kt
│ │ │ ├── components
│ │ │ │ ├── dropdown
│ │ │ │ │ └── M3ElevationTokens.kt
│ │ │ │ ├── SettingsHeadLabel.kt
│ │ │ │ ├── TextField.kt
│ │ │ │ ├── dialogs
│ │ │ │ │ └── NoInternetDialog.kt
│ │ │ │ └── SwitchItem.kt
│ │ │ ├── screens
│ │ │ │ ├── settings
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── ShowPathSwitch.kt
│ │ │ │ │ │ ├── MarqueeSwitch.kt
│ │ │ │ │ │ ├── PureBlackThemeSwitch.kt
│ │ │ │ │ │ ├── SyncedLyricsSwitch.kt
│ │ │ │ │ │ ├── OffsetModeSwitch.kt
│ │ │ │ │ │ ├── MultiPersonSwitch.kt
│ │ │ │ │ │ ├── TranslationSwitch.kt
│ │ │ │ │ │ ├── RomanizationSwitch.kt
│ │ │ │ │ │ ├── ExternalLinkSection.kt
│ │ │ │ │ │ ├── TranslationSection.kt
│ │ │ │ │ │ ├── AppInfoSection.kt
│ │ │ │ │ │ ├── SupportSection.kt
│ │ │ │ │ │ ├── ContributorsSection.kt
│ │ │ │ │ │ ├── UpdateAvailableDialog.kt
│ │ │ │ │ │ ├── Utils.kt
│ │ │ │ │ │ ├── SettingsScreenTopBar.kt
│ │ │ │ │ │ ├── CreditsSection.kt
│ │ │ │ │ │ └── SdCardPathSetting.kt
│ │ │ │ │ └── SettingsViewModel.kt
│ │ │ │ ├── init
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── InitTopBar.kt
│ │ │ │ │ │ ├── permissions
│ │ │ │ │ │ │ ├── PostNotifications.kt
│ │ │ │ │ │ │ ├── NotificationPermission.kt
│ │ │ │ │ │ │ └── AllFilesAccess.kt
│ │ │ │ │ │ └── PermissionItem.kt
│ │ │ │ │ └── InitScreenViewModel.kt
│ │ │ │ ├── lyricsFetch
│ │ │ │ │ └── components
│ │ │ │ │ │ ├── NoConnectionDialogue.kt
│ │ │ │ │ │ ├── CloudProviderTitle.kt
│ │ │ │ │ │ ├── FailedDialogue.kt
│ │ │ │ │ │ ├── NotSubmittedContent.kt
│ │ │ │ │ │ ├── LocalSongContent.kt
│ │ │ │ │ │ └── LanguageSelector.kt
│ │ │ │ └── home
│ │ │ │ │ └── components
│ │ │ │ │ ├── batchDownload
│ │ │ │ │ ├── RateLimitedDialog.kt
│ │ │ │ │ ├── LegacyPromptDialog.kt
│ │ │ │ │ ├── DownloadCompleteDialog.kt
│ │ │ │ │ ├── DownloadProgressDialog.kt
│ │ │ │ │ └── BatchDownloadWarningDialog.kt
│ │ │ │ │ ├── HomeSearchThing.kt
│ │ │ │ │ ├── FilterAndSongCount.kt
│ │ │ │ │ ├── BatchDownloadLyrics.kt
│ │ │ │ │ └── HomeSearchBar.kt
│ │ │ ├── common
│ │ │ │ └── ComposableAnimations.kt
│ │ │ └── Navigator.kt
│ │ │ ├── data
│ │ │ └── remote
│ │ │ │ ├── github
│ │ │ │ └── GithubAPI.kt
│ │ │ │ ├── lyrics_providers
│ │ │ │ ├── spotify
│ │ │ │ │ └── SpotifyLyricsAPI.kt
│ │ │ │ ├── apple
│ │ │ │ │ └── AppleTokenManager.kt
│ │ │ │ └── others
│ │ │ │ │ ├── LRCLibAPI.kt
│ │ │ │ │ └── QQMusicAPI.kt
│ │ │ │ ├── UpdateService.kt
│ │ │ │ └── PaxMusicHelper.kt
│ │ │ └── activities
│ │ │ └── quicksearch
│ │ │ ├── viewmodel
│ │ │ └── QuickLyricsSearchViewModelFactory.kt
│ │ │ └── components
│ │ │ ├── SyncedLyricsLine.kt
│ │ │ ├── SquaredButton.kt
│ │ │ ├── QuickLyricsSongInfo.kt
│ │ │ └── ExpandableOutlinedCard.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── screenshots
├── screenshot1.png
├── screenshot2.png
├── screenshot3.png
├── screenshot4.png
└── screenshot5.png
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── short_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── screenshot1.jpg
│ │ ├── screenshot2.jpg
│ │ ├── screenshot3.jpg
│ │ ├── screenshot4.jpg
│ │ └── screenshot5.jpg
│ └── full_description.txt
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .idea
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── compiler.xml
├── kotlinc.xml
├── discord.xml
├── deploymentTargetDropDown.xml
├── migrations.xml
├── deploymentTargetSelector.xml
├── misc.xml
├── gradle.xml
├── runConfigurations.xml
├── appInsightsSettings.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── .vscode
└── settings.json
├── .gitignore
├── settings.gradle.kts
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── ci.yml
├── gradle.properties
├── README.md
└── gradlew.bat
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
3 |
--------------------------------------------------------------------------------
/app/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale = en-US
--------------------------------------------------------------------------------
/screenshots/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/screenshots/screenshot1.png
--------------------------------------------------------------------------------
/screenshots/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/screenshots/screenshot2.png
--------------------------------------------------------------------------------
/screenshots/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/screenshots/screenshot3.png
--------------------------------------------------------------------------------
/screenshots/screenshot4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/screenshots/screenshot4.png
--------------------------------------------------------------------------------
/screenshots/screenshot5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/screenshots/screenshot5.png
--------------------------------------------------------------------------------
/app/src/main/res/values-lg/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-th/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Android app to download lyrics for songs in your music library.
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
5 | deploymentTargetDropDown.xml
6 | deploymentTargetSelector.xml
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "githubPullRequests.ignoredPullRequestBranches": ["master"],
3 | "java.configuration.updateBuildConfiguration": "automatic"
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #EADDFF
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot1.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot2.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot3.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot4.jpg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lambada10/SongSync/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot5.jpg
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/Exceptions.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util
2 |
3 | class NoTrackFoundException : Exception()
4 | class InternalErrorException(msg: String) : Exception(msg)
5 | class EmptyQueryException : Exception()
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/services/NotificationListener.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.services
2 |
3 | import android.service.notification.NotificationListenerService
4 |
5 | // Required for msm.getActiveSessions()
6 | class NotificationListener : NotificationListenerService()
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Apr 28 09:48:17 IRST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v31/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | .kotlin
17 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/spotify/SpotifySyncedLyricsApi.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.spotify
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class SyncedLinesResponse(
7 | val lyrics: String,
8 | val isError: Boolean
9 | )
10 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/components/dropdown/M3ElevationTokens.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.components.dropdown
2 |
3 | /**
4 | * The tonal elevation tokens.
5 | *
6 | * @see androidx.compose.material3.tokens.ElevationTokens
7 | */
8 | internal object ElevationTokens {
9 | const val Level0 = 0
10 | const val Level1 = 1
11 | const val Level2 = 3
12 | const val Level3 = 6
13 | const val Level4 = 8
14 | const val Level5 = 12
15 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 |
SongSync is a simple Android app to download lyrics for songs in your music library.
Features:
- Download lyrics for whole music library with a single click
- Download lyrics for individual songs in your music library
- Embed lyrics directly to songs
- Download lyrics from various providers
- Search for lyrics for songs not in your music library (and download them)
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | maven("https://jitpack.io")
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven("https://jitpack.io")
15 | }
16 | }
17 |
18 | rootProject.name = "SongSync"
19 | include(":app")
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/spotify/AccessToken.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.spotify
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class AccessTokenResponse(
8 | @SerialName("access_token")
9 | val accessToken: String,
10 | @SerialName("token_type")
11 | val tokenType: String,
12 | @SerialName("expires_in")
13 | val expiresIn: Int
14 | )
15 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/spotify/WebPlayerToken.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.spotify
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class ServerTimeResponse(
7 | val serverTime: Long,
8 | )
9 |
10 | @Serializable
11 | data class WebPlayerTokenResponse(
12 | val clientId: String,
13 | val accessToken: String,
14 | val accessTokenExpirationTimestampMs: Long,
15 | val isAnonymous: Boolean,
16 | )
17 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/LRCLib.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.others
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class LRCLibResponse(
7 | val id: Int,
8 | val name: String,
9 | val trackName: String,
10 | val artistName: String,
11 | val albumName: String,
12 | val duration: Double,
13 | val instrumental: Boolean,
14 | val plainLyrics: String?,
15 | val syncedLyrics: String?
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/Release.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 |
8 | @Parcelize
9 | @Serializable
10 | data class Release(
11 | @SerialName("html_url")
12 | val htmlURL: String,
13 | @SerialName("tag_name")
14 | val tagName: String,
15 | @SerialName("body")
16 | val changelog: String? = null
17 | ) : Parcelable
18 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/ext/IntExt.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util.ext
2 |
3 | fun Int.toLrcTimestamp(): String {
4 | val minutes = this / 60000
5 | val seconds = (this % 60000) / 1000
6 | val milliseconds = this % 1000
7 |
8 | val leadingZeros: Array = arrayOf(
9 | if (minutes < 10) "0" else "",
10 | if (seconds < 10) "0" else "",
11 | if (milliseconds < 10) "00" else if (milliseconds < 100) "0" else ""
12 | )
13 |
14 | return "${leadingZeros[0]}$minutes:${leadingZeros[1]}$seconds.${leadingZeros[2]}$milliseconds"
15 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/Song.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model
2 |
3 | import android.net.Uri
4 | import android.os.Parcelable
5 | import kotlinx.parcelize.Parcelize
6 |
7 | /**
8 | * Data class for representing a song.
9 | * @param title The title of the song.
10 | * @param artist The artist of the song.
11 | * @param imgUri The URI of the image.
12 | * @param filePath The file path of the song.
13 | */
14 | @Parcelize
15 | data class Song(
16 | val title: String?,
17 | val artist: String?,
18 | val imgUri: Uri?,
19 | val filePath: String?
20 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | )
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/ShowPathSwitch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import pl.lambada.songsync.R
6 | import pl.lambada.songsync.ui.components.SwitchItem
7 |
8 | @Composable
9 | fun ShowPathSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
10 | SwitchItem(
11 | label = stringResource(R.string.song_path),
12 | description = stringResource(R.string.song_path_description),
13 | selected = selected,
14 | onClick = { onToggle(!selected) }
15 | )
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/MarqueeSwitch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import pl.lambada.songsync.R
6 | import pl.lambada.songsync.ui.components.SwitchItem
7 |
8 |
9 | @Composable
10 | fun MarqueeSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
11 | SwitchItem(
12 | label = stringResource(R.string.disable_marquee),
13 | description = stringResource(R.string.disable_marquee_summary),
14 | selected = selected,
15 | onClick = { onToggle(!selected) }
16 | )
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/PureBlackThemeSwitch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import pl.lambada.songsync.R
6 | import pl.lambada.songsync.ui.components.SwitchItem
7 |
8 | @Composable
9 | fun PureBlackThemeSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
10 | SwitchItem(
11 | label = stringResource(R.string.pure_black_theme),
12 | description = stringResource(R.string.pure_black_theme_summary),
13 | selected = selected,
14 | onClick = { onToggle(!selected) }
15 | )
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/SyncedLyricsSwitch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import pl.lambada.songsync.R
6 | import pl.lambada.songsync.ui.components.SwitchItem
7 |
8 | @Composable
9 | fun SyncedLyricsSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
10 | SwitchItem(
11 | label = stringResource(id = R.string.synced_lyrics),
12 | description = stringResource(id = R.string.synced_lyrics_summary),
13 | selected = selected,
14 | onClick = { onToggle(!selected) }
15 | )
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/OffsetModeSwitch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import pl.lambada.songsync.R
6 | import pl.lambada.songsync.ui.components.SwitchItem
7 |
8 | @Composable
9 | fun OffsetModeSwitch(
10 | selected: Boolean,
11 | onToggle: (Boolean) -> Unit
12 | ) {
13 | SwitchItem(
14 | label = stringResource(R.string.offset_mode),
15 | description = stringResource(R.string.offset_mode_summary),
16 | selected = selected,
17 | onClick = { onToggle(!selected) }
18 | )
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/MultiPersonSwitch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import pl.lambada.songsync.R
6 | import pl.lambada.songsync.ui.components.SwitchItem
7 |
8 | @Composable
9 | fun MultiPersonSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
10 | SwitchItem(
11 | label = stringResource(id = R.string.multi_person_word_by_word),
12 | description = stringResource(id = R.string.multi_person_word_by_word_summary2),
13 | selected = selected,
14 | onClick = { onToggle(!selected) }
15 | )
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/TranslationSwitch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import pl.lambada.songsync.R
6 | import pl.lambada.songsync.ui.components.SwitchItem
7 |
8 |
9 | @Composable
10 | fun TranslationSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
11 | SwitchItem(
12 | label = stringResource(id = R.string.include_translation),
13 | description = stringResource(id = R.string.include_translation_summary),
14 | selected = selected,
15 | onClick = { onToggle(!selected) }
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: enhancement
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/RomanizationSwitch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import pl.lambada.songsync.R
6 | import pl.lambada.songsync.ui.components.SwitchItem
7 |
8 | @Composable
9 | fun RomanizationSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
10 | SwitchItem(
11 | label = stringResource(id = R.string.include_romanization),
12 | description = stringResource(id = R.string.include_romanization_summary),
13 | selected = selected,
14 | onClick = { onToggle(!selected) }
15 | )
16 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/init/components/InitTopBar.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.init.components
2 |
3 | import androidx.compose.material3.ExperimentalMaterial3Api
4 | import androidx.compose.material3.LargeTopAppBar
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 | import pl.lambada.songsync.R
9 |
10 | @OptIn(ExperimentalMaterial3Api::class)
11 | @Composable
12 | fun InitScreenTopBar() {
13 | LargeTopAppBar(
14 | title = {
15 | Text(
16 | text = stringResource(id = R.string.init_screen_title)
17 | )
18 | }
19 | )
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_song.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/ext/StringExt.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util.ext
2 |
3 | import java.io.File
4 | import java.util.Locale
5 |
6 | fun String.lowercaseWithLocale(): String {
7 | return this.lowercase(Locale.getDefault())
8 | }
9 |
10 | fun String?.toLrcFile(): File? {
11 | if (this == null) return null
12 | val idx = lastIndexOf('.')
13 | return File(
14 | substring(
15 | 0,
16 | if (idx == -1) length else idx
17 | ) + ".lrc"
18 | )
19 | }
20 |
21 | inline fun > String?.toEnum(defaultValue: T): T =
22 | if (this == null) defaultValue
23 | else try {
24 | enumValueOf(this)
25 | } catch (e: IllegalArgumentException) {
26 | defaultValue
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/ext/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util.ext
2 |
3 | import android.content.Context
4 | import android.content.pm.PackageInfo
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 |
8 | /**
9 | * Extension function to get the version name of the application.
10 | *
11 | * @return The version name as a [String].
12 | */
13 | fun Context.getVersion(): String {
14 | val pInfo: PackageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
15 | packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
16 | } else {
17 | @Suppress("deprecation")
18 | packageManager.getPackageInfo(packageName, 0)
19 | }
20 | return pInfo.versionName.toString()
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/PaxMusicFormat.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class PaxResponse(
7 | val type: String,
8 | val content: List?
9 | )
10 |
11 | @Serializable
12 | data class PaxLyrics(
13 | val text: List,
14 | val timestamp: Int,
15 | val oppositeTurn: Boolean,
16 | val background: Boolean,
17 | val backgroundText: List,
18 | val endtime: Int
19 | )
20 |
21 | @Serializable
22 | data class PaxLyricsLineDetails(
23 | val text: String,
24 | val part: Boolean,
25 | val timestamp: Int?,
26 | val endtime: Int?
27 | )
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/ext/FileExt.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util.ext
2 |
3 | import android.util.Log
4 | import java.io.File
5 |
6 | /**
7 | * Replace all characters in the file name that are not allowed to exist in a file name with an underscore.
8 | *
9 | * @return The sanitized [File] instance.
10 | */
11 | fun File.sanitize(): File {
12 | return File(this.parent, this.name.replace(Regex("[/\\\\:*?\"<>|\\t\\n]"), "_"))
13 | }
14 |
15 | /**
16 | * Converts the file path to an LRC file path.
17 | *
18 | * @return The [File] instance with the LRC extension.
19 | */
20 | fun String.toLrcFile(): File? {
21 | return if (this.isNotEmpty()) {
22 | File(this.substringBeforeLast('.') + ".lrc")
23 | } else {
24 | null
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v31/ic_song.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/components/SettingsHeadLabel.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.components
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.dp
9 |
10 | @Composable
11 | fun SettingsHeadLabel(
12 | label: String,
13 | ) {
14 | Text(
15 | text = label,
16 | style = MaterialTheme.typography.labelMedium,
17 | modifier = Modifier.padding(
18 | start = 22.dp,
19 | top = 22.dp,
20 | end = 22.dp,
21 | bottom = 0.dp
22 | ),
23 | color = MaterialTheme.colorScheme.primary
24 | )
25 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/components/NoConnectionDialogue.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.lyricsFetch.components
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.Button
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 | import pl.lambada.songsync.R
9 |
10 | @Composable
11 | fun NoConnectionDialogue(
12 | onDismissRequest: () -> Unit,
13 | onOkRequest: () -> Unit
14 | ) {
15 | AlertDialog(
16 | onDismissRequest = onDismissRequest,
17 | confirmButton = { Button(onOkRequest) { Text(stringResource(R.string.ok)) } },
18 | title = { Text(text = stringResource(id = R.string.error)) },
19 | text = { Text(stringResource(R.string.no_internet_server)) }
20 | )
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_notification.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values-hi/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | इस गाने के बोल उपलब्ध नहीं हैं
4 | गीतों के बोल समूह में डाउनलोड
5 | गीतों के बोल प्राप्त करें
6 | फिर से प्रयास करें
7 | रद्द करें
8 | एरर
9 | ओके
10 | नहीं
11 | गीतों के बोल डाउनलोड हो रहे हैं
12 | गीत: %1$s
13 | कृपया ऐप बंद न करें, इसमें कुछ समय लग सकता है
14 | हाँ
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/data/remote/github/GithubAPI.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.data.remote.github
2 |
3 | import io.ktor.client.request.get
4 | import io.ktor.client.statement.bodyAsText
5 | import pl.lambada.songsync.domain.model.Release
6 | import pl.lambada.songsync.util.networking.Ktor.client
7 | import pl.lambada.songsync.util.networking.Ktor.json
8 |
9 |
10 | object GithubAPI {
11 | private const val BASE_URL = "https://api.github.com/"
12 |
13 | /**
14 | * Gets latest GitHub release information.
15 | * @return The latest release version.
16 | */
17 | suspend fun getLatestRelease(): Release {
18 | val response = client.get(BASE_URL + "repos/Lambada10/SongSync/releases/latest")
19 | val responseBody = response.bodyAsText(Charsets.UTF_8)
20 |
21 | return json.decodeFromString(responseBody)
22 | }
23 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: bug
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Smartphone (please complete the following information):**
27 |
28 | - Device: [e.g. Samsung Galaxy S10, Poco X3 Pro]
29 | - OS: [e.g. MIUI 13 (Android 12), Pixel Experience 13]
30 | - App version [e.g. v1.3]
31 |
32 | **Additional context**
33 | Add any other context about the problem here.
34 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v31/ic_notification.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/ScreenState.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util
2 |
3 | /**
4 | * A sealed class representing the state of a screen.
5 | *
6 | * @param T The type of data associated with the success state.
7 | */
8 | sealed class ScreenState {
9 | /**
10 | * Represents a loading state.
11 | */
12 | data object Loading : ScreenState()
13 |
14 | /**
15 | * Represents a success state with optional data.
16 | *
17 | * @param T The type of data.
18 | * @property data The data associated with the success state.
19 | */
20 | data class Success(val data: T?) : ScreenState()
21 |
22 | /**
23 | * Represents an error state with an exception.
24 | *
25 | * @property exception The exception associated with the error state.
26 | */
27 | data class Error(val exception: Throwable) : ScreenState()
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v31/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/common/ComposableAnimations.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.common
2 |
3 | import androidx.compose.animation.ContentTransform
4 | import androidx.compose.animation.SizeTransform
5 | import pl.lambada.songsync.util.ui.materialSharedAxisXIn
6 | import pl.lambada.songsync.util.ui.materialSharedAxisXOut
7 | import pl.lambada.songsync.util.ui.materialSharedAxisYIn
8 | import pl.lambada.songsync.util.ui.materialSharedAxisYOut
9 |
10 | val AnimatedTextContentTransformation = ContentTransform(
11 | materialSharedAxisXIn(initialOffsetX = { it / 10 }),
12 | materialSharedAxisXOut(targetOffsetX = { -it / 10 }),
13 | sizeTransform = SizeTransform(clip = false)
14 | )
15 |
16 | val AnimatedCardContentTransformation = ContentTransform(
17 | materialSharedAxisYIn(initialOffsetY = { it / 10 }),
18 | materialSharedAxisYOut(targetOffsetY = { -it / 10 }),
19 | sizeTransform = SizeTransform(clip = false)
20 | )
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Netease.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.others
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class NeteaseResponse(
7 | val result: NeteaseResult,
8 | val code: Int
9 | )
10 |
11 | @Serializable
12 | data class NeteaseResult(
13 | val songs: List,
14 | val songCount: Int
15 | )
16 |
17 | @Serializable
18 | data class NeteaseSong(
19 | val name: String,
20 | val id: Long,
21 | val artists: List,
22 | )
23 |
24 | @Serializable
25 | data class NeteaseArtist(
26 | val name: String
27 | )
28 |
29 | @Serializable
30 | data class NeteaseLyricsResponse(
31 | val lrc: NeteaseLyrics,
32 | val tlyric: NeteaseLyrics?,
33 | val romalrc: NeteaseLyrics?,
34 | val code: Int
35 | )
36 |
37 | @Serializable
38 | data class NeteaseLyrics(
39 | val lyric: String
40 | )
41 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ko/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 가사 다중 다운로드
4 | 가사 가져오기
5 | 다시 시도
6 | 취소
7 | 오류
8 | 이 노래에는 가사가 없습니다
9 | 확인
10 | 네
11 | 아니오
12 | 가사 다운로드 중
13 | 노래: %1$s
14 | 진행도: %1$d/%2$d (%3$d%%)
15 | 성공: %1$d, 가사 없음: %2$d, 실패: %3$d
16 | 앱을 닫지 말고 잠시만 기다려 주세요
17 | 다운로드 완료
18 | 성공: %1$d
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/activities/quicksearch/viewmodel/QuickLyricsSearchViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.activities.quicksearch.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import pl.lambada.songsync.data.UserSettingsController
6 | import pl.lambada.songsync.data.remote.lyrics_providers.LyricsProviderService
7 |
8 | class QuickLyricsSearchViewModelFactory(
9 | private val userSettingsController: UserSettingsController,
10 | private val lyricsProviderService: LyricsProviderService
11 | ) : ViewModelProvider.Factory {
12 | override fun create(modelClass: Class): T {
13 | if (modelClass.isAssignableFrom(QuickLyricsSearchViewModel::class.java)) {
14 | @Suppress("UNCHECKED_CAST")
15 | return QuickLyricsSearchViewModel(userSettingsController, lyricsProviderService) as T
16 | }
17 | throw IllegalArgumentException("Unknown ViewModel class")
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/Sort.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model
2 |
3 | import androidx.annotation.StringRes
4 | import pl.lambada.songsync.R
5 |
6 | /**
7 | * Enum class representing sort orders.
8 | * @param queryName The query name for the sort order.
9 | * @param displayName The string resource ID for the display name.
10 | */
11 | enum class SortOrders(val queryName: String, @StringRes val displayName: Int) {
12 | ASCENDING("ASC", R.string.ascending),
13 | DESCENDING("DESC", R.string.descending),
14 | }
15 |
16 | /**
17 | * Enum class representing sort values.
18 | * @param displayName The string resource ID for the display name.
19 | */
20 | enum class SortValues(@StringRes val displayName: Int) {
21 | TITLE(R.string.title),
22 | ARTIST(R.string.artist),
23 | ALBUM(R.string.album),
24 | YEAR(R.string.year),
25 | DURATION(R.string.duration),
26 | DATE_ADDED(R.string.date_added),
27 | DATE_MODIFIED(R.string.date_modified),
28 | }
29 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/networking/Ktor.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util.networking
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.cio.CIO
5 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
6 | import io.ktor.serialization.kotlinx.json.json
7 | import kotlinx.serialization.json.Json
8 |
9 | object Ktor {
10 | val client = HttpClient(CIO.create {
11 | // Here goes all the Engine config
12 | // TODO: Add proxy support
13 | // proxy = ProxyConfig(
14 | // type = Proxy.Type.SOCKS,
15 | // sa = java.net.InetSocketAddress(3030)
16 | // )
17 | }) {
18 | // In case of adding plugins, add them here
19 | install(ContentNegotiation) {
20 | json(Json {
21 | ignoreUnknownKeys = true
22 | encodeDefaults = true
23 | })
24 | }
25 | }
26 |
27 | val json = Json {
28 | ignoreUnknownKeys = true
29 | encodeDefaults = true
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/QQMusic.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.others
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class QQMusicSearchResponse(
7 | val data: QQMusicData
8 | )
9 |
10 | @Serializable
11 | data class QQMusicData(
12 | val song: QQMusicSong
13 | )
14 |
15 | @Serializable
16 | data class QQMusicSong(
17 | val list: List
18 | )
19 |
20 | @Serializable
21 | data class QQMusicSongInfo(
22 | val title: String,
23 | val singer: List,
24 | val album: QQMusicAlbum,
25 | val id: Long
26 | )
27 |
28 | @Serializable
29 | data class QQMusicSinger(
30 | val name: String
31 | )
32 |
33 | @Serializable
34 | data class QQMusicAlbum(
35 | val name: String
36 | )
37 |
38 | @Serializable
39 | data class PaxQQPayload(
40 | val artist: List,
41 | val album: String,
42 | val id: Long,
43 | val title: String
44 | )
--------------------------------------------------------------------------------
/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. For more details, visit
11 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
12 | # org.gradle.parallel=true
13 | #Wed May 22 20:46:42 IRST 2024
14 | android.nonFinalResIds=false
15 | android.nonTransitiveRClass=true
16 | android.useAndroidX=true
17 | kotlin.code.style=official
18 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8 --add-opens=java.base/java.lang=ALL-UNNAMED
19 | org.gradle.unsafe.configuration-cache=true
20 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/init/components/permissions/PostNotifications.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.init.components.permissions
2 |
3 | import android.os.Build
4 | import androidx.annotation.RequiresApi
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.LaunchedEffect
7 | import com.google.accompanist.permissions.ExperimentalPermissionsApi
8 | import com.google.accompanist.permissions.rememberPermissionState
9 |
10 | @RequiresApi(Build.VERSION_CODES.TIRAMISU)
11 | @OptIn(ExperimentalPermissionsApi::class)
12 | @Composable
13 | fun PostNotifications(
14 | onGranted: () -> Unit,
15 | onDismiss: () -> Unit
16 | ) {
17 | var notificationPermission = rememberPermissionState(
18 | permission = android.Manifest.permission.POST_NOTIFICATIONS,
19 | onPermissionResult = {
20 | if (it) {
21 | onGranted()
22 | }
23 | onDismiss()
24 | }
25 | )
26 | LaunchedEffect(Unit) {
27 | notificationPermission.launchPermissionRequest()
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/components/TextField.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.components
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.foundation.text.KeyboardOptions
5 | import androidx.compose.material3.OutlinedTextField
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.text.input.ImeAction
10 | import androidx.compose.ui.unit.dp
11 |
12 | @Composable
13 | fun CommonTextField(
14 | modifier: Modifier = Modifier,
15 | value: String = "",
16 | onValueChange: (String) -> Unit = {},
17 | label: String = "",
18 | singleLine: Boolean = true,
19 | imeAction: ImeAction = ImeAction.Done,
20 | readOnly: Boolean = false,
21 | ) {
22 | OutlinedTextField(
23 | value = value,
24 | onValueChange = onValueChange,
25 | label = { Text(text = label) },
26 | singleLine = singleLine,
27 | shape = RoundedCornerShape(10.dp),
28 | keyboardOptions = KeyboardOptions(imeAction = imeAction),
29 | readOnly = readOnly,
30 | modifier = modifier,
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.others
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class MusixmatchSearchResponse(
7 | val id: Long,
8 | val songName: String,
9 | val artistName: String,
10 | val albumName: String,
11 | val artwork: String,
12 | val releaseDate: String,
13 | val duration: Int,
14 | val url: String,
15 | val albumId: Long,
16 | val hasSyncedLyrics: Boolean,
17 | val hasUnsyncedLyrics: Boolean,
18 | val availableLanguages: List = emptyList(),
19 | val originalLanguage: String? = null,
20 | val syncedLyrics: SyncedLyricsResponse? = null,
21 | val unsyncedLyrics: UnsyncedLyricsResponse? = null
22 | )
23 |
24 | @Serializable
25 | data class SyncedLyricsResponse(
26 | val id: Long,
27 | val duration: Int,
28 | val language: String,
29 | val updatedTime: String,
30 | val lyrics: String
31 | )
32 |
33 | @Serializable
34 | data class UnsyncedLyricsResponse(
35 | val id: Long,
36 | val language: String,
37 | val updatedTime: String,
38 | val lyrics: String,
39 | )
40 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/batchDownload/RateLimitedDialog.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.home.components.batchDownload
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Button
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.res.stringResource
9 | import pl.lambada.songsync.R
10 |
11 | @Composable
12 | fun RateLimitedDialog(onDismiss: () -> Unit) {
13 | AlertDialog(
14 | title = {
15 | Text(text = stringResource(id = R.string.batch_download_lyrics))
16 | },
17 | text = {
18 | Column {
19 | Text(text = stringResource(R.string.spotify_api_rate_limit_reached))
20 | Text(text = stringResource(R.string.please_try_again_later))
21 | Text(text = stringResource(R.string.change_api_strategy))
22 | }
23 | },
24 | onDismissRequest = onDismiss,
25 | confirmButton = {
26 | Button(onClick = onDismiss) {
27 | Text(text = stringResource(id = R.string.ok))
28 | }
29 | }
30 | )
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/ui/MotionConstants.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util.ui
2 |
3 | /*
4 | * Copyright 2021 SOUP
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * https://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 |
20 | import androidx.compose.ui.unit.Dp
21 | import androidx.compose.ui.unit.dp
22 |
23 | object MotionConstants {
24 | const val DefaultMotionDuration: Int = 300
25 | const val DefaultFadeInDuration: Int = 150
26 | const val DefaultFadeOutDuration: Int = 75
27 | val DefaultSlideDistance: Dp = 30.dp
28 |
29 | const val DURATION = 600
30 | const val DURATION_ENTER = 400
31 | const val DURATION_ENTER_SHORT = 300
32 | const val DURATION_EXIT = 200
33 | const val DURATION_EXIT_SHORT = 100
34 |
35 | const val InitialOffset = 0.10f
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/batchDownload/LegacyPromptDialog.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.home.components.batchDownload
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Button
6 | import androidx.compose.material3.OutlinedButton
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.res.stringResource
10 | import pl.lambada.songsync.R
11 |
12 | @Composable
13 | fun LegacyPromptDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
14 | AlertDialog(
15 | title = {
16 | Text(text = stringResource(id = R.string.batch_download_lyrics))
17 | },
18 | text = {
19 | Column {
20 | Text(text = stringResource(R.string.set_sd_path_warn))
21 | }
22 | },
23 | onDismissRequest = onDismiss,
24 | confirmButton = {
25 | Button(onClick = onConfirm) {
26 | Text(text = stringResource(R.string.ok))
27 | }
28 | },
29 | dismissButton = {
30 | OutlinedButton(onClick = onDismiss) {
31 | Text(text = stringResource(R.string.cancel))
32 | }
33 | },
34 | )
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/ExternalLinkSection.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.platform.UriHandler
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.unit.sp
14 | import pl.lambada.songsync.R
15 | import pl.lambada.songsync.ui.components.SettingsHeadLabel
16 |
17 |
18 | @Composable
19 | fun ExternalLinkSection(url: String, uriHandler: UriHandler) {
20 | Column(
21 | modifier = Modifier
22 | .clickable { uriHandler.openUri(url) }
23 | .padding(horizontal = 22.dp, vertical = 16.dp)
24 | ) {
25 | Text(stringResource(R.string.we_are_open_source))
26 | Text(
27 | text = stringResource(R.string.view_on_github),
28 | color = MaterialTheme.colorScheme.onSurfaceVariant,
29 | fontSize = 12.sp,
30 | lineHeight = 16.sp,
31 | )
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/components/CloudProviderTitle.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.lyricsFetch.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.filled.Cloud
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.draw.clip
14 | import androidx.compose.ui.unit.dp
15 | import pl.lambada.songsync.util.Providers
16 |
17 | @Composable
18 | fun CloudProviderTitle(
19 | selectedProvider: Providers,
20 | onExpandProvidersRequest: () -> Unit,
21 | ) {
22 | Row(
23 | modifier = Modifier
24 | .clip(RoundedCornerShape(4.dp))
25 | .clickable(onClick = onExpandProvidersRequest)
26 | .padding(horizontal = 4.dp)
27 | ) {
28 | Icon(
29 | imageVector = Icons.Filled.Cloud,
30 | contentDescription = null,
31 | Modifier.padding(end = 5.dp)
32 | )
33 | Text(text = selectedProvider.displayName)
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/TranslationSection.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.platform.UriHandler
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.unit.sp
14 | import pl.lambada.songsync.ui.components.SettingsHeadLabel
15 | import pl.lambada.songsync.R
16 |
17 | @Composable
18 | fun TranslationSection(uriHandler: UriHandler) {
19 | Column(
20 | modifier = Modifier
21 | .clickable { uriHandler.openUri("https://hosted.weblate.org/engage/songsync/") }
22 | .padding(horizontal = 22.dp, vertical = 16.dp)
23 | ) {
24 | Text(stringResource(id = R.string.help_us_translate))
25 | Text(
26 | text = stringResource(id = R.string.translation_website),
27 | color = MaterialTheme.colorScheme.onSurfaceVariant,
28 | fontSize = 12.sp,
29 | lineHeight = 16.sp,
30 | )
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/components/dialogs/NoInternetDialog.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.components.dialogs
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Button
6 | import androidx.compose.material3.OutlinedButton
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.res.stringResource
10 | import pl.lambada.songsync.R
11 |
12 | @Composable
13 | fun NoInternetDialog(onConfirm: () -> Unit, onIgnore: () -> Unit) {
14 | AlertDialog(
15 | onDismissRequest = { /* don't dismiss */ },
16 | confirmButton = {
17 | Button(onConfirm) {
18 | Text(stringResource(R.string.close_app))
19 | }
20 | },
21 | dismissButton = {
22 | OutlinedButton(onIgnore) {
23 | Text(stringResource(R.string.ignore))
24 | }
25 | },
26 | title = { Text(stringResource(R.string.no_internet_connection)) }, text = {
27 | Column {
28 | Text(stringResource(R.string.you_need_internet_connection))
29 | Text(stringResource(R.string.check_your_connection))
30 | Text(stringResource(R.string.if_connected_spotify_down))
31 | }
32 | }
33 | )
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/ResourceState.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util
2 |
3 | /**
4 | * A sealed class representing the state of a resource.
5 | *
6 | * @param T The type of data associated with the resource state.
7 | * @property data The data associated with the resource state.
8 | * @property message An optional message associated with the resource state.
9 | */
10 | sealed class ResourceState(val data: T? = null, val message: String? = null) {
11 |
12 | /**
13 | * Represents a loading state.
14 | *
15 | * @param T The type of data.
16 | * @property data The data associated with the loading state.
17 | */
18 | class Loading(data: T? = null) : ResourceState(data)
19 |
20 | /**
21 | * Represents a success state with optional data.
22 | *
23 | * @param T The type of data.
24 | * @property data The data associated with the success state.
25 | */
26 | class Success(data: T?) : ResourceState(data)
27 |
28 | /**
29 | * Represents an error state with an optional message and data.
30 | *
31 | * @param T The type of data.
32 | * @property message The message associated with the error state.
33 | * @property data The data associated with the error state.
34 | */
35 | class Error(message: String, data: T? = null) : ResourceState(data, message)
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/AppInfoSection.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.Button
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import pl.lambada.songsync.R
14 |
15 | @Composable
16 | fun AppInfoSection(version: String, onCheckForUpdates: () -> Unit) {
17 | Column(
18 | modifier = Modifier
19 | .padding(horizontal = 22.dp, vertical = 16.dp),
20 | ) {
21 | Text(stringResource(R.string.what_is_songsync))
22 | Text(stringResource(R.string.extra_what_is_songsync))
23 | Text("")
24 | Text(stringResource(R.string.app_version, version))
25 | Row {
26 | Spacer(modifier = Modifier.weight(1f))
27 | Button(
28 | modifier = Modifier.padding(top = 8.dp),
29 | onClick = onCheckForUpdates
30 | ) {
31 | Text(stringResource(R.string.check_for_updates))
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | # Don't warn about missing classes while running R8. (isMinifyEnabled)
24 | -dontwarn org.bouncycastle.jsse.BCSSLParameters
25 | -dontwarn org.bouncycastle.jsse.BCSSLSocket
26 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
27 | -dontwarn org.conscrypt.Conscrypt$Version
28 | -dontwarn org.conscrypt.Conscrypt
29 | -dontwarn org.conscrypt.ConscryptHostnameVerifier
30 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters
31 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket
32 | -dontwarn org.openjsse.net.ssl.OpenJSSE
33 | -dontwarn org.slf4j.impl.StaticLoggerBinder
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/spotify/SpotifyLyricsAPI.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.data.remote.lyrics_providers.spotify
2 |
3 | import io.ktor.client.request.get
4 | import io.ktor.client.request.parameter
5 | import io.ktor.client.statement.bodyAsText
6 | import pl.lambada.songsync.domain.model.lyrics_providers.spotify.SyncedLinesResponse
7 | import pl.lambada.songsync.util.networking.Ktor.client
8 | import pl.lambada.songsync.util.networking.Ktor.json
9 |
10 | class SpotifyLyricsAPI {
11 | private val baseURL = "https://paxsenix.alwaysdata.net/getLyricsSpotify.php"
12 |
13 | /**
14 | * Gets synced lyrics using the song link and returns them as a string formatted as an LRC file.
15 | * @param title The title of the song.
16 | * @param artist The name of the artist.
17 | * @return The synced lyrics as a string.
18 | */
19 | suspend fun getSyncedLyrics(track_url: String): String? {
20 | val response = client.get(baseURL) {
21 | parameter("url", track_url)
22 | }
23 | val responseBody = response.bodyAsText(Charsets.UTF_8)
24 | if (response.status.value !in 200..299)
25 | return null
26 |
27 | val json = json.decodeFromString(responseBody)
28 |
29 | if (json.lyrics == "Not Found.")
30 | return null
31 |
32 | return json.lyrics
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/batchDownload/DownloadCompleteDialog.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.home.components.batchDownload
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Button
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.res.stringResource
9 | import pl.lambada.songsync.R
10 |
11 | @Composable
12 | fun DownloadCompleteDialog(
13 | successCount: Int,
14 | noLyricsCount: Int,
15 | failedCount: Int,
16 | onDismiss: () -> Unit
17 | ) {
18 | AlertDialog(
19 | title = {
20 | Text(text = stringResource(id = R.string.batch_download_lyrics))
21 | },
22 | text = {
23 | Column {
24 | Text(text = stringResource(R.string.download_complete))
25 | Text(text = stringResource(R.string.success, successCount))
26 | Text(text = stringResource(R.string.no_lyrics, noLyricsCount))
27 | Text(text = stringResource(R.string.failed, failedCount))
28 | }
29 | },
30 | onDismissRequest = onDismiss,
31 | confirmButton = {
32 | Button(onClick = onDismiss) {
33 | Text(text = stringResource(id = R.string.ok))
34 | }
35 | }
36 | )
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/SupportSection.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.platform.UriHandler
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.unit.sp
14 | import pl.lambada.songsync.R
15 | import pl.lambada.songsync.ui.components.SettingsHeadLabel
16 |
17 |
18 | @Composable
19 | fun SupportSection(uriHandler: UriHandler) {
20 | Column(
21 | modifier = Modifier
22 | .clickable { uriHandler.openUri("https://t.me/LambadaOT") }
23 | .padding(horizontal = 22.dp, vertical = 16.dp)
24 | ) {
25 | Text(stringResource(R.string.bugs_or_suggestions_contact_us))
26 | Text(
27 | text = stringResource(R.string.telegram_group),
28 | color = MaterialTheme.colorScheme.onSurfaceVariant,
29 | fontSize = 12.sp,
30 | lineHeight = 16.sp,
31 | )
32 | }
33 | Text(
34 | stringResource(R.string.create_issue),
35 | modifier = Modifier.padding(horizontal = 22.dp)
36 | )
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/ContributorsSection.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.platform.UriHandler
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.unit.dp
14 | import androidx.compose.ui.unit.sp
15 |
16 | @Composable
17 | fun ContributorsSection(uriHandler: UriHandler) {
18 | Column{
19 | Contributor.entries.forEach {
20 | val additionalInfo = stringResource(id = it.contributionLevel.stringResource)
21 | Column(
22 | modifier = Modifier
23 | .fillMaxWidth()
24 | .clickable { it.github?.let { it1 -> uriHandler.openUri(it1) } }
25 | .padding(horizontal = 22.dp, vertical = 16.dp)
26 | ) {
27 | Text(text = it.devName)
28 | Text(
29 | text = additionalInfo,
30 | color = MaterialTheme.colorScheme.outline,
31 | fontSize = 12.sp
32 | )
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/UpdateAvailableDialog.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.rememberScrollState
5 | import androidx.compose.foundation.verticalScroll
6 | import androidx.compose.material3.AlertDialog
7 | import androidx.compose.material3.Button
8 | import androidx.compose.material3.OutlinedButton
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.stringResource
13 | import pl.lambada.songsync.R
14 |
15 | @Composable
16 | fun UpdateAvailableDialog(
17 | onDismiss: () -> Unit,
18 | currentVersion: String,
19 | latestVersion: String,
20 | changelog: String,
21 | onDownloadRequest: () -> Unit,
22 | ) {
23 | AlertDialog(
24 | onDismissRequest = onDismiss,
25 | title = { Text(stringResource(R.string.update_available)) },
26 | text = {
27 | Column(Modifier.verticalScroll(rememberScrollState())) {
28 | Text("v$currentVersion -> $latestVersion")
29 | Text(stringResource(R.string.changelog, changelog))
30 | }
31 | },
32 | confirmButton = {
33 | Button(onDownloadRequest) { Text(stringResource(R.string.download)) }
34 | },
35 | dismissButton = {
36 | OutlinedButton(onDismiss) { Text(stringResource(R.string.cancel)) }
37 | }
38 | )
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/init/components/permissions/NotificationPermission.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.init.components.permissions
2 |
3 | import android.content.ComponentName
4 | import android.content.Intent
5 | import android.os.Build
6 | import android.provider.Settings
7 | import androidx.activity.compose.rememberLauncherForActivityResult
8 | import androidx.activity.result.ActivityResultLauncher
9 | import androidx.activity.result.contract.ActivityResultContracts
10 | import androidx.annotation.RequiresApi
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.core.app.NotificationManagerCompat
15 | import pl.lambada.songsync.services.NotificationListener
16 |
17 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1)
18 | @Composable
19 | fun NotificationPermission(
20 | onGranted: () -> Unit,
21 | onDismiss: () -> Unit
22 | ) {
23 | val context = LocalContext.current
24 | val notificationManager = rememberLauncherForActivityResult(
25 | ActivityResultContracts.StartActivityForResult()
26 | ) { result ->
27 | if (
28 | NotificationManagerCompat.getEnabledListenerPackages(context)
29 | .contains(context.packageName)
30 | ) {
31 | onGranted()
32 | }
33 | onDismiss()
34 | }
35 | LaunchedEffect(Unit) {
36 | val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)
37 | notificationManager.launch(intent)
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/components/FailedDialogue.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.lyricsFetch.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Button
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.res.stringResource
9 | import pl.lambada.songsync.R
10 | import pl.lambada.songsync.util.EmptyQueryException
11 | import pl.lambada.songsync.util.NoTrackFoundException
12 | import java.io.FileNotFoundException
13 |
14 | /**
15 | * Composable function to display a dialog for failed operations.
16 | *
17 | * @param onDismissRequest Callback to be invoked when the dialog is dismissed.
18 | * @param onOkRequest Callback to be invoked when the OK button is pressed.
19 | * @param exception The exception that caused the failure.
20 | */
21 | @Composable
22 | fun FailedDialogue(
23 | onDismissRequest: () -> Unit,
24 | onOkRequest: () -> Unit,
25 | exception: Exception
26 | ) {
27 | AlertDialog(
28 | onDismissRequest = onDismissRequest,
29 | confirmButton = { Button(onClick = onOkRequest) { Text(stringResource(R.string.ok)) } },
30 | title = { Text(text = stringResource(id = R.string.error)) },
31 | text = {
32 | when (exception) {
33 | is NoTrackFoundException -> Text(stringResource(R.string.no_results))
34 | is EmptyQueryException -> Text(stringResource(R.string.invalid_query))
35 | else -> Text(exception.toString())
36 | }
37 | }
38 | )
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/Utils.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import pl.lambada.songsync.R
4 |
5 | @Suppress("SpellCheckingInspection")
6 | enum class Contributor(
7 | val devName: String,
8 | val contributionLevel: ContributionLevel,
9 | val github: String? = null,
10 | val telegram: String? = null
11 | ) {
12 | LAMBADA10(
13 | "Lambada10", ContributionLevel.LEAD_DEVELOPER,
14 | github = "https://github.com/Lambada10", telegram = "https://t.me/Lambada10"
15 | ),
16 | NIFT4(
17 | "Nick", ContributionLevel.DEVELOPER,
18 | github = "https://github.com/nift4", telegram = "https://t.me/nift4"
19 | ),
20 | BOBBYESP(
21 | "BobbyESP", ContributionLevel.DEVELOPER,
22 | github = "https://github.com/BobbyESP"
23 | ),
24 | PXEEMO(
25 | "Pxeemo", ContributionLevel.CONTRIBUTOR,
26 | github = "https://github.com/pxeemo"
27 | ),
28 | AKANETAN(
29 | "AkaneTan", ContributionLevel.CONTRIBUTOR,
30 | github = "https://github.com/AkaneTan"
31 | ),
32 | NXOIM(
33 | devName = "nxoim", ContributionLevel.CONTRIBUTOR,
34 | github = "https://github.com/nxoim"
35 | ),
36 | PAXSENIX0(
37 | devName = "Paxsenix0", ContributionLevel.CONTRIBUTOR,
38 | github = "https://github.com/paxsenix0"
39 | )
40 | }
41 |
42 | /**
43 | * Defines the contribution level of a contributor.
44 | */
45 | enum class ContributionLevel(val stringResource: Int) {
46 | CONTRIBUTOR(R.string.contributor),
47 | DEVELOPER(R.string.developer),
48 | LEAD_DEVELOPER(R.string.lead_developer)
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/HomeSearchThing.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.home.components
2 |
3 | import androidx.compose.animation.AnimatedContent
4 | import androidx.compose.animation.SizeTransform
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.animation.slideInVertically
8 | import androidx.compose.animation.slideOutVertically
9 | import androidx.compose.animation.togetherWith
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.unit.dp
15 |
16 | @Composable
17 | fun HomeSearchThing(
18 | showingSearch: Boolean,
19 | searchBar: @Composable () -> Unit,
20 | filterBar: @Composable () -> Unit
21 | ) {
22 | AnimatedContent(
23 | targetState = showingSearch,
24 | transitionSpec = {
25 | if (targetState) {
26 | (slideInVertically { height -> height } + fadeIn()).togetherWith(
27 | slideOutVertically { height -> -height } + fadeOut()
28 | )
29 | } else {
30 | (slideInVertically { height -> -height } + fadeIn()).togetherWith(
31 | slideOutVertically { height -> height } + fadeOut()
32 | )
33 | }.using(
34 | SizeTransform()
35 | )
36 | },
37 | label = "",
38 | modifier = Modifier
39 | .fillMaxWidth()
40 | .height(55.dp)
41 | ) { showing ->
42 | if (showing) searchBar() else filterBar()
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/SettingsScreenTopBar.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButton
9 | import androidx.compose.material3.MediumTopAppBar
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBarScrollBehavior
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import androidx.navigation.NavController
17 | import pl.lambada.songsync.R
18 |
19 | @OptIn(ExperimentalMaterial3Api::class)
20 | @Composable
21 | fun SettingsScreenTopBar(navController: NavController, scrollBehavior: TopAppBarScrollBehavior) {
22 | MediumTopAppBar(
23 | navigationIcon = {
24 | IconButton(
25 | onClick = {
26 | navController.popBackStack(navController.graph.startDestinationId, false)
27 | }
28 | ) {
29 | Icon(
30 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
31 | contentDescription = stringResource(R.string.back),
32 | )
33 | }
34 | },
35 | title = {
36 | Text(
37 | modifier = Modifier.padding(start = 6.dp),
38 | text = stringResource(id = R.string.settings)
39 | )
40 | },
41 | scrollBehavior = scrollBehavior
42 | )
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/apple/AppleTokenManager.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.data.remote.lyrics_providers.apple
2 |
3 | import io.ktor.client.request.get
4 | import io.ktor.client.statement.bodyAsText
5 | import kotlinx.coroutines.sync.Mutex
6 | import kotlinx.coroutines.sync.withLock
7 | import pl.lambada.songsync.util.networking.Ktor.client
8 |
9 | class AppleTokenManager {
10 | private var cachedToken: String? = null
11 | private val mutex = Mutex()
12 |
13 | suspend fun getToken(): String {
14 | mutex.withLock {
15 | cachedToken?.let { return it }
16 |
17 | try {
18 | val mainPageResponse = client.get("https://beta.music.apple.com")
19 | val mainPageBody = mainPageResponse.bodyAsText()
20 |
21 | val indexJsRegex = Regex("""/assets/index~[^/]+\.js""")
22 | val indexJsMatch = indexJsRegex.find(mainPageBody)
23 | ?: throw Exception("Could not find index-legacy script URL")
24 |
25 | val indexJsUri = indexJsMatch.value
26 |
27 | val indexJsResponse = client.get("https://beta.music.apple.com$indexJsUri")
28 | val indexJsBody = indexJsResponse.bodyAsText()
29 |
30 | val tokenRegex = Regex("""eyJh([^"]*)""")
31 | val tokenMatch = tokenRegex.find(indexJsBody)
32 | ?: throw Exception("Could not find token")
33 |
34 | val token = tokenMatch.value
35 | cachedToken = token
36 | return token
37 | } catch (e: Exception) {
38 | throw Exception("Error fetching Apple Music token: ${e.message}", e)
39 | }
40 | }
41 | }
42 |
43 | fun clearToken() {
44 | cachedToken = null
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/components/NotSubmittedContent.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.lyricsFetch.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.material3.Button
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.res.stringResource
13 | import androidx.compose.ui.text.input.ImeAction
14 | import androidx.compose.ui.unit.dp
15 | import pl.lambada.songsync.R
16 | import pl.lambada.songsync.ui.components.CommonTextField
17 |
18 |
19 | @Composable
20 | fun NotSubmittedContent(
21 | querySong: String,
22 | onQuerySongChange: (String) -> Unit,
23 | queryArtist: String,
24 | onQueryArtistChange: (String) -> Unit,
25 | onGetLyricsRequest: () -> Unit
26 | ) {
27 | Column(horizontalAlignment = Alignment.CenterHorizontally) {
28 | Spacer(modifier = Modifier.height(16.dp))
29 | CommonTextField(
30 | value = querySong,
31 | onValueChange = onQuerySongChange,
32 | label = stringResource(id = R.string.song_name_no_args),
33 | imeAction = ImeAction.Next,
34 | modifier = Modifier.fillMaxWidth()
35 | )
36 | Spacer(modifier = Modifier.height(6.dp))
37 | CommonTextField(
38 | value = queryArtist,
39 | onValueChange = onQueryArtistChange,
40 | label = stringResource(R.string.artist_name_no_args),
41 | modifier = Modifier.fillMaxWidth()
42 | )
43 | Spacer(modifier = Modifier.height(8.dp))
44 | Button(onClick = onGetLyricsRequest) {
45 | Text(text = stringResource(id = R.string.get_lyrics))
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | /**
7 | * Data class for storing song information.
8 | * Used for both local and remote songs.
9 | * The only difference is that local songs have songLink set to null.
10 | * @param songName The name of the song.
11 | * @param artistName The name of the artist.
12 | * @param songLink The link to the song.
13 | * @param albumCoverLink The link to the album cover.
14 | * @param lrcLibID The ID for LRCLib.
15 | * @param qqPayload The payload for QQMusic.
16 | * @param neteaseID The ID for Netease.
17 | * @param appleID The ID for Apple Music.
18 | * @param musixmatchID The ID for Musixmatch.
19 | * @param hasSyncedLyrics Flag indicating if the song has synced lyrics (Musixmatch-only).
20 | * @param hasUnsyncedLyrics Flag indicating if the song has unsynced lyrics (Musixmatch-only).
21 | * @param syncedLyrics The synced lyrics (Musixmatch-only).
22 | * @param unsyncedLyrics The unsynced lyrics (Musixmatch-only).
23 | */
24 | @Suppress("SpellCheckingInspection")
25 | @Parcelize
26 | data class SongInfo(
27 | var songName: String?,
28 | var artistName: String? = null,
29 | var songLink: String? = null,
30 | var albumCoverLink: String? = null,
31 | var lrcLibID: Int? = null, // LRCLib-only
32 | var qqPayload: String? = null, // QQMusic-only
33 | var neteaseID: Long? = null, // Netease-only
34 | var appleID: Long? = null, // Apple-only
35 | var musixmatchID: Long? = null, // Musixmatch-only
36 | var hasSyncedLyrics: Boolean? = null, // Musixmatch-only
37 | var hasUnsyncedLyrics: Boolean? = null, // Musixmatch-only
38 | var syncedLyrics: String? = null, // Musixmatch-only
39 | var unsyncedLyrics: String? = null, // Musixmatch-only
40 | var availableLanguages: List = emptyList(), // Musixmatch-only
41 | var originalLanguage: String? = null, // Musixmatch-only
42 | var currentLanguage: String? = null, // Musixmatch-only
43 | ) : Parcelable
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SongSync
2 |
3 | A simple Android app to download lyrics (.lrc files) for songs in your music library.
4 |
5 | ### Features
6 |
7 | - Download lyrics for whole music library with a single click
8 | - Download lyrics for individual songs in your music library
9 | - Embed lyrics directly to song
10 | - Download lyrics from various providers
11 | - Search for lyrics for songs not in your music library (and download them)
12 |
13 | ### Screenshots (v4.0.0)
14 |
15 | 
16 | 
17 | 
18 | 
19 | 
20 |
21 | ### Installation
22 |
23 |
24 |
25 | You can download the latest version of the app from the [releases page](https://github.com/Lambada10/SongSync/releases).
26 |
27 | ### Translation
28 |
29 | If you would like to help translating this app, you can do so [here](https://hosted.weblate.org/engage/songsync/).
30 |
31 | ### License
32 |
33 | This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](https://github.com/Lambada10/SongSync/blob/master/LICENSE) file for details.
34 |
35 | ### Thanks to
36 |
37 | - [Spotify](https://developer.spotify.com/documentation/web-api)
38 | - [SpotifyLyricsAPI](https://github.com/akashrchandran/spotify-lyrics-api)
39 | - [syncedlyrics](https://github.com/0x7d4/syncedlyrics)
40 | - [Statusbar Lyric Ext](https://github.com/cjybyjk/StatusBarLyricExt)
41 | - [Alex](https://github.com/paxsenix0) for access to various apis
42 |
43 | ### Friend projects
44 |
45 | [Gramophone](https://github.com/AkaneTan/Gramophone)
46 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.withContext
12 | import pl.lambada.songsync.R
13 | import pl.lambada.songsync.data.remote.UpdateService
14 | import pl.lambada.songsync.data.remote.UpdateState
15 | import pl.lambada.songsync.util.showToast
16 |
17 | /**
18 | * ViewModel class for the main functionality of the app.
19 | */
20 | class SettingsViewModel(
21 | private val updateService: UpdateService = UpdateService()
22 | ) : ViewModel() {
23 | var updateState by mutableStateOf(UpdateState.Idle)
24 |
25 | fun dismissUpdate() { updateState = UpdateState.Idle }
26 |
27 | fun checkForUpdates(context: Context) {
28 | viewModelScope.launch {
29 | withContext(Dispatchers.IO) { updateService.checkForUpdates(context) }.collect {
30 | updateState = it
31 |
32 | when (it) {
33 | UpdateState.Checking -> showToast(
34 | context,
35 | context.getString(R.string.checking_for_updates),
36 | long = false
37 | )
38 |
39 | is UpdateState.Error -> showToast(
40 | context,
41 | context.getString(R.string.error_checking_for_updates),
42 | long = false
43 | )
44 |
45 | UpdateState.UpToDate -> showToast(
46 | context,
47 | context.getString(R.string.up_to_date),
48 | long = false
49 | )
50 | else -> { }
51 | }
52 | }
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/batchDownload/DownloadProgressDialog.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.home.components.batchDownload
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.OutlinedButton
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.res.stringResource
9 | import pl.lambada.songsync.R
10 | import pl.lambada.songsync.ui.components.AnimatedText
11 |
12 | @Composable
13 | fun DownloadProgressDialog(
14 | currentSongTitle: String?,
15 | count: Int,
16 | total: Int,
17 | percentage: Int,
18 | successCount: Int,
19 | noLyricsCount: Int,
20 | failedCount: Int,
21 | onCancel: () -> Unit,
22 | disableMarquee: Boolean,
23 | ) {
24 | AlertDialog(
25 | title = {
26 | Text(text = stringResource(id = R.string.batch_download_lyrics))
27 | },
28 | text = {
29 | Column {
30 | Text(text = stringResource(R.string.downloading_lyrics))
31 | AnimatedText(
32 | animate = !disableMarquee,
33 | text = stringResource(
34 | R.string.song,
35 | currentSongTitle ?: stringResource(id = R.string.unknown)
36 | )
37 | )
38 | Text(text = stringResource(R.string.progress, count, total, percentage))
39 | Text(
40 | text = stringResource(
41 | R.string.success_failed, successCount, noLyricsCount, failedCount
42 | )
43 | )
44 | Text(text = stringResource(R.string.please_do_not_close_the_app_this_may_take_a_while))
45 | }
46 | },
47 | onDismissRequest = { /* Prevent accidental dismiss */ },
48 | confirmButton = { /* Empty but required */ },
49 | dismissButton = {
50 | OutlinedButton(onClick = onCancel) {
51 | Text(text = stringResource(R.string.cancel))
52 | }
53 | }
54 | )
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/components/SwitchItem.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Switch
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.CombinedModifier
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.text.font.FontStyle
18 | import androidx.compose.ui.unit.dp
19 | import androidx.compose.ui.unit.sp
20 |
21 | @Composable
22 | fun SwitchItem(
23 | label: String,
24 | selected: Boolean,
25 | modifier: Modifier = Modifier,
26 | innerPaddingValues: PaddingValues = PaddingValues(
27 | horizontal = 22.dp,
28 | vertical = 16.dp
29 | ),
30 | description: String = "",
31 | onClick: () -> Unit,
32 | ) {
33 | Row(
34 | modifier = CombinedModifier(
35 | inner = Modifier
36 | .clickable { onClick() }
37 | .padding(innerPaddingValues),
38 | outer = modifier
39 | ),
40 | verticalAlignment = Alignment.CenterVertically
41 | ) {
42 | Column(
43 | modifier = Modifier.weight(1f),
44 | ) {
45 | Text(
46 | text = label,
47 | )
48 | if (description.isNotEmpty() == true) {
49 | Text(
50 | text = description,
51 | color = MaterialTheme.colorScheme.onSurfaceVariant,
52 | fontSize = 12.sp,
53 | lineHeight = 16.sp,
54 | )
55 | }
56 | }
57 | Spacer(modifier = Modifier.width(12.dp))
58 | Switch(
59 | checked = selected,
60 | onCheckedChange = { onClick() }
61 | )
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/data/remote/UpdateService.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.data.remote
2 |
3 | import android.content.Context
4 | import kotlinx.coroutines.flow.flow
5 | import pl.lambada.songsync.data.remote.github.GithubAPI
6 | import pl.lambada.songsync.domain.model.Release
7 | import pl.lambada.songsync.util.ext.getVersion
8 |
9 | class UpdateService {
10 |
11 | /**
12 | * Checks for updates by comparing the latest release version with the current version.
13 | * @param context The context of the application.
14 | * @return A flow emitting the update state.
15 | */
16 | fun checkForUpdates(context: Context) = flow {
17 | emit(UpdateState.Checking)
18 |
19 | try {
20 | val latest = GithubAPI.getLatestRelease()
21 | val isUpdate = isNewerRelease(context, latest)
22 |
23 | emit(
24 | if (isUpdate)
25 | UpdateState.UpdateAvailable(latest)
26 | else
27 | UpdateState.UpToDate
28 | )
29 |
30 | } catch (e: Exception) {
31 | emit(UpdateState.Error(e))
32 | }
33 | }
34 |
35 | /**
36 | * Checks if the latest release is newer than the current version.
37 | * @param context The context of the application.
38 | * @param latestRelease The latest release from the GitHub API.
39 | * @return True if the latest release is newer, false otherwise.
40 | */
41 | private fun isNewerRelease(context: Context, latestRelease: Release): Boolean {
42 | val currentVersion = context
43 | .getVersion()
44 | .replace(".", "")
45 | .toInt()
46 | val latestVersion = latestRelease.tagName
47 | .replace(".", "")
48 | .replace("v", "")
49 | .toInt()
50 |
51 | return latestVersion > currentVersion
52 | }
53 | }
54 |
55 | /**
56 | * Defines the state of the update check.
57 | */
58 | sealed interface UpdateState {
59 | data object Idle : UpdateState
60 | data object Checking : UpdateState
61 | data object UpToDate : UpdateState
62 | data class UpdateAvailable(val release: Release) : UpdateState
63 | data class Error(val reason: Throwable) : UpdateState
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/FilterAndSongCount.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.home.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.automirrored.filled.Sort
8 | import androidx.compose.material.icons.filled.Search
9 | import androidx.compose.material.icons.filled.Sort
10 | import androidx.compose.material.icons.outlined.FilterAlt
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.IconButton
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.res.pluralStringResource
19 | import androidx.compose.ui.res.stringResource
20 | import pl.lambada.songsync.R
21 |
22 | @OptIn(ExperimentalMaterial3Api::class)
23 | @Composable
24 | fun FilterAndSongCount(
25 | displaySongsCount: Int,
26 | onFilterClick: () -> Unit,
27 | onSortClick: () -> Unit,
28 | onSearchClick: () -> Unit
29 | ) {
30 | Row(
31 | verticalAlignment = Alignment.CenterVertically,
32 | horizontalArrangement = Arrangement.Center
33 | ) {
34 | Text(text = pluralStringResource(R.plurals.songs_count, displaySongsCount, displaySongsCount))
35 | Spacer(modifier = Modifier.weight(1f))
36 | IconButton(onClick = onSortClick) {
37 | Icon(
38 | Icons.AutoMirrored.Filled.Sort,
39 | contentDescription = stringResource(R.string.sort),
40 | )
41 | }
42 |
43 | IconButton(onClick = onFilterClick) {
44 | Icon(
45 | Icons.Outlined.FilterAlt,
46 | contentDescription = stringResource(R.string.search),
47 | )
48 | }
49 |
50 | IconButton(onClick = onSearchClick) {
51 | Icon(
52 | Icons.Default.Search,
53 | contentDescription = stringResource(R.string.search),
54 | )
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/lyricsFetch/components/LocalSongContent.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.lyricsFetch.components
2 |
3 | import androidx.compose.animation.AnimatedVisibilityScope
4 | import androidx.compose.animation.ExperimentalSharedTransitionApi
5 | import androidx.compose.animation.SharedTransitionScope
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Downloading
12 | import androidx.compose.material.icons.filled.PlayCircleOutline
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.unit.dp
19 | import pl.lambada.songsync.R
20 | import pl.lambada.songsync.ui.LocalSong
21 | import pl.lambada.songsync.ui.components.SongCard
22 |
23 | @OptIn(ExperimentalSharedTransitionApi::class)
24 | @Composable
25 | fun SharedTransitionScope.LocalSongContent(
26 | song: LocalSong,
27 | animatedVisibilityScope: AnimatedVisibilityScope,
28 | disableMarquee: Boolean
29 | ) {
30 | Row {
31 | if (song.filePath.isNotEmpty()) {
32 | Icon(
33 | imageVector = Icons.Filled.Downloading,
34 | contentDescription = null,
35 | Modifier.padding(end = 5.dp)
36 | )
37 | Text(stringResource(R.string.local_song))
38 | } else {
39 | Icon(
40 | imageVector = Icons.Filled.PlayCircleOutline,
41 | contentDescription = null,
42 | Modifier.padding(end = 5.dp)
43 | )
44 | Text(stringResource(R.string.now_playing_song))
45 | }
46 | }
47 | Spacer(modifier = Modifier.height(6.dp))
48 | SongCard(
49 | filePath = song.filePath,
50 | songName = song.songName,
51 | artists = song.artists,
52 | coverUrl = song.coverUri,
53 | animatedVisibilityScope = animatedVisibilityScope,
54 | animateText = !disableMarquee,
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Build APK
2 |
3 | on:
4 | push:
5 | branches:
6 | - "**"
7 | pull_request:
8 | branches:
9 | - "**"
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build-debug:
14 | name: Build debug APK
15 | runs-on: ubuntu-latest
16 |
17 | permissions:
18 | contents: read
19 | packages: write
20 | checks: write
21 | pull-requests: write
22 | statuses: write
23 | security-events: write
24 |
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v2
28 |
29 | - name: Set up JDK
30 | uses: actions/setup-java@v3
31 | with:
32 | distribution: "zulu"
33 | java-version: "17"
34 |
35 | - name: Build debug APK
36 | run: ./gradlew assembleDebug
37 |
38 | - name: Upload debug APK
39 | uses: actions/upload-artifact@v4
40 | with:
41 | name: app-debug.apk
42 | path: ./app/build/outputs/apk/debug/app-debug.apk
43 |
44 | build-release:
45 | if: github.event_name == 'push'
46 | name: Build signed release APK
47 | runs-on: ubuntu-latest
48 |
49 | env:
50 | RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
51 | RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEYSTORE_ALIAS }}
52 | RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
53 | KEYSTORE_BASE_64: ${{ secrets.KEYSTORE_BASE_64 }}
54 |
55 | permissions:
56 | contents: read
57 | packages: write
58 | checks: write
59 | pull-requests: write
60 | statuses: write
61 | security-events: write
62 |
63 | steps:
64 | - name: Checkout code
65 | uses: actions/checkout@v2
66 |
67 | - name: Set up JDK
68 | uses: actions/setup-java@v3
69 | with:
70 | distribution: "zulu"
71 | java-version: "17"
72 |
73 | - name: Set up signing key
74 | run: |
75 | base64 -d <<< $KEYSTORE_BASE_64 > release.keystore
76 | echo "RELEASE_STORE_FILE=$(realpath release.keystore)" >> $GITHUB_ENV
77 |
78 | - name: Build release APK
79 | run: ./gradlew assembleRelease
80 |
81 | - name: Upload release APK
82 | uses: actions/upload-artifact@v4
83 | with:
84 | name: app-release.apk
85 | path: ./app/build/outputs/apk/release/app-release.apk
86 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/util/MiscelaneousUtils.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.util
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Build
7 | import android.widget.Toast
8 | import androidx.core.content.FileProvider
9 | import java.io.File
10 | import java.nio.file.Files
11 |
12 | fun isLegacyFileAccessRequired(filePath: String?): Boolean {
13 | // Before Android 11, not in internal storage
14 | return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
15 | && filePath?.contains("/storage/emulated/0/") == false
16 | }
17 |
18 | fun openFileFromPath(context: Context, filePath: String) {
19 | val file = File(filePath)
20 | if (!file.exists()) {
21 | showToast(context, "File does not exist")
22 | return
23 | }
24 |
25 | val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
26 | FileProvider.getUriForFile(context, context.packageName + ".provider", file)
27 | } else {
28 | Uri.fromFile(file)
29 | }
30 |
31 | val mime = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
32 | Files.probeContentType(file.toPath())
33 | } else run {
34 | val extension = file.extension
35 | val mimeTypeMap = android.webkit.MimeTypeMap.getSingleton()
36 | mimeTypeMap.getMimeTypeFromExtension(extension)
37 | }
38 |
39 | val intent = Intent(Intent.ACTION_VIEW)
40 | .setDataAndType(uri, mime)
41 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
42 |
43 | if (intent.resolveActivity(context.packageManager) != null) {
44 | context.startActivity(intent)
45 | } else {
46 | Toast.makeText(context, "No app found to open the music file.", Toast.LENGTH_SHORT).show()
47 | }
48 | }
49 |
50 | fun showToast(context: Context, messageResId: Int, vararg args: Any, long: Boolean = true) {
51 | Toast
52 | .makeText(
53 | context,
54 | context.getString(messageResId, *args),
55 | if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
56 | )
57 | .show()
58 | }
59 |
60 | fun showToast(context: Context, message: String, long: Boolean = true) {
61 | Toast
62 | .makeText(
63 | context,
64 | message,
65 | if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
66 | )
67 | .show()
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Apple.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.others
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class AppleMusicSearchResponse(
8 | val results: AppleMusicResults,
9 | val resources: AppleMusicResources? = null
10 | )
11 |
12 | @Serializable
13 | data class AppleMusicResults(
14 | val songs: AppleMusicSongsResult? = null
15 | )
16 |
17 | @Serializable
18 | data class AppleMusicSongsResult(
19 | val data: List
20 | )
21 |
22 | @Serializable
23 | data class AppleMusicSongData(
24 | val id: String,
25 | val type: String,
26 | val href: String
27 | )
28 |
29 | @Serializable
30 | data class AppleMusicResources(
31 | val songs: Map? = null,
32 | val artists: Map? = null
33 | )
34 |
35 | @Serializable
36 | data class AppleMusicSongDetail(
37 | val id: String,
38 | val type: String,
39 | val attributes: AppleMusicSongAttributes,
40 | val relationships: AppleMusicRelationships? = null
41 | )
42 |
43 | @Serializable
44 | data class AppleMusicSongAttributes(
45 | val name: String,
46 | val artistName: String,
47 | val albumName: String,
48 | val artwork: AppleMusicArtwork,
49 | val url: String,
50 | val isrc: String? = null,
51 | val releaseDate: String? = null,
52 | val durationInMillis: Long? = null,
53 | val hasTimeSyncedLyrics: Boolean? = null,
54 | val contentRating: String? = null
55 | )
56 |
57 | @Serializable
58 | data class AppleMusicArtwork(
59 | val url: String,
60 | val width: Int? = null,
61 | val height: Int? = null
62 | )
63 |
64 | @Serializable
65 | data class AppleMusicRelationships(
66 | val artists: AppleMusicArtistsRelation? = null
67 | )
68 |
69 | @Serializable
70 | data class AppleMusicArtistsRelation(
71 | val data: List
72 | )
73 |
74 | @Serializable
75 | data class AppleMusicArtistData(
76 | val id: String,
77 | val type: String
78 | )
79 |
80 | @Serializable
81 | data class AppleMusicArtistDetail(
82 | val id: String,
83 | val attributes: AppleMusicArtistAttributes
84 | )
85 |
86 | @Serializable
87 | data class AppleMusicArtistAttributes(
88 | val name: String,
89 | val url: String? = null
90 | )
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/init/InitScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.init
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.os.Environment
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.setValue
9 | import androidx.core.app.NotificationManagerCompat
10 | import androidx.core.content.ContextCompat
11 | import androidx.lifecycle.ViewModel
12 | import pl.lambada.songsync.data.UserSettingsController
13 |
14 | class InitScreenViewModel(
15 | val userSettingsController: UserSettingsController,
16 | ): ViewModel() {
17 | var allFilesClicked by mutableStateOf(false)
18 | var notificationClicked by mutableStateOf(false)
19 | var notificationAccessClicked by mutableStateOf(false)
20 |
21 | var allFilesPermissionGranted by mutableStateOf(false)
22 | var notificationPermissionGranted by mutableStateOf(false)
23 | var notificationAccessPermissionGranted by mutableStateOf(false)
24 |
25 | fun onLoad(context: Context) {
26 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
27 | allFilesPermissionGranted = Environment.isExternalStorageManager()
28 | } else {
29 | allFilesPermissionGranted = ContextCompat.checkSelfPermission(
30 | context,
31 | android.Manifest.permission.WRITE_EXTERNAL_STORAGE
32 | ) == android.content.pm.PackageManager.PERMISSION_GRANTED
33 | && ContextCompat.checkSelfPermission(
34 | context,
35 | android.Manifest.permission.READ_EXTERNAL_STORAGE
36 | ) == android.content.pm.PackageManager.PERMISSION_GRANTED
37 | }
38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
39 | notificationPermissionGranted = ContextCompat.checkSelfPermission(
40 | context,
41 | android.Manifest.permission.POST_NOTIFICATIONS
42 | ) == android.content.pm.PackageManager.PERMISSION_GRANTED
43 | }
44 | notificationAccessPermissionGranted = NotificationManagerCompat
45 | .getEnabledListenerPackages(context).contains(context.packageName)
46 | }
47 |
48 | fun onProceed() {
49 | userSettingsController.updatePassedInit(true)
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/activities/quicksearch/components/SyncedLyricsLine.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.activities.quicksearch.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.width
8 | import androidx.compose.foundation.rememberScrollState
9 | import androidx.compose.foundation.verticalScroll
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.text.SpanStyle
15 | import androidx.compose.ui.text.buildAnnotatedString
16 | import androidx.compose.ui.text.font.FontFamily
17 | import androidx.compose.ui.text.withStyle
18 | import androidx.compose.ui.unit.dp
19 | import androidx.compose.ui.unit.sp
20 |
21 | @Composable
22 | fun SyncedLyricsLine(
23 | time: String,
24 | lyrics: String,
25 | modifier: Modifier = Modifier
26 | ) {
27 | Row(modifier = modifier) {
28 | val formattedTime = buildAnnotatedString {
29 | append(time.substring(0, time.length - 4))
30 | withStyle(
31 | style = SpanStyle(
32 | fontSize = 12.sp,
33 | color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
34 | )
35 | ) {
36 | append(time.takeLast(4))
37 | }
38 | }
39 | Text(
40 | text = formattedTime,
41 | style = MaterialTheme.typography.bodyMedium.copy(
42 | fontFamily = FontFamily.Monospace
43 | )
44 | )
45 | Spacer(modifier = Modifier.width(8.dp))
46 | Text(
47 | text = lyrics,
48 | style = MaterialTheme.typography.bodyMedium
49 | )
50 | }
51 | }
52 |
53 | @Composable
54 | fun SyncedLyricsColumn(
55 | lyricsList: List>,
56 | modifier: Modifier = Modifier
57 | ) {
58 | Column(
59 | modifier = modifier.verticalScroll(rememberScrollState())
60 | ) {
61 | lyricsList.forEach { (time, lyrics) ->
62 | SyncedLyricsLine(time = time, lyrics = lyrics)
63 | Spacer(modifier = Modifier.height(4.dp))
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/settings/components/CreditsSection.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.settings.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.automirrored.filled.OpenInNew
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.UriHandler
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.unit.dp
21 | import pl.lambada.songsync.R
22 | import pl.lambada.songsync.ui.components.SettingsHeadLabel
23 |
24 |
25 | @Composable
26 | fun CreditsSection(uriHandler: UriHandler) {
27 | Column{
28 | val credits = mapOf(
29 | stringResource(R.string.spotify_api) to "https://developer.spotify.com/documentation/web-api",
30 | stringResource(R.string.spotifylyrics_api) to "https://github.com/akashrchandran/spotify-lyrics-api",
31 | stringResource(R.string.syncedlyrics_py) to "https://github.com/0x7d4/syncedlyrics",
32 | stringResource(R.string.statusbar_lyrics_ext) to "https://github.com/cjybyjk/StatusBarLyricExt"
33 | )
34 | credits.forEach { credit ->
35 | Row(
36 | modifier = Modifier
37 | .fillMaxWidth()
38 | .clickable { uriHandler.openUri(credit.value) }
39 | .padding(22.dp),
40 | verticalAlignment = Alignment.CenterVertically
41 | ) {
42 | Icon(
43 | imageVector = Icons.AutoMirrored.Filled.OpenInNew,
44 | contentDescription = stringResource(id = R.string.open_website)
45 | )
46 | Spacer(modifier = Modifier.width(16.dp))
47 | Text(text = credit.key)
48 | }
49 | }
50 | }
51 | Spacer(modifier = Modifier.height(16.dp))
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/ui/screens/init/components/PermissionItem.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.ui.screens.init.components
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.automirrored.filled.Launch
13 | import androidx.compose.material.icons.filled.CheckCircle
14 | import androidx.compose.material.icons.filled.Launch
15 | import androidx.compose.material.icons.filled.RemoveCircleOutline
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.compose.ui.unit.dp
24 | import androidx.compose.ui.unit.sp
25 |
26 | @Composable
27 | fun PermissionItem(
28 | @StringRes title: Int,
29 | @StringRes description: Int,
30 | onClick: () -> Unit,
31 | granted: Boolean,
32 | innerPaddingValues: PaddingValues = PaddingValues(
33 | horizontal = 8.dp,
34 | vertical = 16.dp
35 | ),
36 | ) {
37 | Row(
38 | modifier = Modifier
39 | .clickable(!granted) { onClick() }
40 | .padding(innerPaddingValues),
41 | verticalAlignment = Alignment.CenterVertically
42 | ) {
43 | Column(
44 | modifier = Modifier.weight(1f),
45 | ) {
46 | Text(
47 | text = stringResource(id = title),
48 | )
49 | Text(
50 | text = stringResource(id = description),
51 | color = MaterialTheme.colorScheme.onSurfaceVariant,
52 | fontSize = 12.sp,
53 | lineHeight = 16.sp,
54 | )
55 | }
56 | Spacer(modifier = Modifier.width(12.dp))
57 | if (granted) {
58 | Icon(
59 | imageVector = Icons.Default.CheckCircle,
60 | contentDescription = null,
61 | tint = MaterialTheme.colorScheme.tertiary
62 | )
63 | } else {
64 | Icon(
65 | imageVector = Icons.AutoMirrored.Default.Launch,
66 | contentDescription = null,
67 | )
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/LRCLibAPI.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.data.remote.lyrics_providers.others
2 |
3 | import io.ktor.client.request.get
4 | import io.ktor.client.statement.bodyAsText
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import pl.lambada.songsync.domain.model.SongInfo
8 | import pl.lambada.songsync.domain.model.lyrics_providers.others.LRCLibResponse
9 | import pl.lambada.songsync.util.EmptyQueryException
10 | import pl.lambada.songsync.util.networking.Ktor.client
11 | import pl.lambada.songsync.util.networking.Ktor.json
12 | import java.net.URLEncoder
13 | import java.nio.charset.StandardCharsets
14 |
15 | class LRCLibAPI {
16 | private val baseURL = "https://lrclib.net/api/"
17 |
18 | /**
19 | * Searches for synced lyrics using the song name and artist name.
20 | * @param query The SongInfo object with songName and artistName fields filled.
21 | * @return Search result as a SongInfo object.
22 | */
23 | suspend fun getSongInfo(query: SongInfo, offset: Int = 0): SongInfo? {
24 | val search = withContext(Dispatchers.IO) {
25 | URLEncoder.encode(
26 | "${query.songName} ${query.artistName}",
27 | StandardCharsets.UTF_8.toString()
28 | )
29 | }
30 |
31 | if (search == "+")
32 | throw EmptyQueryException()
33 |
34 | val response = client.get(
35 | baseURL + "search?q=$search"
36 | )
37 | val responseBody = response.bodyAsText(Charsets.UTF_8)
38 |
39 | if (responseBody == "[]" || response.status.value !in 200..299)
40 | return null
41 |
42 | val json = json.decodeFromString>(responseBody)
43 |
44 | val song = try {
45 | json[offset]
46 | } catch (e: IndexOutOfBoundsException) {
47 | return null
48 | }
49 |
50 | return SongInfo(
51 | songName = song.trackName,
52 | artistName = song.artistName,
53 | lrcLibID = song.id
54 | )
55 | }
56 |
57 | /**
58 | * Searches for synced lyrics using the song name and artist name.
59 | * @param id The ID of the song from search results.
60 | * @return The synced lyrics as a string.
61 | */
62 | suspend fun getSyncedLyrics(id: Int): String? {
63 | val response = client.get(
64 | baseURL + "get/$id"
65 | )
66 | val responseBody = response.bodyAsText(Charsets.UTF_8)
67 |
68 | if (response.status.value !in 200..299 || responseBody == "[]")
69 | return null
70 |
71 | val json = json.decodeFromString(responseBody)
72 | return json.syncedLyrics
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/spotify/SpotifyApi.kt:
--------------------------------------------------------------------------------
1 | package pl.lambada.songsync.domain.model.lyrics_providers.spotify
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class TrackSearchResult(
8 | val tracks: Tracks
9 | )
10 |
11 | @Serializable
12 | data class Tracks(
13 | val href: String,
14 | val items: List