mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 15:26:58 +00:00
Add live information feature (#3862)
* Create live information request * Create LiveInformationManager & store the data using it * Shorten the dev build warning * Add test variable to announcements * Fix logs * Show announcements on the preference dashboard * Shorten the dev build text even more * Improve handling incomplete data * Fix text alignment * Animate announcements & fix ripple effect on ones without a link * Move other warnings to the bottom of the dashboard * Remove unneeded modifier * Revert "Move other warnings to the bottom of the dashboard" This reverts commit 9ecedd3acd0b901b10976256bc96339ef25ffa11. * Allow modifying live information & announcements via resources * Fix spotless issues * Use Kotlinx.serialization for Live Information * Only expose announcements as an immutable list * add close button --------- Co-authored-by: John Andrew Camu <werdna.jac@gmail.com>
This commit is contained in:
@@ -115,6 +115,8 @@
|
||||
<bool name="config_default_smartspace_show_date">true</bool>
|
||||
<bool name="config_default_smartspace_show_time">false</bool>
|
||||
<bool name="config_default_perform_wide_search">true</bool>
|
||||
<bool name="config_default_live_information_enabled">true</bool>
|
||||
<bool name="config_default_live_information_show_announcements">true</bool>
|
||||
<bool name="config_default_enable_dot_pagination">true</bool>
|
||||
<bool name="config_default_enable_material_u_popup">true</bool>
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package app.lawnchair.ui.preferences.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Launch
|
||||
import androidx.compose.material.icons.rounded.NewReleases
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.lawnchair.preferences2.asState
|
||||
import app.lawnchair.ui.preferences.components.layout.ExpandAndShrink
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate
|
||||
import app.lawnchair.ui.preferences.data.liveinfo.liveInformationManager
|
||||
import app.lawnchair.ui.preferences.data.liveinfo.model.Announcement
|
||||
import app.lawnchair.ui.util.addIf
|
||||
import com.android.launcher3.BuildConfig
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun AnnouncementPreference() {
|
||||
val liveInformationManager = liveInformationManager()
|
||||
|
||||
val enabled by liveInformationManager.enabled.asState()
|
||||
val showAnnouncements by liveInformationManager.showAnnouncements.asState()
|
||||
val liveInformation by liveInformationManager.liveInformation.asState()
|
||||
|
||||
if (enabled && showAnnouncements) {
|
||||
AnnouncementPreference(
|
||||
announcements = liveInformation.announcementsImmutable,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnnouncementPreference(
|
||||
announcements: ImmutableList<Announcement>,
|
||||
) {
|
||||
Column {
|
||||
announcements.forEach { announcement ->
|
||||
AnnouncementItem(announcement)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnnouncementItem(
|
||||
announcement: Announcement,
|
||||
) {
|
||||
var show by remember { mutableStateOf(true) }
|
||||
|
||||
ExpandAndShrink(
|
||||
visible = show && announcement.active &&
|
||||
announcement.text.isNotBlank() &&
|
||||
(!announcement.test || BuildConfig.DEBUG),
|
||||
) {
|
||||
AnnouncementItemContent(
|
||||
text = announcement.text,
|
||||
url = announcement.url,
|
||||
onClose = { show = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnnouncementItemContent(
|
||||
text: String,
|
||||
url: String?,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(16.dp, 0.dp, 16.dp, 0.dp),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
) {
|
||||
AnnouncementPreferenceItemContent(text = text, url = url, onClose = onClose)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnnouncementPreferenceItemContent(
|
||||
text: String,
|
||||
url: String?,
|
||||
onClose: (() -> Unit)?,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val hasLink = !url.isNullOrBlank()
|
||||
|
||||
PreferenceTemplate(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.addIf(hasLink) {
|
||||
clickable {
|
||||
val webpage = Uri.parse(url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, webpage)
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {},
|
||||
description = {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
},
|
||||
startWidget = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.NewReleases,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
endWidget = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
if (hasLink) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Launch,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
if (onClose != null) {
|
||||
IconButton(
|
||||
onClick = onClose,
|
||||
modifier = Modifier.size(16.dp).offset(x = (8).dp, y = (-16).dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InfoPreferenceWithoutLinkPreview() {
|
||||
AnnouncementPreferenceItemContent(
|
||||
text = "Very important announcement ",
|
||||
url = "",
|
||||
onClose = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InfoPreferenceWithLinkPreview() {
|
||||
AnnouncementPreferenceItemContent(
|
||||
text = "Very important announcement with a very important link",
|
||||
url = "https://lawnchair.app/",
|
||||
onClose = null,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package app.lawnchair.ui.preferences.data.liveinfo
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import app.lawnchair.ui.preferences.data.liveinfo.model.LiveInformation
|
||||
import com.android.launcher3.R
|
||||
import com.android.launcher3.util.MainThreadInitializedObject
|
||||
import com.patrykmichalik.opto.core.PreferenceManager
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class LiveInformationManager private constructor(context: Context) : PreferenceManager {
|
||||
|
||||
companion object {
|
||||
private val Context.preferencesDataStore by preferencesDataStore(
|
||||
name = "live-information",
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val INSTANCE = MainThreadInitializedObject(::LiveInformationManager)
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(context: Context) = INSTANCE.get(context)!!
|
||||
}
|
||||
|
||||
override val preferencesDataStore = context.preferencesDataStore
|
||||
|
||||
val enabled = preference(
|
||||
key = booleanPreferencesKey(name = "enabled"),
|
||||
defaultValue = context.resources.getBoolean(R.bool.config_default_live_information_enabled),
|
||||
)
|
||||
|
||||
val showAnnouncements = preference(
|
||||
key = booleanPreferencesKey(name = "show_announcements"),
|
||||
defaultValue = context.resources.getBoolean(R.bool.config_default_live_information_show_announcements),
|
||||
)
|
||||
|
||||
val liveInformation = preference(
|
||||
key = stringPreferencesKey(name = "live_information"),
|
||||
defaultValue = LiveInformation.default,
|
||||
parse = { Json.decodeFromString<LiveInformation>(it) },
|
||||
save = { Json.encodeToString(LiveInformation.serializer(), it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun liveInformationManager() = LiveInformationManager.getInstance(LocalContext.current)
|
||||
@@ -0,0 +1,42 @@
|
||||
package app.lawnchair.ui.preferences.data.liveinfo
|
||||
|
||||
import android.util.Log
|
||||
import app.lawnchair.ui.preferences.data.liveinfo.model.LiveInformation
|
||||
import app.lawnchair.util.kotlinxJson
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
|
||||
private val retrofit = Retrofit.Builder()
|
||||
.baseUrl("https://lawnchair.app/")
|
||||
.addConverterFactory(kotlinxJson.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
val liveInformationService: LiveInformationService = retrofit.create()
|
||||
|
||||
suspend fun getLiveInformation(): LiveInformation? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val response: Response<ResponseBody> = liveInformationService.getLiveInformation()
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val responseBody = response.body()?.string() ?: return@withContext null
|
||||
|
||||
val liveInformation = Json.decodeFromString<LiveInformation>(responseBody)
|
||||
Log.v("LiveInformation", "getLiveInformation: $liveInformation")
|
||||
|
||||
return@withContext liveInformation
|
||||
} else {
|
||||
Log.d("LiveInformation", "getLiveInformation: response code ${response.code()}")
|
||||
return@withContext null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("LiveInformation", "getLiveInformation: Error during news retrieval: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package app.lawnchair.ui.preferences.data.liveinfo
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface LiveInformationService {
|
||||
|
||||
@GET("live-information.json")
|
||||
suspend fun getLiveInformation(): Response<ResponseBody>
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package app.lawnchair.ui.preferences.data.liveinfo
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import app.lawnchair.preferences2.asState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SyncLiveInformation(
|
||||
liveInformationManager: LiveInformationManager = liveInformationManager(),
|
||||
) {
|
||||
val enabled by liveInformationManager.enabled.asState()
|
||||
|
||||
LaunchedEffect(enabled) {
|
||||
if (enabled) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
getLiveInformation()?.let { liveInformation ->
|
||||
liveInformationManager.liveInformation.set(liveInformation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package app.lawnchair.ui.preferences.data.liveinfo.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Announcement(
|
||||
val text: String,
|
||||
val url: String? = null,
|
||||
val active: Boolean = true,
|
||||
val test: Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.lawnchair.ui.preferences.data.liveinfo.model
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LiveInformation(
|
||||
private val announcements: List<Announcement>,
|
||||
) {
|
||||
|
||||
val announcementsImmutable: ImmutableList<Announcement>
|
||||
get() = announcements.toImmutableList()
|
||||
|
||||
companion object {
|
||||
val default = LiveInformation(
|
||||
announcements = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,14 @@ import app.lawnchair.preferences.preferenceManager
|
||||
import app.lawnchair.ui.OverflowMenu
|
||||
import app.lawnchair.ui.preferences.LocalNavController
|
||||
import app.lawnchair.ui.preferences.Routes
|
||||
import app.lawnchair.ui.preferences.components.AnnouncementPreference
|
||||
import app.lawnchair.ui.preferences.components.controls.WarningPreference
|
||||
import app.lawnchair.ui.preferences.components.layout.ClickableIcon
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceCategory
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceDivider
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceLayout
|
||||
import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate
|
||||
import app.lawnchair.ui.preferences.data.liveinfo.SyncLiveInformation
|
||||
import app.lawnchair.ui.preferences.subRoute
|
||||
import app.lawnchair.util.isDefaultLauncher
|
||||
import app.lawnchair.util.restartLauncher
|
||||
@@ -51,12 +53,16 @@ import com.android.launcher3.R
|
||||
@Composable
|
||||
fun PreferencesDashboard() {
|
||||
val context = LocalContext.current
|
||||
SyncLiveInformation()
|
||||
|
||||
PreferenceLayout(
|
||||
label = stringResource(id = R.string.settings),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
backArrowVisible = false,
|
||||
actions = { PreferencesOverflowMenu() },
|
||||
) {
|
||||
AnnouncementPreference()
|
||||
|
||||
if (BuildConfig.DEBUG) PreferencesDebugWarning()
|
||||
|
||||
if (!context.isDefaultLauncher()) {
|
||||
@@ -189,7 +195,7 @@ fun PreferencesDebugWarning() {
|
||||
) {
|
||||
WarningPreference(
|
||||
// Don't move to strings.xml, no need to translate this warning
|
||||
text = "Warning: You are currently using a development build. These builds WILL contain bugs, broken features, and unexpected crashes. Use at your own risk!",
|
||||
text = "You are currently using a development build. Use at your own risk!",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user