diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..51831e6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: Build and Release APK + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + + env: + RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} + RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEYSTORE_ALIAS }} + RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} + KEYSTORE_BASE_64: ${{ secrets.KEYSTORE_BASE_64 }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Set up signing key + run: | + echo $KEYSTORE_BASE_64 | base64 -d > release.keystore + echo "RELEASE_STORE_FILE=$(realpath release.keystore)" >> $GITHUB_ENV + + - name: Build release APK + run: ./gradlew assembleRelease + + - name: List Build APKs + run: ls -R ./app/build/outputs/apk/release/ + + - name: Upload APK to GitHub Releases + id: upload-release + uses: softprops/action-gh-release@v2.0.6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: | + ./app/build/outputs/apk/release/app-release.apk + release_tag: ${{ github.event.release.tag_name }} + release_name: Release ${{ github.event.release.tag_name }} + prerelease: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..c36d9cd --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..3aef922 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..44ca2d9 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..6d0ee1c --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..0d3a1fb --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,263 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..e8c9571 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,106 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) + id("com.google.devtools.ksp") +} + +android { + namespace = "com.shub39.rush" + compileSdk = 34 + + defaultConfig { + applicationId = "com.shub39.rush" + minSdk = 29 + targetSdk = 34 + versionCode = 1 + versionName = "0.1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + signingConfigs { + create("release") { + if (System.getenv("RELEASE_STORE_FILE") != null) { + storeFile = file(System.getenv("RELEASE_STORE_FILE")) + storePassword = System.getenv("RELEASE_STORE_PASSWORD") + keyAlias = System.getenv("RELEASE_KEY_ALIAS") + keyPassword = System.getenv("RELEASE_KEY_PASSWORD") + } + } + } + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + if (System.getenv("RELEASE_STORE_FILE") != null) { + signingConfig = signingConfigs["release"] + } + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + implementation(libs.androidx.core.splashscreen) + + implementation(libs.navigation.compose) + implementation(libs.kotlinx.serialization.json) + implementation(libs.retrofit) + implementation(libs.gson) + implementation(libs.converter.gson) + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + implementation(libs.coil.compose) + implementation(libs.coil) + + implementation(libs.androidx.room.runtime) + annotationProcessor(libs.androidx.room.room.compiler) + ksp(libs.androidx.room.room.compiler) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.rxjava2) + implementation(libs.androidx.room.rxjava3) + implementation(libs.androidx.room.guava) + testImplementation(libs.androidx.room.testing) + implementation(libs.androidx.room.paging) + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/app-release.apk b/app/release/app-release.apk new file mode 100644 index 0000000..24a56c6 Binary files /dev/null and b/app/release/app-release.apk differ diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm new file mode 100644 index 0000000..88daf26 Binary files /dev/null and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm new file mode 100644 index 0000000..1d44c6c Binary files /dev/null and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..31c3646 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,37 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.shub39.rush", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "0.1.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File", + "baselineProfiles": [ + { + "minApi": 28, + "maxApi": 30, + "baselineProfiles": [ + "baselineProfiles/1/app-release.dm" + ] + }, + { + "minApi": 31, + "maxApi": 2147483647, + "baselineProfiles": [ + "baselineProfiles/0/app-release.dm" + ] + } + ], + "minSdkVersionForDexing": 29 +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/shub39/rush/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/shub39/rush/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..2bf2164 --- /dev/null +++ b/app/src/androidTest/java/com/shub39/rush/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.shub39.rush + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.shub39.rush", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8c6472e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/MainActivity.kt b/app/src/main/java/com/shub39/rush/MainActivity.kt new file mode 100644 index 0000000..94e8819 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/MainActivity.kt @@ -0,0 +1,37 @@ +package com.shub39.rush + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.navigation.compose.rememberNavController +import com.shub39.rush.component.provideImageLoader +import com.shub39.rush.page.RushApp +import com.shub39.rush.ui.theme.RushTheme +import com.shub39.rush.viewmodel.RushViewModel + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen() + super.onCreate(savedInstanceState) + + enableEdgeToEdge() + setContent { + RushTheme { + val navController = rememberNavController() + val rushViewModel = RushViewModel(application) + val imageLoader = provideImageLoader(context = this) + RushApp( + navController = navController, + rushViewModel = rushViewModel, + imageLoader = imageLoader, + ) + } + } + splashScreen.setKeepOnScreenCondition { + false + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/component/AlbumArt.kt b/app/src/main/java/com/shub39/rush/component/AlbumArt.kt new file mode 100644 index 0000000..760473e --- /dev/null +++ b/app/src/main/java/com/shub39/rush/component/AlbumArt.kt @@ -0,0 +1,57 @@ +package com.shub39.rush.component + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.disk.DiskCache +import coil.request.CachePolicy +import coil.request.ImageRequest +import com.shub39.rush.R +import android.content.Context as Context + + +@Composable +fun ArtFromUrl( + imageUrl: String?, + modifier: Modifier = Modifier, + contentDescription: String? = null, + placeholder: Int = R.drawable.round_music_note_24, + imageLoader: ImageLoader +) { + Image( + painter = rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .apply { + placeholder(placeholder) + error(R.drawable.baseline_landscape_24) + crossfade(true) + } + .build(), + imageLoader = imageLoader + ), + contentDescription = contentDescription, + modifier = modifier, + contentScale = ContentScale.Crop + ) +} + + +@Composable +fun provideImageLoader(context: Context): ImageLoader { + return ImageLoader.Builder(context) + .crossfade(true) + .memoryCachePolicy(CachePolicy.ENABLED) + .diskCachePolicy(CachePolicy.ENABLED) + .diskCache { + DiskCache.Builder() + .directory(context.cacheDir.resolve("image_cache")) + .maxSizePercent(0.02) + .build() + } + .build() +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/component/Empty.kt b/app/src/main/java/com/shub39/rush/component/Empty.kt new file mode 100644 index 0000000..e7a10f6 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/component/Empty.kt @@ -0,0 +1,41 @@ +package com.shub39.rush.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.shub39.rush.R + +@Composable +fun Empty() { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = R.drawable.baseline_library_music_24), + contentDescription = null, + modifier = Modifier.size(128.dp).padding(16.dp), + tint = MaterialTheme.colorScheme.secondary + ) + Text( + text = stringResource(id = R.string.empty), + color = MaterialTheme.colorScheme.secondary + ) + Text( + text = stringResource(id = R.string.suggestion), + color = MaterialTheme.colorScheme.secondary + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/component/SearchResultCard.kt b/app/src/main/java/com/shub39/rush/component/SearchResultCard.kt new file mode 100644 index 0000000..f174cea --- /dev/null +++ b/app/src/main/java/com/shub39/rush/component/SearchResultCard.kt @@ -0,0 +1,59 @@ +package com.shub39.rush.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.ImageLoader +import com.shub39.rush.database.SearchResult + +@Composable +fun SearchResultCard( + result: SearchResult, + onClick: () -> Unit, + imageLoader: ImageLoader +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp), + onClick = { onClick() } + ) { + Row { + ArtFromUrl( + imageUrl = result.artUrl, + contentDescription = result.title, + modifier = Modifier + .size(150.dp) + .padding(16.dp) + .clip(MaterialTheme.shapes.small), + imageLoader = imageLoader + ) + Column( + modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp) + ) { + Text( + text = result.title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = result.artist, + style = MaterialTheme.typography.bodyMedium + ) + result.album?.let { + Text(text = result.album, style = MaterialTheme.typography.bodyMedium) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/component/SongCard.kt b/app/src/main/java/com/shub39/rush/component/SongCard.kt new file mode 100644 index 0000000..5b512fe --- /dev/null +++ b/app/src/main/java/com/shub39/rush/component/SongCard.kt @@ -0,0 +1,82 @@ +package com.shub39.rush.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.ImageLoader +import com.shub39.rush.R +import com.shub39.rush.database.Song + +@Composable +fun SongCard( + result: Song, + onClick: () -> Unit, + onDelete: () -> Unit, + imageLoader: ImageLoader +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp), + onClick = { onClick() } + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + ArtFromUrl( + imageUrl = result.artUrl, + contentDescription = result.title, + modifier = Modifier + .size(70.dp) + .clip(MaterialTheme.shapes.small), + imageLoader = imageLoader + ) + Column( + modifier = Modifier + .padding(start = 8.dp) + .width(240.dp) + ) { + Text( + text = result.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = result.artists, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { onDelete() }) { + Icon( + painter = painterResource(id = R.drawable.round_delete_forever_24), + contentDescription = null, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/database/SearchResult.kt b/app/src/main/java/com/shub39/rush/database/SearchResult.kt new file mode 100644 index 0000000..cbf7779 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/database/SearchResult.kt @@ -0,0 +1,10 @@ +package com.shub39.rush.database + +data class SearchResult( + val title: String, + val artist: String, + val album: String?, + val artUrl: String, + val url: String, + val id: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/database/Song.kt b/app/src/main/java/com/shub39/rush/database/Song.kt new file mode 100644 index 0000000..04b79c2 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/database/Song.kt @@ -0,0 +1,16 @@ +package com.shub39.rush.database + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "songs") +data class Song( + @PrimaryKey(autoGenerate = true) + val id: Long, + val title: String, + val artists: String, + val lyrics: String, + val album: String?, + val sourceUrl: String, + val artUrl: String?, +) \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/database/SongDao.kt b/app/src/main/java/com/shub39/rush/database/SongDao.kt new file mode 100644 index 0000000..42fbb0b --- /dev/null +++ b/app/src/main/java/com/shub39/rush/database/SongDao.kt @@ -0,0 +1,22 @@ +package com.shub39.rush.database + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface SongDao { + @Query("SELECT * FROM songs") + suspend fun getAllSongs(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertSong(song: Song) + + @Delete + suspend fun deleteSong(song: Song) + + @Query("SELECT * FROM songs WHERE id = :id") + suspend fun getSongById(id: Long): Song +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/database/SongDatabase.kt b/app/src/main/java/com/shub39/rush/database/SongDatabase.kt new file mode 100644 index 0000000..9523ee1 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/database/SongDatabase.kt @@ -0,0 +1,28 @@ +package com.shub39.rush.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [Song::class], version = 1, exportSchema = false) +abstract class SongDatabase: RoomDatabase() { + abstract fun songDao(): SongDao + + companion object { + @Volatile + private var INSTANCE: SongDatabase? = null + + fun getDatabase(context: Context): SongDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + SongDatabase::class.java, + "song_database" + ).build() + INSTANCE = instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/genius/ApiService.kt b/app/src/main/java/com/shub39/rush/genius/ApiService.kt new file mode 100644 index 0000000..0f412dd --- /dev/null +++ b/app/src/main/java/com/shub39/rush/genius/ApiService.kt @@ -0,0 +1,15 @@ +package com.shub39.rush.genius + +import com.google.gson.JsonElement +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface ApiService { + @GET("search") + fun search(@Query("q") query: String): Call + + @GET("songs/{songId}") + fun getSong(@Path("songId") songId: Long): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/genius/SongProvider.kt b/app/src/main/java/com/shub39/rush/genius/SongProvider.kt new file mode 100644 index 0000000..d029282 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/genius/SongProvider.kt @@ -0,0 +1,149 @@ +package com.shub39.rush.genius + +import android.util.Log +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.shub39.rush.database.SearchResult +import com.shub39.rush.database.Song +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.IOException +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object SongProvider { + private const val TAG = "GeniusProvider" + private const val BASE_URL = "https://api.genius.com/" + private const val AUTH_HEADER = "Authorization" + private const val BEARER_TOKEN = "Bearer ${Tokens.GENIUS_API}" + + private val apiService: ApiService + + init { + val client = OkHttpClient.Builder().addInterceptor { chain -> + val newRequest: Request = chain.request().newBuilder() + .addHeader(AUTH_HEADER, BEARER_TOKEN) + .build() + chain.proceed(newRequest) + }.build() + + val retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + apiService = retrofit.create(ApiService::class.java) + } + + fun search(query: String): Result> { + return try { + val response: Response = apiService.search(query).execute() + if (response.isSuccessful) { + val jsonHits = response.body()?.asJsonObject + ?.getAsJsonObject("response") + ?.getAsJsonArray("hits") + ?: return Result.failure(Exception("Failed to parse search results")) + + val results = jsonHits.mapNotNull { + try { + val jo = it.asJsonObject.getAsJsonObject("result") + + val title = jo.get("title").asString + val artist = jo.getAsJsonObject("primary_artist").get("name").asString + val album = jo.getAsJsonObject("album")?.get("name")?.asString + val artUrl = jo.get("header_image_thumbnail_url").asString + val url = jo.get("url").asString + val id = jo.get("id").asLong + + SearchResult(title, artist, album, artUrl, url, id) + } catch (e: Exception) { + Log.e(TAG, e.message, e) + null + } + } + + Result.success(results) + } else { + Result.failure(Exception("Search request failed")) + } + } catch (e: IOException) { + Log.e(TAG, e.message, e) + Result.failure(e) + } + } + + fun fetchLyrics(songId: Long): Result { + Log.i(TAG, "Fetching song $songId") + return try { + val response: Response = apiService.getSong(songId).execute() + if (response.isSuccessful) { + val jsonSong = response.body()?.asJsonObject + ?.getAsJsonObject("response") + ?.getAsJsonObject("song") + ?: return Result.failure(Exception("Failed to parse song info")) + + val title = jsonSong.get("title")?.asString ?: "Unknown Title" + val artist = jsonSong.getAsJsonObject("primary_artist")?.get("name")?.asString ?: "Unknown Artist" + val sourceUrl = jsonSong.get("url")?.asString ?: "" + val album = getAlbum(jsonSong) + val artUrl = jsonSong.get("header_image_thumbnail_url")?.asString ?: "" + + val lyricsJsonElement = jsonSong.getAsJsonObject("lyrics")?.getAsJsonObject("dom") + val lyrics = if (lyricsJsonElement != null) { + parseLyricsJsonTag(lyricsJsonElement) + } else { + "Lyrics not available" + } + + Result.success( + Song( + songId, + title, + artist, + lyrics, + album, + sourceUrl, + artUrl + ) + ) + } else { + Result.failure(Exception("Lyrics request failed")) + } + } catch (e: IOException) { + Log.e(TAG, e.message, e) + Result.failure(e) + } catch (e: Exception) { + Log.e(TAG, e.message, e) + Result.failure(e) + } + } + + private fun getAlbum(jsonObject: JsonObject): String? { + val albumJson = jsonObject["album"] ?: return null + if (albumJson.isJsonNull) return null + return albumJson.asJsonObject.get("name").asString + } + + private fun parseLyricsJsonTag(lyricsJsonTag: JsonElement): String { + if (lyricsJsonTag.isJsonPrimitive) return lyricsJsonTag.asString + + val jsonObject = lyricsJsonTag.asJsonObject + if (jsonObject.has("tag") && jsonObject.get("tag").asString == "br") { + return "\n" + } + + if (jsonObject.has("children")) { + var text = "" + val jsonChildren = jsonObject.getAsJsonArray("children") + for (jsonChild in jsonChildren) { + text += parseLyricsJsonTag(jsonChild) + } + return text + } + + return "" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/genius/Tokens.kt b/app/src/main/java/com/shub39/rush/genius/Tokens.kt new file mode 100644 index 0000000..07b9238 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/genius/Tokens.kt @@ -0,0 +1,5 @@ +package com.shub39.rush.genius + +object Tokens { + const val GENIUS_API = "ZTejoT_ojOEasIkT9WrMBhBQOz6eYKK5QULCMECmOhvwqjRZ6WbpamFe3geHnvp3" +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/page/LyricsPage.kt b/app/src/main/java/com/shub39/rush/page/LyricsPage.kt new file mode 100644 index 0000000..6f7008b --- /dev/null +++ b/app/src/main/java/com/shub39/rush/page/LyricsPage.kt @@ -0,0 +1,145 @@ +package com.shub39.rush.page + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.ImageLoader +import com.shub39.rush.R +import com.shub39.rush.component.ArtFromUrl +import com.shub39.rush.component.Empty +import com.shub39.rush.viewmodel.RushViewModel + +@Composable +fun LyricsPage( + rushViewModel: RushViewModel, + imageLoader: ImageLoader +) { + val song by rushViewModel.currentSong.collectAsState() + val fetching by rushViewModel.isFetchingLyrics.collectAsState() + val context = LocalContext.current + + if (fetching) { + Card( + modifier = Modifier + .fillMaxSize() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + strokeCap = StrokeCap.Round + ) + } + } + } else if (song != null) { + Card( + modifier = Modifier + .fillMaxSize() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + ) { + Row( + modifier = Modifier.padding(16.dp), + ) { + ArtFromUrl( + imageUrl = song!!.artUrl, + modifier = Modifier + .size(150.dp) + .clip(MaterialTheme.shapes.small), + imageLoader = imageLoader + ) + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp) + ) { + Text( + text = song!!.title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = song!!.artists, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + song!!.album?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Row { + IconButton(onClick = { openLinkInBrowser(context, song!!.sourceUrl) }) { + Icon( + painter = painterResource(id = R.drawable.genius), + contentDescription = null + ) + } + IconButton(onClick = { /*TODO*/ }) { + Icon( + painter = painterResource(id = R.drawable.round_share_24), + contentDescription = null + ) + } + } + } + } + LazyColumn( + modifier = Modifier.padding(end = 16.dp, start = 16.dp, bottom = 16.dp) + ) { + item { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = song!!.lyrics, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + } + } else { + Card( + modifier = Modifier + .fillMaxSize() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + ) { + Empty() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/page/RushApp.kt b/app/src/main/java/com/shub39/rush/page/RushApp.kt new file mode 100644 index 0000000..5a11d72 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/page/RushApp.kt @@ -0,0 +1,195 @@ +package com.shub39.rush.page + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import coil.ImageLoader +import com.shub39.rush.R +import com.shub39.rush.viewmodel.RushViewModel + +@Composable +fun RushApp( + navController: NavHostController, + rushViewModel: RushViewModel, + imageLoader: ImageLoader +) { + Scaffold( + bottomBar = { BottomBar(navController = navController) }, + topBar = { TopBar(navController = navController) } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = Screens.SavedPage.route, + modifier = Modifier.padding(innerPadding), + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) }, + popEnterTransition = { fadeIn(animationSpec = tween(200)) }, + popExitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + composable(Screens.LyricsPage.route) { + LyricsPage( + rushViewModel = rushViewModel, + imageLoader = imageLoader + ) + } + composable(Screens.SearchPage.route) { + SearchPage( + rushViewModel = rushViewModel, + navController = navController, + imageLoader = imageLoader + ) + } + composable(Screens.SavedPage.route) { + SavedPage( + rushViewModel = rushViewModel, + navController = navController, + imageLoader = imageLoader + ) + } + composable(Screens.SettingsPage.route) { + SettingPage( + rushViewModel = rushViewModel, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBar(navController: NavController) { + val currentBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = currentBackStackEntry?.destination + + TopAppBar( + title = { + if (currentDestination?.route == Screens.SettingsPage.route) { + Text( + text = stringResource(id = R.string.settings), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.primary + ) + } else { + Text( + text = stringResource(id = R.string.app_name), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.primary + ) + } + }, + actions = { + BackHandler( + enabled = currentDestination?.route == Screens.SettingsPage.route + ) { + navController.navigateUp() + } + + IconButton( + onClick = { + if (currentDestination?.route != Screens.SettingsPage.route) { + navController.navigate(Screens.SettingsPage.route){ + launchSingleTop = true + restoreState = true + } + } else { + navController.navigateUp() + } + } + ) { + if (currentDestination?.route != Screens.SettingsPage.route) { + Icon( + painter = painterResource(id = R.drawable.round_settings_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } else { + Icon( + painter = painterResource(id = R.drawable.round_arrow_back_24), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + } + }, + ) +} + +@Composable +fun BottomBar(navController: NavController) { + val screens = listOf( + Screens.LyricsPage, + Screens.SearchPage, + Screens.SavedPage + ) + + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + NavigationBar { + screens.forEach { screen -> + NavigationBarItem( + selected = currentRoute == screen.route, + onClick = { + if (currentRoute != Screens.SettingsPage.route) { + navController.navigate(screen.route) { + launchSingleTop = true + restoreState = true + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + } + } + }, + icon = { + Icon( + painter = painterResource(id = screen.imageId), + contentDescription = null + ) + }, + label = { Text(stringResource(id = screen.labelId)) }, + alwaysShowLabel = false + ) + } + } +} + +fun openLinkInBrowser(context: Context, url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) +} + +sealed class Screens( + val route: String, + val imageId: Int, + val labelId: Int +) { + data object LyricsPage : Screens("lyrics", R.drawable.round_lyrics_24, R.string.lyrics) + data object SearchPage : Screens("search", R.drawable.round_search_24, R.string.search) + data object SavedPage : Screens("saved", R.drawable.round_download_24, R.string.saved) + data object SettingsPage : Screens("settings", R.drawable.round_settings_24, R.string.settings) +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/page/SavedPage.kt b/app/src/main/java/com/shub39/rush/page/SavedPage.kt new file mode 100644 index 0000000..86a064a --- /dev/null +++ b/app/src/main/java/com/shub39/rush/page/SavedPage.kt @@ -0,0 +1,54 @@ +package com.shub39.rush.page + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import coil.ImageLoader +import com.shub39.rush.component.Empty +import com.shub39.rush.component.SongCard +import com.shub39.rush.viewmodel.RushViewModel + +@Composable +fun SavedPage( + navController: NavController, + rushViewModel: RushViewModel, + imageLoader: ImageLoader +) { + val songs = rushViewModel.songs.collectAsState() + + if (songs.value.isEmpty()) { + Empty() + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .animateContentSize() + ) { + items(songs.value, key = { it.id }) { + SongCard( + result = it, + onDelete = { + rushViewModel.deleteSong(it) + }, + onClick = { + rushViewModel.changeCurrentSong(it.id) + navController.navigate(Screens.LyricsPage.route) { + launchSingleTop = true + restoreState = true + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + } + }, + imageLoader = imageLoader + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/page/SearchPage.kt b/app/src/main/java/com/shub39/rush/page/SearchPage.kt new file mode 100644 index 0000000..0743249 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/page/SearchPage.kt @@ -0,0 +1,133 @@ +package com.shub39.rush.page + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import coil.ImageLoader +import com.shub39.rush.R +import com.shub39.rush.component.Empty +import com.shub39.rush.component.SearchResultCard +import com.shub39.rush.viewmodel.RushViewModel + +@Composable +fun SearchPage( + rushViewModel: RushViewModel, + navController: NavController, + imageLoader: ImageLoader +) { + var query by remember { mutableStateOf("") } + val searchResults by rushViewModel.searchResults.collectAsState() + val keyboardController = LocalSoftwareKeyboardController.current + val isFetchingLyrics by rushViewModel.isSearchingLyrics.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + OutlinedTextField( + value = query, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.round_search_24), + contentDescription = null + ) + }, + trailingIcon = { + if (isFetchingLyrics) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeCap = StrokeCap.Round + ) + } + }, + onValueChange = { + query = it + if (query.isNotBlank()) { + rushViewModel.searchSong(it) + } + }, + shape = MaterialTheme.shapes.large, + label = { Text(stringResource(id = R.string.search)) }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp, start = 32.dp, end = 32.dp), + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Go + ), + keyboardActions = KeyboardActions( + onGo = { + keyboardController?.hide() + } + ) + ) + + if (searchResults.isEmpty()) { + Empty() + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (searchResults.isEmpty() && query.isNotBlank()) { + item { + Spacer(modifier = Modifier.padding(16.dp)) + CircularProgressIndicator() + } + } else { + items(searchResults, key = { it.id }) { + SearchResultCard( + result = it, + onClick = { + rushViewModel.changeCurrentSong(it.id) + navController.navigate(Screens.LyricsPage.route){ + launchSingleTop = true + restoreState = true + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + } + }, + imageLoader = imageLoader + ) + } + item { + Spacer(modifier = Modifier.padding(60.dp)) + } + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/page/SettingPage.kt b/app/src/main/java/com/shub39/rush/page/SettingPage.kt new file mode 100644 index 0000000..69e41f5 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/page/SettingPage.kt @@ -0,0 +1,88 @@ +package com.shub39.rush.page + +import androidx.compose.foundation.layout.Arrangement +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.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.shub39.rush.R +import com.shub39.rush.viewmodel.RushViewModel + +@Composable +fun SettingPage( + rushViewModel: RushViewModel, +) { + val songs by rushViewModel.songs.collectAsState() + val context = LocalContext.current + + Column( + modifier = Modifier.fillMaxSize() + ) { + Card( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) { + Row { + Text( + text = stringResource(id = R.string.downloaded), + ) + Spacer(modifier = Modifier.padding(4.dp)) + Text( + text = songs.size.toString(), + fontWeight = FontWeight.Bold + ) + } + Button( + onClick = { + rushViewModel.songs.value.forEach { + rushViewModel.deleteSong(it) + } + }, + shape = MaterialTheme.shapes.small + ) { + Text(text = stringResource(id = R.string.delete_all)) + } + } + } + + OutlinedCard( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 8.dp) + .fillMaxWidth(), + onClick = { + openLinkInBrowser(context, "https://github.com/shub39/Rush") + } + ) { + Text( + text = "Made by shub39", + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/ui/theme/Color.kt b/app/src/main/java/com/shub39/rush/ui/theme/Color.kt new file mode 100644 index 0000000..f7b988e --- /dev/null +++ b/app/src/main/java/com/shub39/rush/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.shub39.rush.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/ui/theme/Theme.kt b/app/src/main/java/com/shub39/rush/ui/theme/Theme.kt new file mode 100644 index 0000000..c5e918c --- /dev/null +++ b/app/src/main/java/com/shub39/rush/ui/theme/Theme.kt @@ -0,0 +1,56 @@ +package com.shub39.rush.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun RushTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/ui/theme/Type.kt b/app/src/main/java/com/shub39/rush/ui/theme/Type.kt new file mode 100644 index 0000000..971acd3 --- /dev/null +++ b/app/src/main/java/com/shub39/rush/ui/theme/Type.kt @@ -0,0 +1,106 @@ +package com.shub39.rush.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.shub39.rush.R + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ), + displayLarge = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp + ), + displayMedium = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp + ), + displaySmall = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp + ), + headlineLarge = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp + ), + headlineMedium = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp + ), + headlineSmall = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp + ), + titleMedium = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp + ), + titleSmall = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp + ), + bodyMedium = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp + ), + bodySmall = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp + ), + labelLarge = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp + ), + labelMedium = TextStyle( + fontFamily = FontFamily(Font(R.font.poppins_regular)), + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp + ), +) \ No newline at end of file diff --git a/app/src/main/java/com/shub39/rush/viewmodel/RushViewModel.kt b/app/src/main/java/com/shub39/rush/viewmodel/RushViewModel.kt new file mode 100644 index 0000000..c5d90af --- /dev/null +++ b/app/src/main/java/com/shub39/rush/viewmodel/RushViewModel.kt @@ -0,0 +1,97 @@ +package com.shub39.rush.viewmodel + +import android.app.Application +import android.content.ContentValues.TAG +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.shub39.rush.database.SearchResult +import com.shub39.rush.database.Song +import com.shub39.rush.database.SongDatabase +import com.shub39.rush.genius.SongProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class RushViewModel(application: Application) : ViewModel() { + + private val database = SongDatabase.getDatabase(application) + private val songDao = database.songDao() + private val _songs = MutableStateFlow(listOf()) + private val _searchResults = MutableStateFlow(listOf()) + private val _currentSongId = MutableStateFlow(null) + private val _currentSong = MutableStateFlow(null) + private val _isSearchingLyrics = MutableStateFlow(false) + private val _isFetchingLyrics = MutableStateFlow(false) + + val songs: StateFlow> get() = _songs + val searchResults: StateFlow> get() = _searchResults + val currentSong: MutableStateFlow get() = _currentSong + val isSearchingLyrics: StateFlow get() = _isSearchingLyrics + val isFetchingLyrics: StateFlow get() = _isFetchingLyrics + + init { + viewModelScope.launch { + _songs.value = songDao.getAllSongs() + } + } + + fun changeCurrentSong(songId: Long) { + _currentSongId.value = songId + fetchLyrics(songId) + } + + fun deleteSong(song: Song) { + viewModelScope.launch { + songDao.deleteSong(song) + _songs.value = songDao.getAllSongs() + } + } + + fun searchSong(query: String) { + viewModelScope.launch { + _isSearchingLyrics.value = true + try { + val result = withContext(Dispatchers.IO) { + SongProvider.search(query) + } + if (result.isSuccess) { + _searchResults.value = result.getOrNull() ?: emptyList() + } else { + Log.e(TAG, result.exceptionOrNull()?.message, result.exceptionOrNull()) + _searchResults.value = emptyList() + } + } finally { + _isSearchingLyrics.value = false + } + } + } + + private fun fetchLyrics(songId: Long = _currentSongId.value!!) { + viewModelScope.launch { + _isFetchingLyrics.value = true + try { + if (songId in songs.value.map { it.id }) { + val result = songDao.getSongById(songId) + _currentSong.value = result + } else { + val result = withContext(Dispatchers.IO) { + SongProvider.fetchLyrics(songId) + } + if (result.isSuccess) { + _currentSong.value = result.getOrNull() + songDao.insertSong(_currentSong.value!!) + _songs.value = songDao.getAllSongs() + } else { + Log.e(TAG, result.exceptionOrNull()?.message, result.exceptionOrNull()) + } + } + } finally { + _isFetchingLyrics.value = false + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml b/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..1c27eed --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..da472c8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_background.png b/app/src/main/res/drawable-hdpi/ic_launcher_background.png new file mode 100644 index 0000000..a221890 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..c1ba9b5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..c1ba9b5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..65c68c4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_background.png b/app/src/main/res/drawable-mdpi/ic_launcher_background.png new file mode 100644 index 0000000..a611674 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..5e5b6f1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..5e5b6f1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..3aa5f9e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_background.png b/app/src/main/res/drawable-xhdpi/ic_launcher_background.png new file mode 100644 index 0000000..fc6d398 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8da7c49 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..8da7c49 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..9c4eaf8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..bc454c7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..1414e2c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..1414e2c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..8bfab6d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..b1836b2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ed8b665 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png b/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..ed8b665 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/app/src/main/res/drawable/baseline_landscape_24.xml b/app/src/main/res/drawable/baseline_landscape_24.xml new file mode 100644 index 0000000..375990c --- /dev/null +++ b/app/src/main/res/drawable/baseline_landscape_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_library_music_24.xml b/app/src/main/res/drawable/baseline_library_music_24.xml new file mode 100644 index 0000000..f5e11b2 --- /dev/null +++ b/app/src/main/res/drawable/baseline_library_music_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/genius.xml b/app/src/main/res/drawable/genius.xml new file mode 100644 index 0000000..222e797 --- /dev/null +++ b/app/src/main/res/drawable/genius.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..cbcfcce --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..27f6691 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/round_arrow_back_24.xml b/app/src/main/res/drawable/round_arrow_back_24.xml new file mode 100644 index 0000000..85258df --- /dev/null +++ b/app/src/main/res/drawable/round_arrow_back_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_delete_forever_24.xml b/app/src/main/res/drawable/round_delete_forever_24.xml new file mode 100644 index 0000000..0ab7888 --- /dev/null +++ b/app/src/main/res/drawable/round_delete_forever_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_download_24.xml b/app/src/main/res/drawable/round_download_24.xml new file mode 100644 index 0000000..5c4fadd --- /dev/null +++ b/app/src/main/res/drawable/round_download_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_lyrics_24.xml b/app/src/main/res/drawable/round_lyrics_24.xml new file mode 100644 index 0000000..58ef188 --- /dev/null +++ b/app/src/main/res/drawable/round_lyrics_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/round_music_note_24.xml b/app/src/main/res/drawable/round_music_note_24.xml new file mode 100644 index 0000000..679b543 --- /dev/null +++ b/app/src/main/res/drawable/round_music_note_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_queue_music_24.xml b/app/src/main/res/drawable/round_queue_music_24.xml new file mode 100644 index 0000000..244ca0b --- /dev/null +++ b/app/src/main/res/drawable/round_queue_music_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_search_24.xml b/app/src/main/res/drawable/round_search_24.xml new file mode 100644 index 0000000..19881dc --- /dev/null +++ b/app/src/main/res/drawable/round_search_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_settings_24.xml b/app/src/main/res/drawable/round_settings_24.xml new file mode 100644 index 0000000..bc83407 --- /dev/null +++ b/app/src/main/res/drawable/round_settings_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_share_24.xml b/app/src/main/res/drawable/round_share_24.xml new file mode 100644 index 0000000..9206c49 --- /dev/null +++ b/app/src/main/res/drawable/round_share_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/font/poppins_regular.ttf b/app/src/main/res/font/poppins_regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/app/src/main/res/font/poppins_regular.ttf differ diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/splash.xml b/app/src/main/res/values-night/splash.xml new file mode 100644 index 0000000..f931e56 --- /dev/null +++ b/app/src/main/res/values-night/splash.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..905b72e --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,11 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #FF282828 + \ No newline at end of file diff --git a/app/src/main/res/values/splash.xml b/app/src/main/res/values/splash.xml new file mode 100644 index 0000000..2e92855 --- /dev/null +++ b/app/src/main/res/values/splash.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..1b25bf0 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + Rush + Lyrics + Search + Saved + Settings + Nothing yet… + Maybe search for something? + Saved lyrics + Delete all + Genius + Share + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..a6784eb --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + +