Initial import: Music_Server, MusicFree, catalog-sync

This commit is contained in:
2026-05-23 16:51:14 +08:00
commit 069af30dba
847 changed files with 179878 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
import com.android.build.OutputFile
import groovy.json.JsonSlurper
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
// cliFile = file("../../node_modules/react-native/cli.js")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The command to run when bundling. By default is 'bundle'
// bundleCommand = "ram-bundle"
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
//
// Added by install-expo-modules
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", rootDir.getAbsoluteFile().getParentFile().getAbsolutePath(), "android", "absolute"].execute(null, rootDir).text.trim())
cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
//
// Added by install-expo-modules
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", rootDir.getAbsoluteFile().getParentFile().getAbsolutePath(), "android", "absolute"].execute(null, rootDir).text.trim())
cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
// !! Add lines
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('keystore.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
static def getVersion() {
def inputFile = new File("../package.json")
def packageJson = new JsonSlurper().parseText(inputFile.text)
return packageJson["version"]
}
// static def versionStringToCode(String version) {
// def parts = version.split('\\.')
// def versionCode = 0
// def multiplier = 1000000
//
// parts.each { part ->
// versionCode += part.toInteger() * multiplier
// multiplier /= 1000
// }
//
// return versionCode.intValue()
// }
def appVersion = getVersion()
def appVersionCode = 400012
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "fun.upup.musicfree"
defaultConfig {
applicationId "fun.upup.musicfree"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode appVersionCode
versionName appVersion
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
// !! Add lines
release {
storeFile file(keystoreProperties['RELEASE_STORE_FILE'])
storePassword keystoreProperties['RELEASE_STORE_PASSWORD']
keyAlias keystoreProperties['RELEASE_KEY_ALIAS']
keyPassword keystoreProperties['RELEASE_KEY_PASSWORD']
}
}
splits {
abi {
reset()
enable true
universalApk true
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.release
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
// !! Add lines
implementation project(':react-native-fs')
implementation 'com.facebook.fresco:animated-gif:2.5.0'
// https://mvnrepository.com/artifact/net.jthink/jaudiotagger
implementation 'net.jthink:jaudiotagger:2.2.5'
implementation 'androidx.core:core-splashscreen:1.0.0'
}
+10
View File
@@ -0,0 +1,10 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
@@ -0,0 +1,31 @@
package `fun`.upup.musicfree
import expo.modules.ReactActivityDelegateWrapper
import expo.modules.splashscreen.SplashScreenManager
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import android.os.Bundle
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "MusicFree"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled))
// https://reactnavigation.org/docs/getting-started/#installing-dependencies-into-a-bare-react-native-project
override fun onCreate(savedInstanceState: Bundle?) {
SplashScreenManager.registerOnActivity(this)
super.onCreate(null);
}
}
@@ -0,0 +1,59 @@
package `fun`.upup.musicfree
import android.content.res.Configuration
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import `fun`.upup.musicfree.lyricUtil.LyricUtilPackage
import `fun`.upup.musicfree.mp3Util.Mp3UtilPackage
import `fun`.upup.musicfree.utils.UtilsPackage
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(UtilsPackage())
add(Mp3UtilPackage())
add(LyricUtilPackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
})
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}
@@ -0,0 +1,182 @@
package `fun`.upup.musicfree.lyricUtil
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import androidx.annotation.RequiresApi
import com.facebook.react.bridge.*
import java.util.*
class LyricUtilModule(private val reactContext: ReactApplicationContext): ReactContextBaseJavaModule(reactContext) {
override fun getName() = "LyricUtil"
private var lyricView: LyricView? = null
@ReactMethod
fun checkSystemAlertPermission(promise: Promise) {
try {
promise.resolve(Settings.canDrawOverlays(reactContext))
} catch (e: Exception) {
promise.reject("Error", e.message)
}
}
@ReactMethod
fun requestSystemAlertPermission(promise: Promise) {
try {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
data = Uri.parse("package:" + reactContext.packageName)
}
currentActivity?.startActivity(intent)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Error", e.message)
}
}
@ReactMethod
fun showStatusBarLyric(initLyric: String?, options: ReadableMap?, promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
if (lyricView == null) {
lyricView = LyricView(reactContext)
}
val mapOptions = mutableMapOf<String, Any>().apply {
if (options == null) {
return@apply
}
if (options.hasKey("topPercent")) {
put("topPercent", options.getDouble("topPercent"))
}
if (options.hasKey("leftPercent")) {
put("leftPercent", options.getDouble("leftPercent"))
}
if (options.hasKey("align")) {
put("align", options.getInt("align"))
}
if (options.hasKey("color")) {
options.getString("color")?.let { put("color", it) }
}
if (options.hasKey("backgroundColor")) {
options.getString("backgroundColor")?.let { put("backgroundColor", it) }
}
if (options.hasKey("widthPercent")) {
put("widthPercent", options.getDouble("widthPercent"))
}
if (options.hasKey("fontSize")) {
put("fontSize", options.getDouble("fontSize"))
}
}
try {
lyricView?.showLyricWindow(initLyric, mapOptions)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun hideStatusBarLyric(promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
lyricView?.hideLyricWindow()
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun setStatusBarLyricText(lyric: String, promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
lyricView?.setText(lyric)
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun setStatusBarLyricAlign(alignment: Int, promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
lyricView?.setAlign(alignment)
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun setStatusBarLyricTop(pct: Double, promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
lyricView?.setTopPercent(pct)
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun setStatusBarLyricLeft(pct: Double, promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
lyricView?.setLeftPercent(pct)
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun setStatusBarLyricWidth(pct: Double, promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
lyricView?.setWidth(pct)
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun setStatusBarLyricFontSize(fontSize: Float, promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
lyricView?.setFontSize(fontSize)
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun setStatusBarColors(textColor: String?, backgroundColor: String?, promise: Promise) {
try {
UiThreadUtil.runOnUiThread {
lyricView?.setColors(textColor, backgroundColor)
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
}
@@ -0,0 +1,19 @@
package `fun`.upup.musicfree.lyricUtil
import android.view.View
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager
class LyricUtilPackage : ReactPackage {
override fun createViewManagers(
reactContext: ReactApplicationContext
): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
override fun createNativeModules(
reactContext: ReactApplicationContext
): MutableList<NativeModule> = listOf(LyricUtilModule(reactContext)).toMutableList()
}
@@ -0,0 +1,222 @@
package `fun`.upup.musicfree.lyricUtil
import android.app.Activity
import android.content.Context
import android.graphics.Color
import android.graphics.PixelFormat
import android.graphics.drawable.ColorDrawable
import android.hardware.SensorManager
import android.os.Build
import android.util.DisplayMetrics
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.OrientationEventListener
import android.view.View
import android.view.WindowManager
import android.widget.TextView
import com.facebook.react.bridge.ReactContext
class LyricView(private val reactContext: ReactContext) : Activity(), View.OnTouchListener {
private var windowManager: WindowManager? = null
private var orientationEventListener: OrientationEventListener? = null
private var layoutParams: WindowManager.LayoutParams? = null
private var tv: TextView? = null
// 窗口信息
private var windowWidth = 0.0
private var windowHeight = 0.0
private var widthPercent = 0.0
private var leftPercent = 0.0
private var topPercent = 0.0
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
Log.d("touch", "Desktop Touch")
return false
}
// 展示歌词窗口
fun showLyricWindow(initText: String?, options: Map<String, Any>) {
try {
if (windowManager == null) {
windowManager = reactContext.getSystemService(WINDOW_SERVICE) as WindowManager
layoutParams = WindowManager.LayoutParams()
val outMetrics = DisplayMetrics()
windowManager?.defaultDisplay?.getMetrics(outMetrics)
windowWidth = outMetrics.widthPixels.toDouble()
windowHeight = outMetrics.heightPixels.toDouble()
layoutParams?.type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
else
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
/*
* topPercent: number;
* leftPercent: number;
* align: number;
* color: string;
* backgroundColor: string;
* widthPercent: number;
* fontSize: number;
*/
val topPercent = options["topPercent"]
val leftPercent = options["leftPercent"]
val align = options["align"]
val color = options["color"]
val backgroundColor = options["backgroundColor"]
val widthPercent = options["widthPercent"]
val fontSize = options["fontSize"]
this.widthPercent = widthPercent?.toString()?.toDouble() ?: 0.5
layoutParams?.width = (this.widthPercent * windowWidth).toInt()
layoutParams?.height = WindowManager.LayoutParams.WRAP_CONTENT
layoutParams?.gravity = Gravity.TOP or Gravity.START
this.leftPercent = leftPercent?.toString()?.toDouble() ?: 0.5
layoutParams?.x = (this.leftPercent * (windowWidth - layoutParams!!.width)).toInt()
layoutParams?.y = 0
layoutParams?.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
layoutParams?.format = PixelFormat.TRANSPARENT
tv = TextView(reactContext).apply {
text = initText ?: ""
textSize = fontSize?.toString()?.toFloat() ?: 14f
setBackgroundColor(Color.parseColor(rgba2argb(backgroundColor?.toString() ?: "#84888153")))
setTextColor(Color.parseColor(rgba2argb(color?.toString() ?: "#FFE9D2")))
setPadding(12, 6, 12, 6)
gravity = align?.toString()?.toInt() ?: Gravity.CENTER
}
windowManager?.addView(tv, layoutParams)
topPercent?.toString()?.toDouble()?.let { setTopPercent(it) }
listenOrientationChange()
}
} catch (e: Exception) {
hideLyricWindow()
throw e
}
}
private fun listenOrientationChange() {
if (windowManager == null) return
if (orientationEventListener == null) {
orientationEventListener = object : OrientationEventListener(reactContext, SensorManager.SENSOR_DELAY_NORMAL) {
override fun onOrientationChanged(orientation: Int) {
if (windowManager != null) {
val outMetrics = DisplayMetrics()
windowManager?.defaultDisplay?.getMetrics(outMetrics)
windowWidth = outMetrics.widthPixels.toDouble()
windowHeight = outMetrics.heightPixels.toDouble()
layoutParams?.width = (widthPercent * windowWidth).toInt()
layoutParams?.x = (leftPercent * (windowWidth - layoutParams!!.width)).toInt()
layoutParams?.y = (topPercent * (windowHeight - tv!!.height)).toInt()
windowManager?.updateViewLayout(tv, layoutParams)
}
}
}
}
if (orientationEventListener?.canDetectOrientation() == true) {
orientationEventListener?.enable()
}
}
private fun unlistenOrientationChange() {
orientationEventListener?.disable()
}
private fun rgba2argb(color: String): String {
return if (color.length == 9) {
color[0] + color.substring(7, 9) + color.substring(1, 7)
} else {
color
}
}
// 隐藏歌词窗口
fun hideLyricWindow() {
if (windowManager != null) {
tv?.let {
try {
windowManager?.removeView(it)
} catch (e: Exception) {
// Handle exception
}
tv = null
}
windowManager = null
layoutParams = null
unlistenOrientationChange()
}
}
// 设置歌词内容
fun setText(text: String) {
tv?.text = text
}
fun setAlign(gravity: Int) {
tv?.gravity = gravity
}
fun setTopPercent(pct: Double) {
var percent = pct.coerceIn(0.0, 1.0)
tv?.let {
layoutParams?.y = (percent * (windowHeight - it.height)).toInt()
windowManager?.updateViewLayout(it, layoutParams)
}
this.topPercent = percent
}
fun setLeftPercent(pct: Double) {
var percent = pct.coerceIn(0.0, 1.0)
tv?.let {
layoutParams?.x = (percent * (windowWidth - layoutParams!!.width)).toInt()
windowManager?.updateViewLayout(it, layoutParams)
}
this.leftPercent = percent
}
fun setColors(textColor: String?, backgroundColor: String?) {
tv?.let {
textColor?.let { color -> it.setTextColor(Color.parseColor(rgba2argb(color))) }
backgroundColor?.let { color ->
it.background = ColorDrawable(Color.parseColor(rgba2argb(color)))
}
}
}
fun setWidth(pct: Double) {
var percent = pct.coerceIn(0.3, 1.0)
tv?.let {
val width = (percent * windowWidth).toInt()
val originalWidth = layoutParams?.width ?: 0
layoutParams?.x = if (width <= originalWidth) {
layoutParams!!.x + (originalWidth - width) / 2
} else {
layoutParams!!.x - (width - originalWidth) / 2
}.coerceAtLeast(0).coerceAtMost((windowWidth - width).toInt())
layoutParams?.width = width
windowManager?.updateViewLayout(it, layoutParams)
}
this.widthPercent = percent
}
fun setFontSize(fontSize: Float) {
tv?.textSize = fontSize
}
}
@@ -0,0 +1,190 @@
package `fun`.upup.musicfree.mp3Util
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import com.facebook.react.bridge.*
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.tag.FieldKey
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class Mp3UtilModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName() = "Mp3Util"
private fun isContentUri(uri: Uri?): Boolean {
return uri?.scheme?.equals("content", ignoreCase = true) == true
}
@ReactMethod
fun getBasicMeta(filePath: String, promise: Promise) {
try {
val uri = Uri.parse(filePath)
val mmr = MediaMetadataRetriever()
if (isContentUri(uri)) {
mmr.setDataSource(reactApplicationContext, uri)
} else {
mmr.setDataSource(filePath)
}
val properties = Arguments.createMap().apply {
putString("duration", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION))
putString("bitrate", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE))
putString("artist", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST))
putString("author", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_AUTHOR))
putString("album", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM))
putString("title", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE))
putString("date", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE))
putString("year", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR))
}
promise.resolve(properties)
} catch (e: Exception) {
promise.reject("Exception", e.message)
}
}
@ReactMethod
fun getMediaMeta(filePaths: ReadableArray, promise: Promise) {
val metas = Arguments.createArray()
val mmr = MediaMetadataRetriever()
for (i in 0 until filePaths.size()) {
try {
val filePath = filePaths.getString(i)
val uri = Uri.parse(filePath)
if (isContentUri(uri)) {
mmr.setDataSource(reactApplicationContext, uri)
} else {
mmr.setDataSource(filePath)
}
val properties = Arguments.createMap().apply {
putString("duration", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION))
putString("bitrate", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE))
putString("artist", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST))
putString("author", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_AUTHOR))
putString("album", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM))
putString("title", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE))
putString("date", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE))
putString("year", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR))
}
metas.pushMap(properties)
} catch (e: Exception) {
metas.pushNull()
}
}
try {
mmr.release()
} catch (ignored: Exception) {
}
promise.resolve(metas)
}
@ReactMethod
fun getMediaCoverImg(filePath: String, promise: Promise) {
try {
val file = File(filePath)
if (!file.exists()) {
promise.reject("File not exist", "File not exist")
return
}
val pathHashCode = file.hashCode()
if (pathHashCode == 0) {
promise.resolve(null)
return
}
val cacheDir = reactContext.cacheDir
val coverFile = File(cacheDir, "image_manager_disk_cache/$pathHashCode.jpg")
if (coverFile.exists()) {
promise.resolve(coverFile.toURI().toString())
return
}
val mmr = MediaMetadataRetriever()
mmr.setDataSource(filePath)
val coverImg = mmr.embeddedPicture
if (coverImg != null) {
val bitmap = BitmapFactory.decodeByteArray(coverImg, 0, coverImg.size)
FileOutputStream(coverFile).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
outputStream.flush()
}
promise.resolve(coverFile.toURI().toString())
} else {
promise.resolve(null)
}
mmr.release()
} catch (ignored: Exception) {
promise.reject("Error", "Got error")
}
}
@ReactMethod
fun getLyric(filePath: String, promise: Promise) {
try {
val file = File(filePath)
if (file.exists()) {
val audioFile = AudioFileIO.read(file)
val tag = audioFile.tag
val lrc = tag.getFirst(FieldKey.LYRICS)
promise.resolve(lrc)
} else {
throw IOException("File not found")
}
} catch (e: Exception) {
promise.reject("Error", e.message)
}
}
@ReactMethod
fun setMediaTag(filePath: String, meta: ReadableMap, promise: Promise) {
try {
val file = File(filePath)
if (file.exists()) {
val audioFile = AudioFileIO.read(file)
val tag = audioFile.tag
meta.getString("title")?.let { tag.setField(FieldKey.TITLE, it) }
meta.getString("artist")?.let { tag.setField(FieldKey.ARTIST, it) }
meta.getString("album")?.let { tag.setField(FieldKey.ALBUM, it) }
meta.getString("lyric")?.let { tag.setField(FieldKey.LYRICS, it) }
meta.getString("comment")?.let { tag.setField(FieldKey.COMMENT, it) }
audioFile.commit()
promise.resolve(true)
} else {
promise.reject("Error", "File Not Exist")
}
} catch (e: Exception) {
promise.reject("Error", e.message)
}
}
@ReactMethod
fun getMediaTag(filePath: String, promise: Promise) {
try {
val file = File(filePath)
if (file.exists()) {
val audioFile = AudioFileIO.read(file)
val tag = audioFile.tag
val properties = Arguments.createMap().apply {
putString("title", tag.getFirst(FieldKey.TITLE))
putString("artist", tag.getFirst(FieldKey.ARTIST))
putString("album", tag.getFirst(FieldKey.ALBUM))
putString("lyric", tag.getFirst(FieldKey.LYRICS))
putString("comment", tag.getFirst(FieldKey.COMMENT))
}
promise.resolve(properties)
} else {
promise.reject("Error", "File Not Found")
}
} catch (e: Exception) {
promise.reject("Error", e.message)
}
}
}
@@ -0,0 +1,19 @@
package `fun`.upup.musicfree.mp3Util
import android.view.View
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager
class Mp3UtilPackage : ReactPackage {
override fun createViewManagers(
reactContext: ReactApplicationContext
): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
override fun createNativeModules(
reactContext: ReactApplicationContext
): MutableList<NativeModule> = listOf(Mp3UtilModule(reactContext)).toMutableList()
}
@@ -0,0 +1,164 @@
package `fun`.upup.musicfree.utils; // replace your-apps-package-name with your apps package name
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.PowerManager
import android.provider.Settings
import android.util.DisplayMetrics
import android.view.WindowInsets
import android.view.WindowManager
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.WritableMap
import kotlin.system.exitProcess
class UtilsModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
private val reactContext: ReactApplicationContext = context;
override fun getName() = "NativeUtils"
@ReactMethod
fun exitApp() {
val activity = reactContext.currentActivity
activity?.finishAndRemoveTask()
android.os.Process.killProcess(android.os.Process.myPid())
exitProcess(0)
}
@ReactMethod
fun checkStoragePermission(promise: Promise) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
promise.resolve(Environment.isExternalStorageManager())
} else {
val readPermission = ContextCompat.checkSelfPermission(reactContext, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
val writePermission = ContextCompat.checkSelfPermission(reactContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
promise.resolve(readPermission && writePermission)
}
}
@ReactMethod
fun requestStoragePermission() {
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
data = Uri.parse("package:${reactContext.packageName}")
}
} else {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${reactContext.packageName}")
}
}
reactContext.currentActivity?.startActivity(intent)
}
@ReactMethod
fun isIgnoringBatteryOptimizations(promise: Promise) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val packageName = reactContext.packageName
val pm = reactContext.getSystemService(Context.POWER_SERVICE) as PowerManager
promise.resolve(pm.isIgnoringBatteryOptimizations(packageName))
} else {
promise.resolve(true)
}
}
@ReactMethod
fun requestIgnoreBatteryOptimizations(promise: Promise) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
val packageName = reactContext.packageName
val pm = reactContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$packageName")
if (reactContext.currentActivity != null) {
reactContext.currentActivity?.startActivity(intent)
} else {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
reactContext.startActivity(intent)
}
}
promise.resolve(true)
} catch (e: Exception) {
promise.reject(e)
}
} else {
promise.resolve(true)
}
}
@ReactMethod(isBlockingSynchronousMethod = true)
fun getWindowDimensions(): WritableMap {
val windowManager = reactApplicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val displayMetrics: DisplayMetrics = reactApplicationContext.resources.displayMetrics
val density = displayMetrics.density
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11 (API 30) 及以上使用新 API
val windowMetrics = windowManager.currentWindowMetrics
val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
val bounds = windowMetrics.bounds
val totalWidthPx = bounds.width()
val totalHeightPx = bounds.height()
val leftInsetPx = insets.left
val rightInsetPx = insets.right
val topInsetPx = insets.top
val bottomInsetPx = insets.bottom
val usableWidthPx = totalWidthPx - leftInsetPx - rightInsetPx
val usableHeightPx = totalHeightPx - topInsetPx - bottomInsetPx
val usableWidthDp = usableWidthPx / density
val usableHeightDp = usableHeightPx / density
Arguments.createMap().apply {
putDouble("width", usableWidthDp.toDouble())
putDouble("height", usableHeightDp.toDouble())
}
} else {
// Android 10 及以下使用旧 API
val display = windowManager.defaultDisplay
val realSize = android.graphics.Point()
display.getRealSize(realSize)
// 获取状态栏和导航栏高度
val resources = reactApplicationContext.resources
var statusBarHeight = 0
var navigationBarHeight = 0
// 状态栏高度
val statusBarResourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
if (statusBarResourceId > 0) {
statusBarHeight = resources.getDimensionPixelSize(statusBarResourceId)
}
// 导航栏高度
val navigationBarResourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
if (navigationBarResourceId > 0) {
navigationBarHeight = resources.getDimensionPixelSize(navigationBarResourceId)
}
val usableWidthPx = realSize.x
val usableHeightPx = realSize.y - statusBarHeight - navigationBarHeight
val usableWidthDp = usableWidthPx / density
val usableHeightDp = usableHeightPx / density
Arguments.createMap().apply {
putDouble("width", usableWidthDp.toDouble())
putDouble("height", usableHeightDp.toDouble())
}
}
}
}
@@ -0,0 +1,19 @@
package `fun`.upup.musicfree.utils
import android.view.View
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ReactShadowNode
import com.facebook.react.uimanager.ViewManager
class UtilsPackage : ReactPackage {
override fun createViewManagers(
reactContext: ReactApplicationContext
): MutableList<ViewManager<View, ReactShadowNode<*>>> = mutableListOf()
override fun createNativeModules(
reactContext: ReactApplicationContext
): MutableList<NativeModule> = listOf(UtilsModule(reactContext)).toMutableList()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB