mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: style widget player and add intent and callbacks on action
This commit is contained in:
parent
c1b9de5099
commit
a33974cd1e
@ -109,6 +109,9 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources.excludes += "DebugProbesKt.bin"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
@ -124,5 +127,13 @@ dependencies {
|
|||||||
implementation "androidx.glance:glance-appwidget:$glanceVersion"
|
implementation "androidx.glance:glance-appwidget:$glanceVersion"
|
||||||
implementation "androidx.glance:glance-appwidget-preview:$glanceVersion"
|
implementation "androidx.glance:glance-appwidget-preview:$glanceVersion"
|
||||||
implementation "androidx.glance:glance-preview:$glanceVersion"
|
implementation "androidx.glance:glance-preview:$glanceVersion"
|
||||||
|
implementation "androidx.glance:glance-material3:$glanceVersion"
|
||||||
|
implementation "androidx.glance:glance-material:$glanceVersion"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||||
|
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
|
||||||
|
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"
|
||||||
}
|
}
|
@ -17,38 +17,36 @@
|
|||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name="${applicationName}"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="false"
|
android:fullBackupContent="false"
|
||||||
android:label="@string/app_name_en"
|
|
||||||
android:name="${applicationName}"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true"
|
android:label="@string/app_name_en"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
>
|
android:usesCleartextTraffic="true">
|
||||||
<!-- Enable Impeller -->
|
<!-- Enable Impeller -->
|
||||||
<!-- <meta-data
|
<!-- <meta-data
|
||||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
android:value="true" /> -->
|
android:value="true" /> -->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:windowSoftInputMode="adjustResize">
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
|
||||||
>
|
|
||||||
<!--
|
<!--
|
||||||
Specifies an Android theme to apply to this Activity as soon as
|
Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI.
|
to determine the Window background behind the Flutter UI.
|
||||||
-->
|
-->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@ -56,12 +54,13 @@
|
|||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:scheme="https"
|
|
||||||
android:host="open.spotify.com"
|
android:host="open.spotify.com"
|
||||||
/>
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -72,23 +71,27 @@
|
|||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<!-- Accepts URIs that begin with "spotify:// -->
|
<!-- Accepts URIs that begin with "spotify:// -->
|
||||||
<data android:scheme="spotify" />
|
<data android:scheme="spotify" />
|
||||||
|
<data android:scheme="spotube" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- AudioService Config -->
|
<!-- AudioService Config -->
|
||||||
<service android:name="com.ryanheise.audioservice.AudioService"
|
<service
|
||||||
android:foregroundServiceType="mediaPlayback"
|
android:name="com.ryanheise.audioservice.AudioService"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
<receiver
|
||||||
|
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
@ -96,23 +99,40 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
<!-- =================== -->
|
<!-- =================== -->
|
||||||
|
|
||||||
<meta-data android:name="com.google.android.gms.car.application"
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.car.application"
|
||||||
android:resource="@xml/automotive_app_desc" />
|
android:resource="@xml/automotive_app_desc" />
|
||||||
|
|
||||||
<!-- Home Widget config -->
|
<!-- Home Widget config -->
|
||||||
<receiver android:name=".glance.HomePlayerWidgetReceiver"
|
<receiver
|
||||||
android:exported="true">
|
android:name=".glance.HomePlayerWidgetReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/home_player_widget_config" />
|
android:resource="@xml/home_player_widget_config" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="es.antonborri.home_widget.HomeWidgetBackgroundReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="es.antonborri.home_widget.action.BACKGROUND" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="es.antonborri.home_widget.HomeWidgetBackgroundService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
<!-- =================== -->
|
<!-- =================== -->
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
@ -3,24 +3,68 @@ package oss.krtirtho.spotube.glance
|
|||||||
import HomeWidgetGlanceState
|
import HomeWidgetGlanceState
|
||||||
import HomeWidgetGlanceStateDefinition
|
import HomeWidgetGlanceStateDefinition
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.glance.GlanceId
|
import androidx.glance.GlanceId
|
||||||
import androidx.glance.GlanceModifier
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.GlanceTheme
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.LocalSize
|
||||||
|
import androidx.glance.action.ActionParameters
|
||||||
|
import androidx.glance.action.actionParametersOf
|
||||||
import androidx.glance.appwidget.GlanceAppWidget
|
import androidx.glance.appwidget.GlanceAppWidget
|
||||||
|
import androidx.glance.appwidget.SizeMode
|
||||||
|
import androidx.glance.appwidget.action.ActionCallback
|
||||||
|
import androidx.glance.appwidget.action.actionRunCallback
|
||||||
|
import androidx.glance.appwidget.components.CircleIconButton
|
||||||
|
import androidx.glance.appwidget.components.Scaffold
|
||||||
import androidx.glance.appwidget.provideContent
|
import androidx.glance.appwidget.provideContent
|
||||||
import androidx.glance.background
|
|
||||||
import androidx.glance.currentState
|
import androidx.glance.currentState
|
||||||
import androidx.glance.layout.Box
|
import androidx.glance.layout.Alignment
|
||||||
import androidx.glance.layout.Column
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.Spacer
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
import androidx.glance.layout.padding
|
import androidx.glance.layout.padding
|
||||||
|
import androidx.glance.layout.size
|
||||||
import androidx.glance.preview.ExperimentalGlancePreviewApi
|
import androidx.glance.preview.ExperimentalGlancePreviewApi
|
||||||
import androidx.glance.preview.Preview
|
import androidx.glance.preview.Preview
|
||||||
import androidx.glance.state.GlanceStateDefinition
|
import androidx.glance.state.GlanceStateDefinition
|
||||||
import androidx.glance.text.Text
|
import com.google.gson.Gson
|
||||||
|
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.widgets.TrackDetailsView
|
||||||
|
import oss.krtirtho.spotube.glance.widgets.TrackProgress
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
val gson = Gson()
|
||||||
|
val serverAddressKey = ActionParameters.Key<String>("serverAddress")
|
||||||
|
|
||||||
|
class Breakpoints {
|
||||||
|
companion object {
|
||||||
|
val SMALL_SQUARE = DpSize(100.dp, 100.dp)
|
||||||
|
val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
|
||||||
|
val BIG_SQUARE = DpSize(250.dp, 250.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class HomePlayerWidget : GlanceAppWidget() {
|
class HomePlayerWidget : GlanceAppWidget() {
|
||||||
|
|
||||||
|
override val sizeMode = SizeMode.Responsive(
|
||||||
|
setOf(
|
||||||
|
Breakpoints.SMALL_SQUARE,
|
||||||
|
Breakpoints.HORIZONTAL_RECTANGLE,
|
||||||
|
Breakpoints.BIG_SQUARE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
override val stateDefinition: GlanceStateDefinition<*>?
|
override val stateDefinition: GlanceStateDefinition<*>?
|
||||||
get() = HomeWidgetGlanceStateDefinition()
|
get() = HomeWidgetGlanceStateDefinition()
|
||||||
|
|
||||||
@ -30,18 +74,112 @@ class HomePlayerWidget : GlanceAppWidget() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalGlancePreviewApi::class)
|
@OptIn(ExperimentalGlancePreviewApi::class)
|
||||||
@Preview(widthDp = 200, heightDp = 150)
|
@Preview(widthDp = 100, heightDp = 100)
|
||||||
@Composable
|
@Composable
|
||||||
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
|
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
|
||||||
val prefs = currentState.preferences
|
val prefs = currentState.preferences
|
||||||
val counter = prefs.getInt("counter", 0)
|
val size = LocalSize.current
|
||||||
Box(modifier = GlanceModifier.background(Color.White).padding(16.dp)) {
|
|
||||||
Column() {
|
val activeTrackStr = prefs.getString("activeTrack", null)
|
||||||
Text("Counter")
|
val isPlaying = prefs.getBoolean("isPlaying", false)
|
||||||
Text(
|
val playbackServerAddress = prefs.getString("playbackServerAddress", null) ?: ""
|
||||||
counter.toString()
|
var activeTrack: Track? = null
|
||||||
)
|
if (activeTrackStr != null) {
|
||||||
|
activeTrack = gson.fromJson(activeTrackStr, Track::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val playIcon = Icon.createWithResource(context, android.R.drawable.ic_media_play);
|
||||||
|
val pauseIcon = Icon.createWithResource(context, android.R.drawable.ic_media_pause);
|
||||||
|
val previousIcon = Icon.createWithResource(context, android.R.drawable.ic_media_previous);
|
||||||
|
val nextIcon = Icon.createWithResource(context, android.R.drawable.ic_media_next);
|
||||||
|
|
||||||
|
GlanceTheme {
|
||||||
|
Scaffold {
|
||||||
|
Column(
|
||||||
|
modifier = GlanceModifier.padding(
|
||||||
|
horizontal = if (size != Breakpoints.SMALL_SQUARE) 8.dp else 2.dp,
|
||||||
|
vertical = 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (size == Breakpoints.HORIZONTAL_RECTANGLE) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Vertical.CenterVertically,
|
||||||
|
) { TrackDetailsView(activeTrack) }
|
||||||
|
} else {
|
||||||
|
TrackDetailsView(activeTrack)
|
||||||
|
}
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
if (size != Breakpoints.SMALL_SQUARE) {
|
||||||
|
TrackProgress(prefs)
|
||||||
|
}
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Row(
|
||||||
|
modifier = GlanceModifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.Horizontal.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircleIconButton(
|
||||||
|
imageProvider = ImageProvider(previousIcon),
|
||||||
|
contentDescription = "Previous",
|
||||||
|
onClick = actionRunCallback<PreviousAction>(
|
||||||
|
parameters = actionParametersOf(serverAddressKey to playbackServerAddress)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
CircleIconButton(
|
||||||
|
imageProvider =
|
||||||
|
if (isPlaying) ImageProvider(pauseIcon)
|
||||||
|
else ImageProvider(playIcon),
|
||||||
|
contentDescription = "Play/Pause",
|
||||||
|
onClick = actionRunCallback<PlayPauseAction>(
|
||||||
|
parameters = actionParametersOf(serverAddressKey to playbackServerAddress)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
CircleIconButton(
|
||||||
|
imageProvider = ImageProvider(nextIcon),
|
||||||
|
contentDescription = "Previous",
|
||||||
|
onClick = actionRunCallback<NextAction>(
|
||||||
|
parameters = actionParametersOf(
|
||||||
|
serverAddressKey to playbackServerAddress
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayPauseAction : InteractiveAction("toggle_playback")
|
||||||
|
class NextAction : InteractiveAction("next")
|
||||||
|
class PreviousAction : InteractiveAction("previous")
|
||||||
|
|
||||||
|
|
||||||
|
abstract class InteractiveAction(val command: String) : ActionCallback {
|
||||||
|
override suspend fun onAction(
|
||||||
|
context: Context,
|
||||||
|
glanceId: GlanceId,
|
||||||
|
parameters: ActionParameters
|
||||||
|
) {
|
||||||
|
val serverAddress = parameters[serverAddressKey]
|
||||||
|
if (serverAddress == null || serverAddress.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val client = OkHttpClient()
|
||||||
|
val request = Request.Builder().url("http://$serverAddress/playback/$command").build()
|
||||||
|
|
||||||
|
client.newCall(request).execute().use { response ->
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
response.body?.string()
|
||||||
|
} else {
|
||||||
|
print("Failed to send command to server")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.models
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AlbumSimple(
|
||||||
|
@SerializedName("album_type")
|
||||||
|
val albumType: AlbumType?,
|
||||||
|
|
||||||
|
@SerializedName("available_markets")
|
||||||
|
val availableMarkets: List<Market>?,
|
||||||
|
|
||||||
|
val href: String?,
|
||||||
|
val id: String?,
|
||||||
|
val images: List<Image>?,
|
||||||
|
val name: String?,
|
||||||
|
|
||||||
|
@SerializedName("release_date")
|
||||||
|
val releaseDate: String?,
|
||||||
|
|
||||||
|
@SerializedName("release_date_precision")
|
||||||
|
val releaseDatePrecision: DatePrecision?,
|
||||||
|
|
||||||
|
val type: String?,
|
||||||
|
val uri: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class AlbumType {
|
||||||
|
album,
|
||||||
|
single,
|
||||||
|
compilation
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DatePrecision {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.models
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Artist(
|
||||||
|
val href: String?,
|
||||||
|
val id: String?,
|
||||||
|
val name: String?,
|
||||||
|
val type: String?,
|
||||||
|
val uri: String?,
|
||||||
|
|
||||||
|
val followers: Followers?,
|
||||||
|
val genres: List<String>?,
|
||||||
|
val images: List<Image>?,
|
||||||
|
|
||||||
|
@SerializedName("popularity")
|
||||||
|
val popularity: Int?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Followers(
|
||||||
|
val total: Int?
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Image(
|
||||||
|
val height: Int?,
|
||||||
|
val width: Int?,
|
||||||
|
val url: String?
|
||||||
|
)
|
@ -0,0 +1,37 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.models
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Track(
|
||||||
|
val album: AlbumSimple?, val artists: List<Artist>?,
|
||||||
|
|
||||||
|
@SerializedName("available_markets") val availableMarkets: List<Market>?,
|
||||||
|
|
||||||
|
@SerializedName("disc_number") val discNumber: Int?,
|
||||||
|
|
||||||
|
@SerializedName("duration_ms") val durationMs: Int,
|
||||||
|
|
||||||
|
val explicit: Boolean?, val href: String?, val id: String?,
|
||||||
|
|
||||||
|
@SerializedName("is_playable") val isPlayable: Boolean?,
|
||||||
|
|
||||||
|
val name: String?,
|
||||||
|
|
||||||
|
@SerializedName("popularity") val popularity: Int?,
|
||||||
|
|
||||||
|
@SerializedName("preview_url") val previewUrl: String?,
|
||||||
|
|
||||||
|
@SerializedName("track_number") val trackNumber: Int?,
|
||||||
|
|
||||||
|
val type: String?, val uri: String?
|
||||||
|
) {
|
||||||
|
val duration: kotlin.time.Duration
|
||||||
|
get() = durationMs.toLong().milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Market {
|
||||||
|
AD, AE, AF, AG, AI, AL, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BL, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BY, BZ, CA, CC, CD, CF, CG, CH, CI, CK, CL, CM, CN, CO, CR, CU, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, ET, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IQ, IR, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KP, KR, KW, KY, KZ, LA, LB, LC, LI, LK, LR, LS, LT, LU, LV, LY, MA, MC, MD, ME, MF, MG, MH, MK, ML, MM, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NI, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PS, PT, PW, PY, QA, RE, RO, RS, RU, RW, SA, SB, SC, SD, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SO, SR, SS, ST, SV, SX, SY, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VC, VE, VG, VI, VN, VU, WF, WS, XK, YE, YT, ZA, ZM, ZW,
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.widgets
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.GlanceTheme
|
||||||
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.LocalSize
|
||||||
|
import androidx.glance.appwidget.ImageProvider
|
||||||
|
import androidx.glance.appwidget.cornerRadius
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.ContentScale
|
||||||
|
import androidx.glance.layout.Spacer
|
||||||
|
import androidx.glance.layout.size
|
||||||
|
import androidx.glance.text.FontWeight
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import oss.krtirtho.spotube.glance.Breakpoints
|
||||||
|
import oss.krtirtho.spotube.glance.models.Track
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TrackDetailsView(activeTrack: Track?) {
|
||||||
|
val size = LocalSize.current
|
||||||
|
|
||||||
|
val artistStr = activeTrack?.artists?.map { it.name }?.joinToString(", ") ?: "<No Artist>"
|
||||||
|
val imgUri = activeTrack?.album?.images?.get(0)?.url
|
||||||
|
?: "https://placehold.co/600x400/000000/FFF.jpg";
|
||||||
|
val title = activeTrack?.name ?: "<No Track>"
|
||||||
|
|
||||||
|
|
||||||
|
Image(
|
||||||
|
provider = ImageProvider(uri = Uri.parse(imgUri)),
|
||||||
|
contentDescription = "Album Art",
|
||||||
|
modifier = GlanceModifier.cornerRadius(8.dp)
|
||||||
|
.size(
|
||||||
|
when (size) {
|
||||||
|
Breakpoints.SMALL_SQUARE -> 70.dp
|
||||||
|
Breakpoints.HORIZONTAL_RECTANGLE -> 100.dp
|
||||||
|
Breakpoints.BIG_SQUARE -> 150.dp
|
||||||
|
else -> 120.dp
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.Fit
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = GlanceTheme.colors.onBackground
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (size != Breakpoints.SMALL_SQUARE) {
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
Text(
|
||||||
|
text = artistStr,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = GlanceTheme.colors.onBackground
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package oss.krtirtho.spotube.glance.widgets
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.GlanceTheme
|
||||||
|
import androidx.glance.LocalSize
|
||||||
|
import androidx.glance.appwidget.LinearProgressIndicator
|
||||||
|
import androidx.glance.background
|
||||||
|
import androidx.glance.currentState
|
||||||
|
import androidx.glance.layout.Column
|
||||||
|
import androidx.glance.layout.Row
|
||||||
|
import androidx.glance.layout.Spacer
|
||||||
|
import androidx.glance.layout.fillMaxWidth
|
||||||
|
import androidx.glance.layout.height
|
||||||
|
import androidx.glance.layout.size
|
||||||
|
import androidx.glance.layout.wrapContentWidth
|
||||||
|
import androidx.glance.text.Text
|
||||||
|
import androidx.glance.text.TextStyle
|
||||||
|
import oss.krtirtho.spotube.glance.Breakpoints
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
fun Duration.format(): String {
|
||||||
|
return this.toComponents { hour, minutes, seconds, nanoseconds ->
|
||||||
|
var paddedSeconds = seconds.toString().padStart(2, '0')
|
||||||
|
var paddedMinutes = minutes.toString().padStart(2, '0')
|
||||||
|
var paddedHour = hour.toString().padStart(2, '0')
|
||||||
|
if (hour == 0L) {
|
||||||
|
"$paddedMinutes:$paddedSeconds"
|
||||||
|
} else {
|
||||||
|
"$paddedHour:$paddedMinutes:$paddedSeconds"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TrackProgress(prefs: SharedPreferences) {
|
||||||
|
val size = LocalSize.current;
|
||||||
|
val progress = prefs.getFloat("progress", 0.0f)
|
||||||
|
var duration = prefs.getInt("duration", 0).seconds
|
||||||
|
|
||||||
|
var startingTime = (duration.inWholeSeconds * progress).toLong().seconds
|
||||||
|
|
||||||
|
var textStyle = TextStyle(
|
||||||
|
color = GlanceTheme.colors.onBackground,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (size == Breakpoints.HORIZONTAL_RECTANGLE) {
|
||||||
|
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = startingTime.format(),
|
||||||
|
style = textStyle
|
||||||
|
)
|
||||||
|
Spacer(modifier = GlanceModifier.size(6.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = GlanceModifier.defaultWeight(),
|
||||||
|
color = GlanceTheme.colors.primary,
|
||||||
|
backgroundColor = GlanceTheme.colors.primaryContainer,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:initialLayout="@layout/glance_default_loading_layout"
|
android:initialLayout="@layout/glance_default_loading_layout"
|
||||||
android:minWidth="40dp"
|
android:minWidth="100dp"
|
||||||
android:minHeight="40dp"
|
android:minHeight="100dp"
|
||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:updatePeriodMillis="10000">
|
android:updatePeriodMillis="10000">
|
||||||
</appwidget-provider>
|
</appwidget-provider>
|
||||||
|
@ -15,4 +15,4 @@ subprojects {
|
|||||||
|
|
||||||
tasks.register("clean", Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
@ -22,3 +22,4 @@ plugins {
|
|||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
include ':app'
|
@ -42,10 +42,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: app_links
|
name: app_links
|
||||||
sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950"
|
sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.3"
|
version: "6.3.2"
|
||||||
app_links_linux:
|
app_links_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
Loading…
Reference in New Issue
Block a user