mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat: responsive and working android home widget
This commit is contained in:
parent
a33974cd1e
commit
fba1876535
@ -133,7 +133,4 @@ dependencies {
|
|||||||
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
|
||||||
implementation 'com.google.code.gson:gson:2.11.0'
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import HomeWidgetGlanceStateDefinition
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -35,14 +36,9 @@ import androidx.glance.preview.Preview
|
|||||||
import androidx.glance.state.GlanceStateDefinition
|
import androidx.glance.state.GlanceStateDefinition
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import es.antonborri.home_widget.HomeWidgetBackgroundIntent
|
import es.antonborri.home_widget.HomeWidgetBackgroundIntent
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import oss.krtirtho.spotube.glance.models.Track
|
import oss.krtirtho.spotube.glance.models.Track
|
||||||
import oss.krtirtho.spotube.glance.widgets.TrackDetailsView
|
import oss.krtirtho.spotube.glance.widgets.TrackDetailsView
|
||||||
import oss.krtirtho.spotube.glance.widgets.TrackProgress
|
import oss.krtirtho.spotube.glance.widgets.TrackProgress
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
val serverAddressKey = ActionParameters.Key<String>("serverAddress")
|
val serverAddressKey = ActionParameters.Key<String>("serverAddress")
|
||||||
@ -83,6 +79,7 @@ class HomePlayerWidget : GlanceAppWidget() {
|
|||||||
val size = LocalSize.current
|
val size = LocalSize.current
|
||||||
|
|
||||||
val activeTrackStr = prefs.getString("activeTrack", null)
|
val activeTrackStr = prefs.getString("activeTrack", null)
|
||||||
|
|
||||||
val isPlaying = prefs.getBoolean("isPlaying", false)
|
val isPlaying = prefs.getBoolean("isPlaying", false)
|
||||||
val playbackServerAddress = prefs.getString("playbackServerAddress", null) ?: ""
|
val playbackServerAddress = prefs.getString("playbackServerAddress", null) ?: ""
|
||||||
var activeTrack: Track? = null
|
var activeTrack: Track? = null
|
||||||
@ -99,17 +96,10 @@ class HomePlayerWidget : GlanceAppWidget() {
|
|||||||
GlanceTheme {
|
GlanceTheme {
|
||||||
Scaffold {
|
Scaffold {
|
||||||
Column(
|
Column(
|
||||||
modifier = GlanceModifier.padding(
|
modifier = GlanceModifier.padding(top = 10.dp)
|
||||||
horizontal = if (size != Breakpoints.SMALL_SQUARE) 8.dp else 2.dp,
|
|
||||||
vertical = 16.dp
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
if (size == Breakpoints.HORIZONTAL_RECTANGLE) {
|
Row(verticalAlignment = Alignment.Vertical.CenterVertically) {
|
||||||
Row(
|
TrackDetailsView(activeTrack)
|
||||||
verticalAlignment = Alignment.Vertical.CenterVertically,
|
|
||||||
) { TrackDetailsView(activeTrack) }
|
|
||||||
} else {
|
|
||||||
TrackDetailsView(activeTrack)
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
if (size != Breakpoints.SMALL_SQUARE) {
|
if (size != Breakpoints.SMALL_SQUARE) {
|
||||||
@ -154,7 +144,7 @@ class HomePlayerWidget : GlanceAppWidget() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayPauseAction : InteractiveAction("toggle_playback")
|
class PlayPauseAction : InteractiveAction("toggle-playback")
|
||||||
class NextAction : InteractiveAction("next")
|
class NextAction : InteractiveAction("next")
|
||||||
class PreviousAction : InteractiveAction("previous")
|
class PreviousAction : InteractiveAction("previous")
|
||||||
|
|
||||||
@ -165,22 +155,19 @@ abstract class InteractiveAction(val command: String) : ActionCallback {
|
|||||||
glanceId: GlanceId,
|
glanceId: GlanceId,
|
||||||
parameters: ActionParameters
|
parameters: ActionParameters
|
||||||
) {
|
) {
|
||||||
val serverAddress = parameters[serverAddressKey]
|
val serverAddress = parameters[serverAddressKey] ?: ""
|
||||||
|
|
||||||
|
Log.d("HomePlayerWidget", "Sending command $command to $serverAddress")
|
||||||
|
|
||||||
if (serverAddress == null || serverAddress.isEmpty()) {
|
if (serverAddress == null || serverAddress.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val client = OkHttpClient()
|
|
||||||
val request = Request.Builder().url("http://$serverAddress/playback/$command").build()
|
|
||||||
|
|
||||||
client.newCall(request).execute().use { response ->
|
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(
|
||||||
if (response.isSuccessful) {
|
context,
|
||||||
response.body?.string()
|
Uri.parse("spotube://playback/$command?serverAddress=$serverAddress")
|
||||||
} else {
|
)
|
||||||
print("Failed to send command to server")
|
backgroundIntent.send()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
|
|||||||
data class Image(
|
data class Image(
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val url: String?
|
val path: String,
|
||||||
)
|
)
|
@ -0,0 +1,14 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.widgets
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
fun Base64ImageProvider(base64: String): ImageProvider {
|
||||||
|
var bytes = Base64.decode(base64, Base64.DEFAULT);
|
||||||
|
|
||||||
|
var bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size);
|
||||||
|
|
||||||
|
return ImageProvider(bitmap)
|
||||||
|
}
|
@ -1,15 +1,20 @@
|
|||||||
package oss.krtirtho.spotube.glance.widgets
|
package oss.krtirtho.spotube.glance.widgets
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.glance.GlanceModifier
|
import androidx.glance.GlanceModifier
|
||||||
import androidx.glance.GlanceTheme
|
import androidx.glance.GlanceTheme
|
||||||
import androidx.glance.Image
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.LocalContext
|
||||||
import androidx.glance.LocalSize
|
import androidx.glance.LocalSize
|
||||||
import androidx.glance.appwidget.ImageProvider
|
|
||||||
import androidx.glance.appwidget.cornerRadius
|
import androidx.glance.appwidget.cornerRadius
|
||||||
|
import androidx.glance.layout.Alignment
|
||||||
|
import androidx.glance.layout.Row
|
||||||
import androidx.glance.layout.Column
|
import androidx.glance.layout.Column
|
||||||
import androidx.glance.layout.ContentScale
|
import androidx.glance.layout.ContentScale
|
||||||
import androidx.glance.layout.Spacer
|
import androidx.glance.layout.Spacer
|
||||||
@ -22,25 +27,30 @@ import oss.krtirtho.spotube.glance.models.Track
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrackDetailsView(activeTrack: Track?) {
|
fun TrackDetailsView(activeTrack: Track?) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val size = LocalSize.current
|
val size = LocalSize.current
|
||||||
|
|
||||||
val artistStr = activeTrack?.artists?.map { it.name }?.joinToString(", ") ?: "<No Artist>"
|
val artistStr = activeTrack?.artists?.map { it.name }?.joinToString(", ") ?: "<No Artist>"
|
||||||
val imgUri = activeTrack?.album?.images?.get(0)?.url
|
val imgLocalPath = activeTrack?.album?.images?.get(0)?.path;
|
||||||
?: "https://placehold.co/600x400/000000/FFF.jpg";
|
|
||||||
val title = activeTrack?.name ?: "<No Track>"
|
val title = activeTrack?.name ?: "<No Track>"
|
||||||
|
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
provider = ImageProvider(uri = Uri.parse(imgUri)),
|
provider =
|
||||||
|
if (imgLocalPath == null)
|
||||||
|
ImageProvider(
|
||||||
|
BitmapFactory.decodeResource(
|
||||||
|
context.resources,
|
||||||
|
android.R.drawable.ic_delete
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else ImageProvider(BitmapFactory.decodeFile(imgLocalPath)),
|
||||||
contentDescription = "Album Art",
|
contentDescription = "Album Art",
|
||||||
modifier = GlanceModifier.cornerRadius(8.dp)
|
modifier = GlanceModifier.cornerRadius(8.dp)
|
||||||
.size(
|
.size(
|
||||||
when (size) {
|
if (size.height < 200.dp) 50.dp
|
||||||
Breakpoints.SMALL_SQUARE -> 70.dp
|
else 100.dp
|
||||||
Breakpoints.HORIZONTAL_RECTANGLE -> 100.dp
|
|
||||||
Breakpoints.BIG_SQUARE -> 150.dp
|
|
||||||
else -> 120.dp
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
contentScale = ContentScale.Fit
|
contentScale = ContentScale.Fit
|
||||||
)
|
)
|
||||||
|
@ -7,85 +7,71 @@ import androidx.glance.GlanceModifier
|
|||||||
import androidx.glance.GlanceTheme
|
import androidx.glance.GlanceTheme
|
||||||
import androidx.glance.LocalSize
|
import androidx.glance.LocalSize
|
||||||
import androidx.glance.appwidget.LinearProgressIndicator
|
import androidx.glance.appwidget.LinearProgressIndicator
|
||||||
import androidx.glance.background
|
|
||||||
import androidx.glance.currentState
|
|
||||||
import androidx.glance.layout.Column
|
import androidx.glance.layout.Column
|
||||||
import androidx.glance.layout.Row
|
import androidx.glance.layout.Row
|
||||||
import androidx.glance.layout.Spacer
|
import androidx.glance.layout.Spacer
|
||||||
import androidx.glance.layout.fillMaxWidth
|
import androidx.glance.layout.fillMaxWidth
|
||||||
import androidx.glance.layout.height
|
|
||||||
import androidx.glance.layout.size
|
import androidx.glance.layout.size
|
||||||
import androidx.glance.layout.wrapContentWidth
|
|
||||||
import androidx.glance.text.Text
|
import androidx.glance.text.Text
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import oss.krtirtho.spotube.glance.Breakpoints
|
import kotlin.math.max
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import oss.krtirtho.spotube.glance.Breakpoints
|
||||||
|
|
||||||
fun Duration.format(): String {
|
fun Duration.format(): String {
|
||||||
return this.toComponents { hour, minutes, seconds, nanoseconds ->
|
return this.toComponents { hour, minutes, seconds, nanoseconds ->
|
||||||
var paddedSeconds = seconds.toString().padStart(2, '0')
|
var paddedSeconds = seconds.toString().padStart(2, '0')
|
||||||
var paddedMinutes = minutes.toString().padStart(2, '0')
|
var paddedMinutes = minutes.toString().padStart(2, '0')
|
||||||
var paddedHour = hour.toString().padStart(2, '0')
|
var paddedHour = hour.toString().padStart(2, '0')
|
||||||
if (hour == 0L) {
|
if (hour == 0L) {
|
||||||
"$paddedMinutes:$paddedSeconds"
|
"$paddedMinutes:$paddedSeconds"
|
||||||
} else {
|
} else {
|
||||||
"$paddedHour:$paddedMinutes:$paddedSeconds"
|
"$paddedHour:$paddedMinutes:$paddedSeconds"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TrackProgress(prefs: SharedPreferences) {
|
fun TrackProgress(prefs: SharedPreferences) {
|
||||||
val size = LocalSize.current;
|
val size = LocalSize.current
|
||||||
val progress = prefs.getFloat("progress", 0.0f)
|
val position = prefs.getInt("position", 0).seconds
|
||||||
var duration = prefs.getInt("duration", 0).seconds
|
var duration = prefs.getInt("duration", 0).seconds
|
||||||
|
|
||||||
var startingTime = (duration.inWholeSeconds * progress).toLong().seconds
|
var progress = position.inWholeSeconds.toFloat() / max(duration.inWholeSeconds.toFloat(), 1.0f)
|
||||||
|
|
||||||
var textStyle = TextStyle(
|
var textStyle =
|
||||||
color = GlanceTheme.colors.onBackground,
|
TextStyle(
|
||||||
)
|
color = GlanceTheme.colors.onBackground,
|
||||||
|
)
|
||||||
|
|
||||||
if (size == Breakpoints.HORIZONTAL_RECTANGLE) {
|
if (size == Breakpoints.HORIZONTAL_RECTANGLE) {
|
||||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(text = position.format(), style = textStyle)
|
||||||
text = startingTime.format(),
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
style = textStyle
|
LinearProgressIndicator(
|
||||||
)
|
progress = progress,
|
||||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
modifier = GlanceModifier.defaultWeight(),
|
||||||
LinearProgressIndicator(
|
color = GlanceTheme.colors.primary,
|
||||||
progress = progress,
|
backgroundColor = GlanceTheme.colors.primaryContainer,
|
||||||
modifier = GlanceModifier.defaultWeight(),
|
)
|
||||||
color = GlanceTheme.colors.primary,
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
backgroundColor = GlanceTheme.colors.primaryContainer,
|
Text(text = duration.format(), style = textStyle)
|
||||||
)
|
|
||||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
|
||||||
Text(
|
|
||||||
text = duration.format(),
|
|
||||||
style = textStyle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column(modifier = GlanceModifier.fillMaxWidth()) {
|
|
||||||
LinearProgressIndicator(
|
|
||||||
progress = progress,
|
|
||||||
modifier = GlanceModifier.fillMaxWidth(),
|
|
||||||
color = GlanceTheme.colors.primary,
|
|
||||||
backgroundColor = GlanceTheme.colors.primaryContainer,
|
|
||||||
)
|
|
||||||
Spacer(modifier = GlanceModifier.size(6.dp))
|
|
||||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
|
||||||
Text(
|
|
||||||
text = startingTime.format(),
|
|
||||||
style = textStyle
|
|
||||||
)
|
|
||||||
Spacer(modifier = GlanceModifier.defaultWeight())
|
|
||||||
Text(
|
|
||||||
text = duration.format(),
|
|
||||||
style = textStyle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Column(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
color = GlanceTheme.colors.primary,
|
||||||
|
backgroundColor = GlanceTheme.colors.primaryContainer,
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
|
Text(text = position.format(), style = textStyle)
|
||||||
|
Spacer(modifier = GlanceModifier.defaultWeight())
|
||||||
|
Text(text = duration.format(), style = textStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import 'package:flutter_discord_rpc/flutter_discord_rpc.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:home_widget/home_widget.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:local_notifier/local_notifier.dart';
|
import 'package:local_notifier/local_notifier.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
@ -27,6 +28,7 @@ import 'package:spotube/hooks/configurators/use_has_touch.dart';
|
|||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
|
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
import 'package:spotube/provider/glance/glance.dart';
|
||||||
import 'package:spotube/provider/server/bonsoir.dart';
|
import 'package:spotube/provider/server/bonsoir.dart';
|
||||||
import 'package:spotube/provider/server/server.dart';
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
||||||
@ -161,6 +163,10 @@ class Spotube extends HookConsumerWidget {
|
|||||||
useEffect(() {
|
useEffect(() {
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
|
|
||||||
|
if (kIsMobile) {
|
||||||
|
HomeWidget.registerInteractivityCallback(glanceBackgroundCallback);
|
||||||
|
}
|
||||||
|
|
||||||
return () {
|
return () {
|
||||||
/// For enabling hot reload for audio player
|
/// For enabling hot reload for audio player
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
|
@ -18,6 +18,7 @@ import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
|||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/provider/glance/glance.dart';
|
||||||
import 'package:spotube/provider/server/routes/connect.dart';
|
import 'package:spotube/provider/server/routes/connect.dart';
|
||||||
import 'package:spotube/services/connectivity_adapter.dart';
|
import 'package:spotube/services/connectivity_adapter.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -39,6 +40,8 @@ class RootApp extends HookConsumerWidget {
|
|||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
||||||
|
|
||||||
|
ref.listen(glanceProvider, (_, __) {});
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
ServiceUtils.checkForUpdates(context, ref);
|
ServiceUtils.checkForUpdates(context, ref);
|
||||||
|
156
lib/provider/glance/glance.dart
Normal file
156
lib/provider/glance/glance.dart
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:home_widget/home_widget.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/provider/server/server.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
|
@pragma("vm:entry-point")
|
||||||
|
Future<void> glanceBackgroundCallback(Uri? data) async {
|
||||||
|
final logger = Logger();
|
||||||
|
try {
|
||||||
|
if (data == null ||
|
||||||
|
data.host != "playback" ||
|
||||||
|
data.pathSegments.isEmpty ||
|
||||||
|
data.queryParameters["serverAddress"] == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final command = data.pathSegments.first;
|
||||||
|
final res = await get(
|
||||||
|
Uri.parse(
|
||||||
|
"http://${data.queryParameters["serverAddress"]}/playback/$command",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception("Failed to execute command: $command\nBody: ${res.body}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("[GlanceBackgroundCallback] $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> _saveWidgetData<T>(String key, T? value) async {
|
||||||
|
try {
|
||||||
|
if (!kIsMobile) return null;
|
||||||
|
|
||||||
|
return await HomeWidget.saveWidgetData<T>(key, value);
|
||||||
|
} catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateWidget() async {
|
||||||
|
try {
|
||||||
|
if (!kIsMobile) return;
|
||||||
|
|
||||||
|
if (kIsAndroid) {
|
||||||
|
await HomeWidget.updateWidget(
|
||||||
|
androidName: 'HomePlayerWidgetReceiver',
|
||||||
|
qualifiedAndroidName:
|
||||||
|
'oss.krtirtho.spotube.glance.HomePlayerWidgetReceiver',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (kIsIOS) {
|
||||||
|
await HomeWidget.updateWidget(
|
||||||
|
name: 'HomePlayerWidgetReceiver',
|
||||||
|
iOSName: 'HomePlayerWidget',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on Exception catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final glanceProvider = Provider((ref) {
|
||||||
|
final server = ref.read(serverProvider);
|
||||||
|
|
||||||
|
server.whenData(
|
||||||
|
(value) async {
|
||||||
|
final (:server, :port) = value;
|
||||||
|
|
||||||
|
await _saveWidgetData(
|
||||||
|
"playbackServerAddress",
|
||||||
|
"${server.address.host}:$port",
|
||||||
|
);
|
||||||
|
await _updateWidget();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.listen(serverProvider, (prev, next) async {
|
||||||
|
next.whenData(
|
||||||
|
(value) async {
|
||||||
|
final (:server, :port) = value;
|
||||||
|
|
||||||
|
await _saveWidgetData(
|
||||||
|
"playbackServerAddress",
|
||||||
|
"${server.address.host}:$port",
|
||||||
|
);
|
||||||
|
await _updateWidget();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
audioPlayerProvider,
|
||||||
|
(previous, next) async {
|
||||||
|
try {
|
||||||
|
if (previous?.activeTrack != next.activeTrack &&
|
||||||
|
next.activeTrack != null) {
|
||||||
|
final jsonTrack = next.activeTrack!.toJson();
|
||||||
|
|
||||||
|
final image = next.activeTrack!.album?.images?.first;
|
||||||
|
final cachedImage =
|
||||||
|
await DefaultCacheManager().getSingleFile(image!.url!);
|
||||||
|
final data = {
|
||||||
|
...jsonTrack,
|
||||||
|
"album": {
|
||||||
|
...jsonTrack["album"],
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
...image.toJson(),
|
||||||
|
"path": cachedImage.path,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _saveWidgetData("activeTrack", jsonEncode(data));
|
||||||
|
|
||||||
|
await _updateWidget();
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final subscriptions = [
|
||||||
|
audioPlayer.playingStream.listen((playing) async {
|
||||||
|
await _saveWidgetData("isPlaying", playing);
|
||||||
|
await _updateWidget();
|
||||||
|
}),
|
||||||
|
audioPlayer.positionStream.listen((position) async {
|
||||||
|
await _saveWidgetData("position", position.inSeconds);
|
||||||
|
await _updateWidget();
|
||||||
|
}),
|
||||||
|
audioPlayer.durationStream.listen((duration) async {
|
||||||
|
await _saveWidgetData("duration", duration.inSeconds);
|
||||||
|
await _updateWidget();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
for (final subscription in subscriptions) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -14,6 +14,10 @@ final serverRouterProvider = Provider((ref) {
|
|||||||
|
|
||||||
router.get("/stream/<trackId>", playbackRoutes.getStreamTrackId);
|
router.get("/stream/<trackId>", playbackRoutes.getStreamTrackId);
|
||||||
|
|
||||||
|
router.get("/playback/toggle-playback", playbackRoutes.togglePlayback);
|
||||||
|
router.get("/playback/previous", playbackRoutes.previousTrack);
|
||||||
|
router.get("/playback/next", playbackRoutes.nextTrack);
|
||||||
|
|
||||||
router.all("/ws", connectRoutes.websocket);
|
router.all("/ws", connectRoutes.websocket);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
@ -188,6 +188,27 @@ class ServerPlaybackRoutes {
|
|||||||
return Response.internalServerError();
|
return Response.internalServerError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @get('/playback/toggle-playback')
|
||||||
|
Future<Response> togglePlayback(Request request) async {
|
||||||
|
audioPlayer.isPlaying
|
||||||
|
? await audioPlayer.pause()
|
||||||
|
: await audioPlayer.resume();
|
||||||
|
|
||||||
|
return Response.ok("Playback toggled");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @get('/playback/previous')
|
||||||
|
Future<Response> previousTrack(Request request) async {
|
||||||
|
await audioPlayer.skipToPrevious();
|
||||||
|
return Response.ok("Previous track");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @get('/playback/next')
|
||||||
|
Future<Response> nextTrack(Request request) async {
|
||||||
|
await audioPlayer.skipToNext();
|
||||||
|
return Response.ok("Next track");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final serverPlaybackRoutesProvider =
|
final serverPlaybackRoutesProvider =
|
||||||
|
Loading…
Reference in New Issue
Block a user