diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6d2d019 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +name: Release + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: set up JDK 19 + uses: actions/setup-java@v4 + with: + java-version: '19' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Decode Keystore + if: github.ref == 'refs/heads/main' + env: + RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }} + run: | + if [ -n "$RELEASE_KEYSTORE_BASE64" ]; then + echo "$RELEASE_KEYSTORE_BASE64" | tr -d '\r\n ' | base64 -d -i > release.keystore + fi + + - name: Build with Gradle + env: + RELEASE_KEYSTORE_PATH: ../release.keystore + RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} + RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }} + RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} + run: ./gradlew assembleRelease + + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: odesli-android + path: odesli/build/outputs/apk/release/*.apk + + - name: Get version from CHANGELOG + id: changelog + uses: mindsers/changelog-reader-action@v2 + with: + validation_level: warn + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Create Release + if: github.ref == 'refs/heads/main' + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.changelog.outputs.version }} + name: Release ${{ steps.changelog.outputs.version }} + body: ${{ steps.changelog.outputs.changes }} + artifacts: "artifacts/*.apk" + token: ${{ secrets.GITHUB_TOKEN }} + allowUpdates: true + artifactErrorsFailBuild: true diff --git a/.gitignore b/.gitignore index 2cf9b31..182c8bc 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ .cxx local.properties /app/release + +# Keystore files +*.keystore +*.jks diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..765f792 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.2.0] + +- Added: Fallback metadata for "Odesli Page" output +- Fixed: Input fields layout overflow +- Fixed: Main content scrolling +- Fixed: FAB blocking bottom content +- Fixed: Settings text cutoff +- Fixed: Result card text alignment + +## [v1.1.3] + +- Fixed: Missing song information (title, artist, artwork) when "Odesli Page" is selected + +## [v1.1.2] + +- Improvement: Better feedback message when "Auto Copy Link" copies a link ("Link copied") + +## [v1.1.1] + +- Fixed: Share to "Odesli Page" failing when "Auto Copy Link" is off +- Fixed: App hanging when Odesli API fails or returns no data +- Added: Feedback toast when "Auto Copy Link" successfully copies a link + +## [v1.1.0] + +- Add support for converting links to an Odesli URL + +## [v1.0.0] + +- Initial Release +- Support for converting between streaming services +- Automatically copy links to clipboard diff --git a/README.md b/README.md index 838eb42..934d96f 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,93 @@ # odesli-android - A simple app that uses the Odesli API to convert songs/albums to many streaming services\ + +A simple app that uses the Odesli API to convert songs/albums to many streaming services\ [Download](https://github.com/prochy-exe/odesli-android/releases/latest) # Compatibility - Thanks to the simplicity of this app, it supports all types of devices within the one APK. + +Thanks to the simplicity of this app, it supports all types of devices within the one APK. # How does it work? - This app uses the Odesli API to convert links to any streaming service of your choosing.\ + +This app uses the Odesli API to convert links to any streaming service of your choosing.\ Generally speaking this conversion process lasts max 10 seconds.\ The request to [Odesli API](https://odesli.co/) is sent using the [Retrofit library](https://github.com/square/retrofit), the [Coil library](https://coil-kt.github.io/coil/) is responsible for asynchronously rendering the song/album cover.\ The Odesli API also has support for country codes to make sure the song or album you are trying to convert will be available for you. That means that the app has to figure out your country in some capacity, however as you might have noticed, I have decided to not ask for location directly and obtain the country code using other means which will be explained in the next paragraph.\ This app is can be used in 2 major ways: - * By copying and converting the link directly into the app - * By sharing the link to the app\ - This option allows you to easily and quickly share the song/album you want to convert to another streaming service.\ - Behavior of this option is customizable via the settings - * Preferred service\ + +- By copying and converting the link directly into the app +- By sharing the link to the app\ + This option allows you to easily and quickly share the song/album you want to convert to another streaming service.\ + Behavior of this option is customizable via the settings + - Preferred service\ This service will be used when sharing a song or an album into the app - * Auto-copy\ + - Auto-copy\ This option will automatically copy the link into your clipboard without showing the UI - * Show card when the song or album wasn't found on the preferred service\ - This option will allow you to pick a different service right away, otherwise a toast will be shown informing you that the preferred service doesn't have the song or album you are trying to convert + - Show card when the song or album wasn't found on the preferred service\ + This option will allow you to pick a different service right away, otherwise a toast will be shown informing you that the preferred service doesn't have the song or album you are trying to convert # Obtaining your country code - Instead of relying on your location which might feel unnecessary, and it totally is, I decide to use the TelephonyService, provided by Android, for devices with a SIM card, and other devices will make a request to [IPinfo](https://ipinfo.io), which is a 3rd party service that allows obtaining basic location information about you.\ - **!! I DO NOT GATHER THIS INFORMATION, IT'S BEING DIRECTLY PASSED TO THE ODESLI API !!** + +Instead of relying on your location which might feel unnecessary, and it totally is, I decide to use the TelephonyService, provided by Android, for devices with a SIM card, and other devices will make a request to [IPinfo](https://ipinfo.io), which is a 3rd party service that allows obtaining basic location information about you.\ + **!! I DO NOT GATHER THIS INFORMATION, IT'S BEING DIRECTLY PASSED TO THE ODESLI API !!** # Screenshots - ![Screenshots](./github_assets/screenshots.png "Screenshots") - - Left to right: - * Main UI - * Settings popup - * Main UI convert - * Share UI +![Screenshots](./github_assets/screenshots.png "Screenshots") + +Left to right: + +- Main UI +- Settings popup +- Main UI convert +- Share UI # Libraries/APIs used - * [Odesli/Songlink](https://odesli.co/) - * [Coil](https://coil-kt.github.io/coil/) - * [Retrofit](https://github.com/square/retrofit) - * [IPinfo](https://ipinfo.io) \ No newline at end of file + +- [Odesli/Songlink](https://odesli.co/) +- [Coil](https://coil-kt.github.io/coil/) +- [Retrofit](https://github.com/square/retrofit) +- [IPinfo](https://ipinfo.io) + +# Building and Signing + +To sign your builds, you'll need to generate a keystore file and configure your repository secrets to allow GitHub Actions to sign your releases. + +### Local Builds + +To build signed releases locally, you need to generate a keystore file: + +1. Generate the keystore (ensure Java/JDK is installed): + + ```bash + keytool -genkey -v -keystore odesli/release.keystore -alias odesli -keyalg RSA -keysize 2048 -validity 10000 -storepass android -keypass android -dname "CN=Odesli Android, OU=Mobile, O=PixP, L=Mountain View, S=California, C=US" + ``` + + _Note: Place the `release.keystore` file inside the `odesli/` directory._ + +2. Build the release APK: + ```bash + ./gradlew assembleRelease + ``` + The build script will automatically detect the `odesli/release.keystore` file. + +### CI/CD (GitHub Actions) + +You'll need your local signature file to generate the base64 encoded content of your keystore file. + +For GitHub Actions to build signed APKs, you must configure the following **Repository Secrets** (Settings > Secrets and variables > Actions): + +1. **`RELEASE_KEYSTORE_BASE64`**: The base64 encoded content of your keystore file. + Generate this by running: + + ```bash + base64 -i odesli/release.keystore -o - + ``` + + (Copy the output string) + +2. **`RELEASE_KEYSTORE_PASSWORD`**: `android` +3. **`RELEASE_KEY_ALIAS`**: `odesli` +4. **`RELEASE_KEY_PASSWORD`**: `android` + +The workflow will decode the secret into a file during the build process, ensuring your key remains secure. diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/odesli/build.gradle.kts b/odesli/build.gradle.kts index 063ea2e..9836ed6 100644 --- a/odesli/build.gradle.kts +++ b/odesli/build.gradle.kts @@ -8,12 +8,14 @@ android { namespace = "com.prochy.odesliandroid" compileSdk = 34 + buildToolsVersion = "34.0.0" + defaultConfig { applicationId = "com.prochy.odesliandroid" minSdk = 21 targetSdk = 34 - versionCode = 1 - versionName = "1.0" + versionCode = 4 + versionName = "1.2.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -21,6 +23,24 @@ android { } } + signingConfigs { + create("release") { + val keystoreFile = System.getenv("RELEASE_KEYSTORE_PATH")?.let { file(it) } ?: file("release.keystore") + val keystorePassword = (System.getenv("RELEASE_KEYSTORE_PASSWORD") ?: "android").trim() + val keyAlias = (System.getenv("RELEASE_KEY_ALIAS") ?: "odesli").trim() + val keyPassword = (System.getenv("RELEASE_KEY_PASSWORD") ?: "android").trim() + + if (keystoreFile.exists() && keystoreFile.length() > 0) { + storeFile = keystoreFile + storePassword = keystorePassword + this.keyAlias = keyAlias + this.keyPassword = keyPassword + } else { + println("Signing Config: Release keystore not found or empty. Falling back to debug signing.") + } + } + } + buildTypes { release { isMinifyEnabled = true @@ -28,7 +48,12 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - signingConfig = signingConfigs.getByName("debug") + val releaseSigning = signingConfigs.getByName("release") + signingConfig = if (releaseSigning.storeFile != null && releaseSigning.storeFile!!.exists()) { + releaseSigning + } else { + signingConfigs.getByName("debug") + } } debug { isMinifyEnabled = false @@ -40,7 +65,6 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_19 sourceCompatibility = JavaVersion.VERSION_19 targetCompatibility = JavaVersion.VERSION_19 } diff --git a/odesli/src/main/java/com/prochy/odesliandroid/activity/Main.kt b/odesli/src/main/java/com/prochy/odesliandroid/activity/Main.kt index 572b22a..cf13562 100644 --- a/odesli/src/main/java/com/prochy/odesliandroid/activity/Main.kt +++ b/odesli/src/main/java/com/prochy/odesliandroid/activity/Main.kt @@ -42,6 +42,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape @@ -79,6 +80,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalTextInputService @@ -104,6 +106,8 @@ import com.prochy.odesliandroid.utils.Utils.Companion.resetButtonColors import com.prochy.odesliandroid.utils.Utils.Companion.resetIconButtonColors import com.prochy.odesliandroid.utils.Utils.Companion.saveBooleanSetting import com.prochy.odesliandroid.utils.Utils.Companion.saveStringSetting +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll class Main : ComponentActivity() { @@ -203,8 +207,7 @@ fun OdesliLayout() { contentWindowInsets = WindowInsets( left = 30, right = 30, - ), - containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), containerColor = MaterialTheme.colorScheme.surfaceContainer, topBar = { LargeTopAppBar( colors = TopAppBarDefaults.topAppBarColors( @@ -254,64 +257,107 @@ fun OdesliLayout() { exit = fadeOut() ) { ExtendedFloatingActionButton( - content = { - Icon( - painter = painterResource(id = R.drawable.ic_send), - contentDescription = "Convert link" - ) - Utils.ButtonSpacer() - Text(text = stringResource(id = R.string.convert_link)) - }, + modifier = Modifier.padding(bottom = 16.dp), onClick = { receivedLinks = false triggeredRequest = true Utils.getMusicData(text, context) { data -> + if (data == null) { + triggeredRequest = false + Toast.makeText(context, context.getString(R.string.unexpected_error), Toast.LENGTH_SHORT).show() + return@getMusicData + } songData = data triggeredRequest = false receivedLinks = true } + }, + content = { + Icon( + painter = painterResource(id = R.drawable.ic_send), + contentDescription = stringResource(id = R.string.convert_link) + ) + Utils.ButtonSpacer() + Text(text = stringResource(id = R.string.convert_link)) } ) } } ) { innerPadding -> + + Column( modifier = Modifier .padding(innerPadding) - .fillMaxHeight(), + .fillMaxHeight() + .verticalScroll(rememberScrollState()), ) { Column( horizontalAlignment = Alignment.CenterHorizontally ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - OutlinedTextField( - modifier = Modifier - .widthIn(1.dp, Dp.Infinity) - .weight(1f), - value = text, - onValueChange = { - text = it - receivedLinks = false - triggeredRequest = false - }, - label = { Text(stringResource(id = R.string.original_url)) }, - singleLine = true - ) - Utils.ButtonSpacer() - DynamicSelectTextField( - modifier = Modifier.weight(1f), - selectedValue = outputService, - options = musicServices, - label = stringResource(id = R.string.output_service), - onValueChangedEvent = { - outputService = it - }, - reset = resetOutputField, - showBackButton = true - ) + val configuration = LocalConfiguration.current + if (configuration.screenWidthDp >= 600) { + // Tablet/Foldable: Row + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + OutlinedTextField( + modifier = Modifier.weight(1f), + value = text, + onValueChange = { + text = it + receivedLinks = false + triggeredRequest = false + }, + label = { Text(stringResource(id = R.string.original_url)) }, + singleLine = true + ) + DynamicSelectTextField( + modifier = Modifier.weight(1f), + selectedValue = outputService, + options = musicServices, + label = stringResource(id = R.string.output_service), + onValueChangedEvent = { + outputService = it + }, + reset = resetOutputField, + showBackButton = true + ) + } + } else { + // Phone: Column + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth(), + value = text, + onValueChange = { + text = it + receivedLinks = false + triggeredRequest = false + }, + label = { Text(stringResource(id = R.string.original_url)) }, + singleLine = true + ) + Utils.ButtonSpacer() + DynamicSelectTextField( + modifier = Modifier + .fillMaxWidth(), + selectedValue = outputService, + options = musicServices, + label = stringResource(id = R.string.output_service), + onValueChangedEvent = { + outputService = it + }, + reset = resetOutputField, + showBackButton = true + ) + } } } Spacer(modifier = Modifier.height(20.dp)) @@ -325,11 +371,11 @@ fun OdesliLayout() { ), shape = RoundedCornerShape(16.dp), modifier = Modifier - .height(600.dp) + .wrapContentHeight() ) { Box( modifier = Modifier - .fillMaxSize(), + .fillMaxWidth(), contentAlignment = Alignment.Center ) { if (triggeredRequest && !receivedLinks) @@ -339,11 +385,16 @@ fun OdesliLayout() { strokeCap = StrokeCap.Round ) if (receivedLinks) { - val thumbnail = songData.entitiesByUniqueId[outputService]?.thumbnailUrl - val title = songData.entitiesByUniqueId[outputService]?.title - val artist = songData.entitiesByUniqueId[outputService]?.artistName + val entity = songData.entitiesByUniqueId[outputService] ?: songData.entitiesByUniqueId.values.firstOrNull() + val thumbnail = entity?.thumbnailUrl + val title = entity?.title + val artist = entity?.artistName val service = getLabelFromService(outputService) - val link = songData.linksByPlatform[outputService]?.url + val link = if (outputService == "odesli") { + songData.pageUrl + } else { + songData.linksByPlatform[outputService]?.url + } val type = songData.entitiesByUniqueId[songData.entitiesByUniqueId.keys.first()]?.type ?: "" Utils.SongInfo( @@ -359,6 +410,10 @@ fun OdesliLayout() { } } + if (!receivedLinks) { + Spacer(modifier = Modifier.height(100.dp)) + } + Spacer(modifier = Modifier.height(100.dp)) if (showCredits) { Dialog( onDismissRequest = { showCredits = false } @@ -573,33 +628,34 @@ fun OdesliLayout() { showBackButton = true ) Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - preferredService.isNotEmpty() - ) { - saveBooleanSetting( - "autoCopy", - !autoCopy, - context - ) - autoCopy = !autoCopy - if (!autoCopy) { - showOnFailed = false + modifier = Modifier + .fillMaxWidth() + .clickable( + preferredService.isNotEmpty() + ) { saveBooleanSetting( - "showOnFailed", - false, + "autoCopy", + !autoCopy, context ) - } - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(id = R.string.auto_copy) - ) - Switch( + autoCopy = !autoCopy + if (!autoCopy) { + showOnFailed = false + saveBooleanSetting( + "showOnFailed", + false, + context + ) + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(id = R.string.auto_copy), + modifier = Modifier.weight(1f) + ) + Switch( enabled = preferredService.isNotEmpty(), checked = autoCopy, onCheckedChange = { @@ -621,25 +677,26 @@ fun OdesliLayout() { ) } Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - preferredService.isNotEmpty() && autoCopy - ) { - saveBooleanSetting( - "showOnFailed", - !showOnFailed, - context - ) - showOnFailed = !showOnFailed - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(id = R.string.show_on_failed_setting) - ) - Switch( + modifier = Modifier + .fillMaxWidth() + .clickable( + preferredService.isNotEmpty() && autoCopy + ) { + saveBooleanSetting( + "showOnFailed", + !showOnFailed, + context + ) + showOnFailed = !showOnFailed + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(id = R.string.show_on_failed_setting), + modifier = Modifier.weight(1f) + ) + Switch( enabled = preferredService.isNotEmpty() && autoCopy, checked = showOnFailed, onCheckedChange = { @@ -739,7 +796,9 @@ fun DynamicSelectTextField( shape = RoundedCornerShape(16.dp), ) { Column( - modifier = Modifier.padding(20.dp) + modifier = Modifier + .padding(20.dp) + .verticalScroll(rememberScrollState()) ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/odesli/src/main/java/com/prochy/odesliandroid/activity/Share.kt b/odesli/src/main/java/com/prochy/odesliandroid/activity/Share.kt index 2b19dec..55df105 100644 --- a/odesli/src/main/java/com/prochy/odesliandroid/activity/Share.kt +++ b/odesli/src/main/java/com/prochy/odesliandroid/activity/Share.kt @@ -13,12 +13,16 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenuItem @@ -85,6 +89,11 @@ class Share : ComponentActivity() { receivedLink = it } } + intent?.action == Intent.ACTION_SEND -> { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + receivedLink = it + } + } } if (receivedLink.isBlank()) { finish() @@ -111,6 +120,15 @@ class Share : ComponentActivity() { ) if (service.isNotBlank() && autoCopy) { Utils.getMusicData(receivedLink, this) { data -> + if (data == null) { + Toast.makeText( + this, + this.getString(R.string.unexpected_error), + Toast.LENGTH_SHORT + ).show() + finish() + return@getMusicData + } val type = data.entitiesByUniqueId[data.entitiesByUniqueId.keys.first()]?.type ?: "" val errorString: String = if (type == "song") { this.getString(R.string.song_not_found_error_popup) @@ -125,11 +143,21 @@ class Share : ComponentActivity() { ).show() finish() } - val platformLink = data.linksByPlatform[service]?.url + val platformLink = if (service == "odesli") { + data.pageUrl + } else { + data.linksByPlatform[service]?.url + } if (!platformLink.isNullOrBlank()) { val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("Link", platformLink) clipboard.setPrimaryClip(clip) + // Feedback for successful copy + Toast.makeText( + this, + this.getString(R.string.link_copied), + Toast.LENGTH_SHORT + ).show() finish() } else { if (showOnFailed) { @@ -206,7 +234,9 @@ fun DynamicSelectTextFieldPopUp( shape = RoundedCornerShape(16.dp), ) { Column( - modifier = Modifier.padding(20.dp) + modifier = Modifier + .padding(20.dp) + .verticalScroll(rememberScrollState()) ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -276,6 +306,12 @@ fun ShareActivityLayout(receivedLink: String) { ) ) } Utils.getMusicData(receivedLink, LocalContext.current) { data -> + if (data == null) { + triggeredRequest.value = false + Toast.makeText(context, context.getString(R.string.unexpected_error), Toast.LENGTH_SHORT).show() + (context as? ComponentActivity)?.finish() + return@getMusicData + } triggeredRequest.value = false receivedLinks.value = true receivedData.value = data @@ -295,13 +331,13 @@ fun ShareActivityLayout(receivedLink: String) { ), shape = RoundedCornerShape(16.dp), modifier = Modifier - .height(dimensionResource(id = R.dimen.card_height)) + .wrapContentHeight() ) { if (triggeredRequest.value) { Box( contentAlignment = Alignment.Center, modifier = Modifier - .fillMaxSize() + .fillMaxWidth() ) { LinearProgressIndicator( color = MaterialTheme.colorScheme.secondary, @@ -326,31 +362,42 @@ fun ShareActivityLayout(receivedLink: String) { } val musicServices = MusicProviders.entries.map { it.service } - val thumbnail = receivedData.value.entitiesByUniqueId[outputService]?.thumbnailUrl - val title = receivedData.value.entitiesByUniqueId[outputService]?.title - val artist = receivedData.value.entitiesByUniqueId[outputService]?.artistName + val targetEntity = if (outputService == "odesli") { + receivedData.value.entitiesByUniqueId.values.firstOrNull() + } else { + receivedData.value.entitiesByUniqueId[outputService] + } + val thumbnail = targetEntity?.thumbnailUrl + val title = targetEntity?.title + val artist = targetEntity?.artistName val service = getLabelFromService(outputService) - val link = receivedData.value.linksByPlatform[outputService]?.url + val link = if (outputService == "odesli") { + receivedData.value.pageUrl + } else { + receivedData.value.linksByPlatform[outputService]?.url + } val type = receivedData.value.entitiesByUniqueId[receivedData.value.entitiesByUniqueId.keys.first()]?.type ?: "" - Utils.SongInfo( - thumbnail = thumbnail.toString(), - title = title.toString(), - artist = artist.toString(), - service = service.toString(), - link = link.toString(), - odesliType = type, - element = { - DynamicSelectTextFieldPopUp( - modifier = Modifier.width(350.dp), - selectedValue = outputService, - options = musicServices, - label = stringResource(id = R.string.output_service), - onValueChangedEvent = { - outputService = it - } - ) - } - ) + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Utils.SongInfo( + thumbnail = thumbnail.toString(), + title = title.toString(), + artist = artist.toString(), + service = service.toString(), + link = link.toString(), + odesliType = type, + element = { + DynamicSelectTextFieldPopUp( + modifier = Modifier.width(350.dp), + selectedValue = outputService, + options = musicServices, + label = stringResource(id = R.string.output_service), + onValueChangedEvent = { + outputService = it + } + ) + } + ) + } } } } diff --git a/odesli/src/main/java/com/prochy/odesliandroid/utils/MusicProviders.kt b/odesli/src/main/java/com/prochy/odesliandroid/utils/MusicProviders.kt index 7d7dba7..d426d9b 100644 --- a/odesli/src/main/java/com/prochy/odesliandroid/utils/MusicProviders.kt +++ b/odesli/src/main/java/com/prochy/odesliandroid/utils/MusicProviders.kt @@ -15,6 +15,7 @@ enum class MusicProviders(val label: String, val service: String) { Deezer("Deezer", "deezer"), Itunes("iTunes", "itunes"), Napster("Napster", "napster"), + Odesli("Odesli Page", "odesli"), Pandora("Pandora", "pandora"), Soundcloud("SoundCloud", "soundcloud"), Spotify("Spotify", "spotify"), diff --git a/odesli/src/main/java/com/prochy/odesliandroid/utils/RetrofitClient.kt b/odesli/src/main/java/com/prochy/odesliandroid/utils/RetrofitClient.kt index ae8c809..39c1cf5 100644 --- a/odesli/src/main/java/com/prochy/odesliandroid/utils/RetrofitClient.kt +++ b/odesli/src/main/java/com/prochy/odesliandroid/utils/RetrofitClient.kt @@ -1,7 +1,5 @@ package com.prochy.odesliandroid.utils -import android.content.Context -import android.widget.Toast import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -10,7 +8,7 @@ import retrofit2.converter.gson.GsonConverterFactory class RetrofitClient { - fun getData(context: Context, link: String, countryCode: String, callback: (OdesliData) -> Unit) { + fun getData(link: String, countryCode: String, callback: (OdesliData?) -> Unit) { val retrofit: Retrofit = Retrofit.Builder().baseUrl("https://api.song.link/").addConverterFactory( GsonConverterFactory.create()).build() @@ -19,17 +17,19 @@ class RetrofitClient { call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if(response.isSuccessful){ - val data: OdesliData = response.body() as OdesliData + val data: OdesliData? = response.body() callback(data) + } else { + callback(null) } } override fun onFailure(call: Call, t: Throwable) { - Toast.makeText(context, "Odesli request Failed", Toast.LENGTH_SHORT).show() + callback(null) } }) } - fun getCountry(context: Context, callback: (LocationData) -> Unit) { + fun getCountry(callback: (LocationData?) -> Unit) { val retrofit: Retrofit = Retrofit.Builder().baseUrl("https://ipinfo.io").addConverterFactory( GsonConverterFactory.create()).build() val service: LocationService = retrofit.create(LocationService::class.java) @@ -37,12 +37,14 @@ class RetrofitClient { call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if(response.isSuccessful){ - val data: LocationData = response.body() as LocationData + val data: LocationData? = response.body() callback(data) + } else { + callback(null) } } override fun onFailure(call: Call, t: Throwable) { - Toast.makeText(context, t.message, Toast.LENGTH_SHORT).show() + callback(null) } }) } diff --git a/odesli/src/main/java/com/prochy/odesliandroid/utils/Utils.kt b/odesli/src/main/java/com/prochy/odesliandroid/utils/Utils.kt index 529b189..24178d8 100644 --- a/odesli/src/main/java/com/prochy/odesliandroid/utils/Utils.kt +++ b/odesli/src/main/java/com/prochy/odesliandroid/utils/Utils.kt @@ -15,10 +15,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledTonalButton @@ -71,16 +75,18 @@ class Utils { } Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly, + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .padding(20.dp) - .fillMaxSize() + .fillMaxWidth() + .wrapContentHeight() + .wrapContentHeight() ) { element() if (link.isEmpty() || link == "null") { Box( modifier = Modifier - .fillMaxSize(), + .fillMaxWidth(), contentAlignment = Alignment.Center ) { Text( @@ -103,16 +109,19 @@ class Utils { Text( artist, style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center, ) Text( service, style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center, ) Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { FilledTonalButton( colors = regularButtonColors(), - content = { Text(text = stringResource(id = R.string.share )) }, + content = { Text(text = stringResource(id = R.string.share ), maxLines = 1, softWrap = false) }, onClick = { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND @@ -124,10 +133,9 @@ class Utils { startActivity(context, shareIntent, null) } ) - ButtonSpacer() FilledTonalButton( colors = regularButtonColors(), - content = { Text(text = stringResource(id = R.string.copy)) }, + content = { Text(text = stringResource(id = R.string.copy), maxLines = 1, softWrap = false) }, onClick = { val clipboard: ClipboardManager? = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager? @@ -135,10 +143,9 @@ class Utils { clipboard?.setPrimaryClip(clip) } ) - ButtonSpacer() FilledTonalButton( colors = regularButtonColors(), - content = { Text(text = stringResource(id = R.string.open )) }, + content = { Text(text = stringResource(id = R.string.open ), maxLines = 1, softWrap = false) }, onClick = { openLink(link, context) } @@ -261,11 +268,15 @@ class Utils { startActivity(context, browserIntent, null) } - fun getMusicData(link: String, context: Context, callback: (OdesliData) -> Unit) { + fun getMusicData(link: String, context: Context, callback: (OdesliData?) -> Unit) { fun retroFitRequest(countryCode: String) { var songData: OdesliData - RetrofitClient().getData(context, link, countryCode) { data -> + RetrofitClient().getData(link, countryCode) { data -> + if (data == null) { + callback(null) + return@getData + } val entitiesByUniqueId = mutableMapOf() data.entitiesByUniqueId.forEach { (_, song) -> // Make sure entities are keyed as service names @@ -289,9 +300,13 @@ class Utils { val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager? var countryCode = tm!!.networkCountryIso if (countryCode.isNullOrEmpty()) { //use a 3rd party service to get country code if device doesn't have SIM - RetrofitClient().getCountry(context) { locationData -> - countryCode = locationData.country - retroFitRequest(countryCode) + RetrofitClient().getCountry() { locationData -> + if (locationData == null) { + callback(null) + } else { + countryCode = locationData.country + retroFitRequest(countryCode) + } } } else { retroFitRequest(countryCode) diff --git a/odesli/src/main/res/values/strings.xml b/odesli/src/main/res/values/strings.xml index 9544741..84a3852 100644 --- a/odesli/src/main/res/values/strings.xml +++ b/odesli/src/main/res/values/strings.xml @@ -24,5 +24,6 @@ Copy Open Image failed to load + Link copied \ No newline at end of file