Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,29 @@
android:resource="@xml/lyric_widget" />
</receiver>

<receiver android:name=".ui.CardWidgetProvider"
android:label="@string/card_widget_title"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="org.akanework.gramophone.ACTION_CARD_WIDGET_REPEAT" />
<action android:name="org.akanework.gramophone.ACTION_CARD_WIDGET_SHUFFLE" />
<action android:name="org.akanework.gramophone.ACTION_CARD_WIDGET_FAVORITE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/card_widget" />
</receiver>

<receiver android:name=".ui.CircleWidgetProvider"
android:label="@string/circle_widget_title"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/circle_widget" />
</receiver>

<service android:name=".ui.LyricWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ import org.akanework.gramophone.logic.utils.exoplayer.GramophoneRenderFactory
import org.akanework.gramophone.ui.AudioPreviewActivity
import org.akanework.gramophone.ui.LyricWidgetProvider
import org.akanework.gramophone.ui.MainActivity
import org.akanework.gramophone.ui.widget.BaseWidgetProvider
import org.akanework.gramophone.ui.widget.DesktopWidgetManager
import org.akanework.gramophone.ui.fragments.compose.MqState.Companion.CLIENT_QB_REFRESH_ALL
import org.akanework.gramophone.ui.fragments.compose.MqState.Companion.CLIENT_QB_REFRESH_CLEAR
import org.akanework.gramophone.ui.fragments.compose.MqState.Companion.CLIENT_QB_REFRESH_ITEM
Expand Down Expand Up @@ -810,10 +812,17 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
return onSetRating(session, controller, mediaItemId, rating)
}

fun toggleCurrentItemFavorite() {
Comment thread
SteveZMTstudios marked this conversation as resolved.
val currentMediaItem = controller?.currentMediaItem ?: return
val isHeart = (currentMediaItem.mediaMetadata.userRating as? HeartRating)?.isHeart == true
controller?.setRating(HeartRating(!isHeart))
}

// When destroying, we should release server side player
// alongside with the mediaSession.
override fun onDestroy() {
Log.i(TAG, "+onDestroy()")
BaseWidgetProvider.savePlaybackSnapshot(this)
instanceForWidgetAndLyricsOnly = null
unregisterReceiver(seekReceiver)
unregisterReceiver(btReceiver)
Expand All @@ -835,6 +844,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
mediaSession = null
broadcastAudioSessionClose()
LyricWidgetProvider.update(this)
DesktopWidgetManager.updateAllWidgets(this)
internalPlaybackThread.quitSafely()
super.onDestroy()
Log.i(TAG, "-onDestroy()")
Expand Down Expand Up @@ -1624,6 +1634,7 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
}
}

DesktopWidgetManager.refreshFromPlayback(this)
lastPlayedManager.save()
}

Expand All @@ -1639,10 +1650,12 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis

override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
refreshMediaButtonCustomLayout()
DesktopWidgetManager.refreshFromPlayback(this)
}

override fun onIsPlayingChanged(isPlaying: Boolean) {
scheduleSendingLyrics(false)
DesktopWidgetManager.updateAllWidgets(this)
lastPlayedManager.save()
}

Expand Down Expand Up @@ -1684,13 +1697,15 @@ class GramophonePlaybackService : MediaLibraryService(), MediaSessionService.Lis
}
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
refreshMediaButtonCustomLayout()
DesktopWidgetManager.refreshFromPlayback(this)
if (needsMissingOnDestroyCallWorkarounds()) {
handler.post { lastPlayedManager.save() }
}
}

override fun onRepeatModeChanged(repeatMode: Int) {
refreshMediaButtonCustomLayout()
DesktopWidgetManager.refreshFromPlayback(this)
if (needsMissingOnDestroyCallWorkarounds()) {
handler.post { lastPlayedManager.save() }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 SteveZMTstudios
*
* Gramophone is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Gramophone is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package org.akanework.gramophone.ui

import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import androidx.media3.common.Player
import org.akanework.gramophone.logic.GramophonePlaybackService
import org.akanework.gramophone.ui.widget.BaseWidgetProvider
import org.akanework.gramophone.ui.widget.CardWidgetActions
import org.akanework.gramophone.ui.widget.CardWidgetPlaybackState
import org.akanework.gramophone.ui.widget.CardWidgetStore
import org.akanework.gramophone.ui.widget.CardWidgetViewsBuilder
import org.akanework.gramophone.ui.widget.DesktopWidgetManager

class CardWidgetProvider : BaseWidgetProvider() {

override fun buildViews(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
state: CardWidgetPlaybackState,
actions: CardWidgetActions
): RemoteViews {
return CardWidgetViewsBuilder.buildCardResponsiveRemoteViews(
context, appWidgetManager, appWidgetId, state, actions
)
}

override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
val action = intent.action ?: return
handleWidgetAction(context, action)
}

private fun handleWidgetAction(context: Context, action: String) {
val service = GramophonePlaybackService.instanceForWidgetAndLyricsOnly
val player = service?.endedWorkaroundPlayer
if (service != null && player != null) {
when (action) {
ACTION_REPEAT -> {
player.repeatMode = nextRepeatMode(player.repeatMode)
}
ACTION_SHUFFLE -> {
player.shuffleModeEnabled = !player.shuffleModeEnabled
}
ACTION_FAVORITE -> {
service.toggleCurrentItemFavorite()
}
}
}
}

companion object {

fun nextRepeatMode(currentMode: Int): Int = when (currentMode) {
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL
Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_OFF
else -> Player.REPEAT_MODE_OFF
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2026 SteveZMTstudios
*
* Gramophone is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Gramophone is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package org.akanework.gramophone.ui

import android.appwidget.AppWidgetManager
import android.content.Context
import android.widget.RemoteViews
import org.akanework.gramophone.ui.widget.BaseWidgetProvider
import org.akanework.gramophone.ui.widget.CardWidgetActions
import org.akanework.gramophone.ui.widget.CardWidgetPlaybackState
import org.akanework.gramophone.ui.widget.CardWidgetViewsBuilder

class CircleWidgetProvider : BaseWidgetProvider() {

override fun buildViews(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
state: CardWidgetPlaybackState,
actions: CardWidgetActions
): RemoteViews {
return CardWidgetViewsBuilder.buildCircleResponsiveRemoteViews(
context, appWidgetManager, appWidgetId, state, actions
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import org.akanework.gramophone.R
import org.akanework.gramophone.ui.components.TabOrderPreference
import org.akanework.gramophone.ui.fragments.BasePreferenceFragment
import org.akanework.gramophone.ui.fragments.BaseSettingsActivity
import org.akanework.gramophone.ui.widget.CardWidgetColorResolver
import org.akanework.gramophone.ui.widget.DesktopWidgetManager

class AppearanceSettingsActivity : BaseSettingsActivity(
R.string.settings_category_appearance,
Expand Down Expand Up @@ -52,6 +54,11 @@ class AppearanceSettingsFragment : BasePreferenceFragment() {
}
}
}

CardWidgetColorResolver.PREF_WIDGET_THEME -> {
CardWidgetColorResolver.clearCache()
context?.let { DesktopWidgetManager.updateAllWidgets(it) }
}
}
}

Expand Down
Loading