Add foundation for clock widget parts
Includes date part and music part
This commit is contained in:
parent
84f35487c1
commit
e8c42464a7
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user