From 5f1e1d2310c10569ae8de6ef304fb952cf624182 Mon Sep 17 00:00:00 2001 From: ztftrue Date: Wed, 3 Apr 2024 19:54:25 +0800 Subject: [PATCH] add: use folder for lyrics --- app/build.gradle.kts | 2 +- .../2.json | 30 +- .../3.json | 370 ++++++++++++++++++ .../java/com/ztftrue/music/MigrationTest.kt | 49 +++ .../java/com/ztftrue/music/MainActivity.kt | 25 ++ .../java/com/ztftrue/music/MusicViewModel.kt | 158 +++++--- .../ztftrue/music/sqlData/MusicDatabase.kt | 52 ++- .../music/sqlData/dao/StorageFolderDao.kt | 30 ++ .../music/sqlData/model/StorageFolder.kt | 33 ++ .../ztftrue/music/ui/other/SettingsPage.kt | 10 +- .../com/ztftrue/music/ui/play/LyricsView.kt | 94 +++-- .../com/ztftrue/music/ui/play/PlayingPage.kt | 9 +- .../com/ztftrue/music/utils/CaptionUtils.kt | 92 ++++- .../java/com/ztftrue/music/utils/Utils.kt | 5 +- 14 files changed, 816 insertions(+), 143 deletions(-) create mode 100644 app/schemas/com.ztftrue.music.sqlData.MusicDatabase/3.json create mode 100644 app/src/androidTest/java/com/ztftrue/music/MigrationTest.kt create mode 100644 app/src/main/java/com/ztftrue/music/sqlData/dao/StorageFolderDao.kt create mode 100644 app/src/main/java/com/ztftrue/music/sqlData/model/StorageFolder.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2581924..495a0b6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -143,7 +143,7 @@ dependencies { // optional - RxJava3 support for Room implementation("androidx.room:room-rxjava3:$roomVersion") // optional - Test helpers - testImplementation("androidx.room:room-testing:$roomVersion") + implementation("androidx.room:room-testing:$roomVersion") // optional - Paging 3 Integration implementation("androidx.room:room-paging:$roomVersion") diff --git a/app/schemas/com.ztftrue.music.sqlData.MusicDatabase/2.json b/app/schemas/com.ztftrue.music.sqlData.MusicDatabase/2.json index 289997a..f7415c7 100644 --- a/app/schemas/com.ztftrue.music.sqlData.MusicDatabase/2.json +++ b/app/schemas/com.ztftrue.music.sqlData.MusicDatabase/2.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "7612f82a538157fcffbf8aa079662086", + "identityHash": "fe6d3dfdaef5ff1b312e6985cf0f5356", "entities": [ { "tableName": "aux", @@ -333,12 +333,38 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "storage_folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `uri` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7612f82a538157fcffbf8aa079662086')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe6d3dfdaef5ff1b312e6985cf0f5356')" ] } } \ No newline at end of file diff --git a/app/schemas/com.ztftrue.music.sqlData.MusicDatabase/3.json b/app/schemas/com.ztftrue.music.sqlData.MusicDatabase/3.json new file mode 100644 index 0000000..5a473e8 --- /dev/null +++ b/app/schemas/com.ztftrue.music.sqlData.MusicDatabase/3.json @@ -0,0 +1,370 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "fe6d3dfdaef5ff1b312e6985cf0f5356", + "entities": [ + { + "tableName": "aux", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `speed` REAL NOT NULL, `pitch` REAL NOT NULL, `echo` INTEGER NOT NULL, `echoDelay` REAL NOT NULL, `echoDecay` REAL NOT NULL, `echoRevert` INTEGER NOT NULL, `equalizer` INTEGER NOT NULL, `equalizerBand` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "speed", + "columnName": "speed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pitch", + "columnName": "pitch", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "echo", + "columnName": "echo", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "echoDelay", + "columnName": "echoDelay", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "echoDecay", + "columnName": "echoDecay", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "echoRevert", + "columnName": "echoRevert", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "equalizer", + "columnName": "equalizer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "equalizerBand", + "columnName": "equalizerBand", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CurrentList", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `listID` INTEGER NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "listID", + "columnName": "listID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "main_tab", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `priority` INTEGER NOT NULL, `isShow` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShow", + "columnName": "isShow", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "PlayConfig", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `repeatModel` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatModel", + "columnName": "repeatModel", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tableId` INTEGER, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, `path` TEXT NOT NULL, `duration` INTEGER NOT NULL, `displayName` TEXT NOT NULL, `album` TEXT NOT NULL, `albumId` INTEGER NOT NULL, `artist` TEXT NOT NULL, `artistId` INTEGER NOT NULL, `genre` TEXT NOT NULL, `genreId` INTEGER NOT NULL, `year` INTEGER NOT NULL, `songNumber` INTEGER NOT NULL, `priority` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, PRIMARY KEY(`tableId`))", + "fields": [ + { + "fieldPath": "tableId", + "columnName": "tableId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "genreId", + "columnName": "genreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songNumber", + "columnName": "songNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFavorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tableId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictionary_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT NOT NULL, `package_name` TEXT NOT NULL, `label` TEXT NOT NULL, `isShow` INTEGER NOT NULL, `autoGo` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isShow", + "columnName": "isShow", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoGo", + "columnName": "autoGo", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "storage_folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `uri` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe6d3dfdaef5ff1b312e6985cf0f5356')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/ztftrue/music/MigrationTest.kt b/app/src/androidTest/java/com/ztftrue/music/MigrationTest.kt new file mode 100644 index 0000000..4bba2e0 --- /dev/null +++ b/app/src/androidTest/java/com/ztftrue/music/MigrationTest.kt @@ -0,0 +1,49 @@ +package com.ztftrue.music + +import androidx.room.Room +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.ztftrue.music.sqlData.MusicDatabase +import com.ztftrue.music.sqlData.MusicDatabase.Companion.MIGRATION_1_2 +import com.ztftrue.music.sqlData.MusicDatabase.Companion.MIGRATION_2_3 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class MigrationTest { + private val TEST_DB = "migration-test" + + // Array of all migrations. + private val ALL_MIGRATIONS = arrayOf( + MIGRATION_1_2, MIGRATION_2_3) + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + MusicDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + @Throws(IOException::class) + fun migrateAll() { + // Create earliest version of the database. + helper.createDatabase(TEST_DB, 1).apply { + close() + } + + // Open latest version of the database. Room validates the schema + // once all migrations execute. + Room.databaseBuilder( + InstrumentationRegistry.getInstrumentation().targetContext, + MusicDatabase::class.java, + TEST_DB + ).addMigrations(*ALL_MIGRATIONS).build().apply { + openHelper.writableDatabase.close() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ztftrue/music/MainActivity.kt b/app/src/main/java/com/ztftrue/music/MainActivity.kt index 6a4a4bf..36b76c6 100644 --- a/app/src/main/java/com/ztftrue/music/MainActivity.kt +++ b/app/src/main/java/com/ztftrue/music/MainActivity.kt @@ -54,6 +54,7 @@ import com.ztftrue.music.play.EVENT_changePlayQueue import com.ztftrue.music.play.PlayService import com.ztftrue.music.sqlData.model.MainTab import com.ztftrue.music.sqlData.model.MusicItem +import com.ztftrue.music.sqlData.model.StorageFolder import com.ztftrue.music.ui.home.BaseLayout import com.ztftrue.music.ui.theme.MusicPitchTheme import com.ztftrue.music.utils.OperateTypeInActivity @@ -270,6 +271,30 @@ class MainActivity : ComponentActivity() { } } } + val folderPickerLauncher: ActivityResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK) { + val treeUri = result.data?.data; + if (treeUri != null) { + contentResolver.takePersistableUriPermission( + treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + CoroutineScope(Dispatchers.IO).launch{ + musicViewModel.getDb(this@MainActivity).StorageFolderDao().insert( + StorageFolder(null, treeUri.toString()) + ) + musicViewModel.dealLyrics( + this@MainActivity, + musicViewModel.currentPlay.value!! + ) + } + + } + } + } private fun openAppSettings() { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) diff --git a/app/src/main/java/com/ztftrue/music/MusicViewModel.kt b/app/src/main/java/com/ztftrue/music/MusicViewModel.kt index f91f5e4..8b23400 100644 --- a/app/src/main/java/com/ztftrue/music/MusicViewModel.kt +++ b/app/src/main/java/com/ztftrue/music/MusicViewModel.kt @@ -2,6 +2,7 @@ package com.ztftrue.music import android.content.ContentUris import android.content.Context +import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever @@ -14,10 +15,11 @@ import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.style.TextAlign +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel import androidx.media3.common.Player -import androidx.media3.common.util.UnstableApi import androidx.navigation.NavHostController +import com.ztftrue.music.sqlData.MusicDatabase import com.ztftrue.music.sqlData.model.DictionaryApp import com.ztftrue.music.sqlData.model.MainTab import com.ztftrue.music.sqlData.model.MusicItem @@ -31,18 +33,22 @@ import com.ztftrue.music.utils.model.AnyListBase import com.ztftrue.music.utils.model.Caption import com.ztftrue.music.utils.model.EqualizerBand import com.ztftrue.music.utils.model.ListStringCaption +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.BufferedReader import java.io.File -import java.io.InputStream +import java.io.InputStreamReader var SongsPlayList = AnyListBase(-1, PlayListType.Songs) var QueuePlayList = AnyListBase(-1, PlayListType.Queue) -@UnstableApi class MusicViewModel : ViewModel() { private val retriever = MediaMetadataRetriever() + var db: MusicDatabase? = null val refreshList = mutableStateOf(false) var navController: NavHostController? = null var themeSelected = mutableIntStateOf(0) @@ -137,6 +143,13 @@ class MusicViewModel : ViewModel() { equalizerBands.addAll(band) } + fun getDb(context: Context): MusicDatabase { + if (db == null) { + db = MusicDatabase.getDatabase(context) + } + return db!! + } + fun dealLyrics(context: Context, currentPlay: MusicItem) { currentCaptionList.clear() if (currentCaptionList.isEmpty()) { @@ -155,17 +168,87 @@ class MusicViewModel : ViewModel() { val text = File("$path.txt") if (text.exists()) { lyricsType = LyricsType.TEXT - currentCaptionList.addAll(readText(text, context)) + currentCaptionList.addAll( + readCaptions( + text.bufferedReader(), + LyricsType.TEXT, + context + ) + ) } else if (File("$path.lrc").exists()) { lyricsType = LyricsType.LRC - currentCaptionList.addAll(readLyrics(File("$path.lrc"), context)) + currentCaptionList.addAll( + readCaptions( + File("$path.lrc").bufferedReader(), + LyricsType.LRC, + context + ) + ) } else if (File("$path.srt").exists()) { lyricsType = LyricsType.SRT - currentCaptionList.addAll(readCaptions(File("$path.srt"), LyricsType.SRT)) + currentCaptionList.addAll( + readCaptions( + File("$path.srt").bufferedReader(), + LyricsType.SRT, + context + ) + ) } else if (File("$path.vtt").exists()) { lyricsType = LyricsType.VTT - currentCaptionList.addAll(readCaptions(File("$path.vtt"), LyricsType.VTT)) + currentCaptionList.addAll( + readCaptions( + File("$path.vtt").bufferedReader(), + LyricsType.VTT, + context + ) + ) } else { + CoroutineScope(Dispatchers.IO).launch { + getDb(context).StorageFolderDao().findAll()?.forEach { storageFolder -> + val treeUri = Uri.parse(storageFolder.uri) + if (treeUri != null) { + context.contentResolver.takePersistableUriPermission( + treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + val pickedDir = DocumentFile.fromTreeUri(context, treeUri) + val d = pickedDir?.listFiles() + d?.forEach { + if (it.isFile && it.canRead() + ) { + val musicName: String = try { + currentPlay.path.substring( + currentPlay.path.lastIndexOf("/")+1, + currentPlay.path.lastIndexOf(".") + ) + } catch (e: Exception) { + "" + } + val fileNameWithSuffix = it.name?.lowercase() ?: "" + val fileName = try { + fileNameWithSuffix.substring( + 0, + fileNameWithSuffix.lastIndexOf(".") + ) + } catch (e: Exception) { + "" + } + if (fileName.trim().lowercase() == musicName.trim().lowercase()) { + if (fileNameWithSuffix.endsWith(".lrc")) { + fileRead(it.uri, context, LyricsType.LRC) + } else if (fileNameWithSuffix.endsWith(".srt")) { + fileRead(it.uri, context, LyricsType.SRT) + } else if (fileNameWithSuffix.endsWith(".vtt")) { + fileRead(it.uri, context, LyricsType.VTT) + } else if (fileNameWithSuffix.endsWith(".txt")) { + fileRead(it.uri, context, LyricsType.TEXT) + } + } + } + } + } + } + } return } } @@ -174,60 +257,29 @@ class MusicViewModel : ViewModel() { itemDuration = duration / if (currentCaptionList.size == 0) 1 else currentCaptionList.size } - private fun readLyrics(file: File, context: Context): ArrayList { - val arrayList = arrayListOf() - val inputStream: InputStream = file.inputStream() - val inputString = inputStream.bufferedReader().use { it.readText() } - val lyricsHashMap: LinkedHashMap = - linkedMapOf() - inputString.split("\n").forEachIndexed { _, it -> - if (it.startsWith("offset:")) { - // TODO - } else { - val captions = CaptionUtils.parseLyricLine(it, context) - val an = ListStringCaption( - text = ArrayList(captions.text.split(Regex("[\\r\\s]+"))), - timeStart = captions.timeStart, - timeEnd = captions.timeEnd - ) - val temp = lyricsHashMap[captions.timeStart] - if (temp != null) { - temp.text.add("\n") - temp.text.addAll(captions.text.split("[\\r\\s]+")) - } else { - lyricsHashMap[captions.timeStart] = an - } - } - } - arrayList.addAll(lyricsHashMap.values) - return arrayList - } - - private fun readText(file: File, context: Context): ArrayList { - val arrayList = arrayListOf() - val inputStream: InputStream = file.inputStream() - val inputString = inputStream.bufferedReader().use { it.readText() } - inputString.split("\n").forEach { - val captions = CaptionUtils.parseLyricLine(it, context) - val an = ListStringCaption( - text = ArrayList(captions.text.split(Regex("[\\n\\r\\s]+"))), - timeStart = captions.timeStart, - timeEnd = captions.timeEnd - ) - arrayList.add(an) - } - return arrayList + private fun fileRead(uri: Uri, context: Context, captionType: LyricsType) { + lyricsType = captionType + val inputStream = + context.contentResolver.openInputStream(uri) + val reader = + BufferedReader(InputStreamReader(inputStream)) + currentCaptionList.addAll(readCaptions(reader, lyricsType, context)) } private fun readCaptions( - file: File, + bufferedReader: BufferedReader, captionType: LyricsType, + context: Context ): ArrayList { val captions = arrayListOf() if (captionType == LyricsType.SRT) { - captions.addAll(CaptionUtils.parseSrtFile(file)) + captions.addAll(CaptionUtils.parseSrtFile(bufferedReader)) } else if (captionType == LyricsType.VTT) { - captions.addAll(CaptionUtils.parseVttFile(file)) + captions.addAll(CaptionUtils.parseVttFile(bufferedReader)) + } else if (captionType == LyricsType.LRC) { + captions.addAll(CaptionUtils.parseLrcFile(bufferedReader, context)) + } else if (captionType == LyricsType.TEXT) { + captions.addAll(CaptionUtils.parseTextFile(bufferedReader, context)) } val arrayList = arrayListOf() captions.forEach { diff --git a/app/src/main/java/com/ztftrue/music/sqlData/MusicDatabase.kt b/app/src/main/java/com/ztftrue/music/sqlData/MusicDatabase.kt index efa2570..df2fa1f 100644 --- a/app/src/main/java/com/ztftrue/music/sqlData/MusicDatabase.kt +++ b/app/src/main/java/com/ztftrue/music/sqlData/MusicDatabase.kt @@ -1,6 +1,7 @@ package com.ztftrue.music.sqlData import android.content.Context +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room.databaseBuilder import androidx.room.RoomDatabase @@ -12,17 +13,27 @@ import com.ztftrue.music.sqlData.dao.DictionaryAppDao import com.ztftrue.music.sqlData.dao.MainTabDao import com.ztftrue.music.sqlData.dao.PlayConfigDao import com.ztftrue.music.sqlData.dao.QueueDao +import com.ztftrue.music.sqlData.dao.StorageFolderDao import com.ztftrue.music.sqlData.model.Auxr import com.ztftrue.music.sqlData.model.CurrentList import com.ztftrue.music.sqlData.model.DictionaryApp import com.ztftrue.music.sqlData.model.MainTab import com.ztftrue.music.sqlData.model.MusicItem import com.ztftrue.music.sqlData.model.PlayConfig +import com.ztftrue.music.sqlData.model.StorageFolder import kotlin.concurrent.Volatile + const val MUSIC_DATABASE_NAME = "default_data.db" -@Database(entities = [Auxr::class, CurrentList::class,MainTab::class, PlayConfig::class,MusicItem::class, DictionaryApp::class], version = 2, exportSchema = true) +@Database( + entities = [Auxr::class, CurrentList::class, MainTab::class, PlayConfig::class, MusicItem::class, DictionaryApp::class, StorageFolder::class], + version = 3, + exportSchema = true, + autoMigrations = [ + AutoMigration(from = 2, to = 3) + ] +) abstract class MusicDatabase : RoomDatabase() { abstract fun AuxDao(): AuxDao abstract fun CurrentListDao(): CurrentListDao @@ -30,11 +41,12 @@ abstract class MusicDatabase : RoomDatabase() { abstract fun MainTabDao(): MainTabDao abstract fun QueueDao(): QueueDao abstract fun DictionaryAppDao(): DictionaryAppDao + abstract fun StorageFolderDao(): StorageFolderDao companion object { @Volatile private var INSTANCE: MusicDatabase? = null - fun getDatabase(context: Context): MusicDatabase { + fun getDatabase(context: Context): MusicDatabase { if (INSTANCE == null) { synchronized(MusicDatabase::class.java) { if (INSTANCE == null) { @@ -43,26 +55,44 @@ abstract class MusicDatabase : RoomDatabase() { MusicDatabase::class.java, MUSIC_DATABASE_NAME ) .addMigrations(MIGRATION_1_2) // Add migration path from version 1 to version 2 + .addMigrations(MIGRATION_2_3) .build() } } } return INSTANCE!! } - private val MIGRATION_1_2: Migration = object : Migration(1, 2) { + + val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + // Perform the necessary database schema changes for migration from version 1 to version 2 + // You may need to alter tables, add new columns, etc. + // For example: + // database.execSQL("ALTER TABLE your_entity ADD COLUMN new_column_name TEXT"); + db.execSQL( + "CREATE TABLE IF NOT EXISTS dictionary_app (\n" + + " id INTEGER PRIMARY KEY AUTOINCREMENT,\n" + + " name TEXT NOT NULL,\n" + + " package_name TEXT NOT NULL,\n" + + " autoGo INTEGER NOT NULL,\n" + + " label TEXT NOT NULL,\n" + + " isShow INTEGER NOT NULL\n" + + ");\n" + ) + } + } + val MIGRATION_2_3: Migration = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { // Perform the necessary database schema changes for migration from version 1 to version 2 // You may need to alter tables, add new columns, etc. // For example: // database.execSQL("ALTER TABLE your_entity ADD COLUMN new_column_name TEXT"); - db.execSQL("CREATE TABLE IF NOT EXISTS dictionary_app (\n" + - " id INTEGER PRIMARY KEY AUTOINCREMENT,\n" + - " name TEXT NOT NULL,\n" + - " package_name TEXT NOT NULL,\n" + - " autoGo INTEGER NOT NULL,\n" + - " label TEXT NOT NULL,\n" + - " isShow INTEGER NOT NULL\n" + - ");\n") + db.execSQL( + "CREATE TABLE IF NOT EXISTS storage_folder (\n" + + " id INTEGER PRIMARY KEY AUTOINCREMENT,\n" + + " uri TEXT NOT NULL\n" + + ");\n" + ) } } } diff --git a/app/src/main/java/com/ztftrue/music/sqlData/dao/StorageFolderDao.kt b/app/src/main/java/com/ztftrue/music/sqlData/dao/StorageFolderDao.kt new file mode 100644 index 0000000..9825d4a --- /dev/null +++ b/app/src/main/java/com/ztftrue/music/sqlData/dao/StorageFolderDao.kt @@ -0,0 +1,30 @@ +package com.ztftrue.music.sqlData.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.ztftrue.music.sqlData.model.StorageFolder + +@Dao +interface StorageFolderDao { + + + @Query("SELECT * FROM storage_folder WHERE id = :id") + fun findById(id: Long): StorageFolder? + + @Query("SELECT * FROM storage_folder ORDER BY id ASC LIMIT 1") + fun findFirstFolder(): StorageFolder? + + @Query("SELECT * FROM storage_folder") + fun findAll(): List? + + @Insert + fun insert(folder: StorageFolder) + + @Update + fun update(folder: StorageFolder) + + @Query("DELETE FROM storage_folder WHERE id = :id") + fun deleteById(id: Long) +} \ No newline at end of file diff --git a/app/src/main/java/com/ztftrue/music/sqlData/model/StorageFolder.kt b/app/src/main/java/com/ztftrue/music/sqlData/model/StorageFolder.kt new file mode 100644 index 0000000..b778439 --- /dev/null +++ b/app/src/main/java/com/ztftrue/music/sqlData/model/StorageFolder.kt @@ -0,0 +1,33 @@ +package com.ztftrue.music.sqlData.model + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity(tableName = "storage_folder") +data class StorageFolder( + @PrimaryKey + val id: Int?, + @ColumnInfo(name = "uri") val uri: String, +) : Parcelable { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StorageFolder + + if (id != other.id) return false + if (uri != other.uri) return false + + return true + } + + override fun hashCode(): Int { + var result = id ?: 0 + result = 31 * result + uri.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ztftrue/music/ui/other/SettingsPage.kt b/app/src/main/java/com/ztftrue/music/ui/other/SettingsPage.kt index 9be0555..ee6751c 100644 --- a/app/src/main/java/com/ztftrue/music/ui/other/SettingsPage.kt +++ b/app/src/main/java/com/ztftrue/music/ui/other/SettingsPage.kt @@ -53,7 +53,6 @@ import androidx.media3.common.util.UnstableApi import androidx.navigation.NavHostController import com.ztftrue.music.MusicViewModel import com.ztftrue.music.R -import com.ztftrue.music.sqlData.MusicDatabase import com.ztftrue.music.sqlData.model.MainTab import com.ztftrue.music.ui.public.BackTopBar import com.ztftrue.music.utils.Utils @@ -252,7 +251,7 @@ fun SettingsPage( } -@Composable +@UnstableApi @Composable fun ManageTabDialog(musicViewModel: MusicViewModel, onDismiss: () -> Unit) { val context = LocalContext.current @@ -260,11 +259,10 @@ fun ManageTabDialog(musicViewModel: MusicViewModel, onDismiss: () -> Unit) { val mainTabList = remember { mutableStateListOf() } var size by remember { mutableIntStateOf(0) } - var db: MusicDatabase? = null + LaunchedEffect(Unit) { scopeMain.launch { - db = MusicDatabase.getDatabase(context) - mainTabList.addAll(db?.MainTabDao()?.findAllMainTabSortByPriority() ?: emptyList()) + mainTabList.addAll(musicViewModel.getDb(context).MainTabDao().findAllMainTabSortByPriority() ?: emptyList()) size = mainTabList.size } } @@ -276,7 +274,7 @@ fun ManageTabDialog(musicViewModel: MusicViewModel, onDismiss: () -> Unit) { } } scopeMain.launch { - db?.MainTabDao()?.updateAll(mainTabList) + musicViewModel.getDb(context).MainTabDao().updateAll(mainTabList) onDismiss() } } diff --git a/app/src/main/java/com/ztftrue/music/ui/play/LyricsView.kt b/app/src/main/java/com/ztftrue/music/ui/play/LyricsView.kt index 6d8df9e..47c27fe 100644 --- a/app/src/main/java/com/ztftrue/music/ui/play/LyricsView.kt +++ b/app/src/main/java/com/ztftrue/music/ui/play/LyricsView.kt @@ -164,7 +164,7 @@ fun LyricsView( } } LaunchedEffect(showMenu) { - if(showMenu){ + if (showMenu) { val list = musicViewModel.dictionaryAppList list.forEach { if (it.autoGo) { @@ -259,47 +259,63 @@ fun LyricsView( } if (musicViewModel.currentCaptionList.size == 0) { - Text( - text = "No Lyrics, Click to import lyrics.\n Support LRC/VTT/SRT/TXT", - color = MaterialTheme.colorScheme.onBackground, - fontSize = MaterialTheme.typography.titleLarge.fontSize, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(2.dp) - .clickable { - if (musicViewModel.currentPlay.value != null) { - val regexPattern = Regex("[<>\"/~'{}?,+=)(^&*%!@#\$]") - val artistsFolder = musicViewModel.currentPlay.value?.artist - ?.replace( - regexPattern, - "_" + Column { + Text( + text = "No Lyrics, Click to import lyrics.\n Support LRC/VTT/SRT/TXT", + color = MaterialTheme.colorScheme.onBackground, + fontSize = MaterialTheme.typography.titleLarge.fontSize, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(2.dp) + .clickable { + if (musicViewModel.currentPlay.value != null) { + val regexPattern = Regex("[<>\"/~'{}?,+=)(^&*%!@#\$]") + val artistsFolder = musicViewModel.currentPlay.value?.artist + ?.replace( + regexPattern, + "_" + ) + val folderPath = "$Lyrics/$artistsFolder" + val folder = context.getExternalFilesDir( + folderPath ) - val folderPath = "$Lyrics/$artistsFolder" - val folder = context.getExternalFilesDir( - folderPath - ) - folder?.mkdirs() - val id = - musicViewModel.currentPlay.value?.name?.replace(regexPattern, "_") - val pathLyrics: String = - context.getExternalFilesDir(folderPath)?.absolutePath + "/$id.lrc" - val path: String = - context.getExternalFilesDir(folderPath)?.absolutePath + "/$id.txt" - val lyrics = File(pathLyrics) - val text = File(path) - if (lyrics.exists()) { - Utils.openFile(lyrics.path, context = context) - } else if (text.exists()) { - Utils.openFile(text.path, context = context) - } else { - val tempPath: String = - context.getExternalFilesDir(folderPath)?.absolutePath + "/$id." - (context as MainActivity).openFilePicker(tempPath) + folder?.mkdirs() + val id = + musicViewModel.currentPlay.value?.name?.replace(regexPattern, "_") + val pathLyrics: String = + context.getExternalFilesDir(folderPath)?.absolutePath + "/$id.lrc" + val path: String = + context.getExternalFilesDir(folderPath)?.absolutePath + "/$id.txt" + val lyrics = File(pathLyrics) + val text = File(path) + if (lyrics.exists()) { + Utils.openFile(lyrics.path, context = context) + } else if (text.exists()) { + Utils.openFile(text.path, context = context) + } else { + val tempPath: String = + context.getExternalFilesDir(folderPath)?.absolutePath + "/$id." + (context as MainActivity).openFilePicker(tempPath) + } } } - } - ) + ) + Text( + text = "Or, Click to set lyrics folder", + color = MaterialTheme.colorScheme.onBackground, + fontSize = MaterialTheme.typography.titleLarge.fontSize, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(2.dp) + .clickable { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + (context as MainActivity).folderPickerLauncher.launch(intent) + } + ) + } + } else { CompositionLocalProvider( LocalTextToolbar provides CustomTextToolbar( diff --git a/app/src/main/java/com/ztftrue/music/ui/play/PlayingPage.kt b/app/src/main/java/com/ztftrue/music/ui/play/PlayingPage.kt index 68116c9..29d0cfd 100644 --- a/app/src/main/java/com/ztftrue/music/ui/play/PlayingPage.kt +++ b/app/src/main/java/com/ztftrue/music/ui/play/PlayingPage.kt @@ -104,7 +104,6 @@ import com.ztftrue.music.play.ACTION_PlayLIST_CHANGE import com.ztftrue.music.play.ACTION_RemoveFromQueue import com.ztftrue.music.play.ACTION_SEEK_TO import com.ztftrue.music.play.ACTION_TRACKS_DELETE -import com.ztftrue.music.sqlData.MusicDatabase import com.ztftrue.music.sqlData.model.DictionaryApp import com.ztftrue.music.sqlData.model.MusicItem import com.ztftrue.music.ui.public.AddMusicToPlayListDialog @@ -992,13 +991,9 @@ fun PlayingPage( item.id = index } CoroutineScope(Dispatchers.IO).launch { - val db: MusicDatabase = - MusicDatabase.getDatabase( - context - ) - db.DictionaryAppDao() + viewModel.getDb(context).DictionaryAppDao() .deleteAll() - db.DictionaryAppDao() + viewModel.getDb(context).DictionaryAppDao() .insertAll(result) } viewModel.dictionaryAppList.clear() diff --git a/app/src/main/java/com/ztftrue/music/utils/CaptionUtils.kt b/app/src/main/java/com/ztftrue/music/utils/CaptionUtils.kt index 2ec4189..1de2e10 100644 --- a/app/src/main/java/com/ztftrue/music/utils/CaptionUtils.kt +++ b/app/src/main/java/com/ztftrue/music/utils/CaptionUtils.kt @@ -3,7 +3,7 @@ package com.ztftrue.music.utils import android.content.Context import com.ztftrue.music.R import com.ztftrue.music.utils.model.Caption -import java.io.File +import java.io.BufferedReader import java.util.Locale import java.util.regex.Matcher import java.util.regex.Pattern @@ -16,9 +16,9 @@ enum class LyricsType { } object CaptionUtils { - fun parseVttFile(file: File): List { + fun parseVttFile(bufferedReader: BufferedReader): List { val captions = mutableListOf() - file.bufferedReader().useLines { lines -> + bufferedReader.useLines { lines -> var startTime = 0L var endTime = 0L val text = StringBuilder() @@ -49,29 +49,32 @@ object CaptionUtils { return captions } - fun parseSrtFile(file: File): List { + fun parseSrtFile(bufferedReader: BufferedReader): List { val subtitles = mutableListOf() var startTime = 0L var endTime = 0L var text = StringBuilder() - file.readLines().forEach { line -> - when { - line.isBlank() -> { - // Blank line indicates the end of a subtitle - if (text.isNotEmpty()) { - subtitles.add(Caption(text.toString().trim(), startTime, endTime)) + bufferedReader.useLines { + for (line in it) { + when { + line.isBlank() -> { + // Blank line indicates the end of a subtitle + if (text.isNotEmpty()) { + subtitles.add(Caption(text.toString().trim(), startTime, endTime)) + } } - } - line.matches(Regex("^\\d+:\\d+:\\d+,\\d+\\s-->\\s\\d+:\\d+:\\d+,\\d+")) -> { - val times = line.split(Pattern.compile("\\s+-->\\s+")) - startTime = captionTimestampToMilliseconds(times[0],",") - endTime = captionTimestampToMilliseconds(times[1],",") - text = StringBuilder() - } - else -> { - // Text line - text.append(line).append("\n") + line.matches(Regex("^\\d+:\\d+:\\d+,\\d+\\s-->\\s\\d+:\\d+:\\d+,\\d+")) -> { + val times = line.split(Pattern.compile("\\s+-->\\s+")) + startTime = captionTimestampToMilliseconds(times[0], ",") + endTime = captionTimestampToMilliseconds(times[1], ",") + text = StringBuilder() + } + + else -> { + // Text line + text.append(line).append("\n") + } } } } @@ -81,7 +84,54 @@ object CaptionUtils { return subtitles } - private fun captionTimestampToMilliseconds(timestamp: String,splitter: String = "."): Long { + fun parseLrcFile(bufferedReader: BufferedReader, context: Context): ArrayList { + val arrayList = arrayListOf() + val lyricsHashMap: LinkedHashMap = + linkedMapOf() + bufferedReader.useLines { + for (line in it) { + if (line.startsWith("offset:")) { + // TODO + } else { + val captions = parseLyricLine(line, context) + val temp = lyricsHashMap[captions.timeStart] + if (temp != null) { + temp.text += "\n" + temp.text += captions.text + } else { + lyricsHashMap[captions.timeStart] = Caption( + text = captions.text, + timeStart = captions.timeStart, + timeEnd = captions.timeEnd + ) + } + } + } + } + arrayList.addAll(lyricsHashMap.values) + return arrayList + } + + fun parseTextFile( + bufferedReader: BufferedReader, + context: Context + ): ArrayList { + val arrayList = arrayListOf() + bufferedReader.useLines { + for (line in it) { + val captions = parseLyricLine(line, context) + val an = Caption( + text = captions.text, + timeStart = captions.timeStart, + timeEnd = captions.timeEnd + ) + arrayList.add(an) + } + } + return arrayList + } + + private fun captionTimestampToMilliseconds(timestamp: String, splitter: String = "."): Long { val parts = timestamp.split(":") val hours = parts[0].toLong() val minutes = parts[1].toLong() diff --git a/app/src/main/java/com/ztftrue/music/utils/Utils.kt b/app/src/main/java/com/ztftrue/music/utils/Utils.kt index a45a3cf..b471551 100644 --- a/app/src/main/java/com/ztftrue/music/utils/Utils.kt +++ b/app/src/main/java/com/ztftrue/music/utils/Utils.kt @@ -13,7 +13,6 @@ import com.ztftrue.music.MusicViewModel import com.ztftrue.music.play.ACTION_AddPlayQueue import com.ztftrue.music.play.ACTION_GET_TRACKS import com.ztftrue.music.play.ACTION_PlayLIST_CHANGE -import com.ztftrue.music.sqlData.MusicDatabase import com.ztftrue.music.sqlData.model.DictionaryApp import com.ztftrue.music.sqlData.model.MusicItem import com.ztftrue.music.utils.model.AnyListBase @@ -106,7 +105,7 @@ object Utils { fun initSettingsData(musicViewModel: MusicViewModel,context: Context) { CoroutineScope(Dispatchers.IO).launch { - val db: MusicDatabase = MusicDatabase.getDatabase(context) + musicViewModel.themeSelected.intValue = context.getSharedPreferences( "SelectedTheme", Context.MODE_PRIVATE @@ -118,7 +117,7 @@ object Utils { SharedPreferencesUtils.getAutoScroll(context) musicViewModel.autoHighLight.value = SharedPreferencesUtils.getAutoHighLight(context) - val dicApps = db.DictionaryAppDao().findAllDictionaryApp() + val dicApps = musicViewModel.getDb(context).DictionaryAppDao().findAllDictionaryApp() if (dicApps.isNullOrEmpty()) { val list = ArrayList() getAllDictionaryActivity(context)