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.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)
}
}

View File

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