Initial commit: SpotiFLAC Android/iOS app
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,87 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.zarz.spotiflac">
|
||||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:label="SpotiFLAC"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
|
||||
<!-- Main launcher -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
|
||||
<!-- Handle Spotify URL sharing -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Handle Spotify deep links -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="open.spotify.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Download Service -->
|
||||
<service
|
||||
android:name=".DownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!-- flutter_local_notifications receivers -->
|
||||
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
|
||||
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Don't delete the meta-data below -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.temp_project
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.zarz.spotiflac
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
class DownloadService : Service() {
|
||||
companion object {
|
||||
const val CHANNEL_ID = "spotiflac_download_channel"
|
||||
const val NOTIFICATION_ID = 1
|
||||
const val ACTION_START = "com.zarz.spotiflac.START_DOWNLOAD"
|
||||
const val ACTION_STOP = "com.zarz.spotiflac.STOP_DOWNLOAD"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_START -> startForegroundService()
|
||||
ACTION_STOP -> stopSelf()
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Download Progress",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Shows download progress for SpotiFLAC"
|
||||
setShowBadge(false)
|
||||
}
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
val notification = createNotification("Downloading...", 0)
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
fun updateProgress(trackName: String, progress: Int) {
|
||||
val notification = createNotification(trackName, progress)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun createNotification(title: String, progress: Int): Notification {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val stopIntent = Intent(this, DownloadService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
val stopPendingIntent = PendingIntent.getService(
|
||||
this, 0, stopIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("SpotiFLAC")
|
||||
.setContentText(title)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setProgress(100, progress, progress == 0)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Cancel", stopPendingIntent)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.zarz.spotiflac
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import gobackend.Gobackend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private val CHANNEL = "com.zarz.spotiflac/backend"
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
scope.launch {
|
||||
try {
|
||||
when (call.method) {
|
||||
"parseSpotifyUrl" -> {
|
||||
val url = call.argument<String>("url") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.parseSpotifyURL(url)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getSpotifyMetadata" -> {
|
||||
val url = call.argument<String>("url") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getSpotifyMetadata(url)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"searchSpotify" -> {
|
||||
val query = call.argument<String>("query") ?: ""
|
||||
val limit = call.argument<Int>("limit") ?: 10
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.searchSpotify(query, limit.toLong())
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"checkAvailability" -> {
|
||||
val spotifyId = call.argument<String>("spotify_id") ?: ""
|
||||
val isrc = call.argument<String>("isrc") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.checkAvailability(spotifyId, isrc)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"downloadTrack" -> {
|
||||
val requestJson = call.arguments as String
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.downloadTrack(requestJson)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"downloadWithFallback" -> {
|
||||
val requestJson = call.arguments as String
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.downloadWithFallback(requestJson)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getDownloadProgress" -> {
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getDownloadProgress()
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"setDownloadDirectory" -> {
|
||||
val path = call.argument<String>("path") ?: ""
|
||||
withContext(Dispatchers.IO) {
|
||||
Gobackend.setDownloadDirectory(path)
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"checkDuplicate" -> {
|
||||
val outputDir = call.argument<String>("output_dir") ?: ""
|
||||
val isrc = call.argument<String>("isrc") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.checkDuplicate(outputDir, isrc)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"buildFilename" -> {
|
||||
val template = call.argument<String>("template") ?: ""
|
||||
val metadata = call.argument<String>("metadata") ?: "{}"
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.buildFilename(template, metadata)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"sanitizeFilename" -> {
|
||||
val filename = call.argument<String>("filename") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.sanitizeFilename(filename)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"fetchLyrics" -> {
|
||||
val spotifyId = call.argument<String>("spotify_id") ?: ""
|
||||
val trackName = call.argument<String>("track_name") ?: ""
|
||||
val artistName = call.argument<String>("artist_name") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.fetchLyrics(spotifyId, trackName, artistName)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getLyricsLRC" -> {
|
||||
val spotifyId = call.argument<String>("spotify_id") ?: ""
|
||||
val trackName = call.argument<String>("track_name") ?: ""
|
||||
val artistName = call.argument<String>("artist_name") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getLyricsLRC(spotifyId, trackName, artistName)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"embedLyricsToFile" -> {
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val lyrics = call.argument<String>("lyrics") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.embedLyricsToFile(filePath, lyrics)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
result.error("ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1a1a2e</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||