diff --git a/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml
index a8423bc9..3b96dd68 100644
--- a/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml
+++ b/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml
@@ -115,11 +115,22 @@
+
+
+
+
+
+
+ android:name="com.readest.native_tts.MediaPlaybackService"
+ android:foregroundServiceType="mediaPlayback"
+ android:exported="true">
+
+
+
+
+ Log.i("MediaPlaybackService", "Audio focus changed: $focusChange, $player.isPlaying")
+ when (focusChange) {
+ AudioManager.AUDIOFOCUS_GAIN -> {
+ player.volume = 1.0f
+ if (!player.isPlaying) player.play()
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
+ player.volume = 0.3f
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
+ if (player.isPlaying) player.pause()
+ }
+ }
+ }
+
+ companion object {
+ private const val CHANNEL_ID = "media2_playback_channel"
+ private const val NOTIFICATION_ID = 1002
+ private const val MEDIA_ROOT_ID = "media_root_id"
+
+ var pluginEventTrigger: ((String, JSObject) -> Unit)? = null
+
+ var currentTitle: String = "Read Aloud"
+ var currentArtist: String = "Reading your content"
+ var currentArtwork: Bitmap? = null
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ val result = audioManager.requestAudioFocus(
+ afChangeListener,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN
+ )
+ if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ Log.d("MediaPlaybackService", "Audio focus granted")
+ } else {
+ Log.w("MediaPlaybackService", "Failed to gain audio focus")
+ }
+
+ player = ExoPlayer.Builder(this).build()
+
+ mediaSession = MediaSessionCompat(baseContext, "ReadestMediaSession").apply {
+ stateBuilder = PlaybackStateCompat.Builder().setActions(
+ PlaybackStateCompat.ACTION_PLAY or
+ PlaybackStateCompat.ACTION_PLAY_PAUSE or
+ PlaybackStateCompat.ACTION_PAUSE or
+ PlaybackStateCompat.ACTION_STOP or
+ PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
+ PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ )
+ setPlaybackState(stateBuilder.build())
+ setCallback(SessionCallback())
+ setSessionToken(sessionToken)
+ isActive = true
+ }
+
+ player.addListener(object : Player.Listener {
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
+ updatePlaybackState()
+ }
+ override fun onPlaybackStateChanged(playbackState: Int) {
+ updatePlaybackState()
+ }
+ })
+
+ val mediaItem = MediaItem.fromUri("asset:///silence.mp3")
+ player.setMediaItem(mediaItem)
+ player.repeatMode = Player.REPEAT_MODE_ONE
+ player.prepare()
+ player.playWhenReady = true
+
+ showNotification(PlaybackStateCompat.STATE_PLAYING)
+ }
+
+ private inner class SessionCallback : MediaSessionCompat.Callback() {
+ override fun onPlay() {
+ player.play()
+ pluginEventTrigger?.invoke("media-session-play", JSObject())
+ updatePlaybackState()
+ }
+
+ override fun onPause() {
+ player.pause()
+ pluginEventTrigger?.invoke("media-session-pause", JSObject())
+ updatePlaybackState()
+ }
+
+ override fun onSkipToNext() {
+ player.seekTo(0)
+ pluginEventTrigger?.invoke("media-session-next", JSObject())
+ }
+
+ override fun onSkipToPrevious() {
+ player.seekTo(0)
+ pluginEventTrigger?.invoke("media-session-previous", JSObject())
+ }
+ }
+
+ private fun updatePlaybackState() {
+ val state = if (player.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
+ mediaSession?.setPlaybackState(
+ stateBuilder.setState(state, player.currentPosition, 1f).build()
+ )
+ showNotification(state)
+ }
+
+ private fun showNotification(playbackState: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(CHANNEL_ID, "Media Controls", NotificationManager.IMPORTANCE_LOW)
+ getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
+ }
+ startForeground(NOTIFICATION_ID, buildNotification(playbackState))
+ }
+
+ private fun buildNotification(playbackState: Int): Notification {
+ val builder = NotificationCompat.Builder(this, CHANNEL_ID).apply {
+ setContentTitle(currentTitle)
+ setContentText(currentArtist)
+ setLargeIcon(currentArtwork)
+ setContentIntent(mediaSession!!.controller.sessionActivity)
+ setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this@MediaPlaybackService, PlaybackStateCompat.ACTION_STOP))
+ setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ setSmallIcon(R.drawable.notification_icon)
+
+ setStyle(
+ androidx.media.app.NotificationCompat.MediaStyle()
+ .setMediaSession(mediaSession?.sessionToken)
+ .setShowActionsInCompactView(0)
+ )
+ }
+ return builder.build()
+ }
+
+ override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
+ return BrowserRoot(MEDIA_ROOT_ID, null)
+ }
+
+ override fun onLoadChildren(parentId: String, result: Result>) {
+ result.sendResult(null)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ MediaButtonReceiver.handleIntent(mediaSession, intent)
+
+ if (intent?.action == "UPDATE_METADATA") {
+ currentTitle = intent.getStringExtra("title") ?: currentTitle
+ currentArtist = intent.getStringExtra("artist") ?: currentArtist
+ val newArtwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra("artwork", Bitmap::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableExtra("artwork")
+ }
+ if (newArtwork != null) {
+ currentArtwork = newArtwork
+ }
+
+ val metadataBuilder = MediaMetadataCompat.Builder()
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentTitle)
+ .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentArtist)
+ .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, currentArtwork)
+
+ mediaSession?.setMetadata(metadataBuilder.build())
+
+ showNotification(if (player.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED)
+ } else if (intent?.action == "UPDATE_PLAYBACK_STATE") {
+ val isPlaying = intent.getBooleanExtra("playing", false)
+ val position = intent.getLongExtra("position", 0L) // in milliseconds
+ val duration = intent.getLongExtra("duration", 0L) // in milliseconds
+
+ if (isPlaying && !player.isPlaying) {
+ player.play()
+ } else if (!isPlaying && player.isPlaying) {
+ player.pause()
+ }
+ player.seekTo(position)
+
+ val state = if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
+ mediaSession?.setPlaybackState(
+ stateBuilder.setState(state, position, 1f).build()
+ )
+ showNotification(state)
+ }
+
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ player.release()
+ mediaSession?.release()
+ }
+}
\ No newline at end of file
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/java/NativeTTSPlugin.kt b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/java/NativeTTSPlugin.kt
index 20de26e8..dd0a8172 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/java/NativeTTSPlugin.kt
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/java/NativeTTSPlugin.kt
@@ -11,13 +11,7 @@ import android.speech.tts.Voice
import android.util.Log
import android.graphics.Bitmap
import android.graphics.BitmapFactory
-import androidx.media3.common.MediaMetadata
-import androidx.media3.common.Player
-import androidx.media3.common.MediaItem
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.common.ForwardingPlayer
-import androidx.media3.session.MediaSession
-import androidx.media3.exoplayer.ExoPlayer
+import androidx.core.content.ContextCompat
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Permission
@@ -92,8 +86,8 @@ class UpdateMediaSessionMetadataArgs {
@InvokeArg
class UpdateMediaSessionStateArgs {
var playing: Boolean? = null
- var position: Double? = null
- var duration: Double? = null
+ var position: Int? = null // in milliseconds
+ var duration: Int? = null // in milliseconds
}
@InvokeArg
@@ -106,78 +100,6 @@ class SetMediaSessionActiveArgs {
var foregroundServiceText: String? = null
}
-class MediaForegroundService : Service() {
- companion object {
- const val NOTIFICATION_ID = 1001
- const val CHANNEL_ID = "tts_media_playback_channel"
- const val ACTION_START = "START_FOREGROUND_SERVICE"
- const val ACTION_STOP = "STOP_FOREGROUND_SERVICE"
-
- var pluginInstance: NativeTTSPlugin? = null
- }
-
- override fun onCreate() {
- super.onCreate()
- createNotificationChannel()
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- when (intent?.action) {
- ACTION_START -> {
- startForegroundService()
- }
- ACTION_STOP -> {
- stopForegroundService()
- }
- }
- return START_STICKY // Restart if killed
- }
-
- override fun onBind(intent: Intent?): IBinder? = null
-
- private fun createNotificationChannel() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val channel = NotificationChannel(
- CHANNEL_ID,
- NativeTTSPlugin.NOTIFICATION_TITLE,
- NotificationManager.IMPORTANCE_MIN
- ).apply {
- description = NativeTTSPlugin.NOTIFICATION_TEXT
- setShowBadge(false)
- setSound(null, null)
- }
- val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- manager.createNotificationChannel(channel)
- }
- }
-
- private fun startForegroundService() {
- val notification = NotificationCompat.Builder(this, CHANNEL_ID)
- .setContentTitle(NativeTTSPlugin.FOREGROUND_SERVICE_TITLE)
- .setContentText(NativeTTSPlugin.FOREGROUND_SERVICE_TEXT)
- .setSmallIcon(android.R.drawable.ic_media_play)
- .setOngoing(true)
- .setSilent(true)
- .setPriority(NotificationCompat.PRIORITY_MIN)
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .build()
-
- startForeground(NOTIFICATION_ID, notification)
- Log.d("MediaForegroundService", "Foreground service started")
- }
-
- private fun stopForegroundService() {
- stopForeground(STOP_FOREGROUND_REMOVE)
- stopSelf()
- Log.d("MediaForegroundService", "Foreground service stopped")
- }
-
- override fun onDestroy() {
- super.onDestroy()
- Log.d("MediaForegroundService", "Service destroyed")
- }
-}
-
@TauriPlugin(
permissions = [
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "postNotification")
@@ -203,18 +125,8 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
private val eventChannels = ConcurrentHashMap>()
private val speakingJobs = ConcurrentHashMap()
-
- private var player: Player? = null
- private var mediaSession: MediaSession? = null
- private var isMediaSessionActive = false
- private var isForegroundServiceRunning = false
-
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
- init {
- MediaForegroundService.pluginInstance = this
- }
-
@Command
fun init(invoke: Invoke) {
coroutineScope.launch {
@@ -509,95 +421,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
}
}
- @UnstableApi
- private class WebViewPlayer(player: Player) : ForwardingPlayer(player) {
- var eventTrigger: ((String, JSObject) -> Unit)? = null
-
- override fun play() {
- eventTrigger?.invoke("media-session-play", JSObject())
- if (!this.playWhenReady) {
- this.playWhenReady = true
- } else {
- this.playWhenReady = false
- }
- }
- override fun pause() {
- eventTrigger?.invoke("media-session-pause", JSObject())
- if (!this.playWhenReady) {
- this.playWhenReady = true
- } else {
- this.playWhenReady = false
- }
- }
- override fun seekToNext() {
- eventTrigger?.invoke("media-session-next", JSObject())
- }
- override fun seekToPrevious() {
- eventTrigger?.invoke("media-session-previous", JSObject())
- }
- }
-
- private fun initializeMediaSession() {
- var basePlayer = ExoPlayer.Builder(activity).build()
- val webViewPlayer = WebViewPlayer(basePlayer)
- webViewPlayer.eventTrigger = { event, data -> trigger(event, data) }
- player = webViewPlayer
-
- mediaSession = MediaSession.Builder(activity, player!!)
- .setCallback(object : MediaSession.Callback {
- override fun onConnect(
- session: MediaSession,
- controller: MediaSession.ControllerInfo
- ): MediaSession.ConnectionResult {
- val builder = MediaSession.ConnectionResult.AcceptedResultBuilder(session)
- builder.setAvailablePlayerCommands(
- Player.Commands.Builder()
- .add(Player.COMMAND_PLAY_PAUSE)
- .add(Player.COMMAND_SEEK_TO_NEXT)
- .add(Player.COMMAND_SEEK_TO_PREVIOUS)
- .build()
- )
- return builder.build()
- }
- })
- .build()
- }
-
- private fun deinitializeMediaSession() {
- mediaSession?.release()
- mediaSession = null
- player?.release()
- player = null
- }
-
- private fun startForegroundService() {
- try {
- val intent = Intent(activity, MediaForegroundService::class.java).apply {
- action = MediaForegroundService.ACTION_START
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- activity.startForegroundService(intent)
- } else {
- activity.startService(intent)
- }
- Log.d(TAG, "Foreground service started")
- } catch (e: Exception) {
- Log.e(TAG, "Failed to start foreground service", e)
- }
- }
-
- private fun stopForegroundService() {
- try {
- val intent = Intent(activity, MediaForegroundService::class.java).apply {
- action = MediaForegroundService.ACTION_STOP
- }
- activity.stopService(intent)
- Log.d(TAG, "Foreground service stopped")
- } catch (e: Exception) {
- Log.e(TAG, "Failed to stop foreground service", e)
- }
- }
-
private suspend fun loadArtworkFromUrl(urlString: String): Bitmap? {
return withContext(Dispatchers.IO) {
try {
@@ -624,50 +447,26 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
}
}
- private var currentArtworkByteArray: ByteArray? = null
-
@Command
fun update_media_session_metadata(invoke: Invoke) {
val args = invoke.parseArgs(UpdateMediaSessionMetadataArgs::class.java)
val title = args.title ?: ""
val artist = args.artist ?: ""
val album = args.album ?: ""
- val artworkUrl = args.artwork ?: ""
-
+
coroutineScope.launch {
try {
- val metadataBuilder = MediaMetadata.Builder()
- .setTitle(title)
- .setArtist(artist)
- .setAlbumTitle(album)
- .apply {
- if (artworkUrl.isNotEmpty()) {
- loadArtworkFromUrl(artworkUrl)?.let { bitmap ->
- java.io.ByteArrayOutputStream().use { stream ->
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
- currentArtworkByteArray = stream.toByteArray()
- }
- }
- }
- currentArtworkByteArray?.let {
- setArtworkData(it, MediaMetadata.PICTURE_TYPE_FRONT_COVER)
- }
- }
-
- val mediaItem: MediaItem = MediaItem.Builder()
- .setMediaId("silent_media")
- .setUri("data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQQAAAAAAA==")
- .setMediaMetadata(metadataBuilder.build())
- .build()
-
- player?.setMediaItems(listOf(mediaItem, mediaItem))
- player?.prepare()
- player?.playWhenReady = true
- player?.setRepeatMode(Player.REPEAT_MODE_ALL)
-
+ val artworkBitmap = args.artwork?.let { loadArtworkFromUrl(it) }
+ val intent = Intent(activity, MediaPlaybackService::class.java).apply {
+ action = "UPDATE_METADATA"
+ putExtra("title", title)
+ putExtra("artist", artist)
+ putExtra("album", album)
+ putExtra("artwork", artworkBitmap)
+ }
+ activity.startService(intent)
invoke.resolve()
} catch (e: Exception) {
- Log.e("NativeBridgePlugin", "Failed to update metadata: ${e.message}")
invoke.reject("Failed to update metadata: ${e.message}")
}
}
@@ -677,15 +476,17 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
fun update_media_session_state(invoke: Invoke) {
var args = invoke.parseArgs(UpdateMediaSessionStateArgs::class.java)
val isPlaying = args.playing ?: false
- val position = args.position ?: 0.0
-
+ val position = args.position ?: 0
+ val duration = args.duration ?: 0
+
try {
- player?.let { player ->
- if (position > 0) {
- player.seekTo((position * 1000).toLong())
- }
- player.playWhenReady = isPlaying
+ val intent = Intent(activity, MediaPlaybackService::class.java).apply {
+ action = "UPDATE_PLAYBACK_STATE"
+ putExtra("playing", isPlaying)
+ putExtra("position", position)
+ putExtra("duration", duration)
}
+ activity.startService(intent)
invoke.resolve()
} catch (e: Exception) {
invoke.reject("Failed to update playback state: ${e.message}")
@@ -696,7 +497,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
fun set_media_session_active(invoke: Invoke) {
var args = invoke.parseArgs(SetMediaSessionActiveArgs::class.java)
val active = args.active ?: true
- val keepAppInForeground = args.keepAppInForeground ?: false
args.notificationTitle?.let { NOTIFICATION_TITLE = it }
args.notificationText?.let { NOTIFICATION_TEXT = it }
@@ -704,41 +504,26 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
args.foregroundServiceText?.let { FOREGROUND_SERVICE_TEXT = it }
try {
- if (active && !isMediaSessionActive) {
- initializeMediaSession()
- if (keepAppInForeground) {
- startForegroundService()
- isForegroundServiceRunning = true
- }
- isMediaSessionActive = true
- Log.d(TAG, "Media session activated with foreground service")
- } else if (!active && isMediaSessionActive) {
- deinitializeMediaSession()
- if (isForegroundServiceRunning) {
- stopForegroundService()
- isForegroundServiceRunning = false
- }
- isMediaSessionActive = false
- Log.d(TAG, "Media session deactivated and foreground service stopped")
+ val intent = Intent(activity, MediaPlaybackService::class.java)
+ if (active) {
+ MediaPlaybackService.pluginEventTrigger = { event, data -> trigger(event, data) }
+ MediaPlaybackService.currentTitle = FOREGROUND_SERVICE_TITLE
+ MediaPlaybackService.currentArtist = FOREGROUND_SERVICE_TEXT
+ ContextCompat.startForegroundService(activity, intent)
+ } else {
+ activity.stopService(intent)
+ MediaPlaybackService.pluginEventTrigger = null
}
invoke.resolve()
} catch (e: Exception) {
- Log.e(TAG, "Failed to set media session active state: ${e.message}")
invoke.reject("Failed to set media session active state: ${e.message}")
}
}
fun destroy() {
try {
- if (isMediaSessionActive) {
- if (isForegroundServiceRunning) {
- stopForegroundService()
- isForegroundServiceRunning = false
- }
- isMediaSessionActive = false
- }
-
- deinitializeMediaSession()
+ val intent = Intent(activity, MediaPlaybackService::class.java)
+ activity.stopService(intent)
coroutineScope.cancel()
textToSpeech?.shutdown()
@@ -747,8 +532,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
speakingJobs.values.forEach { it.cancel() }
speakingJobs.clear()
- MediaForegroundService.pluginInstance = null
-
Log.d(TAG, "Plugin destroyed successfully")
} catch (e: Exception) {
Log.e(TAG, "Error during plugin destruction", e)
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/res/drawable/notification_icon.png b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/res/drawable/notification_icon.png
new file mode 100644
index 00000000..3b7e495a
Binary files /dev/null and b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/res/drawable/notification_icon.png differ
diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx
index 6edf2f1c..b7abb36e 100644
--- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx
+++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx
@@ -324,6 +324,15 @@ const TTSControl: React.FC = ({ bookKey, gridInsets }) => {
await ttsController.start();
}
}
+
+ if (mediaSessionRef.current) {
+ const mediaSession = mediaSessionRef.current;
+ if (mediaSession instanceof TauriMediaSession) {
+ await mediaSession.updatePlaybackState({ playing: !isPlaying });
+ } else {
+ mediaSession.playbackState = isPlaying ? 'paused' : 'playing';
+ }
+ }
}, [isPlaying, isPaused]);
const handleBackward = useCallback(async (byMark = false) => {
diff --git a/apps/readest-app/src/libs/mediaSession.ts b/apps/readest-app/src/libs/mediaSession.ts
index 0df1860c..f11ff142 100644
--- a/apps/readest-app/src/libs/mediaSession.ts
+++ b/apps/readest-app/src/libs/mediaSession.ts
@@ -12,8 +12,8 @@ export interface MediaMetadata {
export interface PlaybackState {
playing: boolean;
- position: number;
- duration: number;
+ position?: number; // in milliseconds
+ duration?: number; // in milliseconds
}
export interface MediaSessionState {
diff --git a/apps/readest-app/src/styles/themes.ts b/apps/readest-app/src/styles/themes.ts
index 44df104f..794d38a1 100644
--- a/apps/readest-app/src/styles/themes.ts
+++ b/apps/readest-app/src/styles/themes.ts
@@ -230,7 +230,6 @@ export const applyCustomTheme = (
const lightThemeName = customTheme ? `${customTheme.name}-light` : `${themeName}-light`;
const darkThemeName = customTheme ? `${customTheme.name}-dark` : `${themeName}-dark`;
- console.log('themeName', themeName);
const lightPalette = customTheme
? generateLightPalette(customTheme.colors.light)
: (themes.find((t) => t.name === themeName) || themes[0]!).colors.light;