mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-25 19:27:24 +02:00
mobile: Update dependencies and adapt to changes
This commit is contained in:
parent
b14dcd67f2
commit
8752ac81dd
15 changed files with 326 additions and 826 deletions
|
|
@ -122,10 +122,12 @@ abstract class MusicusPlayback {
|
|||
///
|
||||
/// Requires [playlist] to be up to date.
|
||||
void updateCurrentTrack(int index) {
|
||||
currentIndex.add(index);
|
||||
if (index != null) {
|
||||
currentIndex.add(index);
|
||||
|
||||
if (playlist.value != null && index >= 0 && index < playlist.value.length) {
|
||||
currentTrack.add(playlist.value[index]);
|
||||
if (playlist.value != null && index >= 0 && index < playlist.value.length) {
|
||||
currentTrack.add(playlist.value[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,3 +198,8 @@ SELECT *
|
|||
FROM tracks
|
||||
WHERE recording = :id
|
||||
ORDER BY "index";
|
||||
|
||||
tracksById:
|
||||
SELECT *
|
||||
FROM tracks
|
||||
WHERE id = :id;
|
||||
|
|
|
|||
|
|
@ -26,23 +26,27 @@ apply plugin: 'kotlin-android'
|
|||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.johrpan.musicus"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -50,8 +54,6 @@ android {
|
|||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig signingConfigs.debug
|
||||
// See https://github.com/ryanheise/audio_service/blob/master/README.md#android-setup
|
||||
shrinkResources false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +64,4 @@ flutter {
|
|||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="de.johrpan.musicus">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- TODO: Actually manage obtaining this permission -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:name="${applicationName}"
|
||||
android:label="Musicus"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
|
|
@ -25,13 +31,21 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name="com.ryanheise.audioservice.AudioService">
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
android:exported="true"
|
||||
tools:ignore="Instantiatable">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
|
|
|
|||
|
|
@ -1,240 +1,12 @@
|
|||
package de.johrpan.musicus
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.annotation.NonNull
|
||||
import android.content.Context
|
||||
import com.ryanheise.audioservice.AudioServicePlugin
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant
|
||||
|
||||
class Document(private val id: String, private val name: String, private val parentId: String?, private val isDirectory: Boolean) {
|
||||
fun toMap(): Map<String, Any?> {
|
||||
return mapOf(
|
||||
"id" to id,
|
||||
"name" to name,
|
||||
"parentId" to parentId,
|
||||
"isDirectory" to isDirectory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val CHANNEL = "de.johrpan.musicus/platform"
|
||||
private val AODT_REQUEST = 0
|
||||
|
||||
private var aodtResult: MethodChannel.Result? = null
|
||||
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
if (call.method == "openTree") {
|
||||
aodtResult = result
|
||||
// We will get the result within onActivityResult
|
||||
openTree()
|
||||
} else if (call.method == "getChildren") {
|
||||
val treeUri = Uri.parse(call.argument<String>("treeUri"))
|
||||
val parentId = call.argument<String>("parentId")
|
||||
val children = getChildren(treeUri, parentId)
|
||||
result.success(children.map { it.toMap() })
|
||||
} else if (call.method == "readFile") {
|
||||
val treeUri = Uri.parse(call.argument<String>("treeUri"))
|
||||
val id = call.argument<String>("id")!!
|
||||
result.success(readFile(treeUri, id))
|
||||
} else if (call.method == "getUriByName") {
|
||||
val treeUri = Uri.parse(call.argument<String>("treeUri"))
|
||||
val parentId = call.argument<String>("parentId")!!
|
||||
val fileName = call.argument<String>("fileName")!!
|
||||
result.success(getUriByName(treeUri, parentId, fileName).toString())
|
||||
} else if (call.method == "readFileByName") {
|
||||
val treeUri = Uri.parse(call.argument<String>("treeUri"))
|
||||
val parentId = call.argument<String>("parentId")!!
|
||||
val fileName = call.argument<String>("fileName")!!
|
||||
result.success(readFileByName(treeUri, parentId, fileName))
|
||||
} else if (call.method == "writeFileByName") {
|
||||
val treeUri = Uri.parse(call.argument<String>("treeUri"))
|
||||
val parentId = call.argument<String>("parentId")!!
|
||||
val fileName = call.argument<String>("fileName")!!
|
||||
val content = call.argument<String>("content")!!
|
||||
writeFileByName(treeUri, parentId, fileName, content)
|
||||
result.success(null)
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode == AODT_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK && data?.data != null) {
|
||||
// Drop all old URIs
|
||||
contentResolver.persistedUriPermissions.forEach {
|
||||
contentResolver.releasePersistableUriPermission(it.uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
|
||||
// We already checked for null
|
||||
val uri = data.data!!
|
||||
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
|
||||
aodtResult?.success(uri.toString())
|
||||
} else {
|
||||
aodtResult?.success(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a document tree using the storage access framework
|
||||
*
|
||||
* The result is handled within [onActivityResult]
|
||||
*/
|
||||
private fun openTree() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
|
||||
startActivityForResult(intent, AODT_REQUEST)
|
||||
}
|
||||
|
||||
/**
|
||||
* List children of a directory
|
||||
*
|
||||
* @param treeUri The treeUri from the ACTION_OPEN_DOCUMENT_TREE request
|
||||
* @param parentId Document ID of the parent directory or null for the top level directory
|
||||
* @return List of directories and files within the directory
|
||||
*/
|
||||
private fun getChildren(treeUri: Uri, parentId: String?): List<Document> {
|
||||
val realParentId = parentId ?: DocumentsContract.getTreeDocumentId(treeUri)
|
||||
val children = ArrayList<Document>()
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, realParentId)
|
||||
|
||||
val cursor = contentResolver.query(
|
||||
childrenUri,
|
||||
arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE),
|
||||
null, null, null)
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getString(0)
|
||||
val name = cursor.getString(1)
|
||||
val isDirectory = cursor.getString(2) == DocumentsContract.Document.MIME_TYPE_DIR
|
||||
|
||||
// Use parentId here to let the consumer know that we are at the top level.
|
||||
children.add(Document(id, name, parentId, isDirectory))
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for a file by name
|
||||
*
|
||||
* @param treeUri The treeUri from the ACTION_OPEN_DOCUMENT_TREE request
|
||||
* @param parentId The directory in which the file is searched for
|
||||
* @param fileName Name of the file
|
||||
* @return The URI of the file or null
|
||||
*/
|
||||
private fun getUriByName(treeUri: Uri, parentId: String, fileName: String): Uri? {
|
||||
var uri: Uri? = null
|
||||
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentId)
|
||||
val projection = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
|
||||
// The file system provider doesn't support a select clause.
|
||||
val cursor = contentResolver.query(childrenUri, projection, null, null, null)
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getString(0)
|
||||
val name = cursor.getString(1)
|
||||
|
||||
if (name == fileName) {
|
||||
uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, id)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
return uri
|
||||
}
|
||||
|
||||
/**
|
||||
* Read content of a file
|
||||
*
|
||||
* @param treeUri The URI from ACTION_OPEN_DOCUMENT_TREE
|
||||
* @param id The document ID of the file
|
||||
* @return File content or null
|
||||
*/
|
||||
private fun readFile(treeUri: Uri, id: String): String? {
|
||||
val uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, id)
|
||||
|
||||
// TODO: Handle errors.
|
||||
val input = contentResolver.openInputStream(uri)!!
|
||||
val result = input.reader().readText()
|
||||
input.close()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Read content of a file by name
|
||||
*
|
||||
* @param treeUri The URI from ACTION_OPEN_DOCUMENT_TREE
|
||||
* @param parentId Document ID of the parent directory
|
||||
* @param fileName Name of the file
|
||||
* @return File content or null
|
||||
*/
|
||||
private fun readFileByName(treeUri: Uri, parentId: String, fileName: String): String? {
|
||||
var uri = getUriByName(treeUri, parentId, fileName)
|
||||
|
||||
return if (uri != null) {
|
||||
// TODO: Handle errors.
|
||||
val input = contentResolver.openInputStream(uri)!!
|
||||
val result = input.reader().readText()
|
||||
input.close()
|
||||
|
||||
return result
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to file by name
|
||||
*
|
||||
* The file will always have the MIME type application/json.
|
||||
*
|
||||
* @param treeUri The URI from ACTION_OPEN_DOCUMENT_TREE
|
||||
* @param parentId Document ID of the parent directory
|
||||
* @param fileName Name of the file
|
||||
* @param content Content to write
|
||||
* @return File content or null
|
||||
*/
|
||||
private fun writeFileByName(treeUri: Uri, parentId: String, fileName: String, content: String) {
|
||||
var uri = getUriByName(treeUri, parentId, fileName);
|
||||
|
||||
if (uri == null) {
|
||||
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, parentId)
|
||||
uri = DocumentsContract.createDocument(contentResolver, parentUri, "application/json", fileName)
|
||||
}
|
||||
|
||||
// TODO: Handle errors.
|
||||
val output = contentResolver.openOutputStream(uri!!)!!;
|
||||
val writer = output.writer()
|
||||
writer.write(content)
|
||||
writer.close()
|
||||
output.close()
|
||||
override fun provideFlutterEngine(context: Context): FlutterEngine {
|
||||
return AudioServicePlugin.getFlutterEngine(context);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
mobile/android/app/src/main/res/raw/keep.xml
Normal file
3
mobile/android/app/src/main/res/raw/keep.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:keep="@drawable/*" />
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.6.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
include ':app'
|
||||
|
||||
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
def plugins = new Properties()
|
||||
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
|
||||
if (pluginsFile.exists()) {
|
||||
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
|
||||
}
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
plugins.each { name, path ->
|
||||
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
|
||||
include ":$name"
|
||||
project(":$name").projectDir = pluginDirectory
|
||||
}
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:musicus_common/musicus_common.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart' as pp;
|
||||
|
||||
import 'settings.dart';
|
||||
import 'platform.dart';
|
||||
import 'playback.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
|
|
@ -14,12 +12,11 @@ Future<void> main() async {
|
|||
final dir = await pp.getApplicationDocumentsDirectory();
|
||||
final dbPath = p.join(dir.path, 'db.sqlite');
|
||||
|
||||
runApp(AudioServiceWidget(
|
||||
child: MusicusApp(
|
||||
runApp(
|
||||
MusicusApp(
|
||||
dbPath: dbPath,
|
||||
settingsStorage: SettingsStorage(),
|
||||
platform: MusicusAndroidPlatform(),
|
||||
playback: Playback(),
|
||||
playback: MusicusMobilePlayback(),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:musicus_common/musicus_common.dart';
|
||||
|
||||
class MusicusAndroidPlatform extends MusicusPlatform {
|
||||
static const _platform = MethodChannel('de.johrpan.musicus/platform');
|
||||
|
||||
@override
|
||||
Future<String> chooseBasePath() async {
|
||||
return await _platform.invokeMethod<String>('openTree');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Document>> getChildren(String parentId) async {
|
||||
final List<Map<dynamic, dynamic>> childrenJson =
|
||||
await _platform.invokeListMethod(
|
||||
'getChildren',
|
||||
{
|
||||
'treeUri': basePath,
|
||||
'parentId': parentId,
|
||||
},
|
||||
);
|
||||
|
||||
return childrenJson
|
||||
.map((childJson) => Document.fromJson(childJson))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getIdentifier(String parentId, String fileName) async {
|
||||
return await _platform.invokeMethod(
|
||||
'getUriByName',
|
||||
{
|
||||
'treeUri': basePath,
|
||||
'parentId': parentId,
|
||||
'fileName': fileName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> readDocument(String id) async {
|
||||
return await _platform.invokeMethod(
|
||||
'readFile',
|
||||
{
|
||||
'treeUri': basePath,
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> readDocumentByName(String parentId, String fileName) async {
|
||||
return await _platform.invokeMethod(
|
||||
'readFileByName',
|
||||
{
|
||||
'treeUri': basePath,
|
||||
'parentId': parentId,
|
||||
'fileName': fileName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> writeDocumentByName(
|
||||
String parentId, String fileName, String contents) async {
|
||||
await _platform.invokeMethod(
|
||||
'writeFileByName',
|
||||
{
|
||||
'treeUri': basePath,
|
||||
'parentId': parentId,
|
||||
'fileName': fileName,
|
||||
'content': contents,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,535 +1,317 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:moor/isolate.dart';
|
||||
import 'package:musicus_client/musicus_client.dart';
|
||||
import 'package:musicus_common/musicus_common.dart';
|
||||
import 'package:musicus_player/musicus_player.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
const _portName = 'playbackService';
|
||||
class MusicusMobilePlayback extends MusicusPlayback {
|
||||
AudioHandler audioHandler;
|
||||
MusicusLibrary library;
|
||||
|
||||
/// Entrypoint for the playback service.
|
||||
void _playbackServiceEntrypoint() {
|
||||
AudioServiceBackground.run(() => _PlaybackService());
|
||||
}
|
||||
@override
|
||||
Future<void> setup(MusicusLibrary musicusLibrary) async {
|
||||
library = musicusLibrary;
|
||||
|
||||
class Playback extends MusicusPlayback {
|
||||
StreamSubscription _playbackServiceStateSubscription;
|
||||
|
||||
/// Start playback service.
|
||||
Future<void> _start() async {
|
||||
if (!AudioService.running) {
|
||||
await AudioService.start(
|
||||
backgroundTaskEntrypoint: _playbackServiceEntrypoint,
|
||||
audioHandler = await AudioService.init(
|
||||
builder: () => MusicusAudioHandler(musicusLibrary),
|
||||
config: AudioServiceConfig(
|
||||
androidNotificationChannelId: 'de.johrpan.musicus.channel.audio',
|
||||
androidNotificationChannelName: 'Musicus playback',
|
||||
androidNotificationChannelDescription:
|
||||
'Keeps Musicus playing in the background',
|
||||
androidNotificationIcon: 'drawable/ic_notification',
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
active.add(true);
|
||||
}
|
||||
listen();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setup() async {
|
||||
if (_playbackServiceStateSubscription != null) {
|
||||
_playbackServiceStateSubscription.cancel();
|
||||
}
|
||||
|
||||
// We will receive updated state information from the playback service,
|
||||
// which runs in its own isolate, through this port.
|
||||
final receivePort = ReceivePort();
|
||||
receivePort.asBroadcastStream(
|
||||
onListen: (subscription) {
|
||||
_playbackServiceStateSubscription = subscription;
|
||||
},
|
||||
).listen((msg) {
|
||||
// If state is null, the background audio service has stopped.
|
||||
if (msg == null) {
|
||||
dispose();
|
||||
} else {
|
||||
if (!active.value) {
|
||||
active.add(true);
|
||||
}
|
||||
|
||||
if (msg is _StatusMessage) {
|
||||
playing.add(msg.playing);
|
||||
} else if (msg is _PositionMessage) {
|
||||
updatePosition(msg.positionMs);
|
||||
} else if (msg is _TrackMessage) {
|
||||
updateCurrentTrack(msg.currentTrack);
|
||||
updateDuration(msg.positionMs, msg.durationMs);
|
||||
} else if (msg is _PlaylistMessage) {
|
||||
playlist.add(msg.playlist);
|
||||
updateCurrentTrack(msg.currentTrack);
|
||||
updateDuration(msg.positionMs, msg.durationMs);
|
||||
}
|
||||
Future<void> listen() async {
|
||||
audioHandler.customEvent.listen((event) {
|
||||
if (event != null && event is PlaylistEvent) {
|
||||
playlist.add(event.playlist);
|
||||
}
|
||||
});
|
||||
|
||||
IsolateNameServer.removePortNameMapping(_portName);
|
||||
IsolateNameServer.registerPortWithName(receivePort.sendPort, _portName);
|
||||
audioHandler.playbackState.listen((event) {
|
||||
if (event != null) {
|
||||
playing.add(event.playing);
|
||||
updatePosition(event.position);
|
||||
updateCurrentTrack(event.queueIndex);
|
||||
}
|
||||
});
|
||||
|
||||
if (AudioService.running) {
|
||||
active.add(true);
|
||||
audioHandler.mediaItem.listen((event) {
|
||||
if (event != null) {
|
||||
updateDuration(event.duration);
|
||||
}
|
||||
});
|
||||
|
||||
// Instruct the background service to send its current state. This will
|
||||
// by handled in the listeners, that were already set in the constructor.
|
||||
AudioService.customAction('sendState');
|
||||
}
|
||||
await audioHandler.customAction('sendState');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addTracks(List<InternalTrack> tracks) async {
|
||||
if (!AudioService.running) {
|
||||
await _start();
|
||||
}
|
||||
|
||||
await AudioService.customAction('addTracks', jsonEncode(tracks));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeTrack(int index) async {
|
||||
if (AudioService.running) {
|
||||
await AudioService.customAction('removeTrack', index);
|
||||
}
|
||||
Future<void> addTracks(List<String> tracks) async {
|
||||
await audioHandler.customAction('addTracks', {'tracks': tracks});
|
||||
active.add(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> playPause() async {
|
||||
if (active.value) {
|
||||
if (playing.value) {
|
||||
await AudioService.pause();
|
||||
} else {
|
||||
await AudioService.play();
|
||||
}
|
||||
if (playing.value) {
|
||||
await audioHandler.pause();
|
||||
} else {
|
||||
await audioHandler.play();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeTrack(int index) async {
|
||||
await audioHandler.customAction('removeTrack', {'index': index});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seekTo(double pos) async {
|
||||
if (active.value && pos >= 0.0 && pos <= 1.0) {
|
||||
final durationMs = duration.value.inMilliseconds;
|
||||
await AudioService.seekTo((pos * durationMs).floor());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
if (AudioService.running) {
|
||||
await AudioService.skipToPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
if (AudioService.running) {
|
||||
await AudioService.skipToNext();
|
||||
if (pos >= 0.0 && pos <= 1.0) {
|
||||
final durationMs = audioHandler.mediaItem.value.duration.inMilliseconds;
|
||||
await audioHandler
|
||||
.seek(Duration(milliseconds: (pos * durationMs).floor()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipTo(int index) async {
|
||||
if (AudioService.running) {
|
||||
await AudioService.customAction('skipTo', index);
|
||||
await audioHandler.skipToQueueItem(index);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
await audioHandler.skipToNext();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
await audioHandler.skipToPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
class MusicusAudioHandler extends BaseAudioHandler {
|
||||
final MusicusLibrary library;
|
||||
|
||||
MusicusPlayer player;
|
||||
|
||||
List<String> playlist = [];
|
||||
int currentTrack = -1;
|
||||
int durationMs = 1000;
|
||||
bool playing = false;
|
||||
|
||||
MusicusAudioHandler(this.library) {
|
||||
player = MusicusPlayer(onComplete: () async {
|
||||
if (currentTrack < playlist.length - 1) {
|
||||
await skipToNext();
|
||||
} else {
|
||||
playing = false;
|
||||
await sendState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
await player.play();
|
||||
playing = true;
|
||||
await sendState();
|
||||
keepSendingPosition();
|
||||
}
|
||||
|
||||
Future<void> pause() async {
|
||||
await player.pause();
|
||||
playing = false;
|
||||
await sendState();
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
playlist.clear();
|
||||
await player.stop();
|
||||
|
||||
super.stop();
|
||||
}
|
||||
|
||||
Future<void> seek(Duration position) async {
|
||||
await player.seekTo(position.inMilliseconds);
|
||||
await sendState();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
if (currentTrack > 0 && currentTrack < playlist.length) {
|
||||
await skipToQueueItem(currentTrack - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_playbackServiceStateSubscription.cancel();
|
||||
Future<void> skipToNext() async {
|
||||
if (currentTrack >= 0 && currentTrack < playlist.length - 1) {
|
||||
await skipToQueueItem(currentTrack + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A message from the playback service to the UI.
|
||||
abstract class _Message {}
|
||||
@override
|
||||
Future<void> skipToQueueItem(int index) async {
|
||||
if (index >= 0 && index < playlist.length) {
|
||||
currentTrack = index;
|
||||
final track = await library.db.tracksById(playlist[index]).getSingle();
|
||||
durationMs = await player.setUri(p.join(library.basePath, track.path));
|
||||
|
||||
/// Playback status update.
|
||||
class _StatusMessage extends _Message {
|
||||
/// Whether the player is playing (or paused).
|
||||
final bool playing;
|
||||
await sendState();
|
||||
await sendMediaItem();
|
||||
}
|
||||
}
|
||||
|
||||
/// Playback position in milliseconds.
|
||||
final int positionMs;
|
||||
@override
|
||||
Future<void> customAction(String name, [Map<String, dynamic> extras]) async {
|
||||
if (name == 'sendState') {
|
||||
await sendPlaylist();
|
||||
await sendMediaItem();
|
||||
await sendState();
|
||||
} else if (name == 'addTracks') {
|
||||
await addTracks(extras['tracks']);
|
||||
} else if (name == 'removeTrack') {
|
||||
await removeTrack(extras['index']);
|
||||
}
|
||||
}
|
||||
|
||||
_StatusMessage({
|
||||
this.playing,
|
||||
this.positionMs,
|
||||
});
|
||||
}
|
||||
Future<void> addTracks(List<String> tracks) async {
|
||||
if (tracks != null && tracks.isNotEmpty) {
|
||||
final wasEmpty = playlist.isEmpty;
|
||||
|
||||
/// The playback position has changed.
|
||||
///
|
||||
/// This could be due to seeking or because time progressed.
|
||||
class _PositionMessage extends _Message {
|
||||
/// Playback position in milliseconds.
|
||||
final int positionMs;
|
||||
playlist.addAll(tracks);
|
||||
await sendPlaylist();
|
||||
|
||||
_PositionMessage({
|
||||
this.positionMs,
|
||||
});
|
||||
}
|
||||
|
||||
/// The current track has changed.
|
||||
///
|
||||
/// This also notifies about the playback position, as the old position could be
|
||||
/// behind the new duration.
|
||||
class _TrackMessage extends _Message {
|
||||
/// Index of the new track within the playlist.
|
||||
final int currentTrack;
|
||||
|
||||
/// Duration of the new track in milliseconds.
|
||||
final int durationMs;
|
||||
|
||||
/// Playback position in milliseconds.
|
||||
final int positionMs;
|
||||
|
||||
_TrackMessage({
|
||||
this.currentTrack,
|
||||
this.durationMs,
|
||||
this.positionMs,
|
||||
});
|
||||
}
|
||||
|
||||
/// The playlist was changed.
|
||||
///
|
||||
/// This also notifies about the current track, as the old index could be out of
|
||||
/// range in the new playlist.
|
||||
class _PlaylistMessage extends _Message {
|
||||
/// The new playlist.
|
||||
final List<InternalTrack> playlist;
|
||||
|
||||
/// The current track.
|
||||
final int currentTrack;
|
||||
|
||||
/// Duration of the current track in milliseconds.
|
||||
final int durationMs;
|
||||
|
||||
/// Playback position in milliseconds.
|
||||
final int positionMs;
|
||||
|
||||
_PlaylistMessage({
|
||||
this.playlist,
|
||||
this.currentTrack,
|
||||
this.durationMs,
|
||||
this.positionMs,
|
||||
});
|
||||
}
|
||||
|
||||
class _PlaybackService extends BackgroundAudioTask {
|
||||
/// The interval between playback position updates in milliseconds.
|
||||
static const positionUpdateInterval = 250;
|
||||
|
||||
static const playControl = MediaControl(
|
||||
androidIcon: 'drawable/ic_play',
|
||||
label: 'Play',
|
||||
action: MediaAction.play,
|
||||
);
|
||||
|
||||
static const pauseControl = MediaControl(
|
||||
androidIcon: 'drawable/ic_pause',
|
||||
label: 'Pause',
|
||||
action: MediaAction.pause,
|
||||
);
|
||||
|
||||
static const stopControl = MediaControl(
|
||||
androidIcon: 'drawable/ic_stop',
|
||||
label: 'Stop',
|
||||
action: MediaAction.stop,
|
||||
);
|
||||
|
||||
final _completer = Completer();
|
||||
final _loading = Completer();
|
||||
final List<InternalTrack> _playlist = [];
|
||||
|
||||
MusicusClientDatabase db;
|
||||
MusicusPlayer _player;
|
||||
int _currentTrack = 0;
|
||||
bool _playing = false;
|
||||
int _durationMs = 1000;
|
||||
|
||||
_PlaybackService() {
|
||||
_player = MusicusPlayer(onComplete: () async {
|
||||
if (_currentTrack < _playlist.length - 1) {
|
||||
await _setCurrentTrack(_currentTrack + 1);
|
||||
_sendTrack();
|
||||
if (wasEmpty) {
|
||||
await skipToQueueItem(0);
|
||||
await play();
|
||||
} else {
|
||||
_playing = false;
|
||||
_sendStatus();
|
||||
_setState();
|
||||
await sendState();
|
||||
}
|
||||
});
|
||||
|
||||
_load();
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize database.
|
||||
Future<void> _load() async {
|
||||
final moorPort = IsolateNameServer.lookupPortByName('moor');
|
||||
final moorIsolate = MoorIsolate.fromConnectPort(moorPort);
|
||||
db = MusicusClientDatabase.connect(connection: await moorIsolate.connect());
|
||||
_loading.complete();
|
||||
Future<void> removeTrack(int index) async {
|
||||
if (index >= 0 && index < playlist.length) {
|
||||
playlist.removeAt(index);
|
||||
|
||||
if (playlist.isNotEmpty) {
|
||||
if (currentTrack == index) {
|
||||
await skipToQueueItem(index);
|
||||
} else if (currentTrack > index) {
|
||||
currentTrack--;
|
||||
}
|
||||
}
|
||||
|
||||
await sendPlaylist();
|
||||
await sendState();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the audio service status for the system.
|
||||
Future<void> _setState() async {
|
||||
final positionMs = await _player.getPosition() ?? 0;
|
||||
final updateTime = DateTime.now().millisecondsSinceEpoch;
|
||||
Future<void> sendPlaylist() async {
|
||||
customEvent.add(PlaylistEvent(playlist));
|
||||
}
|
||||
|
||||
AudioServiceBackground.setState(
|
||||
controls:
|
||||
_playing ? [pauseControl, stopControl] : [playControl, stopControl],
|
||||
basicState:
|
||||
_playing ? BasicPlaybackState.playing : BasicPlaybackState.paused,
|
||||
position: positionMs,
|
||||
updateTime: updateTime,
|
||||
);
|
||||
Future<void> sendState() async {
|
||||
List<MediaControl> controls = [];
|
||||
Set<MediaAction> actions = {};
|
||||
|
||||
if (_playlist.isNotEmpty) {
|
||||
await _loading.future;
|
||||
if (playlist.isNotEmpty) {
|
||||
if (currentTrack < 0 || currentTrack >= playlist.length) {
|
||||
currentTrack = 0;
|
||||
}
|
||||
|
||||
final track = _playlist[_currentTrack];
|
||||
final recordingInfo = await db.getRecording(track.track.recordingId);
|
||||
final workInfo = await db.getWork(recordingInfo.recording.work);
|
||||
if (currentTrack > 0) {
|
||||
controls.add(MediaControl.skipToPrevious);
|
||||
}
|
||||
|
||||
final title = workInfo.composers
|
||||
.map((p) => '${p.firstName} ${p.lastName}')
|
||||
.join(', ');
|
||||
if (playing) {
|
||||
controls.add(MediaControl.pause);
|
||||
} else {
|
||||
controls.add(MediaControl.play);
|
||||
}
|
||||
|
||||
final subtitleBuffer = StringBuffer(workInfo.work.title);
|
||||
if (currentTrack < playlist.length - 1) {
|
||||
controls.add(MediaControl.skipToNext);
|
||||
}
|
||||
|
||||
final partIds = track.track.partIds;
|
||||
if (partIds.isNotEmpty) {
|
||||
subtitleBuffer.write(': ');
|
||||
actions.add(MediaAction.seek);
|
||||
} else {
|
||||
currentTrack = -1;
|
||||
}
|
||||
|
||||
final section = workInfo.sections.lastWhere(
|
||||
(s) => s.beforePartIndex <= partIds[0],
|
||||
orElse: () => null,
|
||||
);
|
||||
playbackState.add(PlaybackState(
|
||||
processingState: AudioProcessingState.ready,
|
||||
playing: playing,
|
||||
controls: controls,
|
||||
systemActions: actions,
|
||||
updatePosition: Duration(milliseconds: await player.getPosition()),
|
||||
queueIndex: currentTrack,
|
||||
));
|
||||
}
|
||||
|
||||
if (section != null) {
|
||||
subtitleBuffer.write(section.title);
|
||||
Future<void> sendMediaItem() async {
|
||||
if (currentTrack >= 0 && currentTrack < playlist.length) {
|
||||
final track =
|
||||
await library.db.tracksById(playlist[currentTrack]).getSingle();
|
||||
|
||||
final recording =
|
||||
await library.db.recordingById(track.recording).getSingle();
|
||||
|
||||
final workInfo = await library.db.getWork(recording.work);
|
||||
|
||||
final partIds = track.workParts
|
||||
.split(',')
|
||||
.where((p) => p.isNotEmpty)
|
||||
.map((p) => int.parse(p))
|
||||
.toList();
|
||||
|
||||
String title;
|
||||
String subtitle;
|
||||
|
||||
if (workInfo != null) {
|
||||
title = '${workInfo.composer.firstName} ${workInfo.composer.lastName}';
|
||||
|
||||
final subtitleBuffer = StringBuffer(workInfo.work.title);
|
||||
|
||||
if (partIds.isNotEmpty) {
|
||||
subtitleBuffer.write(': ');
|
||||
subtitleBuffer
|
||||
.write(partIds.map((i) => workInfo.parts[i].title).join(', '));
|
||||
}
|
||||
|
||||
subtitleBuffer
|
||||
.write(partIds.map((i) => workInfo.parts[i].part.title).join(', '));
|
||||
subtitle = subtitleBuffer.toString();
|
||||
} else {
|
||||
title = '...';
|
||||
subtitle = '...';
|
||||
}
|
||||
|
||||
final subtitle = subtitleBuffer.toString();
|
||||
|
||||
AudioServiceBackground.setMediaItem(MediaItem(
|
||||
id: track.identifier,
|
||||
album: subtitle,
|
||||
title: title,
|
||||
displayTitle: title,
|
||||
displaySubtitle: subtitle,
|
||||
mediaItem.add(MediaItem(
|
||||
id: track.id,
|
||||
title: subtitle,
|
||||
album: title,
|
||||
duration: Duration(milliseconds: durationMs),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message to the UI.
|
||||
void _sendMsg(_Message msg) {
|
||||
final sendPort = IsolateNameServer.lookupPortByName(_portName);
|
||||
sendPort?.send(msg);
|
||||
}
|
||||
|
||||
/// Notify the UI about the current playback status.
|
||||
Future<void> _sendStatus() async {
|
||||
_sendMsg(_StatusMessage(
|
||||
playing: _playing,
|
||||
positionMs: await _player.getPosition(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Notify the UI about the current playback position.
|
||||
Future<void> _sendPosition() async {
|
||||
_sendMsg(_PositionMessage(
|
||||
positionMs: await _player.getPosition(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Notify the UI about the current track.
|
||||
Future<void> _sendTrack() async {
|
||||
_sendMsg(_TrackMessage(
|
||||
currentTrack: _currentTrack,
|
||||
durationMs: _durationMs,
|
||||
positionMs: await _player.getPosition(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Notify the UI about the current playlist.
|
||||
Future<void> _sendPlaylist() async {
|
||||
_sendMsg(_PlaylistMessage(
|
||||
playlist: _playlist,
|
||||
currentTrack: _currentTrack,
|
||||
durationMs: _durationMs,
|
||||
positionMs: await _player.getPosition(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Notify the UI of the new playback position periodically.
|
||||
Future<void> _updatePosition() async {
|
||||
while (_playing) {
|
||||
_sendPosition();
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: positionUpdateInterval));
|
||||
Future<void> keepSendingPosition() async {
|
||||
while (playing) {
|
||||
sendState();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current track, update the player and notify the system.
|
||||
Future<void> _setCurrentTrack(int index) async {
|
||||
_currentTrack = index;
|
||||
_durationMs = await _player.setUri(_playlist[_currentTrack].identifier);
|
||||
_setState();
|
||||
}
|
||||
|
||||
/// Add [tracks] to the playlist.
|
||||
Future<void> _addTracks(List<InternalTrack> tracks) async {
|
||||
final play = _playlist.isEmpty;
|
||||
|
||||
_playlist.addAll(tracks);
|
||||
if (play) {
|
||||
await _setCurrentTrack(0);
|
||||
}
|
||||
|
||||
_sendPlaylist();
|
||||
}
|
||||
|
||||
/// Remove the track at [index] from the playlist.
|
||||
///
|
||||
/// If it was the current track, the next track will be played.
|
||||
Future<void> _removeTrack(int index) async {
|
||||
if (index >= 0 && index < _playlist.length) {
|
||||
_playlist.removeAt(index);
|
||||
|
||||
if (_playlist.isEmpty) {
|
||||
onStop();
|
||||
} else {
|
||||
if (_currentTrack == index) {
|
||||
await _setCurrentTrack(index);
|
||||
} else if (_currentTrack > index) {
|
||||
_currentTrack--;
|
||||
}
|
||||
|
||||
_sendPlaylist();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Jump to the beginning of the track with the index [index].
|
||||
Future<void> _skipTo(int index) async {
|
||||
if (index >= 0 && index < _playlist.length) {
|
||||
await _setCurrentTrack(index);
|
||||
_sendTrack();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onStart() async {
|
||||
_setState();
|
||||
await _completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onCustomAction(String name, dynamic arguments) async {
|
||||
super.onCustomAction(name, arguments);
|
||||
|
||||
// addTracks expects a List<Map<String, dynamic>> as its argument.
|
||||
// skipTo and removeTrack expect an integer as their argument.
|
||||
if (name == 'addTracks') {
|
||||
final tracksJson = jsonDecode(arguments);
|
||||
final List<InternalTrack> tracks = List.castFrom(
|
||||
tracksJson.map((j) => InternalTrack.fromJson(j)).toList());
|
||||
|
||||
_addTracks(tracks);
|
||||
} else if (name == 'removeTrack') {
|
||||
final index = arguments as int;
|
||||
_removeTrack(index);
|
||||
} else if (name == 'skipTo') {
|
||||
final index = arguments as int;
|
||||
_skipTo(index);
|
||||
} else if (name == 'sendState') {
|
||||
_sendPlaylist();
|
||||
_sendStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onPlay() {
|
||||
super.onPlay();
|
||||
|
||||
_player.play();
|
||||
_playing = true;
|
||||
|
||||
_sendStatus();
|
||||
_updatePosition();
|
||||
_setState();
|
||||
}
|
||||
|
||||
@override
|
||||
void onPause() {
|
||||
super.onPause();
|
||||
|
||||
_player.pause();
|
||||
_playing = false;
|
||||
|
||||
_sendStatus();
|
||||
_setState();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onSeekTo(int position) async {
|
||||
super.onSeekTo(position);
|
||||
|
||||
await _player.seekTo(position);
|
||||
|
||||
_sendPosition();
|
||||
_setState();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onSkipToNext() async {
|
||||
super.onSkipToNext();
|
||||
|
||||
if (_playlist.length > 1 && _currentTrack < _playlist.length - 1) {
|
||||
await _setCurrentTrack(_currentTrack + 1);
|
||||
_sendTrack();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onSkipToPrevious() async {
|
||||
super.onSkipToPrevious();
|
||||
|
||||
// If more than five seconds of the current track have been played, go back
|
||||
// to its beginning, else, switch to the previous track.
|
||||
if (await _player.getPosition() > 5000) {
|
||||
await _setCurrentTrack(_currentTrack);
|
||||
_sendTrack();
|
||||
} else if (_playlist.length > 1 && _currentTrack > 0) {
|
||||
await _setCurrentTrack(_currentTrack - 1);
|
||||
_sendTrack();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onStop() {
|
||||
_player.stop();
|
||||
|
||||
AudioServiceBackground.setState(
|
||||
controls: [],
|
||||
basicState: BasicPlaybackState.stopped,
|
||||
);
|
||||
|
||||
_sendMsg(null);
|
||||
|
||||
// This will end onStart.
|
||||
_completer.complete();
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistEvent {
|
||||
final List<String> playlist;
|
||||
PlaylistEvent(this.playlist);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
name: musicus
|
||||
version: 0.1.0
|
||||
description: The classical music player and organizer.
|
||||
author: Elias Projahn <johrpan@gmail.com>
|
||||
homepage: https://musicus.org
|
||||
repository: https://github.com/johrpan/musicus
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.3.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
audio_service:
|
||||
audio_service: ^0.18.4
|
||||
drift: ^1.0.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
musicus_common:
|
||||
path: ../common
|
||||
musicus_database:
|
||||
path: ../database
|
||||
musicus_player:
|
||||
path: ../player
|
||||
path:
|
||||
path_provider:
|
||||
shared_preferences:
|
||||
sqlite3_flutter_libs: ^0.5.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ group 'de.johrpan.musicus_player'
|
|||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.6.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ buildscript {
|
|||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,17 +25,24 @@ apply plugin: 'com.android.library'
|
|||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 31
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
}
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
Loading…
Add table
Add a link
Reference in a new issue