Add foundation for clock widget parts

Includes date part and music part
This commit is contained in:
MM20 2022-02-27 00:41:35 +01:00
parent 84f35487c1
commit e8c42464a7
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
5 changed files with 316 additions and 13 deletions

View File

@ -20,7 +20,7 @@ import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockStyle
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout 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.ClockWidgetVM
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.* 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 @Composable
fun ClockWidget( fun ClockWidget(
@ -31,6 +31,9 @@ fun ClockWidget(
val time by viewModel.getTime(context).collectAsState(System.currentTimeMillis()) val time by viewModel.getTime(context).collectAsState(System.currentTimeMillis())
val layout by viewModel.layout.observeAsState() val layout by viewModel.layout.observeAsState()
val clockStyle by viewModel.clockStyle.observeAsState() val clockStyle by viewModel.clockStyle.observeAsState()
val partProvider by viewModel.getActivePart().collectAsState(null)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
@ -59,27 +62,27 @@ fun ClockWidget(
DynamicZone( DynamicZone(
modifier = Modifier.padding(bottom = 16.dp), modifier = Modifier.padding(bottom = 16.dp),
ClockWidgetLayout.Vertical, ClockWidgetLayout.Vertical,
time provider = partProvider,
) )
} }
} }
if (layout == ClockWidgetLayout.Horizontal) { if (layout == ClockWidgetLayout.Horizontal) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Icon(imageVector = Icons.Rounded.ExpandLess, contentDescription = "") Icon(imageVector = Icons.Rounded.ExpandLess, contentDescription = "")
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(end = 16.dp), .padding(end = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
DynamicZone( DynamicZone(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
ClockWidgetLayout.Horizontal, ClockWidgetLayout.Horizontal,
time provider = partProvider,
) )
Box( Box(
modifier = Modifier modifier = Modifier
@ -128,11 +131,11 @@ fun Clock(
fun DynamicZone( fun DynamicZone(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
layout: ClockWidgetLayout, layout: ClockWidgetLayout,
time: Long, provider: PartProvider?,
) { ) {
Column( Column(
modifier = modifier modifier = modifier
) { ) {
DatePart(time, layout) provider?.Component(layout)
} }
} }

View File

@ -5,23 +5,53 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.provider.AlarmClock import android.provider.AlarmClock
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.lifecycle.BroadcastReceiverLiveData
import de.mm20.launcher2.preferences.LauncherDataStore 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.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
class ClockWidgetVM: ViewModel(), KoinComponent { class ClockWidgetVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val partProviders = MutableStateFlow<List<PartProvider>>(emptyList())
init {
partProviders.value = listOf(DatePartProvider(), MusicPartProvider())
}
fun getActivePart(): Flow<PartProvider?> = 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 layout = dataStore.data.map { it.clockWidget.layout }.asLiveData()
val clockStyle = dataStore.data.map { it.clockWidget.clockStyle }.asLiveData() val clockStyle = dataStore.data.map { it.clockWidget.clockStyle }.asLiveData()

View File

@ -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<Int> = 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
)
}
}
}
}

View File

@ -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<Int> = 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
)
}
}
}
}
}
}

View File

@ -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<Int>
@Composable
fun Component(layout: ClockWidgetLayout)
}