diff --git a/.gitignore b/.gitignore
index a446860..98f2766 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,9 @@
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/deploymentTargetDropDown.xml
+/.idea/kotlinc.xml
+/.idea/misc.xml
+/buildSrc/build
.DS_Store
/build
/captures
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index fb7f4a8..b589d56 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 360e6d4..0ad17cb 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 976b3d2..67c4325 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -110,6 +110,11 @@ dependencies {
implementation(Libraries.androidxComposeConstraintLayout)
implementation(Libraries.androidxSplashScreen)
+ implementation(Libraries.androidxGlance)
+ implementation(Libraries.androidxGlanceAppWidget)
+ implementation(Libraries.androidxGlanceMaterial)
+ implementation(Libraries.androidxGlanceMaterial3)
+
implementation(Libraries.androidxComposeMaterial)
implementation(Libraries.material3)
implementation(Libraries.material3WindowSizeClass)
diff --git a/app/schemas/com.github.pakka_papad.data.AppDatabase/2.json b/app/schemas/com.github.pakka_papad.data.AppDatabase/2.json
new file mode 100644
index 0000000..c74395a
--- /dev/null
+++ b/app/schemas/com.github.pakka_papad.data.AppDatabase/2.json
@@ -0,0 +1,625 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "e3253979ea544495dba0fdae8f495fba",
+ "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)",
+ "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
+ }
+ ],
+ "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"
+ ]
+ }
+ ]
+ }
+ ],
+ "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, 'e3253979ea544495dba0fdae8f495fba')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index db27f40..6671a36 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -36,6 +36,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/changelogs.json b/app/src/main/assets/changelogs.json
index b5ac258..7a39b56 100644
--- a/app/src/main/assets/changelogs.json
+++ b/app/src/main/assets/changelogs.json
@@ -22,5 +22,17 @@
"Added option to select repeat mode",
"Some UI improvements"
]
+ },
+ {
+ "versionCode": 10200,
+ "versionName": "1.2.0",
+ "date": "2 December, 2023",
+ "changes": [
+ "Added folder tab",
+ "Added option to blacklist folder",
+ "Updated settings screen UI",
+ "Added sort option",
+ "Added basic widget (experimental)"
+ ]
}
]
\ 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 d746a2f..1b6b5e6 100644
--- a/app/src/main/java/com/github/pakka_papad/Constants.kt
+++ b/app/src/main/java/com/github/pakka_papad/Constants.kt
@@ -12,7 +12,9 @@ object Constants {
const val ALBUM_ARTIST_TABLE = "album_artist_table"
const val COMPOSER_TABLE = "composer_table"
const val LYRICIST_TABLE = "lyricist_table"
- const val BLACKLIST_TABLE = "blacklist_table"
+ 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 PACKAGE_NAME = "com.github.pakka_papad"
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/Screens.kt b/app/src/main/java/com/github/pakka_papad/Screens.kt
index 7a4889a..f7e1aae 100644
--- a/app/src/main/java/com/github/pakka_papad/Screens.kt
+++ b/app/src/main/java/com/github/pakka_papad/Screens.kt
@@ -4,6 +4,9 @@ import android.os.Parcelable
import androidx.annotation.DrawableRes
import kotlinx.parcelize.Parcelize
+/**
+ * !! Dot not change order of already added objects
+ */
@Parcelize
enum class Screens(@DrawableRes val outlinedIcon: Int, @DrawableRes val filledIcon: Int): Parcelable {
Songs(R.drawable.ic_outline_music_note_40,R.drawable.ic_baseline_music_note_40),
@@ -11,4 +14,5 @@ enum class Screens(@DrawableRes val outlinedIcon: Int, @DrawableRes val filledIc
Artists(R.drawable.ic_outline_person_40,R.drawable.ic_baseline_person_40),
Playlists(R.drawable.ic_outline_library_music_40,R.drawable.ic_baseline_library_music_40),
Genres(R.drawable.ic_baseline_piano_40,R.drawable.ic_baseline_piano_40),
+ Folders(R.drawable.ic_outline_folder_40,R.drawable.ic_baseline_folder_40)
}
diff --git a/app/src/main/java/com/github/pakka_papad/Util.kt b/app/src/main/java/com/github/pakka_papad/Util.kt
index 69b5578..d8a22e1 100644
--- a/app/src/main/java/com/github/pakka_papad/Util.kt
+++ b/app/src/main/java/com/github/pakka_papad/Util.kt
@@ -18,7 +18,7 @@ fun Float.toMBfromB(): String{
return "${mb.round(2)} MB"
}
-val dateFormat = SimpleDateFormat("dd:MM:yyyy")
+val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
fun Long.formatToDate(): String {
val calender = Calendar.getInstance().apply {
diff --git a/app/src/main/java/com/github/pakka_papad/collection/CollectionActions.kt b/app/src/main/java/com/github/pakka_papad/collection/CollectionActions.kt
index 1f4c63b..bf19166 100644
--- a/app/src/main/java/com/github/pakka_papad/collection/CollectionActions.kt
+++ b/app/src/main/java/com/github/pakka_papad/collection/CollectionActions.kt
@@ -26,4 +26,11 @@ sealed class CollectionActions(
text = "Add all to playlist",
icon = R.drawable.ic_baseline_playlist_add_40
)
+
+ data class Sort(override val onClick: () -> Unit) :
+ CollectionActions(
+ onClick = onClick,
+ text = "Sort",
+ icon = R.drawable.ic_baseline_sort_40
+ )
}
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 a3aaff0..da0988d 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
@@ -34,6 +34,8 @@ 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.SortOptionChooser
+import com.github.pakka_papad.components.SortOptions
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.data.music.Song
import com.github.pakka_papad.ui.theme.ZenTheme
@@ -63,6 +65,15 @@ class CollectionFragment : Fragment() {
navController.popBackStack()
}
viewModel.loadCollection(args.collectionType)
+ val sortOptions = listOf(
+ SortOptions.Default,
+ SortOptions.TitleASC,
+ SortOptions.TitleDSC,
+ SortOptions.YearASC,
+ SortOptions.YearDSC,
+ SortOptions.DurationASC,
+ SortOptions.DurationDSC,
+ )
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
@@ -82,6 +93,8 @@ class CollectionFragment : Fragment() {
&& songsListState.firstVisibleItemScrollOffset <= 10) 0f else 1f
}
}
+ var showSortOptions by remember { mutableStateOf(false) }
+ val chosenSortOrder by viewModel.chosenSortOrder.collectAsStateWithLifecycle()
Box {
LazyColumn(
contentPadding = insetsPadding,
@@ -183,10 +196,26 @@ class CollectionFragment : Fragment() {
},
CollectionActions.AddToPlaylist {
addAllSongsToPlaylistClicked(collectionUi?.songs)
+ },
+ CollectionActions.Sort {
+ showSortOptions = true
}
)
)
}
+ 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/CollectionViewModel.kt b/app/src/main/java/com/github/pakka_papad/collection/CollectionViewModel.kt
index 5b88c4c..23f7bb2 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
@@ -4,6 +4,7 @@ import android.app.Application
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+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
@@ -26,6 +27,9 @@ class CollectionViewModel @Inject constructor(
private val _collectionType = MutableStateFlow(null)
+ private val _chosenSortOrder = MutableStateFlow(SortOptions.Default.ordinal)
+ val chosenSortOrder = _chosenSortOrder.asStateFlow()
+
@OptIn(ExperimentalCoroutinesApi::class)
val collectionUi = _collectionType
.flatMapLatest { type ->
@@ -125,6 +129,28 @@ class CollectionViewModel @Inject constructor(
}
else -> flow { }
}
+ }.combine(_chosenSortOrder) { ui, sortOrder ->
+ when(sortOrder){
+ SortOptions.TitleASC.ordinal -> {
+ ui.copy(songs = ui.songs.sortedBy { it.title })
+ }
+ SortOptions.TitleDSC.ordinal -> {
+ ui.copy(songs = ui.songs.sortedByDescending { it.title })
+ }
+ SortOptions.YearASC.ordinal -> {
+ ui.copy(songs = ui.songs.sortedBy { it.year })
+ }
+ SortOptions.YearDSC.ordinal -> {
+ ui.copy(songs = ui.songs.sortedByDescending { it.year })
+ }
+ SortOptions.DurationASC.ordinal -> {
+ ui.copy(songs = ui.songs.sortedBy { it.durationMillis })
+ }
+ SortOptions.DurationDSC.ordinal -> {
+ ui.copy(songs = ui.songs.sortedByDescending { it.durationMillis })
+ }
+ else -> ui
+ }
}.catch { exception ->
Timber.e(exception)
}.stateIn(
@@ -194,4 +220,8 @@ class CollectionViewModel @Inject constructor(
}
}
+ fun updateSortOrder(order: Int){
+ _chosenSortOrder.update { order }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/components/Chips.kt b/app/src/main/java/com/github/pakka_papad/components/Chips.kt
new file mode 100644
index 0000000..cd53529
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/components/Chips.kt
@@ -0,0 +1,107 @@
+package com.github.pakka_papad.components
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowDropDown
+import androidx.compose.material.icons.outlined.KeyboardArrowRight
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+enum class SortType(val reversible: Boolean = false) {
+ Title(true),
+ Album(true),
+ Artist(true),
+ Length(true),
+ Liked(false)
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SortChip(
+ sortOptions: List,
+ onSortSelected: (sortType: SortType, reversed: Boolean?) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ var menuPageNumber by remember { mutableStateOf(0) }
+ var selectedType by remember { mutableStateOf(SortType.Title) }
+ AssistChip(
+ onClick = { menuPageNumber = 1 },
+ label = {
+ Text(
+ text = "Sort by",
+ style = MaterialTheme.typography.labelLarge
+ )
+ },
+ modifier = modifier,
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Outlined.ArrowDropDown,
+ contentDescription = "down arrow",
+ modifier = Modifier.size(18.dp)
+ )
+ }
+ )
+ DropdownMenu(
+ expanded = menuPageNumber != 0,
+ onDismissRequest = {
+ menuPageNumber = 0
+ }
+ ) {
+ when (menuPageNumber) {
+ 1 -> {
+ sortOptions.forEach {
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = it.name
+ )
+ },
+ onClick = {
+ selectedType = it
+ if (it.reversible) {
+ menuPageNumber = 2
+ } else {
+ onSortSelected(selectedType, null)
+ menuPageNumber = 0
+ }
+ },
+ trailingIcon = {
+ if (it.reversible) {
+ Icon(
+ imageVector = Icons.Outlined.KeyboardArrowRight,
+ contentDescription = "right arrow"
+ )
+ }
+ }
+ )
+ }
+ }
+ 2 -> {
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = "Ascending"
+ )
+ },
+ onClick = {
+ onSortSelected(selectedType, false)
+ menuPageNumber = 0
+ }
+ )
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = "Descending"
+ )
+ },
+ onClick = {
+ onSortSelected(selectedType, true)
+ menuPageNumber = 0
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/components/SelectableCards.kt b/app/src/main/java/com/github/pakka_papad/components/SelectableCards.kt
new file mode 100644
index 0000000..de6dc69
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/components/SelectableCards.kt
@@ -0,0 +1,34 @@
+package com.github.pakka_papad.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Checkbox
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun SelectableCard(
+ isSelected: Boolean,
+ onSelectChange: (isSelected: Boolean) -> Unit,
+ content: @Composable RowScope.() -> Unit,
+ modifier: Modifier = Modifier,
+){
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ){
+ Checkbox(
+ checked = isSelected,
+ onCheckedChange = onSelectChange
+ )
+ content()
+ }
+}
\ 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 8534e94..fba9efb 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
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
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
import com.github.pakka_papad.data.music.Song
import kotlinx.coroutines.launch
@@ -238,4 +239,87 @@ fun SongCardV3(
style = MaterialTheme.typography.titleSmall,
overflow = TextOverflow.Ellipsis
)
+}
+
+@Composable
+fun MiniSongCard(
+ song: MiniSong,
+ onSongClicked: () -> Unit,
+ currentlyPlaying: Boolean = false,
+ songOptions: List,
+) {
+ val iconModifier = Modifier.size(26.dp)
+ val spacerModifier = Modifier.width(10.dp)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(70.dp)
+ .clickable(onClick = onSongClicked)
+ .background(if (currentlyPlaying) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.surface)
+ .padding(horizontal = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AsyncImage(
+ model = song.artUri,
+ contentDescription = "song-${song.title}-art",
+ modifier = Modifier
+ .size(50.dp)
+ .clip(MaterialTheme.shapes.medium),
+ contentScale = ContentScale.Crop
+ )
+ Spacer(spacerModifier)
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = song.title,
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ modifier = Modifier.fillMaxWidth(),
+ color = if (currentlyPlaying) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.Bold,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ text = song.artist,
+ maxLines = 1,
+ style = MaterialTheme.typography.titleSmall,
+ modifier = Modifier.fillMaxWidth(),
+ color = if (currentlyPlaying) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ if (songOptions.isNotEmpty()) {
+ Spacer(spacerModifier)
+ var optionsVisible by remember { mutableStateOf(false) }
+ Icon(
+ imageVector = Icons.Outlined.MoreVert,
+ contentDescription = null,
+ modifier = iconModifier
+ .clickable(
+ onClick = {
+ optionsVisible = true
+ },
+ indication = rememberRipple(
+ bounded = false,
+ radius = 20.dp
+ ),
+ interactionSource = remember { MutableInteractionSource() }
+ ),
+ tint = if (currentlyPlaying) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
+ )
+ if (optionsVisible) {
+ OptionsAlertDialog(
+ options = songOptions,
+ title = song.title,
+ onDismissRequest = {
+ optionsVisible = false
+ }
+ )
+ }
+ }
+ }
}
\ No newline at end of file
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
new file mode 100644
index 0000000..2f29123
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/components/SortOptions.kt
@@ -0,0 +1,146 @@
+package com.github.pakka_papad.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.style.TextAlign
+import com.github.pakka_papad.Screens
+
+/**
+ * Do not change order
+ */
+enum class SortOptions(
+ val text: String,
+) {
+ Default(text = "Default"),
+ TitleASC(text = "Title - Ascending"),
+ TitleDSC(text = "Title - Descending"),
+ ArtistASC(text = "Artist - Descending"),
+ ArtistDSC(text = "Artist - Descending"),
+ AlbumASC(text = "Album - Ascending"),
+ AlbumDSC(text = "Album - Descending"),
+ YearASC(text = "Year - Ascending"),
+ YearDSC(text = "Year - Descending"),
+ DurationASC(text = "Duration - Ascending"),
+ DurationDSC(text = "Duration - Descending"),
+ NameASC(text = "Name - Ascending"),
+ NameDSC(text = "Name - Descending"),
+ SongsCountASC(text = "Songs count - Ascending"),
+ SongsCountDSC(text = "Songs count - Descending"),
+}
+
+fun Screens.getSortOptions(): List {
+ return when (this) {
+ Screens.Songs -> listOf(
+ SortOptions.TitleASC,
+ SortOptions.TitleDSC,
+ SortOptions.AlbumASC,
+ SortOptions.AlbumDSC,
+ SortOptions.ArtistASC,
+ SortOptions.ArtistDSC,
+ SortOptions.YearASC,
+ SortOptions.YearDSC,
+ SortOptions.DurationASC,
+ SortOptions.DurationDSC,
+ )
+
+ Screens.Albums -> listOf(
+ SortOptions.TitleASC,
+ SortOptions.TitleDSC,
+ )
+
+ Screens.Artists, Screens.Genres, Screens.Playlists -> listOf(
+ SortOptions.NameASC,
+ SortOptions.NameDSC,
+ SortOptions.SongsCountASC,
+ SortOptions.SongsCountDSC,
+ )
+
+ Screens.Folders -> listOf(
+ SortOptions.Default,
+ SortOptions.NameASC,
+ SortOptions.NameDSC,
+ )
+ }
+}
+
+@Composable
+fun SortOptionChooser(
+ options: List,
+ selectedOption: Int,
+ onOptionSelect: (Int) -> Unit,
+ onChooserDismiss: () -> Unit,
+) {
+ if (options.isEmpty()) return
+ AlertDialog(
+ onDismissRequest = onChooserDismiss,
+ title = {
+ Text(
+ text = "Sort",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ },
+ confirmButton = {
+ Button(
+ onClick = onChooserDismiss,
+ ) {
+ Text(
+ text = "Close",
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
+ },
+ text = {
+ LazyColumn(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ items(
+ items = options,
+ key = { option -> option.ordinal }
+ ) { option ->
+ SortOptionCard(
+ option = option,
+ isSelected = (selectedOption == option.ordinal),
+ onSelect = { onOptionSelect(option.ordinal) }
+ )
+ }
+ }
+ }
+ )
+}
+
+@Composable
+private fun SortOptionCard(
+ option: SortOptions,
+ isSelected: Boolean,
+ onSelect: () -> Unit,
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .clickable(onClick = onSelect),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = isSelected,
+ onClick = onSelect
+ )
+ Text(
+ text = option.text,
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+}
\ No newline at end of file
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 21be4fa..a8d11d0 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
@@ -6,6 +6,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material.icons.outlined.Check
+import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@@ -143,4 +145,51 @@ fun TopBarWithBackArrow(
actions = actions,
backgroundColor = backgroundColor,
titleMaxLines = titleMaxLines,
+)
+
+
+@Composable
+fun CancelConfirmTopBar(
+ onCancelClicked: () -> Unit,
+ onConfirmClicked: () -> Unit,
+ title: String,
+) = CenterAlignedTopBar(
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Outlined.Close,
+ contentDescription = null,
+ modifier = Modifier
+ .padding(16.dp)
+ .size(30.dp)
+ .clickable(
+ interactionSource = remember{ MutableInteractionSource() },
+ indication = rememberRipple(
+ bounded = false,
+ radius = 25.dp,
+ ),
+ onClick = onCancelClicked
+ ),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ },
+ title = title,
+ actions = {
+ Icon(
+ imageVector = Icons.Outlined.Check,
+ contentDescription = null,
+ modifier = Modifier
+ .padding(16.dp)
+ .size(30.dp)
+ .clickable(
+ interactionSource = remember{ MutableInteractionSource() },
+ indication = rememberRipple(
+ bounded = false,
+ radius = 25.dp,
+ ),
+ onClick = onConfirmClicked
+ ),
+ tint = MaterialTheme.colorScheme.onSurface
+ )
+ },
+ titleMaxLines = 1
)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/components/more_options/FolderOptions.kt b/app/src/main/java/com/github/pakka_papad/components/more_options/FolderOptions.kt
new file mode 100644
index 0000000..10e4863
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/components/more_options/FolderOptions.kt
@@ -0,0 +1,21 @@
+package com.github.pakka_papad.components.more_options
+
+import androidx.annotation.DrawableRes
+import com.github.pakka_papad.R
+
+sealed class FolderOptions(
+ override val onClick: () -> Unit,
+ override val text: String,
+ @DrawableRes override val icon: Int,
+) : MoreOptions(
+ onClick = onClick,
+ text = text,
+ icon = icon,
+){
+ data class Blacklist(override val onClick: () -> Unit):
+ FolderOptions(
+ onClick = onClick,
+ text = "Blacklist Folder",
+ icon = R.drawable.ic_baseline_remove_circle_40,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/components/more_options/OptionsDropDown.kt b/app/src/main/java/com/github/pakka_papad/components/more_options/OptionsDropDown.kt
index 045bfaa..4d0712c 100644
--- a/app/src/main/java/com/github/pakka_papad/components/more_options/OptionsDropDown.kt
+++ b/app/src/main/java/com/github/pakka_papad/components/more_options/OptionsDropDown.kt
@@ -20,31 +20,33 @@ fun OptionsDropDown(
offset: DpOffset = DpOffset(0.dp, 0.dp),
) {
if (options.isEmpty()) return
- DropdownMenu(
- expanded = expanded,
- onDismissRequest = onDismissRequest,
- offset = offset,
- ) {
- options.forEach { option ->
- DropdownMenuItem(
- onClick = {
- onDismissRequest()
- option.onClick()
- },
- text = {
- Text(
- text = option.text,
- style = MaterialTheme.typography.bodyMedium,
- )
- },
- leadingIcon = {
- Icon(
- painter = painterResource(option.icon),
- modifier = Modifier.size(24.dp),
- contentDescription = option.text
- )
- }
- )
+ MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = MaterialTheme.shapes.large)) {
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest,
+ offset = offset,
+ ) {
+ options.forEach { option ->
+ DropdownMenuItem(
+ onClick = {
+ onDismissRequest()
+ option.onClick()
+ },
+ text = {
+ Text(
+ text = option.text,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(option.icon),
+ modifier = Modifier.size(24.dp),
+ contentDescription = option.text
+ )
+ }
+ )
+ }
}
}
}
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 6eb662e..34e632a 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
@@ -1,7 +1,10 @@
package com.github.pakka_papad.data
+import androidx.room.AutoMigration
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.*
@@ -15,9 +18,15 @@ import com.github.pakka_papad.data.music.*
AlbumArtist::class,
Composer::class,
Lyricist::class,
- BlacklistedSong::class
+ BlacklistedSong::class,
+ BlacklistedFolder::class,
+ PlayHistory::class,
],
- version = 1, exportSchema = true)
+ version = 2, exportSchema = true,
+ autoMigrations = [
+ AutoMigration(from = 1, to = 2)
+ ]
+)
abstract class AppDatabase: RoomDatabase() {
abstract fun songDao(): SongDao
@@ -38,4 +47,8 @@ abstract class AppDatabase: RoomDatabase() {
abstract fun blacklistDao(): BlacklistDao
+ abstract fun blacklistedFolderDao(): BlacklistedFolderDao
+
+ abstract fun playHistoryDao(): PlayHistoryDao
+
}
\ 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
index 0cee33c..c1258d3 100644
--- a/app/src/main/java/com/github/pakka_papad/data/DataManager.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/DataManager.kt
@@ -1,38 +1,33 @@
package com.github.pakka_papad.data
-import android.content.ContentUris
import android.content.Context
import android.content.Intent
-import android.net.Uri
import android.os.Build
-import android.provider.MediaStore.Audio
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.formatToDate
import com.github.pakka_papad.nowplaying.RepeatMode
import com.github.pakka_papad.player.ZenPlayer
-import com.github.pakka_papad.toMBfromB
-import com.github.pakka_papad.toMS
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 java.io.FileNotFoundException
-import java.util.*
+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) }
@@ -41,21 +36,43 @@ class DataManager(
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()){
- launch { daoCollection.songDao.deleteSong(it) }
+ 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) }
}
}
@@ -63,6 +80,23 @@ class DataManager(
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){ }
}
}
@@ -86,6 +120,7 @@ class DataManager(
artist = song.artist,
)
)
+ blacklistedSongLocations.add(song.location)
}
suspend fun insertPlaylistSongCrossRefs(playlistSongCrossRefs: List) =
@@ -99,103 +134,25 @@ class DataManager(
fun scanForMusic() = scope.launch {
_scanStatus.send(ScanStatus.ScanStarted)
-// notificationManager.sendScanningNotification()
- val blacklistedSongs = daoCollection.blacklistDao.getBlacklistedSongs()
- val blacklistedSongLocations = blacklistedSongs.map { it.location }.toSet()
- val selection = Audio.Media.IS_MUSIC + " != 0"
- val projection = arrayOf(
- Audio.Media.DATA,
- Audio.Media.TITLE,
- Audio.Media.ALBUM_ID,
- Audio.Media.ALBUM,
- Audio.Media.SIZE,
- Audio.Media.DATE_ADDED,
- Audio.Media.DATE_MODIFIED,
- Audio.Media._ID
- )
- val cursor = context.contentResolver.query(
- Audio.Media.EXTERNAL_CONTENT_URI,
- projection,
- selection,
- null,
- Audio.Media.DATE_ADDED,
- null
- ) ?: return@launch
- val totalSongs = cursor.count
- var parsedSongs = 0
- cursor.moveToFirst()
- val mExtractor = MetadataExtractor()
- val dataIndex = cursor.getColumnIndex(Audio.Media.DATA)
- val titleIndex = cursor.getColumnIndex(Audio.Media.TITLE)
- val albumIdIndex = cursor.getColumnIndex(Audio.Media.ALBUM_ID)
- val albumIndex = cursor.getColumnIndex(Audio.Media.ALBUM)
- val sizeIndex = cursor.getColumnIndex(Audio.Media.SIZE)
- val dateAddedIndex = cursor.getColumnIndex(Audio.Media.DATE_ADDED)
- val dateModifiedIndex = cursor.getColumnIndex(Audio.Media.DATE_MODIFIED)
- val songIdIndex = cursor.getColumnIndex(Audio.Media._ID)
- val songCover = Uri.parse("content://media/external/audio/albumart")
-
- val songs = ArrayList()
- val albumArtMap = HashMap()
- val artistSet = TreeSet()
- val albumArtistSet = TreeSet()
- val composerSet = TreeSet()
- val genreSet = TreeSet()
- val lyricistSet = TreeSet()
- do {
- try {
- val file = File(cursor.getString(dataIndex))
- if (blacklistedSongLocations.contains(file.path)) continue
- if (!file.exists()) throw FileNotFoundException()
- val songMetadata = mExtractor.getSongMetadata(file.path)
- val song = Song(
- location = file.path,
- title = cursor.getString(titleIndex),
- album = cursor.getString(albumIndex).trim(),
- size = cursor.getFloat(sizeIndex).toMBfromB(),
- addedDate = cursor.getString(dateAddedIndex).toLong().formatToDate(),
- modifiedDate = cursor.getString(dateModifiedIndex).toLong().formatToDate(),
- artist = songMetadata.artist.trim(),
- albumArtist = songMetadata.albumArtist.trim(),
- composer = songMetadata.composer.trim(),
- genre = songMetadata.genre.trim(),
- lyricist = songMetadata.lyricist.trim(),
- year = songMetadata.year,
- comment = songMetadata.comment,
- durationMillis = songMetadata.duration,
- durationFormatted = songMetadata.duration.toMS(),
- bitrate = songMetadata.bitrate,
- sampleRate = songMetadata.sampleRate,
- bitsPerSample = songMetadata.bitsPerSample,
- mimeType = songMetadata.mimeType,
- artUri = "content://media/external/audio/media/${cursor.getLong(songIdIndex)}/albumart"
- )
- songs.add(song)
- artistSet.add(song.artist)
- albumArtistSet.add(song.albumArtist)
- composerSet.add(song.composer)
- lyricistSet.add(song.lyricist)
- genreSet.add(song.genre)
- if (albumArtMap[song.album] == null) {
- albumArtMap[song.album] =
- ContentUris.withAppendedId(songCover, cursor.getLong(albumIdIndex))
- .toString()
- }
- } catch (e: Exception) {
- Timber.e(e.message ?: e.localizedMessage ?: "FILE_DOES_NOT_EXIST")
+ val (songs, albums) = songExtractor.extract(
+ blacklistedSongLocations,
+ blacklistedFolderPaths,
+ statusListener = { parsed, total ->
+ _scanStatus.trySend(ScanStatus.ScanProgress(parsed, total))
}
- parsedSongs++
- _scanStatus.send(ScanStatus.ScanProgress(parsedSongs, totalSongs))
- } while (cursor.moveToNext())
- cursor.close()
- daoCollection.albumDao.insertAllAlbums(albumArtMap.entries.map { (t, u) -> Album(t, u) })
- daoCollection.artistDao.insertAllArtists(artistSet.map { Artist(it) })
- daoCollection.albumArtistDao.insertAllAlbumArtists(albumArtistSet.map { AlbumArtist(it) })
- daoCollection.composerDao.insertAllComposers(composerSet.map { Composer(it) })
- daoCollection.lyricistDao.insertAllLyricists(lyricistSet.map { Lyricist(it) })
- daoCollection.genreDao.insertAllGenres(genreSet.map { Genre(it) })
+ )
+ 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)
-// notificationManager.removeScanningNotification()
_scanStatus.send(ScanStatus.ScanComplete)
}
@@ -239,6 +196,7 @@ class DataManager(
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 {
@@ -296,4 +254,14 @@ class DataManager(
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 2b34779..95eab50 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,6 +1,8 @@
package com.github.pakka_papad.data
import androidx.datastore.core.Serializer
+import com.github.pakka_papad.Screens
+import com.github.pakka_papad.components.SortOptions
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
@@ -18,6 +20,19 @@ class UserPreferencesSerializer @Inject constructor() : Serializer){
+ if (tabsList.isEmpty()) return
+ coroutineScope.launch {
+ userPreferences.updateData {
+ it.copy {
+ selectedTabs.apply {
+ clear()
+ addAll(tabsList)
+ }
+ }
+ }
+ }
+ }
+
+ val songSortOrder = userPreferences.data
+ .map {
+ it.getChosenSortOrderOrDefault(Screens.Songs.ordinal, SortOptions.TitleASC.ordinal)
+ }
+
+ val albumSortOrder = userPreferences.data
+ .map {
+ it.getChosenSortOrderOrDefault(Screens.Albums.ordinal, SortOptions.TitleASC.ordinal)
+ }
+
+ val artistSortOrder = userPreferences.data
+ .map {
+ it.getChosenSortOrderOrDefault(Screens.Artists.ordinal, SortOptions.NameASC.ordinal)
+ }
+
+ val playlistSortOrder = userPreferences.data
+ .map {
+ it.getChosenSortOrderOrDefault(Screens.Playlists.ordinal, SortOptions.NameASC.ordinal)
+ }
+
+ val genreSortOrder = userPreferences.data
+ .map {
+ it.getChosenSortOrderOrDefault(Screens.Genres.ordinal, SortOptions.NameASC.ordinal)
+ }
+
+ val folderSortOrder = userPreferences.data
+ .map {
+ it.getChosenSortOrderOrDefault(Screens.Folders.ordinal, SortOptions.Default.ordinal)
+ }
+
+ val sortOrder = userPreferences.data
+ .map {
+ it.chosenSortOrderMap
+ }
+ .stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.WhileSubscribed(5_000),
+ initialValue = mapOf(),
+ )
+
+ fun updateSortOrder(screen: Int, order: Int) {
+ coroutineScope.launch {
+ userPreferences.updateData {
+ it.copy {
+ chosenSortOrder[screen] = order
+ }
+ }
+ }
+ }
+
init {
val initJob = coroutineScope.launch {
launch { theme.collect { } }
launch { isOnBoardingComplete.collect { } }
launch { isCrashlyticsDisabled.collect { } }
launch { playbackParams.collect { updatePlaybackParams(it.playbackSpeed,it.playbackPitch) } }
+ launch { selectedTabs.collect{ } }
+ launch { songSortOrder.collect { } }
+ launch { albumSortOrder.collect { } }
+ launch { artistSortOrder.collect { } }
+ launch { playlistSortOrder.collect { } }
+ launch { genreSortOrder.collect { } }
+ launch { folderSortOrder.collect { } }
+ launch { sortOrder.collect { } }
}
coroutineScope.launch {
delay(1.minutes)
diff --git a/app/src/main/java/com/github/pakka_papad/data/analytics/PlayHistory.kt b/app/src/main/java/com/github/pakka_papad/data/analytics/PlayHistory.kt
new file mode 100644
index 0000000..23ad430
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/analytics/PlayHistory.kt
@@ -0,0 +1,26 @@
+package com.github.pakka_papad.data.analytics
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.PrimaryKey
+import com.github.pakka_papad.Constants
+import com.github.pakka_papad.data.music.Song
+
+@Entity(
+ tableName = Constants.Tables.PLAY_HISTORY_TABLE,
+ foreignKeys = [
+ ForeignKey(
+ entity = Song::class,
+ parentColumns = ["location"],
+ childColumns = ["songLocation"],
+ onDelete = ForeignKey.CASCADE
+ )
+ ]
+)
+data class PlayHistory(
+ @PrimaryKey(autoGenerate = true) val id: Long = 0,
+ @ColumnInfo(index = true) val songLocation: String,
+ val timestamp: Long,
+ val playDuration: Long,
+)
diff --git a/app/src/main/java/com/github/pakka_papad/data/analytics/PlayHistoryDao.kt b/app/src/main/java/com/github/pakka_papad/data/analytics/PlayHistoryDao.kt
new file mode 100644
index 0000000..9066805
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/analytics/PlayHistoryDao.kt
@@ -0,0 +1,40 @@
+package com.github.pakka_papad.data.analytics
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Update
+import com.github.pakka_papad.Constants
+import com.github.pakka_papad.data.music.Song
+
+@Dao
+interface PlayHistoryDao {
+
+ @Query(
+ "SELECT * FROM ${Constants.Tables.SONG_TABLE} WHERE location = :location"
+ )
+ suspend fun getSongFromLocation(location: String): Song?
+
+ @Update
+ suspend fun updateSong(song: Song)
+
+ @Insert
+ suspend fun insertRecord(record: PlayHistory)
+
+ @Transaction
+ suspend fun addRecord(location: String, duration: Long) {
+ val song = getSongFromLocation(location) ?: return
+ val time = System.currentTimeMillis()
+ val updatedSong = song.copy(playCount = 1 + song.playCount, lastPlayed = time)
+ val record = PlayHistory(
+ songLocation = location,
+ timestamp = time,
+ playDuration = duration,
+ )
+ updateSong(updatedSong)
+ insertRecord(record)
+ }
+
+
+}
\ No newline at end of file
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
index c6465a3..4c96103 100644
--- 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
@@ -1,5 +1,6 @@
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(
@@ -11,5 +12,7 @@ data class DaoCollection(
val lyricistDao: LyricistDao,
val genreDao: GenreDao,
val playlistDao: PlaylistDao,
- val blacklistDao: BlacklistDao
+ 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/GetAll.kt b/app/src/main/java/com/github/pakka_papad/data/components/GetAll.kt
index 21d7b46..40b4321 100644
--- 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
@@ -20,4 +20,6 @@ class GetAll(
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/daos/AlbumArtistDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/AlbumArtistDao.kt
index 95f91d6..5623a7a 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/AlbumArtistDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/AlbumArtistDao.kt
@@ -19,4 +19,11 @@ interface AlbumArtistDao {
@Query("SELECT * FROM ${Constants.Tables.ALBUM_ARTIST_TABLE} WHERE name = :name")
fun getAlbumArtistWithSongs(name: String): Flow
+ @Transaction
+ @Query("DELETE FROM ${Constants.Tables.ALBUM_ARTIST_TABLE} WHERE name IN " +
+ "(SELECT albumArtist.name as name FROM ${Constants.Tables.ALBUM_ARTIST_TABLE} as albumArtist LEFT JOIN " +
+ "${Constants.Tables.SONG_TABLE} as song ON albumArtist.name = song.albumArtist GROUP BY albumArtist.name " +
+ "HAVING COUNT(song.location) = 0)")
+ suspend fun cleanAlbumArtistTable()
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/daos/AlbumDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/AlbumDao.kt
index 2c585f0..8470832 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/AlbumDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/AlbumDao.kt
@@ -25,4 +25,10 @@ interface AlbumDao {
@Query("SELECT * FROM ${Constants.Tables.ALBUM_TABLE} WHERE name LIKE '%' || :query || '%'")
suspend fun searchAlbums(query: String): List
+ @Transaction
+ @Query("DELETE FROM ${Constants.Tables.ALBUM_TABLE} WHERE name IN " +
+ "(SELECT album.name as name FROM ${Constants.Tables.ALBUM_TABLE} as album LEFT JOIN " +
+ "${Constants.Tables.SONG_TABLE} as song ON album.name = song.album GROUP BY album.name " +
+ "HAVING COUNT(song.location) = 0)")
+ suspend fun cleanAlbumTable()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/daos/ArtistDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/ArtistDao.kt
index 10bc9a5..8a8edb0 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/ArtistDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/ArtistDao.kt
@@ -30,11 +30,11 @@ interface ArtistDao {
@Query("SELECT * FROM ${Constants.Tables.ARTIST_TABLE} WHERE name LIKE '%' || :query || '%'")
suspend fun searchArtists(query: String): List
-// @Transaction
-// @Query("SELECT ${Constants.Tables.ARTIST_TABLE}.name as artistName, COUNT(*) as count " +
-// "FROM ${Constants.Tables.ARTIST_TABLE} JOIN ${Constants.Tables.SONG_TABLE} ON " +
-// "${Constants.Tables.ARTIST_TABLE}.name = ${Constants.Tables.SONG_TABLE}.artist " +
-// "GROUP BY ${Constants.Tables.ARTIST_TABLE}.name")
-// fun getAllArtistsWithSongCount(): Flow>
+ @Transaction
+ @Query("DELETE FROM ${Constants.Tables.ARTIST_TABLE} WHERE name IN " +
+ "(SELECT artist.name as name FROM ${Constants.Tables.ARTIST_TABLE} as artist LEFT JOIN " +
+ "${Constants.Tables.SONG_TABLE} as song ON artist.name = song.artist GROUP BY artist.name " +
+ "HAVING COUNT(song.location) = 0)")
+ suspend fun cleanArtistTable()
}
\ 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
new file mode 100644
index 0000000..7434978
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/BlacklistedFolderDao.kt
@@ -0,0 +1,24 @@
+package com.github.pakka_papad.data.daos
+
+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
+import com.github.pakka_papad.data.music.BlacklistedFolder
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface BlacklistedFolderDao {
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertFolder(vararg folder: BlacklistedFolder)
+
+ @Query("SELECT * FROM ${Constants.Tables.BLACKLISTED_FOLDER_TABLE}")
+ fun getAllFolders(): Flow>
+
+ @Delete
+ suspend fun deleteFolder(folder: BlacklistedFolder)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/daos/ComposerDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/ComposerDao.kt
index 9c33e4b..6e4c73b 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/ComposerDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/ComposerDao.kt
@@ -19,4 +19,10 @@ interface ComposerDao {
@Query("SELECT * FROM ${Constants.Tables.COMPOSER_TABLE} WHERE name = :name")
fun getComposerWithSongs(name: String): Flow
+ @Transaction
+ @Query("DELETE FROM ${Constants.Tables.COMPOSER_TABLE} WHERE name IN " +
+ "(SELECT composer.name as name FROM ${Constants.Tables.COMPOSER_TABLE} as composer LEFT JOIN " +
+ "${Constants.Tables.SONG_TABLE} as song ON composer.name = song.composer GROUP BY composer.name " +
+ "HAVING COUNT(song.location) = 0)")
+ suspend fun cleanComposerTable()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/daos/GenreDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/GenreDao.kt
index 4d16439..1a43983 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/GenreDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/GenreDao.kt
@@ -19,4 +19,13 @@ interface GenreDao {
@Query("SELECT * FROM ${Constants.Tables.GENRE_TABLE} WHERE genre = :genreName")
fun getGenreWithSongs(genreName: String): Flow
+ @Query("DELETE FROM ${Constants.Tables.GENRE_TABLE} WHERE genre = :genre")
+ suspend fun deleteGenre(genre: String)
+
+ @Transaction
+ @Query("DELETE FROM ${Constants.Tables.GENRE_TABLE} WHERE genre IN " +
+ "(SELECT genre.genre as genre FROM ${Constants.Tables.GENRE_TABLE} as genre LEFT JOIN " +
+ "${Constants.Tables.SONG_TABLE} as song ON genre.genre = song.genre GROUP BY genre.genre " +
+ "HAVING COUNT(song.location) = 0)")
+ suspend fun cleanGenreTable()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/daos/LyricistDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/LyricistDao.kt
index 1a7c088..4a95f67 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/LyricistDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/LyricistDao.kt
@@ -19,4 +19,10 @@ interface LyricistDao {
@Query("SELECT * FROM ${Constants.Tables.LYRICIST_TABLE} WHERE name = :name")
fun getLyricistWithSongs(name: String): Flow
+ @Transaction
+ @Query("DELETE FROM ${Constants.Tables.LYRICIST_TABLE} WHERE name IN " +
+ "(SELECT lyricist.name as name FROM ${Constants.Tables.LYRICIST_TABLE} as lyricist LEFT JOIN " +
+ "${Constants.Tables.SONG_TABLE} as song ON lyricist.name = song.lyricist GROUP BY lyricist.name " +
+ "HAVING COUNT(song.location) = 0)")
+ suspend fun cleanLyricistTable()
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/data/daos/SongDao.kt b/app/src/main/java/com/github/pakka_papad/data/daos/SongDao.kt
index af8a310..05bb650 100644
--- a/app/src/main/java/com/github/pakka_papad/data/daos/SongDao.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/daos/SongDao.kt
@@ -26,6 +26,10 @@ interface SongDao {
@Query("DELETE FROM ${Constants.Tables.SONG_TABLE}")
suspend fun deleteAllSongs()
+ @Transaction
+ @Query("DELETE FROM ${Constants.Tables.SONG_TABLE} WHERE location LIKE :prefix || '%'")
+ suspend fun deleteSongsWithPathPrefix(prefix: String)
+
@Query("SELECT * FROM ${Constants.Tables.SONG_TABLE} WHERE title LIKE '%' || :query || '%' OR " +
"artist LIKE '%' || :query || '%' OR " +
"albumArtist LIKE '%' || :query || '%' OR " +
diff --git a/app/src/main/java/com/github/pakka_papad/data/music/BlacklistedFolder.kt b/app/src/main/java/com/github/pakka_papad/data/music/BlacklistedFolder.kt
new file mode 100644
index 0000000..14641c8
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/music/BlacklistedFolder.kt
@@ -0,0 +1,10 @@
+package com.github.pakka_papad.data.music
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.github.pakka_papad.Constants
+
+@Entity(tableName = Constants.Tables.BLACKLISTED_FOLDER_TABLE)
+data class BlacklistedFolder(
+ @PrimaryKey val path: String,
+)
diff --git a/app/src/main/java/com/github/pakka_papad/data/music/MiniSong.kt b/app/src/main/java/com/github/pakka_papad/data/music/MiniSong.kt
new file mode 100644
index 0000000..0895550
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/music/MiniSong.kt
@@ -0,0 +1,8 @@
+package com.github.pakka_papad.data.music
+
+data class MiniSong(
+ val title: String,
+ val location: String,
+ val artist: String,
+ val artUri: String,
+)
diff --git a/app/src/main/java/com/github/pakka_papad/data/music/Song.kt b/app/src/main/java/com/github/pakka_papad/data/music/Song.kt
index 90e5353..0b42531 100644
--- a/app/src/main/java/com/github/pakka_papad/data/music/Song.kt
+++ b/app/src/main/java/com/github/pakka_papad/data/music/Song.kt
@@ -13,52 +13,52 @@ import com.github.pakka_papad.Constants
entity = Album::class,
parentColumns = ["name"],
childColumns = ["album"],
- onDelete = ForeignKey.SET_NULL,
+ onDelete = ForeignKey.SET_DEFAULT,
),
ForeignKey(
entity = Artist::class,
parentColumns = ["name"],
childColumns = ["artist"],
- onDelete = ForeignKey.SET_NULL
+ onDelete = ForeignKey.SET_DEFAULT,
),
ForeignKey(
entity = Genre::class,
parentColumns = ["genre"],
childColumns = ["genre"],
- onDelete = ForeignKey.SET_NULL,
+ onDelete = ForeignKey.SET_DEFAULT,
),
ForeignKey(
entity = AlbumArtist::class,
parentColumns = ["name"],
childColumns = ["albumArtist"],
- onDelete = ForeignKey.SET_NULL
+ onDelete = ForeignKey.SET_DEFAULT,
),
ForeignKey(
entity = Lyricist::class,
parentColumns = ["name"],
childColumns = ["lyricist"],
- onDelete = ForeignKey.SET_NULL
+ onDelete = ForeignKey.SET_DEFAULT,
),
ForeignKey(
entity = Composer::class,
parentColumns = ["name"],
childColumns = ["composer"],
- onDelete = ForeignKey.SET_NULL
+ onDelete = ForeignKey.SET_DEFAULT,
),
]
)
data class Song(
@PrimaryKey val location: String = "",
val title: String,
- @ColumnInfo(index = true) val album: String = "",
+ @ColumnInfo(index = true, defaultValue = "Unknown") val album: String = "",
val size: String,
val addedDate: String,
val modifiedDate: String,
- @ColumnInfo(index = true) val artist: String,
- @ColumnInfo(index = true) val albumArtist: String,
- @ColumnInfo(index = true) val composer: String,
- @ColumnInfo(index = true) val genre: String,
- @ColumnInfo(index = true) val lyricist: String,
+ @ColumnInfo(index = true, defaultValue = "Unknown") val artist: String,
+ @ColumnInfo(index = true, defaultValue = "Unknown") val albumArtist: String,
+ @ColumnInfo(index = true, defaultValue = "Unknown") val composer: String,
+ @ColumnInfo(index = true, defaultValue = "Unknown") val genre: String,
+ @ColumnInfo(index = true, defaultValue = "Unknown") val lyricist: String,
val year: Int,
val comment: String? = null,
val durationMillis: Long,
@@ -69,6 +69,8 @@ data class Song(
val mimeType: String? = null,
val favourite: Boolean = false,
val artUri: String? = null,
+ @ColumnInfo(defaultValue = "0") val playCount: Int = 0,
+ val lastPlayed: Long? = null,
){
data class Metadata(
val artist: String,
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
new file mode 100644
index 0000000..6c635f3
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/data/music/SongExtractor.kt
@@ -0,0 +1,303 @@
+package com.github.pakka_papad.data.music
+
+import android.content.ContentUris
+import android.content.Context
+import android.media.MediaMetadataRetriever
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import com.github.pakka_papad.formatToDate
+import com.github.pakka_papad.toMBfromB
+import com.github.pakka_papad.toMS
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import java.io.File
+import java.io.FileNotFoundException
+import java.util.TreeMap
+
+class SongExtractor(
+ private val scope: CoroutineScope,
+ private val context: Context,
+) {
+
+ private val projection = arrayOf(
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.ALBUM_ID,
+ MediaStore.Audio.Media.DATA,
+ MediaStore.Audio.Media.ALBUM,
+ MediaStore.Audio.Media.SIZE,
+ MediaStore.Audio.Media.DATE_ADDED,
+ MediaStore.Audio.Media.DATE_MODIFIED,
+ )
+
+ fun resolveSong(location: String): Song? {
+ val selection = MediaStore.Audio.Media.DATA + " LIKE ?"
+ val selectionArgs = arrayOf(location)
+ val cursor = context.contentResolver.query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ selection,
+ selectionArgs,
+ MediaStore.Audio.Media.DATE_ADDED,
+ null
+ ) ?: return null
+ val dataIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA)
+ val titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
+ val albumIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)
+ val sizeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)
+ val dateAddedIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_ADDED)
+ val dateModifiedIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_MODIFIED)
+ val songIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
+ var resSong: Song? = null
+ cursor.moveToFirst()
+ try {
+ val songPath = cursor.getString(dataIndex)
+ val songFile = File(songPath)
+ if (!songFile.exists()) throw FileNotFoundException()
+ val size = cursor.getString(sizeIndex)
+ val addedDate = cursor.getString(dateAddedIndex)
+ val modifiedDate = cursor.getString(dateModifiedIndex)
+ val songId = cursor.getLong(songIdIndex)
+ val title = cursor.getString(titleIndex).trim()
+ val album = cursor.getString(albumIndex).trim()
+ resSong = getSong(
+ path = songPath,
+ size = size,
+ addedDate = addedDate,
+ modifiedDate = modifiedDate,
+ songId = songId,
+ title = title,
+ album = album,
+ )
+ } catch (_: Exception){
+
+ }
+ cursor.close()
+ return resSong
+ }
+
+ suspend fun extract(folderPath: String? = null): List {
+ val selection = MediaStore.Audio.Media.DATA + " LIKE ?"
+ val selectionArgs = folderPath?.let {
+ arrayOf("$it%")
+ }
+ val cursor = context.contentResolver.query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ selection,
+ selectionArgs,
+ MediaStore.Audio.Media.DATE_ADDED,
+ null
+ ) ?: return emptyList()
+ val dataIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA)
+ val titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
+ val albumIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)
+ val sizeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)
+ val dateAddedIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_ADDED)
+ val dateModifiedIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_MODIFIED)
+ val songIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
+ val dSongs = ArrayList>()
+ cursor.moveToFirst()
+ do {
+ try {
+ val songPath = cursor.getString(dataIndex)
+ val songFile = File(songPath)
+ if (!songFile.exists()) throw FileNotFoundException()
+ if (folderPath != null && songFile.parentFile?.absolutePath != folderPath) throw Exception()
+ val size = cursor.getString(sizeIndex)
+ val addedDate = cursor.getString(dateAddedIndex)
+ val modifiedDate = cursor.getString(dateModifiedIndex)
+ val songId = cursor.getLong(songIdIndex)
+ val title = cursor.getString(titleIndex).trim()
+ val album = cursor.getString(albumIndex).trim()
+ dSongs.add(scope.async {
+ getSong(
+ path = songPath,
+ size = size,
+ addedDate = addedDate,
+ modifiedDate = modifiedDate,
+ songId = songId,
+ title = title,
+ album = album,
+ )
+ })
+ } catch (_: Exception){
+
+ }
+ } while (cursor.moveToNext())
+ val songs = dSongs.awaitAll()
+ cursor.close()
+ return songs
+ }
+
+ fun extractMini(folderPath: String? = null): List {
+ val projectionForMini = arrayOf(
+ MediaStore.Audio.Media._ID,
+ MediaStore.Audio.Media.TITLE,
+ MediaStore.Audio.Media.DATA,
+ MediaStore.Audio.Media.ARTIST,
+ )
+ val selection = MediaStore.Audio.Media.DATA + " LIKE ?"
+ val selectionArgs = folderPath?.let {
+ arrayOf("$it%")
+ }
+ val cursor = context.contentResolver.query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ projectionForMini,
+ selection,
+ selectionArgs,
+ MediaStore.Audio.Media.DATE_ADDED,
+ null
+ ) ?: return emptyList()
+ val dataIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA)
+ val titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
+ val artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)
+ val songIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
+ val songs = ArrayList()
+ cursor.moveToFirst()
+ do {
+ try {
+ val songPath = cursor.getString(dataIndex)
+ val songFile = File(songPath)
+ if (!songFile.exists()) throw FileNotFoundException()
+ if (folderPath != null && songFile.parentFile?.absolutePath != folderPath) throw Exception()
+ songs.add(
+ MiniSong(
+ location = songPath,
+ title = cursor.getString(titleIndex).trim(),
+ artUri = "content://media/external/audio/media/${cursor.getLong(songIdIndex)}/albumart",
+ artist = cursor.getString(artistIndex)
+ )
+ )
+ } catch (_: Exception){
+
+ }
+ } while (cursor.moveToNext())
+ cursor.close()
+ return songs
+ }
+
+ suspend fun extract(
+ blacklistedSongLocations: HashSet,
+ blacklistedFolderPaths: HashSet,
+ statusListener: ((parsed: Int, total: Int) -> Unit)? = null
+ ): Pair,List> {
+ val selection = StringBuilder()
+ val selectionArgs = arrayListOf()
+ selection.append(MediaStore.Audio.Media.IS_MUSIC + " != 0 ")
+ blacklistedFolderPaths.forEachIndexed { index, path ->
+ selection.append(" AND NOT ")
+ .append(MediaStore.Audio.Media.DATA)
+ .append(" LIKE ?")
+ selectionArgs.add("$path%")
+ }
+ val cursor = context.contentResolver.query(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ selection.toString(),
+ selectionArgs.toTypedArray(),
+ MediaStore.Audio.Media.DATE_ADDED,
+ null
+ ) ?: return Pair(emptyList(), emptyList())
+ val songCover = Uri.parse("content://media/external/audio/albumart")
+ val albumArtMap = TreeMap()
+ val dataIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA)
+ val titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
+ val albumIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID)
+ val albumIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)
+ val sizeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)
+ val dateAddedIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_ADDED)
+ val dateModifiedIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_MODIFIED)
+ val songIdIndex = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
+ val dSongs = ArrayList>()
+ val total = cursor.count
+ var parsed = 0
+ cursor.moveToFirst()
+ do {
+ try {
+ val songPath = cursor.getString(dataIndex)
+ val songFile = File(songPath)
+ if (!songFile.exists()) throw FileNotFoundException()
+ if (blacklistedSongLocations.contains(songFile.path)) continue
+ val size = cursor.getString(sizeIndex)
+ val addedDate = cursor.getString(dateAddedIndex)
+ val modifiedDate = cursor.getString(dateModifiedIndex)
+ val songId = cursor.getLong(songIdIndex)
+ val title = cursor.getString(titleIndex).trim()
+ val album = cursor.getString(albumIndex).trim()
+ albumArtMap[album] = cursor.getLong(albumIdIndex)
+ dSongs.add(scope.async {
+ getSong(
+ path = songPath,
+ size = size,
+ addedDate = addedDate,
+ modifiedDate = modifiedDate,
+ songId = songId,
+ title = title,
+ album = album,
+ )
+ })
+ parsed++
+ statusListener?.invoke(parsed, total)
+ } catch (_: Exception){
+
+ }
+ } while (cursor.moveToNext())
+ val songs = dSongs.awaitAll()
+ cursor.close()
+ val albums = albumArtMap.map { (t, u) -> Album(t, ContentUris.withAppendedId(songCover, u).toString()) }
+ return Pair(songs,albums)
+ }
+
+ companion object {
+ private const val UNKNOWN = "Unknown"
+ }
+
+ private fun getSong(
+ path: String,
+ size: String,
+ addedDate: String,
+ modifiedDate: String,
+ songId: Long,
+ title: String,
+ album: String,
+ ): Song {
+ val extractor = MediaMetadataRetriever()
+ extractor.setDataSource(path)
+ val durationMillis = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0
+ val sampleRate = if (Build.VERSION.SDK_INT >= 31){
+ extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE)?.toFloatOrNull() ?: 0f
+ } else 0f
+ val bitsPerSample = if (Build.VERSION.SDK_INT >= 31){
+ extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITS_PER_SAMPLE)?.toIntOrNull() ?: 0
+ } else 0
+ val song = Song(
+ location = path,
+ title = title,
+ album = album,
+ size = size.toFloat().toMBfromB(),
+ addedDate = addedDate.toLong().formatToDate(),
+ modifiedDate = modifiedDate.toLong().formatToDate(),
+ artist = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)?.trim() ?: UNKNOWN,
+ albumArtist = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST)?.trim() ?: UNKNOWN,
+ composer = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER)?.trim() ?: UNKNOWN,
+ genre = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)?.trim() ?: UNKNOWN,
+ lyricist = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_WRITER)?.trim() ?: UNKNOWN,
+ year = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR)?.toIntOrNull() ?: 0,
+ comment = null,
+ durationMillis = durationMillis,
+ durationFormatted = durationMillis.toMS(),
+ bitrate = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toFloatOrNull() ?: 0f,
+ sampleRate = sampleRate,
+ bitsPerSample = bitsPerSample,
+ mimeType = extractor.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE),
+ favourite = false,
+ artUri = "content://media/external/audio/media/$songId/albumart"
+ )
+ extractor.release()
+ return song
+ }
+}
\ 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 cb07faa..3c227f0 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,5 +1,6 @@
package com.github.pakka_papad.di
+import android.annotation.SuppressLint
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
@@ -20,6 +21,7 @@ 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.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
@@ -49,7 +51,8 @@ object AppModule {
@ApplicationContext context: Context,
notificationManager: ZenNotificationManager,
db: AppDatabase,
- scope: CoroutineScope
+ scope: CoroutineScope,
+ extractor: SongExtractor,
): DataManager {
return DataManager(
context = context,
@@ -64,8 +67,11 @@ object AppModule {
genreDao = db.genreDao(),
playlistDao = db.playlistDao(),
blacklistDao = db.blacklistDao(),
+ blacklistedFolderDao = db.blacklistedFolderDao(),
+ playHistoryDao = db.playHistoryDao()
),
scope = scope,
+ songExtractor = extractor
)
}
@@ -75,6 +81,7 @@ object AppModule {
return ZenNotificationManager(context)
}
+ @SuppressLint("UnsafeOptInUsageError")
@Singleton
@Provides
fun providesExoPlayer(
@@ -135,4 +142,16 @@ object AppModule {
firebase = FirebaseCrashlytics.getInstance()
)
}
+
+ @Singleton
+ @Provides
+ fun providesSongExtractor(
+ @ApplicationContext context: Context,
+ ): SongExtractor {
+ val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+ return SongExtractor(
+ scope = scope,
+ context = context,
+ )
+ }
}
\ No newline at end of file
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 cbd2b95..d6c34df 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
@@ -23,6 +23,7 @@ import com.github.pakka_papad.components.FullScreenSadMessage
import com.github.pakka_papad.components.SongCardV1
import com.github.pakka_papad.components.more_options.SongOptions
import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.formatToDate
@Composable
fun AllSongs(
@@ -142,6 +143,10 @@ fun SongInfo(
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")
+ 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") }
},
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
new file mode 100644
index 0000000..4695fdf
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/home/Files.kt
@@ -0,0 +1,150 @@
+package com.github.pakka_papad.home
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.MoreVert
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+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.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.github.pakka_papad.components.FullScreenSadMessage
+import com.github.pakka_papad.R
+import com.github.pakka_papad.components.MiniSongCard
+import com.github.pakka_papad.components.more_options.FolderOptions
+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
+import com.github.pakka_papad.data.music.Song
+import com.github.pakka_papad.storage_explorer.Directory
+import com.github.pakka_papad.storage_explorer.DirectoryContents
+
+@Composable
+fun Files(
+ contents: DirectoryContents,
+ onDirectoryClicked: (Directory) -> Unit,
+ onSongClicked: (index: Int) -> Unit,
+ currentSong: Song?,
+ onAddToPlaylistClicked: (MiniSong) -> Unit,
+ onAddToQueueClicked: (MiniSong) -> Unit,
+ onFolderAddToBlacklistRequest: (Directory) -> Unit,
+){
+ if (contents.directories.isEmpty() && contents.songs.isEmpty()){
+ FullScreenSadMessage("Nothing here")
+ return
+ }
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),
+ ){
+ items(
+ items = contents.directories,
+ key = { it.absolutePath }
+ ){
+ Folder(
+ folder = it,
+ onDirectoryClicked = onDirectoryClicked,
+ options = listOf(
+ FolderOptions.Blacklist { onFolderAddToBlacklistRequest(it) }
+ )
+ )
+ }
+ itemsIndexed(
+ items = contents.songs,
+ key = { index, song -> song.location }
+ ){index, song ->
+ MiniSongCard(
+ song = song,
+ onSongClicked = { onSongClicked(index) },
+ songOptions = listOf(
+ SongOptions.AddToPlaylist{ onAddToPlaylistClicked(song) },
+ SongOptions.AddToQueue{ onAddToQueueClicked(song) },
+ ),
+ currentlyPlaying = (song.location == currentSong?.location)
+ )
+ }
+ }
+}
+
+@Composable
+fun Folder(
+ folder: Directory,
+ onDirectoryClicked: (Directory) -> Unit,
+ options: List,
+){
+ val resource = painterResource(R.drawable.ic_baseline_folder_40)
+ var showClickIndicator by remember { mutableStateOf(false) }
+ var showFileMenu by remember { mutableStateOf(false) }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ onDirectoryClicked(folder)
+ showClickIndicator = true
+ }
+ .padding(horizontal = 10.dp, vertical = 6.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = resource,
+ contentDescription = null,
+ modifier = Modifier.size(50.dp)
+ )
+ Text(
+ text = folder.name,
+ modifier = Modifier.weight(1f),
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ if(showClickIndicator){
+ CircularProgressIndicator(
+ modifier = Modifier.size(26.dp),
+ strokeWidth = 2.dp,
+ )
+ } else {
+ Icon(
+ imageVector = Icons.Outlined.MoreVert,
+ contentDescription = null,
+ modifier = Modifier
+ .size(26.dp)
+ .clickable(
+ onClick = {
+ showFileMenu = true
+ },
+ indication = rememberRipple(
+ bounded = false,
+ radius = 20.dp
+ ),
+ interactionSource = remember { MutableInteractionSource() }
+ ),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ }
+ if (showFileMenu){
+ OptionsAlertDialog(
+ options = options,
+ title = folder.name,
+ onDismissRequest = {
+ showFileMenu = false
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/home/HomeBottomBar.kt b/app/src/main/java/com/github/pakka_papad/home/HomeBottomBar.kt
index 79a22ac..f485d1b 100644
--- a/app/src/main/java/com/github/pakka_papad/home/HomeBottomBar.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/HomeBottomBar.kt
@@ -17,14 +17,18 @@ fun HomeBottomBar(
currentScreen: Screens,
onScreenChange: (Screens) -> Unit,
bottomBarColor: Color,
+ selectedTabs: List?
) {
+ if (selectedTabs == null) return
+ val screens = Screens.values()
BottomAppBar(
modifier = Modifier
.background(bottomBarColor)
.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom))
.height(88.dp),
) {
- Screens.values().forEach { screen ->
+ selectedTabs.filter { it >= 0 && it < screens.size }
+ .map { screens[it] }.forEach { screen ->
NavigationBarItem(
selected = (currentScreen == screen),
onClick = {
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 c5dfbd6..4cfadbd 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
@@ -14,9 +14,9 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.BottomSheetScaffold
+import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.rememberBottomSheetScaffoldState
+import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material.rememberSwipeableState
import androidx.compose.material3.*
import androidx.compose.runtime.*
@@ -108,9 +108,12 @@ class HomeFragment : Fragment() {
val systemUiController = rememberSystemUiController()
val themePreference by preferenceProvider.theme.collectAsStateWithLifecycle()
ZenTheme(themePreference, systemUiController) {
+ val selectedTabs by preferenceProvider.selectedTabs.collectAsStateWithLifecycle()
var currentScreen by rememberSaveable { mutableStateOf(Screens.Songs) }
val scope = rememberCoroutineScope()
+ val sortOrder by preferenceProvider.sortOrder.collectAsStateWithLifecycle()
+
val songs by viewModel.songs.collectAsStateWithLifecycle()
val allSongsListState = rememberLazyListState()
@@ -130,6 +133,8 @@ class HomeFragment : Fragment() {
val genresWithSongCount by viewModel.genresWithSongCount.collectAsStateWithLifecycle()
val allGenresListState = rememberLazyListState()
+ val files by viewModel.filesInCurrentDestination.collectAsStateWithLifecycle()
+
val dataRetrieved by remember {
derivedStateOf {
songs != null && albums != null && personsWithSongCount != null
@@ -169,12 +174,25 @@ class HomeFragment : Fragment() {
val repeatMode by viewModel.repeatMode.collectAsStateWithLifecycle()
val playerScaffoldState = rememberBottomSheetScaffoldState()
+ val isExplorerAtRoot by viewModel.isExplorerAtRoot.collectAsStateWithLifecycle()
+
+ val isQueueBottomSheetExpanded by remember(playerScaffoldState.bottomSheetState) {
+ derivedStateOf {
+ playerScaffoldState.bottomSheetState.currentValue
+ .equals(SheetValue.Expanded)
+ }
+ }
+
BackHandler(
- enabled = swipeableState.currentValue == 1,
+ enabled = (currentScreen == Screens.Folders && !isExplorerAtRoot) || swipeableState.currentValue == 1,
onBack = {
- scope.launch {
- if (playerScaffoldState.bottomSheetState.isExpanded) playerScaffoldState.bottomSheetState.collapse()
- else swipeableState.animateTo(0)
+ if (swipeableState.currentValue == 1){
+ scope.launch {
+ if (isQueueBottomSheetExpanded) playerScaffoldState.bottomSheetState.hide()
+ else swipeableState.animateTo(0)
+ }
+ } else {
+ viewModel.moveToParent()
}
}
)
@@ -205,9 +223,6 @@ class HomeFragment : Fragment() {
val expandQueueBottomSheet = remember<() -> Unit>{
{ scope.launch { playerScaffoldState.bottomSheetState.expand() } }
}
- val collapseQueueBottomSheet = remember<() -> Unit>{
- { scope.launch { playerScaffoldState.bottomSheetState.collapse() } }
- }
val updateScreen = remember<(Screens) -> Unit>{ {
if (currentScreen == it){
scope.launch {
@@ -217,6 +232,7 @@ class HomeFragment : Fragment() {
Screens.Artists -> allPersonsListState.scrollToItem(0)
Screens.Playlists -> allPlaylistsListState.scrollToItem(0)
Screens.Genres -> allGenresListState.scrollToItem(0)
+ else -> {}
}
}
} else {
@@ -234,6 +250,9 @@ class HomeFragment : Fragment() {
HomeTopBar(
onSettingsClicked = navigateToSettings,
onSearchClicked = navigateToSearch,
+ currentScreen = currentScreen,
+ onSortOptionChosen = viewModel::saveSortOption,
+ currentSortOrder = sortOrder,
)
},
content = {
@@ -242,7 +261,9 @@ class HomeFragment : Fragment() {
.padding(
top = it.calculateTopPadding(),
bottom = if (currentSong == null) 88.dp else 146.dp,
- start = windowInsets.calculateStartPadding(LayoutDirection.Ltr),
+ start = windowInsets.calculateStartPadding(
+ LayoutDirection.Ltr
+ ),
end = windowInsets.calculateEndPadding(LayoutDirection.Ltr)
)
.fillMaxSize(),
@@ -253,7 +274,10 @@ class HomeFragment : Fragment() {
modifier = Modifier.align(Alignment.Center)
)
} else {
- AnimatedContent(targetState = currentScreen) { targetScreen ->
+ AnimatedContent(
+ targetState = currentScreen,
+ label = ""
+ ) { targetScreen ->
when (targetScreen) {
Screens.Songs -> {
AllSongs(
@@ -302,6 +326,17 @@ class HomeFragment : Fragment() {
onGenreClicked = this@HomeFragment::navigateToCollection
)
}
+ Screens.Folders -> {
+ Files(
+ contents = files,
+ onDirectoryClicked = viewModel::onFileClicked,
+ onSongClicked = viewModel::onFileClicked,
+ currentSong = currentSong,
+ onAddToPlaylistClicked = this@HomeFragment::addToPlaylistClicked,
+ onAddToQueueClicked = viewModel::addToQueue,
+ onFolderAddToBlacklistRequest = viewModel::onFolderBlacklist
+ )
+ }
}
}
}
@@ -316,7 +351,9 @@ class HomeFragment : Fragment() {
.fillMaxWidth()
.background(bottomBarColor)
.padding(
- start = windowInsets.calculateStartPadding(LayoutDirection.Ltr),
+ start = windowInsets.calculateStartPadding(
+ LayoutDirection.Ltr
+ ),
end = windowInsets.calculateEndPadding(LayoutDirection.Ltr),
)
) {
@@ -397,8 +434,7 @@ class HomeFragment : Fragment() {
queue = queue,
onFavouriteClicked = viewModel::changeFavouriteValue,
currentSong = it,
- onDownArrowClicked = collapseQueueBottomSheet,
- expanded = playerScaffoldState.bottomSheetState.isExpanded,
+ expanded = isQueueBottomSheetExpanded,
exoPlayer = exoPlayer,
onDrag = viewModel::onSongDrag
)
@@ -409,9 +445,8 @@ class HomeFragment : Fragment() {
bottomStart = 0.dp,
bottomEnd = 0.dp
),
- sheetElevation = 20.dp,
sheetPeekHeight = 0.dp,
- sheetGesturesEnabled = true,
+ sheetContainerColor = MaterialTheme.colorScheme.secondaryContainer,
)
}
},
@@ -426,7 +461,8 @@ class HomeFragment : Fragment() {
HomeBottomBar(
currentScreen = currentScreen,
onScreenChange = updateScreen,
- bottomBarColor = bottomBarColor
+ bottomBarColor = bottomBarColor,
+ selectedTabs = selectedTabs,
)
}
}
@@ -519,4 +555,14 @@ class HomeFragment : Fragment() {
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/HomeTopBar.kt b/app/src/main/java/com/github/pakka_papad/home/HomeTopBar.kt
index d62368c..f6ea12c 100644
--- a/app/src/main/java/com/github/pakka_papad/home/HomeTopBar.kt
+++ b/app/src/main/java/com/github/pakka_papad/home/HomeTopBar.kt
@@ -12,9 +12,14 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
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.draw.rotate
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
@@ -22,68 +27,108 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.pakka_papad.components.SmallTopBar
+import com.github.pakka_papad.R
+import com.github.pakka_papad.Screens
+import com.github.pakka_papad.components.SortOptionChooser
+import com.github.pakka_papad.components.getSortOptions
@Composable
fun HomeTopBar(
onSettingsClicked: () -> Unit,
onSearchClicked: () -> Unit,
-) = SmallTopBar(
- leadingIcon = { },
- title = buildAnnotatedString {
- withStyle(
- SpanStyle(
- fontWeight = FontWeight.ExtraBold,
- fontSize = 26.sp,
- )
- ) {
- append("Zen ")
- }
- withStyle(
- SpanStyle(
- color = MaterialTheme.colorScheme.primary,
- fontWeight = FontWeight.ExtraBold,
- fontSize = 26.sp
+ currentScreen: Screens,
+ onSortOptionChosen: (currScreen: Int, option: Int) -> Unit,
+ currentSortOrder: Map,
+) {
+ var sortMenuVisible by remember { mutableStateOf(false) }
+ val options by remember(currentScreen.ordinal) { derivedStateOf {
+ currentScreen.getSortOptions()
+ } }
+ SmallTopBar(
+ leadingIcon = { },
+ title = buildAnnotatedString {
+ withStyle(
+ SpanStyle(
+ fontWeight = FontWeight.ExtraBold,
+ fontSize = 26.sp,
+ )
+ ) {
+ append("Zen ")
+ }
+ withStyle(
+ SpanStyle(
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.ExtraBold,
+ fontSize = 26.sp
+ )
+ ) {
+ append("Music")
+ }
+ },
+ actions = {
+ Icon(
+ imageVector = Icons.Outlined.Search,
+ contentDescription = "search-btn",
+ modifier = Modifier
+ .size(48.dp)
+ .padding(9.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(
+ bounded = false,
+ radius = 25.dp,
+ ),
+ onClick = onSearchClicked
+ ),
+ tint = MaterialTheme.colorScheme.onSurface,
)
- ) {
- append("Music")
- }
- },
- actions = {
- Icon(
- imageVector = Icons.Outlined.Search,
- contentDescription = "search-btn",
- modifier = Modifier
- .size(48.dp)
- .padding(9.dp)
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = rememberRipple(
- bounded = false,
- radius = 25.dp,
+ Icon(
+ painter = painterResource(id = R.drawable.ic_baseline_sort_40),
+ contentDescription = "sort-btn",
+ modifier = Modifier
+ .size(48.dp)
+ .padding(9.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(
+ bounded = false,
+ radius = 25.dp,
+ ),
+ onClick = { sortMenuVisible = true }
),
- onClick = onSearchClicked
- ),
- tint = MaterialTheme.colorScheme.onSurface,
- )
- Icon(
- imageVector = Icons.Outlined.Settings,
- contentDescription = "settings-btn",
- modifier = Modifier
- .size(48.dp)
- .padding(9.dp)
- .rotate(90f)
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = rememberRipple(
- bounded = false,
- radius = 25.dp,
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ Icon(
+ imageVector = Icons.Outlined.Settings,
+ contentDescription = "settings-btn",
+ modifier = Modifier
+ .size(48.dp)
+ .padding(9.dp)
+ .rotate(90f)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(
+ bounded = false,
+ radius = 25.dp,
+ ),
+ onClick = onSettingsClicked
),
- onClick = onSettingsClicked
- ),
- tint = MaterialTheme.colorScheme.onSurface,
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ },
+ titleMaxLines = 1,
+ backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
+ onBackgroundColor = MaterialTheme.colorScheme.onSurface,
+ )
+ if (sortMenuVisible){
+ SortOptionChooser(
+ options = options,
+ selectedOption = currentSortOrder[currentScreen.ordinal] ?: options.first().ordinal,
+ onOptionSelect = { option ->
+ onSortOptionChosen(currentScreen.ordinal, option)
+ sortMenuVisible = false
+ },
+ onChooserDismiss = { sortMenuVisible = false }
)
- },
- titleMaxLines = 1,
- backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
- onBackgroundColor = MaterialTheme.colorScheme.onSurface,
-)
+ }
+}
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 b5d6246..18cf0e5 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
@@ -6,8 +6,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
+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.storage_explorer.*
+import com.github.pakka_papad.storage_explorer.Directory
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@@ -19,10 +23,26 @@ class HomeViewModel @Inject constructor(
private val context: Application,
private val manager: DataManager,
private val exoPlayer: ExoPlayer,
+ private val songExtractor: SongExtractor,
+ private val prefs: ZenPreferenceProvider,
) : ViewModel() {
val songs = manager.getAll.songs()
- .catch { exception ->
+ .combine(prefs.songSortOrder){ songs, sortOrder ->
+ when(sortOrder){
+ SortOptions.TitleASC.ordinal -> songs.sortedBy { it.title }
+ SortOptions.TitleDSC.ordinal -> songs.sortedByDescending { it.title }
+ SortOptions.AlbumASC.ordinal -> songs.sortedBy { it.album }
+ SortOptions.AlbumDSC.ordinal -> songs.sortedByDescending { it.album }
+ SortOptions.ArtistASC.ordinal -> songs.sortedBy { it.artist }
+ SortOptions.ArtistDSC.ordinal -> songs.sortedByDescending { it.artist }
+ SortOptions.YearASC.ordinal -> songs.sortedBy { it.year }
+ SortOptions.YearDSC.ordinal -> songs.sortedByDescending { it.year }
+ SortOptions.DurationASC.ordinal -> songs.sortedBy { it.durationMillis }
+ SortOptions.DurationDSC.ordinal -> songs.sortedByDescending { it.durationMillis }
+ else -> songs
+ }
+ }.catch { exception ->
Timber.e(exception)
}.stateIn(
scope = viewModelScope,
@@ -31,7 +51,13 @@ class HomeViewModel @Inject constructor(
)
val albums = manager.getAll.albums()
- .catch { exception ->
+ .combine(prefs.albumSortOrder){ albums, sortOrder ->
+ when(sortOrder){
+ SortOptions.TitleASC.ordinal -> albums.sortedBy { it.name }
+ SortOptions.TitleDSC.ordinal -> albums.sortedByDescending { it.name }
+ else -> albums
+ }
+ }.catch { exception ->
Timber.e(exception)
}.stateIn(
scope = viewModelScope,
@@ -55,6 +81,14 @@ class HomeViewModel @Inject constructor(
Person.Composer -> manager.getAll.composers()
Person.Lyricist -> manager.getAll.lyricists()
}
+ }.combine(prefs.artistSortOrder){ artists, sortOrder ->
+ when(sortOrder){
+ SortOptions.NameASC.ordinal -> artists.sortedBy { it.name }
+ SortOptions.NameDSC.ordinal -> artists.sortedByDescending { it.name }
+ SortOptions.SongsCountASC.ordinal -> artists.sortedBy { it.count }
+ SortOptions.SongsCountDSC.ordinal -> artists.sortedByDescending { it.count }
+ else -> artists
+ }
}.catch { exception ->
Timber.e(exception)
}.stateIn(
@@ -64,7 +98,15 @@ class HomeViewModel @Inject constructor(
)
val playlistsWithSongCount = manager.getAll.playlists()
- .catch { exception ->
+ .combine(prefs.playlistSortOrder){ playlists, sortOrder ->
+ when(sortOrder){
+ SortOptions.NameASC.ordinal -> playlists.sortedBy { it.playlistName }
+ SortOptions.NameDSC.ordinal -> playlists.sortedByDescending { it.playlistName }
+ SortOptions.SongsCountASC.ordinal -> playlists.sortedBy { it.count }
+ SortOptions.SongsCountDSC.ordinal -> playlists.sortedByDescending { it.count }
+ else -> playlists
+ }
+ }.catch { exception ->
Timber.e(exception)
}.stateIn(
scope = viewModelScope,
@@ -73,7 +115,15 @@ class HomeViewModel @Inject constructor(
)
val genresWithSongCount = manager.getAll.genres()
- .catch { exception ->
+ .combine(prefs.genreSortOrder){ genres, sortOrder ->
+ when(sortOrder){
+ SortOptions.NameASC.ordinal -> genres.sortedBy { it.genreName }
+ SortOptions.NameDSC.ordinal -> genres.sortedByDescending { it.genreName }
+ SortOptions.SongsCountASC.ordinal -> genres.sortedBy { it.count }
+ SortOptions.SongsCountDSC.ordinal -> genres.sortedByDescending { it.count }
+ else -> genres
+ }
+ }.catch { exception ->
Timber.e(exception)
}.stateIn(
scope = viewModelScope,
@@ -81,6 +131,10 @@ class HomeViewModel @Inject constructor(
initialValue = emptyList()
)
+ fun saveSortOption(screen: Int, option: Int){
+ prefs.updateSortOrder(screen, option)
+ }
+
val currentSong = manager.currentSong
val queue = manager.queue
@@ -113,6 +167,7 @@ class HomeViewModel @Inject constructor(
override fun onCleared() {
super.onCleared()
exoPlayer.removeListener(exoPlayerListener)
+ explorer.removeListener(directoryChangeListener)
}
/**
@@ -132,6 +187,17 @@ class HomeViewModel @Inject constructor(
}
}
+ fun onFolderBlacklist(folder: Directory){
+ viewModelScope.launch {
+ try {
+ manager.addFolderToBlacklist(folder.absolutePath)
+ showToast("Done")
+ } catch (_: Exception){
+ showToast("Some error occurred")
+ }
+ }
+ }
+
fun onPlaylistCreate(playlistName: String) {
viewModelScope.launch {
manager.createPlaylist(playlistName)
@@ -188,6 +254,11 @@ class HomeViewModel @Inject constructor(
}
}
+ fun addToQueue(song: MiniSong) {
+ val resolvedSong = songExtractor.resolveSong(song.location) ?: return
+ addToQueue(resolvedSong)
+ }
+
/**
* Create and set a new queue in exoplayer.
* Old queue is discarded.
@@ -214,4 +285,69 @@ class HomeViewModel @Inject constructor(
fun onSongDrag(fromIndex: Int, toIndex: Int) = manager.moveItem(fromIndex,toIndex)
+
+ private val _filesInCurrentDestination = MutableStateFlow(DirectoryContents())
+ val filesInCurrentDestination = _filesInCurrentDestination
+ .combine(prefs.folderSortOrder){ files, sortOrder ->
+ when(sortOrder){
+ SortOptions.NameASC.ordinal -> {
+ DirectoryContents(
+ directories = files.directories.sortedBy { it.name },
+ songs = files.songs.sortedBy { it.title }
+ )
+ }
+ SortOptions.NameDSC.ordinal -> {
+ DirectoryContents(
+ directories = files.directories.sortedByDescending { it.name },
+ songs = files.songs.sortedByDescending { it.title }
+ )
+ }
+ else -> files
+ }
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5_000),
+ initialValue = DirectoryContents()
+ )
+
+ private val explorer = MusicFileExplorer(songExtractor)
+
+ private val _isExplorerAtRoot = MutableStateFlow(true)
+ val isExplorerAtRoot = _isExplorerAtRoot.asStateFlow()
+
+ private val directoryChangeListener = object : MusicFileExplorer.DirectoryChangeListener {
+ override fun onDirectoryChanged(path: String, files: DirectoryContents) {
+ _filesInCurrentDestination.update { files }
+ }
+ }
+
+ init {
+ viewModelScope.launch {
+ explorer.addListener(directoryChangeListener)
+ }
+ }
+
+ fun onFileClicked(songIndex: Int){
+ viewModelScope.launch {
+ if(songIndex < 0 || songIndex >= filesInCurrentDestination.value.songs.size) return@launch
+ val song = songExtractor.resolveSong(filesInCurrentDestination.value.songs[songIndex].location)
+ song?.let {
+ setQueue(listOf(song))
+ }
+ }
+ }
+
+ fun onFileClicked(file: Directory){
+ viewModelScope.launch(Dispatchers.Default) {
+ explorer.moveInsideDirectory(file.absolutePath)
+ _isExplorerAtRoot.update { explorer.isRoot }
+ }
+ }
+
+ fun moveToParent() {
+ viewModelScope.launch(Dispatchers.Default) {
+ explorer.moveToParent()
+ _isExplorerAtRoot.update { explorer.isRoot }
+ }
+ }
}
\ 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 06772e3..3e78a05 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
@@ -2,22 +2,15 @@ package com.github.pakka_papad.nowplaying
import androidx.compose.foundation.ExperimentalFoundationApi
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.KeyboardArrowDown
-import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
import androidx.media3.exoplayer.ExoPlayer
import com.github.pakka_papad.components.SongCardV2
import com.github.pakka_papad.data.music.Song
@@ -30,28 +23,10 @@ fun ColumnScope.Queue(
queue: List,
onFavouriteClicked: (Song) -> Unit,
currentSong: Song?,
- onDownArrowClicked: () -> Unit,
expanded: Boolean,
exoPlayer: ExoPlayer,
onDrag: (fromIndex: Int, toIndex: Int) -> Unit,
) {
- Icon(
- imageVector = Icons.Outlined.KeyboardArrowDown,
- contentDescription = "down arrow icon",
- modifier = Modifier
- .background(MaterialTheme.colorScheme.secondaryContainer)
- .fillMaxWidth()
- .size(36.dp)
- .clickable(
- onClick = onDownArrowClicked,
- indication = rememberRipple(
- bounded = true,
- radius = 18.dp
- ),
- interactionSource = remember { MutableInteractionSource() }
- ),
- tint = MaterialTheme.colorScheme.onSecondaryContainer
- )
val listState = rememberLazyListState()
LaunchedEffect(key1 = currentSong, key2 = expanded) {
delay(600)
@@ -77,7 +52,7 @@ fun ColumnScope.Queue(
.dragContainer(dragDropState),
state = listState,
contentPadding = WindowInsets.systemBars
- .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)
+ .only(WindowInsetsSides.Bottom)
.asPaddingValues(),
) {
itemsIndexed(
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 85f66f4..fc5908f 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
@@ -2,6 +2,7 @@ package com.github.pakka_papad.player
import android.app.NotificationManager
import android.app.Service
+import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@@ -15,20 +16,25 @@ import android.widget.Toast
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
+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.data.ZenCrashReporter
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.data.music.Song
import com.github.pakka_papad.data.notification.ZenNotificationManager
+import com.github.pakka_papad.widgets.WidgetBroadcast
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import timber.log.Timber
import java.io.File
import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
-@AndroidEntryPoint
+@UnstableApi @AndroidEntryPoint
class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback {
@Inject
@@ -53,6 +59,20 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
private val job = SupervisorJob()
private val scope = CoroutineScope(job + Dispatchers.Default)
+ private val playTimeThresholdMs = 10.seconds.inWholeMilliseconds
+
+ private val playbackStatsListener = PlaybackStatsListener(false) { eventTime, playbackStats ->
+ if (playbackStats.totalPlayTimeMs < playTimeThresholdMs) return@PlaybackStatsListener
+ val window = eventTime.timeline.getWindow(eventTime.windowIndex, Timeline.Window())
+ try {
+ window.mediaItem.localConfiguration?.tag?.let {
+ dataManager.addPlayHistory(it as String, playbackStats.totalPlayTimeMs)
+ }
+ } catch (_ : Exception){
+
+ }
+ }
+
companion object {
const val MEDIA_SESSION = "media_session"
}
@@ -67,6 +87,16 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
try {
dataManager.updateCurrentSong(exoPlayer.currentMediaItemIndex)
+ dataManager.getSongAtIndex(exoPlayer.currentMediaItemIndex)?.let { song ->
+ val broadcast = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE).apply {
+ putExtra(WidgetBroadcast.WIDGET_BROADCAST, WidgetBroadcast.SONG_CHANGED)
+ putExtra("imageUri", song.artUri)
+ putExtra("title", song.title)
+ putExtra("artist", song.artist)
+ putExtra("album", song.album)
+ }
+ this@ZenPlayer.applicationContext.sendBroadcast(broadcast)
+ }
} catch (e: Exception) {
Timber.e(e)
}
@@ -76,6 +106,11 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
+ val broadcast = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE).apply {
+ putExtra(WidgetBroadcast.WIDGET_BROADCAST, WidgetBroadcast.IS_PLAYING_CHANGED)
+ putExtra("isPlaying", isPlaying)
+ }
+ this@ZenPlayer.applicationContext.sendBroadcast(broadcast)
updateMediaSessionState()
updateMediaSessionMetadata()
}
@@ -136,6 +171,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
broadcastReceiver?.startListening(this)
mediaSession.setCallback(mediaSessionCallback)
exoPlayer.addListener(exoPlayerListener)
+ exoPlayer.addAnalyticsListener(playbackStatsListener)
startForeground(
ZenNotificationManager.PLAYER_NOTIFICATION_ID,
@@ -181,6 +217,7 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
exoPlayer.stop()
exoPlayer.clearMediaItems()
exoPlayer.removeListener(exoPlayerListener)
+ exoPlayer.removeAnalyticsListener(playbackStatsListener)
mediaSession.release()
dataManager.stopPlayerRunning()
broadcastReceiver?.stopListening()
@@ -189,6 +226,14 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
job.cancel()
systemNotificationManager = null
broadcastReceiver = null
+ val broadcast = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE).apply {
+ putExtra(WidgetBroadcast.WIDGET_BROADCAST, WidgetBroadcast.SONG_CHANGED)
+ putExtra("imageUri", "")
+ putExtra("title", "")
+ putExtra("artist", "")
+ putExtra("album", "")
+ }
+ applicationContext.sendBroadcast(broadcast)
}
private fun updateMediaSessionMetadata() {
@@ -258,26 +303,38 @@ class ZenPlayer : Service(), DataManager.Callback, ZenBroadcastReceiver.Callback
@Synchronized
override fun setQueue(newQueue: List, startPlayingFromIndex: Int) {
- exoPlayer.stop()
- exoPlayer.clearMediaItems()
- val mediaItems = newQueue.map {
- MediaItem.fromUri(Uri.fromFile(File(it.location)))
+ scope.launch {
+ val mediaItems = newQueue.map {
+ MediaItem.Builder().apply {
+ setUri(Uri.fromFile(File(it.location)))
+ setTag(it.location)
+ }.build()
+ }
+ withContext(Dispatchers.Main){
+ exoPlayer.stop()
+ exoPlayer.clearMediaItems()
+ exoPlayer.addMediaItems(mediaItems)
+ exoPlayer.prepare()
+ exoPlayer.seekTo(startPlayingFromIndex,0)
+ exoPlayer.repeatMode = dataManager.repeatMode.value.toExoPlayerRepeatMode()
+ exoPlayer.playbackParameters = preferencesProvider.playbackParams.value
+ .toCorrectedParams()
+ .toExoPlayerPlaybackParameters()
+ exoPlayer.play()
+ }
+ updateMediaSessionState()
+ updateMediaSessionMetadata()
}
- exoPlayer.addMediaItems(mediaItems)
- exoPlayer.prepare()
- exoPlayer.seekTo(startPlayingFromIndex,0)
- exoPlayer.repeatMode = dataManager.repeatMode.value.toExoPlayerRepeatMode()
- exoPlayer.playbackParameters = preferencesProvider.playbackParams.value
- .toCorrectedParams()
- .toExoPlayerPlaybackParameters()
- exoPlayer.play()
- updateMediaSessionState()
- updateMediaSessionMetadata()
}
@Synchronized
override fun addToQueue(song: Song) {
- exoPlayer.addMediaItem(MediaItem.fromUri(Uri.fromFile(File(song.location))))
+ exoPlayer.addMediaItem(
+ MediaItem.Builder().apply {
+ setUri(Uri.fromFile(File(song.location)))
+ setTag(song.location)
+ }.build()
+ )
}
@Synchronized
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 4d5c9de..a1ef718 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
@@ -3,16 +3,13 @@ package com.github.pakka_papad.restore
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material3.Checkbox
import androidx.compose.material.Text
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.components.SelectableCard
import com.github.pakka_papad.data.music.BlacklistedSong
@Composable
@@ -22,6 +19,7 @@ fun RestoreContent(
paddingValues: PaddingValues,
onSelectChanged: (index: Int, isSelected: Boolean) -> Unit,
){
+ if (songs.size != selectList.size) return
LazyColumn(
modifier = Modifier
.fillMaxSize(),
@@ -42,24 +40,15 @@ fun RestoreContent(
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SelectableBlacklistedSong(
song: BlacklistedSong,
isSelected: Boolean,
onSelectChange: (isSelected: Boolean) -> Unit,
-){
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(10.dp),
- verticalAlignment = Alignment.CenterVertically,
- ){
- Checkbox(
- checked = isSelected,
- onCheckedChange = onSelectChange
- )
- Spacer(Modifier.width(10.dp))
+) = SelectableCard(
+ isSelected = isSelected,
+ onSelectChange = onSelectChange,
+ content = {
Column {
Text(
text = song.title,
@@ -78,4 +67,4 @@ fun SelectableBlacklistedSong(
)
}
}
-}
\ No newline at end of file
+)
\ No newline at end of file
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 8e9da07..f63995b 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
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -19,6 +20,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
+import com.github.pakka_papad.components.CancelConfirmTopBar
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.ui.theme.ZenTheme
import dagger.hilt.android.AndroidEntryPoint
@@ -48,14 +50,18 @@ 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()
+ }
+ }
Scaffold(
topBar = {
- RestoreTopBar(
+ CancelConfirmTopBar(
onCancelClicked = navController::popBackStack,
- onConfirmClicked = {
- viewModel.restoreSongs()
- navController.popBackStack()
- }
+ onConfirmClicked = viewModel::restoreSongs,
+ title = "Restore songs"
)
},
content = { paddingValues ->
diff --git a/app/src/main/java/com/github/pakka_papad/restore/RestoreTopBar.kt b/app/src/main/java/com/github/pakka_papad/restore/RestoreTopBar.kt
deleted file mode 100644
index 05b9450..0000000
--- a/app/src/main/java/com/github/pakka_papad/restore/RestoreTopBar.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.github.pakka_papad.restore
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Check
-import androidx.compose.material.icons.outlined.Close
-import androidx.compose.material.ripple.rememberRipple
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import com.github.pakka_papad.components.CenterAlignedTopBar
-
-@Composable
-fun RestoreTopBar(
- onCancelClicked: () -> Unit,
- onConfirmClicked: () -> Unit,
-) = CenterAlignedTopBar(
- leadingIcon = {
- Icon(
- imageVector = Icons.Outlined.Close,
- contentDescription = null,
- modifier = Modifier
- .padding(16.dp)
- .size(30.dp)
- .clickable(
- interactionSource = remember{ MutableInteractionSource() },
- indication = rememberRipple(
- bounded = false,
- radius = 25.dp,
- ),
- onClick = onCancelClicked
- ),
- tint = MaterialTheme.colorScheme.onSurface,
- )
- },
- title = "Restore Songs",
- actions = {
- Icon(
- imageVector = Icons.Outlined.Check,
- contentDescription = null,
- modifier = Modifier
- .padding(16.dp)
- .size(30.dp)
- .clickable(
- interactionSource = remember{ MutableInteractionSource() },
- indication = rememberRipple(
- bounded = false,
- radius = 25.dp,
- ),
- onClick = onConfirmClicked
- ),
- tint = MaterialTheme.colorScheme.onSurface
- )
- },
- titleMaxLines = 1
-)
\ No newline at end of file
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 fb1c51c..f06b056 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
@@ -7,9 +7,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.pakka_papad.data.DataManager
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.onEach
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -38,6 +41,9 @@ class RestoreViewModel @Inject constructor(
_restoreList[index] = isSelected
}
+ private val _restored = MutableStateFlow(false)
+ val restored = _restored.asStateFlow()
+
fun restoreSongs(){
viewModelScope.launch {
val blacklist = blackListedSongs.value
@@ -50,6 +56,8 @@ class RestoreViewModel @Inject constructor(
} catch (e: Exception){
Timber.e(e)
Toast.makeText(context,"Some error occurred",Toast.LENGTH_SHORT).show()
+ } finally {
+ _restored.update { true }
}
}
}
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
new file mode 100644
index 0000000..232a79c
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderContent.kt
@@ -0,0 +1,56 @@
+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
+import androidx.compose.material.Text
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.github.pakka_papad.components.SelectableCard
+import com.github.pakka_papad.data.music.BlacklistedFolder
+
+@Composable
+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(),
+ ) {
+ itemsIndexed(
+ items = folders,
+ key = { index, folder -> folder.path }
+ ) { index, folder ->
+ SelectableBlacklistedFolder(
+ folder = folder,
+ isSelected = selectList[index],
+ onSelectChange = { onSelectChanged(index, it) }
+ )
+ }
+ }
+}
+
+@Composable
+fun SelectableBlacklistedFolder(
+ folder: BlacklistedFolder,
+ isSelected: Boolean,
+ onSelectChange: (Boolean) -> Unit,
+) = SelectableCard(
+ isSelected = isSelected,
+ onSelectChange = onSelectChange,
+ content = {
+ Text(
+ text = folder.path,
+ modifier = Modifier.weight(1f),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleMedium
+ )
+ }
+)
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
new file mode 100644
index 0000000..748d0b8
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderFragment.kt
@@ -0,0 +1,114 @@
+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.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.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+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.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.components.CancelConfirmTopBar
+import com.github.pakka_papad.data.ZenPreferenceProvider
+import com.github.pakka_papad.ui.theme.ZenTheme
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class RestoreFolderFragment: Fragment() {
+
+ private val viewModel: RestoreFolderViewModel by viewModels()
+
+ private lateinit var navController: NavController
+
+ @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 {
+ val themePreference by preferenceProvider.theme.collectAsStateWithLifecycle()
+ val folders by viewModel.folders.collectAsStateWithLifecycle()
+ val selectList = viewModel.restoreFolderList
+
+ val restored by viewModel.restored.collectAsStateWithLifecycle()
+ LaunchedEffect(key1 = restored){
+ if (restored){
+ navController.popBackStack()
+ }
+ }
+
+ ZenTheme(themePreference) {
+
+ Scaffold(
+ topBar = {
+ CancelConfirmTopBar(
+ onCancelClicked = navController::popBackStack,
+ onConfirmClicked = viewModel::restoreFolders,
+ title = "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
+ ){
+ CircularProgressIndicator()
+ }
+ } 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
+ )
+ }
+ },
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..55d02ca
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/restore_folder/RestoreFolderViewModel.kt
@@ -0,0 +1,67 @@
+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 dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class RestoreFolderViewModel @Inject constructor(
+ private val context: Application,
+ private val manager: DataManager,
+): ViewModel() {
+
+ private val _restoreFoldersList = mutableStateListOf()
+ val restoreFolderList : List = _restoreFoldersList
+
+ val folders = manager.getAll.blacklistedFolders()
+ .onEach {
+ while (_restoreFoldersList.size < it.size) _restoreFoldersList.add(false)
+ while (_restoreFoldersList.size > it.size) _restoreFoldersList.removeLast()
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5_000),
+ initialValue = emptyList()
+ )
+
+ fun updateRestoreList(index: Int, isSelected: Boolean){
+ if (index >= _restoreFoldersList.size) return
+ _restoreFoldersList[index] = isSelected
+ }
+
+ private val _restored = MutableStateFlow(false)
+ val restored = _restored.asStateFlow()
+
+ fun restoreFolders(){
+ viewModelScope.launch {
+ 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")
+ } catch (_ : Exception){
+ showToast("Some error occurred")
+ } finally {
+ _restored.update { true }
+ }
+ }
+ }
+
+ 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/select_playlist/SelectPlaylistFragment.kt b/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistFragment.kt
index d143fbe..d43f908 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
@@ -20,6 +20,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
+import com.github.pakka_papad.components.CancelConfirmTopBar
import com.github.pakka_papad.data.ZenPreferenceProvider
import com.github.pakka_papad.ui.theme.ZenTheme
import dagger.hilt.android.AndroidEntryPoint
@@ -56,12 +57,13 @@ class SelectPlaylistFragment: Fragment() {
val selectList = viewModel.selectList
Scaffold(
topBar = {
- SelectPlaylistTopBar(
+ CancelConfirmTopBar(
onCancelClicked = navController::popBackStack,
onConfirmClicked = {
viewModel.addSongsToPlaylists(args.songLocations)
navController.popBackStack()
- }
+ },
+ title = "Select Playlists"
)
},
content = { paddingValues ->
diff --git a/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistTopBar.kt b/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistTopBar.kt
deleted file mode 100644
index 56be3d6..0000000
--- a/app/src/main/java/com/github/pakka_papad/select_playlist/SelectPlaylistTopBar.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.github.pakka_papad.select_playlist
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Check
-import androidx.compose.material.icons.outlined.Close
-import androidx.compose.material.ripple.rememberRipple
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import com.github.pakka_papad.components.CenterAlignedTopBar
-
-@Composable
-fun SelectPlaylistTopBar(
- onCancelClicked: () -> Unit,
- onConfirmClicked: () -> Unit,
-) = CenterAlignedTopBar(
- leadingIcon = {
- Icon(
- imageVector = Icons.Outlined.Close,
- contentDescription = null,
- modifier = Modifier
- .padding(16.dp)
- .size(30.dp)
- .clickable(
- interactionSource = remember{ MutableInteractionSource() },
- indication = rememberRipple(
- bounded = false,
- radius = 25.dp,
- ),
- onClick = onCancelClicked
- ),
- tint = MaterialTheme.colorScheme.onSurface,
- )
- },
- title = "Select Playlists",
- actions = {
- Icon(
- imageVector = Icons.Outlined.Check,
- contentDescription = null,
- modifier = Modifier
- .padding(16.dp)
- .size(30.dp)
- .clickable(
- interactionSource = remember{ MutableInteractionSource() },
- indication = rememberRipple(
- bounded = false,
- radius = 25.dp,
- ),
- onClick = onConfirmClicked
- ),
- tint = MaterialTheme.colorScheme.onSurface
- )
- },
- titleMaxLines = 1
-)
\ No newline at end of file
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 5805da7..d5c6c72 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
@@ -43,11 +43,13 @@ class SelectPlaylistViewModel @Inject constructor(
fun addSongsToPlaylists(songLocations: Array) {
viewModelScope.launch {
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 songLocations) {
+ for (songLocation in validSongs) {
list += PlaylistSongCrossRef(playlists[it].playlistId, songLocation)
}
list.toList()
@@ -58,6 +60,10 @@ class SelectPlaylistViewModel @Inject constructor(
} 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()
+ }
}
}
}
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 00982e4..df323e8 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
@@ -48,6 +48,8 @@ class SettingsFragment : Fragment() {
val scanStatus by viewModel.scanStatus.collectAsStateWithLifecycle()
val isCrashlyticsDisabled by preferenceProvider.isCrashlyticsDisabled.collectAsStateWithLifecycle()
+ val tabsSelection by viewModel.tabsSelection.collectAsStateWithLifecycle()
+
val restoreClicked = remember{ {
if (navController.currentDestination?.id == R.id.settingsFragment){
navController.navigate(R.id.action_settingsFragment_to_restoreFragment)
@@ -58,6 +60,11 @@ class SettingsFragment : Fragment() {
navController.navigate(R.id.action_settingsFragment_to_whatsNewFragment)
}
} }
+ val onRestoreFoldersClicked = remember{ {
+ if (navController.currentDestination?.id == R.id.settingsFragment){
+ navController.navigate(R.id.action_settingsFragment_to_restoreFolderFragment)
+ }
+ } }
ZenTheme(themePreference) {
Scaffold(
@@ -85,7 +92,12 @@ class SettingsFragment : Fragment() {
onRestoreClicked = restoreClicked,
disabledCrashlytics = isCrashlyticsDisabled,
onAutoReportCrashClicked = preferenceProvider::toggleCrashlytics,
- onWhatsNewClicked = whatsNewClicked
+ onWhatsNewClicked = whatsNewClicked,
+ onRestoreFoldersClicked = onRestoreFoldersClicked,
+ tabsSelection = tabsSelection,
+ onTabsSelectChange = viewModel::onTabsSelectChanged,
+ onTabsOrderChanged = viewModel::onTabsOrderChanged,
+ onTabsOrderConfirmed = viewModel::saveTabsOrder
)
}
)
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 a56f429..ccd6752 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
@@ -7,16 +7,28 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.widget.Toast
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -29,10 +41,13 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.github.pakka_papad.BuildConfig
import com.github.pakka_papad.R
-import com.github.pakka_papad.components.OutlinedBox
+import com.github.pakka_papad.Screens
import com.github.pakka_papad.data.UserPreferences
import com.github.pakka_papad.data.UserPreferences.Accent
import com.github.pakka_papad.data.music.ScanStatus
+import com.github.pakka_papad.nowplaying.DraggableItem
+import com.github.pakka_papad.nowplaying.dragContainer
+import com.github.pakka_papad.nowplaying.rememberDragDropState
import com.github.pakka_papad.ui.theme.ThemePreference
import com.github.pakka_papad.ui.theme.getSeedColor
import timber.log.Timber
@@ -48,27 +63,46 @@ fun SettingsList(
disabledCrashlytics: Boolean,
onAutoReportCrashClicked: (Boolean) -> Unit,
onWhatsNewClicked: () -> Unit,
+ onRestoreFoldersClicked: () -> Unit,
+ tabsSelection: List>,
+ onTabsSelectChange: (Screens, Boolean) -> Unit,
+ onTabsOrderChanged: (fromIdx: Int, toIdx: Int) -> Unit,
+ onTabsOrderConfirmed: () -> Unit,
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 10.dp),
contentPadding = paddingValues,
- verticalArrangement = Arrangement.spacedBy(10.dp)
+ verticalArrangement = Arrangement.spacedBy(4.dp)
) {
+ item {
+ GroupTitle(title = "Look and feel")
+ }
item {
LookAndFeelSettings(
themePreference = themePreference,
onPreferenceChanged = onThemePreferenceChanged,
+ tabsSelection = tabsSelection,
+ onTabsSelectChange = onTabsSelectChange,
+ onTabsOrderChanged = onTabsOrderChanged,
+ onTabsOrderConfirmed = onTabsOrderConfirmed,
)
}
+ item {
+ GroupTitle(title = "Music library")
+ }
item {
MusicLibrarySettings(
scanStatus = scanStatus,
onScanClicked = onScanClicked,
- onRestoreClicked = onRestoreClicked
+ onRestoreClicked = onRestoreClicked,
+ onRestoreFoldersClicked = onRestoreFoldersClicked
)
}
+ item {
+ GroupTitle(title = "Report bug")
+ }
item {
ReportBug(
disabledCrashlytics = disabledCrashlytics,
@@ -83,103 +117,113 @@ fun SettingsList(
}
}
+@Composable
+fun GroupTitle(
+ title: String,
+){
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier
+ .padding(start = 8.dp, top = 8.dp)
+ )
+}
+
@Composable
private fun LookAndFeelSettings(
themePreference: ThemePreference,
onPreferenceChanged: (ThemePreference) -> Unit,
+ tabsSelection: List>,
+ onTabsSelectChange: (Screens, Boolean) -> Unit,
+ onTabsOrderChanged: (fromIdx: Int, toIdx: Int) -> Unit,
+ onTabsOrderConfirmed: () -> Unit,
) {
- val spacerModifier = Modifier.height(10.dp)
- OutlinedBox(
- label = "Look and feel",
- contentPadding = PaddingValues(vertical = 13.dp, horizontal = 20.dp),
- modifier = Modifier.padding(10.dp)
- ) {
- Column(Modifier.fillMaxWidth()) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "Material You",
- style = MaterialTheme.typography.titleMedium
- )
- Switch(checked = themePreference.useMaterialYou, onCheckedChange = {
- onPreferenceChanged(themePreference.copy(useMaterialYou = it))
- })
- }
- Spacer(spacerModifier)
- }
- val seedColor by remember(themePreference.accent){
- derivedStateOf {
- themePreference.accent.getSeedColor()
+ val seedColor by remember(themePreference.accent){
+ derivedStateOf {
+ themePreference.accent.getSeedColor()
+ }
+ }
+ var showAccentSelector by remember { mutableStateOf(false) }
+ var showSelectorDialog by remember { mutableStateOf(false) }
+ val isSystemInDarkMode = isSystemInDarkTheme()
+ val icon by remember(themePreference.theme) { derivedStateOf {
+ when (themePreference.theme) {
+ UserPreferences.Theme.LIGHT_MODE, UserPreferences.Theme.UNRECOGNIZED -> R.drawable.baseline_light_mode_40
+ UserPreferences.Theme.DARK_MODE -> R.drawable.baseline_dark_mode_40
+ UserPreferences.Theme.USE_SYSTEM_MODE -> {
+ if (isSystemInDarkMode){
+ R.drawable.baseline_dark_mode_40
+ } else {
+ R.drawable.baseline_light_mode_40
}
}
- var showAccentSelector by remember { mutableStateOf(false) }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .alpha(if (themePreference.useMaterialYou) 0.5f else 1f),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = "Accent color",
- style = MaterialTheme.typography.titleMedium
- )
- Canvas(
- modifier = Modifier
- .size(50.dp)
- .clickable(
- enabled = (!themePreference.useMaterialYou),
- onClick = { showAccentSelector = true }
- )
- ){
- drawCircle(color = seedColor)
+ }
+ } }
+ var showRearrangeTabsDialog by remember{ mutableStateOf(false) }
+
+ Column(
+ modifier = Modifier.group()
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ Setting(
+ title = "Material You",
+ icon = R.drawable.baseline_palette_40,
+ description = "Use a theme generated from your device wallpaper",
+ isChecked = themePreference.useMaterialYou,
+ onCheckedChanged = {
+ onPreferenceChanged(themePreference.copy(useMaterialYou = it))
+ },
+ )
+ }
+ AccentSetting(
+ onClick = {
+ if (!themePreference.useMaterialYou) {
+ showAccentSelector = true
}
+ },
+ seedColor = seedColor,
+ modifier = Modifier
+ .alpha(if (themePreference.useMaterialYou) 0.5f else 1f),
+ )
+ Setting(
+ title = "Theme mode",
+ icon = icon,
+ onClick = { showSelectorDialog = true },
+ description = "Choose a theme mode"
+ )
+ Setting(
+ title = "Tabs arrangement",
+ icon = R.drawable.ic_baseline_library_music_40,
+ description = "Select and reorder the tabs shown",
+ onClick = { showRearrangeTabsDialog = true }
+ )
+ }
+ if (showAccentSelector){
+ AccentSelectorDialog(
+ themePreference = themePreference,
+ onPreferenceChanged = onPreferenceChanged,
+ onDismissRequest = { showAccentSelector = false }
+ )
+ }
+ if (showSelectorDialog) {
+ ThemeSelectorDialog(
+ themePreference = themePreference,
+ onPreferenceChanged = onPreferenceChanged,
+ onDismissRequest = { showSelectorDialog = false }
+ )
+ }
+ if (showRearrangeTabsDialog){
+ RearrangeTabsDialog(
+ tabsSelection = tabsSelection,
+ onDismissRequest = { showRearrangeTabsDialog = false },
+ onSelectChange = onTabsSelectChange,
+ onTabsOrderChanged = onTabsOrderChanged,
+ onTabsOrderConfirmed = {
+ showRearrangeTabsDialog = false
+ onTabsOrderConfirmed()
}
- if (showAccentSelector){
- AccentSelectorDialog(
- themePreference = themePreference,
- onPreferenceChanged = onPreferenceChanged,
- onDismissRequest = { showAccentSelector = false }
- )
- }
- Spacer(spacerModifier)
- var showSelectorDialog by remember { mutableStateOf(false) }
- val buttonText = when (themePreference.theme) {
- UserPreferences.Theme.LIGHT_MODE, UserPreferences.Theme.UNRECOGNIZED -> "Light"
- UserPreferences.Theme.DARK_MODE -> "Dark"
- UserPreferences.Theme.USE_SYSTEM_MODE -> "System"
- }
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "App theme",
- style = MaterialTheme.typography.titleMedium
- )
- Button(
- onClick = { showSelectorDialog = true },
- content = {
- Text(
- text = buttonText,
- style = MaterialTheme.typography.titleMedium
- )
- }
- )
- }
- if (showSelectorDialog) {
- ThemeSelectorDialog(
- themePreference = themePreference,
- onPreferenceChanged = onPreferenceChanged,
- onDismissRequest = { showSelectorDialog = false }
- )
- }
- }
+ )
}
}
@@ -189,6 +233,7 @@ private fun AccentSelectorDialog(
onPreferenceChanged: (ThemePreference) -> Unit,
onDismissRequest: () -> Unit,
){
+ val sizeModifier = Modifier.size(50.dp)
AlertDialog(
title = {
Text(
@@ -218,19 +263,28 @@ private fun AccentSelectorDialog(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
- Canvas(Modifier.size(50.dp).clickable {
- onPreferenceChanged(themePreference.copy(accent = Accent.Default))
- }) {
+ Canvas(
+ modifier = sizeModifier
+ .clickable {
+ onPreferenceChanged(themePreference.copy(accent = Accent.Default))
+ }
+ ) {
drawCircle(Accent.Default.getSeedColor())
}
- Canvas(Modifier.size(50.dp).clickable {
- onPreferenceChanged(themePreference.copy(accent = Accent.Malibu))
- }) {
+ Canvas(
+ modifier = sizeModifier
+ .clickable {
+ onPreferenceChanged(themePreference.copy(accent = Accent.Malibu))
+ }
+ ) {
drawCircle(Accent.Malibu.getSeedColor())
}
- Canvas(Modifier.size(50.dp).clickable {
- onPreferenceChanged(themePreference.copy(accent = Accent.Melrose))
- }) {
+ Canvas(
+ modifier = sizeModifier
+ .clickable {
+ onPreferenceChanged(themePreference.copy(accent = Accent.Melrose))
+ }
+ ) {
drawCircle(Accent.Melrose.getSeedColor())
}
}
@@ -238,19 +292,28 @@ private fun AccentSelectorDialog(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
- Canvas(Modifier.size(50.dp).clickable {
- onPreferenceChanged(themePreference.copy(accent = Accent.Elm))
- }) {
+ Canvas(
+ modifier = sizeModifier
+ .clickable {
+ onPreferenceChanged(themePreference.copy(accent = Accent.Elm))
+ }
+ ) {
drawCircle(Accent.Elm.getSeedColor())
}
- Canvas(Modifier.size(50.dp).clickable {
- onPreferenceChanged(themePreference.copy(accent = Accent.Magenta))
- }) {
+ Canvas(
+ modifier = sizeModifier
+ .clickable {
+ onPreferenceChanged(themePreference.copy(accent = Accent.Magenta))
+ }
+ ) {
drawCircle(Accent.Magenta.getSeedColor())
}
- Canvas(Modifier.size(50.dp).clickable {
- onPreferenceChanged(themePreference.copy(accent = Accent.JacksonsPurple))
- }) {
+ Canvas(
+ modifier = sizeModifier
+ .clickable {
+ onPreferenceChanged(themePreference.copy(accent = Accent.JacksonsPurple))
+ }
+ ) {
drawCircle(Accent.JacksonsPurple.getSeedColor())
}
}
@@ -344,84 +407,164 @@ private fun ThemeSelectorDialog(
)
}
+@OptIn(ExperimentalFoundationApi::class)
@Composable
-private fun MusicLibrarySettings(
- scanStatus: ScanStatus,
- onScanClicked: () -> Unit,
- onRestoreClicked: () -> Unit,
-) {
- OutlinedBox(
- label = "Music library",
- contentPadding = PaddingValues(horizontal = 20.dp, vertical = 13.dp),
- modifier = Modifier.padding(10.dp)
- ) {
- Column(Modifier.fillMaxWidth()) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "Rescan for music",
- style = MaterialTheme.typography.titleMedium,
- )
- Button(
- onClick = {
- if (scanStatus is ScanStatus.ScanNotRunning){
- onScanClicked()
- }
- },
- content = {
- when (scanStatus) {
- is ScanStatus.ScanNotRunning -> {
- Text(
- text = "Scan",
- style = MaterialTheme.typography.bodyLarge,
- )
- }
- is ScanStatus.ScanComplete -> {
- Text(
- text = "Done",
- style = MaterialTheme.typography.bodyLarge
- )
- }
- is ScanStatus.ScanProgress -> {
- var totalSongs by remember { mutableStateOf(0) }
- var scanProgress by remember { mutableStateOf(0f) }
- scanProgress = (scanStatus.parsed.toFloat()) / (scanStatus.total.toFloat())
- totalSongs = scanStatus.total
- CircularProgressIndicator(
- progress = scanProgress,
- color = MaterialTheme.colorScheme.onPrimary,
- )
- }
- else -> {}
- }
- }
- )
- }
- Spacer(Modifier.height(10.dp))
- Row(
+private fun RearrangeTabsDialog(
+ tabsSelection: List>,
+ onDismissRequest: () -> Unit,
+ onSelectChange: (Screens, Boolean) -> Unit,
+ onTabsOrderChanged: (fromIdx: Int, toIdx: Int) -> Unit,
+ onTabsOrderConfirmed: () -> Unit,
+){
+ AlertDialog(
+ title = {
+ Text(
+ text = "App tabs",
+ style = MaterialTheme.typography.titleLarge,
modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
+ textAlign = TextAlign.Center,
+ )
+ },
+ onDismissRequest = onDismissRequest,
+ confirmButton = {
+ Button(
+ onClick = onTabsOrderConfirmed
) {
Text(
- text = "Restore blacklisted songs",
+ text = "OK",
style = MaterialTheme.typography.titleMedium
)
- Button(
- onClick = onRestoreClicked,
- content = {
- Text(
- text = "Restore",
- style = MaterialTheme.typography.titleMedium
+ }
+ },
+ text = {
+ val listState = rememberLazyListState()
+ val dragDropState = rememberDragDropState(
+ lazyListState = listState,
+ onMove = onTabsOrderChanged,
+ )
+ LazyColumn(
+ state = listState,
+ verticalArrangement = Arrangement.spacedBy(6.dp),
+ modifier = Modifier
+ .dragContainer(dragDropState)
+ ){
+ itemsIndexed(
+ items = tabsSelection,
+ key = { index, screenChoice -> screenChoice.first.ordinal }
+ ){ index, screenChoice ->
+ DraggableItem(dragDropState, index) {
+ SelectableMovableScreen(
+ screen = screenChoice.first,
+ isSelected = screenChoice.second,
+ onSelectChange = { isSelected -> onSelectChange(screenChoice.first, isSelected) },
)
}
- )
+ }
}
- }
+ },
+
+ )
+}
+
+
+@Composable
+private fun SelectableMovableScreen(
+ screen: Screens,
+ isSelected: Boolean,
+ onSelectChange: (Boolean) -> Unit,
+){
+ val spaceModifier = Modifier.width(12.dp)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .background(MaterialTheme.colorScheme.secondaryContainer),
+ verticalAlignment = Alignment.CenterVertically,
+ ){
+ Checkbox(
+ checked = isSelected,
+ onCheckedChange = onSelectChange,
+ )
+ Icon(
+ painter = painterResource(id = screen.filledIcon),
+ contentDescription = "${screen.name} screen icon",
+ modifier = Modifier.size(35.dp),
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Spacer(spaceModifier)
+ Text(
+ text = screen.name,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSecondaryContainer,
+ modifier = Modifier.weight(1f)
+ )
+ Spacer(spaceModifier)
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_drag_indicator_40),
+ contentDescription = "move icon",
+ modifier = Modifier.size(32.dp),
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+}
+@Composable
+private fun MusicLibrarySettings(
+ scanStatus: ScanStatus,
+ onScanClicked: () -> Unit,
+ onRestoreClicked: () -> Unit,
+ onRestoreFoldersClicked: () -> Unit,
+) {
+ var progress by remember { mutableStateOf(0f) }
+ LaunchedEffect(key1 = scanStatus){
+ if (scanStatus is ScanStatus.ScanComplete) {
+ progress = 1f
+ } else if (scanStatus is ScanStatus.ScanProgress){
+ progress = if (scanStatus.total == 0){
+ 1f
+ } else {
+ scanStatus.parsed.toFloat()/scanStatus.total.toFloat()
+ }
+ }
+ }
+ Column(
+ modifier = Modifier
+ .group()
+ .animateContentSize(),
+ ) {
+ Setting(
+ title = "Rescan for music",
+ icon = Icons.Outlined.Search,
+ description = "Search for all the songs on this device and update the library",
+ onClick = {
+ if (scanStatus is ScanStatus.ScanNotRunning){
+ onScanClicked()
+ }
+ },
+ )
+ if (scanStatus is ScanStatus.ScanProgress){
+ LinearProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ progress = progress
+ )
+ } else if (scanStatus is ScanStatus.ScanComplete){
+ Text(
+ text = "Done",
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ Setting(
+ title = "Restore blacklisted songs",
+ icon = R.drawable.baseline_settings_backup_restore_40,
+ onClick = onRestoreClicked
+ )
+ Setting(
+ title = "Restore blacklisted folders",
+ icon = R.drawable.baseline_settings_backup_restore_40,
+ onClick = onRestoreFoldersClicked
+ )
}
}
@@ -440,63 +583,38 @@ private fun ReportBug(
onAutoReportCrashClicked: (Boolean) -> Unit,
){
val context = LocalContext.current
- OutlinedBox(
- label = "Report",
- contentPadding = PaddingValues(horizontal = 20.dp, vertical = 13.dp),
- modifier = Modifier.padding(10.dp)
+ Column(
+ modifier = Modifier.group()
) {
- Column(Modifier.fillMaxWidth()) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "Auto crash reporting",
- style = MaterialTheme.typography.titleMedium,
- )
- Switch(
- checked = !disabledCrashlytics,
- onCheckedChange = onAutoReportCrashClicked
- )
- }
- Spacer(Modifier.height(10.dp))
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "Report any bugs/crashes",
- style = MaterialTheme.typography.titleMedium
- )
- Button(
- onClick = {
- val intent = Intent(Intent.ACTION_SENDTO)
- intent.apply {
- putExtra(Intent.EXTRA_EMAIL, arrayOf("music.zen@outlook.com"))
- putExtra(Intent.EXTRA_SUBJECT, "Zen Music | Bug Report")
- putExtra(
- Intent.EXTRA_TEXT,
- getSystemDetail() + "\n\n[Describe the bug or crash here]"
- )
- data = Uri.parse("mailto:")
- }
- try {
- context.startActivity(intent)
- } catch (e: Exception) {
- Timber.e(e)
- }
- },
- content = {
- Text(
- text = "Report",
- style = MaterialTheme.typography.titleMedium
- )
- }
- )
+ Setting(
+ title = "Auto crash reporting",
+ icon = R.drawable.baseline_send_40,
+ description = "Enable this to automatically send crash reports to the developer",
+ isChecked = !disabledCrashlytics,
+ onCheckedChanged = onAutoReportCrashClicked,
+ )
+ Setting(
+ title = "Report any bugs/crashes",
+ icon = R.drawable.baseline_bug_report_40,
+ description = "Manually report any bugs or crashes you faced",
+ onClick = {
+ val intent = Intent(Intent.ACTION_SENDTO)
+ intent.apply {
+ putExtra(Intent.EXTRA_EMAIL, arrayOf("music.zen@outlook.com"))
+ putExtra(Intent.EXTRA_SUBJECT, "Zen Music | Bug Report")
+ putExtra(
+ Intent.EXTRA_TEXT,
+ getSystemDetail() + "\n\n[Describe the bug or crash here]"
+ )
+ data = Uri.parse("mailto:")
+ }
+ try {
+ context.startActivity(intent)
+ } catch (e: Exception) {
+ Timber.e(e)
+ }
}
- }
+ )
}
}
@@ -606,4 +724,175 @@ private fun MadeBy(
}
Spacer(Modifier.height(36.dp))
}
+}
+
+private fun Modifier.group() = composed {
+ this
+ .fillMaxWidth()
+ .padding(8.dp)
+ .clip(MaterialTheme.shapes.large)
+ .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
+}
+
+
+@Composable
+private fun Setting(
+ title: String,
+ icon: ImageVector,
+ modifier: Modifier = Modifier,
+ description: String? = null,
+ onClick: (() -> Unit)? = null,
+ isChecked: Boolean? = null,
+ onCheckedChanged: ((Boolean) -> Unit)? = null,
+){
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .then(
+ if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier
+ )
+ .padding(10.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .padding(6.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ ){
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ )
+ description?.let {
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
+ )
+ }
+ }
+ isChecked?.let {
+ Switch(
+ checked = isChecked,
+ onCheckedChange = onCheckedChanged,
+ )
+ }
+ }
+}
+
+
+@Composable
+private fun Setting(
+ title: String,
+ @DrawableRes icon: Int,
+ modifier: Modifier = Modifier,
+ description: String? = null,
+ onClick: (() -> Unit)? = null,
+ isChecked: Boolean? = null,
+ onCheckedChanged: ((Boolean) -> Unit)? = null,
+){
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .then(
+ if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier
+ )
+ .padding(10.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(id = icon),
+ contentDescription = null,
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .padding(6.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ ){
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ )
+ description?.let {
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
+ )
+ }
+ }
+ isChecked?.let {
+ Switch(
+ checked = isChecked,
+ onCheckedChange = onCheckedChanged,
+ )
+ }
+ }
+}
+
+@Composable
+private fun AccentSetting(
+ onClick: () -> Unit,
+ seedColor: Color,
+ modifier: Modifier = Modifier,
+){
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(10.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(id = R.drawable.baseline_colorize_40),
+ contentDescription = null,
+ modifier = Modifier
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primaryContainer)
+ .padding(6.dp),
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ ){
+ Text(
+ text = "Accent color",
+ style = MaterialTheme.typography.titleMedium,
+ )
+ Text(
+ text = "Choose a theme color",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
+ )
+ }
+ Box(
+ modifier = Modifier
+ .height(32.dp)
+ .width(52.dp)
+ .clip(RoundedCornerShape(16.dp))
+ .background(color = seedColor)
+ )
+ }
}
\ No newline at end of file
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 a85fa9d..3c80155 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
@@ -1,20 +1,27 @@
package com.github.pakka_papad.settings
import android.app.Application
+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.notification.ZenNotificationManager
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.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val context: Application,
private val manager: DataManager,
+ private val prefs: ZenPreferenceProvider
) : ViewModel() {
val scanStatus = manager.scanStatus
@@ -31,4 +38,63 @@ class SettingsViewModel @Inject constructor(
manager.scanForMusic()
}
+ private val _tabsSelection = MutableStateFlow>>(listOf())
+ val tabsSelection = _tabsSelection.asStateFlow()
+
+ init {
+ val selectedScreens = prefs.selectedTabs.value ?: listOf()
+ val allScreens = Screens.values()
+ val currentSelection = arrayListOf>()
+ selectedScreens.forEach {
+ try {
+ currentSelection += Pair(allScreens[it],true)
+ } catch (_: Exception){
+
+ }
+ }
+ allScreens.forEach {
+ if (!selectedScreens.contains(it.ordinal)){
+ currentSelection += Pair(it,false)
+ }
+ }
+ _tabsSelection.update { currentSelection.toList() }
+ }
+
+ fun onTabsSelectChanged(screen: Screens, isSelected: Boolean){
+ viewModelScope.launch {
+ val newSelection = _tabsSelection.value.map {
+ if (it.first.ordinal == screen.ordinal){
+ Pair(it.first, isSelected)
+ } else {
+ it
+ }
+ }
+ _tabsSelection.update { newSelection }
+ }
+ }
+
+ fun onTabsOrderChanged(fromIndex: Int, toIndex: Int){
+ viewModelScope.launch {
+ val newOrder = _tabsSelection.value.toMutableList().apply {
+ add(toIndex, removeAt(fromIndex))
+ }.toList()
+ _tabsSelection.update { newOrder }
+ }
+ }
+
+ fun saveTabsOrder() {
+ viewModelScope.launch {
+ val order = _tabsSelection.value.filter { it.second }.map { it.first.ordinal }
+ if (order.isEmpty()){
+ Toast.makeText(context, "Minimum one tab selection is required", Toast.LENGTH_SHORT).show()
+ } else if (order.size > 5){
+ Toast.makeText(context, "Maximum of five tab selections are allowed", Toast.LENGTH_SHORT).show()
+ } else if(!order.contains(Screens.Songs.ordinal)) {
+ Toast.makeText(context, "Songs tab cannot be removed", Toast.LENGTH_SHORT).show()
+ } else {
+ prefs.updateSelectedTabs(order)
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/storage_explorer/Directory.kt b/app/src/main/java/com/github/pakka_papad/storage_explorer/Directory.kt
new file mode 100644
index 0000000..eeb12d7
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/storage_explorer/Directory.kt
@@ -0,0 +1,6 @@
+package com.github.pakka_papad.storage_explorer
+
+data class Directory(
+ val name: String,
+ val absolutePath: String,
+)
diff --git a/app/src/main/java/com/github/pakka_papad/storage_explorer/DirectoryContents.kt b/app/src/main/java/com/github/pakka_papad/storage_explorer/DirectoryContents.kt
new file mode 100644
index 0000000..9066b12
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/storage_explorer/DirectoryContents.kt
@@ -0,0 +1,8 @@
+package com.github.pakka_papad.storage_explorer
+
+import com.github.pakka_papad.data.music.MiniSong
+
+data class DirectoryContents(
+ val directories: List = listOf(),
+ val songs: List = 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
new file mode 100644
index 0000000..a4d71cb
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/storage_explorer/FileExplorer.kt
@@ -0,0 +1,73 @@
+package com.github.pakka_papad.storage_explorer
+
+import android.os.Environment
+import com.github.pakka_papad.data.music.SongExtractor
+import java.io.File
+import java.io.FileFilter
+
+class MusicFileExplorer(
+ private val songExtractor: SongExtractor
+) {
+
+ val root = Environment.getExternalStorageDirectory().absolutePath
+ var currentPath = root
+
+ val isRoot: Boolean
+ get() {
+ return root == currentPath
+ }
+
+ private val filterDirectories = object : FileFilter {
+ override fun accept(pathname: File?): Boolean {
+ if (pathname == null || !pathname.exists()) return false
+ return pathname.isDirectory
+ }
+ }
+
+ private val listeners = arrayListOf()
+
+ fun addListener(listener: DirectoryChangeListener){
+ listeners.add(listener)
+ val directory = File(currentPath)
+ if (!directory.exists() || !directory.isDirectory) return
+ val directories = (directory.listFiles(filterDirectories) ?: arrayOf()).map {
+ Directory(name = it.name, absolutePath = it.absolutePath)
+ }
+ val songs = songExtractor.extractMini(currentPath)
+ listener.onDirectoryChanged(currentPath,DirectoryContents(directories,songs))
+ }
+
+ fun removeListener(listener: DirectoryChangeListener){
+ listeners.remove(listener)
+ }
+
+ fun moveInsideDirectory(directoryPath: String){
+ currentPath = directoryPath
+ val directory = File(currentPath)
+ if(!directory.exists() || !directory.isDirectory) return
+ val directories = (directory.listFiles(filterDirectories) ?: arrayOf()).map {
+ Directory(name = it.name, absolutePath = it.absolutePath)
+ }
+ val songs = songExtractor.extractMini(currentPath)
+ listeners.forEach { it.onDirectoryChanged(currentPath, DirectoryContents(directories,songs)) }
+ }
+
+ fun moveToParent(){
+ if (currentPath == root) return
+ var directory = File(currentPath)
+ directory = directory.parentFile ?: return
+ if (!directory.exists() || !directory.isDirectory) return
+ currentPath = directory.absolutePath
+ val directories = (directory.listFiles(filterDirectories) ?: arrayOf()).map {
+ Directory(name = it.name, absolutePath = it.absolutePath)
+ }
+ val songs = songExtractor.extractMini(currentPath)
+ listeners.forEach { it.onDirectoryChanged(currentPath, DirectoryContents(directories,songs)) }
+ }
+
+
+ interface DirectoryChangeListener {
+ fun onDirectoryChanged(path: String, files: DirectoryContents)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/widgets/MusicControlWidget.kt b/app/src/main/java/com/github/pakka_papad/widgets/MusicControlWidget.kt
new file mode 100644
index 0000000..db1c959
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/widgets/MusicControlWidget.kt
@@ -0,0 +1,240 @@
+package com.github.pakka_papad.widgets
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetManager
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.ImageProvider as UriImageProvider
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.provideContent
+import androidx.glance.appwidget.state.updateAppWidgetState
+import androidx.glance.background
+import androidx.glance.currentState
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Column
+import androidx.glance.layout.ContentScale
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxHeight
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.padding
+import androidx.glance.layout.width
+import androidx.glance.layout.wrapContentWidth
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import com.github.pakka_papad.Constants
+import com.github.pakka_papad.R
+import com.github.pakka_papad.player.ZenBroadcastReceiver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+
+val imageUriKey = stringPreferencesKey("image_uri")
+val albumKey = stringPreferencesKey("album")
+val titleKey = stringPreferencesKey("title")
+val artistKey = stringPreferencesKey("artist")
+val isPlayingKey = booleanPreferencesKey("is_playing")
+
+object MusicControlWidget : GlanceAppWidget() {
+
+ private lateinit var pendingPausePlayIntent: PendingIntent
+ private lateinit var pendingPreviousIntent: PendingIntent
+ private lateinit var pendingNextIntent: PendingIntent
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ pendingPausePlayIntent = PendingIntent.getBroadcast(
+ context, ZenBroadcastReceiver.PAUSE_PLAY_ACTION_REQUEST_CODE,
+ Intent(Constants.PACKAGE_NAME).putExtra(
+ ZenBroadcastReceiver.AUDIO_CONTROL,
+ ZenBroadcastReceiver.ZEN_PLAYER_PAUSE_PLAY
+ ),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ pendingPreviousIntent = PendingIntent.getBroadcast(
+ context, ZenBroadcastReceiver.PREVIOUS_ACTION_REQUEST_CODE,
+ Intent(Constants.PACKAGE_NAME).putExtra(
+ ZenBroadcastReceiver.AUDIO_CONTROL,
+ ZenBroadcastReceiver.ZEN_PLAYER_PREVIOUS
+ ),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ pendingNextIntent = PendingIntent.getBroadcast(
+ context, ZenBroadcastReceiver.NEXT_ACTION_REQUEST_CODE,
+ Intent(Constants.PACKAGE_NAME).putExtra(
+ ZenBroadcastReceiver.AUDIO_CONTROL,
+ ZenBroadcastReceiver.ZEN_PLAYER_NEXT
+ ),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ provideContent {
+ GlanceTheme {
+ WidgetContent()
+ }
+ }
+ }
+
+ @Composable
+ private fun WidgetContent() {
+ val imageUri = currentState(imageUriKey) ?: ""
+ val title = currentState(titleKey) ?: ""
+ val album = currentState(albumKey) ?: ""
+ val artist = currentState(artistKey) ?: ""
+ val isPlaying = currentState(isPlayingKey) ?: false
+ Row(
+ modifier = GlanceModifier
+ .fillMaxSize()
+ .then(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
+ GlanceModifier
+ .cornerRadius(28.dp)
+ .background(GlanceTheme.colors.secondaryContainer)
+ .padding(12.dp)
+ } else {
+ GlanceModifier
+ .background(ImageProvider(R.drawable.music_widget_background))
+ }
+ ),
+ ) {
+ Image(
+ provider = UriImageProvider(imageUri.toUri()),
+ contentDescription = null,
+ modifier = GlanceModifier
+ .wrapContentWidth()
+ .fillMaxHeight()
+ .then(
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ GlanceModifier
+ .cornerRadius(16.dp)
+ } else {
+ GlanceModifier
+ }
+ ) ,
+ contentScale = ContentScale.Fit
+ )
+ Spacer(GlanceModifier.width(12.dp))
+ Column(
+ modifier = GlanceModifier
+ .fillMaxSize()
+ ) {
+ Text(
+ text = title,
+ style = TextStyle(
+ color = GlanceTheme.colors.onSecondaryContainer,
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold
+ ),
+ maxLines = 1,
+ )
+ Text(
+ text = artist,
+ style = TextStyle(
+ color = GlanceTheme.colors.onSecondaryContainer,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Normal
+ ),
+ maxLines = 1,
+ )
+ Row(
+ modifier = GlanceModifier
+ .fillMaxSize(),
+ verticalAlignment = Alignment.Bottom,
+ horizontalAlignment = Alignment.Horizontal.CenterHorizontally
+ ) {
+ Image(
+ provider = ImageProvider(R.drawable.ic_baseline_skip_previous_40),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(GlanceTheme.colors.onSecondaryContainer),
+ modifier = GlanceModifier
+ .clickable {
+ pendingPreviousIntent.send()
+ }
+ )
+ Spacer(GlanceModifier.width(6.dp))
+ Image(
+ provider = ImageProvider(
+ if (isPlaying) {
+ R.drawable.ic_baseline_pause_40
+ } else {
+ R.drawable.ic_baseline_play_arrow_40
+ }
+ ),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(GlanceTheme.colors.onSecondaryContainer),
+ modifier = GlanceModifier
+ .clickable {
+ pendingPausePlayIntent.send()
+ }
+ )
+ Spacer(GlanceModifier.width(6.dp))
+ Image(
+ provider = ImageProvider(R.drawable.ic_baseline_skip_next_40),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(GlanceTheme.colors.onSecondaryContainer),
+ modifier = GlanceModifier
+ .clickable {
+ pendingNextIntent.send()
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+class MusicControlWidgetReceiver : GlanceAppWidgetReceiver() {
+
+ override val glanceAppWidget: GlanceAppWidget
+ get() = MusicControlWidget
+
+ private val scope = CoroutineScope(SupervisorJob())
+
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ val action = intent.extras?.getString(WidgetBroadcast.WIDGET_BROADCAST) ?: return
+ scope.launch {
+ GlanceAppWidgetManager(context)
+ .getGlanceIds(MusicControlWidget.javaClass)
+ .forEach { glanceId ->
+ when (action) {
+ WidgetBroadcast.SONG_CHANGED -> {
+ updateAppWidgetState(context, glanceId) { prefs ->
+ prefs[imageUriKey] = intent.getStringExtra("imageUri") ?: ""
+ prefs[albumKey] = intent.getStringExtra("album") ?: ""
+ prefs[titleKey] = intent.getStringExtra("title") ?: ""
+ prefs[artistKey] = intent.getStringExtra("artist") ?: ""
+ }
+ }
+
+ WidgetBroadcast.IS_PLAYING_CHANGED -> {
+ updateAppWidgetState(context, glanceId) { prefs ->
+ prefs[isPlayingKey] = intent.getBooleanExtra("isPlaying", false)
+ }
+ }
+
+ else -> {
+
+ }
+ }
+ glanceAppWidget.update(context, glanceId)
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/pakka_papad/widgets/WidgetBroadcast.kt b/app/src/main/java/com/github/pakka_papad/widgets/WidgetBroadcast.kt
new file mode 100644
index 0000000..2e1fbec
--- /dev/null
+++ b/app/src/main/java/com/github/pakka_papad/widgets/WidgetBroadcast.kt
@@ -0,0 +1,8 @@
+package com.github.pakka_papad.widgets
+
+object WidgetBroadcast {
+ const val WIDGET_BROADCAST = "widget_broadcast"
+
+ const val SONG_CHANGED = "song_changed"
+ const val IS_PLAYING_CHANGED = "is_playing_changed"
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/github/pakka_papad/data/UserPreferences.proto b/app/src/main/proto/com/github/pakka_papad/data/UserPreferences.proto
index 16b72f9..7d4871f 100644
--- a/app/src/main/proto/com/github/pakka_papad/data/UserPreferences.proto
+++ b/app/src/main/proto/com/github/pakka_papad/data/UserPreferences.proto
@@ -27,4 +27,6 @@ message UserPreferences {
int32 playbackPitch = 2;
}
PlaybackParams playbackParams = 6;
+ repeated int32 selectedTabs = 7;
+ map chosenSortOrder = 8;
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/baseline_bug_report_40.xml b/app/src/main/res/drawable/baseline_bug_report_40.xml
new file mode 100644
index 0000000..5e89aee
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_bug_report_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_colorize_40.xml b/app/src/main/res/drawable/baseline_colorize_40.xml
new file mode 100644
index 0000000..a37a6a2
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_colorize_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_dark_mode_40.xml b/app/src/main/res/drawable/baseline_dark_mode_40.xml
new file mode 100644
index 0000000..cd27819
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_dark_mode_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_drag_indicator_40.xml b/app/src/main/res/drawable/baseline_drag_indicator_40.xml
new file mode 100644
index 0000000..63f57ba
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_drag_indicator_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_light_mode_40.xml b/app/src/main/res/drawable/baseline_light_mode_40.xml
new file mode 100644
index 0000000..72cae6d
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_light_mode_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_palette_40.xml b/app/src/main/res/drawable/baseline_palette_40.xml
new file mode 100644
index 0000000..051b536
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_palette_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_send_40.xml b/app/src/main/res/drawable/baseline_send_40.xml
new file mode 100644
index 0000000..83d7f27
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_send_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_settings_backup_restore_40.xml b/app/src/main/res/drawable/baseline_settings_backup_restore_40.xml
new file mode 100644
index 0000000..a8dc5f7
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_settings_backup_restore_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_folder_40.xml b/app/src/main/res/drawable/ic_baseline_folder_40.xml
new file mode 100644
index 0000000..6df8b5f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_folder_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_folder_40.xml b/app/src/main/res/drawable/ic_outline_folder_40.xml
new file mode 100644
index 0000000..048730e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_folder_40.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/music_widget_background.xml b/app/src/main/res/drawable/music_widget_background.xml
new file mode 100644
index 0000000..ba65062
--- /dev/null
+++ b/app/src/main/res/drawable/music_widget_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/music_control_widget_preview_layout.xml b/app/src/main/res/layout/music_control_widget_preview_layout.xml
new file mode 100644
index 0000000..9dc447f
--- /dev/null
+++ b/app/src/main/res/layout/music_control_widget_preview_layout.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/app_nav.xml b/app/src/main/res/navigation/app_nav.xml
index d3b1ba3..b446a28 100644
--- a/app/src/main/res/navigation/app_nav.xml
+++ b/app/src/main/res/navigation/app_nav.xml
@@ -73,6 +73,13 @@
app:exitAnim="@anim/zen_open_exit"
app:popEnterAnim="@anim/zen_close_enter"
app:popExitAnim="@anim/zen_close_exit" />
+
+
\ 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 4dc57f0..d720981 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
Zen Music
+ Music Control Widget
\ No newline at end of file
diff --git a/app/src/main/res/xml/music_control_widget_info.xml b/app/src/main/res/xml/music_control_widget_info.xml
new file mode 100644
index 0000000..e305645
--- /dev/null
+++ b/app/src/main/res/xml/music_control_widget_info.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties b/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties
deleted file mode 100644
index 1a49aa1..0000000
--- a/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties
+++ /dev/null
@@ -1 +0,0 @@
-implementation-classpath=D\:/Zen/Zen/buildSrc/build/classes/java/main;D\:/Zen/Zen/buildSrc/build/classes/groovy/main;D\:/Zen/Zen/buildSrc/build/classes/kotlin/main;D\:/Zen/Zen/buildSrc/build/resources/main
diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
index 945788a..b49c81f 100644
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -13,6 +13,7 @@ object Versions {
const val androidxEspresso = "3.4.0"
const val androidxComposeConstraintLayout = "1.0.1"
const val androidxSplashScreen = "1.0.0-beta02"
+ const val androidxGlance = "1.0.0-beta01"
const val appCompat = "1.4.2"
const val navigation = "2.5.3"
@@ -25,7 +26,7 @@ object Versions {
const val googleServices = "4.3.15"
const val crashlyticsGradlePlugin = "2.9.4"
- const val material3 = "1.0.1"
+ const val material3 = "1.1.2"
const val accompanist = "0.28.0"
const val junit = "4.13.2"
@@ -67,6 +68,10 @@ object Libraries {
const val androidxComposeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:${Versions.androidxComposeUi}"
const val androidxComposeConstraintLayout = "androidx.constraintlayout:constraintlayout-compose:${Versions.androidxComposeConstraintLayout}"
const val androidxSplashScreen = "androidx.core:core-splashscreen:${Versions.androidxSplashScreen}"
+ const val androidxGlance = "androidx.glance:glance:${Versions.androidxGlance}"
+ const val androidxGlanceAppWidget = "androidx.glance:glance-appwidget:${Versions.androidxGlance}"
+ const val androidxGlanceMaterial = "androidx.glance:glance-material:${Versions.androidxGlance}"
+ const val androidxGlanceMaterial3 = "androidx.glance:glance-material3:${Versions.androidxGlance}"
const val roomRuntime = "androidx.room:room-runtime:${Versions.room}"
const val roomKtx = "androidx.room:room-ktx:${Versions.room}"
@@ -122,7 +127,10 @@ object AnnotationProcessors {
}
object AppVersion {
- const val Code = 2
- const val Name = "1.1"
+ private const val Major = 1
+ private const val Minor = 2
+ private const val Patch = 0
+ const val Code = Major*10000 + Minor*100 + Patch
+ const val Name = "$Major.$Minor.$Patch"
}