diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index f1727aa..3a504e6 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -13,6 +13,7 @@
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 0ad17cb..8978d23 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 67c4325..1579bb0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,8 +1,9 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import com.google.protobuf.gradle.builtins
import com.google.protobuf.gradle.generateProtoTasks
import com.google.protobuf.gradle.protobuf
import com.google.protobuf.gradle.protoc
-import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id ("com.android.application")
@@ -14,6 +15,7 @@ plugins {
id("com.google.protobuf") version "0.8.19"
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
+ id("dev.shreyaspatil.compose-compiler-report-generator") version "1.1.0"
}
android {
@@ -24,6 +26,12 @@ android {
keyAlias = gradleLocalProperties(rootDir)["KEY_ALIAS"] as String
keyPassword = gradleLocalProperties(rootDir)["KEY_PASSWORD"] as String
}
+ create("ir"){
+ storeFile = gradleLocalProperties(rootDir)["IR_STORE_FILE"]?.let { file(it) }
+ storePassword = gradleLocalProperties(rootDir)["IR_STORE_PASSWORD"] as String
+ keyAlias = gradleLocalProperties(rootDir)["IR_KEY_ALIAS"] as String
+ keyPassword = gradleLocalProperties(rootDir)["IR_KEY_PASSWORD"] as String
+ }
}
namespace = "com.github.pakka_papad"
compileSdk = Api.compileSdk
@@ -54,6 +62,7 @@ android {
buildTypes {
debug {
versionNameSuffix = "-debug"
+ applicationIdSuffix = ".debug"
resValue("string","app_version_name",AppVersion.Name+versionNameSuffix)
}
release {
@@ -66,15 +75,27 @@ android {
signingConfig = signingConfigs.findByName("prod")
resValue("string","app_version_name",AppVersion.Name)
}
+ create("internalRelease") {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ versionNameSuffix = "-ir"
+ applicationIdSuffix = ".ir"
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ signingConfig = signingConfigs.findByName("ir")
+ resValue("string","app_version_name",AppVersion.Name+versionNameSuffix)
+ }
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "11"
}
buildFeatures {
@@ -95,6 +116,7 @@ android {
}
dependencies {
+ implementation(project(":m3utils"))
implementation(Libraries.androidxCore)
implementation(Libraries.androidxLifecycle)
@@ -115,6 +137,8 @@ dependencies {
implementation(Libraries.androidxGlanceMaterial)
implementation(Libraries.androidxGlanceMaterial3)
+ implementation(Libraries.androidxWorkManager)
+
implementation(Libraries.androidxComposeMaterial)
implementation(Libraries.material3)
implementation(Libraries.material3WindowSizeClass)
@@ -129,12 +153,15 @@ dependencies {
testImplementation(Libraries.junit)
androidTestImplementation(Libraries.androidxJunit)
+ androidTestImplementation(Libraries.androidxTestKtx)
androidTestImplementation(Libraries.androidxEspresso)
debugImplementation(Libraries.leakcanary)
implementation(Libraries.hilt)
+ implementation(Libraries.hiltWork)
kapt(AnnotationProcessors.hiltCompiler)
+ kapt(AnnotationProcessors.hiltCompilerWork)
implementation(Libraries.timber)
@@ -155,6 +182,19 @@ dependencies {
implementation(Libraries.coilCompose)
implementation(Libraries.palette)
implementation(Libraries.lottie)
+
+ implementation(Libraries.crashActivity)
+
+ testImplementation(Libraries.mockk)
+ testImplementation(Libraries.coroutinesTest)
+}
+
+allprojects {
+ tasks.withType().configureEach {
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+ }
}
protobuf {
diff --git a/app/schemas/com.github.pakka_papad.data.AppDatabase/3.json b/app/schemas/com.github.pakka_papad.data.AppDatabase/3.json
new file mode 100644
index 0000000..1e26488
--- /dev/null
+++ b/app/schemas/com.github.pakka_papad.data.AppDatabase/3.json
@@ -0,0 +1,680 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "4b1dcc01f92eb411ad3d2e6217a07b2f",
+ "entities": [
+ {
+ "tableName": "song_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`location` TEXT NOT NULL, `title` TEXT NOT NULL, `album` TEXT NOT NULL DEFAULT 'Unknown', `size` TEXT NOT NULL, `addedDate` TEXT NOT NULL, `modifiedDate` TEXT NOT NULL, `artist` TEXT NOT NULL DEFAULT 'Unknown', `albumArtist` TEXT NOT NULL DEFAULT 'Unknown', `composer` TEXT NOT NULL DEFAULT 'Unknown', `genre` TEXT NOT NULL DEFAULT 'Unknown', `lyricist` TEXT NOT NULL DEFAULT 'Unknown', `year` INTEGER NOT NULL, `comment` TEXT, `durationMillis` INTEGER NOT NULL, `durationFormatted` TEXT NOT NULL, `bitrate` REAL NOT NULL, `sampleRate` REAL NOT NULL, `bitsPerSample` INTEGER NOT NULL, `mimeType` TEXT, `favourite` INTEGER NOT NULL, `artUri` TEXT, `playCount` INTEGER NOT NULL DEFAULT 0, `lastPlayed` INTEGER, PRIMARY KEY(`location`), FOREIGN KEY(`album`) REFERENCES `album_table`(`name`) ON UPDATE NO ACTION ON DELETE SET DEFAULT , FOREIGN KEY(`artist`) REFERENCES `artist_table`(`name`) ON UPDATE NO ACTION ON DELETE SET DEFAULT , FOREIGN KEY(`genre`) REFERENCES `genre_table`(`genre`) ON UPDATE NO ACTION ON DELETE SET DEFAULT , FOREIGN KEY(`albumArtist`) REFERENCES `album_artist_table`(`name`) ON UPDATE NO ACTION ON DELETE SET DEFAULT , FOREIGN KEY(`lyricist`) REFERENCES `lyricist_table`(`name`) ON UPDATE NO ACTION ON DELETE SET DEFAULT , FOREIGN KEY(`composer`) REFERENCES `composer_table`(`name`) ON UPDATE NO ACTION ON DELETE SET DEFAULT )",
+ "fields": [
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "album",
+ "columnName": "album",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'Unknown'"
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedDate",
+ "columnName": "addedDate",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modifiedDate",
+ "columnName": "modifiedDate",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'Unknown'"
+ },
+ {
+ "fieldPath": "albumArtist",
+ "columnName": "albumArtist",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'Unknown'"
+ },
+ {
+ "fieldPath": "composer",
+ "columnName": "composer",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'Unknown'"
+ },
+ {
+ "fieldPath": "genre",
+ "columnName": "genre",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'Unknown'"
+ },
+ {
+ "fieldPath": "lyricist",
+ "columnName": "lyricist",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'Unknown'"
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "durationMillis",
+ "columnName": "durationMillis",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "durationFormatted",
+ "columnName": "durationFormatted",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bitrate",
+ "columnName": "bitrate",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sampleRate",
+ "columnName": "sampleRate",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bitsPerSample",
+ "columnName": "bitsPerSample",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mimeType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "favourite",
+ "columnName": "favourite",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "artUri",
+ "columnName": "artUri",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "playCount",
+ "columnName": "playCount",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastPlayed",
+ "columnName": "lastPlayed",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "location"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_song_table_album",
+ "unique": false,
+ "columnNames": [
+ "album"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_song_table_album` ON `${TABLE_NAME}` (`album`)"
+ },
+ {
+ "name": "index_song_table_artist",
+ "unique": false,
+ "columnNames": [
+ "artist"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_song_table_artist` ON `${TABLE_NAME}` (`artist`)"
+ },
+ {
+ "name": "index_song_table_albumArtist",
+ "unique": false,
+ "columnNames": [
+ "albumArtist"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_song_table_albumArtist` ON `${TABLE_NAME}` (`albumArtist`)"
+ },
+ {
+ "name": "index_song_table_composer",
+ "unique": false,
+ "columnNames": [
+ "composer"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_song_table_composer` ON `${TABLE_NAME}` (`composer`)"
+ },
+ {
+ "name": "index_song_table_genre",
+ "unique": false,
+ "columnNames": [
+ "genre"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_song_table_genre` ON `${TABLE_NAME}` (`genre`)"
+ },
+ {
+ "name": "index_song_table_lyricist",
+ "unique": false,
+ "columnNames": [
+ "lyricist"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_song_table_lyricist` ON `${TABLE_NAME}` (`lyricist`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "album_table",
+ "onDelete": "SET DEFAULT",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "album"
+ ],
+ "referencedColumns": [
+ "name"
+ ]
+ },
+ {
+ "table": "artist_table",
+ "onDelete": "SET DEFAULT",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "artist"
+ ],
+ "referencedColumns": [
+ "name"
+ ]
+ },
+ {
+ "table": "genre_table",
+ "onDelete": "SET DEFAULT",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "genre"
+ ],
+ "referencedColumns": [
+ "genre"
+ ]
+ },
+ {
+ "table": "album_artist_table",
+ "onDelete": "SET DEFAULT",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "albumArtist"
+ ],
+ "referencedColumns": [
+ "name"
+ ]
+ },
+ {
+ "table": "lyricist_table",
+ "onDelete": "SET DEFAULT",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "lyricist"
+ ],
+ "referencedColumns": [
+ "name"
+ ]
+ },
+ {
+ "table": "composer_table",
+ "onDelete": "SET DEFAULT",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "composer"
+ ],
+ "referencedColumns": [
+ "name"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "album_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `albumArtUri` TEXT, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "albumArtUri",
+ "columnName": "albumArtUri",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "artist_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "playlist_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistName` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `artUri` TEXT DEFAULT NULL)",
+ "fields": [
+ {
+ "fieldPath": "playlistId",
+ "columnName": "playlistId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "playlistName",
+ "columnName": "playlistName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "artUri",
+ "columnName": "artUri",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "playlistId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "playlist_song_cross_ref_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` INTEGER NOT NULL, `location` TEXT NOT NULL, PRIMARY KEY(`playlistId`, `location`), FOREIGN KEY(`playlistId`) REFERENCES `playlist_table`(`playlistId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`location`) REFERENCES `song_table`(`location`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "playlistId",
+ "columnName": "playlistId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "playlistId",
+ "location"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_playlist_song_cross_ref_table_location",
+ "unique": false,
+ "columnNames": [
+ "location"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_song_cross_ref_table_location` ON `${TABLE_NAME}` (`location`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "playlist_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "playlistId"
+ ],
+ "referencedColumns": [
+ "playlistId"
+ ]
+ },
+ {
+ "table": "song_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "location"
+ ],
+ "referencedColumns": [
+ "location"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "genre_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`genre` TEXT NOT NULL, PRIMARY KEY(`genre`))",
+ "fields": [
+ {
+ "fieldPath": "genre",
+ "columnName": "genre",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "genre"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "album_artist_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "composer_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "lyricist_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "blacklist_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`location` TEXT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, PRIMARY KEY(`location`))",
+ "fields": [
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "artist",
+ "columnName": "artist",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "location"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "blacklisted_folder_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, PRIMARY KEY(`path`))",
+ "fields": [
+ {
+ "fieldPath": "path",
+ "columnName": "path",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "path"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "play_history_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songLocation` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playDuration` INTEGER NOT NULL, FOREIGN KEY(`songLocation`) REFERENCES `song_table`(`location`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "songLocation",
+ "columnName": "songLocation",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "playDuration",
+ "columnName": "playDuration",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_play_history_table_songLocation",
+ "unique": false,
+ "columnNames": [
+ "songLocation"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_play_history_table_songLocation` ON `${TABLE_NAME}` (`songLocation`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "song_table",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "songLocation"
+ ],
+ "referencedColumns": [
+ "location"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "thumbnail_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`location` TEXT NOT NULL, `addedOn` INTEGER NOT NULL, `artCount` INTEGER NOT NULL, `deleteThis` INTEGER NOT NULL, PRIMARY KEY(`location`))",
+ "fields": [
+ {
+ "fieldPath": "location",
+ "columnName": "location",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addedOn",
+ "columnName": "addedOn",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "artCount",
+ "columnName": "artCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deleteThis",
+ "columnName": "deleteThis",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "location"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_thumbnail_table_location",
+ "unique": true,
+ "columnNames": [
+ "location"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_thumbnail_table_location` ON `${TABLE_NAME}` (`location`)"
+ }
+ ],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4b1dcc01f92eb411ad3d2e6217a07b2f')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/github/pakka_papad/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/github/pakka_papad/ExampleInstrumentedTest.kt
deleted file mode 100644
index ef1bcf0..0000000
--- a/app/src/androidTest/java/com/github/pakka_papad/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.pakka_papad
-
-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("tech.zemn.mobile", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 0000000..fb410b5
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Debug Zen Music
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6671a36..13e0177 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -33,6 +33,13 @@
android:value="" />
+
+
+
@@ -46,6 +53,17 @@
android:name="android.appwidget.provider"
android:resource="@xml/music_control_widget_info" />
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/changelogs.json b/app/src/main/assets/changelogs.json
index 7a39b56..1e8ae15 100644
--- a/app/src/main/assets/changelogs.json
+++ b/app/src/main/assets/changelogs.json
@@ -34,5 +34,15 @@
"Added sort option",
"Added basic widget (experimental)"
]
+ },
+ {
+ "versionCode": 102002,
+ "versionName": "1.2.2",
+ "date": "6 January, 2024",
+ "changes": [
+ "Added sleep timer",
+ "Updated player screen UI",
+ "Updated playlist tab UI"
+ ]
}
]
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/Constants.kt b/app/src/main/java/com/github/pakka_papad/Constants.kt
index 1b6b5e6..23f441c 100644
--- a/app/src/main/java/com/github/pakka_papad/Constants.kt
+++ b/app/src/main/java/com/github/pakka_papad/Constants.kt
@@ -15,6 +15,8 @@ object Constants {
const val BLACKLIST_TABLE = "blacklist_table" // for songs
const val BLACKLISTED_FOLDER_TABLE = "blacklisted_folder_table"
const val PLAY_HISTORY_TABLE = "play_history_table"
+ const val THUMBNAIL_TABLE = "thumbnail_table"
}
- const val PACKAGE_NAME = "com.github.pakka_papad"
+ const val PACKAGE_NAME = BuildConfig.APPLICATION_ID
+ const val MESSAGE_DURATION = 3500L
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/CrashActivity.kt b/app/src/main/java/com/github/pakka_papad/CrashActivity.kt
new file mode 100644
index 0000000..2268ccb
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/CrashActivity.kt
@@ -0,0 +1,96 @@
+package com.github.pakka_papad
+
+import android.graphics.Color
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+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.sizeIn
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+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.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.core.view.WindowCompat
+import cat.ereza.customactivityoncrash.CustomActivityOnCrash
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.github.pakka_papad.ui.theme.DefaultTheme
+
+class CrashActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val config = CustomActivityOnCrash.getConfigFromIntent(intent) ?: run {
+ finish()
+ return
+ }
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ window.statusBarColor = Color.TRANSPARENT
+
+ setContent {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.bug))
+ DefaultTheme {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .widthIn(max = 500.dp)
+ .background(MaterialTheme.colorScheme.surface)
+ .windowInsetsPadding(WindowInsets.systemBars)
+ .padding(20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ LottieAnimation(
+ composition = composition,
+ iterations = 200,
+ modifier = Modifier
+ .weight(1f)
+ .sizeIn(maxWidth = 400.dp, maxHeight = 400.dp)
+ )
+ Text(
+ text = getString(cat.ereza.customactivityoncrash.R.string.customactivityoncrash_error_activity_error_occurred_explanation),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp)
+ .clip(MaterialTheme.shapes.large)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .clickable {
+ CustomActivityOnCrash.restartApplication(this@CrashActivity, config)
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Restart",
+ color = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ZenApp.kt b/app/src/main/java/com/github/pakka_papad/ZenApp.kt
index 746e326..2a1c050 100644
--- a/app/src/main/java/com/github/pakka_papad/ZenApp.kt
+++ b/app/src/main/java/com/github/pakka_papad/ZenApp.kt
@@ -2,20 +2,43 @@ package com.github.pakka_papad
import android.app.Application
import android.graphics.Bitmap
+import androidx.hilt.work.HiltWorkerFactory
+import androidx.work.Configuration
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import cat.ereza.customactivityoncrash.config.CaocConfig
+import cat.ereza.customactivityoncrash.config.CaocConfig.BACKGROUND_MODE_SILENT
import coil.ImageLoader
import coil.ImageLoaderFactory
+import com.github.pakka_papad.workers.ThumbnailWorker
+import com.google.firebase.FirebaseApp
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
+import javax.inject.Inject
@HiltAndroidApp
-class ZenApp: Application(), ImageLoaderFactory {
+class ZenApp: Application(), ImageLoaderFactory, Configuration.Provider {
+
+ @Inject lateinit var hiltWorkerFactory: HiltWorkerFactory
override fun onCreate() {
super.onCreate()
+ FirebaseApp.initializeApp(this)
+
if (BuildConfig.DEBUG){
Timber.plant(Timber.DebugTree())
}
+
+ CaocConfig.Builder.create().apply {
+ restartActivity(MainActivity::class.java)
+ errorActivity(CrashActivity::class.java)
+ backgroundMode(BACKGROUND_MODE_SILENT)
+ apply()
+ }
+
+ WorkManager.getInstance(this)
+ .enqueue(OneTimeWorkRequestBuilder().build())
}
override fun newImageLoader(): ImageLoader {
@@ -25,4 +48,10 @@ class ZenApp: Application(), ImageLoaderFactory {
error(R.drawable.error)
}.build()
}
+
+ override fun getWorkManagerConfiguration(): Configuration {
+ return Configuration.Builder()
+ .setWorkerFactory(hiltWorkerFactory)
+ .build()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/collection/CollectionContent.kt b/app/src/main/java/com/github/pakka_papad/collection/CollectionContent.kt
index 8a74594..efe4252 100644
--- a/app/src/main/java/com/github/pakka_papad/collection/CollectionContent.kt
+++ b/app/src/main/java/com/github/pakka_papad/collection/CollectionContent.kt
@@ -1,7 +1,26 @@
package com.github.pakka_papad.collection
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+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.lazy.*
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toBitmap
+import androidx.palette.graphics.Palette
+import coil.compose.AsyncImage
import com.github.pakka_papad.components.SongCardV1
import com.github.pakka_papad.components.more_options.SongOptions
import com.github.pakka_papad.data.music.Song
@@ -53,4 +72,54 @@ fun LazyListScope.collectionContent(
SongInfo(song) { infoVisible = false }
}
}
+}
+
+@Composable
+fun CollectionImage(
+ imageUri: String? = "",
+ title: String? = ""
+){
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(240.dp),
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ val surface = MaterialTheme.colorScheme.surface
+ val onSurface = MaterialTheme.colorScheme.onSurface
+ var textColor by remember { mutableStateOf(onSurface) }
+ var backgroundColor by remember { mutableStateOf(surface) }
+ AsyncImage(
+ model = imageUri,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.fillMaxSize(),
+ onSuccess = { result ->
+ Palette.Builder(result.result.drawable.toBitmap()).generate { palette ->
+ palette?.let {
+ it.mutedSwatch?.let { vbs ->
+ backgroundColor = Color(vbs.rgb)
+ textColor = Color(vbs.titleTextColor).copy(alpha = 1f)
+ }
+ }
+ }
+ }
+ )
+ Text(
+ text = title ?: "",
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ Brush.verticalGradient(
+ listOf(Color.Transparent, backgroundColor)
+ )
+ )
+ .padding(10.dp),
+ maxLines = 2,
+ color = textColor,
+ fontWeight = FontWeight.ExtraBold,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/collection/CollectionFragment.kt b/app/src/main/java/com/github/pakka_papad/collection/CollectionFragment.kt
index da0988d..b5570bc 100644
--- a/app/src/main/java/com/github/pakka_papad/collection/CollectionFragment.kt
+++ b/app/src/main/java/com/github/pakka_papad/collection/CollectionFragment.kt
@@ -10,19 +10,16 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import androidx.core.graphics.drawable.toBitmap
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.LayoutDirection
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -30,10 +27,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
-import androidx.palette.graphics.Palette
-import coil.compose.AsyncImage
import com.github.pakka_papad.R
import com.github.pakka_papad.components.FullScreenSadMessage
+import com.github.pakka_papad.components.Snackbar
import com.github.pakka_papad.components.SortOptionChooser
import com.github.pakka_papad.components.SortOptions
import com.github.pakka_papad.data.ZenPreferenceProvider
@@ -78,15 +74,10 @@ class CollectionFragment : Fragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val themePreference by preferenceProvider.theme.collectAsStateWithLifecycle()
- ZenTheme(
- themePreference = themePreference
- ) {
+ ZenTheme(themePreference) {
val collectionUi by viewModel.collectionUi.collectAsStateWithLifecycle()
val songsListState = rememberLazyListState()
val currentSong by viewModel.currentSong.collectAsStateWithLifecycle()
- val insetsPadding =
- WindowInsets.systemBars.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
- .asPaddingValues()
val topBarContainerAlpha by remember {
derivedStateOf {
if (songsListState.firstVisibleItemIndex == 0
@@ -95,127 +86,117 @@ class CollectionFragment : Fragment() {
}
var showSortOptions by remember { mutableStateOf(false) }
val chosenSortOrder by viewModel.chosenSortOrder.collectAsStateWithLifecycle()
- Box {
- LazyColumn(
- contentPadding = insetsPadding,
- state = songsListState,
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.surface),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- item {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .height(240.dp),
- contentAlignment = Alignment.BottomCenter
- ) {
- val surface = MaterialTheme.colorScheme.surface
- val onSurface = MaterialTheme.colorScheme.onSurface
- var textColor by remember { mutableStateOf(onSurface) }
- var backgroundColor by remember { mutableStateOf(surface) }
- AsyncImage(
- model = collectionUi?.topBarBackgroundImageUri,
- contentDescription = null,
- contentScale = ContentScale.Crop,
- modifier = Modifier.fillMaxSize(),
- onSuccess = { result ->
- Palette.Builder(result.result.drawable.toBitmap()).generate { palette ->
- palette?.let {
- it.mutedSwatch?.let { vbs ->
- backgroundColor = Color(vbs.rgb)
- textColor = Color(vbs.titleTextColor).copy(alpha = 1f)
- }
- }
- }
- }
- )
- Text(
- text = collectionUi?.topBarTitle ?: "",
- style = MaterialTheme.typography.headlineMedium,
- modifier = Modifier
- .fillMaxWidth()
- .background(
- Brush.verticalGradient(
- listOf(Color.Transparent, backgroundColor)
- )
- )
- .padding(10.dp),
- maxLines = 2,
- color = textColor,
- fontWeight = FontWeight.ExtraBold,
- overflow = TextOverflow.Ellipsis
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val message by viewModel.message.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = message){
+ if (message.isEmpty()) return@LaunchedEffect
+ snackbarHostState.showSnackbar(message)
+ }
+
+ Scaffold(
+ topBar = {
+ CollectionTopBar(
+ topBarTitle = collectionUi?.topBarTitle ?: "",
+ alpha = topBarContainerAlpha,
+ onBackArrowPressed = navController::popBackStack,
+ actions = listOf(
+ CollectionActions.AddToQueue {
+ collectionUi?.songs?.let { viewModel.addToQueue(it) }
+ },
+ CollectionActions.AddToPlaylist {
+ addAllSongsToPlaylistClicked(collectionUi?.songs)
+ },
+ CollectionActions.Sort {
+ showSortOptions = true
+ }
+ )
+ )
+ },
+ content = { paddingValues ->
+ val padding by remember {
+ derivedStateOf {
+ PaddingValues(
+ start = paddingValues.calculateStartPadding(LayoutDirection.Ltr),
+ end = paddingValues.calculateEndPadding(LayoutDirection.Ltr),
+ bottom = paddingValues.calculateBottomPadding(),
)
}
}
- if (collectionUi == null) {
- item {
- CircularProgressIndicator()
- }
- } else if (collectionUi?.error != null) {
+ LazyColumn(
+ contentPadding = padding,
+ state = songsListState,
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.surface),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
item {
- FullScreenSadMessage(
- message = collectionUi?.error,
+ CollectionImage(
+ imageUri = collectionUi?.topBarBackgroundImageUri,
+ title = collectionUi?.topBarTitle,
)
}
- } else if (collectionUi?.songs?.isEmpty() == true) {
- item {
- FullScreenSadMessage(
- message = "No songs found",
+ if (collectionUi == null) {
+ item {
+ CircularProgressIndicator()
+ }
+ } else if (collectionUi?.error != null) {
+ item {
+ FullScreenSadMessage(
+ message = collectionUi?.error,
+ )
+ }
+ } else if (collectionUi?.songs?.isEmpty() == true) {
+ item {
+ FullScreenSadMessage(
+ message = stringResource(R.string.no_songs_found),
+ )
+ }
+ } else {
+ collectionContent(
+ songs = collectionUi?.songs ?: emptyList(),
+ onSongClicked = {
+ viewModel.setQueue(collectionUi?.songs,it)
+ },
+ onSongFavouriteClicked = viewModel::changeFavouriteValue,
+ currentSong = currentSong,
+ onAddToQueueClicked = viewModel::addToQueue,
+ onPlayAllClicked = {
+ viewModel.setQueue(collectionUi?.songs,0)
+ },
+ onShuffleClicked = {
+ viewModel.shufflePlay(collectionUi?.songs)
+ },
+ onAddToPlaylistsClicked = this@CollectionFragment::addToPlaylistClicked,
+ isPlaylistCollection = args.collectionType?.type == CollectionType.PlaylistType,
+ onRemoveFromPlaylistClicked = viewModel::removeFromPlaylist
)
}
- } else {
- collectionContent(
- songs = collectionUi?.songs ?: emptyList(),
- onSongClicked = {
- viewModel.setQueue(collectionUi?.songs,it)
- },
- onSongFavouriteClicked = viewModel::changeFavouriteValue,
- currentSong = currentSong,
- onAddToQueueClicked = viewModel::addToQueue,
- onPlayAllClicked = {
- viewModel.setQueue(collectionUi?.songs,0)
- },
- onShuffleClicked = {
- viewModel.shufflePlay(collectionUi?.songs)
+ }
+ if (showSortOptions){
+ SortOptionChooser(
+ options = sortOptions,
+ selectedOption = chosenSortOrder,
+ onOptionSelect = { option ->
+ viewModel.updateSortOrder(option)
+ showSortOptions = false
},
- onAddToPlaylistsClicked = this@CollectionFragment::addToPlaylistClicked,
- isPlaylistCollection = args.collectionType?.type == CollectionType.PlaylistType,
- onRemoveFromPlaylistClicked = viewModel::removeFromPlaylist
+ onChooserDismiss = {
+ showSortOptions = false
+ }
)
}
- }
- CollectionTopBar(
- topBarTitle = collectionUi?.topBarTitle ?: "",
- alpha = topBarContainerAlpha,
- onBackArrowPressed = navController::popBackStack,
- actions = listOf(
- CollectionActions.AddToQueue {
- collectionUi?.songs?.let { viewModel.addToQueue(it) }
- },
- CollectionActions.AddToPlaylist {
- addAllSongsToPlaylistClicked(collectionUi?.songs)
- },
- CollectionActions.Sort {
- showSortOptions = true
+ },
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ snackbar = {
+ Snackbar(it)
}
)
- )
- }
- if (showSortOptions){
- SortOptionChooser(
- options = sortOptions,
- selectedOption = chosenSortOrder,
- onOptionSelect = { option ->
- viewModel.updateSortOrder(option)
- showSortOptions = false
- },
- onChooserDismiss = {
- showSortOptions = false
- }
- )
- }
+ }
+ )
}
}
}
diff --git a/app/src/main/java/com/github/pakka_papad/collection/CollectionTopBar.kt b/app/src/main/java/com/github/pakka_papad/collection/CollectionTopBar.kt
index 48787b0..4f2104d 100644
--- a/app/src/main/java/com/github/pakka_papad/collection/CollectionTopBar.kt
+++ b/app/src/main/java/com/github/pakka_papad/collection/CollectionTopBar.kt
@@ -16,9 +16,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.more_options.OptionsDropDown
@OptIn(ExperimentalMaterial3Api::class)
@@ -44,11 +46,14 @@ fun CollectionTopBar(
navigationIcon = {
Icon(
imageVector = Icons.Outlined.ArrowBack,
- contentDescription = "back arrow",
+ contentDescription = stringResource(R.string.back_button),
modifier = Modifier
.padding(6.dp)
.size(40.dp)
- .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), CircleShape)
+ .background(
+ MaterialTheme.colorScheme.surface.copy(alpha = 0.5f),
+ CircleShape
+ )
.padding(5.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
@@ -86,7 +91,7 @@ private fun CollectionTopBarActions(
var dropDownMenuExpanded by remember { mutableStateOf(false) }
Icon(
imageVector = Icons.Outlined.MoreVert,
- contentDescription = null,
+ contentDescription = stringResource(R.string.more_menu_button),
modifier = Modifier
.padding(6.dp)
.size(40.dp)
diff --git a/app/src/main/java/com/github/pakka_papad/collection/CollectionViewModel.kt b/app/src/main/java/com/github/pakka_papad/collection/CollectionViewModel.kt
index 23f7bb2..d9effd0 100644
--- a/app/src/main/java/com/github/pakka_papad/collection/CollectionViewModel.kt
+++ b/app/src/main/java/com/github/pakka_papad/collection/CollectionViewModel.kt
@@ -1,16 +1,20 @@
package com.github.pakka_papad.collection
-import android.app.Application
-import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.github.pakka_papad.Constants
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.SortOptions
-import com.github.pakka_papad.data.DataManager
-import com.github.pakka_papad.data.music.PlaylistSongCrossRef
import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.data.services.PlayerService
+import com.github.pakka_papad.data.services.PlaylistService
+import com.github.pakka_papad.data.services.QueueService
+import com.github.pakka_papad.data.services.SongService
+import com.github.pakka_papad.util.MessageStore
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -18,24 +22,30 @@ import javax.inject.Inject
@HiltViewModel
class CollectionViewModel @Inject constructor(
- private val context: Application,
- private val manager: DataManager,
+ private val messageStore: MessageStore,
+ private val playlistService: PlaylistService,
+ private val songService: SongService,
+ private val playerService: PlayerService,
+ private val queueService: QueueService,
) : ViewModel() {
- val currentSong = manager.currentSong
- val queue = manager.queue
+ val currentSong = queueService.currentSong
+ private val queue = queueService.queue
private val _collectionType = MutableStateFlow(null)
private val _chosenSortOrder = MutableStateFlow(SortOptions.Default.ordinal)
val chosenSortOrder = _chosenSortOrder.asStateFlow()
+ private val _message = MutableStateFlow("")
+ val message = _message.asStateFlow()
+
@OptIn(ExperimentalCoroutinesApi::class)
val collectionUi = _collectionType
.flatMapLatest { type ->
when (type?.type) {
CollectionType.AlbumType -> {
- manager.findCollection.getAlbumWithSongsByName(type.id).map {
+ songService.getAlbumWithSongsByName(type.id).map {
if (it == null) CollectionUi()
else {
CollectionUi(
@@ -47,7 +57,7 @@ class CollectionViewModel @Inject constructor(
}
}
CollectionType.ArtistType -> {
- manager.findCollection.getArtistWithSongsByName(type.id).map {
+ songService.getArtistWithSongsByName(type.id).map {
if (it == null) CollectionUi()
else {
CollectionUi(
@@ -59,19 +69,20 @@ class CollectionViewModel @Inject constructor(
}
}
CollectionType.PlaylistType -> {
- manager.findCollection.getPlaylistWithSongsById(type.id.toLong()).map {
+ playlistService.getPlaylistWithSongsById(type.id.toLong()).map {
if (it == null) CollectionUi()
else {
CollectionUi(
songs = it.songs,
topBarTitle = it.playlist.playlistName,
- topBarBackgroundImageUri = it.songs.randomOrNull()?.artUri ?: ""
+ topBarBackgroundImageUri = it.playlist.artUri ?:
+ it.songs.randomOrNull()?.artUri ?: ""
)
}
}
}
CollectionType.AlbumArtistType -> {
- manager.findCollection.getAlbumArtistWithSings(type.id).map {
+ songService.getAlbumArtistWithSongsByName(type.id).map {
if (it == null) CollectionUi()
else {
CollectionUi(
@@ -83,7 +94,7 @@ class CollectionViewModel @Inject constructor(
}
}
CollectionType.ComposerType -> {
- manager.findCollection.getComposerWithSongs(type.id).map {
+ songService.getComposerWithSongsByName(type.id).map {
if (it == null) CollectionUi()
else {
CollectionUi(
@@ -95,7 +106,7 @@ class CollectionViewModel @Inject constructor(
}
}
CollectionType.LyricistType -> {
- manager.findCollection.getLyricistWithSongs(type.id).map {
+ songService.getLyricistWithSongsByName(type.id).map {
if (it == null) CollectionUi()
else {
CollectionUi(
@@ -107,7 +118,7 @@ class CollectionViewModel @Inject constructor(
}
}
CollectionType.GenreType -> {
- manager.findCollection.getGenreWithSongs(type.id).map {
+ songService.getGenreWithSongsByName(type.id).map {
if (it == null) CollectionUi()
else {
CollectionUi(
@@ -119,7 +130,7 @@ class CollectionViewModel @Inject constructor(
}
}
CollectionType.FavouritesType -> {
- manager.findCollection.getFavourites().map {
+ songService.getFavouriteSongs().map {
CollectionUi(
songs = it,
topBarTitle = "Favourites",
@@ -167,34 +178,31 @@ class CollectionViewModel @Inject constructor(
fun setQueue(songs: List?, startPlayingFromIndex: Int = 0) {
if (songs == null) return
- manager.setQueue(songs, startPlayingFromIndex)
- Toast.makeText(context,"Playing",Toast.LENGTH_SHORT).show()
+ queueService.setQueue(songs, startPlayingFromIndex)
+ playerService.startServiceIfNotRunning(songs, startPlayingFromIndex)
+ showMessage(messageStore.getString(R.string.playing))
}
fun addToQueue(song: Song) {
if (queue.isEmpty()) {
- manager.setQueue(listOf(song), 0)
+ queueService.setQueue(listOf(song), 0)
+ playerService.startServiceIfNotRunning(listOf(song), 0)
} else {
- val result = manager.addToQueue(song)
- Toast.makeText(
- context,
- if (result) "Added ${song.title} to queue" else "Song already in queue",
- Toast.LENGTH_SHORT
- ).show()
+ val result = queueService.append(song)
+ showMessage(
+ if (result) messageStore.getString(R.string.added_to_queue, song.title)
+ else messageStore.getString(R.string.song_already_in_queue)
+ )
}
}
fun addToQueue(songs: List) {
if (queue.isEmpty()) {
- manager.setQueue(songs, 0)
+ queueService.setQueue(songs, 0)
+ playerService.startServiceIfNotRunning(songs, 0)
} else {
- var result = false
- songs.forEach { result = result or manager.addToQueue(it) }
- Toast.makeText(
- context,
- if (result) "Done" else "Song already in queue",
- Toast.LENGTH_SHORT
- ).show()
+ val result = queueService.append(songs)
+ showMessage(messageStore.getString(if (result) R.string.done else R.string.song_already_in_queue))
}
}
@@ -202,7 +210,8 @@ class CollectionViewModel @Inject constructor(
if (song == null) return
val updatedSong = song.copy(favourite = !song.favourite)
viewModelScope.launch(Dispatchers.IO) {
- manager.updateSong(updatedSong)
+ queueService.update(updatedSong)
+ songService.updateSong(updatedSong)
}
}
@@ -210,12 +219,11 @@ class CollectionViewModel @Inject constructor(
viewModelScope.launch {
try {
val playlistId = _collectionType.value?.id?.toLong() ?: throw IllegalArgumentException()
- val playlistSongCrossRef = PlaylistSongCrossRef(playlistId,song.location)
- manager.deletePlaylistSongCrossRef(playlistSongCrossRef)
- Toast.makeText(context,"Removed",Toast.LENGTH_SHORT).show()
+ playlistService.removeSongsFromPlaylist(listOf(song.location), playlistId)
+ showMessage(messageStore.getString(R.string.done))
} catch (e: Exception){
Timber.e(e)
- Toast.makeText(context,"Some error occurred",Toast.LENGTH_SHORT).show()
+ showMessage(messageStore.getString(R.string.some_error_occurred))
}
}
}
@@ -224,4 +232,11 @@ class CollectionViewModel @Inject constructor(
_chosenSortOrder.update { order }
}
+ private fun showMessage(message: String){
+ viewModelScope.launch {
+ _message.update { message }
+ delay(Constants.MESSAGE_DURATION)
+ _message.update { "" }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/components/BlockingProgressIndicator.kt b/app/src/main/java/com/github/pakka_papad/components/BlockingProgressIndicator.kt
new file mode 100644
index 0000000..c778940
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/components/BlockingProgressIndicator.kt
@@ -0,0 +1,29 @@
+package com.github.pakka_papad.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+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.unit.dp
+
+@Composable
+fun BlockingProgressIndicator(){
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(0.75f)
+ .height(80.dp)
+ .clip(MaterialTheme.shapes.large)
+ .background(MaterialTheme.colorScheme.primaryContainer),
+ contentAlignment = Alignment.Center
+ ){
+ CircularProgressIndicator(
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/components/FullScreenSadMessage.kt b/app/src/main/java/com/github/pakka_papad/components/FullScreenSadMessage.kt
index e3274f1..f17ea0c 100644
--- a/app/src/main/java/com/github/pakka_papad/components/FullScreenSadMessage.kt
+++ b/app/src/main/java/com/github/pakka_papad/components/FullScreenSadMessage.kt
@@ -9,6 +9,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.github.pakka_papad.R
@@ -28,12 +29,12 @@ fun FullScreenSadMessage(
content = {
Icon(
modifier = Modifier
- .weight(1f,false)
- .aspectRatio(1f,false)
+ .weight(1f, false)
+ .aspectRatio(1f, false)
.fillMaxSize()
.padding(24.dp),
painter = painterResource(R.drawable.ic_baseline_sentiment_very_dissatisfied_40),
- contentDescription = "sad-face",
+ contentDescription = stringResource(R.string.sad_face_image),
tint = MaterialTheme.colorScheme.onSurface
)
message?.let {
diff --git a/app/src/main/java/com/github/pakka_papad/components/PlaylistCards.kt b/app/src/main/java/com/github/pakka_papad/components/PlaylistCards.kt
index 440e768..deeee83 100644
--- a/app/src/main/java/com/github/pakka_papad/components/PlaylistCards.kt
+++ b/app/src/main/java/com/github/pakka_papad/components/PlaylistCards.kt
@@ -3,7 +3,17 @@ package com.github.pakka_papad.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+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.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.MoreVert
@@ -11,14 +21,22 @@ import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+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.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.more_options.OptionsAlertDialog
import com.github.pakka_papad.components.more_options.PlaylistOptions
import com.github.pakka_papad.data.music.PlaylistWithSongCount
@@ -136,3 +154,61 @@ fun PlaylistCard(
}
}
+@Composable
+fun PlaylistCardV2(
+ playlistWithSongCount: PlaylistWithSongCount,
+ onPlaylistClicked: (Long) -> Unit,
+ options: List = listOf(),
+) {
+ var optionsVisible by remember { mutableStateOf(false) }
+ Column(
+ modifier = Modifier
+ .widthIn(max = 200.dp)
+ .clickable(onClick = { onPlaylistClicked(playlistWithSongCount.playlistId) })
+ .padding(10.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ AsyncImage(
+ model = playlistWithSongCount.artUri,
+ contentDescription = stringResource(R.string.playlist_art),
+ modifier = Modifier
+ .aspectRatio(ratio = 1f, matchHeightConstraintsFirst = false)
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium),
+ contentScale = ContentScale.Crop,
+ )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = playlistWithSongCount.playlistName,
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ modifier = Modifier.weight(1f),
+ fontWeight = FontWeight.Bold,
+ overflow = TextOverflow.Ellipsis
+ )
+ Icon(
+ imageVector = Icons.Outlined.MoreVert,
+ contentDescription = stringResource(R.string.more_menu_button),
+ modifier = Modifier
+ .size(26.dp)
+ .clickable(
+ onClick = {
+ optionsVisible = true
+ },
+ indication = rememberRipple(bounded = false, radius = 20.dp),
+ interactionSource = remember { MutableInteractionSource() }
+ )
+ )
+ }
+ if (optionsVisible) {
+ OptionsAlertDialog(
+ options = options,
+ title = playlistWithSongCount.playlistName,
+ onDismissRequest = { optionsVisible = false }
+ )
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/github/pakka_papad/components/Snackbar.kt b/app/src/main/java/com/github/pakka_papad/components/Snackbar.kt
new file mode 100644
index 0000000..e584a8a
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/components/Snackbar.kt
@@ -0,0 +1,18 @@
+package com.github.pakka_papad.components
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarData
+import androidx.compose.runtime.Composable
+import androidx.compose.material3.Snackbar as M3Snackbar
+
+@Composable
+fun Snackbar(
+ snackbarData: SnackbarData
+){
+ M3Snackbar(
+ snackbarData = snackbarData,
+ shape = MaterialTheme.shapes.medium,
+ containerColor = MaterialTheme.colorScheme.secondaryContainer,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/components/SongCards.kt b/app/src/main/java/com/github/pakka_papad/components/SongCards.kt
index fba9efb..d5cd93a 100644
--- a/app/src/main/java/com/github/pakka_papad/components/SongCards.kt
+++ b/app/src/main/java/com/github/pakka_papad/components/SongCards.kt
@@ -23,10 +23,12 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.more_options.OptionsAlertDialog
import com.github.pakka_papad.components.more_options.SongOptions
import com.github.pakka_papad.data.music.MiniSong
@@ -58,7 +60,7 @@ private fun SongCardBase(
) {
AsyncImage(
model = song.artUri,
- contentDescription = "song-${song.title}-art",
+ contentDescription = stringResource(R.string.song_image),
modifier = Modifier
.size(50.dp)
.clip(MaterialTheme.shapes.medium),
@@ -93,7 +95,7 @@ private fun SongCardBase(
val favouriteButtonScale = remember { Animatable(1f) }
Icon(
imageVector = if (song.favourite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
- contentDescription = null,
+ contentDescription = stringResource(R.string.favourite_button),
modifier = iconModifier
.scale(favouriteButtonScale.value)
.clickable(
@@ -136,7 +138,7 @@ private fun SongCardBase(
var optionsVisible by remember { mutableStateOf(false) }
Icon(
imageVector = Icons.Outlined.MoreVert,
- contentDescription = null,
+ contentDescription = stringResource(R.string.more_menu_button),
modifier = iconModifier
.clickable(
onClick = {
@@ -215,7 +217,7 @@ fun SongCardV3(
) {
AsyncImage(
model = song.artUri,
- contentDescription = "song-album-art",
+ contentDescription = stringResource(R.string.song_image),
modifier = Modifier
.aspectRatio(1f)
.fillMaxWidth()
@@ -261,7 +263,7 @@ fun MiniSongCard(
) {
AsyncImage(
model = song.artUri,
- contentDescription = "song-${song.title}-art",
+ contentDescription = stringResource(R.string.song_image),
modifier = Modifier
.size(50.dp)
.clip(MaterialTheme.shapes.medium),
@@ -297,7 +299,7 @@ fun MiniSongCard(
var optionsVisible by remember { mutableStateOf(false) }
Icon(
imageVector = Icons.Outlined.MoreVert,
- contentDescription = null,
+ contentDescription = stringResource(R.string.more_menu_button),
modifier = iconModifier
.clickable(
onClick = {
diff --git a/app/src/main/java/com/github/pakka_papad/components/SortOptions.kt b/app/src/main/java/com/github/pakka_papad/components/SortOptions.kt
index 2f29123..fbbcba2 100644
--- a/app/src/main/java/com/github/pakka_papad/components/SortOptions.kt
+++ b/app/src/main/java/com/github/pakka_papad/components/SortOptions.kt
@@ -14,7 +14,9 @@ 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.stringResource
import androidx.compose.ui.text.style.TextAlign
+import com.github.pakka_papad.R
import com.github.pakka_papad.Screens
/**
@@ -87,7 +89,7 @@ fun SortOptionChooser(
onDismissRequest = onChooserDismiss,
title = {
Text(
- text = "Sort",
+ text = stringResource(R.string.sort),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
@@ -97,7 +99,7 @@ fun SortOptionChooser(
onClick = onChooserDismiss,
) {
Text(
- text = "Close",
+ text = stringResource(R.string.close),
style = MaterialTheme.typography.bodyLarge,
)
}
diff --git a/app/src/main/java/com/github/pakka_papad/components/TopAppBars.kt b/app/src/main/java/com/github/pakka_papad/components/TopAppBars.kt
index a8d11d0..2b04ca9 100644
--- a/app/src/main/java/com/github/pakka_papad/components/TopAppBars.kt
+++ b/app/src/main/java/com/github/pakka_papad/components/TopAppBars.kt
@@ -15,9 +15,11 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import com.github.pakka_papad.R
@Composable
private fun BaseTopBar(
@@ -106,7 +108,7 @@ fun SmallTopBar(
)
},
actions = actions,
- colors = TopAppBarDefaults.smallTopAppBarColors(
+ colors = TopAppBarDefaults.topAppBarColors(
containerColor = backgroundColor
)
)
@@ -126,7 +128,7 @@ fun TopBarWithBackArrow(
leadingIcon = {
Icon(
imageVector = Icons.Outlined.ArrowBack,
- contentDescription = null,
+ contentDescription = stringResource(R.string.back_button),
modifier = Modifier
.padding(16.dp)
.size(30.dp)
@@ -157,12 +159,12 @@ fun CancelConfirmTopBar(
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Close,
- contentDescription = null,
+ contentDescription = stringResource(R.string.close_button),
modifier = Modifier
.padding(16.dp)
.size(30.dp)
.clickable(
- interactionSource = remember{ MutableInteractionSource() },
+ interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = 25.dp,
@@ -176,12 +178,12 @@ fun CancelConfirmTopBar(
actions = {
Icon(
imageVector = Icons.Outlined.Check,
- contentDescription = null,
+ contentDescription = stringResource(R.string.check_button),
modifier = Modifier
.padding(16.dp)
.size(30.dp)
.clickable(
- interactionSource = remember{ MutableInteractionSource() },
+ interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = 25.dp,
diff --git a/app/src/main/java/com/github/pakka_papad/data/AppDatabase.kt b/app/src/main/java/com/github/pakka_papad/data/AppDatabase.kt
index 34e632a..6e8b663 100644
--- a/app/src/main/java/com/github/pakka_papad/data/AppDatabase.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/AppDatabase.kt
@@ -5,8 +5,29 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.pakka_papad.data.analytics.PlayHistory
import com.github.pakka_papad.data.analytics.PlayHistoryDao
-import com.github.pakka_papad.data.daos.*
-import com.github.pakka_papad.data.music.*
+import com.github.pakka_papad.data.daos.AlbumArtistDao
+import com.github.pakka_papad.data.daos.AlbumDao
+import com.github.pakka_papad.data.daos.ArtistDao
+import com.github.pakka_papad.data.daos.BlacklistDao
+import com.github.pakka_papad.data.daos.BlacklistedFolderDao
+import com.github.pakka_papad.data.daos.ComposerDao
+import com.github.pakka_papad.data.daos.GenreDao
+import com.github.pakka_papad.data.daos.LyricistDao
+import com.github.pakka_papad.data.daos.PlaylistDao
+import com.github.pakka_papad.data.daos.SongDao
+import com.github.pakka_papad.data.music.Album
+import com.github.pakka_papad.data.music.AlbumArtist
+import com.github.pakka_papad.data.music.Artist
+import com.github.pakka_papad.data.music.BlacklistedFolder
+import com.github.pakka_papad.data.music.BlacklistedSong
+import com.github.pakka_papad.data.music.Composer
+import com.github.pakka_papad.data.music.Genre
+import com.github.pakka_papad.data.music.Lyricist
+import com.github.pakka_papad.data.music.Playlist
+import com.github.pakka_papad.data.music.PlaylistSongCrossRef
+import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.data.thumbnails.Thumbnail
+import com.github.pakka_papad.data.thumbnails.ThumbnailDao
@Database(entities = [
Song::class,
@@ -21,10 +42,12 @@ import com.github.pakka_papad.data.music.*
BlacklistedSong::class,
BlacklistedFolder::class,
PlayHistory::class,
+ Thumbnail::class,
],
- version = 2, exportSchema = true,
+ version = 3, exportSchema = true,
autoMigrations = [
- AutoMigration(from = 1, to = 2)
+ AutoMigration(from = 1, to = 2),
+ AutoMigration(from = 2, to = 3)
]
)
abstract class AppDatabase: RoomDatabase() {
@@ -51,4 +74,5 @@ abstract class AppDatabase: RoomDatabase() {
abstract fun playHistoryDao(): PlayHistoryDao
+ abstract fun thumbnailDao(): ThumbnailDao
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/DataManager.kt b/app/src/main/java/com/github/pakka_papad/data/DataManager.kt
deleted file mode 100644
index c1258d3..0000000
--- a/app/src/main/java/com/github/pakka_papad/data/DataManager.kt
+++ /dev/null
@@ -1,267 +0,0 @@
-package com.github.pakka_papad.data
-
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.widget.Toast
-import androidx.compose.runtime.mutableStateListOf
-import com.github.pakka_papad.data.components.*
-import com.github.pakka_papad.data.music.*
-import com.github.pakka_papad.data.notification.ZenNotificationManager
-import com.github.pakka_papad.nowplaying.RepeatMode
-import com.github.pakka_papad.player.ZenPlayer
-import kotlinx.coroutines.*
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.flow.update
-import timber.log.Timber
-import java.io.File
-import kotlin.collections.HashSet
-
-class DataManager(
- private val context: Context,
- private val notificationManager: ZenNotificationManager,
- private val daoCollection: DaoCollection,
- private val scope: CoroutineScope,
- private val songExtractor: SongExtractor,
-) {
-
- val getAll by lazy { GetAll(daoCollection) }
-
- val findCollection by lazy { FindCollection(daoCollection) }
-
- val querySearch by lazy { QuerySearch(daoCollection) }
-
- val blacklistedSongLocations = HashSet()
- val blacklistedFolderPaths = HashSet()
-
- init {
- cleanData()
- buildBlacklistStore()
- }
-
- fun cleanData() {
- scope.launch {
- val jobs = mutableListOf()
- daoCollection.songDao.getSongs().forEach {
- try {
- if(!File(it.location).exists()){
- jobs += launch { daoCollection.songDao.deleteSong(it) }
- }
- } catch (_: Exception){
-
- }
- }
- jobs.joinAll()
- jobs.clear()
- daoCollection.albumDao.cleanAlbumTable()
- daoCollection.artistDao.cleanArtistTable()
- daoCollection.albumArtistDao.cleanAlbumArtistTable()
- daoCollection.composerDao.cleanComposerTable()
- daoCollection.lyricistDao.cleanLyricistTable()
- daoCollection.genreDao.cleanGenreTable()
- }
- }
-
- private fun buildBlacklistStore(){
- scope.launch {
- val blacklistedSongs = daoCollection.blacklistDao.getBlacklistedSongs()
- blacklistedSongs.forEach { blacklistedSongLocations.add(it.location) }
- val blacklistedFolders = daoCollection.blacklistedFolderDao.getAllFolders().first()
- blacklistedFolders.forEach { blacklistedFolderPaths.add(it.path) }
- }
- }
-
- suspend fun removeFromBlacklist(data: List){
- data.forEach {
- Timber.d("bs: $it")
- daoCollection.blacklistDao.deleteBlacklistedSong(it)
- blacklistedSongLocations.remove(it.location)
- }
- }
-
- suspend fun addFolderToBlacklist(path: String){
- daoCollection.songDao.deleteSongsWithPathPrefix(path)
- daoCollection.blacklistedFolderDao.insertFolder(BlacklistedFolder(path))
- blacklistedFolderPaths.add(path)
- cleanData()
- }
-
- suspend fun removeFoldersFromBlacklist(folders: List){
- folders.forEach { folder ->
- try {
- daoCollection.blacklistedFolderDao.deleteFolder(folder)
- blacklistedFolderPaths.remove(folder.path)
- } catch (_: Exception){ }
- }
- }
-
- suspend fun createPlaylist(playlistName: String) {
- if (playlistName.trim().isEmpty()) return
- val playlist = PlaylistExceptId(
- playlistName = playlistName.trim(),
- createdAt = System.currentTimeMillis()
- )
- daoCollection.playlistDao.insertPlaylist(playlist)
- showToast("Playlist $playlistName created")
- }
-
- suspend fun deletePlaylist(playlist: Playlist) = daoCollection.playlistDao.deletePlaylist(playlist)
- suspend fun deleteSong(song: Song) {
- daoCollection.songDao.deleteSong(song)
- daoCollection.blacklistDao.addSong(
- BlacklistedSong(
- location = song.location,
- title = song.title,
- artist = song.artist,
- )
- )
- blacklistedSongLocations.add(song.location)
- }
-
- suspend fun insertPlaylistSongCrossRefs(playlistSongCrossRefs: List) =
- daoCollection.playlistDao.insertPlaylistSongCrossRef(playlistSongCrossRefs)
-
- suspend fun deletePlaylistSongCrossRef(playlistSongCrossRef: PlaylistSongCrossRef) =
- daoCollection.playlistDao.deletePlaylistSongCrossRef(playlistSongCrossRef)
-
- private val _scanStatus = Channel(onBufferOverflow = BufferOverflow.DROP_OLDEST)
- val scanStatus = _scanStatus.receiveAsFlow()
-
- fun scanForMusic() = scope.launch {
- _scanStatus.send(ScanStatus.ScanStarted)
- val (songs, albums) = songExtractor.extract(
- blacklistedSongLocations,
- blacklistedFolderPaths,
- statusListener = { parsed, total ->
- _scanStatus.trySend(ScanStatus.ScanProgress(parsed, total))
- }
- )
- val artists = songs.map { it.artist }.toSet().map { Artist(it) }
- val albumArtists = songs.map { it.albumArtist }.toSet().map { AlbumArtist(it) }
- val lyricists = songs.map { it.lyricist }.toSet().map { Lyricist(it) }
- val composers = songs.map { it.composer }.toSet().map { Composer(it) }
- val genres = songs.map { it.genre }.toSet().map { Genre(it) }
- daoCollection.albumDao.insertAllAlbums(albums)
- daoCollection.artistDao.insertAllArtists(artists)
- daoCollection.albumArtistDao.insertAllAlbumArtists(albumArtists)
- daoCollection.lyricistDao.insertAllLyricists(lyricists)
- daoCollection.composerDao.insertAllComposers(composers)
- daoCollection.genreDao.insertAllGenres(genres)
- daoCollection.songDao.insertAllSongs(songs)
- _scanStatus.send(ScanStatus.ScanComplete)
- }
-
- private fun showToast(message: String) {
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
- }
-
- private var callback: Callback? = null
-
- private val _queue = mutableStateListOf()
- val queue: List = _queue
-
- private val _currentSong = MutableStateFlow(null)
- val currentSong = _currentSong.asStateFlow()
-
- fun moveItem(fromIndex: Int, toIndex: Int) {
- _queue.apply { add(toIndex, removeAt(fromIndex)) }
- }
-
- suspend fun updateSong(song: Song) {
- if (_currentSong.value?.location == song.location) {
- _currentSong.update { song }
- callback?.updateNotification()
- }
- for (idx in _queue.indices) {
- if (_queue[idx].location == song.location) {
- _queue[idx] = song
- break
- }
- }
- daoCollection.songDao.updateSong(song)
- }
-
- private val _repeatMode = MutableStateFlow(RepeatMode.NO_REPEAT)
- val repeatMode = _repeatMode.asStateFlow()
-
- fun updateRepeatMode(newRepeatMode: RepeatMode){
- _repeatMode.update { newRepeatMode }
- }
-
- private var remIdx = 0
-
- @Synchronized
- @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
- fun setQueue(newQueue: List, startPlayingFromIndex: Int) {
- if (newQueue.isEmpty()) return
- _queue.apply {
- clear()
- addAll(newQueue)
- }
- _currentSong.value = newQueue[startPlayingFromIndex]
- if (callback == null) {
- val intent = Intent(context, ZenPlayer::class.java)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- context.startForegroundService(intent)
- } else {
- context.startService(intent)
- }
- remIdx = startPlayingFromIndex
- } else {
- callback?.setQueue(newQueue, startPlayingFromIndex)
- }
- }
-
- /**
- * Returns true if added to queue else returns false if already in queue
- */
- @Synchronized
- fun addToQueue(song: Song): Boolean {
- if (_queue.any { it.location == song.location }) return false
- _queue.add(song)
- callback?.addToQueue(song)
- return true
- }
-
- fun setPlayerRunning(callback: Callback) {
- this.callback = callback
- this.callback?.setQueue(_queue, remIdx)
- }
-
- fun updateCurrentSong(currentSongIndex: Int) {
- if (currentSongIndex < 0 || currentSongIndex >= _queue.size) return
- _currentSong.update { _queue[currentSongIndex] }
- }
-
- fun getSongAtIndex(index: Int): Song? {
- if (index < 0 || index >= _queue.size) return null
- return _queue[index]
- }
-
- fun stopPlayerRunning() {
- this.callback = null
- _currentSong.update { null }
- _queue.clear()
- }
-
- interface Callback {
- fun setQueue(newQueue: List, startPlayingFromIndex: Int)
- fun addToQueue(song: Song)
- fun updateNotification()
- }
-
- fun addPlayHistory(songLocation: String, duration: Long){
- scope.launch {
- try {
- daoCollection.playHistoryDao.addRecord(songLocation, duration)
- } catch (_: Exception){
-
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/UserPreferencesSerializer.kt b/app/src/main/java/com/github/pakka_papad/data/UserPreferencesSerializer.kt
index 95eab50..6acf58c 100644
--- a/app/src/main/java/com/github/pakka_papad/data/UserPreferencesSerializer.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/UserPreferencesSerializer.kt
@@ -1,5 +1,6 @@
package com.github.pakka_papad.data
+import android.os.Build
import androidx.datastore.core.Serializer
import com.github.pakka_papad.Screens
import com.github.pakka_papad.components.SortOptions
@@ -10,7 +11,7 @@ import javax.inject.Inject
class UserPreferencesSerializer @Inject constructor() : Serializer {
override val defaultValue: UserPreferences
get() = UserPreferences.getDefaultInstance().copy {
- useMaterialYouTheme = false
+ useMaterialYouTheme = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
chosenTheme = UserPreferences.Theme.DARK_MODE
chosenAccent = UserPreferences.Accent.Elm
onBoardingComplete = false
diff --git a/app/src/main/java/com/github/pakka_papad/data/components/DaoCollection.kt b/app/src/main/java/com/github/pakka_papad/data/components/DaoCollection.kt
deleted file mode 100644
index 4c96103..0000000
--- a/app/src/main/java/com/github/pakka_papad/data/components/DaoCollection.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.github.pakka_papad.data.components
-
-import com.github.pakka_papad.data.analytics.PlayHistoryDao
-import com.github.pakka_papad.data.daos.*
-
-data class DaoCollection(
- val songDao: SongDao,
- val albumDao: AlbumDao,
- val artistDao: ArtistDao,
- val albumArtistDao: AlbumArtistDao,
- val composerDao: ComposerDao,
- val lyricistDao: LyricistDao,
- val genreDao: GenreDao,
- val playlistDao: PlaylistDao,
- val blacklistDao: BlacklistDao,
- val blacklistedFolderDao: BlacklistedFolderDao,
- val playHistoryDao: PlayHistoryDao,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/components/FindCollection.kt b/app/src/main/java/com/github/pakka_papad/data/components/FindCollection.kt
deleted file mode 100644
index eec618b..0000000
--- a/app/src/main/java/com/github/pakka_papad/data/components/FindCollection.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.github.pakka_papad.data.components
-
-class FindCollection(
- private val daoCollection: DaoCollection,
-) {
- fun getPlaylistWithSongsById(id: Long) = daoCollection.playlistDao.getPlaylistWithSongs(id)
-
- fun getAlbumWithSongsByName(albumName: String) = daoCollection.albumDao.getAlbumWithSongsByName(albumName)
-
- fun getArtistWithSongsByName(artistName: String) = daoCollection.artistDao.getArtistWithSongsByName(artistName)
-
- fun getAlbumArtistWithSings(albumArtistName: String) = daoCollection.albumArtistDao.getAlbumArtistWithSongs(albumArtistName)
-
- fun getComposerWithSongs(composerName: String) = daoCollection.composerDao.getComposerWithSongs(composerName)
-
- fun getLyricistWithSongs(lyricistName: String) = daoCollection.lyricistDao.getLyricistWithSongs(lyricistName)
-
- fun getGenreWithSongs(genreName: String) = daoCollection.genreDao.getGenreWithSongs(genreName)
-
- fun getFavourites() = daoCollection.songDao.getAllFavourites()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/components/GetAll.kt b/app/src/main/java/com/github/pakka_papad/data/components/GetAll.kt
deleted file mode 100644
index 40b4321..0000000
--- a/app/src/main/java/com/github/pakka_papad/data/components/GetAll.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.github.pakka_papad.data.components
-
-class GetAll(
- private val daoCollection: DaoCollection,
-) {
- fun songs() = daoCollection.songDao.getAllSongs()
-
- fun albums() = daoCollection.albumDao.getAllAlbums()
-
- fun artists() = daoCollection.songDao.getAllArtistsWithSongCount()
-
- fun albumArtists() = daoCollection.songDao.getAllAlbumArtistsWithSongCount()
-
- fun composers() = daoCollection.songDao.getAllComposersWithSongCount()
-
- fun lyricists() = daoCollection.songDao.getAllLyricistsWithSongCount()
-
- fun playlists() = daoCollection.playlistDao.getAllPlaylistWithSongCount()
-
- fun genres() = daoCollection.songDao.getAllGenresWithSongCount()
-
- fun blacklistedSongs() = daoCollection.blacklistDao.getBlacklistedSongsFlow()
-
- fun blacklistedFolders() = daoCollection.blacklistedFolderDao.getAllFolders()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/components/QuerySearch.kt b/app/src/main/java/com/github/pakka_papad/data/components/QuerySearch.kt
deleted file mode 100644
index d3f6db1..0000000
--- a/app/src/main/java/com/github/pakka_papad/data/components/QuerySearch.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.github.pakka_papad.data.components
-
-class QuerySearch(
- private val daoCollection: DaoCollection,
-) {
- suspend fun searchSongs(query: String) = daoCollection.songDao.searchSongs(query)
-
- suspend fun searchAlbums(query: String) = daoCollection.albumDao.searchAlbums(query)
-
- suspend fun searchArtists(query: String) = daoCollection.artistDao.searchArtists(query)
-
- suspend fun searchAlbumArtists(query: String) = daoCollection.albumArtistDao.searchAlbumArtists(query)
-
- suspend fun searchComposers(query: String) = daoCollection.composerDao.searchComposers(query)
-
- suspend fun searchLyricists(query: String) = daoCollection.lyricistDao.searchLyricists(query)
-
- suspend fun searchPlaylists(query: String) = daoCollection.playlistDao.searchPlaylists(query)
-
- suspend fun searchGenres(query: String) = daoCollection.genreDao.searchGenres(query)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/daos/BlacklistedFolderDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/BlacklistedFolderDao.kt
index 7434978..3f18620 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/BlacklistedFolderDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/BlacklistedFolderDao.kt
@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.Flow
interface BlacklistedFolderDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
- suspend fun insertFolder(vararg folder: BlacklistedFolder)
+ suspend fun insertFolder(folder: BlacklistedFolder)
@Query("SELECT * FROM ${Constants.Tables.BLACKLISTED_FOLDER_TABLE}")
fun getAllFolders(): Flow>
diff --git a/app/src/main/java/com/github/pakka_papad/data/daos/PlaylistDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/PlaylistDao.kt
index a516559..9685aed 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/PlaylistDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/PlaylistDao.kt
@@ -11,8 +11,14 @@ interface PlaylistDao {
@Insert(entity = Playlist::class)
suspend fun insertPlaylist(playlist: PlaylistExceptId): Long
- @Delete(entity = Playlist::class)
- suspend fun deletePlaylist(playlist: Playlist)
+ @Update(entity = Playlist::class)
+ suspend fun updatePlaylist(playlist: Playlist)
+
+ @Query("DELETE FROM ${Constants.Tables.PLAYLIST_TABLE} WHERE playlistId = :playlistId")
+ suspend fun deletePlaylist(playlistId: Long)
+
+ @Query("SELECT * FROM ${Constants.Tables.PLAYLIST_TABLE} WHERE playlistId = :playlistId")
+ suspend fun getPlaylist(playlistId: Long): Playlist?
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertPlaylistSongCrossRef(playlistSongCrossRefs: List)
diff --git a/app/src/main/java/com/github/pakka_papad/data/music/Playlist.kt b/app/src/main/java/com/github/pakka_papad/data/music/Playlist.kt
index 02f31a8..8ff238a 100644
--- a/app/src/main/java/com/github/pakka_papad/data/music/Playlist.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/music/Playlist.kt
@@ -1,5 +1,6 @@
package com.github.pakka_papad.data.music
+import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.github.pakka_papad.Constants
@@ -9,9 +10,11 @@ data class Playlist(
@PrimaryKey(autoGenerate = true) val playlistId: Long,
val playlistName: String,
val createdAt: Long,
+ @ColumnInfo(defaultValue = "NULL") val artUri: String? = null,
)
data class PlaylistExceptId(
val playlistName: String,
val createdAt: Long,
+ val artUri: String? = null,
)
diff --git a/app/src/main/java/com/github/pakka_papad/data/music/PlaylistWithSongCount.kt b/app/src/main/java/com/github/pakka_papad/data/music/PlaylistWithSongCount.kt
index ddf1df0..c4ca9c1 100644
--- a/app/src/main/java/com/github/pakka_papad/data/music/PlaylistWithSongCount.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/music/PlaylistWithSongCount.kt
@@ -4,5 +4,6 @@ data class PlaylistWithSongCount(
val playlistId: Long,
val playlistName: String,
val createdAt: Long,
+ val artUri: String?,
val count: Int = 0,
)
diff --git a/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt b/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt
index 08f284d..9342258 100644
--- a/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt
@@ -7,14 +7,30 @@ import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import com.github.pakka_papad.data.ZenCrashReporter
+import com.github.pakka_papad.data.daos.AlbumArtistDao
+import com.github.pakka_papad.data.daos.AlbumDao
+import com.github.pakka_papad.data.daos.ArtistDao
+import com.github.pakka_papad.data.daos.BlacklistDao
+import com.github.pakka_papad.data.daos.BlacklistedFolderDao
+import com.github.pakka_papad.data.daos.ComposerDao
+import com.github.pakka_papad.data.daos.GenreDao
+import com.github.pakka_papad.data.daos.LyricistDao
+import com.github.pakka_papad.data.daos.SongDao
import com.github.pakka_papad.formatToDate
import com.github.pakka_papad.toMBfromB
import com.github.pakka_papad.toMS
import kotlinx.coroutines.CompletionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
import java.io.File
import java.io.FileNotFoundException
import java.util.TreeMap
@@ -24,8 +40,44 @@ class SongExtractor(
private val scope: CoroutineScope,
private val context: Context,
private val crashReporter: ZenCrashReporter,
+ private val songDao: SongDao,
+ private val albumDao: AlbumDao,
+ private val artistDao: ArtistDao,
+ private val albumArtistDao: AlbumArtistDao,
+ private val composerDao: ComposerDao,
+ private val lyricistDao: LyricistDao,
+ private val genreDao: GenreDao,
+ private val blacklistDao: BlacklistDao,
+ private val blacklistedFolderDao: BlacklistedFolderDao,
) {
+ init {
+ cleanData()
+ }
+
+ fun cleanData() {
+ scope.launch {
+ val jobs = mutableListOf()
+ songDao.getSongs().forEach {
+ try {
+ if(!File(it.location).exists()){
+ jobs += launch { songDao.deleteSong(it) }
+ }
+ } catch (_: Exception){
+
+ }
+ }
+ jobs.joinAll()
+ jobs.clear()
+ albumDao.cleanAlbumTable()
+ artistDao.cleanArtistTable()
+ albumArtistDao.cleanAlbumArtistTable()
+ composerDao.cleanComposerTable()
+ lyricistDao.cleanLyricistTable()
+ genreDao.cleanGenreTable()
+ }
+ }
+
private val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
@@ -184,7 +236,58 @@ class SongExtractor(
return songs
}
- suspend fun extract(
+ private val _scanStatus = Channel(onBufferOverflow = BufferOverflow.DROP_OLDEST)
+ val scanStatus = _scanStatus.receiveAsFlow()
+
+ fun scanForMusic() {
+ scope.launch {
+ _scanStatus.send(ScanStatus.ScanStarted)
+ val blacklistedSongLocations = blacklistDao
+ .getBlacklistedSongs()
+ .map { it.location }
+ .toHashSet()
+ val blacklistedFolderPaths = blacklistedFolderDao
+ .getAllFolders()
+ .first()
+ .map { it.path }
+ .toHashSet()
+ val (songs, albums) = extract(
+ blacklistedSongLocations,
+ blacklistedFolderPaths,
+ statusListener = { parsed, total ->
+ _scanStatus.trySend(ScanStatus.ScanProgress(parsed, total))
+ }
+ )
+ val insertJobs = listOf(
+ launch {
+ val artists = songs.map { it.artist }.toSet().map { Artist(it) }
+ artistDao.insertAllArtists(artists)
+ },
+ launch {
+ val albumArtists = songs.map { it.albumArtist }.toSet().map { AlbumArtist(it) }
+ albumArtistDao.insertAllAlbumArtists(albumArtists)
+ },
+ launch {
+ val lyricists = songs.map { it.lyricist }.toSet().map { Lyricist(it) }
+ lyricistDao.insertAllLyricists(lyricists)
+ },
+ launch {
+ val composers = songs.map { it.composer }.toSet().map { Composer(it) }
+ composerDao.insertAllComposers(composers)
+ },
+ launch {
+ val genres = songs.map { it.genre }.toSet().map { Genre(it) }
+ genreDao.insertAllGenres(genres)
+ }
+ )
+ albumDao.insertAllAlbums(albums)
+ insertJobs.joinAll()
+ songDao.insertAllSongs(songs)
+ _scanStatus.send(ScanStatus.ScanComplete)
+ }
+ }
+
+ private suspend fun extract(
blacklistedSongLocations: HashSet,
blacklistedFolderPaths: HashSet,
statusListener: ((parsed: Int, total: Int) -> Unit)? = null
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/AnalyticsService.kt b/app/src/main/java/com/github/pakka_papad/data/services/AnalyticsService.kt
new file mode 100644
index 0000000..96a8c9c
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/AnalyticsService.kt
@@ -0,0 +1,20 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.data.analytics.PlayHistoryDao
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+interface AnalyticsService {
+ fun logSongPlay(songLocation: String, playDuration: Long)
+}
+
+class AnalyticsServiceImpl(
+ private val playHistoryDao: PlayHistoryDao,
+ private val scope: CoroutineScope,
+): AnalyticsService {
+ override fun logSongPlay(songLocation: String, playDuration: Long) {
+ scope.launch {
+ playHistoryDao.addRecord(songLocation, playDuration)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/BlacklistService.kt b/app/src/main/java/com/github/pakka_papad/data/services/BlacklistService.kt
new file mode 100644
index 0000000..5bb6b50
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/BlacklistService.kt
@@ -0,0 +1,87 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.data.daos.AlbumArtistDao
+import com.github.pakka_papad.data.daos.AlbumDao
+import com.github.pakka_papad.data.daos.ArtistDao
+import com.github.pakka_papad.data.daos.BlacklistDao
+import com.github.pakka_papad.data.daos.BlacklistedFolderDao
+import com.github.pakka_papad.data.daos.ComposerDao
+import com.github.pakka_papad.data.daos.GenreDao
+import com.github.pakka_papad.data.daos.LyricistDao
+import com.github.pakka_papad.data.daos.SongDao
+import com.github.pakka_papad.data.music.BlacklistedFolder
+import com.github.pakka_papad.data.music.BlacklistedSong
+import com.github.pakka_papad.data.music.Song
+import kotlinx.coroutines.flow.Flow
+
+interface BlacklistService {
+ val blacklistedSongs: Flow>
+ val blacklistedFolders: Flow>
+
+ suspend fun blacklistSongs(songs: List)
+ suspend fun whitelistSongs(blacklistedSongs: List)
+
+ suspend fun blacklistFolders(folderPaths: List)
+ suspend fun whitelistFolders(folders: List)
+}
+
+class BlacklistServiceImpl(
+ private val blacklistDao: BlacklistDao,
+ private val blacklistedFolderDao: BlacklistedFolderDao,
+ private val songDao: SongDao,
+ private val albumDao: AlbumDao,
+ private val artistDao: ArtistDao,
+ private val albumArtistDao: AlbumArtistDao,
+ private val composerDao: ComposerDao,
+ private val lyricistDao: LyricistDao,
+ private val genreDao: GenreDao,
+): BlacklistService {
+
+ override val blacklistedSongs: Flow>
+ = blacklistDao.getBlacklistedSongsFlow()
+
+ override val blacklistedFolders: Flow>
+ = blacklistedFolderDao.getAllFolders()
+
+ override suspend fun blacklistSongs(songs: List) {
+ songs.forEach { song ->
+ songDao.deleteSong(song)
+ blacklistDao.addSong(
+ BlacklistedSong(
+ location = song.location,
+ title = song.title,
+ artist = song.artist,
+ )
+ )
+ }
+ }
+
+ override suspend fun whitelistSongs(blacklistedSongs: List) {
+ blacklistedSongs.forEach { blacklistedSong ->
+ blacklistDao.deleteBlacklistedSong(blacklistedSong)
+ }
+ }
+
+ override suspend fun blacklistFolders(folderPaths: List) {
+ folderPaths.forEach { folderPath ->
+ songDao.deleteSongsWithPathPrefix(folderPath)
+ blacklistedFolderDao.insertFolder(BlacklistedFolder(folderPath))
+ }
+ cleanData()
+ }
+
+ private suspend fun cleanData(){
+ albumDao.cleanAlbumTable()
+ artistDao.cleanArtistTable()
+ albumArtistDao.cleanAlbumArtistTable()
+ composerDao.cleanComposerTable()
+ lyricistDao.cleanLyricistTable()
+ genreDao.cleanGenreTable()
+ }
+
+ override suspend fun whitelistFolders(folders: List) {
+ folders.forEach { folder ->
+ blacklistedFolderDao.deleteFolder(folder)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/PlayerService.kt b/app/src/main/java/com/github/pakka_papad/data/services/PlayerService.kt
new file mode 100644
index 0000000..aec56d8
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/PlayerService.kt
@@ -0,0 +1,29 @@
+package com.github.pakka_papad.data.services
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.player.ZenPlayer
+
+interface PlayerService {
+ fun startServiceIfNotRunning(songs: List, startPlayingFromPosition: Int)
+}
+
+class PlayerServiceImpl(
+ private val context: Context,
+): PlayerService {
+
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
+ override fun startServiceIfNotRunning(songs: List, startPlayingFromPosition: Int) {
+ if (ZenPlayer.isRunning.get()) return
+ val intent = Intent(context, ZenPlayer::class.java)
+ intent.putExtra("locations", songs.map { it.location }.toTypedArray())
+ intent.putExtra("startPosition", startPlayingFromPosition)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/PlaylistService.kt b/app/src/main/java/com/github/pakka_papad/data/services/PlaylistService.kt
new file mode 100644
index 0000000..61b656e
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/PlaylistService.kt
@@ -0,0 +1,78 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.data.daos.PlaylistDao
+import com.github.pakka_papad.data.music.Playlist
+import com.github.pakka_papad.data.music.PlaylistExceptId
+import com.github.pakka_papad.data.music.PlaylistSongCrossRef
+import com.github.pakka_papad.data.music.PlaylistWithSongCount
+import com.github.pakka_papad.data.music.PlaylistWithSongs
+import com.github.pakka_papad.data.thumbnails.ThumbnailDao
+import kotlinx.coroutines.flow.Flow
+
+interface PlaylistService {
+ val playlists: Flow>
+
+ fun getPlaylistWithSongsById(playlistId: Long): Flow
+
+ suspend fun createPlaylist(name: String): Boolean
+ suspend fun deletePlaylist(playlistId: Long)
+ suspend fun updatePlaylist(updatedPlaylist: Playlist)
+
+ suspend fun addSongsToPlaylist(songLocations: List, playlistId: Long)
+ suspend fun removeSongsFromPlaylist(songLocations: List, playlistId: Long)
+}
+
+class PlaylistServiceImpl(
+ private val playlistDao: PlaylistDao,
+ private val thumbnailDao: ThumbnailDao,
+): PlaylistService {
+ override val playlists: Flow>
+ = playlistDao.getAllPlaylistWithSongCount()
+
+ override fun getPlaylistWithSongsById(playlistId: Long): Flow {
+ return playlistDao.getPlaylistWithSongs(playlistId)
+ }
+
+ override suspend fun createPlaylist(name: String): Boolean {
+ if (name.isBlank()) return false
+ val playlist = PlaylistExceptId(
+ playlistName = name.trim(),
+ createdAt = System.currentTimeMillis()
+ )
+ playlistDao.insertPlaylist(playlist)
+ return true
+ }
+
+ override suspend fun deletePlaylist(playlistId: Long) {
+ val playlist = playlistDao.getPlaylist(playlistId)
+ playlistDao.deletePlaylist(playlistId)
+ if (playlist?.artUri == null) return
+ thumbnailDao.markDelete(playlist.artUri)
+ }
+
+ override suspend fun updatePlaylist(updatedPlaylist: Playlist) {
+ playlistDao.updatePlaylist(updatedPlaylist)
+ }
+
+ override suspend fun addSongsToPlaylist(songLocations: List, playlistId: Long) {
+ playlistDao.insertPlaylistSongCrossRef(
+ songLocations.map {
+ PlaylistSongCrossRef(
+ playlistId = playlistId,
+ location = it
+ )
+ }
+ )
+ }
+
+ override suspend fun removeSongsFromPlaylist(songLocations: List, playlistId: Long) {
+ songLocations.forEach {
+ playlistDao.deletePlaylistSongCrossRef(
+ PlaylistSongCrossRef(
+ playlistId = playlistId,
+ location = it
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/QueueService.kt b/app/src/main/java/com/github/pakka_papad/data/services/QueueService.kt
new file mode 100644
index 0000000..3592e25
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/QueueService.kt
@@ -0,0 +1,157 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.nowplaying.RepeatMode
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import org.jetbrains.annotations.VisibleForTesting
+import java.util.TreeSet
+
+interface QueueService {
+ val queue: List
+
+ val currentSong: StateFlow
+
+ fun append(song: Song): Boolean
+ fun append(songs: List): Boolean
+ fun update(song: Song): Boolean
+ fun moveSong(initialPosition: Int, finalPosition: Int): Boolean
+
+ fun clearQueue()
+ fun setQueue(songs: List, startPlayingFromPosition: Int)
+
+ fun getSongAtIndex(index: Int): Song?
+
+ fun setCurrentSong(currentSongIndex: Int)
+
+ val repeatMode: StateFlow
+
+ fun updateRepeatMode(mode: RepeatMode)
+
+ interface Listener {
+ fun onAppend(song: Song)
+ fun onAppend(songs: List)
+ fun onUpdate(updatedSong: Song, position: Int)
+ fun onMove(from: Int, to: Int)
+ fun onClear()
+ fun onSetQueue(songs: List, startPlayingFromPosition: Int)
+ }
+
+ fun addListener(listener: Listener)
+ fun removeListener(listener: Listener)
+}
+
+class QueueServiceImpl() : QueueService {
+
+ @VisibleForTesting
+ internal val mutableQueue = ArrayList()
+
+ @VisibleForTesting
+ internal val locations = TreeSet()
+
+ override val queue: List
+ get() = mutableQueue.toList()
+
+ @VisibleForTesting
+ internal val _currentSong = MutableStateFlow(null)
+ override val currentSong: StateFlow = _currentSong.asStateFlow()
+
+ override fun append(song: Song): Boolean {
+ if (locations.contains(song.location)) return false
+ locations.add(song.location)
+ mutableQueue.add(song)
+ callbacks.forEach { it.onAppend(song) }
+ return true
+ }
+
+ override fun append(songs: List): Boolean {
+ val songsNotInQueue = songs.filter { !locations.contains(it.location) }
+ if (songsNotInQueue.isEmpty()) return false
+ songsNotInQueue.forEach {
+ mutableQueue.add(it)
+ locations.add(it.location)
+ }
+ callbacks.forEach { it.onAppend(songs) }
+ return true
+ }
+
+ override fun update(song: Song): Boolean {
+ if (!locations.contains(song.location)) return false
+ var position = -1
+ for (idx in mutableQueue.indices){
+ if (mutableQueue[idx].location == song.location){
+ mutableQueue[idx] = song
+ position = idx
+ break
+ }
+ }
+ if (song.location == _currentSong.value?.location){
+ _currentSong.update { song }
+ }
+ if (position != -1){
+ callbacks.forEach { it.onUpdate(song, position) }
+ }
+ return true
+ }
+
+ override fun moveSong(initialPosition: Int, finalPosition: Int): Boolean {
+ if (initialPosition < 0 || initialPosition >= mutableQueue.size) return false
+ if (finalPosition < 0 || finalPosition >= mutableQueue.size) return false
+ if (initialPosition == finalPosition) return false
+ mutableQueue.apply {
+ add(finalPosition, removeAt(initialPosition))
+ }
+ callbacks.forEach { it.onMove(initialPosition, finalPosition) }
+ return true
+ }
+
+ override fun clearQueue() {
+ mutableQueue.clear()
+ _currentSong.update { null }
+ locations.clear()
+ callbacks.forEach { it.onClear() }
+ }
+
+ override fun setQueue(songs: List, startPlayingFromPosition: Int) {
+ if(songs.isEmpty()) return
+ mutableQueue.apply {
+ clear()
+ addAll(songs)
+ }
+ val idx = if (startPlayingFromPosition >= songs.size || startPlayingFromPosition < 0) 0
+ else startPlayingFromPosition
+ _currentSong.update { mutableQueue[idx] }
+ locations.clear()
+ songs.forEach { locations.add(it.location) }
+ callbacks.forEach { it.onSetQueue(songs, idx) }
+ }
+
+ override fun getSongAtIndex(index: Int): Song? {
+ if (index < 0 || index >= mutableQueue.size) return null
+ return mutableQueue[index]
+ }
+
+ override fun setCurrentSong(currentSongIndex: Int) {
+ if (currentSongIndex < 0 || currentSongIndex >= mutableQueue.size) return
+ _currentSong.update { mutableQueue[currentSongIndex] }
+ }
+
+ private val _repeatMode = MutableStateFlow(RepeatMode.NO_REPEAT)
+ override val repeatMode: StateFlow = _repeatMode.asStateFlow()
+
+ override fun updateRepeatMode(mode: RepeatMode) {
+ _repeatMode.update { mode }
+ }
+
+ private val callbacks = ArrayList()
+
+ override fun addListener(listener: QueueService.Listener) {
+ callbacks.add(listener)
+ }
+
+ override fun removeListener(listener: QueueService.Listener) {
+ callbacks.remove(listener)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/SearchService.kt b/app/src/main/java/com/github/pakka_papad/data/services/SearchService.kt
new file mode 100644
index 0000000..71f3745
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/SearchService.kt
@@ -0,0 +1,72 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.data.daos.AlbumArtistDao
+import com.github.pakka_papad.data.daos.AlbumDao
+import com.github.pakka_papad.data.daos.ArtistDao
+import com.github.pakka_papad.data.daos.ComposerDao
+import com.github.pakka_papad.data.daos.GenreDao
+import com.github.pakka_papad.data.daos.LyricistDao
+import com.github.pakka_papad.data.daos.PlaylistDao
+import com.github.pakka_papad.data.daos.SongDao
+import com.github.pakka_papad.data.music.Album
+import com.github.pakka_papad.data.music.AlbumArtist
+import com.github.pakka_papad.data.music.Artist
+import com.github.pakka_papad.data.music.Composer
+import com.github.pakka_papad.data.music.Genre
+import com.github.pakka_papad.data.music.Lyricist
+import com.github.pakka_papad.data.music.Playlist
+import com.github.pakka_papad.data.music.Song
+
+interface SearchService {
+ suspend fun searchSongs(query: String): List
+ suspend fun searchAlbums(query: String): List
+ suspend fun searchArtists(query: String): List
+ suspend fun searchAlbumArtists(query: String): List
+ suspend fun searchComposers(query: String): List
+ suspend fun searchLyricists(query: String): List
+ suspend fun searchPlaylists(query: String): List
+ suspend fun searchGenres(query: String): List
+}
+
+class SearchServiceImpl(
+ private val songDao: SongDao,
+ private val albumDao: AlbumDao,
+ private val artistDao: ArtistDao,
+ private val albumArtistDao: AlbumArtistDao,
+ private val composerDao: ComposerDao,
+ private val lyricistDao: LyricistDao,
+ private val genreDao: GenreDao,
+ private val playlistDao: PlaylistDao,
+): SearchService {
+ override suspend fun searchSongs(query: String): List {
+ return songDao.searchSongs(query)
+ }
+
+ override suspend fun searchAlbums(query: String): List {
+ return albumDao.searchAlbums(query)
+ }
+
+ override suspend fun searchArtists(query: String): List {
+ return artistDao.searchArtists(query)
+ }
+
+ override suspend fun searchAlbumArtists(query: String): List {
+ return albumArtistDao.searchAlbumArtists(query)
+ }
+
+ override suspend fun searchComposers(query: String): List {
+ return composerDao.searchComposers(query)
+ }
+
+ override suspend fun searchLyricists(query: String): List {
+ return lyricistDao.searchLyricists(query)
+ }
+
+ override suspend fun searchPlaylists(query: String): List {
+ return playlistDao.searchPlaylists(query)
+ }
+
+ override suspend fun searchGenres(query: String): List {
+ return genreDao.searchGenres(query)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/SleepTimerService.kt b/app/src/main/java/com/github/pakka_papad/data/services/SleepTimerService.kt
new file mode 100644
index 0000000..a1c54d2
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/SleepTimerService.kt
@@ -0,0 +1,61 @@
+package com.github.pakka_papad.data.services
+
+import android.app.PendingIntent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import org.jetbrains.annotations.VisibleForTesting
+
+interface SleepTimerService {
+ val isRunning: StateFlow
+ val timeLeft: StateFlow
+
+ fun cancel()
+ fun begin(duration: Int)
+}
+
+class SleepTimerServiceImpl(
+ private val scope: CoroutineScope,
+ private val closeIntent: PendingIntent,
+): SleepTimerService {
+
+ private var timerJob: Job? = null
+
+ @VisibleForTesting
+ internal val _isRunning = MutableStateFlow(false)
+ override val isRunning: StateFlow
+ = _isRunning.asStateFlow()
+
+ @VisibleForTesting
+ internal val _timeLeft = MutableStateFlow(0)
+ override val timeLeft: StateFlow
+ = _timeLeft.asStateFlow()
+
+ override fun cancel() {
+ timerJob?.cancel()
+ timerJob = null
+ _isRunning.update { false }
+ }
+
+ override fun begin(duration: Int) {
+ if (duration == 0) return
+ if (timerJob != null) return
+ timerJob = scope.launch {
+ _isRunning.update { true }
+ var left = duration
+ _timeLeft.update { left }
+ while (left >= 0){
+ delay(1000L)
+ left--
+ _timeLeft.update { left }
+ }
+ closeIntent.send()
+ _isRunning.update { false }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/SongService.kt b/app/src/main/java/com/github/pakka_papad/data/services/SongService.kt
new file mode 100644
index 0000000..fc844ab
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/SongService.kt
@@ -0,0 +1,107 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.data.daos.AlbumArtistDao
+import com.github.pakka_papad.data.daos.AlbumDao
+import com.github.pakka_papad.data.daos.ArtistDao
+import com.github.pakka_papad.data.daos.ComposerDao
+import com.github.pakka_papad.data.daos.GenreDao
+import com.github.pakka_papad.data.daos.LyricistDao
+import com.github.pakka_papad.data.daos.SongDao
+import com.github.pakka_papad.data.music.Album
+import com.github.pakka_papad.data.music.AlbumArtistWithSongCount
+import com.github.pakka_papad.data.music.AlbumArtistWithSongs
+import com.github.pakka_papad.data.music.AlbumWithSongs
+import com.github.pakka_papad.data.music.ArtistWithSongCount
+import com.github.pakka_papad.data.music.ArtistWithSongs
+import com.github.pakka_papad.data.music.ComposerWithSongCount
+import com.github.pakka_papad.data.music.ComposerWithSongs
+import com.github.pakka_papad.data.music.GenreWithSongCount
+import com.github.pakka_papad.data.music.GenreWithSongs
+import com.github.pakka_papad.data.music.LyricistWithSongCount
+import com.github.pakka_papad.data.music.LyricistWithSongs
+import com.github.pakka_papad.data.music.Song
+import kotlinx.coroutines.flow.Flow
+
+interface SongService {
+ val songs: Flow>
+ val albums: Flow>
+ val artists: Flow>
+ val albumArtists: Flow>
+ val composers: Flow>
+ val lyricists: Flow>
+ val genres: Flow>
+
+ fun getAlbumWithSongsByName(albumName: String): Flow
+ fun getArtistWithSongsByName(artistName: String): Flow
+ fun getAlbumArtistWithSongsByName(albumArtistName: String): Flow
+ fun getComposerWithSongsByName(composerName: String): Flow
+ fun getLyricistWithSongsByName(lyricistName: String): Flow
+ fun getGenreWithSongsByName(genre: String): Flow
+
+ fun getFavouriteSongs(): Flow>
+
+ suspend fun updateSong(song: Song)
+}
+
+class SongServiceImpl(
+ private val songDao: SongDao,
+ private val albumDao: AlbumDao,
+ private val artistDao: ArtistDao,
+ private val albumArtistDao: AlbumArtistDao,
+ private val composerDao: ComposerDao,
+ private val lyricistDao: LyricistDao,
+ private val genreDao: GenreDao,
+) :SongService {
+ override val songs: Flow>
+ = songDao.getAllSongs()
+
+ override val albums: Flow>
+ = albumDao.getAllAlbums()
+
+ override val artists: Flow>
+ = songDao.getAllArtistsWithSongCount()
+
+ override val albumArtists: Flow>
+ = songDao.getAllAlbumArtistsWithSongCount()
+
+ override val composers: Flow>
+ = songDao.getAllComposersWithSongCount()
+
+ override val lyricists: Flow>
+ = songDao.getAllLyricistsWithSongCount()
+
+ override val genres: Flow>
+ = songDao.getAllGenresWithSongCount()
+
+ override fun getAlbumWithSongsByName(albumName: String): Flow {
+ return albumDao.getAlbumWithSongsByName(albumName)
+ }
+
+ override fun getArtistWithSongsByName(artistName: String): Flow {
+ return artistDao.getArtistWithSongsByName(artistName)
+ }
+
+ override fun getAlbumArtistWithSongsByName(albumArtistName: String): Flow {
+ return albumArtistDao.getAlbumArtistWithSongs(albumArtistName)
+ }
+
+ override fun getComposerWithSongsByName(composerName: String): Flow {
+ return composerDao.getComposerWithSongs(composerName)
+ }
+
+ override fun getLyricistWithSongsByName(lyricistName: String): Flow {
+ return lyricistDao.getLyricistWithSongs(lyricistName)
+ }
+
+ override fun getGenreWithSongsByName(genre: String): Flow {
+ return genreDao.getGenreWithSongs(genre)
+ }
+
+ override fun getFavouriteSongs(): Flow> {
+ return songDao.getAllFavourites()
+ }
+
+ override suspend fun updateSong(song: Song) {
+ songDao.updateSong(song)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/services/ThumbnailService.kt b/app/src/main/java/com/github/pakka_papad/data/services/ThumbnailService.kt
new file mode 100644
index 0000000..9abc25f
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/services/ThumbnailService.kt
@@ -0,0 +1,175 @@
+package com.github.pakka_papad.data.services
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.graphics.Paint
+import androidx.core.graphics.scale
+import androidx.core.net.toUri
+import com.github.pakka_papad.data.thumbnails.Thumbnail
+import com.github.pakka_papad.data.thumbnails.ThumbnailDao
+import java.io.File
+
+interface ThumbnailService {
+ fun createThumbnailImage(imageUris: List): String?
+
+ suspend fun insert(thumbnail: Thumbnail)
+ suspend fun getThumbnailByPath(location: String): Thumbnail?
+ suspend fun deleteThumbnail(thumbnail: Thumbnail)
+ suspend fun markDelete(location: String)
+ suspend fun getPendingDeletions(): List
+}
+
+class ThumbnailServiceImpl(
+ private val context: Context,
+ private val thumbnailDao: ThumbnailDao,
+): ThumbnailService {
+
+ companion object {
+ private const val IMAGE_SIZE = 1600
+ private const val PARTS = 3
+ private const val DEGREES = 9f
+ private const val THUMBNAIL_DIR = "thumbnails"
+ }
+
+ override suspend fun insert(thumbnail: Thumbnail) {
+ thumbnailDao.insert(thumbnail)
+ }
+
+ override suspend fun getThumbnailByPath(location: String): Thumbnail? {
+ return thumbnailDao.getThumbnail(location)
+ }
+
+ override suspend fun deleteThumbnail(thumbnail: Thumbnail) {
+ thumbnailDao.delete(thumbnail)
+ val file = File(thumbnail.location)
+ if (!file.exists()) return
+ file.delete()
+ }
+
+ override suspend fun markDelete(location: String) {
+ thumbnailDao.markDelete(location)
+ }
+
+ override suspend fun getPendingDeletions(): List {
+ return thumbnailDao.getPendingDeletions()
+ }
+
+ override fun createThumbnailImage(imageUris: List): String? {
+ val options = BitmapFactory.Options().apply {
+ outHeight = 200
+ outWidth = 200
+ }
+ val bitmaps = imageUris.asSequence()
+ .filterNotNull()
+ .distinct()
+ .mapNotNull {
+ BitmapFactory.decodeStream(
+ context.contentResolver.openInputStream(it.toUri()),
+ null,
+ options
+ )
+ }
+ .take(9)
+ .toList()
+ if (bitmaps.isEmpty()) return null
+ val arranged = arrangeBitmaps(bitmaps)
+ val mergedImage = create(arranged, IMAGE_SIZE, PARTS)
+ val finalImage = rotate(mergedImage, IMAGE_SIZE, DEGREES)
+ val result = saveImage(finalImage)
+ finalImage.recycle()
+ mergedImage.recycle()
+ return result
+ }
+
+ private fun saveImage(image: Bitmap): String? {
+ val dir = File(context.filesDir, THUMBNAIL_DIR)
+ if (!dir.exists()) {
+ val created = dir.mkdir()
+ if (!created) return null
+ }
+ val file = File(dir, "thumb-${System.currentTimeMillis()}.png")
+ val result = image.compress(Bitmap.CompressFormat.PNG, 100, file.outputStream())
+ if (!result) return null
+ return file.path
+ }
+
+ private fun arrangeBitmaps(list: List): List {
+ return when {
+ list.size == 1 -> {
+ val item = list[0]
+ listOf(item, item, item, item, item, item, item, item, item)
+ }
+ list.size == 2 -> {
+ val item1 = list[0]
+ val item2 = list[1]
+ listOf(item1, item2, item1, item2, item1, item2, item1, item2, item1)
+ }
+ list.size == 3 -> {
+ val item1 = list[0]
+ val item2 = list[1]
+ val item3 = list[2]
+ listOf(item1, item2, item3, item3, item1, item2, item2, item3, item1)
+ }
+ list.size == 4 -> {
+ val item1 = list[0]
+ val item2 = list[1]
+ val item3 = list[2]
+ val item4 = list[3]
+ listOf(item1, item2, item3, item4, item1, item2, item3, item4, item1)
+ }
+ list.size < 9 -> { // 5 to 8
+ val item1 = list[0]
+ val item2 = list[1]
+ val item3 = list[2]
+ val item4 = list[3]
+ val item5 = list[4]
+ listOf(item1, item2, item3, item4, item5, item2, item3, item4, item1)
+ }
+ else -> list // case 9
+ }
+ }
+
+ private fun create(images: List, imageSize: Int, parts: Int): Bitmap {
+ val result = Bitmap.createBitmap(imageSize, imageSize, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(result)
+ canvas.drawARGB(0,0,0,0)
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ val onePartSize = imageSize / parts
+
+ images.forEachIndexed { i, bitmap ->
+ val bit = bitmap.scale(onePartSize, onePartSize)
+ canvas.drawBitmap(
+ bit,
+ (onePartSize * (i % parts)).toFloat() + (i % 3) * 50,
+ (onePartSize * (i / parts)).toFloat() + (i / 3) * 50,
+ paint
+ )
+ bit.recycle()
+ }
+ return result
+ }
+
+ private fun rotate(bitmap: Bitmap, imageSize: Int, degrees: Float): Bitmap {
+ val matrix = Matrix()
+ matrix.postRotate(degrees)
+
+ val rotated = Bitmap.createBitmap(bitmap, 0, 0, imageSize, imageSize, matrix, true)
+ bitmap.recycle()
+ val cropStart = imageSize * 25 / 100
+ val cropEnd: Int = (cropStart * 1.5).toInt()
+ val cropped = Bitmap.createBitmap(
+ rotated,
+ cropStart,
+ cropStart,
+ imageSize - cropEnd,
+ imageSize - cropEnd
+ )
+ rotated.recycle()
+
+ return cropped
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/thumbnails/Thumbnail.kt b/app/src/main/java/com/github/pakka_papad/data/thumbnails/Thumbnail.kt
new file mode 100644
index 0000000..3d82e37
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/thumbnails/Thumbnail.kt
@@ -0,0 +1,19 @@
+package com.github.pakka_papad.data.thumbnails
+
+import androidx.room.Entity
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import com.github.pakka_papad.Constants
+
+@Entity(
+ tableName = Constants.Tables.THUMBNAIL_TABLE,
+ indices = [
+ Index(value = ["location"], unique = true)
+ ]
+)
+data class Thumbnail(
+ @PrimaryKey val location: String,
+ val addedOn: Long,
+ val artCount: Int,
+ val deleteThis: Boolean,
+)
diff --git a/app/src/main/java/com/github/pakka_papad/data/thumbnails/ThumbnailDao.kt b/app/src/main/java/com/github/pakka_papad/data/thumbnails/ThumbnailDao.kt
new file mode 100644
index 0000000..d02221d
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/thumbnails/ThumbnailDao.kt
@@ -0,0 +1,27 @@
+package com.github.pakka_papad.data.thumbnails
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.github.pakka_papad.Constants
+
+@Dao
+interface ThumbnailDao {
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE, entity = Thumbnail::class)
+ suspend fun insert(thumbnail: Thumbnail)
+
+ @Query("SELECT * FROM ${Constants.Tables.THUMBNAIL_TABLE} WHERE location = :location")
+ suspend fun getThumbnail(location: String): Thumbnail?
+
+ @Delete(entity = Thumbnail::class)
+ suspend fun delete(thumbnail: Thumbnail)
+
+ @Query("UPDATE ${Constants.Tables.THUMBNAIL_TABLE} SET deleteThis = 1 WHERE location = :location")
+ suspend fun markDelete(location: String)
+
+ @Query("SELECT * FROM ${Constants.Tables.THUMBNAIL_TABLE} WHERE deleteThis = 1")
+ suspend fun getPendingDeletions(): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/di/AppModule.kt b/app/src/main/java/com/github/pakka_papad/di/AppModule.kt
index 3ffeb72..e744f8b 100644
--- a/app/src/main/java/com/github/pakka_papad/di/AppModule.kt
+++ b/app/src/main/java/com/github/pakka_papad/di/AppModule.kt
@@ -1,7 +1,9 @@
package com.github.pakka_papad.di
import android.annotation.SuppressLint
+import android.app.PendingIntent
import android.content.Context
+import android.content.Intent
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
@@ -13,19 +15,23 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.mp3.Mp3Extractor
import androidx.room.Room
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
import com.github.pakka_papad.Constants
import com.github.pakka_papad.data.*
-import com.github.pakka_papad.data.components.DaoCollection
import com.github.pakka_papad.data.music.SongExtractor
import com.github.pakka_papad.data.notification.ZenNotificationManager
+import com.github.pakka_papad.data.services.*
+import com.github.pakka_papad.player.ZenBroadcastReceiver
+import com.github.pakka_papad.util.MessageStore
+import com.github.pakka_papad.util.MessageStoreImpl
import com.google.firebase.crashlytics.FirebaseCrashlytics
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@@ -45,36 +51,6 @@ object AppModule {
).build()
}
- @Singleton
- @Provides
- fun providesDataManager(
- @ApplicationContext context: Context,
- notificationManager: ZenNotificationManager,
- db: AppDatabase,
- scope: CoroutineScope,
- extractor: SongExtractor,
- ): DataManager {
- return DataManager(
- context = context,
- notificationManager = notificationManager,
- daoCollection = DaoCollection(
- songDao = db.songDao(),
- albumDao = db.albumDao(),
- artistDao = db.artistDao(),
- albumArtistDao = db.albumArtistDao(),
- composerDao = db.composerDao(),
- lyricistDao = db.lyricistDao(),
- genreDao = db.genreDao(),
- playlistDao = db.playlistDao(),
- blacklistDao = db.blacklistDao(),
- blacklistedFolderDao = db.blacklistedFolderDao(),
- playHistoryDao = db.playHistoryDao()
- ),
- scope = scope,
- songExtractor = extractor
- )
- }
-
@Singleton
@Provides
fun providesNotificationManager(@ApplicationContext context: Context): ZenNotificationManager {
@@ -95,8 +71,8 @@ object AppModule {
setUsage(USAGE_MEDIA)
}.build()
return ExoPlayer.Builder(context).apply {
- setMediaSourceFactory(DefaultMediaSourceFactory(context,extractorsFactory))
- setAudioAttributes(audioAttributes,true)
+ setMediaSourceFactory(DefaultMediaSourceFactory(context, extractorsFactory))
+ setAudioAttributes(audioAttributes, true)
setHandleAudioBecomingNoisy(true)
}.build()
}
@@ -148,12 +124,151 @@ object AppModule {
fun providesSongExtractor(
@ApplicationContext context: Context,
crashReporter: ZenCrashReporter,
+ db: AppDatabase,
): SongExtractor {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
return SongExtractor(
scope = scope,
context = context,
crashReporter = crashReporter,
+ songDao = db.songDao(),
+ albumDao = db.albumDao(),
+ artistDao = db.artistDao(),
+ albumArtistDao = db.albumArtistDao(),
+ composerDao = db.composerDao(),
+ lyricistDao = db.lyricistDao(),
+ genreDao = db.genreDao(),
+ blacklistDao = db.blacklistDao(),
+ blacklistedFolderDao = db.blacklistedFolderDao(),
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesBlacklistService(
+ db: AppDatabase
+ ): BlacklistService {
+ return BlacklistServiceImpl(
+ blacklistDao = db.blacklistDao(),
+ blacklistedFolderDao = db.blacklistedFolderDao(),
+ songDao = db.songDao(),
+ albumDao = db.albumDao(),
+ artistDao = db.artistDao(),
+ albumArtistDao = db.albumArtistDao(),
+ composerDao = db.composerDao(),
+ lyricistDao = db.lyricistDao(),
+ genreDao = db.genreDao(),
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesPlaylistService(
+ db: AppDatabase
+ ): PlaylistService {
+ return PlaylistServiceImpl(
+ playlistDao = db.playlistDao(),
+ thumbnailDao = db.thumbnailDao(),
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesSongService(
+ db: AppDatabase
+ ): SongService {
+ return SongServiceImpl(
+ songDao = db.songDao(),
+ albumDao = db.albumDao(),
+ artistDao = db.artistDao(),
+ albumArtistDao = db.albumArtistDao(),
+ composerDao = db.composerDao(),
+ lyricistDao = db.lyricistDao(),
+ genreDao = db.genreDao(),
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesQueueService(): QueueService {
+ return QueueServiceImpl()
+ }
+
+ @Singleton
+ @Provides
+ fun providesPlayerService(
+ @ApplicationContext context: Context
+ ): PlayerService {
+ return PlayerServiceImpl(
+ context = context
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesAnalyticsService(
+ db: AppDatabase,
+ ): AnalyticsService {
+ return AnalyticsServiceImpl(
+ playHistoryDao = db.playHistoryDao(),
+ scope = CoroutineScope(Job() + Dispatchers.IO)
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesSearchService(
+ db: AppDatabase,
+ ): SearchService {
+ return SearchServiceImpl(
+ songDao = db.songDao(),
+ albumDao = db.albumDao(),
+ artistDao = db.artistDao(),
+ albumArtistDao = db.albumArtistDao(),
+ composerDao = db.composerDao(),
+ lyricistDao = db.lyricistDao(),
+ genreDao = db.genreDao(),
+ playlistDao = db.playlistDao()
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesMessageStore(
+ @ApplicationContext context: Context,
+ ): MessageStore {
+ return MessageStoreImpl(
+ context = context,
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesSleepTimerService(
+ @ApplicationContext context: Context,
+ ): SleepTimerService {
+ return SleepTimerServiceImpl(
+ scope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
+ closeIntent = PendingIntent.getBroadcast(
+ context, ZenBroadcastReceiver.CANCEL_ACTION_REQUEST_CODE,
+ Intent(Constants.PACKAGE_NAME).putExtra(
+ ZenBroadcastReceiver.AUDIO_CONTROL,
+ ZenBroadcastReceiver.ZEN_PLAYER_CANCEL
+ ),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ }
+
+ @Singleton
+ @Provides
+ fun providesThumbnailService(
+ @ApplicationContext context: Context,
+ db: AppDatabase
+ ): ThumbnailService {
+ return ThumbnailServiceImpl(
+ context = context,
+ thumbnailDao = db.thumbnailDao(),
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/home/Albums.kt b/app/src/main/java/com/github/pakka_papad/home/Albums.kt
index 19302eb..3421f11 100644
--- a/app/src/main/java/com/github/pakka_papad/home/Albums.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/Albums.kt
@@ -12,9 +12,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.FullScreenSadMessage
import com.github.pakka_papad.data.music.Album
@@ -27,7 +29,7 @@ fun Albums(
if (albums == null) return
if (albums.isEmpty()) {
FullScreenSadMessage(
- message = "Oops! No albums found",
+ message = stringResource(R.string.oops_no_albums_found),
paddingValues = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
)
} else {
@@ -66,7 +68,7 @@ fun AlbumCard(
) {
AsyncImage(
model = album.albumArtUri,
- contentDescription = null,
+ contentDescription = stringResource(R.string.album_art),
modifier = Modifier
.aspectRatio(ratio = 1f, matchHeightConstraintsFirst = false)
.fillMaxWidth()
diff --git a/app/src/main/java/com/github/pakka_papad/home/AllSongs.kt b/app/src/main/java/com/github/pakka_papad/home/AllSongs.kt
index d6c34df..517e692 100644
--- a/app/src/main/java/com/github/pakka_papad/home/AllSongs.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/AllSongs.kt
@@ -12,6 +12,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
@@ -19,6 +20,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.window.DialogProperties
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.FullScreenSadMessage
import com.github.pakka_papad.components.SongCardV1
import com.github.pakka_papad.components.more_options.SongOptions
@@ -41,7 +43,7 @@ fun AllSongs(
if (songs == null) return
if (songs.isEmpty()) {
FullScreenSadMessage(
- message = "No songs found on this device",
+ message = stringResource(R.string.no_songs_found_on_this_device),
paddingValues = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues()
)
} else {
@@ -110,7 +112,7 @@ fun SongInfo(
onClick = onDismissRequest,
) {
Text(
- text = "Close",
+ text = stringResource(R.string.close),
style = MaterialTheme.typography.bodyLarge,
)
}
@@ -123,32 +125,32 @@ fun SongInfo(
val scrollState = rememberScrollState()
Text(
text = buildAnnotatedString {
- append("Location\n")
+ append("${stringResource(R.string.location)}\n")
withStyle(spanStyle) { append(song.location) }
- append("\n\nSize\n")
+ append("\n\n${stringResource(R.string.size)}\n")
withStyle(spanStyle) { append(song.size) }
- append("\n\nAlbum\n")
+ append("\n\n${stringResource(R.string.album)}\n")
withStyle(spanStyle) { append(song.album) }
- append("\n\nArtist\n")
+ append("\n\n${stringResource(R.string.artist)}\n")
withStyle(spanStyle) { append(song.artist) }
- append("\n\nAlbum artist\n")
+ append("\n\n${stringResource(R.string.album_artist)}\n")
withStyle(spanStyle) { append(song.albumArtist) }
- append("\n\nComposer\n")
+ append("\n\n${stringResource(R.string.composer)}\n")
withStyle(spanStyle) { append(song.composer) }
- append("\n\nLyricist\n")
+ append("\n\n${stringResource(R.string.lyricist)}\n")
withStyle(spanStyle) { append(song.lyricist) }
- append("\n\nGenre\n")
+ append("\n\n${stringResource(R.string.genre)}\n")
withStyle(spanStyle) { append(song.genre) }
- append("\n\nYear\n")
- withStyle(spanStyle) { append(if (song.year == 0) "Unknown" else song.year.toString()) }
- append("\n\nDuration\n")
- withStyle(spanStyle) { append(if (song.durationMillis == 0L) "Unknown" else song.durationFormatted) }
- append("\n\nPlay count\n")
+ append("\n\n${stringResource(R.string.year)}\n")
+ withStyle(spanStyle) { append(if (song.year == 0) stringResource(R.string.unknown) else song.year.toString()) }
+ append("\n\n${stringResource(R.string.duration)}\n")
+ withStyle(spanStyle) { append(if (song.durationMillis == 0L) stringResource(R.string.unknown) else song.durationFormatted) }
+ append("\n\n${stringResource(R.string.play_count)}\n")
withStyle(spanStyle) { append(song.playCount.toString()) }
- append("\n\nLast played on\n")
- withStyle(spanStyle) { append(if (song.lastPlayed == null) "Never" else song.lastPlayed.formatToDate()) }
- append("\n\nMime type\n")
- withStyle(spanStyle) { append(song.mimeType ?: "Unknown") }
+ append("\n\n${stringResource(R.string.last_played_on)}\n")
+ withStyle(spanStyle) { append(if (song.lastPlayed == null) stringResource(R.string.never) else song.lastPlayed.formatToDate()) }
+ append("\n\n${stringResource(R.string.mime_type)}\n")
+ withStyle(spanStyle) { append(song.mimeType ?: stringResource(R.string.unknown)) }
},
modifier = Modifier
.verticalScroll(scrollState)
diff --git a/app/src/main/java/com/github/pakka_papad/home/Files.kt b/app/src/main/java/com/github/pakka_papad/home/Files.kt
index 4695fdf..3e44578 100644
--- a/app/src/main/java/com/github/pakka_papad/home/Files.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/Files.kt
@@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.github.pakka_papad.components.FullScreenSadMessage
@@ -45,7 +46,7 @@ fun Files(
onFolderAddToBlacklistRequest: (Directory) -> Unit,
){
if (contents.directories.isEmpty() && contents.songs.isEmpty()){
- FullScreenSadMessage("Nothing here")
+ FullScreenSadMessage(stringResource(R.string.nothing_here))
return
}
LazyColumn(
@@ -103,7 +104,7 @@ fun Folder(
) {
Icon(
painter = resource,
- contentDescription = null,
+ contentDescription = stringResource(R.string.folder_icon),
modifier = Modifier.size(50.dp)
)
Text(
@@ -121,7 +122,7 @@ fun Folder(
} else {
Icon(
imageVector = Icons.Outlined.MoreVert,
- contentDescription = null,
+ contentDescription = stringResource(R.string.more_menu_button),
modifier = Modifier
.size(26.dp)
.clickable(
diff --git a/app/src/main/java/com/github/pakka_papad/home/Genres.kt b/app/src/main/java/com/github/pakka_papad/home/Genres.kt
index 82d521e..399f1fb 100644
--- a/app/src/main/java/com/github/pakka_papad/home/Genres.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/Genres.kt
@@ -15,9 +15,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.FullScreenSadMessage
import com.github.pakka_papad.components.more_options.GenreOptions
import com.github.pakka_papad.components.more_options.OptionsAlertDialog
@@ -31,7 +34,7 @@ fun Genres(
) {
if (genresWithSongCount.isEmpty()) {
FullScreenSadMessage(
- message = "Nothing found"
+ message = stringResource(R.string.nothing_found)
)
return
}
@@ -82,7 +85,11 @@ fun GenreCard(
fontWeight = FontWeight.Bold,
)
Text(
- text = "${genreWithSongCount.count} ${if (genreWithSongCount.count == 1) "song" else "songs"}",
+ text = pluralStringResource(
+ id = R.plurals.song_count,
+ count = genreWithSongCount.count,
+ genreWithSongCount.count
+ ),
maxLines = 1,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.fillMaxWidth(),
@@ -93,7 +100,7 @@ fun GenreCard(
var optionsVisible by remember { mutableStateOf(false) }
Icon(
imageVector = Icons.Outlined.MoreVert,
- contentDescription = null,
+ contentDescription = stringResource(R.string.more_menu_button),
modifier = Modifier
.size(26.dp)
.clickable(
diff --git a/app/src/main/java/com/github/pakka_papad/home/HomeFragment.kt b/app/src/main/java/com/github/pakka_papad/home/HomeFragment.kt
index 4cfadbd..2965997 100644
--- a/app/src/main/java/com/github/pakka_papad/home/HomeFragment.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/HomeFragment.kt
@@ -10,17 +10,44 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+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.systemBars
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material.rememberSwipeableState
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.LocalAbsoluteTonalElevation
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SheetValue
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.material3.rememberStandardBottomSheetState
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
@@ -31,22 +58,19 @@ import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import com.github.pakka_papad.Constants
-import com.github.pakka_papad.R
import com.github.pakka_papad.Screens
-import com.github.pakka_papad.collection.CollectionType
import com.github.pakka_papad.components.BottomSheet
+import com.github.pakka_papad.components.Snackbar
import com.github.pakka_papad.data.ZenPreferenceProvider
-import com.github.pakka_papad.data.music.*
-import com.github.pakka_papad.nowplaying.NowPlayingOptions
+import com.github.pakka_papad.data.services.SleepTimerService
import com.github.pakka_papad.nowplaying.NowPlayingScreen
-import com.github.pakka_papad.nowplaying.NowPlayingTopBar
+import com.github.pakka_papad.nowplaying.PlayerHelper
import com.github.pakka_papad.nowplaying.Queue
import com.github.pakka_papad.player.ZenBroadcastReceiver
import com.github.pakka_papad.ui.theme.ZenTheme
@@ -63,13 +87,12 @@ class HomeFragment : Fragment() {
private val viewModel: HomeViewModel by viewModels()
- @Inject
- lateinit var exoPlayer: ExoPlayer
+ @Inject lateinit var exoPlayer: ExoPlayer
+ @Inject lateinit var preferenceProvider: ZenPreferenceProvider
+ @Inject lateinit var sleepTimerService: SleepTimerService
- @Inject
- lateinit var preferenceProvider: ZenPreferenceProvider
-
- @OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class,
+ @OptIn(
+ ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class,
ExperimentalMaterialApi::class
)
override fun onCreateView(
@@ -102,6 +125,9 @@ class HomeFragment : Fragment() {
),
PendingIntent.FLAG_IMMUTABLE
)
+
+ val navHelper = HomeNavHelper(navController, lifecycle)
+ val playerHelper = PlayerHelper(exoPlayer)
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
@@ -128,13 +154,16 @@ class HomeFragment : Fragment() {
val allPersonsListState = rememberLazyListState()
val playlistsWithSongCount by viewModel.playlistsWithSongCount.collectAsStateWithLifecycle()
- val allPlaylistsListState = rememberLazyListState()
+ val allPlaylistsListState = rememberLazyGridState()
val genresWithSongCount by viewModel.genresWithSongCount.collectAsStateWithLifecycle()
val allGenresListState = rememberLazyListState()
val files by viewModel.filesInCurrentDestination.collectAsStateWithLifecycle()
+ val isSleepTimerRunning by sleepTimerService.isRunning.collectAsStateWithLifecycle()
+ val sleepTimerLeft by sleepTimerService.timeLeft.collectAsStateWithLifecycle()
+
val dataRetrieved by remember {
derivedStateOf {
songs != null && albums != null && personsWithSongCount != null
@@ -151,28 +180,38 @@ class HomeFragment : Fragment() {
swipeableState.animateTo(0)
}
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val message by viewModel.message.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = message){
+ if (message.isEmpty()) return@LaunchedEffect
+ snackbarHostState.showSnackbar(message)
+ }
+
val bottomBarYOffset by remember {
derivedStateOf {
- val progress = if (swipeableState.progress.from == 0){
+ val progress = if (swipeableState.progress.from == 0) {
// at 0 or moving away from 0
if (swipeableState.progress.to == 0) 1f
- else if (swipeableState.progress.fraction < 0.25f) 1f-swipeableState.progress.fraction*4
+ else if (swipeableState.progress.fraction < 0.25f) 1f - swipeableState.progress.fraction * 4
else 0f
} else {
// at 1 or moving away from 1
if (swipeableState.progress.to == 1) 0f
- else if (swipeableState.progress.fraction > 0.75f) 1f-(1f-swipeableState.progress.fraction)*4
+ else if (swipeableState.progress.fraction > 0.75f) 1f - (1f - swipeableState.progress.fraction) * 4
else 0f
}
- 500*(1f-progress)
+ 500 * (1f - progress)
}
}
val windowInsets = WindowInsets.systemBars.asPaddingValues()
val queue = viewModel.queue
- val playbackParams by preferenceProvider.playbackParams.collectAsStateWithLifecycle()
+ val playbackParams by preferenceProvider.playbackParams.collectAsStateWithLifecycle()
val repeatMode by viewModel.repeatMode.collectAsStateWithLifecycle()
- val playerScaffoldState = rememberBottomSheetScaffoldState()
+ val playerScaffoldState = rememberBottomSheetScaffoldState(
+ bottomSheetState = rememberStandardBottomSheetState(skipHiddenState = false)
+ )
val isExplorerAtRoot by viewModel.isExplorerAtRoot.collectAsStateWithLifecycle()
@@ -186,7 +225,7 @@ class HomeFragment : Fragment() {
BackHandler(
enabled = (currentScreen == Screens.Folders && !isExplorerAtRoot) || swipeableState.currentValue == 1,
onBack = {
- if (swipeableState.currentValue == 1){
+ if (swipeableState.currentValue == 1) {
scope.launch {
if (isQueueBottomSheetExpanded) playerScaffoldState.bottomSheetState.hide()
else swipeableState.animateTo(0)
@@ -197,36 +236,21 @@ class HomeFragment : Fragment() {
}
)
- val navigateToSettings = remember{ {
- if (navController.currentDestination?.id == R.id.homeFragment){
- navController.navigate(R.id.action_homeFragment_to_settingsFragment)
- }
- } }
- val navigateToSearch = remember{ {
- if(navController.currentDestination?.id == R.id.homeFragment){
- navController.navigate(R.id.action_homeFragment_to_searchFragment)
- }
- } }
- val songScreenSongClicked = remember{
- { index: Int -> viewModel.setQueue(songs,index) }
+ val songScreenSongClicked = remember {
+ { index: Int -> viewModel.setQueue(songs, index) }
}
- val songScreenPlayAllClicked = remember{ { viewModel.setQueue(songs) } }
- val songScreenShuffleClicked = remember{ { viewModel.shufflePlay(songs) } }
- val miniPlayerPlayPauseClicked = remember{ {
- if (swipeableState.currentValue == 0){
- pendingPausePlayIntent.send()
- }
+ val songScreenPlayAllClicked = remember { { viewModel.setQueue(songs) } }
+ val songScreenShuffleClicked = remember { { viewModel.shufflePlay(songs) } }
+ val miniPlayerPlayPauseClicked = remember { {
+ if (swipeableState.currentValue == 0) { pendingPausePlayIntent.send() }
} }
- val nowPlayingBackArrowClicked = remember<() -> Unit>{ {
- scope.launch { swipeableState.animateTo(0) }
- } }
- val expandQueueBottomSheet = remember<() -> Unit>{
+ val expandQueueBottomSheet = remember<() -> Unit> {
{ scope.launch { playerScaffoldState.bottomSheetState.expand() } }
}
- val updateScreen = remember<(Screens) -> Unit>{ {
- if (currentScreen == it){
+ val updateScreen = remember<(Screens) -> Unit> { {
+ if (currentScreen == it) {
scope.launch {
- when(it){
+ when (it) {
Screens.Songs -> allSongsListState.scrollToItem(0)
Screens.Albums -> allAlbumsGridState.scrollToItem(0)
Screens.Artists -> allPersonsListState.scrollToItem(0)
@@ -240,16 +264,25 @@ class HomeFragment : Fragment() {
}
} }
+ val homeScreenBottomPadding by remember(currentSong) {
+ derivedStateOf {
+ if (currentSong == null) 88.dp else 146.dp
+ }
+ }
+
Box(
modifier = Modifier
- .fillMaxSize(),
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.BottomCenter
- ){
+ ) {
Scaffold(
+ modifier = Modifier
+ .padding(bottom = homeScreenBottomPadding),
topBar = {
HomeTopBar(
- onSettingsClicked = navigateToSettings,
- onSearchClicked = navigateToSearch,
+ onSettingsClicked = navHelper::navigateToSettings,
+ onSearchClicked = navHelper::navigateToSearch,
currentScreen = currentScreen,
onSortOptionChosen = viewModel::saveSortOption,
currentSortOrder = sortOrder,
@@ -260,7 +293,6 @@ class HomeFragment : Fragment() {
modifier = Modifier
.padding(
top = it.calculateTopPadding(),
- bottom = if (currentSong == null) 88.dp else 146.dp,
start = windowInsets.calculateStartPadding(
LayoutDirection.Ltr
),
@@ -289,7 +321,7 @@ class HomeFragment : Fragment() {
onAddToQueueClicked = viewModel::addToQueue,
onPlayAllClicked = songScreenPlayAllClicked,
onShuffleClicked = songScreenShuffleClicked,
- onAddToPlaylistsClicked = this@HomeFragment::addToPlaylistClicked,
+ onAddToPlaylistsClicked = navHelper::navigateToChoosePlaylist,
onBlacklistClicked = viewModel::onSongBlacklist
)
}
@@ -297,13 +329,13 @@ class HomeFragment : Fragment() {
Albums(
albums = albums,
gridState = allAlbumsGridState,
- onAlbumClicked = this@HomeFragment::navigateToCollection
+ onAlbumClicked = navHelper::navigateToViewDetails
)
}
Screens.Artists -> {
Persons(
personsWithSongCount = personsWithSongCount,
- onPersonClicked = this@HomeFragment::navigateToCollection,
+ onPersonClicked = navHelper::navigateToViewDetails,
listState = allPersonsListState,
selectedPerson = selectedPerson,
onPersonSelect = viewModel::onPersonSelect
@@ -312,10 +344,10 @@ class HomeFragment : Fragment() {
Screens.Playlists -> {
Playlists(
playlistsWithSongCount = playlistsWithSongCount,
- onPlaylistClicked = this@HomeFragment::navigateToCollection,
+ onPlaylistClicked = navHelper::navigateToViewDetails,
listState = allPlaylistsListState,
onPlaylistCreate = viewModel::onPlaylistCreate,
- onFavouritesClicked = this@HomeFragment::navigateToCollection,
+ onFavouritesClicked = navHelper::navigateToViewDetails,
onDeletePlaylistClicked = viewModel::deletePlaylist,
)
}
@@ -323,37 +355,41 @@ class HomeFragment : Fragment() {
Genres(
genresWithSongCount = genresWithSongCount,
listState = allGenresListState,
- onGenreClicked = this@HomeFragment::navigateToCollection
+ onGenreClicked = navHelper::navigateToViewDetails
)
}
Screens.Folders -> {
- Files(
- contents = files,
- onDirectoryClicked = viewModel::onFileClicked,
- onSongClicked = viewModel::onFileClicked,
- currentSong = currentSong,
- onAddToPlaylistClicked = this@HomeFragment::addToPlaylistClicked,
- onAddToQueueClicked = viewModel::addToQueue,
- onFolderAddToBlacklistRequest = viewModel::onFolderBlacklist
- )
+ Files(
+ contents = files,
+ onDirectoryClicked = viewModel::onFileClicked,
+ onSongClicked = viewModel::onFileClicked,
+ currentSong = currentSong,
+ onAddToPlaylistClicked = navHelper::navigateToChoosePlaylist,
+ onAddToQueueClicked = viewModel::addToQueue,
+ onFolderAddToBlacklistRequest = viewModel::onFolderBlacklist
+ )
}
}
}
}
}
},
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ snackbar = { Snackbar(it) }
+ )
+ }
)
BottomSheet(
- peekHeight = ( if(currentSong == null) 88.dp else 146.dp) + windowInsets.calculateBottomPadding(),
+ peekHeight = homeScreenBottomPadding + windowInsets.calculateBottomPadding(),
peekContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(bottomBarColor)
.padding(
- start = windowInsets.calculateStartPadding(
- LayoutDirection.Ltr
- ),
+ start = windowInsets.calculateStartPadding(LayoutDirection.Ltr),
end = windowInsets.calculateEndPadding(LayoutDirection.Ltr),
)
) {
@@ -369,22 +405,27 @@ class HomeFragment : Fragment() {
var progress by remember { mutableStateOf(0f) }
DisposableEffect(Unit) {
- progress = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
+ progress =
+ playerHelper.currentPosition/ playerHelper.duration
val listener = object : Player.Listener {
- override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
+ override fun onMediaItemTransition(
+ mediaItem: MediaItem?,
+ reason: Int
+ ) {
super.onMediaItemTransition(mediaItem, reason)
progress = 0f
}
}
- exoPlayer.addListener(listener)
+ playerHelper.addListener(listener)
onDispose {
- exoPlayer.removeListener(listener)
+ playerHelper.removeListener(listener)
}
}
if (songPlaying == true && swipeableState.currentValue == 0) {
LaunchedEffect(Unit) {
while (true) {
- progress = exoPlayer.currentPosition.toFloat() / exoPlayer.duration.toFloat()
+ progress =
+ playerHelper.currentPosition / playerHelper.duration
delay(40)
}
}
@@ -401,17 +442,6 @@ class HomeFragment : Fragment() {
currentSong?.let {
BottomSheetScaffold(
scaffoldState = playerScaffoldState,
- topBar = {
- NowPlayingTopBar(
- onBackArrowPressed = nowPlayingBackArrowClicked,
- title = it.title,
- options = listOf(
- NowPlayingOptions.SaveToPlaylist {
- if(swipeableState.currentValue == 1) saveToPlaylistClicked(queue)
- }
- )
- )
- },
content = { paddingValues ->
NowPlayingScreen(
paddingValues = paddingValues,
@@ -420,13 +450,19 @@ class HomeFragment : Fragment() {
onPreviousPressed = pendingPreviousIntent::send,
onNextPressed = pendingNextIntent::send,
songPlaying = songPlaying,
- exoPlayer = exoPlayer,
+ playerHelper = playerHelper,
+ currentSongPlaying = songPlaying,
onFavouriteClicked = viewModel::changeFavouriteValue,
onQueueClicked = expandQueueBottomSheet,
repeatMode = repeatMode,
toggleRepeatMode = viewModel::toggleRepeatMode,
playbackParams = playbackParams,
- updatePlaybackParams = preferenceProvider::updatePlaybackParams
+ updatePlaybackParams = preferenceProvider::updatePlaybackParams,
+ isTimerRunning = isSleepTimerRunning,
+ timeLeft = sleepTimerLeft,
+ onTimerBegin = sleepTimerService::begin,
+ onTimerCancel = sleepTimerService::cancel,
+ onSaveQueueClicked = { navHelper.navigateToChoosePlaylist(queue) },
)
},
sheetContent = {
@@ -435,7 +471,7 @@ class HomeFragment : Fragment() {
onFavouriteClicked = viewModel::changeFavouriteValue,
currentSong = it,
expanded = isQueueBottomSheetExpanded,
- exoPlayer = exoPlayer,
+ playerHelper = playerHelper,
onDrag = viewModel::onSongDrag
)
},
@@ -457,7 +493,7 @@ class HomeFragment : Fragment() {
.graphicsLayer {
translationY = bottomBarYOffset
}
- ){
+ ) {
HomeBottomBar(
currentScreen = currentScreen,
onScreenChange = updateScreen,
@@ -470,99 +506,4 @@ class HomeFragment : Fragment() {
}
}
}
-
- private fun navigateToCollection(album: Album){
- if (navController.currentDestination?.id != R.id.homeFragment) return
- navController.navigate(
- HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
- CollectionType(CollectionType.AlbumType,album.name)
- )
- )
- }
-
- private fun navigateToCollection(personWithSongCount: PersonWithSongCount) {
- if (navController.currentDestination?.id != R.id.homeFragment) return
- when(personWithSongCount){
- is ArtistWithSongCount -> {
- navController.navigate(
- HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
- CollectionType(CollectionType.ArtistType,personWithSongCount.name)
- )
- )
- }
- is AlbumArtistWithSongCount -> {
- navController.navigate(
- HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
- CollectionType(CollectionType.AlbumArtistType,personWithSongCount.name)
- )
- )
- }
- is ComposerWithSongCount -> {
- navController.navigate(
- HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
- CollectionType(CollectionType.ComposerType,personWithSongCount.name)
- )
- )
- }
- is LyricistWithSongCount -> {
- navController.navigate(
- HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
- CollectionType(CollectionType.LyricistType,personWithSongCount.name)
- )
- )
- }
- }
- }
-
- private fun navigateToCollection(playlistId: Long){
- if (navController.currentDestination?.id != R.id.homeFragment) return
- navController.navigate(
- HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
- CollectionType(CollectionType.PlaylistType,playlistId.toString())
- )
- )
- }
-
- private fun navigateToCollection(genreWithSongCount: GenreWithSongCount){
- if (navController.currentDestination?.id != R.id.homeFragment) return
- navController.navigate(
- HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
- CollectionType(CollectionType.GenreType,genreWithSongCount.genreName)
- )
- )
- }
-
- private fun navigateToCollection(){
- if (navController.currentDestination?.id != R.id.homeFragment) return
- navController.navigate(
- HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
- CollectionType(CollectionType.FavouritesType)
- )
- )
- }
-
- private fun saveToPlaylistClicked(queue: List){
- lifecycleScope.launch {
- val songLocations = queue.map { it.location }
- if (navController.currentDestination?.id != R.id.homeFragment) return@launch
- navController.navigate(
- HomeFragmentDirections
- .actionHomeFragmentToSelectPlaylistFragment(songLocations.toTypedArray())
- )
- }
- }
-
- private fun addToPlaylistClicked(song: Song){
- saveToPlaylistClicked(listOf(song))
- }
-
- private fun addToPlaylistClicked(song: MiniSong){
- lifecycleScope.launch {
- if (navController.currentDestination?.id != R.id.homeFragment) return@launch
- navController.navigate(
- HomeFragmentDirections
- .actionHomeFragmentToSelectPlaylistFragment(arrayOf(song.location))
- )
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/home/HomeNavHelper.kt b/app/src/main/java/com/github/pakka_papad/home/HomeNavHelper.kt
new file mode 100644
index 0000000..eca699e
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/home/HomeNavHelper.kt
@@ -0,0 +1,134 @@
+package com.github.pakka_papad.home
+
+import androidx.compose.runtime.Stable
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
+import androidx.navigation.NavController
+import com.github.pakka_papad.R
+import com.github.pakka_papad.collection.CollectionType
+import com.github.pakka_papad.data.music.Album
+import com.github.pakka_papad.data.music.AlbumArtistWithSongCount
+import com.github.pakka_papad.data.music.ArtistWithSongCount
+import com.github.pakka_papad.data.music.ComposerWithSongCount
+import com.github.pakka_papad.data.music.GenreWithSongCount
+import com.github.pakka_papad.data.music.LyricistWithSongCount
+import com.github.pakka_papad.data.music.MiniSong
+import com.github.pakka_papad.data.music.PersonWithSongCount
+import com.github.pakka_papad.data.music.Song
+import kotlinx.coroutines.launch
+
+@Stable
+class HomeNavHelper(
+ private val navController: NavController,
+ private val lifecycle: Lifecycle,
+) {
+ private fun check(): Boolean {
+ return navController.currentDestination?.id == R.id.homeFragment
+ }
+
+ fun navigateToSettings() {
+ if (!check()) return
+ navController.navigate(R.id.action_homeFragment_to_settingsFragment)
+ }
+
+ fun navigateToSearch() {
+ if (!check()) return
+ navController.navigate(R.id.action_homeFragment_to_searchFragment)
+ }
+
+ fun navigateToViewDetails(album: Album) {
+ if (!check()) return
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
+ CollectionType(CollectionType.AlbumType, album.name)
+ )
+ )
+ }
+
+ fun navigateToViewDetails(personWithSongCount: PersonWithSongCount) {
+ if (!check()) return
+ when (personWithSongCount) {
+ is ArtistWithSongCount -> {
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
+ CollectionType(CollectionType.ArtistType, personWithSongCount.name)
+ )
+ )
+ }
+ is AlbumArtistWithSongCount -> {
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
+ CollectionType(CollectionType.AlbumArtistType, personWithSongCount.name)
+ )
+ )
+ }
+ is ComposerWithSongCount -> {
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
+ CollectionType(CollectionType.ComposerType, personWithSongCount.name)
+ )
+ )
+ }
+ is LyricistWithSongCount -> {
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
+ CollectionType(CollectionType.LyricistType, personWithSongCount.name)
+ )
+ )
+ }
+ }
+ }
+
+ fun navigateToViewDetails(playlistId: Long) {
+ if (!check()) return
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
+ CollectionType(CollectionType.PlaylistType, playlistId.toString())
+ )
+ )
+ }
+
+ fun navigateToViewDetails(genreWithSongCount: GenreWithSongCount) {
+ if (!check()) return
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
+ CollectionType(CollectionType.GenreType, genreWithSongCount.genreName)
+ )
+ )
+ }
+
+ fun navigateToViewDetails() {
+ if (!check()) return
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToCollectionFragment(
+ CollectionType(CollectionType.FavouritesType)
+ )
+ )
+ }
+
+ private fun navigateToChoosePlaylists(locations: List) {
+ if (!check()) return
+ navController.navigate(
+ HomeFragmentDirections.actionHomeFragmentToSelectPlaylistFragment(
+ locations.toTypedArray()
+ )
+ )
+ }
+
+ fun navigateToChoosePlaylist(song: Song) {
+ if (!check()) return
+ navigateToChoosePlaylists(listOf(song.location))
+ }
+
+ fun navigateToChoosePlaylist(songs: List) {
+ if (!check()) return
+ lifecycle.coroutineScope.launch {
+ navigateToChoosePlaylists(songs.map { it.location })
+ }
+ }
+
+ fun navigateToChoosePlaylist(song: MiniSong) {
+ if (!check()) return
+ navigateToChoosePlaylists(listOf(song.location))
+ }
+}
diff --git a/app/src/main/java/com/github/pakka_papad/home/HomeViewModel.kt b/app/src/main/java/com/github/pakka_papad/home/HomeViewModel.kt
index 18cf0e5..530a4f4 100644
--- a/app/src/main/java/com/github/pakka_papad/home/HomeViewModel.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/HomeViewModel.kt
@@ -1,17 +1,22 @@
package com.github.pakka_papad.home
-import android.app.Application
-import android.widget.Toast
+import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
+import com.github.pakka_papad.Constants
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.SortOptions
-import com.github.pakka_papad.data.DataManager
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.data.music.*
+import com.github.pakka_papad.data.services.BlacklistService
+import com.github.pakka_papad.data.services.PlayerService
+import com.github.pakka_papad.data.services.PlaylistService
+import com.github.pakka_papad.data.services.QueueService
+import com.github.pakka_papad.data.services.SongService
import com.github.pakka_papad.storage_explorer.*
-import com.github.pakka_papad.storage_explorer.Directory
+import com.github.pakka_papad.util.MessageStore
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@@ -20,14 +25,18 @@ import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
- private val context: Application,
- private val manager: DataManager,
+ private val messageStore: MessageStore,
private val exoPlayer: ExoPlayer,
private val songExtractor: SongExtractor,
private val prefs: ZenPreferenceProvider,
+ private val playlistService: PlaylistService,
+ private val blacklistService: BlacklistService,
+ private val songService: SongService,
+ private val queueService: QueueService,
+ private val playerService: PlayerService,
) : ViewModel() {
- val songs = manager.getAll.songs()
+ val songs = songService.songs
.combine(prefs.songSortOrder){ songs, sortOrder ->
when(sortOrder){
SortOptions.TitleASC.ordinal -> songs.sortedBy { it.title }
@@ -50,7 +59,7 @@ class HomeViewModel @Inject constructor(
initialValue = null
)
- val albums = manager.getAll.albums()
+ val albums = songService.albums
.combine(prefs.albumSortOrder){ albums, sortOrder ->
when(sortOrder){
SortOptions.TitleASC.ordinal -> albums.sortedBy { it.name }
@@ -76,10 +85,10 @@ class HomeViewModel @Inject constructor(
val personsWithSongCount = _selectedPerson
.flatMapLatest {
when (it) {
- Person.Artist -> manager.getAll.artists()
- Person.AlbumArtist -> manager.getAll.albumArtists()
- Person.Composer -> manager.getAll.composers()
- Person.Lyricist -> manager.getAll.lyricists()
+ Person.Artist -> songService.artists
+ Person.AlbumArtist -> songService.albumArtists
+ Person.Composer -> songService.composers
+ Person.Lyricist -> songService.lyricists
}
}.combine(prefs.artistSortOrder){ artists, sortOrder ->
when(sortOrder){
@@ -97,7 +106,7 @@ class HomeViewModel @Inject constructor(
initialValue = null
)
- val playlistsWithSongCount = manager.getAll.playlists()
+ val playlistsWithSongCount = playlistService.playlists
.combine(prefs.playlistSortOrder){ playlists, sortOrder ->
when(sortOrder){
SortOptions.NameASC.ordinal -> playlists.sortedBy { it.playlistName }
@@ -114,7 +123,7 @@ class HomeViewModel @Inject constructor(
initialValue = emptyList()
)
- val genresWithSongCount = manager.getAll.genres()
+ val genresWithSongCount = songService.genres
.combine(prefs.genreSortOrder){ genres, sortOrder ->
when(sortOrder){
SortOptions.NameASC.ordinal -> genres.sortedBy { it.genreName }
@@ -135,19 +144,50 @@ class HomeViewModel @Inject constructor(
prefs.updateSortOrder(screen, option)
}
- val currentSong = manager.currentSong
+ val currentSong = queueService.currentSong
- val queue = manager.queue
+ val queue = mutableStateListOf()
- val repeatMode = manager.repeatMode
+ val repeatMode = queueService.repeatMode
fun toggleRepeatMode(){
- manager.updateRepeatMode(repeatMode.value.next())
+ queueService.updateRepeatMode(repeatMode.value.next())
}
private val _currentSongPlaying = MutableStateFlow(null)
val currentSongPlaying = _currentSongPlaying.asStateFlow()
+ private val queueServiceListener = object : QueueService.Listener {
+ override fun onAppend(song: Song) {
+ queue.add(song)
+ }
+
+ override fun onAppend(songs: List) {
+ queue.addAll(songs)
+ }
+
+ override fun onUpdate(updatedSong: Song, position: Int) {
+ if (position < 0 || position >= queue.size) return
+ queue[position] = updatedSong
+ }
+
+ override fun onMove(from: Int, to: Int) {
+ if (from < 0 || to < 0 || from >= queue.size || to >= queue.size) return
+ queue.apply { add(to, removeAt(from)) }
+ }
+
+ override fun onClear() {
+ queue.clear()
+ }
+
+ override fun onSetQueue(songs: List, startPlayingFromPosition: Int) {
+ queue.apply {
+ clear()
+ addAll(songs)
+ }
+ }
+ }
+
private val exoPlayerListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
@@ -158,16 +198,26 @@ class HomeViewModel @Inject constructor(
init {
_currentSongPlaying.update { exoPlayer.isPlaying }
exoPlayer.addListener(exoPlayerListener)
+ queue.addAll(queueService.queue)
+ queueService.addListener(queueServiceListener)
}
- private fun showToast(message: String) {
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
+ private val _message = MutableStateFlow("")
+ val message = _message.asStateFlow()
+
+ private fun showMessage(message: String){
+ viewModelScope.launch {
+ _message.update { message }
+ delay(Constants.MESSAGE_DURATION)
+ _message.update { "" }
+ }
}
override fun onCleared() {
super.onCleared()
exoPlayer.removeListener(exoPlayerListener)
explorer.removeListener(directoryChangeListener)
+ queueService.removeListener(queueServiceListener)
}
/**
@@ -178,11 +228,11 @@ class HomeViewModel @Inject constructor(
fun onSongBlacklist(song: Song) {
viewModelScope.launch {
try {
- manager.deleteSong(song)
- showToast("Done")
+ blacklistService.blacklistSongs(listOf(song))
+ showMessage(messageStore.getString(R.string.done))
} catch (e: Exception) {
Timber.e(e)
- showToast("Some error occurred")
+ showMessage(messageStore.getString(R.string.some_error_occurred))
}
}
}
@@ -190,33 +240,28 @@ class HomeViewModel @Inject constructor(
fun onFolderBlacklist(folder: Directory){
viewModelScope.launch {
try {
- manager.addFolderToBlacklist(folder.absolutePath)
- showToast("Done")
+ blacklistService.blacklistFolders(listOf(folder.absolutePath))
+ showMessage(messageStore.getString(R.string.done))
} catch (_: Exception){
- showToast("Some error occurred")
+ showMessage(messageStore.getString(R.string.some_error_occurred))
}
}
}
fun onPlaylistCreate(playlistName: String) {
viewModelScope.launch {
- manager.createPlaylist(playlistName)
+ playlistService.createPlaylist(playlistName)
}
}
fun deletePlaylist(playlistWithSongCount: PlaylistWithSongCount) {
viewModelScope.launch {
try {
- val playlist = Playlist(
- playlistId = playlistWithSongCount.playlistId,
- playlistName = playlistWithSongCount.playlistName,
- createdAt = playlistWithSongCount.createdAt
- )
- manager.deletePlaylist(playlist)
- showToast("Done")
+ playlistService.deletePlaylist(playlistWithSongCount.playlistId)
+ showMessage(messageStore.getString(R.string.done))
} catch (e: Exception) {
Timber.e(e)
- showToast("Some error occurred")
+ showMessage(messageStore.getString(R.string.some_error_occurred))
}
}
}
@@ -226,30 +271,15 @@ class HomeViewModel @Inject constructor(
*/
fun addToQueue(song: Song) {
if (queue.isEmpty()) {
- manager.setQueue(listOf(song), 0)
+// manager.setQueue(listOf(song), 0)
+ queueService.setQueue(listOf(song),0)
+ playerService.startServiceIfNotRunning(listOf(song), 0)
} else {
- val result = manager.addToQueue(song)
+ val result = queueService.append(song)
if (result) {
- showToast("Added ${song.title} to queue")
+ showMessage(messageStore.getString(R.string.added_to_queue, song.title))
} else {
- showToast("Song already in queue")
- }
- }
- }
-
- /**
- * Adds a list of songs to the end queue
- */
- fun addToQueue(songs: List) {
- if (queue.isEmpty()) {
- manager.setQueue(songs, 0)
- } else {
- var result = false
- songs.forEach { result = result or manager.addToQueue(it) }
- if (result) {
- showToast("Done")
- } else {
- showToast("Songs already in queue")
+ showMessage(messageStore.getString(R.string.song_already_in_queue))
}
}
}
@@ -268,8 +298,10 @@ class HomeViewModel @Inject constructor(
*/
fun setQueue(songs: List?, startPlayingFromIndex: Int = 0) {
if (songs == null) return
- manager.setQueue(songs, startPlayingFromIndex)
- showToast("Playing")
+// manager.setQueue(songs, startPlayingFromIndex)
+ queueService.setQueue(songs, startPlayingFromIndex)
+ playerService.startServiceIfNotRunning(songs, startPlayingFromIndex)
+ showMessage(messageStore.getString(R.string.playing))
}
/**
@@ -279,11 +311,13 @@ class HomeViewModel @Inject constructor(
if (song == null) return
val updatedSong = song.copy(favourite = !song.favourite)
viewModelScope.launch(Dispatchers.IO) {
- manager.updateSong(updatedSong)
+// manager.updateSong(updatedSong)
+ queueService.update(updatedSong)
+ songService.updateSong(updatedSong)
}
}
- fun onSongDrag(fromIndex: Int, toIndex: Int) = manager.moveItem(fromIndex,toIndex)
+ fun onSongDrag(fromIndex: Int, toIndex: Int) = queueService.moveSong(fromIndex, toIndex)
private val _filesInCurrentDestination = MutableStateFlow(DirectoryContents())
@@ -322,13 +356,13 @@ class HomeViewModel @Inject constructor(
}
init {
- viewModelScope.launch {
+ viewModelScope.launch(Dispatchers.IO) {
explorer.addListener(directoryChangeListener)
}
}
fun onFileClicked(songIndex: Int){
- viewModelScope.launch {
+ viewModelScope.launch(Dispatchers.IO) {
if(songIndex < 0 || songIndex >= filesInCurrentDestination.value.songs.size) return@launch
val song = songExtractor.resolveSong(filesInCurrentDestination.value.songs[songIndex].location)
song?.let {
@@ -338,14 +372,14 @@ class HomeViewModel @Inject constructor(
}
fun onFileClicked(file: Directory){
- viewModelScope.launch(Dispatchers.Default) {
+ viewModelScope.launch(Dispatchers.IO) {
explorer.moveInsideDirectory(file.absolutePath)
_isExplorerAtRoot.update { explorer.isRoot }
}
}
fun moveToParent() {
- viewModelScope.launch(Dispatchers.Default) {
+ viewModelScope.launch(Dispatchers.IO) {
explorer.moveToParent()
_isExplorerAtRoot.update { explorer.isRoot }
}
diff --git a/app/src/main/java/com/github/pakka_papad/home/MiniPlayer.kt b/app/src/main/java/com/github/pakka_papad/home/MiniPlayer.kt
index 6680aea..39cb667 100644
--- a/app/src/main/java/com/github/pakka_papad/home/MiniPlayer.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/MiniPlayer.kt
@@ -17,6 +17,7 @@ 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -38,7 +39,7 @@ fun MiniPlayer(
) {
AsyncImage(
model = song.artUri,
- contentDescription = null,
+ contentDescription = stringResource(R.string.song_image),
modifier = Modifier
.size(56.dp)
.padding(8.dp)
@@ -60,7 +61,9 @@ fun MiniPlayer(
painter = painterResource(
if (showPlayButton) R.drawable.ic_baseline_play_arrow_40 else R.drawable.ic_baseline_pause_40
),
- contentDescription = null,
+ contentDescription = stringResource(
+ if (showPlayButton) R.string.play_button else R.string.pause_button
+ ),
modifier = Modifier
.size(56.dp)
.padding(6.dp)
diff --git a/app/src/main/java/com/github/pakka_papad/home/Persons.kt b/app/src/main/java/com/github/pakka_papad/home/Persons.kt
index 850b2e9..bb24fa5 100644
--- a/app/src/main/java/com/github/pakka_papad/home/Persons.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/Persons.kt
@@ -14,9 +14,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.FullScreenSadMessage
import com.github.pakka_papad.components.more_options.OptionsAlertDialog
import com.github.pakka_papad.components.more_options.PersonOptions
@@ -33,7 +36,7 @@ fun Persons(
if (personsWithSongCount == null) return
if (personsWithSongCount.isEmpty()) {
FullScreenSadMessage(
- message = "No artists found",
+ message = stringResource(R.string.no_artists_found),
paddingValues = WindowInsets.systemBars.only(WindowInsetsSides.Bottom)
.asPaddingValues(),
)
@@ -93,7 +96,11 @@ fun PersonCard(
fontWeight = FontWeight.Bold,
)
Text(
- text = "${personWithSongCount.count} ${if (personWithSongCount.count == 1) "song" else "songs"}",
+ text = pluralStringResource(
+ id = R.plurals.song_count,
+ count = personWithSongCount.count,
+ personWithSongCount.count
+ ),
maxLines = 1,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.fillMaxWidth(),
@@ -104,7 +111,7 @@ fun PersonCard(
var optionsVisible by remember { mutableStateOf(false) }
Icon(
imageVector = Icons.Outlined.MoreVert,
- contentDescription = null,
+ contentDescription = stringResource(R.string.more_menu_button),
modifier = Modifier
.size(26.dp)
.clickable(
diff --git a/app/src/main/java/com/github/pakka_papad/home/PlayShuffleCard.kt b/app/src/main/java/com/github/pakka_papad/home/PlayShuffleCard.kt
index dfba878..03af70b 100644
--- a/app/src/main/java/com/github/pakka_papad/home/PlayShuffleCard.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/PlayShuffleCard.kt
@@ -8,6 +8,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.github.pakka_papad.R
@@ -37,12 +38,12 @@ fun PlayShuffleCard(
Icon(
painter = painterResource(R.drawable.ic_baseline_play_arrow_40),
modifier = iconModifier,
- contentDescription = "play-all-button"
+ contentDescription = stringResource(R.string.play_all_button)
)
if (configuration.screenWidthDp > 340) {
Spacer(spacerModifier)
Text(
- text = "Play All",
+ text = stringResource(R.string.play_all),
style = MaterialTheme.typography.titleMedium,
)
}
@@ -57,12 +58,12 @@ fun PlayShuffleCard(
Icon(
painter = painterResource(R.drawable.ic_baseline_shuffle_40),
modifier = iconModifier,
- contentDescription = "shuffle-button",
+ contentDescription = stringResource(R.string.shuffle_button),
)
if (configuration.screenWidthDp > 340) {
Spacer(spacerModifier)
Text(
- text = "Shuffle",
+ text = stringResource(R.string.shuffle),
style = MaterialTheme.typography.titleMedium,
)
}
diff --git a/app/src/main/java/com/github/pakka_papad/home/Playlists.kt b/app/src/main/java/com/github/pakka_papad/home/Playlists.kt
index e22d996..44aa3f6 100644
--- a/app/src/main/java/com/github/pakka_papad/home/Playlists.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/Playlists.kt
@@ -1,50 +1,91 @@
package com.github.pakka_papad.home
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.github.pakka_papad.R
-import com.github.pakka_papad.components.PlaylistCard
+import com.github.pakka_papad.components.PlaylistCardV2
import com.github.pakka_papad.components.more_options.PlaylistOptions
+import com.github.pakka_papad.data.UserPreferences
import com.github.pakka_papad.data.music.PlaylistWithSongCount
+import com.github.pakka_papad.ui.theme.LocalThemePreference
+import com.github.pakka_papad.ui.theme.harmonize
+import scheme.Scheme
@Composable
fun Playlists(
playlistsWithSongCount: List?,
onPlaylistClicked: (Long) -> Unit,
- listState: LazyListState,
+ listState: LazyGridState,
onPlaylistCreate: (String) -> Unit,
onFavouritesClicked: () -> Unit,
onDeletePlaylistClicked: (PlaylistWithSongCount) -> Unit,
) {
if (playlistsWithSongCount == null) return
- LazyColumn(
+ LazyVerticalGrid(
modifier = Modifier
.fillMaxSize(),
state = listState,
+ columns = GridCells.Adaptive(150.dp),
contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
) {
item {
CreatePlaylistCard(
onPlaylistCreate = onPlaylistCreate,
- onFavouritesClicked = onFavouritesClicked
+ )
+ }
+ item {
+ FavouritesCard(
+ onFavouritesClicked = onFavouritesClicked,
)
}
items(
items = playlistsWithSongCount,
key = { it.playlistId }
) { playlist ->
- PlaylistCard(
+ PlaylistCardV2(
playlistWithSongCount = playlist,
onPlaylistClicked = onPlaylistClicked,
options = listOf(
@@ -57,69 +98,96 @@ fun Playlists(
}
}
-@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun FavouritesCard(
+ onFavouritesClicked: () -> Unit
+) {
+ val themePreference = LocalThemePreference.current
+ val isSystemDark = isSystemInDarkTheme()
+ val scheme by remember(themePreference) { derivedStateOf {
+ val isDark = when(themePreference.theme){
+ UserPreferences.Theme.LIGHT_MODE, UserPreferences.Theme.UNRECOGNIZED -> false
+ UserPreferences.Theme.DARK_MODE -> true
+ UserPreferences.Theme.USE_SYSTEM_MODE -> isSystemDark
+ }
+ if (isDark) Scheme.dark(Color(0xFFE90064).toArgb())
+ else Scheme.light(Color(0xFFE90064).toArgb())
+ } }
+ Column(
+ modifier = Modifier
+ .widthIn(max = 200.dp)
+ .clickable(onClick = onFavouritesClicked)
+ .padding(10.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ modifier = Modifier
+ .aspectRatio(ratio = 1f, matchHeightConstraintsFirst = false)
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .background(
+ Brush.linearGradient(
+ colors = listOf(
+ harmonize(Color(scheme.primary)),
+ harmonize(Color(scheme.primaryContainer))
+ )
+ )
+ )
+ .padding(45.dp),
+ imageVector = Icons.Outlined.FavoriteBorder,
+ contentDescription = stringResource(R.string.favourite_button),
+ tint = Color(scheme.onPrimary)
+ )
+ Text(
+ text = stringResource(R.string.favourites),
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ modifier = Modifier.fillMaxWidth(),
+ fontWeight = FontWeight.Bold,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+}
+
@Composable
fun CreatePlaylistCard(
onPlaylistCreate: (String) -> Unit,
- onFavouritesClicked: () -> Unit,
) {
var isDialogVisible by remember { mutableStateOf(false) }
var playlistName by remember { mutableStateOf("") }
- val spacerModifier = Modifier.width(8.dp)
- val iconModifier = Modifier.size(24.dp)
- val configuration = LocalConfiguration.current
- Row(
+ Column(
modifier = Modifier
- .fillMaxWidth()
- .height(70.dp)
- .padding(horizontal = 10.dp),
- horizontalArrangement = Arrangement.spacedBy(10.dp),
- verticalAlignment = Alignment.CenterVertically
+ .widthIn(max = 200.dp)
+ .clickable(onClick = { isDialogVisible = true })
+ .padding(10.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
) {
- Button(
- onClick = onFavouritesClicked,
- modifier = Modifier
- .weight(1f),
- shape = MaterialTheme.shapes.large
- ) {
- Icon(
- painter = painterResource(R.drawable.ic_baseline_favorite_24),
- modifier = iconModifier,
- contentDescription = "create-new-playlist"
- )
- if (configuration.screenWidthDp > 340){
- Spacer(spacerModifier)
- Text(
- text = "Favourites",
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onPrimary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
- Button(
- onClick = { isDialogVisible = true },
+ Icon(
+ painter = painterResource(R.drawable.ic_baseline_playlist_add_40),
modifier = Modifier
- .weight(1f),
- shape = MaterialTheme.shapes.large
- ) {
- Icon(
- painter = painterResource(R.drawable.ic_baseline_playlist_add_40),
- modifier = iconModifier,
- contentDescription = "create-new-playlist"
- )
- if (configuration.screenWidthDp > 340) {
- Spacer(spacerModifier)
- Text(
- text = "New Playlist",
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onPrimary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
+ .aspectRatio(ratio = 1f, matchHeightConstraintsFirst = false)
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .background(
+ Brush.linearGradient(
+ colors = listOf(
+ MaterialTheme.colorScheme.primary,
+ MaterialTheme.colorScheme.primaryContainer
+ )
+ )
)
- }
- }
+ .padding(45.dp),
+ contentDescription = stringResource(R.string.create_playlist_button),
+ tint = MaterialTheme.colorScheme.onPrimary,
+ )
+ Text(
+ text = stringResource(R.string.new_playlist),
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ modifier = Modifier.fillMaxWidth(),
+ fontWeight = FontWeight.Bold,
+ overflow = TextOverflow.Ellipsis
+ )
}
AnimatedVisibility (isDialogVisible) {
AlertDialog(
@@ -133,7 +201,7 @@ fun CreatePlaylistCard(
}
) {
Text(
- text = "Create",
+ text = stringResource(R.string.create),
style = MaterialTheme.typography.bodyLarge
)
}
@@ -146,14 +214,14 @@ fun CreatePlaylistCard(
}
) {
Text(
- text = "Cancel",
+ text = stringResource(R.string.cancel),
style = MaterialTheme.typography.bodyLarge
)
}
},
title = {
Text(
- text = "Create playlist"
+ text = stringResource(R.string.create_playlist)
)
},
text = {
@@ -163,7 +231,7 @@ fun CreatePlaylistCard(
playlistName = it
},
label = {
- Text(text = "Playlist name")
+ Text(text = stringResource(R.string.playlist_name))
},
textStyle = MaterialTheme.typography.bodyLarge,
singleLine = true,
diff --git a/app/src/main/java/com/github/pakka_papad/nowplaying/MusicSlider.kt b/app/src/main/java/com/github/pakka_papad/nowplaying/MusicSlider.kt
index c924e21..9024956 100644
--- a/app/src/main/java/com/github/pakka_papad/nowplaying/MusicSlider.kt
+++ b/app/src/main/java/com/github/pakka_papad/nowplaying/MusicSlider.kt
@@ -8,50 +8,55 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
-import androidx.media3.exoplayer.ExoPlayer
-import kotlinx.coroutines.delay
-import com.github.pakka_papad.toMS
import com.github.pakka_papad.R
+import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.toMS
+import kotlinx.coroutines.delay
@Composable
fun MusicSlider(
modifier: Modifier,
- mediaPlayer: ExoPlayer,
+ playerHelper: PlayerHelper,
+ currentSongPlaying: Boolean?,
+ song: Song, // to update slider when song is changed in paused state
duration: Long,
) {
- var currentValue by remember { mutableStateOf(mediaPlayer.currentPosition) }
- var isPlaying by remember { mutableStateOf(mediaPlayer.isPlaying) }
+ var currentValue by remember { mutableStateOf(playerHelper.currentPosition.toLong()) }
DisposableEffect(Unit) {
val listener = object : Player.Listener {
- override fun onIsPlayingChanged(isPlaying_: Boolean) {
- isPlaying = isPlaying_
- }
-
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
currentValue = 0L
}
}
- mediaPlayer.addListener(listener)
+ playerHelper.addListener(listener)
onDispose {
- mediaPlayer.removeListener(listener)
+ playerHelper.removeListener(listener)
}
}
- if (isPlaying) {
+ if (currentSongPlaying == true) {
LaunchedEffect(Unit) {
while (true) {
- currentValue = mediaPlayer.currentPosition
+ currentValue = playerHelper.currentPosition.toLong()
delay(33)
}
}
+ } else {
+ currentValue = playerHelper.currentPosition.toLong()
}
val primaryColor = MaterialTheme.colorScheme.primary
Column(
@@ -78,7 +83,7 @@ fun MusicSlider(
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
- mediaPlayer.seekTo(currentValue)
+ playerHelper.seekTo(currentValue)
}
}
)
diff --git a/app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingScreen.kt b/app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingScreen.kt
index 861b523..bce7845 100644
--- a/app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingScreen.kt
+++ b/app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingScreen.kt
@@ -9,15 +9,47 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+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.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.ripple.rememberRipple
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -27,40 +59,47 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import androidx.media3.exoplayer.ExoPlayer
import coil.compose.AsyncImage
import com.github.pakka_papad.R
import com.github.pakka_papad.data.UserPreferences.PlaybackParams
-import com.github.pakka_papad.nowplaying.RepeatMode as RepeatModeEnum
import com.github.pakka_papad.data.music.Song
import com.github.pakka_papad.round
import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.min
+import com.github.pakka_papad.nowplaying.RepeatMode as RepeatModeEnum
@Composable
fun NowPlayingScreen(
paddingValues: PaddingValues,
song: Song?,
+ currentSongPlaying: Boolean?,
onPausePlayPressed: () -> Unit,
onPreviousPressed: () -> Unit,
onNextPressed: () -> Unit,
songPlaying: Boolean?,
- exoPlayer: ExoPlayer,
+ playerHelper: PlayerHelper,
onFavouriteClicked: () -> Unit,
onQueueClicked: () -> Unit,
repeatMode: RepeatModeEnum,
toggleRepeatMode: () -> Unit,
playbackParams: PlaybackParams,
updatePlaybackParams: (speed: Int, pitch: Int) -> Unit,
+ isTimerRunning: Boolean,
+ timeLeft: Int,
+ onTimerBegin: (Int) -> Unit,
+ onTimerCancel: () -> Unit,
+ onSaveQueueClicked: () -> Unit,
) {
if (song == null || songPlaying == null) return
val configuration = LocalConfiguration.current
- val screenHeight = max(configuration.screenHeightDp - 60, 0) // subtracting 60 for TopBarHeight
+ val screenHeight = max(configuration.screenHeightDp - 20, 0) // subtracting 20 for Scrim height
val screenWidth = configuration.screenWidthDp
if (configuration.orientation == ORIENTATION_LANDSCAPE) {
Row(
@@ -72,13 +111,13 @@ fun NowPlayingScreen(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
- val albumArtMaxWidth = ((0.4f) * screenWidth).toInt()
- val infoAndControlsMaxWidth = ((0.6f) * screenWidth).toInt()
+ val albumArtMaxWidth = ((0.5f) * screenWidth).toInt()
+ val infoAndControlsMaxWidth = ((0.5f) * screenWidth).toInt()
if (albumArtMaxWidth >= 50 && screenHeight >= 50) {
val imageSize = min(albumArtMaxWidth, screenHeight)
AlbumArt(
song = song,
- modifier = Modifier.size((imageSize * 0.8f).dp),
+ modifier = Modifier.size((imageSize * 0.9f).dp),
)
}
InfoAndControls(
@@ -87,7 +126,8 @@ fun NowPlayingScreen(
onPreviousPressed = onPreviousPressed,
onNextPressed = onNextPressed,
showPlayButton = !songPlaying,
- exoPlayer = exoPlayer,
+ playerHelper = playerHelper,
+ currentSongPlaying = currentSongPlaying,
onFavouriteClicked = onFavouriteClicked,
onQueueClicked = onQueueClicked,
modifier = Modifier
@@ -97,6 +137,11 @@ fun NowPlayingScreen(
toggleRepeatMode = toggleRepeatMode,
playbackParams = playbackParams,
updatePlaybackParams = updatePlaybackParams,
+ isTimerRunning = isTimerRunning,
+ timeLeft = timeLeft,
+ onTimerBegin = onTimerBegin,
+ onTimerCancel = onTimerCancel,
+ onSaveQueueClicked = onSaveQueueClicked,
)
}
} else {
@@ -109,14 +154,14 @@ fun NowPlayingScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
- val albumArtMaxHeight = ((0.6f) * screenHeight).toInt()
- val infoAndControlsMaxHeight = ((0.4f) * screenHeight).toInt()
+ val albumArtMaxHeight = ((0.5f) * screenHeight).toInt()
+ val infoAndControlsMaxHeight = ((0.5f) * screenHeight).toInt()
if (screenWidth >= 50 && albumArtMaxHeight >= 50) {
val imageSize = min(screenWidth, albumArtMaxHeight)
AlbumArt(
song = song,
modifier = Modifier
- .size((imageSize * 0.8f).dp)
+ .size((imageSize * 0.9f).dp)
.weight(1f),
)
}
@@ -126,7 +171,8 @@ fun NowPlayingScreen(
onPreviousPressed = onPreviousPressed,
onNextPressed = onNextPressed,
showPlayButton = !songPlaying,
- exoPlayer = exoPlayer,
+ playerHelper = playerHelper,
+ currentSongPlaying = currentSongPlaying,
onFavouriteClicked = onFavouriteClicked,
onQueueClicked = onQueueClicked,
modifier = Modifier
@@ -136,6 +182,11 @@ fun NowPlayingScreen(
toggleRepeatMode = toggleRepeatMode,
playbackParams = playbackParams,
updatePlaybackParams = updatePlaybackParams,
+ isTimerRunning = isTimerRunning,
+ timeLeft = timeLeft,
+ onTimerBegin = onTimerBegin,
+ onTimerCancel = onTimerCancel,
+ onSaveQueueClicked = onSaveQueueClicked,
)
}
}
@@ -148,7 +199,7 @@ private fun AlbumArt(
) {
AsyncImage(
model = song.artUri,
- contentDescription = null,
+ contentDescription = stringResource(R.string.song_image),
modifier = modifier
.aspectRatio(1f)
.shadow(
@@ -164,11 +215,12 @@ private fun AlbumArt(
@Composable
private fun InfoAndControls(
song: Song,
+ currentSongPlaying: Boolean?,
onPausePlayPressed: () -> Unit,
onPreviousPressed: () -> Unit,
onNextPressed: () -> Unit,
showPlayButton: Boolean,
- exoPlayer: ExoPlayer,
+ playerHelper: PlayerHelper,
onFavouriteClicked: () -> Unit,
onQueueClicked: () -> Unit,
modifier: Modifier = Modifier,
@@ -176,6 +228,11 @@ private fun InfoAndControls(
toggleRepeatMode: () -> Unit,
playbackParams: PlaybackParams,
updatePlaybackParams: (speed: Int, pitch: Int) -> Unit,
+ isTimerRunning: Boolean,
+ timeLeft: Int,
+ onTimerBegin: (Int) -> Unit,
+ onTimerCancel: () -> Unit,
+ onSaveQueueClicked: () -> Unit,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -188,16 +245,23 @@ private fun InfoAndControls(
modifier = Modifier.weight(1f)
)
Row(
- horizontalArrangement = Arrangement.SpaceBetween,
+ horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 24.dp)
+ .padding(horizontal = 24.dp, vertical = 12.dp)
) {
PlaybackSpeedAndPitchController(
playbackParams = playbackParams,
updatePlaybackParams = updatePlaybackParams
)
+ SleepTimerButton(
+ isRunning = isTimerRunning,
+ timeLeft = timeLeft,
+ beginTimer = onTimerBegin,
+ cancelTimer = onTimerCancel,
+ )
+ SaveQueue(onSaveQueueClicked = onSaveQueueClicked)
RepeatModeController(
currentRepeatMode = repeatMode,
toggleRepeatMode = toggleRepeatMode
@@ -205,15 +269,17 @@ private fun InfoAndControls(
}
MusicSlider(
modifier = Modifier
- .weight(0.7f)
- .padding(vertical = 0.dp, horizontal = 24.dp),
- mediaPlayer = exoPlayer,
+ .padding(24.dp),
+ playerHelper = playerHelper,
+ currentSongPlaying = currentSongPlaying,
duration = song.durationMillis,
+ song = song,
)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.widthIn(max = 370.dp)
+ modifier = Modifier
+ .fillMaxWidth()
) {
LikeButton(
song = song,
@@ -252,7 +318,7 @@ private fun LikeButton(
val favouriteButtonScale = remember { Animatable(1f) }
Image(
imageVector = if (song.favourite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
- contentDescription = "like button",
+ contentDescription = stringResource(R.string.favourite_button),
modifier = modifier
.size(50.dp)
.scale(favouriteButtonScale.value)
@@ -296,7 +362,7 @@ private fun PreviousButton(
modifier: Modifier = Modifier,
) = Image(
painter = painterResource(R.drawable.ic_baseline_skip_previous_40),
- contentDescription = "previous button",
+ contentDescription = stringResource(R.string.previous_button),
modifier = modifier
.size(70.dp)
.clip(RoundedCornerShape(35.dp))
@@ -320,7 +386,9 @@ private fun PausePlayButton(
painter = painterResource(
if (showPlayButton) R.drawable.ic_baseline_play_arrow_40 else R.drawable.ic_baseline_pause_40
),
- contentDescription = "play/pause button",
+ contentDescription = stringResource(
+ if (showPlayButton) R.string.play_button else R.string.pause_button
+ ),
modifier = modifier
.size(70.dp)
.clip(CircleShape)
@@ -344,7 +412,7 @@ private fun NextButton(
modifier: Modifier = Modifier,
) = Image(
painter = painterResource(R.drawable.ic_baseline_skip_next_40),
- contentDescription = "next button",
+ contentDescription = stringResource(R.string.next_button),
modifier = modifier
.size(70.dp)
.clip(RoundedCornerShape(35.dp))
@@ -365,7 +433,7 @@ private fun QueueButton(
modifier: Modifier = Modifier,
) = Image(
painter = painterResource(R.drawable.ic_baseline_queue_music_40),
- contentDescription = "queue button",
+ contentDescription = stringResource(R.string.queue_button),
modifier = modifier
.size(50.dp)
.clickable(
@@ -391,6 +459,7 @@ private fun SongInfo(
modifier = modifier,
verticalArrangement = Arrangement.Center
) {
+ val spacerModifier = Modifier.height(8.dp)
Text(
text = song.title,
style = MaterialTheme.typography.headlineSmall,
@@ -400,6 +469,7 @@ private fun SongInfo(
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface,
)
+ Spacer(spacerModifier)
Text(
text = song.artist,
style = MaterialTheme.typography.titleMedium,
@@ -408,6 +478,7 @@ private fun SongInfo(
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface,
)
+ Spacer(spacerModifier)
Text(
text = song.album,
style = MaterialTheme.typography.titleMedium,
@@ -428,8 +499,14 @@ fun PlaybackSpeedAndPitchController(
var showDialog by remember { mutableStateOf(false) }
Icon(
painter = painterResource(R.drawable.baseline_speed_24),
- contentDescription = "speed and pitch controller",
- modifier = Modifier.size(30.dp).clickable { showDialog = true },
+ contentDescription = stringResource(R.string.speed_and_pitch_controller),
+ modifier = Modifier
+ .size(30.dp)
+ .clickable(
+ onClick = { showDialog = true },
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false, radius = 20.dp)
+ ),
tint = MaterialTheme.colorScheme.onSurface
)
if (showDialog) {
@@ -447,7 +524,7 @@ fun PlaybackSpeedAndPitchController(
)
},
content = {
- Text(text = "Ok")
+ Text(text = stringResource(R.string.save))
}
)
},
@@ -455,7 +532,7 @@ fun PlaybackSpeedAndPitchController(
OutlinedButton(
onClick = { showDialog = false },
content = {
- Text(text = "Cancel")
+ Text(text = stringResource(R.string.cancel))
}
)
},
@@ -466,14 +543,14 @@ fun PlaybackSpeedAndPitchController(
modifier = Modifier.fillMaxWidth()
){
Text(
- text = "Speed: ${newSpeed}x",
+ text = stringResource(R.string.speed_x, newSpeed),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.alignByBaseline()
)
TextButton(
onClick = { newSpeed = 1f },
content = {
- Text(text = "Reset")
+ Text(text = stringResource(R.string.reset))
},
modifier = Modifier.alignByBaseline()
)
@@ -489,14 +566,14 @@ fun PlaybackSpeedAndPitchController(
modifier = Modifier.fillMaxWidth(),
){
Text(
- text = "Pitch: ${newPitch}x",
+ text = stringResource(R.string.pitch_x, newPitch),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.alignByBaseline()
)
TextButton(
onClick = { newPitch = 1f },
content = {
- Text(text = "Reset")
+ Text(text = stringResource(R.string.reset))
},
modifier = Modifier.alignByBaseline()
)
@@ -520,12 +597,166 @@ fun RepeatModeController(
) {
Icon(
painter = painterResource(currentRepeatMode.iconResource),
- contentDescription = "repeat mode",
+ contentDescription = stringResource(R.string.repeat_mode_button),
+ modifier = Modifier
+ .size(30.dp)
+ .clickable(
+ onClick = toggleRepeatMode,
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false, radius = 20.dp),
+ ),
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+}
+
+@Composable
+private fun SaveQueue(
+ onSaveQueueClicked: () -> Unit,
+){
+ Icon(
+ painter = painterResource(R.drawable.ic_baseline_playlist_add_40),
+ contentDescription = stringResource(R.string.repeat_mode_button),
modifier = Modifier
.size(30.dp)
.clickable(
- onClick = toggleRepeatMode
+ onClick = onSaveQueueClicked,
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false, radius = 20.dp),
),
tint = MaterialTheme.colorScheme.onSurface
)
+}
+
+@Composable
+private fun SleepTimerButton(
+ isRunning: Boolean,
+ timeLeft: Int,
+ beginTimer: (Int) -> Unit,
+ cancelTimer: () -> Unit,
+){
+ var showTimerDialog by remember { mutableStateOf(false) }
+ Icon(
+ painter = painterResource(R.drawable.outline_timer_24),
+ contentDescription = stringResource(R.string.sleep_timer_button),
+ modifier = Modifier
+ .size(30.dp)
+ .clickable(
+ onClick = { showTimerDialog = true },
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false, radius = 20.dp),
+ ),
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ if (showTimerDialog) {
+ var minutes by remember { mutableStateOf(null) }
+ var seconds by remember { mutableStateOf(null) }
+ val time by remember(timeLeft) { derivedStateOf {
+ val mins = timeLeft/60
+ val secs = timeLeft%60
+ val sMinutes = if (mins < 10) "0$mins" else mins.toString()
+ val sSeconds = if (secs < 10) "0$secs" else secs.toString()
+ "$sMinutes:$sSeconds"
+ } }
+ AlertDialog(
+ onDismissRequest = { showTimerDialog = false },
+ title = {
+ Text(
+ text = stringResource(R.string.sleep_timer),
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ )
+ },
+ text = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ){
+ if (isRunning) {
+ Text(
+ text = stringResource(R.string.stopping_in, time),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ } else {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ OutlinedTextField(
+ value = minutes?.toString() ?: "",
+ onValueChange = {
+ if (it.length > 2) return@OutlinedTextField
+ minutes = try {
+ if (it.isEmpty()) null else it.toInt()
+ } catch (_: Exception) { minutes }
+ },
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ label = {
+ Text(text = "mm")
+ },
+ textStyle = MaterialTheme.typography.titleLarge,
+ modifier = Modifier
+ .width(80.dp)
+ )
+ Text(
+ text = ":",
+ modifier = Modifier
+ .width(12.dp),
+ textAlign = TextAlign.Center,
+ fontWeight = FontWeight.ExtraBold,
+ style = MaterialTheme.typography.titleLarge
+ )
+ OutlinedTextField(
+ value = seconds?.toString() ?: "",
+ onValueChange = {
+ if (it.length > 2) return@OutlinedTextField
+ if (it.isEmpty()) {
+ seconds = null
+ return@OutlinedTextField
+ }
+ val num = try {
+ it.toInt()
+ } catch (_: Exception) { seconds }
+ if (num != null && num > 59) return@OutlinedTextField
+ seconds = num
+ },
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ label = {
+ Text(text = "ss")
+ },
+ textStyle = MaterialTheme.typography.titleLarge,
+ modifier = Modifier
+ .width(80.dp)
+ )
+ }
+ }
+ }
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ if (isRunning) {
+ cancelTimer()
+ } else {
+ beginTimer((minutes ?: 0)*60+(seconds ?: 0))
+ }
+ showTimerDialog = false
+ },
+ content = {
+ Text(text = stringResource(if (isRunning) R.string.stop_timer else R.string.start_timer))
+ }
+ )
+ },
+ dismissButton = {
+ OutlinedButton(
+ onClick = { showTimerDialog = false },
+ content = {
+ Text(text = stringResource(R.string.close))
+ }
+ )
+ },
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingTopBar.kt b/app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingTopBar.kt
index 0cb042a..76c954e 100644
--- a/app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingTopBar.kt
+++ b/app/src/main/java/com/github/pakka_papad/nowplaying/NowPlayingTopBar.kt
@@ -12,8 +12,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
+import com.github.pakka_papad.R
import com.github.pakka_papad.components.TopBarWithBackArrow
import com.github.pakka_papad.components.more_options.OptionsDropDown
@@ -29,7 +31,7 @@ fun NowPlayingTopBar(
var dropDownMenuExpanded by remember { mutableStateOf(false) }
Icon(
imageVector = Icons.Outlined.MoreVert,
- contentDescription = null,
+ contentDescription = stringResource(R.string.more_menu_button),
modifier = Modifier
.padding(16.dp)
.size(30.dp)
diff --git a/app/src/main/java/com/github/pakka_papad/nowplaying/PlayerHelper.kt b/app/src/main/java/com/github/pakka_papad/nowplaying/PlayerHelper.kt
new file mode 100644
index 0000000..ff2f8f4
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/nowplaying/PlayerHelper.kt
@@ -0,0 +1,31 @@
+package com.github.pakka_papad.nowplaying
+
+import androidx.compose.runtime.Stable
+import androidx.media3.common.Player.Listener
+import androidx.media3.exoplayer.ExoPlayer
+
+@Stable
+class PlayerHelper(
+ private val exoPlayer: ExoPlayer,
+) {
+ val currentPosition: Float
+ get() = exoPlayer.currentPosition.toFloat()
+
+ val duration: Float
+ get() = exoPlayer.duration.toFloat()
+
+ val currentMediaItemIndex: Int
+ get() = exoPlayer.currentMediaItemIndex
+
+ fun addListener(listener: Listener) = exoPlayer::addListener
+
+ fun removeListener(listener: Listener) = exoPlayer::removeListener
+
+ fun seekTo(mediaItemIndex: Int, positionMs: Long) {
+ exoPlayer.seekTo(mediaItemIndex, positionMs)
+ }
+
+ fun seekTo(positionMs: Long) {
+ exoPlayer.seekTo(positionMs)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/nowplaying/Queue.kt b/app/src/main/java/com/github/pakka_papad/nowplaying/Queue.kt
index 3e78a05..715cd5b 100644
--- a/app/src/main/java/com/github/pakka_papad/nowplaying/Queue.kt
+++ b/app/src/main/java/com/github/pakka_papad/nowplaying/Queue.kt
@@ -11,11 +11,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.media3.exoplayer.ExoPlayer
import com.github.pakka_papad.components.SongCardV2
import com.github.pakka_papad.data.music.Song
import kotlinx.coroutines.delay
-import timber.log.Timber
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -24,24 +22,22 @@ fun ColumnScope.Queue(
onFavouriteClicked: (Song) -> Unit,
currentSong: Song?,
expanded: Boolean,
- exoPlayer: ExoPlayer,
+ playerHelper: PlayerHelper,
onDrag: (fromIndex: Int, toIndex: Int) -> Unit,
) {
val listState = rememberLazyListState()
LaunchedEffect(key1 = currentSong, key2 = expanded) {
delay(600)
if (!expanded) {
- listState.scrollToItem(exoPlayer.currentMediaItemIndex)
+ listState.scrollToItem(playerHelper.currentMediaItemIndex)
return@LaunchedEffect
}
if (!listState.isScrollInProgress) {
- listState.animateScrollToItem(exoPlayer.currentMediaItemIndex)
+ listState.animateScrollToItem(playerHelper.currentMediaItemIndex)
}
}
val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
- Timber.d("dd from:$fromIndex to:$toIndex")
onDrag(fromIndex,toIndex)
- exoPlayer.moveMediaItem(fromIndex,toIndex)
}
LazyColumn(
modifier = Modifier
@@ -63,7 +59,7 @@ fun ColumnScope.Queue(
DraggableItem(dragDropState, index) {
SongCardV2(
song = song,
- onSongClicked = { if(!isPlaying){ exoPlayer.seekTo(index,0) } },
+ onSongClicked = { if(!isPlaying){ playerHelper.seekTo(index,0) } },
onFavouriteClicked = onFavouriteClicked,
currentlyPlaying = isPlaying,
)
diff --git a/app/src/main/java/com/github/pakka_papad/onboarding/OnBoardingViewModel.kt b/app/src/main/java/com/github/pakka_papad/onboarding/OnBoardingViewModel.kt
index 79b3849..0f40e64 100644
--- a/app/src/main/java/com/github/pakka_papad/onboarding/OnBoardingViewModel.kt
+++ b/app/src/main/java/com/github/pakka_papad/onboarding/OnBoardingViewModel.kt
@@ -1,10 +1,9 @@
package com.github.pakka_papad.onboarding
-import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.github.pakka_papad.data.DataManager
import com.github.pakka_papad.data.music.ScanStatus
+import com.github.pakka_papad.data.music.SongExtractor
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
@@ -12,11 +11,10 @@ import javax.inject.Inject
@HiltViewModel
class OnBoardingViewModel @Inject constructor(
- private val context: Application,
- private val manager: DataManager,
+ private val songExtractor: SongExtractor,
) : ViewModel() {
- val scanStatus = manager.scanStatus
+ val scanStatus = songExtractor.scanStatus
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(
@@ -27,7 +25,7 @@ class OnBoardingViewModel @Inject constructor(
)
fun scanForMusic() {
- manager.scanForMusic()
+ songExtractor.scanForMusic()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/onboarding/Page.kt b/app/src/main/java/com/github/pakka_papad/onboarding/Page.kt
index b61fe0a..08e3471 100644
--- a/app/src/main/java/com/github/pakka_papad/onboarding/Page.kt
+++ b/app/src/main/java/com/github/pakka_papad/onboarding/Page.kt
@@ -8,8 +8,10 @@ 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.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import com.github.pakka_papad.R
import com.github.pakka_papad.data.music.ScanStatus
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
@@ -84,7 +86,7 @@ fun PageIndicator(
.align(Alignment.CenterStart)
){
Text(
- text = "Back",
+ text = stringResource(R.string.back),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimary,
)
@@ -120,7 +122,7 @@ fun PageIndicator(
enabled = nextEnabled
){
Text(
- text = if(pageCount-1 == currentPage) "Finish" else "Next",
+ text = if(pageCount-1 == currentPage) stringResource(R.string.finish) else stringResource(R.string.next),
style = MaterialTheme.typography.titleMedium,
color = if (nextEnabled) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface,
)
diff --git a/app/src/main/java/com/github/pakka_papad/onboarding/PermissionPages.kt b/app/src/main/java/com/github/pakka_papad/onboarding/PermissionPages.kt
index 6b40b65..190e294 100644
--- a/app/src/main/java/com/github/pakka_papad/onboarding/PermissionPages.kt
+++ b/app/src/main/java/com/github/pakka_papad/onboarding/PermissionPages.kt
@@ -3,6 +3,7 @@ package com.github.pakka_papad.onboarding
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
import com.airbnb.lottie.compose.*
import com.github.pakka_papad.R
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@@ -15,10 +16,10 @@ fun NotificationPermissionPage(
) = PermissionPage(
permissionState = permissionState,
lottieRawRes = R.raw.notification_permission,
- header = "Notification access",
- description = "Zen needs access to post notifications regarding the media playing",
- grantedMessage = "Notification access granted",
- notGrantedMessage = "Grant access to notification"
+ header = stringResource(R.string.notification_access),
+ description = stringResource(R.string.zen_needs_access_to_post_notifications_regarding_the_media_playing),
+ grantedMessage = stringResource(R.string.notification_access_granted),
+ notGrantedMessage = stringResource(R.string.grant_access_to_notification)
)
@OptIn(ExperimentalPermissionsApi::class)
@@ -28,10 +29,10 @@ fun ReadAudioPermissionPage(
) = PermissionPage(
permissionState = permissionState,
lottieRawRes = R.raw.storage_permission,
- header = "Audio files access",
- description = "Zen needs access to read audio files present on the device",
- grantedMessage = "Access granted",
- notGrantedMessage = "Grant access to read audio"
+ header = stringResource(R.string.audio_files_access),
+ description = stringResource(R.string.zen_needs_access_to_read_audio_files_present_on_the_device),
+ grantedMessage = stringResource(R.string.access_granted),
+ notGrantedMessage = stringResource(R.string.grant_access_to_read_audio)
)
@OptIn(ExperimentalPermissionsApi::class)
@@ -41,8 +42,8 @@ fun ReadStoragePermissionPage(
) = PermissionPage(
permissionState = permissionState,
lottieRawRes = R.raw.storage_permission,
- header = "Storage access",
- description = "Zen needs storage access to read audio files present on the device",
- grantedMessage = "Access granted",
- notGrantedMessage = "Grant access to read storage"
+ header = stringResource(R.string.storage_access),
+ description = stringResource(R.string.zen_needs_storage_access_to_read_audio_files_present_on_the_device),
+ grantedMessage = stringResource(R.string.access_granted),
+ notGrantedMessage = stringResource(R.string.grant_access_to_read_storage)
)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/player/ZenPlayer.kt b/app/src/main/java/com/github/pakka_papad/player/ZenPlayer.kt
index fc5908f..1783163 100644
--- a/app/src/main/java/com/github/pakka_papad/player/ZenPlayer.kt
+++ b/app/src/main/java/com/github/pakka_papad/player/ZenPlayer.kt
@@ -20,37 +20,46 @@ import androidx.media3.common.Timeline
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.PlaybackStatsListener
-import com.github.pakka_papad.*
-import com.github.pakka_papad.data.DataManager
+import com.github.pakka_papad.Constants
import com.github.pakka_papad.data.ZenCrashReporter
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.data.music.SongExtractor
import com.github.pakka_papad.data.notification.ZenNotificationManager
+import com.github.pakka_papad.data.services.AnalyticsService
+import com.github.pakka_papad.data.services.QueueService
+import com.github.pakka_papad.data.services.SleepTimerService
+import com.github.pakka_papad.data.services.SongService
+import com.github.pakka_papad.toCorrectedParams
+import com.github.pakka_papad.toExoPlayerPlaybackParameters
import com.github.pakka_papad.widgets.WidgetBroadcast
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
+import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@UnstableApi @AndroidEntryPoint
-class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback {
-
- @Inject
- lateinit var notificationManager: ZenNotificationManager
-
- @Inject
- lateinit var dataManager: DataManager
-
- @Inject
- lateinit var exoPlayer: ExoPlayer
-
- @Inject
- lateinit var crashReporter: ZenCrashReporter
-
- @Inject
- lateinit var preferencesProvider: ZenPreferenceProvider
+class ZenPlayer : Service(), QueueService.Listener, ZenBroadcastReceiver.Callback {
+
+ @Inject lateinit var notificationManager: ZenNotificationManager
+ @Inject lateinit var songExtractor: SongExtractor
+ @Inject lateinit var queueService: QueueService
+ @Inject lateinit var songService: SongService
+ @Inject lateinit var sleepTimerService: SleepTimerService
+ @Inject lateinit var analyticsService: AnalyticsService
+ @Inject lateinit var exoPlayer: ExoPlayer
+ @Inject lateinit var crashReporter: ZenCrashReporter
+ @Inject lateinit var preferencesProvider: ZenPreferenceProvider
private var broadcastReceiver: ZenBroadcastReceiver? = null
@@ -66,7 +75,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
val window = eventTime.timeline.getWindow(eventTime.windowIndex, Timeline.Window())
try {
window.mediaItem.localConfiguration?.tag?.let {
- dataManager.addPlayHistory(it as String, playbackStats.totalPlayTimeMs)
+ analyticsService.logSongPlay(it as String, playbackStats.totalPlayTimeMs)
}
} catch (_ : Exception){
@@ -75,6 +84,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
companion object {
const val MEDIA_SESSION = "media_session"
+ val isRunning = AtomicBoolean(false)
}
private lateinit var mediaSession: MediaSessionCompat
@@ -86,8 +96,8 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
super.onMediaItemTransition(mediaItem, reason)
try {
- dataManager.updateCurrentSong(exoPlayer.currentMediaItemIndex)
- dataManager.getSongAtIndex(exoPlayer.currentMediaItemIndex)?.let { song ->
+ queueService.setCurrentSong(exoPlayer.currentMediaItemIndex)
+ queueService.getSongAtIndex(exoPlayer.currentMediaItemIndex)?.let { song ->
val broadcast = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE).apply {
putExtra(WidgetBroadcast.WIDGET_BROADCAST, WidgetBroadcast.SONG_CHANGED)
putExtra("imageUri", song.artUri)
@@ -119,8 +129,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
super.onPlayerError(error)
crashReporter.logException(error)
if (error.errorCode == PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND){
- dataManager.cleanData()
- showToast("Could not find the song ${dataManager.currentSong.value?.title ?: ""} at the specified path")
+ songExtractor.cleanData()
onBroadcastCancel()
}
}
@@ -164,7 +173,22 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
broadcastReceiver = ZenBroadcastReceiver()
mediaSession = MediaSessionCompat(this, MEDIA_SESSION)
systemNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- dataManager.setPlayerRunning(this)
+ isRunning.set(true)
+ queueService.addListener(this)
+ if (intent == null) Toast.makeText(this, "null intent", Toast.LENGTH_SHORT).show()
+ intent?.let {
+ val locations = it.getStringArrayExtra("locations") ?: return@let
+ val startPosition = it.getIntExtra("startPosition", 0)
+ if (locations.isEmpty()) return@let
+ val mediaItems = locations.map { location ->
+ MediaItem.Builder().apply {
+ setUri(Uri.fromFile(File(location)))
+ setTag(location)
+ }.build()
+ }
+ setQueue(mediaItems, startPosition)
+ }
+
IntentFilter(Constants.PACKAGE_NAME).also {
registerReceiver(broadcastReceiver, it)
}
@@ -178,7 +202,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
notificationManager.getPlayerNotification(
session = mediaSession,
showPlayButton = false,
- isLiked = dataManager.getSongAtIndex(exoPlayer.currentMediaItemIndex)?.favourite ?: false
+ isLiked = queueService.getSongAtIndex(exoPlayer.currentMediaItemIndex)?.favourite ?: false
)
)
@@ -192,7 +216,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
}
}
scope.launch {
- dataManager.repeatMode.collect {
+ queueService.repeatMode.collect {
withContext(Dispatchers.Main) { exoPlayer.repeatMode = it.toExoPlayerRepeatMode() }
}
}
@@ -213,13 +237,16 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
}
private fun stopService() {
- unregisterReceiver(broadcastReceiver)
+ isRunning.set(false)
+ queueService.clearQueue()
+ queueService.removeListener(this)
+ sleepTimerService.cancel()
exoPlayer.stop()
exoPlayer.clearMediaItems()
exoPlayer.removeListener(exoPlayerListener)
exoPlayer.removeAnalyticsListener(playbackStatsListener)
+ unregisterReceiver(broadcastReceiver)
mediaSession.release()
- dataManager.stopPlayerRunning()
broadcastReceiver?.stopListening()
systemNotificationManager?.cancel(ZenNotificationManager.PLAYER_NOTIFICATION_ID)
scope.cancel()
@@ -240,7 +267,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
scope.launch {
var currentSong: Song? = null
withContext(Dispatchers.Main) {
- currentSong = dataManager.getSongAtIndex(exoPlayer.currentMediaItemIndex)
+ currentSong = queueService.getSongAtIndex(exoPlayer.currentMediaItemIndex)
}
if (currentSong == null) return@launch
mediaSession.setMetadata(
@@ -270,7 +297,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
notificationManager.getPlayerNotification(
session = mediaSession,
showPlayButton = !exoPlayer.isPlaying,
- isLiked = dataManager.getSongAtIndex(exoPlayer.currentMediaItemIndex)?.favourite ?: false
+ isLiked = queueService.getSongAtIndex(exoPlayer.currentMediaItemIndex)?.favourite ?: false
)
)
}
@@ -301,22 +328,16 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
}
}
- @Synchronized
- override fun setQueue(newQueue: List, startPlayingFromIndex: Int) {
+ private fun setQueue(mediaItems: List, startPosition: Int){
scope.launch {
- val mediaItems = newQueue.map {
- MediaItem.Builder().apply {
- setUri(Uri.fromFile(File(it.location)))
- setTag(it.location)
- }.build()
- }
+ val repeatMode = queueService.repeatMode.first()
withContext(Dispatchers.Main){
exoPlayer.stop()
exoPlayer.clearMediaItems()
exoPlayer.addMediaItems(mediaItems)
exoPlayer.prepare()
- exoPlayer.seekTo(startPlayingFromIndex,0)
- exoPlayer.repeatMode = dataManager.repeatMode.value.toExoPlayerRepeatMode()
+ exoPlayer.seekTo(startPosition,0)
+ exoPlayer.repeatMode = repeatMode.toExoPlayerRepeatMode()
exoPlayer.playbackParameters = preferencesProvider.playbackParams.value
.toCorrectedParams()
.toExoPlayerPlaybackParameters()
@@ -327,8 +348,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
}
}
- @Synchronized
- override fun addToQueue(song: Song) {
+ override fun onAppend(song: Song) {
exoPlayer.addMediaItem(
MediaItem.Builder().apply {
setUri(Uri.fromFile(File(song.location)))
@@ -337,10 +357,44 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
)
}
- @Synchronized
- override fun updateNotification() {
- updateMediaSessionState()
- updateMediaSessionMetadata()
+ override fun onAppend(songs: List) {
+ exoPlayer.addMediaItems(
+ songs.map {
+ MediaItem.Builder().apply {
+ setUri(Uri.fromFile(File(it.location)))
+ setTag(it.location)
+ }.build()
+ }
+ )
+ }
+
+ override fun onUpdate(updatedSong: Song, position: Int) {
+ scope.launch {
+ val performUpdate = withContext(Dispatchers.Main) {
+ exoPlayer.currentMediaItemIndex == position
+ }
+ if (!performUpdate) return@launch
+ updateMediaSessionState()
+ updateMediaSessionMetadata()
+ }
+ }
+
+ override fun onMove(from: Int, to: Int) {
+ exoPlayer.moveMediaItem(from, to)
+ }
+
+ override fun onClear() {
+
+ }
+
+ override fun onSetQueue(songs: List, startPlayingFromPosition: Int) {
+ val mediaItems = songs.map {
+ MediaItem.Builder().apply {
+ setUri(Uri.fromFile(File(it.location)))
+ setTag(it.location)
+ }.build()
+ }
+ setQueue(mediaItems, startPlayingFromPosition)
}
/**
@@ -383,10 +437,12 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
* DataManager then calls updateNotification of DataManager.Callback
*/
override fun onBroadcastLike() {
- val currentSong = dataManager.getSongAtIndex(exoPlayer.currentMediaItemIndex) ?: return
+ val currentSong = queueService.getSongAtIndex(exoPlayer.currentMediaItemIndex) ?: return
val updatedSong = currentSong.copy(favourite = !currentSong.favourite)
scope.launch {
- dataManager.updateSong(updatedSong)
+// onUpdateCurrentSong()
+ queueService.update(updatedSong)
+ songService.updateSong(updatedSong)
}
}
diff --git a/app/src/main/java/com/github/pakka_papad/restore/RestoreContent.kt b/app/src/main/java/com/github/pakka_papad/restore/RestoreContent.kt
index a1ef718..98be1b5 100644
--- a/app/src/main/java/com/github/pakka_papad/restore/RestoreContent.kt
+++ b/app/src/main/java/com/github/pakka_papad/restore/RestoreContent.kt
@@ -16,14 +16,12 @@ import com.github.pakka_papad.data.music.BlacklistedSong
fun RestoreContent(
songs: List,
selectList: List,
- paddingValues: PaddingValues,
onSelectChanged: (index: Int, isSelected: Boolean) -> Unit,
){
if (songs.size != selectList.size) return
LazyColumn(
modifier = Modifier
.fillMaxSize(),
- contentPadding = paddingValues,
) {
itemsIndexed(
items = songs,
diff --git a/app/src/main/java/com/github/pakka_papad/restore/RestoreFragment.kt b/app/src/main/java/com/github/pakka_papad/restore/RestoreFragment.kt
index f63995b..0baf88e 100644
--- a/app/src/main/java/com/github/pakka_papad/restore/RestoreFragment.kt
+++ b/app/src/main/java/com/github/pakka_papad/restore/RestoreFragment.kt
@@ -4,25 +4,32 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.res.stringResource
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
+import com.github.pakka_papad.R
+import com.github.pakka_papad.components.BlockingProgressIndicator
import com.github.pakka_papad.components.CancelConfirmTopBar
+import com.github.pakka_papad.components.Snackbar
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.ui.theme.ZenTheme
+import com.github.pakka_papad.util.Resource
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -36,7 +43,6 @@ class RestoreFragment: Fragment() {
@Inject
lateinit var preferenceProvider: ZenPreferenceProvider
- @OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -50,45 +56,69 @@ class RestoreFragment: Fragment() {
ZenTheme(theme) {
val songs by viewModel.blackListedSongs.collectAsStateWithLifecycle()
val selectList = viewModel.restoreList
- val restored by viewModel.restored.collectAsStateWithLifecycle()
- LaunchedEffect(key1 = restored){
- if (restored){
- navController.popBackStack()
+
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val restoreState by viewModel.restoreState.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = restoreState){
+ if (restoreState is Resource.Idle || restoreState is Resource.Loading) return@LaunchedEffect
+ if (restoreState is Resource.Success){
+ snackbarHostState.showSnackbar(
+ message = getString(R.string.done_rescan_to_see_all_the_restored_songs),
+ )
+ } else {
+ restoreState.message?.let {
+ snackbarHostState.showSnackbar(message = it)
+ }
}
+ navController.popBackStack()
}
+
+ BackHandler(
+ enabled = restoreState is Resource.Loading,
+ onBack = { }
+ )
+
Scaffold(
topBar = {
CancelConfirmTopBar(
- onCancelClicked = navController::popBackStack,
+ onCancelClicked = {
+ if (restoreState is Resource.Idle) {
+ navController.popBackStack()
+ }
+ },
onConfirmClicked = viewModel::restoreSongs,
- title = "Restore songs"
+ title = stringResource(R.string.restore_songs)
)
},
content = { paddingValues ->
- val insetsPadding =
- WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
- if (selectList.size != songs.size) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues),
- contentAlignment = Alignment.Center
- ){
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ){
+ if (selectList.size != songs.size){
CircularProgressIndicator()
+ } else {
+ RestoreContent(
+ songs = songs,
+ selectList = selectList,
+ onSelectChanged = viewModel::updateRestoreList
+ )
+ if (restoreState is Resource.Loading){
+ BlockingProgressIndicator()
+ }
}
- } else {
- RestoreContent(
- songs = songs,
- selectList = selectList,
- paddingValues = PaddingValues(
- top = paddingValues.calculateTopPadding(),
- start = insetsPadding.calculateStartPadding(LayoutDirection.Ltr),
- end = insetsPadding.calculateEndPadding(LayoutDirection.Ltr),
- bottom = insetsPadding.calculateBottomPadding()
- ),
- onSelectChanged = viewModel::updateRestoreList
- )
}
+ },
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ snackbar = {
+ Snackbar(it)
+ }
+ )
}
)
}
diff --git a/app/src/main/java/com/github/pakka_papad/restore/RestoreViewModel.kt b/app/src/main/java/com/github/pakka_papad/restore/RestoreViewModel.kt
index f06b056..d765fc3 100644
--- a/app/src/main/java/com/github/pakka_papad/restore/RestoreViewModel.kt
+++ b/app/src/main/java/com/github/pakka_papad/restore/RestoreViewModel.kt
@@ -1,11 +1,12 @@
package com.github.pakka_papad.restore
-import android.app.Application
-import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.github.pakka_papad.data.DataManager
+import com.github.pakka_papad.R
+import com.github.pakka_papad.data.services.BlacklistService
+import com.github.pakka_papad.util.MessageStore
+import com.github.pakka_papad.util.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -14,19 +15,20 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import org.jetbrains.annotations.VisibleForTesting
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class RestoreViewModel @Inject constructor(
- private val context: Application,
- private val manager: DataManager
+ private val messageStore: MessageStore,
+ private val blacklistService: BlacklistService,
) : ViewModel() {
private val _restoreList = mutableStateListOf()
val restoreList : List = _restoreList
- val blackListedSongs = manager.getAll.blacklistedSongs()
+ val blackListedSongs = blacklistService.blacklistedSongs
.onEach {
while (_restoreList.size < it.size) _restoreList.add(false)
while (_restoreList.size > it.size) _restoreList.removeLast()
@@ -37,27 +39,29 @@ class RestoreViewModel @Inject constructor(
)
fun updateRestoreList(index: Int, isSelected: Boolean){
+ if (restoreState.value !is Resource.Idle) return
if (index >= _restoreList.size) return
_restoreList[index] = isSelected
}
- private val _restored = MutableStateFlow(false)
- val restored = _restored.asStateFlow()
+ @VisibleForTesting
+ internal val _restoreState = MutableStateFlow>(Resource.Idle())
+ val restoreState = _restoreState.asStateFlow()
fun restoreSongs(){
+ if (restoreState.value !is Resource.Idle) return
viewModelScope.launch {
+ _restoreState.update { Resource.Loading() }
val blacklist = blackListedSongs.value
val toRestore = _restoreList.indices
.filter { _restoreList[it] }
.map { blacklist[it] }
try {
- manager.removeFromBlacklist(toRestore)
- Toast.makeText(context,"Rescan to see all the restored songs",Toast.LENGTH_SHORT).show()
+ blacklistService.whitelistSongs(toRestore)
+ _restoreState.update { Resource.Success(Unit) }
} catch (e: Exception){
Timber.e(e)
- Toast.makeText(context,"Some error occurred",Toast.LENGTH_SHORT).show()
- } finally {
- _restored.update { true }
+ _restoreState.update { Resource.Error(messageStore.getString(R.string.some_error_occurred)) }
}
}
}
diff --git a/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderContent.kt b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderContent.kt
index 232a79c..6838f39 100644
--- a/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderContent.kt
+++ b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderContent.kt
@@ -1,6 +1,5 @@
package com.github.pakka_papad.restore_folder
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -15,12 +14,10 @@ import com.github.pakka_papad.data.music.BlacklistedFolder
fun RestoreFoldersContent(
folders: List,
selectList: List,
- paddingValues: PaddingValues,
onSelectChanged: (index: Int, isSelected: Boolean) -> Unit,
) {
if (folders.size != selectList.size) return
LazyColumn(
- contentPadding = paddingValues,
modifier = Modifier
.fillMaxSize(),
) {
diff --git a/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderFragment.kt b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderFragment.kt
index 748d0b8..4f32b75 100644
--- a/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderFragment.kt
+++ b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderFragment.kt
@@ -1,39 +1,37 @@
package com.github.pakka_papad.restore_folder
-import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.res.stringResource
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
+import com.github.pakka_papad.R
+import com.github.pakka_papad.components.BlockingProgressIndicator
import com.github.pakka_papad.components.CancelConfirmTopBar
+import com.github.pakka_papad.components.Snackbar
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.ui.theme.ZenTheme
+import com.github.pakka_papad.util.Resource
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -47,15 +45,12 @@ class RestoreFolderFragment: Fragment() {
@Inject
lateinit var preferenceProvider: ZenPreferenceProvider
- @OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
navController = findNavController()
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
- intent.addCategory(Intent.CATEGORY_DEFAULT)
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
@@ -63,49 +58,71 @@ class RestoreFolderFragment: Fragment() {
val folders by viewModel.folders.collectAsStateWithLifecycle()
val selectList = viewModel.restoreFolderList
- val restored by viewModel.restored.collectAsStateWithLifecycle()
- LaunchedEffect(key1 = restored){
- if (restored){
- navController.popBackStack()
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val restoreState by viewModel.restored.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = restoreState){
+ if (restoreState is Resource.Idle || restoreState is Resource.Loading) return@LaunchedEffect
+ if (restoreState is Resource.Success){
+ snackbarHostState.showSnackbar(
+ message = getString(R.string.done_rescan_to_see_all_the_restored_songs),
+ )
+ } else {
+ restoreState.message?.let {
+ snackbarHostState.showSnackbar(message = it)
+ }
}
+ navController.popBackStack()
}
+ BackHandler(
+ enabled = restoreState is Resource.Loading,
+ onBack = { }
+ )
+
ZenTheme(themePreference) {
Scaffold(
topBar = {
CancelConfirmTopBar(
- onCancelClicked = navController::popBackStack,
+ onCancelClicked = {
+ if (restoreState is Resource.Idle){
+ navController.popBackStack()
+ }
+ },
onConfirmClicked = viewModel::restoreFolders,
- title = "Restore folders"
+ title = stringResource(R.string.restore_folders)
)
},
content = { paddingValues ->
- val insetsPadding =
- WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
- if (selectList.size != folders.size) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues),
- contentAlignment = Alignment.Center
- ){
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ) {
+ if (selectList.size != folders.size){
CircularProgressIndicator()
+ } else {
+ RestoreFoldersContent(
+ folders = folders,
+ selectList = selectList,
+ onSelectChanged = viewModel::updateRestoreList
+ )
+ if(restoreState is Resource.Loading){
+ BlockingProgressIndicator()
+ }
}
- } else {
- RestoreFoldersContent(
- folders = folders,
- paddingValues = PaddingValues(
- top = paddingValues.calculateTopPadding(),
- start = insetsPadding.calculateStartPadding(LayoutDirection.Ltr),
- end = insetsPadding.calculateEndPadding(LayoutDirection.Ltr),
- bottom = insetsPadding.calculateBottomPadding()
- ),
- selectList = selectList,
- onSelectChanged = viewModel::updateRestoreList
- )
}
},
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ snackbar = {
+ Snackbar(it)
+ }
+ )
+ }
)
}
}
diff --git a/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderViewModel.kt b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderViewModel.kt
index 55d02ca..e8ed249 100644
--- a/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderViewModel.kt
+++ b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderViewModel.kt
@@ -1,11 +1,12 @@
package com.github.pakka_papad.restore_folder
-import android.app.Application
-import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.github.pakka_papad.data.DataManager
+import com.github.pakka_papad.R
+import com.github.pakka_papad.data.services.BlacklistService
+import com.github.pakka_papad.util.MessageStore
+import com.github.pakka_papad.util.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -14,18 +15,19 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import org.jetbrains.annotations.VisibleForTesting
import javax.inject.Inject
@HiltViewModel
class RestoreFolderViewModel @Inject constructor(
- private val context: Application,
- private val manager: DataManager,
+ private val messageStore: MessageStore,
+ private val blacklistService: BlacklistService,
): ViewModel() {
private val _restoreFoldersList = mutableStateListOf()
val restoreFolderList : List = _restoreFoldersList
- val folders = manager.getAll.blacklistedFolders()
+ val folders = blacklistService.blacklistedFolders
.onEach {
while (_restoreFoldersList.size < it.size) _restoreFoldersList.add(false)
while (_restoreFoldersList.size > it.size) _restoreFoldersList.removeLast()
@@ -37,31 +39,29 @@ class RestoreFolderViewModel @Inject constructor(
)
fun updateRestoreList(index: Int, isSelected: Boolean){
+ if (restored.value !is Resource.Idle) return
if (index >= _restoreFoldersList.size) return
_restoreFoldersList[index] = isSelected
}
- private val _restored = MutableStateFlow(false)
+ @VisibleForTesting
+ internal val _restored = MutableStateFlow>(Resource.Idle())
val restored = _restored.asStateFlow()
fun restoreFolders(){
+ if (restored.value !is Resource.Idle) return
viewModelScope.launch {
+ _restored.update { Resource.Loading() }
val allFolders = folders.value
val toRestore = _restoreFoldersList.indices
.filter { _restoreFoldersList[it] }
.map { allFolders[it] }
try {
- manager.removeFoldersFromBlacklist(toRestore)
- showToast("Done. Rescan to see all the songs")
+ blacklistService.whitelistFolders(toRestore)
+ _restored.update { Resource.Success(Unit) }
} catch (_ : Exception){
- showToast("Some error occurred")
- } finally {
- _restored.update { true }
+ _restored.update { Resource.Error(messageStore.getString(R.string.some_error_occurred)) }
}
}
}
-
- private fun showToast(message: String) {
- Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/search/ResultContent.kt b/app/src/main/java/com/github/pakka_papad/search/ResultContent.kt
index 7ff223a..ca89855 100644
--- a/app/src/main/java/com/github/pakka_papad/search/ResultContent.kt
+++ b/app/src/main/java/com/github/pakka_papad/search/ResultContent.kt
@@ -40,6 +40,7 @@ fun TextCard(
@Composable
fun ResultContent(
+ contentPadding: PaddingValues,
searchResult: SearchResult,
showGrid: Boolean,
searchType: SearchType,
@@ -56,8 +57,7 @@ fun ResultContent(
columns = if (showGrid) GridCells.Adaptive(150.dp) else GridCells.Fixed(1),
modifier = Modifier
.fillMaxSize(),
- contentPadding = WindowInsets.systemBars.only(
- WindowInsetsSides.Bottom).asPaddingValues(),
+ contentPadding = contentPadding,
) {
when(searchType){
SearchType.Songs -> {
diff --git a/app/src/main/java/com/github/pakka_papad/search/SearchBar.kt b/app/src/main/java/com/github/pakka_papad/search/SearchBar.kt
index f01c725..58387ce 100644
--- a/app/src/main/java/com/github/pakka_papad/search/SearchBar.kt
+++ b/app/src/main/java/com/github/pakka_papad/search/SearchBar.kt
@@ -18,11 +18,12 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import com.github.pakka_papad.R
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
query: String,
@@ -37,10 +38,14 @@ fun SearchBar(
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal))
) {
+ val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
TextField(
modifier = Modifier.fillMaxWidth(),
- colors = TextFieldDefaults.textFieldColors(
- containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = containerColor,
+ unfocusedContainerColor = containerColor,
+ disabledContainerColor = containerColor,
+ errorContainerColor = containerColor,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
@@ -51,7 +56,7 @@ fun SearchBar(
leadingIcon = {
Icon(
imageVector = Icons.Outlined.ArrowBack,
- contentDescription = null,
+ contentDescription = stringResource(R.string.back_button),
modifier = Modifier
.size(48.dp)
.padding(9.dp)
@@ -68,7 +73,9 @@ fun SearchBar(
trailingIcon = {
Icon(
imageVector = if (query.isEmpty()) Icons.Outlined.Search else Icons.Outlined.Close,
- contentDescription = null,
+ contentDescription = stringResource(
+ if (query.isEmpty()) R.string.search_icon else R.string.clear_button
+ ),
modifier = Modifier
.size(48.dp)
.padding(9.dp)
@@ -84,7 +91,7 @@ fun SearchBar(
},
placeholder = {
Text(
- text = "Search for songs, albums, artists, playlists...",
+ text = stringResource(R.string.search_for_songs_albums_artists_playlists),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
diff --git a/app/src/main/java/com/github/pakka_papad/search/SearchFragment.kt b/app/src/main/java/com/github/pakka_papad/search/SearchFragment.kt
index a2b31b9..3b41bb5 100644
--- a/app/src/main/java/com/github/pakka_papad/search/SearchFragment.kt
+++ b/app/src/main/java/com/github/pakka_papad/search/SearchFragment.kt
@@ -5,8 +5,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.*
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -21,6 +23,7 @@ import androidx.navigation.fragment.findNavController
import com.github.pakka_papad.R
import com.github.pakka_papad.collection.CollectionType
import com.github.pakka_papad.components.FullScreenSadMessage
+import com.github.pakka_papad.components.Snackbar
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.data.music.*
import com.github.pakka_papad.ui.theme.ZenTheme
@@ -37,7 +40,6 @@ class SearchFragment : Fragment() {
@Inject
lateinit var preferenceProvider: ZenPreferenceProvider
- @OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -52,6 +54,13 @@ class SearchFragment : Fragment() {
val query by viewModel.query.collectAsStateWithLifecycle()
val searchResult by viewModel.searchResult.collectAsStateWithLifecycle()
val searchType by viewModel.searchType.collectAsStateWithLifecycle()
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val message by viewModel.message.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = message){
+ if (message.isEmpty()) return@LaunchedEffect
+ snackbarHostState.showSnackbar(message)
+ }
val showGrid by remember {
derivedStateOf {
@@ -73,15 +82,12 @@ class SearchFragment : Fragment() {
Box(
modifier = Modifier
.fillMaxSize()
- .padding(paddingValues)
- .windowInsetsPadding(
- WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
- )
) {
if (searchResult.errorMsg != null) {
FullScreenSadMessage(searchResult.errorMsg)
} else {
ResultContent(
+ contentPadding = paddingValues,
searchResult = searchResult,
showGrid = showGrid,
searchType = searchType,
@@ -96,6 +102,14 @@ class SearchFragment : Fragment() {
)
}
}
+ },
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ snackbar = {
+ Snackbar(it)
+ }
+ )
}
)
}
diff --git a/app/src/main/java/com/github/pakka_papad/search/SearchViewModel.kt b/app/src/main/java/com/github/pakka_papad/search/SearchViewModel.kt
index acba606..d003b4b 100644
--- a/app/src/main/java/com/github/pakka_papad/search/SearchViewModel.kt
+++ b/app/src/main/java/com/github/pakka_papad/search/SearchViewModel.kt
@@ -1,20 +1,33 @@
package com.github.pakka_papad.search
-import android.app.Application
-import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.github.pakka_papad.data.DataManager
+import com.github.pakka_papad.Constants
+import com.github.pakka_papad.R
import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.data.services.PlayerService
+import com.github.pakka_papad.data.services.QueueService
+import com.github.pakka_papad.data.services.SearchService
+import com.github.pakka_papad.util.MessageStore
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class SearchViewModel @Inject constructor(
- private val context: Application,
- private val manager: DataManager,
+ private val messageStore: MessageStore,
+ private val playerService: PlayerService,
+ private val queueService: QueueService,
+ private val searchService: SearchService,
) : ViewModel() {
private val _query = MutableStateFlow("")
@@ -30,14 +43,14 @@ class SearchViewModel @Inject constructor(
SearchResult()
} else {
when (type) {
- SearchType.Songs -> SearchResult(songs = manager.querySearch.searchSongs(trimmedQuery))
- SearchType.Albums -> SearchResult(albums = manager.querySearch.searchAlbums(trimmedQuery))
- SearchType.Artists -> SearchResult(artists = manager.querySearch.searchArtists(trimmedQuery))
- SearchType.AlbumArtists -> SearchResult(albumArtists = manager.querySearch.searchAlbumArtists(trimmedQuery))
- SearchType.Composers -> SearchResult(composers = manager.querySearch.searchComposers(trimmedQuery))
- SearchType.Lyricists -> SearchResult(lyricists = manager.querySearch.searchLyricists(trimmedQuery))
- SearchType.Genres -> SearchResult(genres = manager.querySearch.searchGenres(trimmedQuery))
- SearchType.Playlists -> SearchResult(playlists = manager.querySearch.searchPlaylists(trimmedQuery))
+ SearchType.Songs -> SearchResult(songs = searchService.searchSongs(trimmedQuery))
+ SearchType.Albums -> SearchResult(albums = searchService.searchAlbums(trimmedQuery))
+ SearchType.Artists -> SearchResult(artists = searchService.searchArtists(trimmedQuery))
+ SearchType.AlbumArtists -> SearchResult(albumArtists = searchService.searchAlbumArtists(trimmedQuery))
+ SearchType.Composers -> SearchResult(composers = searchService.searchComposers(trimmedQuery))
+ SearchType.Lyricists -> SearchResult(lyricists = searchService.searchLyricists(trimmedQuery))
+ SearchType.Genres -> SearchResult(genres = searchService.searchGenres(trimmedQuery))
+ SearchType.Playlists -> SearchResult(playlists = searchService.searchPlaylists(trimmedQuery))
}
}
}.catch { exception ->
@@ -48,6 +61,9 @@ class SearchViewModel @Inject constructor(
initialValue = SearchResult()
)
+ private val _message = MutableStateFlow("")
+ val message = _message.asStateFlow()
+
fun clearQueryText() {
_query.update { "" }
}
@@ -62,8 +78,16 @@ class SearchViewModel @Inject constructor(
fun setQueue(songs: List?, startPlayingFromIndex: Int = 0) {
if (songs == null) return
- manager.setQueue(songs, startPlayingFromIndex)
- Toast.makeText(context, "Playing", Toast.LENGTH_SHORT).show()
+ queueService.setQueue(songs, startPlayingFromIndex)
+ playerService.startServiceIfNotRunning(songs, startPlayingFromIndex)
+ showMessage(messageStore.getString(R.string.playing))
}
+ private fun showMessage(message: String){
+ viewModelScope.launch {
+ _message.update { message }
+ delay(Constants.MESSAGE_DURATION)
+ _message.update { "" }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistContent.kt b/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistContent.kt
index 49c7842..3b688e5 100644
--- a/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistContent.kt
+++ b/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistContent.kt
@@ -1,6 +1,5 @@
package com.github.pakka_papad.select_playlist
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -13,13 +12,11 @@ import com.github.pakka_papad.data.music.PlaylistWithSongCount
fun SelectPlaylistContent(
playlists: List,
selectList: List,
- paddingValues: PaddingValues,
onSelectChanged: (index: Int) -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
- contentPadding = paddingValues,
) {
itemsIndexed(
items = playlists,
diff --git a/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistFragment.kt b/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistFragment.kt
index d43f908..ee2d155 100644
--- a/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistFragment.kt
+++ b/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistFragment.kt
@@ -4,25 +4,33 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.res.stringResource
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
+import com.github.pakka_papad.R
+import com.github.pakka_papad.components.BlockingProgressIndicator
import com.github.pakka_papad.components.CancelConfirmTopBar
+import com.github.pakka_papad.components.Snackbar
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.ui.theme.ZenTheme
+import com.github.pakka_papad.util.Resource
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -38,7 +46,6 @@ class SelectPlaylistFragment: Fragment() {
@Inject
lateinit var preferenceProvider: ZenPreferenceProvider
- @OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -55,42 +62,72 @@ class SelectPlaylistFragment: Fragment() {
ZenTheme(theme) {
val playlists by viewModel.playlistsWithSongCount.collectAsStateWithLifecycle()
val selectList = viewModel.selectList
+
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val insertState by viewModel.insertState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(key1 = insertState){
+ if (insertState is Resource.Idle || insertState is Resource.Loading) return@LaunchedEffect
+ if (insertState is Resource.Success){
+ insertState.data?.let {
+ snackbarHostState.showSnackbar(message = it)
+ }
+ } else {
+ insertState.message?.let {
+ snackbarHostState.showSnackbar(message = it)
+ }
+ }
+ navController.popBackStack()
+ }
+
+ BackHandler(
+ enabled = insertState is Resource.Loading,
+ onBack = { }
+ )
+
Scaffold(
topBar = {
CancelConfirmTopBar(
- onCancelClicked = navController::popBackStack,
+ onCancelClicked = {
+ if (insertState is Resource.Idle){
+ navController.popBackStack()
+ }
+ },
onConfirmClicked = {
viewModel.addSongsToPlaylists(args.songLocations)
- navController.popBackStack()
},
- title = "Select Playlists"
+ title = stringResource(R.string.select_playlists)
)
},
content = { paddingValues ->
- val insetsPadding =
- WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
- if (selectList.size != playlists.size) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues),
- contentAlignment = Alignment.Center
- ){
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ){
+ if (selectList.size != playlists.size){
CircularProgressIndicator()
+ } else {
+ SelectPlaylistContent(
+ playlists = playlists,
+ selectList = selectList,
+ onSelectChanged = viewModel::toggleSelectAtIndex
+ )
+ if (insertState is Resource.Loading){
+ BlockingProgressIndicator()
+ }
}
- } else {
- SelectPlaylistContent(
- playlists = playlists,
- selectList = selectList,
- paddingValues = PaddingValues(
- top = paddingValues.calculateTopPadding(),
- start = insetsPadding.calculateStartPadding(LayoutDirection.Ltr),
- end = insetsPadding.calculateEndPadding(LayoutDirection.Ltr),
- bottom = insetsPadding.calculateBottomPadding()
- ),
- onSelectChanged = viewModel::toggleSelectAtIndex
- )
}
+ },
+ snackbarHost = {
+ SnackbarHost(
+ hostState = snackbarHostState,
+ snackbar = {
+ Snackbar(it)
+ }
+ )
}
)
}
diff --git a/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistViewModel.kt b/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistViewModel.kt
index d5c6c72..2b02dde 100644
--- a/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistViewModel.kt
+++ b/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistViewModel.kt
@@ -1,31 +1,38 @@
package com.github.pakka_papad.select_playlist
-import android.app.Application
-import android.widget.Toast
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.github.pakka_papad.data.DataManager
-import com.github.pakka_papad.data.music.PlaylistSongCrossRef
+import com.github.pakka_papad.R
+import com.github.pakka_papad.data.services.BlacklistService
+import com.github.pakka_papad.data.services.PlaylistService
+import com.github.pakka_papad.util.MessageStore
+import com.github.pakka_papad.util.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import org.jetbrains.annotations.VisibleForTesting
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class SelectPlaylistViewModel @Inject constructor(
- private val context: Application,
- private val manager: DataManager,
+ private val messageStore: MessageStore,
+ private val blacklistService: BlacklistService,
+ private val playlistService: PlaylistService,
) : ViewModel() {
private val _selectList = mutableStateListOf()
val selectList: List = _selectList
- val playlistsWithSongCount = manager.getAll.playlists()
+ val playlistsWithSongCount = playlistService.playlists
.onEach {
while (_selectList.size < it.size) _selectList.add(false)
while (_selectList.size > it.size) _selectList.removeLast()
@@ -36,34 +43,49 @@ class SelectPlaylistViewModel @Inject constructor(
)
fun toggleSelectAtIndex(index: Int) {
+ if (insertState.value !is Resource.Idle) return
if (index >= _selectList.size) return
_selectList[index] = !_selectList[index]
}
+ @VisibleForTesting
+ internal val _insertState = MutableStateFlow>(Resource.Idle())
+ val insertState = _insertState.asStateFlow()
+
fun addSongsToPlaylists(songLocations: Array) {
+ if (insertState.value !is Resource.Idle) return
viewModelScope.launch {
+ _insertState.update { Resource.Loading() }
val playlists = playlistsWithSongCount.value
- val validSongs = songLocations.filter { !manager.blacklistedSongLocations.contains(it) }
- val anyBlacklistedSong = songLocations.any { manager.blacklistedSongLocations.contains(it) }
- val playlistSongCrossRefs = _selectList.indices
- .filter { _selectList[it] }
- .map {
- val list = ArrayList()
- for (songLocation in validSongs) {
- list += PlaylistSongCrossRef(playlists[it].playlistId, songLocation)
- }
- list.toList()
+ val blacklistedSongs = blacklistService.blacklistedSongs
+ .first().asSequence()
+ .map { it.location }
+ .toSet()
+ val validSongs = songLocations.filter { !blacklistedSongs.contains(it) }
+ val anyBlacklistedSong = songLocations.any { blacklistedSongs.contains(it) }
+ var error = false
+ selectList.forEachIndexed { index, isSelected ->
+ if (!isSelected) return@forEachIndexed
+ try {
+ val playlist = playlists[index]
+ playlistService.addSongsToPlaylist(validSongs, playlist.playlistId)
+ } catch (e: Exception) {
+ Timber.e(e)
+ error = true
}
- try {
- manager.insertPlaylistSongCrossRefs(playlistSongCrossRefs.flatten())
- Toast.makeText(context,"Done",Toast.LENGTH_SHORT).show()
- } catch (e: Exception){
- Timber.e(e)
- Toast.makeText(context,"Some error occurred",Toast.LENGTH_SHORT).show()
- } finally {
- if (anyBlacklistedSong){
- Toast.makeText(context,"Blacklisted songs have not been added to playlist",Toast.LENGTH_SHORT).show()
+ }
+ if (!error) {
+ _insertState.update {
+ Resource.Success(
+ messageStore.getString(R.string.done) + if (anyBlacklistedSong) {
+ ". " + messageStore.getString(R.string.blacklisted_songs_have_not_been_added_to_playlist)
+ } else {
+ ""
+ }
+ )
}
+ } else {
+ _insertState.update { Resource.Error(messageStore.getString(R.string.some_error_occurred)) }
}
}
}
diff --git a/app/src/main/java/com/github/pakka_papad/settings/SettingsFragment.kt b/app/src/main/java/com/github/pakka_papad/settings/SettingsFragment.kt
index df323e8..e4466a6 100644
--- a/app/src/main/java/com/github/pakka_papad/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/github/pakka_papad/settings/SettingsFragment.kt
@@ -5,12 +5,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.*
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@@ -34,7 +34,6 @@ class SettingsFragment : Fragment() {
@Inject
lateinit var preferenceProvider: ZenPreferenceProvider
- @OptIn(ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -71,7 +70,7 @@ class SettingsFragment : Fragment() {
topBar = {
TopBarWithBackArrow(
onBackArrowPressed = navController::popBackStack,
- title = "Settings",
+ title = stringResource(R.string.settings),
actions = { }
)
},
diff --git a/app/src/main/java/com/github/pakka_papad/settings/SettingsList.kt b/app/src/main/java/com/github/pakka_papad/settings/SettingsList.kt
index ccd6752..bbe6773 100644
--- a/app/src/main/java/com/github/pakka_papad/settings/SettingsList.kt
+++ b/app/src/main/java/com/github/pakka_papad/settings/SettingsList.kt
@@ -77,7 +77,7 @@ fun SettingsList(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
item {
- GroupTitle(title = "Look and feel")
+ GroupTitle(title = stringResource(R.string.look_and_feel))
}
item {
LookAndFeelSettings(
@@ -90,7 +90,7 @@ fun SettingsList(
)
}
item {
- GroupTitle(title = "Music library")
+ GroupTitle(title = stringResource(R.string.music_library))
}
item {
MusicLibrarySettings(
@@ -101,7 +101,7 @@ fun SettingsList(
)
}
item {
- GroupTitle(title = "Report bug")
+ GroupTitle(title = stringResource(R.string.report_bug))
}
item {
ReportBug(
@@ -169,7 +169,7 @@ private fun LookAndFeelSettings(
Setting(
title = "Material You",
icon = R.drawable.baseline_palette_40,
- description = "Use a theme generated from your device wallpaper",
+ description = stringResource(R.string.use_a_theme_generated_from_your_device_wallpaper),
isChecked = themePreference.useMaterialYou,
onCheckedChanged = {
onPreferenceChanged(themePreference.copy(useMaterialYou = it))
@@ -187,15 +187,15 @@ private fun LookAndFeelSettings(
.alpha(if (themePreference.useMaterialYou) 0.5f else 1f),
)
Setting(
- title = "Theme mode",
+ title = stringResource(R.string.theme_mode),
icon = icon,
onClick = { showSelectorDialog = true },
- description = "Choose a theme mode"
+ description = stringResource(R.string.choose_a_theme_mode)
)
Setting(
- title = "Tabs arrangement",
+ title = stringResource(R.string.tabs_arrangement),
icon = R.drawable.ic_baseline_library_music_40,
- description = "Select and reorder the tabs shown",
+ description = stringResource(R.string.select_and_reorder_the_tabs_shown),
onClick = { showRearrangeTabsDialog = true }
)
}
@@ -237,7 +237,7 @@ private fun AccentSelectorDialog(
AlertDialog(
title = {
Text(
- text = "Accent color",
+ text = stringResource(R.string.accent_color),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
@@ -249,7 +249,7 @@ private fun AccentSelectorDialog(
onClick = onDismissRequest
) {
Text(
- text = "OK",
+ text = stringResource(R.string.save),
style = MaterialTheme.typography.titleMedium
)
}
@@ -331,7 +331,7 @@ private fun ThemeSelectorDialog(
AlertDialog(
title = {
Text(
- text = "App theme",
+ text = stringResource(R.string.theme_mode),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
@@ -343,7 +343,7 @@ private fun ThemeSelectorDialog(
onClick = onDismissRequest
) {
Text(
- text = "OK",
+ text = stringResource(R.string.save),
style = MaterialTheme.typography.titleMedium
)
}
@@ -366,7 +366,7 @@ private fun ThemeSelectorDialog(
}
)
Text(
- text = "Light mode",
+ text = stringResource(R.string.light_mode),
style = MaterialTheme.typography.titleMedium
)
}
@@ -382,7 +382,7 @@ private fun ThemeSelectorDialog(
}
)
Text(
- text = "Dark mode",
+ text = stringResource(R.string.dark_mode),
style = MaterialTheme.typography.titleMedium
)
}
@@ -398,7 +398,7 @@ private fun ThemeSelectorDialog(
}
)
Text(
- text = "System mode",
+ text = stringResource(R.string.system_mode),
style = MaterialTheme.typography.titleMedium
)
}
@@ -419,7 +419,7 @@ private fun RearrangeTabsDialog(
AlertDialog(
title = {
Text(
- text = "App tabs",
+ text = stringResource(R.string.app_tabs),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
@@ -431,7 +431,7 @@ private fun RearrangeTabsDialog(
onClick = onTabsOrderConfirmed
) {
Text(
- text = "OK",
+ text = stringResource(R.string.save),
style = MaterialTheme.typography.titleMedium
)
}
@@ -487,7 +487,7 @@ private fun SelectableMovableScreen(
)
Icon(
painter = painterResource(id = screen.filledIcon),
- contentDescription = "${screen.name} screen icon",
+ contentDescription = stringResource(R.string.screen_icon, screen.name),
modifier = Modifier.size(35.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
@@ -501,7 +501,7 @@ private fun SelectableMovableScreen(
Spacer(spaceModifier)
Icon(
painter = painterResource(id = R.drawable.baseline_drag_indicator_40),
- contentDescription = "move icon",
+ contentDescription = stringResource(R.string.drag_icon),
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
@@ -533,9 +533,9 @@ private fun MusicLibrarySettings(
.animateContentSize(),
) {
Setting(
- title = "Rescan for music",
+ title = stringResource(R.string.rescan_for_music),
icon = Icons.Outlined.Search,
- description = "Search for all the songs on this device and update the library",
+ description = stringResource(R.string.search_for_all_the_songs_on_this_device_and_update_the_library),
onClick = {
if (scanStatus is ScanStatus.ScanNotRunning){
onScanClicked()
@@ -549,19 +549,19 @@ private fun MusicLibrarySettings(
)
} else if (scanStatus is ScanStatus.ScanComplete){
Text(
- text = "Done",
+ text = stringResource(R.string.done),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface
)
}
Setting(
- title = "Restore blacklisted songs",
+ title = stringResource(R.string.restore_blacklisted_songs),
icon = R.drawable.baseline_settings_backup_restore_40,
onClick = onRestoreClicked
)
Setting(
- title = "Restore blacklisted folders",
+ title = stringResource(R.string.restore_blacklisted_folders),
icon = R.drawable.baseline_settings_backup_restore_40,
onClick = onRestoreFoldersClicked
)
@@ -587,16 +587,16 @@ private fun ReportBug(
modifier = Modifier.group()
) {
Setting(
- title = "Auto crash reporting",
+ title = stringResource(R.string.auto_crash_reporting),
icon = R.drawable.baseline_send_40,
- description = "Enable this to automatically send crash reports to the developer",
+ description = stringResource(R.string.enable_this_to_automatically_send_crash_reports_to_the_developer),
isChecked = !disabledCrashlytics,
onCheckedChanged = onAutoReportCrashClicked,
)
Setting(
- title = "Report any bugs/crashes",
+ title = stringResource(R.string.report_any_bugs_crashes),
icon = R.drawable.baseline_bug_report_40,
- description = "Manually report any bugs or crashes you faced",
+ description = stringResource(R.string.manually_report_any_bugs_or_crashes_you_faced),
onClick = {
val intent = Intent(Intent.ACTION_SENDTO)
intent.apply {
@@ -604,7 +604,7 @@ private fun ReportBug(
putExtra(Intent.EXTRA_SUBJECT, "Zen Music | Bug Report")
putExtra(
Intent.EXTRA_TEXT,
- getSystemDetail() + "\n\n[Describe the bug or crash here]"
+ getSystemDetail() + "\n\n[${context.getString(R.string.describe_the_bug_or_crash_here)}]\n"
)
data = Uri.parse("mailto:")
}
@@ -636,14 +636,14 @@ private fun MadeBy(
Text(
text = buildAnnotatedString {
withStyle(semiTransparentSpanStyle) {
- append("App version ")
+ append("${stringResource(R.string.app_version)} ")
}
append(appVersion)
},
style = MaterialTheme.typography.titleSmall,
)
Text(
- text = "Check what's new!",
+ text = stringResource(R.string.check_what_s_new),
modifier = Modifier
.alpha(0.5f)
.clickable(onClick = onWhatsNewClicked),
@@ -653,7 +653,7 @@ private fun MadeBy(
Text(
text = buildAnnotatedString {
withStyle(semiTransparentSpanStyle) {
- append("Made by ")
+ append("${stringResource(R.string.made_by)} ")
}
append("Sumit Bera")
},
@@ -757,7 +757,7 @@ private fun Setting(
) {
Icon(
imageVector = icon,
- contentDescription = null,
+ contentDescription = stringResource(R.string.setting_icon, title),
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
@@ -814,7 +814,7 @@ private fun Setting(
) {
Icon(
painter = painterResource(id = icon),
- contentDescription = null,
+ contentDescription = stringResource(R.string.setting_icon, title),
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
@@ -878,11 +878,11 @@ private fun AccentSetting(
.weight(1f)
){
Text(
- text = "Accent color",
+ text = stringResource(R.string.accent_color),
style = MaterialTheme.typography.titleMedium,
)
Text(
- text = "Choose a theme color",
+ text = stringResource(R.string.choose_an_accent_color),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
)
diff --git a/app/src/main/java/com/github/pakka_papad/settings/SettingsViewModel.kt b/app/src/main/java/com/github/pakka_papad/settings/SettingsViewModel.kt
index 3c80155..34f26b0 100644
--- a/app/src/main/java/com/github/pakka_papad/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/github/pakka_papad/settings/SettingsViewModel.kt
@@ -5,9 +5,9 @@ import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.pakka_papad.Screens
-import com.github.pakka_papad.data.DataManager
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.data.music.ScanStatus
+import com.github.pakka_papad.data.music.SongExtractor
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -20,11 +20,11 @@ import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val context: Application,
- private val manager: DataManager,
+ private val songExtractor: SongExtractor,
private val prefs: ZenPreferenceProvider
) : ViewModel() {
- val scanStatus = manager.scanStatus
+ val scanStatus = songExtractor.scanStatus
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(
@@ -35,7 +35,7 @@ class SettingsViewModel @Inject constructor(
)
fun scanForMusic() {
- manager.scanForMusic()
+ songExtractor.scanForMusic()
}
private val _tabsSelection = MutableStateFlow>>(listOf())
diff --git a/app/src/main/java/com/github/pakka_papad/storage_explorer/FileExplorer.kt b/app/src/main/java/com/github/pakka_papad/storage_explorer/FileExplorer.kt
index a4d71cb..4500ec9 100644
--- a/app/src/main/java/com/github/pakka_papad/storage_explorer/FileExplorer.kt
+++ b/app/src/main/java/com/github/pakka_papad/storage_explorer/FileExplorer.kt
@@ -9,8 +9,8 @@ class MusicFileExplorer(
private val songExtractor: SongExtractor
) {
- val root = Environment.getExternalStorageDirectory().absolutePath
- var currentPath = root
+ private val root = Environment.getExternalStorageDirectory().absolutePath
+ private var currentPath = root
val isRoot: Boolean
get() {
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/DefaultTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/DefaultTheme.kt
new file mode 100644
index 0000000..fac804d
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/DefaultTheme.kt
@@ -0,0 +1,72 @@
+package com.github.pakka_papad.ui.accent_colours
+
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.ui.graphics.Color
+
+val DefaultLightColors by lazy {
+ lightColorScheme(
+ primary = Color(0xFF006D40),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFF69FDAD),
+ onPrimaryContainer = Color(0xFF002110),
+ secondary = Color(0xFF4E6354),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFFD1E8D5),
+ onSecondaryContainer = Color(0xFF0C1F14),
+ tertiary = Color(0xFF3B6470),
+ onTertiary = Color(0xFFFFFFFF),
+ tertiaryContainer = Color(0xFFBFE9F8),
+ onTertiaryContainer = Color(0xFF001F27),
+ error = Color(0xFFBA1A1A),
+ errorContainer = Color(0xFFFFDAD6),
+ onError = Color(0xFFFFFFFF),
+ onErrorContainer = Color(0xFF410002),
+ background = Color(0xFFFBFDF8),
+ onBackground = Color(0xFF191C1A),
+ surface = Color(0xFFFBFDF8),
+ onSurface = Color(0xFF191C1A),
+ surfaceVariant = Color(0xFFDCE5DB),
+ onSurfaceVariant = Color(0xFF414942),
+ outline = Color(0xFF717972),
+ inverseOnSurface = Color(0xFFF0F1EC),
+ inverseSurface = Color(0xFF2E312E),
+ inversePrimary = Color(0xFF47E093),
+ surfaceTint = Color(0xFF006D40),
+ )
+}
+
+
+val DefaultDarkColors by lazy {
+ darkColorScheme(
+ primary = Color(0xFF47E093),
+ onPrimary = Color(0xFF00391F),
+ primaryContainer = Color(0xFF00522F),
+ onPrimaryContainer = Color(0xFF69FDAD),
+ secondary = Color(0xFFB5CCBA),
+ onSecondary = Color(0xFF213528),
+ secondaryContainer = Color(0xFF374B3D),
+ onSecondaryContainer = Color(0xFFD1E8D5),
+ tertiary = Color(0xFFA3CDDB),
+ onTertiary = Color(0xFF033541),
+ tertiaryContainer = Color(0xFF224C58),
+ onTertiaryContainer = Color(0xFFBFE9F8),
+ error = Color(0xFFFFB4AB),
+ errorContainer = Color(0xFF93000A),
+ onError = Color(0xFF690005),
+ onErrorContainer = Color(0xFFFFDAD6),
+ background = Color(0xFF191C1A),
+ onBackground = Color(0xFFE1E3DE),
+ surface = Color(0xFF191C1A),
+ onSurface = Color(0xFFE1E3DE),
+ surfaceVariant = Color(0xFF414942),
+ onSurfaceVariant = Color(0xFFC0C9C0),
+ outline = Color(0xFF8A938B),
+ inverseOnSurface = Color(0xFF191C1A),
+ inverseSurface = Color(0xFFE1E3DE),
+ inversePrimary = Color(0xFF006D40),
+ surfaceTint = Color(0xFF47E093),
+ )
+}
+
+val default_seed by lazy { Color(0xFF17C379) }
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/ElmTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/ElmTheme.kt
new file mode 100644
index 0000000..5f734c6
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/ElmTheme.kt
@@ -0,0 +1,73 @@
+package com.github.pakka_papad.ui.accent_colours
+
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.ui.graphics.Color
+
+
+val ElmLightColors by lazy {
+ lightColorScheme(
+ primary = Color(0xFF006972),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFF8BF2FF),
+ onPrimaryContainer = Color(0xFF001F23),
+ secondary = Color(0xFF4A6366),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFFCDE7EB),
+ onSecondaryContainer = Color(0xFF051F22),
+ tertiary = Color(0xFF515E7D),
+ onTertiary = Color(0xFFFFFFFF),
+ tertiaryContainer = Color(0xFFD8E2FF),
+ onTertiaryContainer = Color(0xFF0C1B36),
+ error = Color(0xFFBA1A1A),
+ errorContainer = Color(0xFFFFDAD6),
+ onError = Color(0xFFFFFFFF),
+ onErrorContainer = Color(0xFF410002),
+ background = Color(0xFFFAFDFD),
+ onBackground = Color(0xFF191C1D),
+ surface = Color(0xFFFAFDFD),
+ onSurface = Color(0xFF191C1D),
+ surfaceVariant = Color(0xFFDAE4E6),
+ onSurfaceVariant = Color(0xFF3F484A),
+ outline = Color(0xFF6F797A),
+ inverseOnSurface = Color(0xFFEFF1F1),
+ inverseSurface = Color(0xFF2D3131),
+ inversePrimary = Color(0xFF4ED8E8),
+ surfaceTint = Color(0xFF006972),
+ )
+}
+
+
+val ElmDarkColors by lazy {
+ darkColorScheme(
+ primary = Color(0xFF4ED8E8),
+ onPrimary = Color(0xFF00363C),
+ primaryContainer = Color(0xFF004F56),
+ onPrimaryContainer = Color(0xFF8BF2FF),
+ secondary = Color(0xFFB1CBCF),
+ onSecondary = Color(0xFF1C3437),
+ secondaryContainer = Color(0xFF324B4E),
+ onSecondaryContainer = Color(0xFFCDE7EB),
+ tertiary = Color(0xFFB9C6EA),
+ onTertiary = Color(0xFF22304D),
+ tertiaryContainer = Color(0xFF394664),
+ onTertiaryContainer = Color(0xFFD8E2FF),
+ error = Color(0xFFFFB4AB),
+ errorContainer = Color(0xFF93000A),
+ onError = Color(0xFF690005),
+ onErrorContainer = Color(0xFFFFDAD6),
+ background = Color(0xFF191C1D),
+ onBackground = Color(0xFFE0E3E3),
+ surface = Color(0xFF191C1D),
+ onSurface = Color(0xFFE0E3E3),
+ surfaceVariant = Color(0xFF3F484A),
+ onSurfaceVariant = Color(0xFFBEC8CA),
+ outline = Color(0xFF899294),
+ inverseOnSurface = Color(0xFF191C1D),
+ inverseSurface = Color(0xFFE0E3E3),
+ inversePrimary = Color(0xFF006972),
+ surfaceTint = Color(0xFF4ED8E8),
+ )
+}
+
+val elm_seed by lazy { Color(0xFF247881) }
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/JacksonsPurpleTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/JacksonsPurpleTheme.kt
new file mode 100644
index 0000000..1c1788b
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/JacksonsPurpleTheme.kt
@@ -0,0 +1,71 @@
+package com.github.pakka_papad.ui.accent_colours
+
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.ui.graphics.Color
+
+val JacksonsPurpleLightColors by lazy {
+ lightColorScheme(
+ primary = Color(0xFF4853BD),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFFDFE0FF),
+ onPrimaryContainer = Color(0xFF000767),
+ secondary = Color(0xFF5C5D72),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFFE1E0F9),
+ onSecondaryContainer = Color(0xFF181A2C),
+ tertiary = Color(0xFF78536B),
+ onTertiary = Color(0xFFFFFFFF),
+ tertiaryContainer = Color(0xFFFFD7EF),
+ onTertiaryContainer = Color(0xFF2E1126),
+ error = Color(0xFFBA1A1A),
+ errorContainer = Color(0xFFFFDAD6),
+ onError = Color(0xFFFFFFFF),
+ onErrorContainer = Color(0xFF410002),
+ background = Color(0xFFFFFBFF),
+ onBackground = Color(0xFF1B1B1F),
+ surface = Color(0xFFFFFBFF),
+ onSurface = Color(0xFF1B1B1F),
+ surfaceVariant = Color(0xFFE3E1EC),
+ onSurfaceVariant = Color(0xFF46464F),
+ outline = Color(0xFF777680),
+ inverseOnSurface = Color(0xFFF3F0F4),
+ inverseSurface = Color(0xFF303034),
+ inversePrimary = Color(0xFFBDC2FF),
+ surfaceTint = Color(0xFF4853BD),
+ )
+}
+
+val JacksonsPurpleDarkColors by lazy {
+ darkColorScheme(
+ primary = Color(0xFFBDC2FF),
+ onPrimary = Color(0xFF121E8E),
+ primaryContainer = Color(0xFF2E39A4),
+ onPrimaryContainer = Color(0xFFDFE0FF),
+ secondary = Color(0xFFC4C4DD),
+ onSecondary = Color(0xFF2D2F42),
+ secondaryContainer = Color(0xFF444559),
+ onSecondaryContainer = Color(0xFFE1E0F9),
+ tertiary = Color(0xFFE7B9D6),
+ onTertiary = Color(0xFF45263C),
+ tertiaryContainer = Color(0xFF5E3C53),
+ onTertiaryContainer = Color(0xFFFFD7EF),
+ error = Color(0xFFFFB4AB),
+ errorContainer = Color(0xFF93000A),
+ onError = Color(0xFF690005),
+ onErrorContainer = Color(0xFFFFDAD6),
+ background = Color(0xFF1B1B1F),
+ onBackground = Color(0xFFE4E1E6),
+ surface = Color(0xFF1B1B1F),
+ onSurface = Color(0xFFE4E1E6),
+ surfaceVariant = Color(0xFF46464F),
+ onSurfaceVariant = Color(0xFFC7C5D0),
+ outline = Color(0xFF91909A),
+ inverseOnSurface = Color(0xFF1B1B1F),
+ inverseSurface = Color(0xFFE4E1E6),
+ inversePrimary = Color(0xFF4853BD),
+ surfaceTint = Color(0xFFBDC2FF),
+ )
+}
+
+val jacksons_purple_seed by lazy { Color(0xFF242F9B) }
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MagentaTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MagentaTheme.kt
new file mode 100644
index 0000000..cfa3d66
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MagentaTheme.kt
@@ -0,0 +1,71 @@
+package com.github.pakka_papad.ui.accent_colours
+
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.ui.graphics.Color
+
+val MagentaLightColors by lazy {
+ lightColorScheme(
+ primary = Color(0xFFA62E72),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFFFFD8E7),
+ onPrimaryContainer = Color(0xFF3D0025),
+ secondary = Color(0xFF725762),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFFFED9E6),
+ onSecondaryContainer = Color(0xFF2A151F),
+ tertiary = Color(0xFF7E5539),
+ onTertiary = Color(0xFFFFFFFF),
+ tertiaryContainer = Color(0xFFFFDCC7),
+ onTertiaryContainer = Color(0xFF301401),
+ error = Color(0xFFBA1A1A),
+ errorContainer = Color(0xFFFFDAD6),
+ onError = Color(0xFFFFFFFF),
+ onErrorContainer = Color(0xFF410002),
+ background = Color(0xFFFFFBFF),
+ onBackground = Color(0xFF1F1A1C),
+ surface = Color(0xFFFFFBFF),
+ onSurface = Color(0xFF1F1A1C),
+ surfaceVariant = Color(0xFFF1DEE3),
+ onSurfaceVariant = Color(0xFF504348),
+ outline = Color(0xFF827378),
+ inverseOnSurface = Color(0xFFF9EEF0),
+ inverseSurface = Color(0xFF352F31),
+ inversePrimary = Color(0xFFFFAFD2),
+ surfaceTint = Color(0xFFA62E72),
+ )
+}
+
+val MagentaDarkColors by lazy {
+ darkColorScheme(
+ primary = Color(0xFFFFAFD2),
+ onPrimary = Color(0xFF63003F),
+ primaryContainer = Color(0xFF870F59),
+ onPrimaryContainer = Color(0xFFFFD8E7),
+ secondary = Color(0xFFE0BDCA),
+ onSecondary = Color(0xFF412A34),
+ secondaryContainer = Color(0xFF59404A),
+ onSecondaryContainer = Color(0xFFFED9E6),
+ tertiary = Color(0xFFF2BB98),
+ onTertiary = Color(0xFF492810),
+ tertiaryContainer = Color(0xFF633E24),
+ onTertiaryContainer = Color(0xFFFFDCC7),
+ error = Color(0xFFFFB4AB),
+ errorContainer = Color(0xFF93000A),
+ onError = Color(0xFF690005),
+ onErrorContainer = Color(0xFFFFDAD6),
+ background = Color(0xFF1F1A1C),
+ onBackground = Color(0xFFEBE0E2),
+ surface = Color(0xFF1F1A1C),
+ onSurface = Color(0xFFEBE0E2),
+ surfaceVariant = Color(0xFF504348),
+ onSurfaceVariant = Color(0xFFD4C2C7),
+ outline = Color(0xFF9D8C92),
+ inverseOnSurface = Color(0xFF1F1A1C),
+ inverseSurface = Color(0xFFEBE0E2),
+ inversePrimary = Color(0xFFA62E72),
+ surfaceTint = Color(0xFFFFAFD2),
+ )
+}
+
+val magenta_seed by lazy { Color(0xFFF56EB3) }
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MalibuTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MalibuTheme.kt
new file mode 100644
index 0000000..68ea00a
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MalibuTheme.kt
@@ -0,0 +1,72 @@
+package com.github.pakka_papad.ui.accent_colours
+
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.ui.graphics.Color
+
+
+val MalibuLightColors by lazy {
+ lightColorScheme(
+ primary = Color(0xFF00658E),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFFC7E7FF),
+ onPrimaryContainer = Color(0xFF001E2E),
+ secondary = Color(0xFF4F616E),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFFD2E5F5),
+ onSecondaryContainer = Color(0xFF0B1D29),
+ tertiary = Color(0xFF63597C),
+ onTertiary = Color(0xFFFFFFFF),
+ tertiaryContainer = Color(0xFFE8DDFF),
+ onTertiaryContainer = Color(0xFF1E1635),
+ error = Color(0xFFBA1A1A),
+ errorContainer = Color(0xFFFFDAD6),
+ onError = Color(0xFFFFFFFF),
+ onErrorContainer = Color(0xFF410002),
+ background = Color(0xFFFCFCFF),
+ onBackground = Color(0xFF191C1E),
+ surface = Color(0xFFFCFCFF),
+ onSurface = Color(0xFF191C1E),
+ surfaceVariant = Color(0xFFDDE3EA),
+ onSurfaceVariant = Color(0xFF41484D),
+ outline = Color(0xFF71787E),
+ inverseOnSurface = Color(0xFFF0F1F3),
+ inverseSurface = Color(0xFF2E3133),
+ inversePrimary = Color(0xFF84CFFF),
+ surfaceTint = Color(0xFF00658E),
+ )
+}
+
+val MalibuDarkColors by lazy {
+ darkColorScheme(
+ primary = Color(0xFF84CFFF),
+ onPrimary = Color(0xFF00344C),
+ primaryContainer = Color(0xFF004C6C),
+ onPrimaryContainer = Color(0xFFC7E7FF),
+ secondary = Color(0xFFB6C9D8),
+ onSecondary = Color(0xFF21323E),
+ secondaryContainer = Color(0xFF374955),
+ onSecondaryContainer = Color(0xFFD2E5F5),
+ tertiary = Color(0xFFCDC0E9),
+ onTertiary = Color(0xFF342B4B),
+ tertiaryContainer = Color(0xFF4B4263),
+ onTertiaryContainer = Color(0xFFE8DDFF),
+ error = Color(0xFFFFB4AB),
+ errorContainer = Color(0xFF93000A),
+ onError = Color(0xFF690005),
+ onErrorContainer = Color(0xFFFFDAD6),
+ background = Color(0xFF191C1E),
+ onBackground = Color(0xFFE2E2E5),
+ surface = Color(0xFF191C1E),
+ onSurface = Color(0xFFE2E2E5),
+ surfaceVariant = Color(0xFF41484D),
+ onSurfaceVariant = Color(0xFFC1C7CE),
+ outline = Color(0xFF8B9198),
+ inverseOnSurface = Color(0xFF191C1E),
+ inverseSurface = Color(0xFFE2E2E5),
+ inversePrimary = Color(0xFF00658E),
+ surfaceTint = Color(0xFF84CFFF),
+ )
+}
+
+val malibu_seed by lazy { Color(0xFF89CFFD) }
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MelroseTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MelroseTheme.kt
new file mode 100644
index 0000000..a3bb520
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/MelroseTheme.kt
@@ -0,0 +1,72 @@
+package com.github.pakka_papad.ui.accent_colours
+
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.ui.graphics.Color
+
+val MelroseLightColors by lazy {
+ lightColorScheme(
+ primary = Color(0xFF5D52A9),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFFE5DEFF),
+ onPrimaryContainer = Color(0xFF180164),
+ secondary = Color(0xFF5F5C71),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFFE5DFF9),
+ onSecondaryContainer = Color(0xFF1B192C),
+ tertiary = Color(0xFF7B5265),
+ onTertiary = Color(0xFFFFFFFF),
+ tertiaryContainer = Color(0xFFFFD8E7),
+ onTertiaryContainer = Color(0xFF301121),
+ error = Color(0xFFBA1A1A),
+ errorContainer = Color(0xFFFFDAD6),
+ onError = Color(0xFFFFFFFF),
+ onErrorContainer = Color(0xFF410002),
+ background = Color(0xFFFFFBFF),
+ onBackground = Color(0xFF1C1B1F),
+ surface = Color(0xFFFFFBFF),
+ onSurface = Color(0xFF1C1B1F),
+ surfaceVariant = Color(0xFFE5E0EC),
+ onSurfaceVariant = Color(0xFF47464F),
+ outline = Color(0xFF78767F),
+ inverseOnSurface = Color(0xFFF4EFF4),
+ inverseSurface = Color(0xFF313034),
+ inversePrimary = Color(0xFFC7BFFF),
+ surfaceTint = Color(0xFF5D52A9),
+ )
+}
+
+val MelroseDarkColors by lazy {
+ darkColorScheme(
+ primary = Color(0xFFC7BFFF),
+ onPrimary = Color(0xFF2E2177),
+ primaryContainer = Color(0xFF453A8F),
+ onPrimaryContainer = Color(0xFFE5DEFF),
+ secondary = Color(0xFFC8C3DC),
+ onSecondary = Color(0xFF312E41),
+ secondaryContainer = Color(0xFF474459),
+ onSecondaryContainer = Color(0xFFE5DFF9),
+ tertiary = Color(0xFFECB8CE),
+ onTertiary = Color(0xFF482536),
+ tertiaryContainer = Color(0xFF613B4D),
+ onTertiaryContainer = Color(0xFFFFD8E7),
+ error = Color(0xFFFFB4AB),
+ errorContainer = Color(0xFF93000A),
+ onError = Color(0xFF690005),
+ onErrorContainer = Color(0xFFFFDAD6),
+ background = Color(0xFF1C1B1F),
+ onBackground = Color(0xFFE5E1E6),
+ surface = Color(0xFF1C1B1F),
+ onSurface = Color(0xFFE5E1E6),
+ surfaceVariant = Color(0xFF47464F),
+ onSurfaceVariant = Color(0xFFC9C5D0),
+ outline = Color(0xFF928F99),
+ inverseOnSurface = Color(0xFF1C1B1F),
+ inverseSurface = Color(0xFFE5E1E6),
+ inversePrimary = Color(0xFF5D52A9),
+ surfaceTint = Color(0xFFC7BFFF),
+ )
+}
+
+val melrose_seed by lazy { Color(0xFFADA2FF) }
+
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/default/DefaultColor.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/default/DefaultColor.kt
deleted file mode 100644
index 427ee5c..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/default/DefaultColor.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.default
-
-import androidx.compose.ui.graphics.Color
-
-val default_theme_light_primary = Color(0xFF006D40)
-val default_theme_light_onPrimary = Color(0xFFFFFFFF)
-val default_theme_light_primaryContainer = Color(0xFF69FDAD)
-val default_theme_light_onPrimaryContainer = Color(0xFF002110)
-val default_theme_light_secondary = Color(0xFF4E6354)
-val default_theme_light_onSecondary = Color(0xFFFFFFFF)
-val default_theme_light_secondaryContainer = Color(0xFFD1E8D5)
-val default_theme_light_onSecondaryContainer = Color(0xFF0C1F14)
-val default_theme_light_tertiary = Color(0xFF3B6470)
-val default_theme_light_onTertiary = Color(0xFFFFFFFF)
-val default_theme_light_tertiaryContainer = Color(0xFFBFE9F8)
-val default_theme_light_onTertiaryContainer = Color(0xFF001F27)
-val default_theme_light_error = Color(0xFFBA1A1A)
-val default_theme_light_errorContainer = Color(0xFFFFDAD6)
-val default_theme_light_onError = Color(0xFFFFFFFF)
-val default_theme_light_onErrorContainer = Color(0xFF410002)
-val default_theme_light_background = Color(0xFFFBFDF8)
-val default_theme_light_onBackground = Color(0xFF191C1A)
-val default_theme_light_surface = Color(0xFFFBFDF8)
-val default_theme_light_onSurface = Color(0xFF191C1A)
-val default_theme_light_surfaceVariant = Color(0xFFDCE5DB)
-val default_theme_light_onSurfaceVariant = Color(0xFF414942)
-val default_theme_light_outline = Color(0xFF717972)
-val default_theme_light_inverseOnSurface = Color(0xFFF0F1EC)
-val default_theme_light_inverseSurface = Color(0xFF2E312E)
-val default_theme_light_inversePrimary = Color(0xFF47E093)
-val default_theme_light_surfaceTint = Color(0xFF006D40)
-
-val default_theme_dark_primary = Color(0xFF47E093)
-val default_theme_dark_onPrimary = Color(0xFF00391F)
-val default_theme_dark_primaryContainer = Color(0xFF00522F)
-val default_theme_dark_onPrimaryContainer = Color(0xFF69FDAD)
-val default_theme_dark_secondary = Color(0xFFB5CCBA)
-val default_theme_dark_onSecondary = Color(0xFF213528)
-val default_theme_dark_secondaryContainer = Color(0xFF374B3D)
-val default_theme_dark_onSecondaryContainer = Color(0xFFD1E8D5)
-val default_theme_dark_tertiary = Color(0xFFA3CDDB)
-val default_theme_dark_onTertiary = Color(0xFF033541)
-val default_theme_dark_tertiaryContainer = Color(0xFF224C58)
-val default_theme_dark_onTertiaryContainer = Color(0xFFBFE9F8)
-val default_theme_dark_error = Color(0xFFFFB4AB)
-val default_theme_dark_errorContainer = Color(0xFF93000A)
-val default_theme_dark_onError = Color(0xFF690005)
-val default_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
-val default_theme_dark_background = Color(0xFF191C1A)
-val default_theme_dark_onBackground = Color(0xFFE1E3DE)
-val default_theme_dark_surface = Color(0xFF191C1A)
-val default_theme_dark_onSurface = Color(0xFFE1E3DE)
-val default_theme_dark_surfaceVariant = Color(0xFF414942)
-val default_theme_dark_onSurfaceVariant = Color(0xFFC0C9C0)
-val default_theme_dark_outline = Color(0xFF8A938B)
-val default_theme_dark_inverseOnSurface = Color(0xFF191C1A)
-val default_theme_dark_inverseSurface = Color(0xFFE1E3DE)
-val default_theme_dark_inversePrimary = Color(0xFF006D40)
-val default_theme_dark_surfaceTint = Color(0xFF47E093)
-
-
-val default_seed = Color(0xFF17C379)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/default/DefaultTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/default/DefaultTheme.kt
deleted file mode 100644
index b7b8c7e..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/default/DefaultTheme.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.default
-
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.lightColorScheme
-
-val DefaultLightColors = lightColorScheme(
- primary = default_theme_light_primary,
- onPrimary = default_theme_light_onPrimary,
- primaryContainer = default_theme_light_primaryContainer,
- onPrimaryContainer = default_theme_light_onPrimaryContainer,
- secondary = default_theme_light_secondary,
- onSecondary = default_theme_light_onSecondary,
- secondaryContainer = default_theme_light_secondaryContainer,
- onSecondaryContainer = default_theme_light_onSecondaryContainer,
- tertiary = default_theme_light_tertiary,
- onTertiary = default_theme_light_onTertiary,
- tertiaryContainer = default_theme_light_tertiaryContainer,
- onTertiaryContainer = default_theme_light_onTertiaryContainer,
- error = default_theme_light_error,
- errorContainer = default_theme_light_errorContainer,
- onError = default_theme_light_onError,
- onErrorContainer = default_theme_light_onErrorContainer,
- background = default_theme_light_background,
- onBackground = default_theme_light_onBackground,
- surface = default_theme_light_surface,
- onSurface = default_theme_light_onSurface,
- surfaceVariant = default_theme_light_surfaceVariant,
- onSurfaceVariant = default_theme_light_onSurfaceVariant,
- outline = default_theme_light_outline,
- inverseOnSurface = default_theme_light_inverseOnSurface,
- inverseSurface = default_theme_light_inverseSurface,
- inversePrimary = default_theme_light_inversePrimary,
- surfaceTint = default_theme_light_surfaceTint,
-)
-
-
-val DefaultDarkColors = darkColorScheme(
- primary = default_theme_dark_primary,
- onPrimary = default_theme_dark_onPrimary,
- primaryContainer = default_theme_dark_primaryContainer,
- onPrimaryContainer = default_theme_dark_onPrimaryContainer,
- secondary = default_theme_dark_secondary,
- onSecondary = default_theme_dark_onSecondary,
- secondaryContainer = default_theme_dark_secondaryContainer,
- onSecondaryContainer = default_theme_dark_onSecondaryContainer,
- tertiary = default_theme_dark_tertiary,
- onTertiary = default_theme_dark_onTertiary,
- tertiaryContainer = default_theme_dark_tertiaryContainer,
- onTertiaryContainer = default_theme_dark_onTertiaryContainer,
- error = default_theme_dark_error,
- errorContainer = default_theme_dark_errorContainer,
- onError = default_theme_dark_onError,
- onErrorContainer = default_theme_dark_onErrorContainer,
- background = default_theme_dark_background,
- onBackground = default_theme_dark_onBackground,
- surface = default_theme_dark_surface,
- onSurface = default_theme_dark_onSurface,
- surfaceVariant = default_theme_dark_surfaceVariant,
- onSurfaceVariant = default_theme_dark_onSurfaceVariant,
- outline = default_theme_dark_outline,
- inverseOnSurface = default_theme_dark_inverseOnSurface,
- inverseSurface = default_theme_dark_inverseSurface,
- inversePrimary = default_theme_dark_inversePrimary,
- surfaceTint = default_theme_dark_surfaceTint,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/elm/ElmColor.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/elm/ElmColor.kt
deleted file mode 100644
index e158f41..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/elm/ElmColor.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.elm
-import androidx.compose.ui.graphics.Color
-
-val elm_theme_light_primary = Color(0xFF006972)
-val elm_theme_light_onPrimary = Color(0xFFFFFFFF)
-val elm_theme_light_primaryContainer = Color(0xFF8BF2FF)
-val elm_theme_light_onPrimaryContainer = Color(0xFF001F23)
-val elm_theme_light_secondary = Color(0xFF4A6366)
-val elm_theme_light_onSecondary = Color(0xFFFFFFFF)
-val elm_theme_light_secondaryContainer = Color(0xFFCDE7EB)
-val elm_theme_light_onSecondaryContainer = Color(0xFF051F22)
-val elm_theme_light_tertiary = Color(0xFF515E7D)
-val elm_theme_light_onTertiary = Color(0xFFFFFFFF)
-val elm_theme_light_tertiaryContainer = Color(0xFFD8E2FF)
-val elm_theme_light_onTertiaryContainer = Color(0xFF0C1B36)
-val elm_theme_light_error = Color(0xFFBA1A1A)
-val elm_theme_light_errorContainer = Color(0xFFFFDAD6)
-val elm_theme_light_onError = Color(0xFFFFFFFF)
-val elm_theme_light_onErrorContainer = Color(0xFF410002)
-val elm_theme_light_background = Color(0xFFFAFDFD)
-val elm_theme_light_onBackground = Color(0xFF191C1D)
-val elm_theme_light_surface = Color(0xFFFAFDFD)
-val elm_theme_light_onSurface = Color(0xFF191C1D)
-val elm_theme_light_surfaceVariant = Color(0xFFDAE4E6)
-val elm_theme_light_onSurfaceVariant = Color(0xFF3F484A)
-val elm_theme_light_outline = Color(0xFF6F797A)
-val elm_theme_light_inverseOnSurface = Color(0xFFEFF1F1)
-val elm_theme_light_inverseSurface = Color(0xFF2D3131)
-val elm_theme_light_inversePrimary = Color(0xFF4ED8E8)
-val elm_theme_light_surfaceTint = Color(0xFF006972)
-
-val elm_theme_dark_primary = Color(0xFF4ED8E8)
-val elm_theme_dark_onPrimary = Color(0xFF00363C)
-val elm_theme_dark_primaryContainer = Color(0xFF004F56)
-val elm_theme_dark_onPrimaryContainer = Color(0xFF8BF2FF)
-val elm_theme_dark_secondary = Color(0xFFB1CBCF)
-val elm_theme_dark_onSecondary = Color(0xFF1C3437)
-val elm_theme_dark_secondaryContainer = Color(0xFF324B4E)
-val elm_theme_dark_onSecondaryContainer = Color(0xFFCDE7EB)
-val elm_theme_dark_tertiary = Color(0xFFB9C6EA)
-val elm_theme_dark_onTertiary = Color(0xFF22304D)
-val elm_theme_dark_tertiaryContainer = Color(0xFF394664)
-val elm_theme_dark_onTertiaryContainer = Color(0xFFD8E2FF)
-val elm_theme_dark_error = Color(0xFFFFB4AB)
-val elm_theme_dark_errorContainer = Color(0xFF93000A)
-val elm_theme_dark_onError = Color(0xFF690005)
-val elm_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
-val elm_theme_dark_background = Color(0xFF191C1D)
-val elm_theme_dark_onBackground = Color(0xFFE0E3E3)
-val elm_theme_dark_surface = Color(0xFF191C1D)
-val elm_theme_dark_onSurface = Color(0xFFE0E3E3)
-val elm_theme_dark_surfaceVariant = Color(0xFF3F484A)
-val elm_theme_dark_onSurfaceVariant = Color(0xFFBEC8CA)
-val elm_theme_dark_outline = Color(0xFF899294)
-val elm_theme_dark_inverseOnSurface = Color(0xFF191C1D)
-val elm_theme_dark_inverseSurface = Color(0xFFE0E3E3)
-val elm_theme_dark_inversePrimary = Color(0xFF006972)
-val elm_theme_dark_surfaceTint = Color(0xFF4ED8E8)
-
-val elm_seed = Color(0xFF247881)
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/elm/ElmTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/elm/ElmTheme.kt
deleted file mode 100644
index 2bb5741..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/elm/ElmTheme.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.elm
-
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.material3.darkColorScheme
-
-
-val ElmLightColors = lightColorScheme(
- primary = elm_theme_light_primary,
- onPrimary = elm_theme_light_onPrimary,
- primaryContainer = elm_theme_light_primaryContainer,
- onPrimaryContainer = elm_theme_light_onPrimaryContainer,
- secondary = elm_theme_light_secondary,
- onSecondary = elm_theme_light_onSecondary,
- secondaryContainer = elm_theme_light_secondaryContainer,
- onSecondaryContainer = elm_theme_light_onSecondaryContainer,
- tertiary = elm_theme_light_tertiary,
- onTertiary = elm_theme_light_onTertiary,
- tertiaryContainer = elm_theme_light_tertiaryContainer,
- onTertiaryContainer = elm_theme_light_onTertiaryContainer,
- error = elm_theme_light_error,
- errorContainer = elm_theme_light_errorContainer,
- onError = elm_theme_light_onError,
- onErrorContainer = elm_theme_light_onErrorContainer,
- background = elm_theme_light_background,
- onBackground = elm_theme_light_onBackground,
- surface = elm_theme_light_surface,
- onSurface = elm_theme_light_onSurface,
- surfaceVariant = elm_theme_light_surfaceVariant,
- onSurfaceVariant = elm_theme_light_onSurfaceVariant,
- outline = elm_theme_light_outline,
- inverseOnSurface = elm_theme_light_inverseOnSurface,
- inverseSurface = elm_theme_light_inverseSurface,
- inversePrimary = elm_theme_light_inversePrimary,
- surfaceTint = elm_theme_light_surfaceTint,
-)
-
-
-val ElmDarkColors = darkColorScheme(
- primary = elm_theme_dark_primary,
- onPrimary = elm_theme_dark_onPrimary,
- primaryContainer = elm_theme_dark_primaryContainer,
- onPrimaryContainer = elm_theme_dark_onPrimaryContainer,
- secondary = elm_theme_dark_secondary,
- onSecondary = elm_theme_dark_onSecondary,
- secondaryContainer = elm_theme_dark_secondaryContainer,
- onSecondaryContainer = elm_theme_dark_onSecondaryContainer,
- tertiary = elm_theme_dark_tertiary,
- onTertiary = elm_theme_dark_onTertiary,
- tertiaryContainer = elm_theme_dark_tertiaryContainer,
- onTertiaryContainer = elm_theme_dark_onTertiaryContainer,
- error = elm_theme_dark_error,
- errorContainer = elm_theme_dark_errorContainer,
- onError = elm_theme_dark_onError,
- onErrorContainer = elm_theme_dark_onErrorContainer,
- background = elm_theme_dark_background,
- onBackground = elm_theme_dark_onBackground,
- surface = elm_theme_dark_surface,
- onSurface = elm_theme_dark_onSurface,
- surfaceVariant = elm_theme_dark_surfaceVariant,
- onSurfaceVariant = elm_theme_dark_onSurfaceVariant,
- outline = elm_theme_dark_outline,
- inverseOnSurface = elm_theme_dark_inverseOnSurface,
- inverseSurface = elm_theme_dark_inverseSurface,
- inversePrimary = elm_theme_dark_inversePrimary,
- surfaceTint = elm_theme_dark_surfaceTint,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/jacksons_purple/JacksonsPurpleColor.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/jacksons_purple/JacksonsPurpleColor.kt
deleted file mode 100644
index e2d0dde..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/jacksons_purple/JacksonsPurpleColor.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.jacksons_purple
-import androidx.compose.ui.graphics.Color
-
-val jacksons_purple_theme_light_primary = Color(0xFF4853BD)
-val jacksons_purple_theme_light_onPrimary = Color(0xFFFFFFFF)
-val jacksons_purple_theme_light_primaryContainer = Color(0xFFDFE0FF)
-val jacksons_purple_theme_light_onPrimaryContainer = Color(0xFF000767)
-val jacksons_purple_theme_light_secondary = Color(0xFF5C5D72)
-val jacksons_purple_theme_light_onSecondary = Color(0xFFFFFFFF)
-val jacksons_purple_theme_light_secondaryContainer = Color(0xFFE1E0F9)
-val jacksons_purple_theme_light_onSecondaryContainer = Color(0xFF181A2C)
-val jacksons_purple_theme_light_tertiary = Color(0xFF78536B)
-val jacksons_purple_theme_light_onTertiary = Color(0xFFFFFFFF)
-val jacksons_purple_theme_light_tertiaryContainer = Color(0xFFFFD7EF)
-val jacksons_purple_theme_light_onTertiaryContainer = Color(0xFF2E1126)
-val jacksons_purple_theme_light_error = Color(0xFFBA1A1A)
-val jacksons_purple_theme_light_errorContainer = Color(0xFFFFDAD6)
-val jacksons_purple_theme_light_onError = Color(0xFFFFFFFF)
-val jacksons_purple_theme_light_onErrorContainer = Color(0xFF410002)
-val jacksons_purple_theme_light_background = Color(0xFFFFFBFF)
-val jacksons_purple_theme_light_onBackground = Color(0xFF1B1B1F)
-val jacksons_purple_theme_light_surface = Color(0xFFFFFBFF)
-val jacksons_purple_theme_light_onSurface = Color(0xFF1B1B1F)
-val jacksons_purple_theme_light_surfaceVariant = Color(0xFFE3E1EC)
-val jacksons_purple_theme_light_onSurfaceVariant = Color(0xFF46464F)
-val jacksons_purple_theme_light_outline = Color(0xFF777680)
-val jacksons_purple_theme_light_inverseOnSurface = Color(0xFFF3F0F4)
-val jacksons_purple_theme_light_inverseSurface = Color(0xFF303034)
-val jacksons_purple_theme_light_inversePrimary = Color(0xFFBDC2FF)
-val jacksons_purple_theme_light_surfaceTint = Color(0xFF4853BD)
-
-val jacksons_purple_theme_dark_primary = Color(0xFFBDC2FF)
-val jacksons_purple_theme_dark_onPrimary = Color(0xFF121E8E)
-val jacksons_purple_theme_dark_primaryContainer = Color(0xFF2E39A4)
-val jacksons_purple_theme_dark_onPrimaryContainer = Color(0xFFDFE0FF)
-val jacksons_purple_theme_dark_secondary = Color(0xFFC4C4DD)
-val jacksons_purple_theme_dark_onSecondary = Color(0xFF2D2F42)
-val jacksons_purple_theme_dark_secondaryContainer = Color(0xFF444559)
-val jacksons_purple_theme_dark_onSecondaryContainer = Color(0xFFE1E0F9)
-val jacksons_purple_theme_dark_tertiary = Color(0xFFE7B9D6)
-val jacksons_purple_theme_dark_onTertiary = Color(0xFF45263C)
-val jacksons_purple_theme_dark_tertiaryContainer = Color(0xFF5E3C53)
-val jacksons_purple_theme_dark_onTertiaryContainer = Color(0xFFFFD7EF)
-val jacksons_purple_theme_dark_error = Color(0xFFFFB4AB)
-val jacksons_purple_theme_dark_errorContainer = Color(0xFF93000A)
-val jacksons_purple_theme_dark_onError = Color(0xFF690005)
-val jacksons_purple_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
-val jacksons_purple_theme_dark_background = Color(0xFF1B1B1F)
-val jacksons_purple_theme_dark_onBackground = Color(0xFFE4E1E6)
-val jacksons_purple_theme_dark_surface = Color(0xFF1B1B1F)
-val jacksons_purple_theme_dark_onSurface = Color(0xFFE4E1E6)
-val jacksons_purple_theme_dark_surfaceVariant = Color(0xFF46464F)
-val jacksons_purple_theme_dark_onSurfaceVariant = Color(0xFFC7C5D0)
-val jacksons_purple_theme_dark_outline = Color(0xFF91909A)
-val jacksons_purple_theme_dark_inverseOnSurface = Color(0xFF1B1B1F)
-val jacksons_purple_theme_dark_inverseSurface = Color(0xFFE4E1E6)
-val jacksons_purple_theme_dark_inversePrimary = Color(0xFF4853BD)
-val jacksons_purple_theme_dark_surfaceTint = Color(0xFFBDC2FF)
-
-
-val jacksons_purple_seed = Color(0xFF242F9B)
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/jacksons_purple/JacksonsPurpleTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/jacksons_purple/JacksonsPurpleTheme.kt
deleted file mode 100644
index e5bb75a..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/jacksons_purple/JacksonsPurpleTheme.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.jacksons_purple
-
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.material3.darkColorScheme
-
-
-val JacksonsPurpleLightColors = lightColorScheme(
- primary = jacksons_purple_theme_light_primary,
- onPrimary = jacksons_purple_theme_light_onPrimary,
- primaryContainer = jacksons_purple_theme_light_primaryContainer,
- onPrimaryContainer = jacksons_purple_theme_light_onPrimaryContainer,
- secondary = jacksons_purple_theme_light_secondary,
- onSecondary = jacksons_purple_theme_light_onSecondary,
- secondaryContainer = jacksons_purple_theme_light_secondaryContainer,
- onSecondaryContainer = jacksons_purple_theme_light_onSecondaryContainer,
- tertiary = jacksons_purple_theme_light_tertiary,
- onTertiary = jacksons_purple_theme_light_onTertiary,
- tertiaryContainer = jacksons_purple_theme_light_tertiaryContainer,
- onTertiaryContainer = jacksons_purple_theme_light_onTertiaryContainer,
- error = jacksons_purple_theme_light_error,
- errorContainer = jacksons_purple_theme_light_errorContainer,
- onError = jacksons_purple_theme_light_onError,
- onErrorContainer = jacksons_purple_theme_light_onErrorContainer,
- background = jacksons_purple_theme_light_background,
- onBackground = jacksons_purple_theme_light_onBackground,
- surface = jacksons_purple_theme_light_surface,
- onSurface = jacksons_purple_theme_light_onSurface,
- surfaceVariant = jacksons_purple_theme_light_surfaceVariant,
- onSurfaceVariant = jacksons_purple_theme_light_onSurfaceVariant,
- outline = jacksons_purple_theme_light_outline,
- inverseOnSurface = jacksons_purple_theme_light_inverseOnSurface,
- inverseSurface = jacksons_purple_theme_light_inverseSurface,
- inversePrimary = jacksons_purple_theme_light_inversePrimary,
- surfaceTint = jacksons_purple_theme_light_surfaceTint,
-)
-
-
-val JacksonsPurpleDarkColors = darkColorScheme(
- primary = jacksons_purple_theme_dark_primary,
- onPrimary = jacksons_purple_theme_dark_onPrimary,
- primaryContainer = jacksons_purple_theme_dark_primaryContainer,
- onPrimaryContainer = jacksons_purple_theme_dark_onPrimaryContainer,
- secondary = jacksons_purple_theme_dark_secondary,
- onSecondary = jacksons_purple_theme_dark_onSecondary,
- secondaryContainer = jacksons_purple_theme_dark_secondaryContainer,
- onSecondaryContainer = jacksons_purple_theme_dark_onSecondaryContainer,
- tertiary = jacksons_purple_theme_dark_tertiary,
- onTertiary = jacksons_purple_theme_dark_onTertiary,
- tertiaryContainer = jacksons_purple_theme_dark_tertiaryContainer,
- onTertiaryContainer = jacksons_purple_theme_dark_onTertiaryContainer,
- error = jacksons_purple_theme_dark_error,
- errorContainer = jacksons_purple_theme_dark_errorContainer,
- onError = jacksons_purple_theme_dark_onError,
- onErrorContainer = jacksons_purple_theme_dark_onErrorContainer,
- background = jacksons_purple_theme_dark_background,
- onBackground = jacksons_purple_theme_dark_onBackground,
- surface = jacksons_purple_theme_dark_surface,
- onSurface = jacksons_purple_theme_dark_onSurface,
- surfaceVariant = jacksons_purple_theme_dark_surfaceVariant,
- onSurfaceVariant = jacksons_purple_theme_dark_onSurfaceVariant,
- outline = jacksons_purple_theme_dark_outline,
- inverseOnSurface = jacksons_purple_theme_dark_inverseOnSurface,
- inverseSurface = jacksons_purple_theme_dark_inverseSurface,
- inversePrimary = jacksons_purple_theme_dark_inversePrimary,
- surfaceTint = jacksons_purple_theme_dark_surfaceTint,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/magenta/MagentaColor.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/magenta/MagentaColor.kt
deleted file mode 100644
index aa9e8f2..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/magenta/MagentaColor.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.magenta
-import androidx.compose.ui.graphics.Color
-
-val magenta_theme_light_primary = Color(0xFFA62E72)
-val magenta_theme_light_onPrimary = Color(0xFFFFFFFF)
-val magenta_theme_light_primaryContainer = Color(0xFFFFD8E7)
-val magenta_theme_light_onPrimaryContainer = Color(0xFF3D0025)
-val magenta_theme_light_secondary = Color(0xFF725762)
-val magenta_theme_light_onSecondary = Color(0xFFFFFFFF)
-val magenta_theme_light_secondaryContainer = Color(0xFFFED9E6)
-val magenta_theme_light_onSecondaryContainer = Color(0xFF2A151F)
-val magenta_theme_light_tertiary = Color(0xFF7E5539)
-val magenta_theme_light_onTertiary = Color(0xFFFFFFFF)
-val magenta_theme_light_tertiaryContainer = Color(0xFFFFDCC7)
-val magenta_theme_light_onTertiaryContainer = Color(0xFF301401)
-val magenta_theme_light_error = Color(0xFFBA1A1A)
-val magenta_theme_light_errorContainer = Color(0xFFFFDAD6)
-val magenta_theme_light_onError = Color(0xFFFFFFFF)
-val magenta_theme_light_onErrorContainer = Color(0xFF410002)
-val magenta_theme_light_background = Color(0xFFFFFBFF)
-val magenta_theme_light_onBackground = Color(0xFF1F1A1C)
-val magenta_theme_light_surface = Color(0xFFFFFBFF)
-val magenta_theme_light_onSurface = Color(0xFF1F1A1C)
-val magenta_theme_light_surfaceVariant = Color(0xFFF1DEE3)
-val magenta_theme_light_onSurfaceVariant = Color(0xFF504348)
-val magenta_theme_light_outline = Color(0xFF827378)
-val magenta_theme_light_inverseOnSurface = Color(0xFFF9EEF0)
-val magenta_theme_light_inverseSurface = Color(0xFF352F31)
-val magenta_theme_light_inversePrimary = Color(0xFFFFAFD2)
-val magenta_theme_light_surfaceTint = Color(0xFFA62E72)
-
-val magenta_theme_dark_primary = Color(0xFFFFAFD2)
-val magenta_theme_dark_onPrimary = Color(0xFF63003F)
-val magenta_theme_dark_primaryContainer = Color(0xFF870F59)
-val magenta_theme_dark_onPrimaryContainer = Color(0xFFFFD8E7)
-val magenta_theme_dark_secondary = Color(0xFFE0BDCA)
-val magenta_theme_dark_onSecondary = Color(0xFF412A34)
-val magenta_theme_dark_secondaryContainer = Color(0xFF59404A)
-val magenta_theme_dark_onSecondaryContainer = Color(0xFFFED9E6)
-val magenta_theme_dark_tertiary = Color(0xFFF2BB98)
-val magenta_theme_dark_onTertiary = Color(0xFF492810)
-val magenta_theme_dark_tertiaryContainer = Color(0xFF633E24)
-val magenta_theme_dark_onTertiaryContainer = Color(0xFFFFDCC7)
-val magenta_theme_dark_error = Color(0xFFFFB4AB)
-val magenta_theme_dark_errorContainer = Color(0xFF93000A)
-val magenta_theme_dark_onError = Color(0xFF690005)
-val magenta_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
-val magenta_theme_dark_background = Color(0xFF1F1A1C)
-val magenta_theme_dark_onBackground = Color(0xFFEBE0E2)
-val magenta_theme_dark_surface = Color(0xFF1F1A1C)
-val magenta_theme_dark_onSurface = Color(0xFFEBE0E2)
-val magenta_theme_dark_surfaceVariant = Color(0xFF504348)
-val magenta_theme_dark_onSurfaceVariant = Color(0xFFD4C2C7)
-val magenta_theme_dark_outline = Color(0xFF9D8C92)
-val magenta_theme_dark_inverseOnSurface = Color(0xFF1F1A1C)
-val magenta_theme_dark_inverseSurface = Color(0xFFEBE0E2)
-val magenta_theme_dark_inversePrimary = Color(0xFFA62E72)
-val magenta_theme_dark_surfaceTint = Color(0xFFFFAFD2)
-
-
-val magenta_seed = Color(0xFFF56EB3)
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/magenta/MagentaTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/magenta/MagentaTheme.kt
deleted file mode 100644
index b69449d..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/magenta/MagentaTheme.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.magenta
-
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.material3.darkColorScheme
-
-
-val MagentaLightColors = lightColorScheme(
- primary = magenta_theme_light_primary,
- onPrimary = magenta_theme_light_onPrimary,
- primaryContainer = magenta_theme_light_primaryContainer,
- onPrimaryContainer = magenta_theme_light_onPrimaryContainer,
- secondary = magenta_theme_light_secondary,
- onSecondary = magenta_theme_light_onSecondary,
- secondaryContainer = magenta_theme_light_secondaryContainer,
- onSecondaryContainer = magenta_theme_light_onSecondaryContainer,
- tertiary = magenta_theme_light_tertiary,
- onTertiary = magenta_theme_light_onTertiary,
- tertiaryContainer = magenta_theme_light_tertiaryContainer,
- onTertiaryContainer = magenta_theme_light_onTertiaryContainer,
- error = magenta_theme_light_error,
- errorContainer = magenta_theme_light_errorContainer,
- onError = magenta_theme_light_onError,
- onErrorContainer = magenta_theme_light_onErrorContainer,
- background = magenta_theme_light_background,
- onBackground = magenta_theme_light_onBackground,
- surface = magenta_theme_light_surface,
- onSurface = magenta_theme_light_onSurface,
- surfaceVariant = magenta_theme_light_surfaceVariant,
- onSurfaceVariant = magenta_theme_light_onSurfaceVariant,
- outline = magenta_theme_light_outline,
- inverseOnSurface = magenta_theme_light_inverseOnSurface,
- inverseSurface = magenta_theme_light_inverseSurface,
- inversePrimary = magenta_theme_light_inversePrimary,
- surfaceTint = magenta_theme_light_surfaceTint,
-)
-
-
-val MagentaDarkColors = darkColorScheme(
- primary = magenta_theme_dark_primary,
- onPrimary = magenta_theme_dark_onPrimary,
- primaryContainer = magenta_theme_dark_primaryContainer,
- onPrimaryContainer = magenta_theme_dark_onPrimaryContainer,
- secondary = magenta_theme_dark_secondary,
- onSecondary = magenta_theme_dark_onSecondary,
- secondaryContainer = magenta_theme_dark_secondaryContainer,
- onSecondaryContainer = magenta_theme_dark_onSecondaryContainer,
- tertiary = magenta_theme_dark_tertiary,
- onTertiary = magenta_theme_dark_onTertiary,
- tertiaryContainer = magenta_theme_dark_tertiaryContainer,
- onTertiaryContainer = magenta_theme_dark_onTertiaryContainer,
- error = magenta_theme_dark_error,
- errorContainer = magenta_theme_dark_errorContainer,
- onError = magenta_theme_dark_onError,
- onErrorContainer = magenta_theme_dark_onErrorContainer,
- background = magenta_theme_dark_background,
- onBackground = magenta_theme_dark_onBackground,
- surface = magenta_theme_dark_surface,
- onSurface = magenta_theme_dark_onSurface,
- surfaceVariant = magenta_theme_dark_surfaceVariant,
- onSurfaceVariant = magenta_theme_dark_onSurfaceVariant,
- outline = magenta_theme_dark_outline,
- inverseOnSurface = magenta_theme_dark_inverseOnSurface,
- inverseSurface = magenta_theme_dark_inverseSurface,
- inversePrimary = magenta_theme_dark_inversePrimary,
- surfaceTint = magenta_theme_dark_surfaceTint,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/malibu/MalibuColor.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/malibu/MalibuColor.kt
deleted file mode 100644
index 0842b96..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/malibu/MalibuColor.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.malibu
-import androidx.compose.ui.graphics.Color
-
-val malibu_theme_light_primary = Color(0xFF00658E)
-val malibu_theme_light_onPrimary = Color(0xFFFFFFFF)
-val malibu_theme_light_primaryContainer = Color(0xFFC7E7FF)
-val malibu_theme_light_onPrimaryContainer = Color(0xFF001E2E)
-val malibu_theme_light_secondary = Color(0xFF4F616E)
-val malibu_theme_light_onSecondary = Color(0xFFFFFFFF)
-val malibu_theme_light_secondaryContainer = Color(0xFFD2E5F5)
-val malibu_theme_light_onSecondaryContainer = Color(0xFF0B1D29)
-val malibu_theme_light_tertiary = Color(0xFF63597C)
-val malibu_theme_light_onTertiary = Color(0xFFFFFFFF)
-val malibu_theme_light_tertiaryContainer = Color(0xFFE8DDFF)
-val malibu_theme_light_onTertiaryContainer = Color(0xFF1E1635)
-val malibu_theme_light_error = Color(0xFFBA1A1A)
-val malibu_theme_light_errorContainer = Color(0xFFFFDAD6)
-val malibu_theme_light_onError = Color(0xFFFFFFFF)
-val malibu_theme_light_onErrorContainer = Color(0xFF410002)
-val malibu_theme_light_background = Color(0xFFFCFCFF)
-val malibu_theme_light_onBackground = Color(0xFF191C1E)
-val malibu_theme_light_surface = Color(0xFFFCFCFF)
-val malibu_theme_light_onSurface = Color(0xFF191C1E)
-val malibu_theme_light_surfaceVariant = Color(0xFFDDE3EA)
-val malibu_theme_light_onSurfaceVariant = Color(0xFF41484D)
-val malibu_theme_light_outline = Color(0xFF71787E)
-val malibu_theme_light_inverseOnSurface = Color(0xFFF0F1F3)
-val malibu_theme_light_inverseSurface = Color(0xFF2E3133)
-val malibu_theme_light_inversePrimary = Color(0xFF84CFFF)
-val malibu_theme_light_surfaceTint = Color(0xFF00658E)
-
-val malibu_theme_dark_primary = Color(0xFF84CFFF)
-val malibu_theme_dark_onPrimary = Color(0xFF00344C)
-val malibu_theme_dark_primaryContainer = Color(0xFF004C6C)
-val malibu_theme_dark_onPrimaryContainer = Color(0xFFC7E7FF)
-val malibu_theme_dark_secondary = Color(0xFFB6C9D8)
-val malibu_theme_dark_onSecondary = Color(0xFF21323E)
-val malibu_theme_dark_secondaryContainer = Color(0xFF374955)
-val malibu_theme_dark_onSecondaryContainer = Color(0xFFD2E5F5)
-val malibu_theme_dark_tertiary = Color(0xFFCDC0E9)
-val malibu_theme_dark_onTertiary = Color(0xFF342B4B)
-val malibu_theme_dark_tertiaryContainer = Color(0xFF4B4263)
-val malibu_theme_dark_onTertiaryContainer = Color(0xFFE8DDFF)
-val malibu_theme_dark_error = Color(0xFFFFB4AB)
-val malibu_theme_dark_errorContainer = Color(0xFF93000A)
-val malibu_theme_dark_onError = Color(0xFF690005)
-val malibu_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
-val malibu_theme_dark_background = Color(0xFF191C1E)
-val malibu_theme_dark_onBackground = Color(0xFFE2E2E5)
-val malibu_theme_dark_surface = Color(0xFF191C1E)
-val malibu_theme_dark_onSurface = Color(0xFFE2E2E5)
-val malibu_theme_dark_surfaceVariant = Color(0xFF41484D)
-val malibu_theme_dark_onSurfaceVariant = Color(0xFFC1C7CE)
-val malibu_theme_dark_outline = Color(0xFF8B9198)
-val malibu_theme_dark_inverseOnSurface = Color(0xFF191C1E)
-val malibu_theme_dark_inverseSurface = Color(0xFFE2E2E5)
-val malibu_theme_dark_inversePrimary = Color(0xFF00658E)
-val malibu_theme_dark_surfaceTint = Color(0xFF84CFFF)
-
-
-val malibu_seed = Color(0xFF89CFFD)
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/malibu/MalibuTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/malibu/MalibuTheme.kt
deleted file mode 100644
index fa5b533..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/malibu/MalibuTheme.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.malibu
-
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.material3.darkColorScheme
-
-
-val MalibuLightColors = lightColorScheme(
- primary = malibu_theme_light_primary,
- onPrimary = malibu_theme_light_onPrimary,
- primaryContainer = malibu_theme_light_primaryContainer,
- onPrimaryContainer = malibu_theme_light_onPrimaryContainer,
- secondary = malibu_theme_light_secondary,
- onSecondary = malibu_theme_light_onSecondary,
- secondaryContainer = malibu_theme_light_secondaryContainer,
- onSecondaryContainer = malibu_theme_light_onSecondaryContainer,
- tertiary = malibu_theme_light_tertiary,
- onTertiary = malibu_theme_light_onTertiary,
- tertiaryContainer = malibu_theme_light_tertiaryContainer,
- onTertiaryContainer = malibu_theme_light_onTertiaryContainer,
- error = malibu_theme_light_error,
- errorContainer = malibu_theme_light_errorContainer,
- onError = malibu_theme_light_onError,
- onErrorContainer = malibu_theme_light_onErrorContainer,
- background = malibu_theme_light_background,
- onBackground = malibu_theme_light_onBackground,
- surface = malibu_theme_light_surface,
- onSurface = malibu_theme_light_onSurface,
- surfaceVariant = malibu_theme_light_surfaceVariant,
- onSurfaceVariant = malibu_theme_light_onSurfaceVariant,
- outline = malibu_theme_light_outline,
- inverseOnSurface = malibu_theme_light_inverseOnSurface,
- inverseSurface = malibu_theme_light_inverseSurface,
- inversePrimary = malibu_theme_light_inversePrimary,
- surfaceTint = malibu_theme_light_surfaceTint,
-)
-
-
-val MalibuDarkColors = darkColorScheme(
- primary = malibu_theme_dark_primary,
- onPrimary = malibu_theme_dark_onPrimary,
- primaryContainer = malibu_theme_dark_primaryContainer,
- onPrimaryContainer = malibu_theme_dark_onPrimaryContainer,
- secondary = malibu_theme_dark_secondary,
- onSecondary = malibu_theme_dark_onSecondary,
- secondaryContainer = malibu_theme_dark_secondaryContainer,
- onSecondaryContainer = malibu_theme_dark_onSecondaryContainer,
- tertiary = malibu_theme_dark_tertiary,
- onTertiary = malibu_theme_dark_onTertiary,
- tertiaryContainer = malibu_theme_dark_tertiaryContainer,
- onTertiaryContainer = malibu_theme_dark_onTertiaryContainer,
- error = malibu_theme_dark_error,
- errorContainer = malibu_theme_dark_errorContainer,
- onError = malibu_theme_dark_onError,
- onErrorContainer = malibu_theme_dark_onErrorContainer,
- background = malibu_theme_dark_background,
- onBackground = malibu_theme_dark_onBackground,
- surface = malibu_theme_dark_surface,
- onSurface = malibu_theme_dark_onSurface,
- surfaceVariant = malibu_theme_dark_surfaceVariant,
- onSurfaceVariant = malibu_theme_dark_onSurfaceVariant,
- outline = malibu_theme_dark_outline,
- inverseOnSurface = malibu_theme_dark_inverseOnSurface,
- inverseSurface = malibu_theme_dark_inverseSurface,
- inversePrimary = malibu_theme_dark_inversePrimary,
- surfaceTint = malibu_theme_dark_surfaceTint,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/melrose/MelroseColor.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/melrose/MelroseColor.kt
deleted file mode 100644
index 65cfe9b..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/melrose/MelroseColor.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.melrose
-import androidx.compose.ui.graphics.Color
-
-val melrose_theme_light_primary = Color(0xFF5D52A9)
-val melrose_theme_light_onPrimary = Color(0xFFFFFFFF)
-val melrose_theme_light_primaryContainer = Color(0xFFE5DEFF)
-val melrose_theme_light_onPrimaryContainer = Color(0xFF180164)
-val melrose_theme_light_secondary = Color(0xFF5F5C71)
-val melrose_theme_light_onSecondary = Color(0xFFFFFFFF)
-val melrose_theme_light_secondaryContainer = Color(0xFFE5DFF9)
-val melrose_theme_light_onSecondaryContainer = Color(0xFF1B192C)
-val melrose_theme_light_tertiary = Color(0xFF7B5265)
-val melrose_theme_light_onTertiary = Color(0xFFFFFFFF)
-val melrose_theme_light_tertiaryContainer = Color(0xFFFFD8E7)
-val melrose_theme_light_onTertiaryContainer = Color(0xFF301121)
-val melrose_theme_light_error = Color(0xFFBA1A1A)
-val melrose_theme_light_errorContainer = Color(0xFFFFDAD6)
-val melrose_theme_light_onError = Color(0xFFFFFFFF)
-val melrose_theme_light_onErrorContainer = Color(0xFF410002)
-val melrose_theme_light_background = Color(0xFFFFFBFF)
-val melrose_theme_light_onBackground = Color(0xFF1C1B1F)
-val melrose_theme_light_surface = Color(0xFFFFFBFF)
-val melrose_theme_light_onSurface = Color(0xFF1C1B1F)
-val melrose_theme_light_surfaceVariant = Color(0xFFE5E0EC)
-val melrose_theme_light_onSurfaceVariant = Color(0xFF47464F)
-val melrose_theme_light_outline = Color(0xFF78767F)
-val melrose_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
-val melrose_theme_light_inverseSurface = Color(0xFF313034)
-val melrose_theme_light_inversePrimary = Color(0xFFC7BFFF)
-val melrose_theme_light_surfaceTint = Color(0xFF5D52A9)
-
-val melrose_theme_dark_primary = Color(0xFFC7BFFF)
-val melrose_theme_dark_onPrimary = Color(0xFF2E2177)
-val melrose_theme_dark_primaryContainer = Color(0xFF453A8F)
-val melrose_theme_dark_onPrimaryContainer = Color(0xFFE5DEFF)
-val melrose_theme_dark_secondary = Color(0xFFC8C3DC)
-val melrose_theme_dark_onSecondary = Color(0xFF312E41)
-val melrose_theme_dark_secondaryContainer = Color(0xFF474459)
-val melrose_theme_dark_onSecondaryContainer = Color(0xFFE5DFF9)
-val melrose_theme_dark_tertiary = Color(0xFFECB8CE)
-val melrose_theme_dark_onTertiary = Color(0xFF482536)
-val melrose_theme_dark_tertiaryContainer = Color(0xFF613B4D)
-val melrose_theme_dark_onTertiaryContainer = Color(0xFFFFD8E7)
-val melrose_theme_dark_error = Color(0xFFFFB4AB)
-val melrose_theme_dark_errorContainer = Color(0xFF93000A)
-val melrose_theme_dark_onError = Color(0xFF690005)
-val melrose_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
-val melrose_theme_dark_background = Color(0xFF1C1B1F)
-val melrose_theme_dark_onBackground = Color(0xFFE5E1E6)
-val melrose_theme_dark_surface = Color(0xFF1C1B1F)
-val melrose_theme_dark_onSurface = Color(0xFFE5E1E6)
-val melrose_theme_dark_surfaceVariant = Color(0xFF47464F)
-val melrose_theme_dark_onSurfaceVariant = Color(0xFFC9C5D0)
-val melrose_theme_dark_outline = Color(0xFF928F99)
-val melrose_theme_dark_inverseOnSurface = Color(0xFF1C1B1F)
-val melrose_theme_dark_inverseSurface = Color(0xFFE5E1E6)
-val melrose_theme_dark_inversePrimary = Color(0xFF5D52A9)
-val melrose_theme_dark_surfaceTint = Color(0xFFC7BFFF)
-
-
-val melrose_seed = Color(0xFFADA2FF)
diff --git a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/melrose/MelroseTheme.kt b/app/src/main/java/com/github/pakka_papad/ui/accent_colours/melrose/MelroseTheme.kt
deleted file mode 100644
index 52fed33..0000000
--- a/app/src/main/java/com/github/pakka_papad/ui/accent_colours/melrose/MelroseTheme.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.github.pakka_papad.ui.accent_colours.melrose
-
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.material3.darkColorScheme
-
-
-val MelroseLightColors = lightColorScheme(
- primary = melrose_theme_light_primary,
- onPrimary = melrose_theme_light_onPrimary,
- primaryContainer = melrose_theme_light_primaryContainer,
- onPrimaryContainer = melrose_theme_light_onPrimaryContainer,
- secondary = melrose_theme_light_secondary,
- onSecondary = melrose_theme_light_onSecondary,
- secondaryContainer = melrose_theme_light_secondaryContainer,
- onSecondaryContainer = melrose_theme_light_onSecondaryContainer,
- tertiary = melrose_theme_light_tertiary,
- onTertiary = melrose_theme_light_onTertiary,
- tertiaryContainer = melrose_theme_light_tertiaryContainer,
- onTertiaryContainer = melrose_theme_light_onTertiaryContainer,
- error = melrose_theme_light_error,
- errorContainer = melrose_theme_light_errorContainer,
- onError = melrose_theme_light_onError,
- onErrorContainer = melrose_theme_light_onErrorContainer,
- background = melrose_theme_light_background,
- onBackground = melrose_theme_light_onBackground,
- surface = melrose_theme_light_surface,
- onSurface = melrose_theme_light_onSurface,
- surfaceVariant = melrose_theme_light_surfaceVariant,
- onSurfaceVariant = melrose_theme_light_onSurfaceVariant,
- outline = melrose_theme_light_outline,
- inverseOnSurface = melrose_theme_light_inverseOnSurface,
- inverseSurface = melrose_theme_light_inverseSurface,
- inversePrimary = melrose_theme_light_inversePrimary,
- surfaceTint = melrose_theme_light_surfaceTint,
-)
-
-
-val MelroseDarkColors = darkColorScheme(
- primary = melrose_theme_dark_primary,
- onPrimary = melrose_theme_dark_onPrimary,
- primaryContainer = melrose_theme_dark_primaryContainer,
- onPrimaryContainer = melrose_theme_dark_onPrimaryContainer,
- secondary = melrose_theme_dark_secondary,
- onSecondary = melrose_theme_dark_onSecondary,
- secondaryContainer = melrose_theme_dark_secondaryContainer,
- onSecondaryContainer = melrose_theme_dark_onSecondaryContainer,
- tertiary = melrose_theme_dark_tertiary,
- onTertiary = melrose_theme_dark_onTertiary,
- tertiaryContainer = melrose_theme_dark_tertiaryContainer,
- onTertiaryContainer = melrose_theme_dark_onTertiaryContainer,
- error = melrose_theme_dark_error,
- errorContainer = melrose_theme_dark_errorContainer,
- onError = melrose_theme_dark_onError,
- onErrorContainer = melrose_theme_dark_onErrorContainer,
- background = melrose_theme_dark_background,
- onBackground = melrose_theme_dark_onBackground,
- surface = melrose_theme_dark_surface,
- onSurface = melrose_theme_dark_onSurface,
- surfaceVariant = melrose_theme_dark_surfaceVariant,
- onSurfaceVariant = melrose_theme_dark_onSurfaceVariant,
- outline = melrose_theme_dark_outline,
- inverseOnSurface = melrose_theme_dark_inverseOnSurface,
- inverseSurface = melrose_theme_dark_inverseSurface,
- inversePrimary = melrose_theme_dark_inversePrimary,
- surfaceTint = melrose_theme_dark_surfaceTint,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/theme/Colors.kt b/app/src/main/java/com/github/pakka_papad/ui/theme/Colors.kt
index d87ac11..72d654e 100644
--- a/app/src/main/java/com/github/pakka_papad/ui/theme/Colors.kt
+++ b/app/src/main/java/com/github/pakka_papad/ui/theme/Colors.kt
@@ -1,15 +1,24 @@
package com.github.pakka_papad.ui.theme
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import blend.Blend
@Composable
fun Colors(paddingValues: PaddingValues) {
@@ -63,4 +72,14 @@ fun ColorCard(
text = colorType
)
}
-}
\ No newline at end of file
+}
+
+@Composable
+fun harmonize(from: Color, to: Color = MaterialTheme.colorScheme.primary): Color {
+ val res by remember(key1 = to) { derivedStateOf {
+ Color(
+ Blend.harmonize(from.toArgb(), to.toArgb())
+ )
+ } }
+ return res
+}
diff --git a/app/src/main/java/com/github/pakka_papad/ui/theme/Theme.kt b/app/src/main/java/com/github/pakka_papad/ui/theme/Theme.kt
index 0f6b831..f1317be 100644
--- a/app/src/main/java/com/github/pakka_papad/ui/theme/Theme.kt
+++ b/app/src/main/java/com/github/pakka_papad/ui/theme/Theme.kt
@@ -6,31 +6,25 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.github.pakka_papad.data.UserPreferences
-import com.github.pakka_papad.ui.accent_colours.default.DefaultDarkColors
-import com.github.pakka_papad.ui.accent_colours.default.DefaultLightColors
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@Composable
fun DefaultTheme(content: @Composable () -> Unit) {
- val isSystemInDarkTheme = isSystemInDarkTheme()
- val systemUiController = rememberSystemUiController()
- DisposableEffect(key1 = isSystemInDarkTheme) {
- systemUiController.setSystemBarsColor(
- color = Color.Transparent,
- darkIcons = !isSystemInDarkTheme
- )
- onDispose { }
- }
- MaterialTheme(
- colorScheme = if (isSystemInDarkTheme) DefaultDarkColors else DefaultLightColors,
- typography = ZenTypography,
- content = content
+ ZenTheme(
+ themePreference = ThemePreference(
+ useMaterialYou = true,
+ theme = UserPreferences.Theme.USE_SYSTEM_MODE,
+ accent = UserPreferences.Accent.Elm,
+ ),
+ content = content,
)
}
@@ -66,6 +60,12 @@ fun ZenTheme(
MaterialTheme(
colorScheme = colourScheme,
typography = ZenTypography,
- content = content
+ content = {
+ CompositionLocalProvider(LocalThemePreference provides themePreference) {
+ content()
+ }
+ }
)
-}
\ No newline at end of file
+}
+
+val LocalThemePreference = compositionLocalOf { ThemePreference() }
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/ui/theme/ThemePreference.kt b/app/src/main/java/com/github/pakka_papad/ui/theme/ThemePreference.kt
index edfec32..375c464 100644
--- a/app/src/main/java/com/github/pakka_papad/ui/theme/ThemePreference.kt
+++ b/app/src/main/java/com/github/pakka_papad/ui/theme/ThemePreference.kt
@@ -3,24 +3,24 @@ package com.github.pakka_papad.ui.theme
import androidx.compose.material3.ColorScheme
import androidx.compose.ui.graphics.Color
import com.github.pakka_papad.data.UserPreferences
-import com.github.pakka_papad.ui.accent_colours.default.DefaultDarkColors
-import com.github.pakka_papad.ui.accent_colours.default.DefaultLightColors
-import com.github.pakka_papad.ui.accent_colours.default.default_seed
-import com.github.pakka_papad.ui.accent_colours.elm.ElmDarkColors
-import com.github.pakka_papad.ui.accent_colours.elm.ElmLightColors
-import com.github.pakka_papad.ui.accent_colours.elm.elm_seed
-import com.github.pakka_papad.ui.accent_colours.jacksons_purple.JacksonsPurpleDarkColors
-import com.github.pakka_papad.ui.accent_colours.jacksons_purple.JacksonsPurpleLightColors
-import com.github.pakka_papad.ui.accent_colours.jacksons_purple.jacksons_purple_seed
-import com.github.pakka_papad.ui.accent_colours.magenta.MagentaDarkColors
-import com.github.pakka_papad.ui.accent_colours.magenta.MagentaLightColors
-import com.github.pakka_papad.ui.accent_colours.magenta.magenta_seed
-import com.github.pakka_papad.ui.accent_colours.malibu.MalibuDarkColors
-import com.github.pakka_papad.ui.accent_colours.malibu.MalibuLightColors
-import com.github.pakka_papad.ui.accent_colours.malibu.malibu_seed
-import com.github.pakka_papad.ui.accent_colours.melrose.MelroseDarkColors
-import com.github.pakka_papad.ui.accent_colours.melrose.MelroseLightColors
-import com.github.pakka_papad.ui.accent_colours.melrose.melrose_seed
+import com.github.pakka_papad.ui.accent_colours.DefaultDarkColors
+import com.github.pakka_papad.ui.accent_colours.DefaultLightColors
+import com.github.pakka_papad.ui.accent_colours.default_seed
+import com.github.pakka_papad.ui.accent_colours.ElmDarkColors
+import com.github.pakka_papad.ui.accent_colours.ElmLightColors
+import com.github.pakka_papad.ui.accent_colours.elm_seed
+import com.github.pakka_papad.ui.accent_colours.JacksonsPurpleDarkColors
+import com.github.pakka_papad.ui.accent_colours.JacksonsPurpleLightColors
+import com.github.pakka_papad.ui.accent_colours.jacksons_purple_seed
+import com.github.pakka_papad.ui.accent_colours.MagentaDarkColors
+import com.github.pakka_papad.ui.accent_colours.MagentaLightColors
+import com.github.pakka_papad.ui.accent_colours.magenta_seed
+import com.github.pakka_papad.ui.accent_colours.MalibuDarkColors
+import com.github.pakka_papad.ui.accent_colours.MalibuLightColors
+import com.github.pakka_papad.ui.accent_colours.malibu_seed
+import com.github.pakka_papad.ui.accent_colours.MelroseDarkColors
+import com.github.pakka_papad.ui.accent_colours.MelroseLightColors
+import com.github.pakka_papad.ui.accent_colours.melrose_seed
data class ThemePreference(
val useMaterialYou: Boolean = false,
diff --git a/app/src/main/java/com/github/pakka_papad/util/MessageStore.kt b/app/src/main/java/com/github/pakka_papad/util/MessageStore.kt
new file mode 100644
index 0000000..659d4b9
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/util/MessageStore.kt
@@ -0,0 +1,22 @@
+package com.github.pakka_papad.util
+
+import android.content.Context
+import androidx.annotation.StringRes
+import javax.inject.Inject
+
+interface MessageStore {
+ fun getString(@StringRes id: Int): String
+ fun getString(@StringRes id: Int, vararg formatArgs: Any): String
+}
+
+class MessageStoreImpl @Inject constructor(
+ private val context: Context
+) : MessageStore {
+ override fun getString(id: Int): String {
+ return context.getString(id)
+ }
+
+ override fun getString(id: Int, vararg formatArgs: Any): String {
+ return context.getString(id, *formatArgs)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/workers/ThumbnailWorker.kt b/app/src/main/java/com/github/pakka_papad/workers/ThumbnailWorker.kt
new file mode 100644
index 0000000..84fcb2f
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/workers/ThumbnailWorker.kt
@@ -0,0 +1,91 @@
+package com.github.pakka_papad.workers
+
+import android.content.Context
+import androidx.hilt.work.HiltWorker
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.github.pakka_papad.data.music.Playlist
+import com.github.pakka_papad.data.music.PlaylistWithSongCount
+import com.github.pakka_papad.data.services.PlaylistService
+import com.github.pakka_papad.data.services.ThumbnailService
+import com.github.pakka_papad.data.thumbnails.Thumbnail
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.days
+
+@HiltWorker
+class ThumbnailWorker @AssistedInject constructor(
+ @Assisted context: Context,
+ @Assisted workParams: WorkerParameters,
+ private val thumbnailService: ThumbnailService,
+ private val playlistService: PlaylistService,
+): CoroutineWorker(context, workParams) {
+
+ override suspend fun doWork(): Result {
+ createPlaylistThumbnails()
+ deletePlaylistThumbnails()
+ return Result.success()
+ }
+
+ private val sevenDays = 7.days.inWholeMilliseconds
+
+ private fun isThumbnailGood(
+ thumbnail: Thumbnail?,
+ playlist: PlaylistWithSongCount,
+ ): Boolean {
+ return ( thumbnail != null &&
+ (thumbnail.artCount >= 9 || thumbnail.artCount == playlist.count) &&
+ thumbnail.addedOn + sevenDays >= System.currentTimeMillis()
+ )
+ }
+
+ private suspend fun createPlaylistThumbnails() = coroutineScope {
+ val playlists = playlistService.playlists.first()
+ val jobs = mutableListOf()
+ playlists.forEach { playlist ->
+ launch {
+ val existingThumbnail = if (playlist.artUri == null) null else
+ thumbnailService.getThumbnailByPath(playlist.artUri)
+ if (isThumbnailGood(existingThumbnail, playlist)) {
+ return@launch
+ }
+ val songs = playlistService.getPlaylistWithSongsById(playlist.playlistId)
+ .first()?.songs ?: return@launch
+ val uri = thumbnailService
+ .createThumbnailImage(songs.map { it.artUri }) ?: return@launch
+ val newThumbnail = Thumbnail(
+ location = uri,
+ addedOn = System.currentTimeMillis(),
+ artCount = minOf(9, songs.size),
+ deleteThis = false
+ )
+ thumbnailService.insert(newThumbnail)
+ val updatedPlaylist = Playlist(
+ playlistId = playlist.playlistId,
+ playlistName = playlist.playlistName,
+ createdAt = playlist.createdAt,
+ artUri = uri,
+ )
+ playlistService.updatePlaylist(updatedPlaylist)
+ if (existingThumbnail != null) {
+ thumbnailService.markDelete(existingThumbnail.location)
+ }
+ }.also {
+ jobs.add(it)
+ }
+ }
+ jobs.joinAll()
+ }
+
+ private suspend fun deletePlaylistThumbnails() = coroutineScope {
+ thumbnailService.getPendingDeletions()
+ .forEach {
+ thumbnailService.deleteThumbnail(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/outline_timer_24.xml b/app/src/main/res/drawable/outline_timer_24.xml
new file mode 100644
index 0000000..c0d25dd
--- /dev/null
+++ b/app/src/main/res/drawable/outline_timer_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/raw/bug.json b/app/src/main/res/raw/bug.json
new file mode 100644
index 0000000..5ea20b7
--- /dev/null
+++ b/app/src/main/res/raw/bug.json
@@ -0,0 +1,9689 @@
+{
+ "v": "5.4.3",
+ "fr": 60,
+ "ip": 0,
+ "op": 480,
+ "w": 1005,
+ "h": 750,
+ "nm": "Bugs - Loop",
+ "ddd": 0,
+ "assets": [
+ {
+ "id": "comp_0",
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "Head Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 221
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 127.376,
+ 83.681,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 39.954,
+ 26.3,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -16.741,
+ 16.748
+ ],
+ [
+ -2.154,
+ 5.277
+ ],
+ [
+ 6.272,
+ 3.644
+ ],
+ [
+ 0.027,
+ 0.016
+ ],
+ [
+ 0.733,
+ 0.377
+ ],
+ [
+ 0.107,
+ 0.054
+ ],
+ [
+ 0.687,
+ 0.307
+ ],
+ [
+ 0.175,
+ 0.075
+ ],
+ [
+ 0.65,
+ 0.248
+ ],
+ [
+ 0.232,
+ 0.084
+ ],
+ [
+ 0.618,
+ 0.195
+ ],
+ [
+ 0.286,
+ 0.084
+ ],
+ [
+ 0.587,
+ 0.148
+ ],
+ [
+ 0.338,
+ 0.077
+ ],
+ [
+ 0.556,
+ 0.105
+ ],
+ [
+ 0.398,
+ 0.064
+ ],
+ [
+ 0.515,
+ 0.064
+ ],
+ [
+ 0.491,
+ 0.045
+ ],
+ [
+ 0.439,
+ 0.027
+ ],
+ [
+ 0.839,
+ 0.005
+ ],
+ [
+ 0.108,
+ 0
+ ],
+ [
+ 0.951,
+ -0.061
+ ],
+ [
+ 0.207,
+ -0.016
+ ],
+ [
+ 0.732,
+ -0.093
+ ],
+ [
+ 0.266,
+ -0.039
+ ],
+ [
+ 0.658,
+ -0.126
+ ],
+ [
+ 0.288,
+ -0.061
+ ],
+ [
+ 0.62,
+ -0.159
+ ],
+ [
+ 0.3,
+ -0.083
+ ],
+ [
+ 0.591,
+ -0.191
+ ],
+ [
+ 0.305,
+ -0.105
+ ],
+ [
+ 0.565,
+ -0.22
+ ],
+ [
+ 0.31,
+ -0.129
+ ],
+ [
+ 0.538,
+ -0.247
+ ],
+ [
+ 0.315,
+ -0.153
+ ],
+ [
+ 0.51,
+ -0.27
+ ],
+ [
+ 0.322,
+ -0.18
+ ],
+ [
+ 0.479,
+ -0.289
+ ],
+ [
+ 0.329,
+ -0.209
+ ],
+ [
+ 0.444,
+ -0.302
+ ],
+ [
+ 0.338,
+ -0.242
+ ],
+ [
+ 0.404,
+ -0.306
+ ],
+ [
+ 0.353,
+ -0.283
+ ],
+ [
+ 0.353,
+ -0.297
+ ],
+ [
+ 0.376,
+ -0.335
+ ],
+ [
+ 0.283,
+ -0.263
+ ],
+ [
+ 0.416,
+ -0.41
+ ],
+ [
+ 0.17,
+ -0.173
+ ],
+ [
+ 1.623,
+ -2.158
+ ],
+ [
+ -4.045,
+ -4.046
+ ]
+ ],
+ "o": [
+ [
+ 4.029,
+ -4.032
+ ],
+ [
+ -4.617,
+ -6.151
+ ],
+ [
+ -0.029,
+ -0.016
+ ],
+ [
+ -0.716,
+ -0.414
+ ],
+ [
+ -0.108,
+ -0.054
+ ],
+ [
+ -0.674,
+ -0.339
+ ],
+ [
+ -0.174,
+ -0.078
+ ],
+ [
+ -0.639,
+ -0.276
+ ],
+ [
+ -0.231,
+ -0.087
+ ],
+ [
+ -0.61,
+ -0.221
+ ],
+ [
+ -0.284,
+ -0.09
+ ],
+ [
+ -0.581,
+ -0.171
+ ],
+ [
+ -0.336,
+ -0.084
+ ],
+ [
+ -0.55,
+ -0.125
+ ],
+ [
+ -0.395,
+ -0.074
+ ],
+ [
+ -0.511,
+ -0.082
+ ],
+ [
+ -0.488,
+ -0.061
+ ],
+ [
+ -0.436,
+ -0.04
+ ],
+ [
+ -0.83,
+ -0.052
+ ],
+ [
+ -0.108,
+ -0.001
+ ],
+ [
+ -0.963,
+ 0
+ ],
+ [
+ -0.207,
+ 0.013
+ ],
+ [
+ -0.74,
+ 0.058
+ ],
+ [
+ -0.267,
+ 0.034
+ ],
+ [
+ -0.666,
+ 0.097
+ ],
+ [
+ -0.29,
+ 0.056
+ ],
+ [
+ -0.627,
+ 0.134
+ ],
+ [
+ -0.301,
+ 0.077
+ ],
+ [
+ -0.598,
+ 0.167
+ ],
+ [
+ -0.308,
+ 0.1
+ ],
+ [
+ -0.573,
+ 0.199
+ ],
+ [
+ -0.313,
+ 0.122
+ ],
+ [
+ -0.546,
+ 0.226
+ ],
+ [
+ -0.319,
+ 0.147
+ ],
+ [
+ -0.518,
+ 0.251
+ ],
+ [
+ -0.325,
+ 0.172
+ ],
+ [
+ -0.486,
+ 0.272
+ ],
+ [
+ -0.332,
+ 0.201
+ ],
+ [
+ -0.451,
+ 0.287
+ ],
+ [
+ -0.343,
+ 0.233
+ ],
+ [
+ -0.411,
+ 0.294
+ ],
+ [
+ -0.358,
+ 0.273
+ ],
+ [
+ -0.359,
+ 0.288
+ ],
+ [
+ -0.383,
+ 0.324
+ ],
+ [
+ -0.288,
+ 0.258
+ ],
+ [
+ -0.426,
+ 0.396
+ ],
+ [
+ -0.172,
+ 0.17
+ ],
+ [
+ -1.85,
+ 1.877
+ ],
+ [
+ 2.162,
+ 5.297
+ ],
+ [
+ 16.748,
+ 16.74
+ ]
+ ],
+ "v": [
+ [
+ 30.336,
+ 9.296
+ ],
+ [
+ 39.704,
+ -4.809
+ ],
+ [
+ 23.226,
+ -19.679
+ ],
+ [
+ 23.142,
+ -19.728
+ ],
+ [
+ 20.966,
+ -20.911
+ ],
+ [
+ 20.645,
+ -21.074
+ ],
+ [
+ 18.602,
+ -22.042
+ ],
+ [
+ 18.079,
+ -22.27
+ ],
+ [
+ 16.145,
+ -23.057
+ ],
+ [
+ 15.449,
+ -23.311
+ ],
+ [
+ 13.609,
+ -23.939
+ ],
+ [
+ 12.752,
+ -24.193
+ ],
+ [
+ 11.002,
+ -24.679
+ ],
+ [
+ 9.988,
+ -24.912
+ ],
+ [
+ 8.331,
+ -25.267
+ ],
+ [
+ 7.138,
+ -25.462
+ ],
+ [
+ 5.603,
+ -25.696
+ ],
+ [
+ 4.132,
+ -25.84
+ ],
+ [
+ 2.824,
+ -25.959
+ ],
+ [
+ 0.319,
+ -26.04
+ ],
+ [
+ -0.002,
+ -26.05
+ ],
+ [
+ -2.872,
+ -25.957
+ ],
+ [
+ -3.489,
+ -25.901
+ ],
+ [
+ -5.701,
+ -25.685
+ ],
+ [
+ -6.497,
+ -25.563
+ ],
+ [
+ -8.487,
+ -25.239
+ ],
+ [
+ -9.351,
+ -25.054
+ ],
+ [
+ -11.224,
+ -24.624
+ ],
+ [
+ -12.122,
+ -24.375
+ ],
+ [
+ -13.908,
+ -23.846
+ ],
+ [
+ -14.825,
+ -23.533
+ ],
+ [
+ -16.533,
+ -22.909
+ ],
+ [
+ -17.467,
+ -22.529
+ ],
+ [
+ -19.094,
+ -21.821
+ ],
+ [
+ -20.044,
+ -21.37
+ ],
+ [
+ -21.586,
+ -20.587
+ ],
+ [
+ -22.556,
+ -20.06
+ ],
+ [
+ -24.002,
+ -19.216
+ ],
+ [
+ -24.996,
+ -18.604
+ ],
+ [
+ -26.337,
+ -17.715
+ ],
+ [
+ -27.361,
+ -17.007
+ ],
+ [
+ -28.579,
+ -16.1
+ ],
+ [
+ -29.65,
+ -15.274
+ ],
+ [
+ -30.712,
+ -14.389
+ ],
+ [
+ -31.857,
+ -13.41
+ ],
+ [
+ -32.708,
+ -12.62
+ ],
+ [
+ -33.977,
+ -11.421
+ ],
+ [
+ -34.484,
+ -10.898
+ ],
+ [
+ -39.704,
+ -4.845
+ ],
+ [
+ -30.301,
+ 9.31
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.987999949736,
+ 0.556999954523,
+ 0.670999983245,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 39.954,
+ 26.3
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 4,
+ "nm": "Right Wing Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 221
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 172.625,
+ 87.994,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 41.481,
+ 1.338,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 17.786,
+ -0.972
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 33.893
+ ],
+ [
+ 5.462,
+ 10.085
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 27.659,
+ -2.481
+ ],
+ [
+ 0,
+ -12.933
+ ],
+ [
+ -7.521,
+ 15.122
+ ]
+ ],
+ "v": [
+ [
+ -24.731,
+ -22.245
+ ],
+ [
+ -24.731,
+ 49.587
+ ],
+ [
+ 24.731,
+ -14.538
+ ],
+ [
+ 16.084,
+ -49.587
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.987999949736,
+ 0.556999954523,
+ 0.670999983245,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 24.981,
+ 49.838
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 3,
+ "ty": 4,
+ "nm": "Left Wing Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 221
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 82.133,
+ 87.386,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 8.513,
+ 0.951,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 7.553,
+ 15.05
+ ],
+ [
+ 0,
+ -13.027
+ ],
+ [
+ -0.335,
+ -2.908
+ ],
+ [
+ -0.058,
+ -0.438
+ ],
+ [
+ -0.117,
+ -0.726
+ ],
+ [
+ -0.09,
+ -0.486
+ ],
+ [
+ -0.125,
+ -0.601
+ ],
+ [
+ -3.916,
+ -6.693
+ ],
+ [
+ 0.013,
+ -0.01
+ ],
+ [
+ -2.293,
+ -2.695
+ ],
+ [
+ -0.006,
+ 0.005
+ ],
+ [
+ -1.229,
+ -1.236
+ ],
+ [
+ -11.256,
+ -0.993
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -5.536,
+ 10.13
+ ],
+ [
+ 0,
+ 3.018
+ ],
+ [
+ 0.05,
+ 0.44
+ ],
+ [
+ 0.097,
+ 0.735
+ ],
+ [
+ 0.08,
+ 0.488
+ ],
+ [
+ 0.111,
+ 0.608
+ ],
+ [
+ 1.596,
+ 7.551
+ ],
+ [
+ -0.012,
+ 0.008
+ ],
+ [
+ 1.863,
+ 3.151
+ ],
+ [
+ 0.006,
+ -0.005
+ ],
+ [
+ 1.17,
+ 1.366
+ ],
+ [
+ 8.247,
+ 8.078
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -17.741,
+ -0.969
+ ]
+ ],
+ "v": [
+ [
+ -15.997,
+ -49.701
+ ],
+ [
+ -24.764,
+ -14.43
+ ],
+ [
+ -24.243,
+ -5.54
+ ],
+ [
+ -24.085,
+ -4.222
+ ],
+ [
+ -23.76,
+ -2.031
+ ],
+ [
+ -23.505,
+ -0.569
+ ],
+ [
+ -23.156,
+ 1.247
+ ],
+ [
+ -14.854,
+ 22.757
+ ],
+ [
+ -14.889,
+ 22.785
+ ],
+ [
+ -8.644,
+ 31.574
+ ],
+ [
+ -8.626,
+ 31.56
+ ],
+ [
+ -5.028,
+ 35.462
+ ],
+ [
+ 24.764,
+ 49.701
+ ],
+ [
+ 24.764,
+ -22.192
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.987999949736,
+ 0.556999954523,
+ 0.670999983245,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 25.013,
+ 49.951
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 4,
+ "ty": 4,
+ "nm": "Body Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 221
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 127.378,
+ 121.859,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 53.728,
+ 64.479,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ -35.472
+ ],
+ [
+ 29.536,
+ 0
+ ],
+ [
+ 0,
+ 35.473
+ ],
+ [
+ -29.535,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 35.473
+ ],
+ [
+ -29.535,
+ 0
+ ],
+ [
+ 0,
+ -35.472
+ ],
+ [
+ 29.536,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 53.478,
+ -0.001
+ ],
+ [
+ 0,
+ 64.229
+ ],
+ [
+ -53.478,
+ -0.001
+ ],
+ [
+ 0,
+ -64.229
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.910000011968,
+ 0.501999978458,
+ 0.607999973671,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 53.728,
+ 64.479
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 5,
+ "ty": 4,
+ "nm": "Right Leg back Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 20,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 40,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 60,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 80,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 100,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 120,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 140,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 160,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 180,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 200,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 221,
+ "s": [
+ 0
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 241,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 261,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 281,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 301,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 321,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 342,
+ "s": [
+ 0
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 360,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 380,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 400,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ -16.519
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 420,
+ "s": [
+ -16.519
+ ],
+ "e": [
+ 5.022
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 440,
+ "s": [
+ 5.022
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 461
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 169.513,
+ 164.347,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 6.317,
+ 5.906,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -1.862,
+ 2.148
+ ],
+ [
+ -2.227,
+ -1.93
+ ],
+ [
+ 0,
+ -0.001
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.001,
+ 0
+ ],
+ [
+ -1.521,
+ -2.463
+ ],
+ [
+ -0.342,
+ -1.443
+ ],
+ [
+ -0.25,
+ -0.21
+ ],
+ [
+ 2.914,
+ -3.36
+ ],
+ [
+ 3.36,
+ 2.913
+ ],
+ [
+ -1.165,
+ 3.145
+ ],
+ [
+ 1.271,
+ 1.441
+ ],
+ [
+ 0.518,
+ 0.458
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 1.931,
+ -2.227
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.001,
+ 0.001
+ ],
+ [
+ 2.155,
+ 1.933
+ ],
+ [
+ 0.781,
+ 1.259
+ ],
+ [
+ 0.269,
+ 0.182
+ ],
+ [
+ 3.36,
+ 2.913
+ ],
+ [
+ -2.914,
+ 3.36
+ ],
+ [
+ -2.535,
+ -2.199
+ ],
+ [
+ 0.695,
+ -1.791
+ ],
+ [
+ -0.466,
+ -0.514
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.013,
+ -1.958
+ ]
+ ],
+ "v": [
+ [
+ -8.704,
+ -11.689
+ ],
+ [
+ -1.177,
+ -12.226
+ ],
+ [
+ -1.177,
+ -12.225
+ ],
+ [
+ -1.175,
+ -12.225
+ ],
+ [
+ -1.174,
+ -12.223
+ ],
+ [
+ 4.369,
+ -5.594
+ ],
+ [
+ 6.065,
+ -1.514
+ ],
+ [
+ 6.844,
+ -0.926
+ ],
+ [
+ 7.653,
+ 10.434
+ ],
+ [
+ -3.708,
+ 11.243
+ ],
+ [
+ -5.984,
+ 2.36
+ ],
+ [
+ -6.922,
+ -2.901
+ ],
+ [
+ -8.398,
+ -4.36
+ ],
+ [
+ -8.378,
+ -4.382
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.910000011968,
+ 0.501999978458,
+ 0.607999973671,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 10.816,
+ 14.406
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 6,
+ "ty": 4,
+ "nm": "Right Leg Middle Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 20,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 40,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 60,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 80,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 100,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 120,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 140,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 160,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 180,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 200,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 221,
+ "s": [
+ 0
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 241,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 261,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 281,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 301,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 321,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 342,
+ "s": [
+ 0
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 360,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 380,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 400,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 12.972
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 420,
+ "s": [
+ 12.972
+ ],
+ "e": [
+ -33.731
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 440,
+ "s": [
+ -33.731
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 461
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 181.563,
+ 124.451,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 6.176,
+ 5.944,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 2.844
+ ],
+ [
+ -2.947,
+ 0.002
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.763,
+ -0.865
+ ],
+ [
+ -1.203,
+ -0.866
+ ],
+ [
+ -0.326,
+ 0.006
+ ],
+ [
+ 0,
+ -4.448
+ ],
+ [
+ 4.448,
+ 0
+ ],
+ [
+ 1.18,
+ 3.14
+ ],
+ [
+ 1.905,
+ 0.256
+ ],
+ [
+ 0.693,
+ 0.007
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ -2.947
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.894,
+ 0.05
+ ],
+ [
+ 1.416,
+ 0.439
+ ],
+ [
+ 0.323,
+ -0.039
+ ],
+ [
+ 4.447,
+ 0
+ ],
+ [
+ 0,
+ 4.448
+ ],
+ [
+ -3.355,
+ 0.001
+ ],
+ [
+ -0.649,
+ -1.809
+ ],
+ [
+ -0.687,
+ -0.083
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.804,
+ -0.161
+ ]
+ ],
+ "v": [
+ [
+ -13.426,
+ -4.358
+ ],
+ [
+ -8.091,
+ -9.694
+ ],
+ [
+ -8.09,
+ -9.694
+ ],
+ [
+ -8.089,
+ -9.694
+ ],
+ [
+ -8.086,
+ -9.694
+ ],
+ [
+ 0.443,
+ -8.316
+ ],
+ [
+ 4.397,
+ -6.345
+ ],
+ [
+ 5.372,
+ -6.413
+ ],
+ [
+ 13.426,
+ 1.641
+ ],
+ [
+ 5.372,
+ 9.694
+ ],
+ [
+ -2.166,
+ 4.474
+ ],
+ [
+ -6.322,
+ 1.115
+ ],
+ [
+ -8.394,
+ 0.979
+ ],
+ [
+ -8.393,
+ 0.949
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.910000011968,
+ 0.501999978458,
+ 0.607999973671,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 4.799,
+ 5.181
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ -8.877,
+ -4.764
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 18.776,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 7,
+ "ty": 4,
+ "nm": "Left Leg back Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 20,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 40,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 60,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 80,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 100,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 120,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 140,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 160,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 180,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 200,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 221,
+ "s": [
+ 0
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 241,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 261,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 281,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 301,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 321,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 342,
+ "s": [
+ 0
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 360,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 380,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 400,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ -21.946
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 420,
+ "s": [
+ -21.946
+ ],
+ "e": [
+ 35.526
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 440,
+ "s": [
+ 35.526
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 461
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 85.359,
+ 164.351,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 15.317,
+ 5.906,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.863,
+ 2.148
+ ],
+ [
+ 2.228,
+ -1.93
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.001,
+ -0.001
+ ],
+ [
+ 1.521,
+ -2.463
+ ],
+ [
+ 0.342,
+ -1.443
+ ],
+ [
+ 0.25,
+ -0.209
+ ],
+ [
+ -2.914,
+ -3.36
+ ],
+ [
+ -3.36,
+ 2.913
+ ],
+ [
+ 1.166,
+ 3.146
+ ],
+ [
+ -1.271,
+ 1.441
+ ],
+ [
+ -0.519,
+ 0.459
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -1.931,
+ -2.227
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.001
+ ],
+ [
+ -2.154,
+ 1.933
+ ],
+ [
+ -0.782,
+ 1.259
+ ],
+ [
+ -0.27,
+ 0.182
+ ],
+ [
+ -3.361,
+ 2.914
+ ],
+ [
+ 2.914,
+ 3.36
+ ],
+ [
+ 2.535,
+ -2.199
+ ],
+ [
+ -0.695,
+ -1.791
+ ],
+ [
+ 0.466,
+ -0.514
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.013,
+ -1.958
+ ]
+ ],
+ "v": [
+ [
+ 8.704,
+ -11.689
+ ],
+ [
+ 1.177,
+ -12.226
+ ],
+ [
+ 1.176,
+ -12.225
+ ],
+ [
+ 1.175,
+ -12.225
+ ],
+ [
+ 1.173,
+ -12.222
+ ],
+ [
+ -4.368,
+ -5.594
+ ],
+ [
+ -6.064,
+ -1.514
+ ],
+ [
+ -6.844,
+ -0.926
+ ],
+ [
+ -7.653,
+ 10.434
+ ],
+ [
+ 3.707,
+ 11.243
+ ],
+ [
+ 5.983,
+ 2.36
+ ],
+ [
+ 6.921,
+ -2.901
+ ],
+ [
+ 8.399,
+ -4.36
+ ],
+ [
+ 8.378,
+ -4.382
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.910000011968,
+ 0.501999978458,
+ 0.607999973671,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 10.817,
+ 14.406
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 8,
+ "ty": 4,
+ "nm": "Left Leg Middle Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 20,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 40,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 60,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 80,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 100,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 120,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 140,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 160,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 180,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 200,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 221,
+ "s": [
+ 0
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 241,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 261,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 281,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 301,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 321,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 342,
+ "s": [
+ 0
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 360,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 380,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 400,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 23.175
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 420,
+ "s": [
+ 23.175
+ ],
+ "e": [
+ -9.918
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 440,
+ "s": [
+ -9.918
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 461
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 74.309,
+ 123.455,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 22.176,
+ 4.944,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 2.843
+ ],
+ [
+ 2.946,
+ 0.002
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.001,
+ 0
+ ],
+ [
+ 2.763,
+ -0.865
+ ],
+ [
+ 1.203,
+ -0.866
+ ],
+ [
+ 0.326,
+ 0.006
+ ],
+ [
+ 0,
+ -4.448
+ ],
+ [
+ -4.448,
+ 0
+ ],
+ [
+ -1.18,
+ 3.14
+ ],
+ [
+ -1.905,
+ 0.255
+ ],
+ [
+ -0.693,
+ 0.007
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ -2.947
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.001,
+ 0
+ ],
+ [
+ -2.894,
+ 0.049
+ ],
+ [
+ -1.415,
+ 0.439
+ ],
+ [
+ -0.324,
+ -0.039
+ ],
+ [
+ -4.448,
+ 0
+ ],
+ [
+ 0,
+ 4.447
+ ],
+ [
+ 3.354,
+ 0
+ ],
+ [
+ 0.649,
+ -1.809
+ ],
+ [
+ 0.687,
+ -0.084
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.803,
+ -0.161
+ ]
+ ],
+ "v": [
+ [
+ 13.426,
+ -4.357
+ ],
+ [
+ 8.092,
+ -9.694
+ ],
+ [
+ 8.09,
+ -9.694
+ ],
+ [
+ 8.089,
+ -9.694
+ ],
+ [
+ 8.086,
+ -9.694
+ ],
+ [
+ -0.444,
+ -8.316
+ ],
+ [
+ -4.397,
+ -6.345
+ ],
+ [
+ -5.372,
+ -6.413
+ ],
+ [
+ -13.426,
+ 1.641
+ ],
+ [
+ -5.372,
+ 9.694
+ ],
+ [
+ 2.166,
+ 4.475
+ ],
+ [
+ 6.322,
+ 1.115
+ ],
+ [
+ 8.394,
+ 0.98
+ ],
+ [
+ 8.393,
+ 0.95
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.910000011968,
+ 0.501999978458,
+ 0.607999973671,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 13.676,
+ 9.944
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 9,
+ "ty": 4,
+ "nm": "Right Leg Front Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 20,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 40,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 60,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 80,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 100,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 120,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 140,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 160,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 180,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 200,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 221,
+ "s": [
+ 0
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 241,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 261,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 281,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 301,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 321,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 342,
+ "s": [
+ 0
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 360,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 380,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 400,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ -11.13
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 420,
+ "s": [
+ -11.13
+ ],
+ "e": [
+ 20.189
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 440,
+ "s": [
+ 20.189
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 461
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 175.215,
+ 91.377,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 5.715,
+ 21.63,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -1.599,
+ -2.352
+ ],
+ [
+ -2.437,
+ 1.655
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.001,
+ 0
+ ],
+ [
+ -1.799,
+ 2.268
+ ],
+ [
+ -0.508,
+ 1.393
+ ],
+ [
+ -0.272,
+ 0.179
+ ],
+ [
+ 2.501,
+ 3.678
+ ],
+ [
+ 3.677,
+ -2.501
+ ],
+ [
+ -0.79,
+ -3.261
+ ],
+ [
+ 1.431,
+ -1.282
+ ],
+ [
+ 0.57,
+ -0.395
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 1.657,
+ 2.437
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.001,
+ -0.001
+ ],
+ [
+ 2.365,
+ -1.669
+ ],
+ [
+ 0.923,
+ -1.16
+ ],
+ [
+ 0.29,
+ -0.15
+ ],
+ [
+ 3.678,
+ -2.501
+ ],
+ [
+ -2.502,
+ -3.677
+ ],
+ [
+ -2.774,
+ 1.887
+ ],
+ [
+ 0.48,
+ 1.86
+ ],
+ [
+ -0.523,
+ 0.456
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -2.228,
+ 1.71
+ ]
+ ],
+ "v": [
+ [
+ -9.866,
+ 10.812
+ ],
+ [
+ -2.453,
+ 12.225
+ ],
+ [
+ -2.452,
+ 12.224
+ ],
+ [
+ -2.451,
+ 12.224
+ ],
+ [
+ -2.449,
+ 12.222
+ ],
+ [
+ 3.829,
+ 6.286
+ ],
+ [
+ 5.99,
+ 2.432
+ ],
+ [
+ 6.833,
+ 1.939
+ ],
+ [
+ 8.964,
+ -9.249
+ ],
+ [
+ -2.224,
+ -11.379
+ ],
+ [
+ -5.522,
+ -2.823
+ ],
+ [
+ -7.068,
+ 2.292
+ ],
+ [
+ -8.707,
+ 3.569
+ ],
+ [
+ -8.688,
+ 3.593
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.910000011968,
+ 0.501999978458,
+ 0.607999973671,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 11.715,
+ 14.13
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 10,
+ "ty": 4,
+ "nm": "Left Leg Front Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 20,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 40,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 60,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 80,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 100,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 120,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 140,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 160,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 180,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 200,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 221,
+ "s": [
+ 0
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 241,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 261,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 281,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 301,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 321,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 342,
+ "s": [
+ 0
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 360,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 380,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 400,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ -25.562
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 420,
+ "s": [
+ -25.562
+ ],
+ "e": [
+ 5.621
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 440,
+ "s": [
+ 5.621
+ ],
+ "e": [
+ 0
+ ]
+ },
+ {
+ "t": 461
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 80.157,
+ 92.381,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 18.215,
+ 22.631,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 1.599,
+ -2.351
+ ],
+ [
+ 2.437,
+ 1.656
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0.001
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.001,
+ 0
+ ],
+ [
+ 1.799,
+ 2.268
+ ],
+ [
+ 0.507,
+ 1.393
+ ],
+ [
+ 0.272,
+ 0.178
+ ],
+ [
+ -2.502,
+ 3.678
+ ],
+ [
+ -3.678,
+ -2.502
+ ],
+ [
+ 0.79,
+ -3.261
+ ],
+ [
+ -1.431,
+ -1.282
+ ],
+ [
+ -0.57,
+ -0.395
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -1.657,
+ 2.437
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -0.001,
+ -0.001
+ ],
+ [
+ -2.365,
+ -1.669
+ ],
+ [
+ -0.923,
+ -1.16
+ ],
+ [
+ -0.29,
+ -0.149
+ ],
+ [
+ -3.678,
+ -2.502
+ ],
+ [
+ 2.501,
+ -3.677
+ ],
+ [
+ 2.774,
+ 1.886
+ ],
+ [
+ -0.481,
+ 1.86
+ ],
+ [
+ 0.522,
+ 0.456
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 2.227,
+ 1.709
+ ]
+ ],
+ "v": [
+ [
+ 9.866,
+ 10.811
+ ],
+ [
+ 2.454,
+ 12.225
+ ],
+ [
+ 2.453,
+ 12.225
+ ],
+ [
+ 2.452,
+ 12.224
+ ],
+ [
+ 2.451,
+ 12.224
+ ],
+ [
+ 2.449,
+ 12.221
+ ],
+ [
+ -3.829,
+ 6.286
+ ],
+ [
+ -5.989,
+ 2.432
+ ],
+ [
+ -6.833,
+ 1.94
+ ],
+ [
+ -8.963,
+ -9.249
+ ],
+ [
+ 2.225,
+ -11.378
+ ],
+ [
+ 5.523,
+ -2.823
+ ],
+ [
+ 7.069,
+ 2.292
+ ],
+ [
+ 8.707,
+ 3.569
+ ],
+ [
+ 8.689,
+ 3.594
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.910000011968,
+ 0.501999978458,
+ 0.607999973671,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 11.715,
+ 14.131
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 2,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ }
+ ]
+ }
+ ],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "Magnifier Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "s": true,
+ "x": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.038
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "n": [
+ "0p833_0p038_0p167_0"
+ ],
+ "t": 0,
+ "s": [
+ 561.726
+ ],
+ "e": [
+ 397.726
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 1.656
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 1.539
+ ]
+ },
+ "n": [
+ "0p833_1p656_0p167_1p539"
+ ],
+ "t": 158,
+ "s": [
+ 397.726
+ ],
+ "e": [
+ 362.046
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.89
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.092
+ ]
+ },
+ "n": [
+ "0p833_0p89_0p167_0p092"
+ ],
+ "t": 213,
+ "s": [
+ 362.046
+ ],
+ "e": [
+ 755.184
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 1.168
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.483
+ ]
+ },
+ "n": [
+ "0p833_1p168_0p167_0p483"
+ ],
+ "t": 298,
+ "s": [
+ 755.184
+ ],
+ "e": [
+ 825.698
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.41
+ ],
+ "y": [
+ 1.968
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.077
+ ]
+ },
+ "n": [
+ "0p41_1p968_0p167_0p077"
+ ],
+ "t": 365,
+ "s": [
+ 825.698
+ ],
+ "e": [
+ 561.726
+ ]
+ },
+ {
+ "t": 480
+ }
+ ],
+ "ix": 3
+ },
+ "y": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 1.864
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0
+ ]
+ },
+ "n": [
+ "0p833_1p864_0p167_0"
+ ],
+ "t": 0,
+ "s": [
+ 424.58
+ ],
+ "e": [
+ 524.58
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 1.025
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.153
+ ]
+ },
+ "n": [
+ "0p833_1p025_0p167_0p153"
+ ],
+ "t": 158,
+ "s": [
+ 524.58
+ ],
+ "e": [
+ 327.82
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.976
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.028
+ ]
+ },
+ "n": [
+ "0p833_0p976_0p167_0p028"
+ ],
+ "t": 213,
+ "s": [
+ 327.82
+ ],
+ "e": [
+ 600.6
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.947
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ -0.026
+ ]
+ },
+ "n": [
+ "0p833_0p947_0p167_-0p026"
+ ],
+ "t": 298,
+ "s": [
+ 600.6
+ ],
+ "e": [
+ 398.261
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.41
+ ],
+ "y": [
+ 1.968
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ -0.704
+ ]
+ },
+ "n": [
+ "0p41_1p968_0p167_-0p704"
+ ],
+ "t": 365,
+ "s": [
+ 398.261
+ ],
+ "e": [
+ 424.58
+ ]
+ },
+ {
+ "t": 480
+ }
+ ],
+ "ix": 4
+ }
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 284.046,
+ 284.4,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 63.655,
+ -63.655
+ ],
+ [
+ 63.655,
+ 63.655
+ ],
+ [
+ -63.656,
+ 63.656
+ ],
+ [
+ -63.656,
+ -63.655
+ ]
+ ],
+ "o": [
+ [
+ -63.656,
+ 63.655
+ ],
+ [
+ -63.656,
+ -63.655
+ ],
+ [
+ 63.655,
+ -63.655
+ ],
+ [
+ 63.655,
+ 63.656
+ ]
+ ],
+ "v": [
+ [
+ 115.259,
+ 115.258
+ ],
+ [
+ -115.258,
+ 115.258
+ ],
+ [
+ -115.258,
+ -115.259
+ ],
+ [
+ 115.259,
+ -115.259
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 1,
+ 1,
+ 1,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 10,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 218.678,
+ 218.679
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 4,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "v": [
+ [
+ 33.9,
+ -7.819
+ ],
+ [
+ -7.82,
+ 33.9
+ ],
+ [
+ -33.9,
+ 7.819
+ ],
+ [
+ 7.819,
+ -33.9
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.310000011968,
+ 0.310000011968,
+ 0.380000005984,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 370.488,
+ 371.196
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 2",
+ "np": 4,
+ "cix": 2,
+ "bm": 0,
+ "ix": 2,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ },
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 63.655,
+ -63.655
+ ],
+ [
+ 63.655,
+ 63.655
+ ],
+ [
+ -63.656,
+ 63.656
+ ],
+ [
+ -63.656,
+ -63.655
+ ]
+ ],
+ "o": [
+ [
+ -63.656,
+ 63.655
+ ],
+ [
+ -63.656,
+ -63.655
+ ],
+ [
+ 63.655,
+ -63.655
+ ],
+ [
+ 63.655,
+ 63.656
+ ]
+ ],
+ "v": [
+ [
+ 49.891,
+ 49.537
+ ],
+ [
+ -180.626,
+ 49.537
+ ],
+ [
+ -180.626,
+ -180.98
+ ],
+ [
+ 49.891,
+ -180.98
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ind": 1,
+ "ty": "sh",
+ "ix": 2,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ 70.749,
+ 70.748
+ ],
+ [
+ 77.714,
+ -77.715
+ ],
+ [
+ -77.714,
+ -77.714
+ ],
+ [
+ -77.879,
+ 57.42
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ -11.521,
+ 11.52
+ ],
+ [
+ 11.52,
+ 11.521
+ ],
+ [
+ 0,
+ 0
+ ]
+ ],
+ "o": [
+ [
+ -77.714,
+ -77.715
+ ],
+ [
+ -77.714,
+ 77.714
+ ],
+ [
+ 70.503,
+ 70.504
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 11.521,
+ 11.52
+ ],
+ [
+ 11.52,
+ -11.521
+ ],
+ [
+ 0,
+ 0
+ ],
+ [
+ 58.064,
+ -77.923
+ ]
+ ],
+ "v": [
+ [
+ 75.346,
+ -206.435
+ ],
+ [
+ -206.082,
+ -206.435
+ ],
+ [
+ -206.082,
+ 74.993
+ ],
+ [
+ 52.542,
+ 94.615
+ ],
+ [
+ 230.556,
+ 272.63
+ ],
+ [
+ 272.276,
+ 272.63
+ ],
+ [
+ 272.276,
+ 230.91
+ ],
+ [
+ 94.361,
+ 52.995
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 2",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "mm",
+ "mm": 1,
+ "nm": "Merge Paths 1",
+ "mn": "ADBE Vector Filter - Merge",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.263000009574,
+ 0.263000009574,
+ 0.325,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 284.046,
+ 284.4
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 3",
+ "np": 6,
+ "cix": 2,
+ "bm": 0,
+ "ix": 3,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 2,
+ "ty": 4,
+ "nm": "trkmt",
+ "td": 1,
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 495.691,
+ 377.62,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 510.441,
+ 357.903,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -44.805,
+ 56.867
+ ],
+ [
+ -24.984,
+ 20.006
+ ],
+ [
+ -111.038,
+ -26.15
+ ],
+ [
+ -63.399,
+ -36.532
+ ],
+ [
+ -50.06,
+ 5.139
+ ],
+ [
+ -46.89,
+ -23.349
+ ],
+ [
+ -0.606,
+ -45.871
+ ],
+ [
+ 38.348,
+ -57.319
+ ],
+ [
+ 189.499,
+ 34.413
+ ],
+ [
+ 38.147,
+ 16.263
+ ],
+ [
+ 54.7,
+ 59.094
+ ],
+ [
+ 14.997,
+ 47.397
+ ]
+ ],
+ "o": [
+ [
+ 22.376,
+ -28.4
+ ],
+ [
+ 86.745,
+ -69.462
+ ],
+ [
+ 72.384,
+ 17.047
+ ],
+ [
+ 43.4,
+ 25.009
+ ],
+ [
+ 47.721,
+ -4.899
+ ],
+ [
+ 45.736,
+ 22.774
+ ],
+ [
+ 0.911,
+ 68.889
+ ],
+ [
+ -113.496,
+ 169.648
+ ],
+ [
+ -40.795,
+ -7.408
+ ],
+ [
+ -76.376,
+ -32.562
+ ],
+ [
+ -33.2,
+ -35.869
+ ],
+ [
+ -21.666,
+ -68.48
+ ]
+ ],
+ "v": [
+ [
+ -441.944,
+ -170.037
+ ],
+ [
+ -368.577,
+ -243.562
+ ],
+ [
+ -47.069,
+ -331.503
+ ],
+ [
+ 144.356,
+ -223.876
+ ],
+ [
+ 283.608,
+ -191.623
+ ],
+ [
+ 438.74,
+ -202.079
+ ],
+ [
+ 502.471,
+ -61.96
+ ],
+ [
+ 444.379,
+ 141.391
+ ],
+ [
+ -95.78,
+ 323.24
+ ],
+ [
+ -214.284,
+ 287.847
+ ],
+ [
+ -413.534,
+ 148.254
+ ],
+ [
+ -495.334,
+ 25.334
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.925,
+ 0.980000035903,
+ 0.984000052658,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 517.25,
+ 357.903
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 4,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 3,
+ "ty": 0,
+ "nm": "Bug",
+ "tt": 1,
+ "refId": "comp_0",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 0,
+ "s": [
+ 0
+ ],
+ "e": [
+ -64.233
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 8,
+ "s": [
+ -64.233
+ ],
+ "e": [
+ -66.642
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 22,
+ "s": [
+ -66.642
+ ],
+ "e": [
+ -78.841
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 50,
+ "s": [
+ -78.841
+ ],
+ "e": [
+ 1.302
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 80,
+ "s": [
+ 1.302
+ ],
+ "e": [
+ -16.313
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 95,
+ "s": [
+ -16.313
+ ],
+ "e": [
+ 34.072
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 110,
+ "s": [
+ 34.072
+ ],
+ "e": [
+ 71.14
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 158,
+ "s": [
+ 71.14
+ ],
+ "e": [
+ 83.164
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 204,
+ "s": [
+ 83.164
+ ],
+ "e": [
+ 187.398
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 228,
+ "s": [
+ 187.398
+ ],
+ "e": [
+ 181.539
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 251,
+ "s": [
+ 181.539
+ ],
+ "e": [
+ 116.335
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 283,
+ "s": [
+ 116.335
+ ],
+ "e": [
+ 182.269
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 313,
+ "s": [
+ 182.269
+ ],
+ "e": [
+ 243.732
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 335,
+ "s": [
+ 243.732
+ ],
+ "e": [
+ 346.818
+ ]
+ },
+ {
+ "i": {
+ "x": [
+ 0.833
+ ],
+ "y": [
+ 0.833
+ ]
+ },
+ "o": {
+ "x": [
+ 0.167
+ ],
+ "y": [
+ 0.167
+ ]
+ },
+ "n": [
+ "0p833_0p833_0p167_0p167"
+ ],
+ "t": 364,
+ "s": [
+ 346.818
+ ],
+ "e": [
+ 360
+ ]
+ },
+ {
+ "t": 480
+ }
+ ],
+ "ix": 10
+ },
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.787
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.487
+ },
+ "n": "0p833_0p787_0p167_0p487",
+ "t": 0,
+ "s": [
+ 496.5,
+ 361,
+ 0
+ ],
+ "e": [
+ 180.652,
+ 211.183,
+ 0
+ ],
+ "to": [
+ -25.6666660308838,
+ -139,
+ 0
+ ],
+ "ti": [
+ 38.5121078491211,
+ 221.946929931641,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.829
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.133
+ },
+ "n": "0p833_0p829_0p167_0p133",
+ "t": 110,
+ "s": [
+ 180.652,
+ 211.183,
+ 0
+ ],
+ "e": [
+ 666.689,
+ 78.876,
+ 0
+ ],
+ "to": [
+ -16.0409030914307,
+ -92.4444122314453,
+ 0
+ ],
+ "ti": [
+ -215.122787475586,
+ -79.44287109375,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.832
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.158
+ },
+ "n": "0p833_0p832_0p167_0p158",
+ "t": 204,
+ "s": [
+ 666.689,
+ 78.876,
+ 0
+ ],
+ "e": [
+ 733.81,
+ 345.281,
+ 0
+ ],
+ "to": [
+ 109.584777832031,
+ 40.4686546325684,
+ 0
+ ],
+ "ti": [
+ -0.57743167877197,
+ -97.171501159668,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.788
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.165
+ },
+ "n": "0p833_0p788_0p167_0p165",
+ "t": 251,
+ "s": [
+ 733.81,
+ 345.281,
+ 0
+ ],
+ "e": [
+ 987.279,
+ 618.193,
+ 0
+ ],
+ "to": [
+ 0.80155664682388,
+ 134.887756347656,
+ 0
+ ],
+ "ti": [
+ 6.83080244064331,
+ -182.008163452148,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.91
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.13
+ },
+ "n": "0p833_0p91_0p167_0p13",
+ "t": 313,
+ "s": [
+ 987.279,
+ 618.193,
+ 0
+ ],
+ "e": [
+ 602.46,
+ 795.607,
+ 0
+ ],
+ "to": [
+ -7.21150970458984,
+ 192.152191162109,
+ 0
+ ],
+ "ti": [
+ 117.196182250977,
+ 67.7952270507812,
+ 0
+ ]
+ },
+ {
+ "i": {
+ "x": 0.833,
+ "y": 0.535
+ },
+ "o": {
+ "x": 0.167,
+ "y": 0.249
+ },
+ "n": "0p833_0p535_0p167_0p249",
+ "t": 360,
+ "s": [
+ 602.46,
+ 795.607,
+ 0
+ ],
+ "e": [
+ 496.5,
+ 361,
+ 0
+ ],
+ "to": [
+ -111.463989257812,
+ -64.4792938232422,
+ 0
+ ],
+ "ti": [
+ -0.74696934223175,
+ 1.09439694881439,
+ 0
+ ]
+ },
+ {
+ "t": 480
+ }
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 125,
+ 125,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "w": 250,
+ "h": 250,
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ },
+ {
+ "ddd": 0,
+ "ind": 4,
+ "ty": 4,
+ "nm": "BG Outlines",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 11
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 10
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 495.691,
+ 377.62,
+ 0
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 510.441,
+ 357.903,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ],
+ "ix": 6
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "gr",
+ "it": [
+ {
+ "ind": 0,
+ "ty": "sh",
+ "ix": 1,
+ "ks": {
+ "a": 0,
+ "k": {
+ "i": [
+ [
+ -44.805,
+ 56.867
+ ],
+ [
+ -24.984,
+ 20.006
+ ],
+ [
+ -111.038,
+ -26.15
+ ],
+ [
+ -63.399,
+ -36.532
+ ],
+ [
+ -50.06,
+ 5.139
+ ],
+ [
+ -46.89,
+ -23.349
+ ],
+ [
+ -0.606,
+ -45.871
+ ],
+ [
+ 38.348,
+ -57.319
+ ],
+ [
+ 189.499,
+ 34.413
+ ],
+ [
+ 38.147,
+ 16.263
+ ],
+ [
+ 54.7,
+ 59.094
+ ],
+ [
+ 14.997,
+ 47.397
+ ]
+ ],
+ "o": [
+ [
+ 22.376,
+ -28.4
+ ],
+ [
+ 86.745,
+ -69.462
+ ],
+ [
+ 72.384,
+ 17.047
+ ],
+ [
+ 43.4,
+ 25.009
+ ],
+ [
+ 47.721,
+ -4.899
+ ],
+ [
+ 45.736,
+ 22.774
+ ],
+ [
+ 0.911,
+ 68.889
+ ],
+ [
+ -113.496,
+ 169.648
+ ],
+ [
+ -40.795,
+ -7.408
+ ],
+ [
+ -76.376,
+ -32.562
+ ],
+ [
+ -33.2,
+ -35.869
+ ],
+ [
+ -21.666,
+ -68.48
+ ]
+ ],
+ "v": [
+ [
+ -441.944,
+ -170.037
+ ],
+ [
+ -368.577,
+ -243.562
+ ],
+ [
+ -47.069,
+ -331.503
+ ],
+ [
+ 144.356,
+ -223.876
+ ],
+ [
+ 283.608,
+ -191.623
+ ],
+ [
+ 438.74,
+ -202.079
+ ],
+ [
+ 502.471,
+ -61.96
+ ],
+ [
+ 444.379,
+ 141.391
+ ],
+ [
+ -95.78,
+ 323.24
+ ],
+ [
+ -214.284,
+ 287.847
+ ],
+ [
+ -413.534,
+ 148.254
+ ],
+ [
+ -495.334,
+ 25.334
+ ]
+ ],
+ "c": true
+ },
+ "ix": 2
+ },
+ "nm": "Path 1",
+ "mn": "ADBE Vector Shape - Group",
+ "hd": false
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.925,
+ 0.980000035903,
+ 0.984000052658,
+ 1
+ ],
+ "ix": 4
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 5
+ },
+ "r": 1,
+ "bm": 0,
+ "nm": "Fill 1",
+ "mn": "ADBE Vector Graphic - Fill",
+ "hd": false
+ },
+ {
+ "ty": "tr",
+ "p": {
+ "a": 0,
+ "k": [
+ 517.25,
+ 357.903
+ ],
+ "ix": 2
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ],
+ "ix": 1
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100
+ ],
+ "ix": 3
+ },
+ "r": {
+ "a": 0,
+ "k": 0,
+ "ix": 6
+ },
+ "o": {
+ "a": 0,
+ "k": 100,
+ "ix": 7
+ },
+ "sk": {
+ "a": 0,
+ "k": 0,
+ "ix": 4
+ },
+ "sa": {
+ "a": 0,
+ "k": 0,
+ "ix": 5
+ },
+ "nm": "Transform"
+ }
+ ],
+ "nm": "Group 1",
+ "np": 4,
+ "cix": 2,
+ "bm": 0,
+ "ix": 1,
+ "mn": "ADBE Vector Group",
+ "hd": false
+ }
+ ],
+ "ip": 0,
+ "op": 480,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d720981..fcafd79 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,131 @@
Zen Music
Music Control Widget
+
+
+ Done. Rescan to see all the restored songs
+ Some error occurred
+ Done
+ Blacklisted songs have not been added to playlist
+ Song already in queue
+ Playing
+ Added %1$s to queue
+
+
+ Restore songs
+ Close button
+ Check button
+ Back button
+ Clear button
+ Search icon
+ Search for songs, albums, artists, playlists…
+ Sad face image
+ More menu button
+ No songs found
+ Close
+ Sort
+ Select Playlists
+ Notification access
+ Notification access granted
+ Grant access to notification
+ Zen needs access to post notifications regarding the media playing
+ Audio files access
+ Access granted
+ Grant access to read audio
+ Zen needs access to read audio files present on the device
+ Storage access
+ Zen needs storage access to read audio files present on the device
+ Grant access to read storage
+ Finish
+ Next
+ Back
+ Restore folders
+ No songs found on this device
+ Play all button
+ Play All
+ Shuffle button
+ Shuffle
+ Song image
+ Favourite button
+ Location
+ Size
+ Album
+ Artist
+ Album artist
+ Composer
+ Lyricist
+ Genre
+ Year
+ Unknown
+ Duration
+ Play count
+ Last played on
+ Never
+ Mime type
+ Oops! No albums found
+ Album art
+ No artists found
+ Create playlist button
+ Favourites
+ New Playlist
+ Create
+ Cancel
+ Create playlist
+ Playlist name
+ Nothing found
+ Nothing here
+ Folder icon
+ Play button
+ Pause button
+ Repeat mode button
+ Reset
+ Pitch: %1$sx
+ Speed: %1$sx
+ Save
+ Speed and pitch controller
+ Queue button
+ Next button
+ Previous button
+ Settings
+ Look and feel
+ Music library
+ Report bug
+ Use a theme generated from your device wallpaper
+ Theme mode
+ Choose a theme mode
+ Tabs arrangement
+ Select and reorder the tabs shown
+ Accent color
+ Light mode
+ Dark mode
+ System mode
+ App tabs
+ %1$s screen icon
+ Drag icon
+ Rescan for music
+ Search for all the songs on this device and update the library
+ Restore blacklisted songs
+ Restore blacklisted folders
+ Auto crash reporting
+ Enable this to automatically send crash reports to the developer
+ Report any bugs/crashes
+ Manually report any bugs or crashes you faced
+ Describe the bug or crash here
+ App version
+ Check what\'s new!
+ Made by
+ Choose an accent color
+ %1$s setting icon
+ Sleep Timer
+ Sleep timer button
+ Stop timer
+ Start timer
+ Stopping in %1$s
+ Playlist art
+
+
+
+ - %1$s song
+ - %1$s songs
+
\ No newline at end of file
diff --git a/app/src/test/java/com/github/pakka_papad/ExampleUnitTest.kt b/app/src/test/java/com/github/pakka_papad/ExampleUnitTest.kt
deleted file mode 100644
index c0e7dad..0000000
--- a/app/src/test/java/com/github/pakka_papad/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.pakka_papad
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/github/pakka_papad/MainDispatcherRule.kt b/app/src/test/java/com/github/pakka_papad/MainDispatcherRule.kt
new file mode 100644
index 0000000..c5f4f30
--- /dev/null
+++ b/app/src/test/java/com/github/pakka_papad/MainDispatcherRule.kt
@@ -0,0 +1,26 @@
+package com.github.pakka_papad
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+class MainDispatcherRule @OptIn(ExperimentalCoroutinesApi::class) constructor(
+ private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+) : TestWatcher() {
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun starting(description: Description?) {
+ super.starting(description)
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun finished(description: Description?) {
+ super.finished(description)
+ Dispatchers.resetMain()
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/github/pakka_papad/Util.kt b/app/src/test/java/com/github/pakka_papad/Util.kt
new file mode 100644
index 0000000..b75252e
--- /dev/null
+++ b/app/src/test/java/com/github/pakka_papad/Util.kt
@@ -0,0 +1,10 @@
+package com.github.pakka_papad
+
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+
+fun assertCollectionEquals(expected: Collection, actual: Collection) {
+ assertEquals(expected.size, actual.size)
+ actual.forEach { assertContains(expected, it) }
+ expected.forEach { assertContains(actual, it) }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/github/pakka_papad/collection/CollectionViewModelTest.kt b/app/src/test/java/com/github/pakka_papad/collection/CollectionViewModelTest.kt
new file mode 100644
index 0000000..51175bc
--- /dev/null
+++ b/app/src/test/java/com/github/pakka_papad/collection/CollectionViewModelTest.kt
@@ -0,0 +1,294 @@
+package com.github.pakka_papad.collection
+
+import com.github.pakka_papad.MainDispatcherRule
+import com.github.pakka_papad.components.SortOptions
+import com.github.pakka_papad.data.music.Artist
+import com.github.pakka_papad.data.music.ArtistWithSongs
+import com.github.pakka_papad.data.music.Playlist
+import com.github.pakka_papad.data.music.PlaylistWithSongs
+import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.data.services.PlayerService
+import com.github.pakka_papad.data.services.PlaylistService
+import com.github.pakka_papad.data.services.QueueService
+import com.github.pakka_papad.data.services.SongService
+import com.github.pakka_papad.util.MessageStore
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import io.mockk.verifyOrder
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class CollectionViewModelTest {
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ private val messageStore: MessageStore = mockk()
+ private val playlistService: PlaylistService = mockk(relaxed = true)
+ private val songService: SongService = mockk(relaxed = true)
+ private val playerService: PlayerService = mockk(relaxed = true)
+ private val queueService: QueueService = mockk(relaxed = true)
+ private lateinit var viewModel: CollectionViewModel
+
+ private val artistName = "Some artist"
+ private lateinit var artistWithSongs: ArtistWithSongs
+ private val mockMessage = "Sample message"
+ private val queueList = mutableListOf()
+
+ @Before
+ fun setup() {
+ artistWithSongs = ArtistWithSongs(
+ artist = Artist(artistName),
+ songs = buildList {
+ repeat(5) {
+ val mockSong = mockk(relaxed = true)
+ every { mockSong.location } returns "/storage/emulated/0/song$it.mp3"
+ every { mockSong.title } returns "song$it"
+ add(mockSong)
+ }
+ shuffle()
+ }
+ )
+ every { songService.getArtistWithSongsByName(artistName) } returns flowOf(artistWithSongs)
+ every { queueService.currentSong } returns MutableStateFlow(null)
+ every { queueService.queue } returns queueList
+ every { messageStore.getString(any()) } returns mockMessage
+ every { messageStore.getString(allAny()) } returns mockMessage
+ viewModel = CollectionViewModel(
+ messageStore = messageStore,
+ playlistService = playlistService,
+ songService = songService,
+ playerService = playerService,
+ queueService = queueService,
+ )
+ assertEquals("", viewModel.message.value)
+ assertNull(viewModel.collectionUi.value)
+ assertEquals(viewModel.chosenSortOrder.value, SortOptions.Default.ordinal)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private fun TestScope.load() {
+ backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
+ viewModel.loadCollection(CollectionType(CollectionType.ArtistType, artistName))
+ viewModel.collectionUi.collect()
+ }
+ }
+
+ @Test
+ fun `verify correct loading`() = runTest {
+ // Given
+ load()
+
+ // When
+
+ // Then
+ assertEquals(artistWithSongs.songs, viewModel.collectionUi.value?.songs)
+ }
+
+ @Test
+ fun `verify sort order update`() = runTest {
+ // Given
+ load()
+
+ // When
+ val sortOrder = SortOptions.TitleASC.ordinal
+ viewModel.updateSortOrder(sortOrder)
+
+ // Then
+ assertEquals(sortOrder, viewModel.chosenSortOrder.value)
+ assertEquals(
+ artistWithSongs.songs.sortedBy { it.title },
+ viewModel.collectionUi.value?.songs
+ )
+ }
+
+ @Test
+ fun `verify setQueue`() = runTest {
+ // Given
+ load()
+
+ // When
+ viewModel.setQueue(artistWithSongs.songs, 0)
+
+ // Then
+ verify(exactly = 1) {
+ queueService.setQueue(artistWithSongs.songs, 0)
+ playerService.startServiceIfNotRunning(artistWithSongs.songs, 0)
+ }
+ verifyOrder {
+ queueService.setQueue(artistWithSongs.songs, 0)
+ playerService.startServiceIfNotRunning(artistWithSongs.songs, 0)
+ }
+ }
+
+ @Test
+ fun `given empty queue verify addToQueue song`() = runTest {
+ // Given
+ load()
+
+ // When
+ val mockSong = mockk()
+ every { mockSong.location } returns "/storage/emulated/0/song0.mp3"
+ viewModel.addToQueue(mockSong)
+
+ // Then
+ verify(exactly = 1) {
+ queueService.setQueue(listOf(mockSong), 0)
+ playerService.startServiceIfNotRunning(listOf(mockSong), 0)
+ }
+ verifyOrder {
+ queueService.setQueue(listOf(mockSong), 0)
+ playerService.startServiceIfNotRunning(listOf(mockSong), 0)
+ }
+ }
+
+ private fun fillQueue(): List {
+ val songs = buildList {
+ repeat(7) {
+ val mockSong = mockk()
+ every { mockSong.location } returns "/storage/emulated/0/song$it.mp3"
+ add(mockSong)
+ }
+ }
+ queueList.addAll(songs)
+ queueService.setQueue(songs, 1)
+ return songs
+ }
+
+ @Test
+ fun `given non-empty queue verify addToQueue song`() = runTest {
+ // Given
+ load()
+ val songs = fillQueue()
+
+ // When
+ val mockSong = mockk()
+ every { mockSong.location } returns "/storage/emulated/0/new-song.mp3"
+ viewModel.addToQueue(mockSong)
+
+ // Then
+ verify(exactly = 1) {
+ queueService.append(mockSong)
+ }
+ }
+
+ @Test
+ fun `given empty queue verify addToQueue songs`() = runTest {
+ // Given
+ load()
+
+ // When
+ val mockSongs = buildList {
+ repeat(3) {
+ val mockSong = mockk()
+ every { mockSong.location } returns "/storage/emulated/0/new-song$it.mp3"
+ add(mockSong)
+ }
+ }
+ viewModel.addToQueue(mockSongs)
+
+ // Then
+ verify(exactly = 1) {
+ queueService.setQueue(mockSongs, 0)
+ playerService.startServiceIfNotRunning(mockSongs, 0)
+ }
+ verifyOrder {
+ queueService.setQueue(mockSongs, 0)
+ playerService.startServiceIfNotRunning(mockSongs, 0)
+ }
+ }
+
+ @Test
+ fun `given non-empty queue verify addToQueue songs`() = runTest {
+ // Given
+ load()
+ val songs = fillQueue()
+
+ // When
+ val mockSongs = buildList {
+ repeat(3) {
+ val mockSong = mockk()
+ every { mockSong.location } returns "/storage/emulated/0/new-song$it.mp3"
+ add(mockSong)
+ }
+ }
+ viewModel.addToQueue(mockSongs)
+
+ // Then
+ verify(exactly = 1) {
+ queueService.append(mockSongs)
+ }
+ }
+
+ @Test
+ fun `verify change favourite`() = runTest {
+ // Given
+ load()
+
+ // When
+ val location = "/storage/emulated/0/song9.mp3"
+ val mockSong = Song(
+ location = location,
+ title = "", size = "", addedDate = "", modifiedDate = "", artist = "",
+ albumArtist = "", composer = "", lyricist = "", genre = "", year = 0,
+ durationMillis = 0L, durationFormatted = "", bitrate = 0f, sampleRate = 0f)
+ viewModel.changeFavouriteValue(mockSong)
+
+ // Then
+ val slot1 = slot()
+ val slot2 = slot()
+ coVerify(exactly = 1) {
+ queueService.update(capture(slot1))
+ songService.updateSong(capture(slot2))
+ }
+ assertEquals(location, slot1.captured.location)
+ assertEquals(location, slot2.captured.location)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `verify remove from playlist`() = runTest {
+ // Given
+ val playlistId = 3445L
+ every { playlistService.getPlaylistWithSongsById(playlistId) } returns flowOf(
+ PlaylistWithSongs(
+ playlist = Playlist(
+ playlistId = playlistId,
+ playlistName = "Playlist-0",
+ createdAt = 0L
+ ),
+ songs = artistWithSongs.songs
+ )
+ )
+
+ backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
+ viewModel.loadCollection(CollectionType(CollectionType.PlaylistType, playlistId.toString()))
+ viewModel.collectionUi.collect()
+ }
+
+ // When
+ val mockSong = mockk(relaxed = true)
+ every { mockSong.location } returns "/storage/emulated/0/song0.mp3"
+ viewModel.removeFromPlaylist(mockSong)
+
+ // Then
+ val expectedList = listOf(mockSong.location)
+ coVerify(exactly = 1) {
+ playlistService.removeSongsFromPlaylist(expectedList, playlistId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/github/pakka_papad/data/services/BlacklistServiceImplTest.kt b/app/src/test/java/com/github/pakka_papad/data/services/BlacklistServiceImplTest.kt
new file mode 100644
index 0000000..9069b60
--- /dev/null
+++ b/app/src/test/java/com/github/pakka_papad/data/services/BlacklistServiceImplTest.kt
@@ -0,0 +1,283 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.data.daos.AlbumArtistDao
+import com.github.pakka_papad.data.daos.AlbumDao
+import com.github.pakka_papad.data.daos.ArtistDao
+import com.github.pakka_papad.data.daos.BlacklistDao
+import com.github.pakka_papad.data.daos.BlacklistedFolderDao
+import com.github.pakka_papad.data.daos.ComposerDao
+import com.github.pakka_papad.data.daos.GenreDao
+import com.github.pakka_papad.data.daos.LyricistDao
+import com.github.pakka_papad.data.daos.SongDao
+import com.github.pakka_papad.data.music.BlacklistedFolder
+import com.github.pakka_papad.data.music.BlacklistedSong
+import com.github.pakka_papad.data.music.Song
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+
+class BlacklistServiceImplTest {
+
+ private val blacklistDao = mockk()
+ private val blacklistedFolderDao = mockk()
+ private val songDao = mockk()
+ private val albumDao = mockk()
+ private val artistDao = mockk()
+ private val albumArtistDao = mockk()
+ private val composerDao = mockk()
+ private val lyricistDao = mockk()
+ private val genreDao = mockk()
+
+ private val initialBlacklistedSongs: List
+ private val initialBlacklistedFolders: List
+
+ private val blacklistedSongsFlow: MutableStateFlow>
+ private val blacklistedFoldersFlow: MutableStateFlow>
+
+ init {
+ initialBlacklistedSongs = buildList {
+ repeat(8){
+ add(
+ BlacklistedSong(
+ location = "/storage/emulated/0/folder0/song$it.mp3",
+ title = "Song$it",
+ artist = "Artist$it"
+ )
+ )
+ }
+ }
+ initialBlacklistedFolders = buildList {
+ repeat(7){
+ add(
+ BlacklistedFolder("/storage/emulated/0/blacklist_folder$it")
+ )
+ }
+ }
+ blacklistedSongsFlow = MutableStateFlow(initialBlacklistedSongs)
+ blacklistedFoldersFlow = MutableStateFlow(initialBlacklistedFolders)
+ }
+
+ @Before
+ fun setup() {
+ every { blacklistDao.getBlacklistedSongsFlow() } returns blacklistedSongsFlow
+ every { blacklistedFolderDao.getAllFolders() } returns blacklistedFoldersFlow
+ }
+
+ @Test
+ fun `verify blacklisting song feature`() = runTest {
+ // Given
+ val recentBlacklistSongs = mutableListOf()
+ val toBlacklist = buildList {
+ repeat(4){
+ val mockSong = mockk(relaxed = true)
+ every { mockSong.location } returns "/storage/emulated/0/folder1/song$it.mp3"
+ every { mockSong.title } returns "New Song$it"
+ every { mockSong.artist } returns "New Artist$it"
+ recentBlacklistSongs.add(
+ BlacklistedSong(mockSong.location, mockSong.title, mockSong.artist)
+ )
+ add(mockSong)
+ }
+ }
+
+ val blacklistDaoSongSlots = mutableListOf()
+ coEvery { blacklistDao.addSong(capture(blacklistDaoSongSlots)) } answers {
+ blacklistedSongsFlow.update { oldList ->
+ oldList + firstArg()
+ }
+ }
+ val songDaoSongSlots = mutableListOf()
+ coEvery { songDao.deleteSong(capture(songDaoSongSlots)) } returns Unit
+
+ val service = BlacklistServiceImpl(
+ blacklistDao = blacklistDao,
+ blacklistedFolderDao = blacklistedFolderDao,
+ songDao = songDao,
+ albumDao = albumDao,
+ artistDao = artistDao,
+ albumArtistDao = albumArtistDao,
+ composerDao = composerDao,
+ lyricistDao = lyricistDao,
+ genreDao = genreDao,
+ )
+
+ assertEquals(service.blacklistedSongs.first(), initialBlacklistedSongs)
+
+ // When
+ service.blacklistSongs(toBlacklist)
+
+ // Then
+ val currBlacklistedSongs = service.blacklistedSongs.first()
+ assertEquals(currBlacklistedSongs.size, initialBlacklistedSongs.size + toBlacklist.size)
+ initialBlacklistedSongs.forEach {
+ assertContains(currBlacklistedSongs, it)
+ }
+ recentBlacklistSongs.forEach {
+ assertContains(currBlacklistedSongs, it)
+ }
+ coVerify(exactly = toBlacklist.size) { blacklistDao.addSong(capture(blacklistDaoSongSlots)) }
+ coVerify(exactly = toBlacklist.size) { songDao.deleteSong(capture(songDaoSongSlots)) }
+ songDaoSongSlots.forEach {
+ assertContains(toBlacklist, it)
+ }
+ }
+
+ @Test
+ fun `verify blacklisting folder feature`() = runTest {
+ // Given
+ val recentBlacklistFolders = mutableListOf()
+ val toBlacklist = buildList {
+ repeat(5){
+ add("/storage/emulated/0/folder2/blacklist_folder$it")
+ recentBlacklistFolders.add(BlacklistedFolder("/storage/emulated/0/folder2/blacklist_folder$it"))
+ }
+ }
+
+ val blacklistFolderDaoSlots = mutableListOf()
+ coEvery { blacklistedFolderDao.insertFolder(capture(blacklistFolderDaoSlots)) } answers {
+ blacklistedFoldersFlow.update { oldList ->
+ oldList + firstArg()
+ }
+ }
+ println(blacklistFolderDaoSlots)
+ val songDaoSlots = mutableListOf()
+ coEvery { songDao.deleteSongsWithPathPrefix(capture(songDaoSlots)) } returns Unit
+
+ coEvery { albumDao.cleanAlbumTable() } returns Unit
+ coEvery { artistDao.cleanArtistTable() } returns Unit
+ coEvery { albumArtistDao.cleanAlbumArtistTable() } returns Unit
+ coEvery { composerDao.cleanComposerTable() } returns Unit
+ coEvery { lyricistDao.cleanLyricistTable() } returns Unit
+ coEvery { genreDao.cleanGenreTable() } returns Unit
+
+
+ val service = BlacklistServiceImpl(
+ blacklistDao = blacklistDao,
+ blacklistedFolderDao = blacklistedFolderDao,
+ songDao = songDao,
+ albumDao = albumDao,
+ artistDao = artistDao,
+ albumArtistDao = albumArtistDao,
+ composerDao = composerDao,
+ lyricistDao = lyricistDao,
+ genreDao = genreDao,
+ )
+
+ assertEquals(service.blacklistedFolders.first(), initialBlacklistedFolders)
+
+ // When
+ service.blacklistFolders(toBlacklist)
+
+ // Then
+ val currBlacklistedFolders = service.blacklistedFolders.first()
+ assertEquals(currBlacklistedFolders.size, initialBlacklistedFolders.size + toBlacklist.size, currBlacklistedFolders.toString())
+ initialBlacklistedFolders.forEach {
+ assertContains(currBlacklistedFolders, it)
+ }
+ recentBlacklistFolders.forEach {
+ assertContains(currBlacklistedFolders, it)
+ }
+ coVerify(exactly = toBlacklist.size) {
+ blacklistedFolderDao.insertFolder(capture(blacklistFolderDaoSlots))
+ }
+ coVerify(exactly = toBlacklist.size) {
+ songDao.deleteSongsWithPathPrefix(capture(songDaoSlots))
+ }
+ songDaoSlots.forEach {
+ assertContains(toBlacklist, it)
+ }
+ coVerify(atLeast = 1) {
+ albumDao.cleanAlbumTable()
+ artistDao.cleanArtistTable()
+ albumArtistDao.cleanAlbumArtistTable()
+ lyricistDao.cleanLyricistTable()
+ composerDao.cleanComposerTable()
+ genreDao.cleanGenreTable()
+ }
+ }
+
+ @Test
+ fun `verify whitelisting song feature`() = runTest {
+ // Given
+ val toWhitelist = initialBlacklistedSongs.take(3)
+
+ val blacklistDaoSlots = mutableListOf()
+ coEvery { blacklistDao.deleteBlacklistedSong(capture(blacklistDaoSlots)) } answers {
+ blacklistedSongsFlow.update { oldList ->
+ val arg = firstArg()
+ oldList.filter { it != arg }
+ }
+ }
+
+ val service = BlacklistServiceImpl(
+ blacklistDao = blacklistDao,
+ blacklistedFolderDao = blacklistedFolderDao,
+ songDao = songDao,
+ albumDao = albumDao,
+ artistDao = artistDao,
+ albumArtistDao = albumArtistDao,
+ composerDao = composerDao,
+ lyricistDao = lyricistDao,
+ genreDao = genreDao,
+ )
+
+ assertEquals(service.blacklistedSongs.first(), initialBlacklistedSongs)
+
+ // When
+ service.whitelistSongs(toWhitelist)
+
+ // Then
+ val currBlacklistedSongs = service.blacklistedSongs.first()
+ coVerify(exactly = toWhitelist.size) { blacklistDao.deleteBlacklistedSong(capture(blacklistDaoSlots)) }
+ toWhitelist.forEach { w ->
+ assertEquals(null, currBlacklistedSongs.find { it == w })
+ }
+ }
+
+ @Test
+ fun `verify whitelisting folder feature`() = runTest {
+ // Given
+ val toWhitelist = initialBlacklistedFolders.take(2)
+
+ val slots = mutableListOf()
+ coEvery { blacklistedFolderDao.deleteFolder(capture(slots)) } answers {
+ blacklistedFoldersFlow.update { oldList ->
+ val arg = firstArg()
+ oldList.filter { it != arg }
+ }
+ }
+
+ val service = BlacklistServiceImpl(
+ blacklistDao = blacklistDao,
+ blacklistedFolderDao = blacklistedFolderDao,
+ songDao = songDao,
+ albumDao = albumDao,
+ artistDao = artistDao,
+ albumArtistDao = albumArtistDao,
+ composerDao = composerDao,
+ lyricistDao = lyricistDao,
+ genreDao = genreDao,
+ )
+
+ assertEquals(service.blacklistedFolders.first(), initialBlacklistedFolders)
+
+ // When
+ service.whitelistFolders(toWhitelist)
+
+ // Then
+ val currBlacklistedFolders = service.blacklistedFolders.first()
+ coVerify(exactly = toWhitelist.size) { blacklistedFolderDao.deleteFolder(capture(slots)) }
+ toWhitelist.forEach { w ->
+ assertEquals(null, currBlacklistedFolders.find { it == w })
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/github/pakka_papad/data/services/PlaylistServiceImplTest.kt b/app/src/test/java/com/github/pakka_papad/data/services/PlaylistServiceImplTest.kt
new file mode 100644
index 0000000..2e71b43
--- /dev/null
+++ b/app/src/test/java/com/github/pakka_papad/data/services/PlaylistServiceImplTest.kt
@@ -0,0 +1,397 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.assertCollectionEquals
+import com.github.pakka_papad.data.daos.PlaylistDao
+import com.github.pakka_papad.data.music.Playlist
+import com.github.pakka_papad.data.music.PlaylistExceptId
+import com.github.pakka_papad.data.music.PlaylistSongCrossRef
+import com.github.pakka_papad.data.music.PlaylistWithSongCount
+import com.github.pakka_papad.data.thumbnails.ThumbnailDao
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class PlaylistServiceImplTest {
+
+ private val playlistDao = mockk()
+ private val thumbnailDao = mockk()
+ private val initialPlaylistsWithSongCount: List
+ private val playlistsFlow: MutableStateFlow>
+
+ init {
+ initialPlaylistsWithSongCount = buildList {
+ repeat(8) {
+ add(
+ PlaylistWithSongCount(
+ playlistId = it.toLong(),
+ playlistName = "Playlist $it",
+ createdAt = 100L + it,
+ count = it,
+ artUri = "art$it"
+ )
+ )
+ }
+ }
+ playlistsFlow = MutableStateFlow(initialPlaylistsWithSongCount)
+ every { playlistDao.getAllPlaylistWithSongCount() } returns playlistsFlow
+ }
+
+ @Test
+ fun `verify create playlist with blank input`() = runTest {
+ // Given
+ val playlistSlot = slot()
+ coEvery { playlistDao.insertPlaylist(capture(playlistSlot)) } answers {
+ val playlist = PlaylistWithSongCount(
+ playlistId = initialPlaylistsWithSongCount.size.toLong(),
+ playlistName = playlistSlot.captured.playlistName,
+ createdAt = playlistSlot.captured.createdAt,
+ count = 0,
+ artUri = null,
+ )
+ playlistsFlow.update { it + playlist }
+ playlist.playlistId
+ }
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ val result = service.createPlaylist("")
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ assertFalse(result)
+ assertCollectionEquals(initialPlaylistsWithSongCount, currPlaylists)
+ assertFalse(playlistSlot.isCaptured)
+ }
+
+ @Test
+ fun `verify create playlist with input having leading and trailing spaces`() = runTest {
+ // Given
+ val playlistSlot = slot()
+ coEvery { playlistDao.insertPlaylist(capture(playlistSlot)) } answers {
+ val playlist = PlaylistWithSongCount(
+ playlistId = initialPlaylistsWithSongCount.size.toLong(),
+ playlistName = playlistSlot.captured.playlistName,
+ createdAt = playlistSlot.captured.createdAt,
+ count = 0,
+ artUri = null,
+ )
+ playlistsFlow.update { it + playlist }
+ playlist.playlistId
+ }
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ val name = " New Playlist "
+ val result = service.createPlaylist(name)
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ assertTrue(result)
+ assertTrue(playlistSlot.isCaptured)
+ coVerify(exactly = 1) { playlistDao.insertPlaylist(any()) }
+ val otherPlaylists = currPlaylists.filter { !initialPlaylistsWithSongCount.contains(it) }
+ assertEquals(1, otherPlaylists.size)
+ }
+
+ @Test
+ fun `verify create playlist with input having no leading or trailing spaces`() = runTest {
+ // Given
+ val playlistSlot = slot()
+ coEvery { playlistDao.insertPlaylist(capture(playlistSlot)) } answers {
+ val playlist = PlaylistWithSongCount(
+ playlistId = initialPlaylistsWithSongCount.size.toLong(),
+ playlistName = playlistSlot.captured.playlistName,
+ createdAt = playlistSlot.captured.createdAt,
+ count = 0,
+ artUri = null,
+ )
+ playlistsFlow.update { it + playlist }
+ playlist.playlistId
+ }
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ val name = "New Playlist"
+ val result = service.createPlaylist(name)
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ assertTrue(result)
+ assertTrue(playlistSlot.isCaptured)
+ coVerify(exactly = 1) { playlistDao.insertPlaylist(any()) }
+ val otherPlaylists = currPlaylists.filter { !initialPlaylistsWithSongCount.contains(it) }
+ assertEquals(1, otherPlaylists.size)
+ }
+
+ @Test
+ fun `verify delete playlist with valid playlistId`() = runTest {
+ // Given
+ val playlistId = 1L
+ val playlistSlot = slot()
+ coEvery { playlistDao.deletePlaylist(capture(playlistSlot)) } answers {
+ playlistsFlow.update { oldList ->
+ oldList.filter { it.playlistId != playlistSlot.captured }
+ }
+ }
+ coEvery { playlistDao.getPlaylist(playlistId) } answers {
+ val playlistWithSongCount = playlistsFlow.value.firstOrNull {
+ it.playlistId == firstArg()
+ }
+ if (playlistWithSongCount == null) null
+ else Playlist(
+ playlistId = playlistWithSongCount.playlistId,
+ playlistName = playlistWithSongCount.playlistName,
+ createdAt = playlistWithSongCount.createdAt,
+ artUri = playlistWithSongCount.artUri,
+ )
+ }
+ val thumbLoc = slot()
+ coEvery { thumbnailDao.markDelete(capture(thumbLoc)) } returns Unit
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ service.deletePlaylist(playlistId)
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ coVerify(exactly = 1) { playlistDao.deletePlaylist(any()) }
+ val removedPlaylists = initialPlaylistsWithSongCount.filter { !currPlaylists.contains(it) }
+ assertEquals(1, removedPlaylists.size)
+ assertEquals(playlistId, removedPlaylists[0].playlistId)
+ assertEquals(thumbLoc.captured, "art$playlistId")
+ }
+
+ @Test
+ fun `verify delete playlist with invalid playlistId`() = runTest {
+ // Given
+ val playlistId = 31L
+ val playlistSlot = slot()
+ coEvery { playlistDao.deletePlaylist(capture(playlistSlot)) } answers {
+ playlistsFlow.update { oldList ->
+ oldList.filter { it.playlistId != playlistSlot.captured }
+ }
+ }
+ coEvery { playlistDao.getPlaylist(playlistId) } answers {
+ val playlistWithSongCount = playlistsFlow.value.firstOrNull {
+ it.playlistId == firstArg()
+ }
+ if (playlistWithSongCount == null) null
+ else Playlist(
+ playlistId = playlistWithSongCount.playlistId,
+ playlistName = playlistWithSongCount.playlistName,
+ createdAt = playlistWithSongCount.createdAt,
+ artUri = playlistWithSongCount.artUri,
+ )
+ }
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ service.deletePlaylist(playlistId)
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ coVerify(exactly = 1) { playlistDao.deletePlaylist(any()) }
+ val removedPlaylists = initialPlaylistsWithSongCount.filter { !currPlaylists.contains(it) }
+ assertEquals(0, removedPlaylists.size)
+ assertCollectionEquals(initialPlaylistsWithSongCount, currPlaylists)
+ }
+
+ @Test
+ fun `verify addSongToPlaylist with valid playlistId`() = runTest {
+ // Given
+ val songLocations = buildList {
+ repeat(4){ add("/storage/emulated/0/folder0/song$it.mp3") }
+ }
+ val playlistId = 1L
+ val refs = slot>()
+ coEvery { playlistDao.insertPlaylistSongCrossRef(capture(refs)) } answers {
+ refs.captured.forEach { ref ->
+ val currList = playlistsFlow.value
+ val playlist = currList
+ .firstOrNull { it.playlistId == ref.playlistId }
+ ?: return@forEach
+ val updatedPlaylist = playlist.copy(
+ count = playlist.count + 1
+ )
+ val newList = currList.filter { it != playlist } + updatedPlaylist
+ playlistsFlow.update { newList }
+ }
+ }
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ service.addSongsToPlaylist(songLocations, playlistId)
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ coVerify(exactly = 1) { playlistDao.insertPlaylistSongCrossRef(any()) }
+ val (iPNotChanged, iPChanged) = initialPlaylistsWithSongCount
+ .partition { it.playlistId != playlistId }
+ val (cPNotChanged, cPChanged) = currPlaylists
+ .partition { it.playlistId != playlistId }
+ assertCollectionEquals(iPNotChanged, cPNotChanged)
+ assertEquals(1, iPChanged.size)
+ assertEquals(1, cPChanged.size)
+ assertEquals(iPChanged[0].playlistId, cPChanged[0].playlistId)
+ assertEquals(iPChanged[0].count + songLocations.size, cPChanged[0].count)
+ }
+
+ @Test
+ fun `verify addSongToPlaylist with invalid playlistId`() = runTest {
+ // Given
+ val songLocations = buildList {
+ repeat(4){ add("/storage/emulated/0/folder0/song$it.mp3") }
+ }
+ val playlistId = 31L
+ val refs = slot>()
+ coEvery { playlistDao.insertPlaylistSongCrossRef(capture(refs)) } answers {
+ refs.captured.forEach { ref ->
+ val currList = playlistsFlow.value
+ val playlist = currList
+ .firstOrNull { it.playlistId == ref.playlistId }
+ ?: return@forEach
+ val updatedPlaylist = playlist.copy(
+ count = playlist.count + 1
+ )
+ val newList = currList.filter { it != playlist } + updatedPlaylist
+ playlistsFlow.update { newList }
+ }
+ }
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ service.addSongsToPlaylist(songLocations, playlistId)
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ coVerify(exactly = 1) { playlistDao.insertPlaylistSongCrossRef(any()) }
+ assertCollectionEquals(initialPlaylistsWithSongCount, currPlaylists)
+ }
+
+ @Test
+ fun `verify removeSongFromPlaylist with valid playlistId`() = runTest {
+ // Given
+ val songLocations = buildList {
+ repeat(2) { add("/storage/emulated/0/song$it.mp3") }
+ }
+ val playlistId = 3L
+ val slots = mutableListOf()
+ coEvery { playlistDao.deletePlaylistSongCrossRef(capture(slots)) } answers {
+ val arg = firstArg()
+ val playlist = playlistsFlow.value
+ .firstOrNull { it.playlistId == arg.playlistId }
+ playlist?.let { p ->
+ val updatedPlaylist = p.copy(
+ count = p.count - 1
+ )
+ playlistsFlow.update { oldList ->
+ oldList.filter { it.playlistId != arg.playlistId } + updatedPlaylist
+ }
+ }
+ }
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ service.removeSongsFromPlaylist(songLocations, playlistId)
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ coVerify(exactly = songLocations.size) { playlistDao.deletePlaylistSongCrossRef(any()) }
+ val (iPNotChanged, iPChanged) = initialPlaylistsWithSongCount
+ .partition { it.playlistId != playlistId }
+ val (cPNotChanged, cPChanged) = currPlaylists
+ .partition { it.playlistId != playlistId }
+ assertCollectionEquals(iPNotChanged, cPNotChanged)
+ assertEquals(1, iPChanged.size)
+ assertEquals(1, cPChanged.size)
+ assertEquals(iPChanged[0].playlistId, cPChanged[0].playlistId)
+ assertEquals(iPChanged[0].count - songLocations.size, cPChanged[0].count)
+ }
+
+ @Test
+ fun `verify removeSongFromPlaylist with invalid playlistId`() = runTest {
+ // Given
+ val songLocations = buildList {
+ repeat(2) { add("/storage/emulated/0/song$it.mp3") }
+ }
+ val playlistId = 31L
+ val slots = mutableListOf()
+ coEvery { playlistDao.deletePlaylistSongCrossRef(capture(slots)) } answers {
+ val arg = firstArg()
+ val playlist = playlistsFlow.value
+ .firstOrNull { it.playlistId == arg.playlistId }
+ playlist?.let { p ->
+ val updatedPlaylist = p.copy(
+ count = p.count - 1
+ )
+ playlistsFlow.update { oldList ->
+ oldList.filter { it.playlistId != arg.playlistId } + updatedPlaylist
+ }
+ }
+ }
+
+ val service = PlaylistServiceImpl(
+ playlistDao = playlistDao,
+ thumbnailDao = thumbnailDao,
+ )
+ assertCollectionEquals(initialPlaylistsWithSongCount, service.playlists.first())
+
+ // When
+ service.removeSongsFromPlaylist(songLocations, playlistId)
+
+ // Then
+ val currPlaylists = service.playlists.first()
+ coVerify(exactly = songLocations.size) { playlistDao.deletePlaylistSongCrossRef(any()) }
+ assertCollectionEquals(initialPlaylistsWithSongCount, currPlaylists)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/github/pakka_papad/data/services/QueueServiceImplTest.kt b/app/src/test/java/com/github/pakka_papad/data/services/QueueServiceImplTest.kt
new file mode 100644
index 0000000..e08d4b8
--- /dev/null
+++ b/app/src/test/java/com/github/pakka_papad/data/services/QueueServiceImplTest.kt
@@ -0,0 +1,450 @@
+package com.github.pakka_papad.data.services
+
+import com.github.pakka_papad.assertCollectionEquals
+import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.nowplaying.RepeatMode
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class QueueServiceImplTest {
+
+ private val service = QueueServiceImpl()
+ private val callback = mockk(relaxed = true)
+
+ @Before
+ fun setup(){
+ service.addListener(callback)
+ }
+
+ @After
+ fun close(){
+ assertCollectionEquals(service.locations, service.queue.map { it.location })
+ }
+
+ @Test
+ fun `given empty queue verify append song`() = runTest {
+ // Given
+ assertEquals(emptyList(), service.queue)
+
+ // When
+ val mockSongLocation = "storage/emulated/0/song0.mp3"
+ val mockkSong = mockk()
+ every { mockkSong.location } returns mockSongLocation
+ val result = service.append(mockkSong)
+
+ // Then
+ assertTrue(result)
+ assertEquals(1, service.queue.size)
+ assertEquals(mockSongLocation, service.queue[0].location)
+ assertEquals(1, service.locations.size)
+ assertContains(service.locations, mockSongLocation)
+ verify(exactly = 1) {
+ callback.onAppend(mockkSong)
+ }
+ verify(exactly = 0) {
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `given empty queue verify append songs`() = runTest {
+ // Given
+ assertEquals(emptyList(), service.queue)
+
+ // When
+ val mockSongLocations = mutableListOf()
+ val mockkSongs = buildList {
+ repeat(5) {
+ val loc = "storage/emulated/0/song$it.mp3"
+ val mockSong = mockk()
+ every { mockSong.location } returns loc
+ mockSongLocations.add(loc)
+ add(mockSong)
+ }
+ }
+ val result = service.append(mockkSongs)
+
+ // Then
+ assertTrue(result)
+ assertEquals(mockkSongs, service.queue)
+ verify(exactly = 1) {
+ callback.onAppend(mockkSongs)
+ }
+ verify(exactly = 0) {
+ callback.onAppend(capture(slot()))
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ private fun setupQueue(){
+ repeat(6){
+ val mockSong = mockk(relaxed = true)
+ every { mockSong.location } returns "/storage/emulated/0/mock-song$it.mp3"
+ service.mutableQueue.add(mockSong)
+ service.locations.add(mockSong.location)
+ }
+ }
+
+ @Test
+ fun `given non-empty queue verify append song`() = runTest {
+ // Given
+ setupQueue()
+ val initialQueue = service.queue
+ val initialLocations = service.locations.toList()
+
+ // When
+ val mockSongLocation = "/storage/emulated/0/song0.mp3"
+ val mockSong = mockk()
+ every { mockSong.location } returns mockSongLocation
+ service.append(mockSong)
+
+ // Then
+ assertEquals(initialQueue + mockSong, service.queue)
+ assertCollectionEquals(initialLocations + mockSongLocation, service.locations)
+ verify(exactly = 1) { callback.onAppend(mockSong) }
+ verify(exactly = 0) {
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `given non-empty queue verify append songs`() = runTest {
+ // Given
+ setupQueue()
+ val initialQueue = service.queue
+ val initialLocations = service.locations.toList()
+
+ // When
+ val mockSongLocations = mutableListOf()
+ val mockSongs = buildList {
+ repeat(5) {
+ val mockSong = mockk()
+ every { mockSong.location } returns "/storage/emulated/0/song$it.mp3"
+ mockSongLocations.add(mockSong.location)
+ add(mockSong)
+ }
+ }
+ service.append(mockSongs)
+
+ // Then
+ assertEquals(initialQueue + mockSongs, service.queue)
+ assertCollectionEquals(initialLocations + mockSongLocations, service.locations)
+ verify(exactly = 1) { callback.onAppend(mockSongs) }
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify update song when song is not in queue`() = runTest {
+ // Given
+ setupQueue()
+ val initialQueue = service.queue
+ val initialLocations = service.locations.toList()
+
+ // When
+ val mockSongLocation = "storage/emulated/0/song0.mp3"
+ val mockkSong = mockk()
+ every { mockkSong.location } returns mockSongLocation
+ val result = service.update(mockkSong)
+
+ // Then
+ assertFalse(result)
+ assertEquals(initialQueue, service.queue)
+ assertCollectionEquals(initialLocations, service.locations)
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify update song when song is in queue`() = runTest {
+ // Given
+ setupQueue()
+ val iQueue = service.queue
+ val initialLocations = service.locations.toList()
+
+ // When
+ val song = mockk()
+ every { song.location } returns iQueue[0].location
+ every { song.favourite } returns true
+ val result = service.update(song)
+
+ // Then
+ assertTrue(result)
+ assertEquals(
+ listOf(song) + iQueue.slice(1 until iQueue.size),
+ service.queue
+ )
+ assertCollectionEquals(initialLocations, service.locations)
+ verify(exactly = 1) { callback.onUpdate(song, any()) }
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify update song when song is in queue and currently playing`() = runTest {
+ // Given
+ setupQueue()
+ val iQueue = service.queue
+ service._currentSong.update { service.queue[0] }
+ val initialLocations = service.locations.toList()
+
+ // When
+ val song = mockk()
+ every { song.location } returns iQueue[0].location
+ every { song.favourite } returns true
+ val result = service.update(song)
+
+ // Then
+ assertTrue(result)
+ assertEquals(
+ listOf(song) + iQueue.slice(1 until iQueue.size),
+ service.queue
+ )
+ assertCollectionEquals(initialLocations, service.locations)
+ verify(exactly = 1) { callback.onUpdate(song, 0) }
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify moveSong with invalid indices`() = runTest {
+ // Given
+ setupQueue()
+ val s = service.queue.size
+ val initialLocations = service.locations.toList()
+
+ // When
+ val result = service.moveSong(1,s)
+
+ // Then
+ assertFalse(result)
+ assertCollectionEquals(initialLocations, service.locations)
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify moveSong with valid indices`() = runTest {
+ // Given
+ setupQueue()
+ val s = service.queue.size
+ val initialLocations = service.locations.toList()
+
+ // When
+ val result = service.moveSong(1,s-1)
+
+ // Then
+ assertTrue(result)
+ assertCollectionEquals(initialLocations, service.locations)
+ verify(exactly = 1) { callback.onMove(1, s-1) }
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify clearQueue`() = runTest {
+ // Given
+ setupQueue()
+
+ // When
+ service.clearQueue()
+
+ // Then
+ verify(exactly = 1) { callback.onClear() }
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onSetQueue(any(), any())
+ }
+ assertEquals(0, service.locations.size)
+ }
+
+ @Test
+ fun `verify setQueue`() = runTest {
+ // Given
+ setupQueue()
+
+ // When
+ val newQueue = buildList {
+ repeat(5) {
+ val mockSong = mockk()
+ every { mockSong.location } returns "/storage/emulated/0/song$it.mp3"
+ add(mockSong)
+ }
+ }
+ service.setQueue(newQueue, 1)
+
+ // Then
+ assertEquals(newQueue, service.queue)
+ assertCollectionEquals(service.locations, newQueue.map { it.location })
+ verify(exactly = 1) { callback.onSetQueue(newQueue, 1) }
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ }
+ }
+
+ @Test
+ fun `verify getSongAtIndex with valid index`() = runTest {
+ // Given
+ setupQueue()
+
+ // When
+ val result = service.getSongAtIndex(service.queue.size-1)
+
+ // Then
+ assertNotNull(result)
+ assertContains(service.queue, result)
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify getSongAtIndex with invalid index`() = runTest {
+ // Given
+ setupQueue()
+
+ // When
+ val result = service.getSongAtIndex(service.queue.size)
+
+ // Then
+ assertNull(result)
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify setCurrentSong with valid index`() = runTest {
+ // Given
+ setupQueue()
+
+ // When
+ val index = service.queue.size-1
+ val song = service.queue[index]
+ service.setCurrentSong(index)
+
+ // Then
+ assertEquals(service.currentSong.value?.location, song.location)
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify setCurrentSong with invalid index`() = runTest {
+ // Given
+ setupQueue()
+
+ // When
+ val index = service.queue.size
+ service.setCurrentSong(index)
+
+ // Then
+ verify(exactly = 0) {
+ callback.onAppend(any())
+ callback.onAppend(any>())
+ callback.onUpdate(any(), any())
+ callback.onMove(any(), any())
+ callback.onClear()
+ callback.onSetQueue(any(), any())
+ }
+ }
+
+ @Test
+ fun `verify update repeat mode`() = runTest {
+ // Given
+ setupQueue()
+
+ // When
+ service.updateRepeatMode(RepeatMode.REPEAT_ALL)
+
+ // Then
+ assertEquals(service.repeatMode.value, RepeatMode.REPEAT_ALL)
+ verify(exactly = 0) {
+ callback.onAppend(any