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