From e8c42464a7dbc5dc1d9c89ae6911c923d44383d3 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 27 Feb 2022 00:41:35 +0100 Subject: [PATCH] Add foundation for clock widget parts Includes date part and music part --- .../ui/launcher/widgets/clock/ClockWidget.kt | 17 +- .../launcher/widgets/clock/ClockWidgetVM.kt | 42 +++- .../widgets/clock/parts/DatePartProvider.kt | 75 ++++++++ .../widgets/clock/parts/MusicPartProvider.kt | 181 ++++++++++++++++++ .../widgets/clock/parts/PartProvider.kt | 14 ++ 5 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/DatePartProvider.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/MusicPartProvider.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/PartProvider.kt diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt index 53f897f4..3a19803e 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt @@ -20,7 +20,7 @@ import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockStyle import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidgetVM import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.* -import de.mm20.launcher2.ui.launcher.widgets.clock.parts.DatePart +import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider @Composable fun ClockWidget( @@ -31,6 +31,9 @@ fun ClockWidget( val time by viewModel.getTime(context).collectAsState(System.currentTimeMillis()) val layout by viewModel.layout.observeAsState() val clockStyle by viewModel.clockStyle.observeAsState() + + val partProvider by viewModel.getActivePart().collectAsState(null) + Box( modifier = Modifier .fillMaxWidth(), @@ -59,27 +62,27 @@ fun ClockWidget( DynamicZone( modifier = Modifier.padding(bottom = 16.dp), ClockWidgetLayout.Vertical, - time + provider = partProvider, ) } } if (layout == ClockWidgetLayout.Horizontal) { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Icon(imageVector = Icons.Rounded.ExpandLess, contentDescription = "") Row( modifier = Modifier .fillMaxWidth() - .padding(end = 16.dp), + .padding(end = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { DynamicZone( modifier = Modifier.weight(1f), ClockWidgetLayout.Horizontal, - time + provider = partProvider, ) Box( modifier = Modifier @@ -128,11 +131,11 @@ fun Clock( fun DynamicZone( modifier: Modifier = Modifier, layout: ClockWidgetLayout, - time: Long, + provider: PartProvider?, ) { Column( modifier = modifier ) { - DatePart(time, layout) + provider?.Component(layout) } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt index 413c759f..839fc660 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt @@ -5,23 +5,53 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.provider.AlarmClock +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import de.mm20.launcher2.ktx.tryStartActivity -import de.mm20.launcher2.lifecycle.BroadcastReceiverLiveData import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.ui.launcher.widgets.clock.parts.DatePartProvider +import de.mm20.launcher2.ui.launcher.widgets.clock.parts.MusicPartProvider +import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject -class ClockWidgetVM: ViewModel(), KoinComponent { +class ClockWidgetVM : ViewModel(), KoinComponent { private val dataStore: LauncherDataStore by inject() + private val partProviders = MutableStateFlow>(emptyList()) + + init { + partProviders.value = listOf(DatePartProvider(), MusicPartProvider()) + } + + fun getActivePart(): Flow = channelFlow { + partProviders.collectLatest { providers -> + if (providers.isEmpty()) { + send(null) + return@collectLatest + } + val rankings = providers.map { it.getRanking() } + combine(rankings) { r -> + var prov = providers[0] + for (i in 1 until providers.size) { + if (r[i - 1] < r[i]) { + prov = providers[i] + } + } + Log.d("MM20", prov.toString()) + return@combine prov + }.collectLatest { + send(it) + } + } + } + + val layout = dataStore.data.map { it.clockWidget.layout }.asLiveData() val clockStyle = dataStore.data.map { it.clockWidget.clockStyle }.asLiveData() diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/DatePartProvider.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/DatePartProvider.kt new file mode 100644 index 00000000..2898352b --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/DatePartProvider.kt @@ -0,0 +1,75 @@ +package de.mm20.launcher2.ui.launcher.widgets.clock.parts + +import android.content.ContentUris +import android.content.Intent +import android.provider.CalendarContract +import android.text.format.DateFormat +import android.text.format.DateUtils +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.em +import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.preferences.Settings +import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidgetVM +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import java.text.SimpleDateFormat +import java.util.* + +class DatePartProvider: PartProvider { + override fun getRanking(): Flow = flow { + emit(1) + } + + @Composable + override fun Component(layout: Settings.ClockWidgetSettings.ClockWidgetLayout) { + val viewModel: ClockWidgetVM = viewModel() + val time by viewModel.getTime(LocalContext.current).collectAsState(System.currentTimeMillis()) + val verticalLayout = layout == Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical + val context = LocalContext.current + TextButton(onClick = { + val startMillis = System.currentTimeMillis() + val builder = CalendarContract.CONTENT_URI.buildUpon() + builder.appendPath("time") + ContentUris.appendId(builder, startMillis) + val intent = Intent(Intent.ACTION_VIEW) + .setData(builder.build()) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + context.startActivity(intent) + }) { + if (verticalLayout) { + Text( + text = DateUtils.formatDateTime( + context, + time, + DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR + ), + style = MaterialTheme.typography.titleMedium, + color = Color.White + ) + } else { + val line1Format = DateFormat.getBestDateTimePattern(Locale.getDefault(), "EEEE") + val line2Format = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM dd yyyy") + val format = SimpleDateFormat("$line1Format\n$line2Format") + Text( + text = format.format(time), + lineHeight = 1.2.em, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Medium + ), + color = Color.White + ) + } + } + } + +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/MusicPartProvider.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/MusicPartProvider.kt new file mode 100644 index 00000000..fc64bda2 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/MusicPartProvider.kt @@ -0,0 +1,181 @@ +package de.mm20.launcher2.ui.launcher.widgets.clock.parts + +import android.app.PendingIntent +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.SkipNext +import androidx.compose.material.icons.rounded.SkipPrevious +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import de.mm20.launcher2.music.MusicRepository +import de.mm20.launcher2.music.PlaybackState +import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout +import de.mm20.launcher2.ui.R +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class MusicPartProvider : PartProvider, KoinComponent { + + private val musicRepository: MusicRepository by inject() + + override fun getRanking(): Flow = channelFlow { + musicRepository.playbackState.collectLatest { + if (it == PlaybackState.Stopped) send(0) + else send(50) + } + } + + @OptIn( + ExperimentalAnimationGraphicsApi::class, + androidx.compose.foundation.ExperimentalFoundationApi::class + ) + @Composable + override fun Component(layout: ClockWidgetLayout) { + val context = LocalContext.current + + val title by musicRepository.title.collectAsState(null) + val artist by musicRepository.artist.collectAsState(null) + val state by musicRepository.playbackState.collectAsState(PlaybackState.Stopped) + + val playIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause) + + if (layout === ClockWidgetLayout.Horizontal) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(8.dp) + .combinedClickable( + onClick = { + try { + musicRepository.openPlayer()?.send() + } catch (e: PendingIntent.CanceledException) { + } + }, + onLongClick = { + musicRepository.openPlayerChooser(context) + } + ) + ) { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + artist?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + IconButton(onClick = { musicRepository.togglePause() }) { + Icon( + painter = rememberAnimatedVectorPainter( + animatedImageVector = playIcon, + atEnd = state == PlaybackState.Playing + ), contentDescription = null + ) + } + IconButton(onClick = { musicRepository.next() }) { + Icon( + imageVector = Icons.Rounded.SkipNext, + contentDescription = null + ) + } + } + } + if (layout === ClockWidgetLayout.Vertical) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + modifier = Modifier + .combinedClickable( + onClick = { + try { + musicRepository.openPlayer()?.send() + } catch (e: PendingIntent.CanceledException) { + } + }, + onLongClick = { + musicRepository.openPlayerChooser(context) + } + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center + ) + } + artist?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center + ) + } + } + Row { + IconButton(onClick = { musicRepository.previous() }) { + Icon( + imageVector = Icons.Rounded.SkipPrevious, + contentDescription = null + ) + } + IconButton(onClick = { musicRepository.togglePause() }) { + Icon( + painter = rememberAnimatedVectorPainter( + animatedImageVector = playIcon, + atEnd = state == PlaybackState.Playing + ), contentDescription = null + ) + } + IconButton(onClick = { musicRepository.next() }) { + Icon( + imageVector = Icons.Rounded.SkipNext, + contentDescription = null + ) + } + } + } + } + } + +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/PartProvider.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/PartProvider.kt new file mode 100644 index 00000000..fdd7e5b2 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/PartProvider.kt @@ -0,0 +1,14 @@ +package de.mm20.launcher2.ui.launcher.widgets.clock.parts + +import androidx.compose.runtime.Composable +import de.mm20.launcher2.preferences.Settings +import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout +import kotlinx.coroutines.flow.Flow + +interface PartProvider { + + fun getRanking(): Flow + + @Composable + fun Component(layout: ClockWidgetLayout) +} \ No newline at end of file