Add Smartspacer integration

This commit is contained in:
MM20
2026-01-03 00:21:02 +01:00
parent 737eec6eac
commit e313931b4b
20 changed files with 591 additions and 44 deletions

View File

@@ -118,6 +118,10 @@ dependencies {
implementation(libs.coil.core)
implementation(libs.coil.compose)
implementation(libs.smartspacer) {
exclude(group = "com.github.skydoves", module = "balloon")
}
implementation(project(":libs:material-color-utilities"))
implementation(project(":core:base"))

View File

@@ -0,0 +1 @@
-keep class com.kieronquinn.app.smartspacer.sdk.** { *; }

View File

@@ -1,9 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER"/>
<uses-sdk tools:overrideLibrary="com.kieronquinn.app.smartspacer.sdk,com.kieronquinn.app.smartspacer.sdk.client" />
<application>
<activity

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -46,7 +47,7 @@ fun Banner(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.padding(end = 16.dp),
modifier = Modifier.padding(end = 16.dp).size(24.dp),
painter = painterResource(icon),
contentDescription = null
)

View File

@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
@@ -55,6 +56,7 @@ import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@@ -356,6 +358,7 @@ fun ConfigureClockWidgetSheet(
val monospaced by viewModel.monospaced.collectAsState()
val useAccentColor by viewModel.useThemeColor.collectAsState()
val parts by viewModel.parts.collectAsState()
val smartspacer by viewModel.useSmartspacer.collectAsState()
BottomSheetDialog(onDismissRequest = onDismiss) {
Column(
@@ -618,42 +621,60 @@ fun ConfigureClockWidgetSheet(
Column(
modifier = Modifier.fillMaxWidth()
) {
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_date_part),
summary = stringResource(R.string.preference_clockwidget_date_part_summary),
icon = R.drawable.today_24px,
value = parts?.date == true,
onValueChanged = {
viewModel.setDatePart(it)
}
)
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_music_part),
summary = stringResource(R.string.preference_clockwidget_music_part_summary),
icon = R.drawable.music_note_24px,
value = parts?.music == true,
onValueChanged = {
viewModel.setMusicPart(it)
}
)
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_alarm_part),
summary = stringResource(R.string.preference_clockwidget_alarm_part_summary),
icon = R.drawable.alarm_24px,
value = parts?.alarm == true,
onValueChanged = {
viewModel.setAlarmPart(it)
}
)
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_battery_part),
summary = stringResource(R.string.preference_clockwidget_battery_part_summary),
icon = R.drawable.battery_full_24px,
value = parts?.battery == true,
onValueChanged = {
viewModel.setBatteryPart(it)
}
)
if (smartspacer == true) {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.preference_clockwidget_smartspacer),
icon = R.drawable.info_24px,
primaryAction = {
Button(
onClick = {
viewModel.disableSmartspacer()
}
) {
Text(stringResource(R.string.turn_off))
}
}
)
}
if (smartspacer == false) {
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_date_part),
summary = stringResource(R.string.preference_clockwidget_date_part_summary),
icon = R.drawable.today_24px,
value = parts?.date == true,
onValueChanged = {
viewModel.setDatePart(it)
}
)
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_music_part),
summary = stringResource(R.string.preference_clockwidget_music_part_summary),
icon = R.drawable.music_note_24px,
value = parts?.music == true,
onValueChanged = {
viewModel.setMusicPart(it)
}
)
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_alarm_part),
summary = stringResource(R.string.preference_clockwidget_alarm_part_summary),
icon = R.drawable.alarm_24px,
value = parts?.alarm == true,
onValueChanged = {
viewModel.setAlarmPart(it)
}
)
SwitchPreference(
title = stringResource(R.string.preference_clockwidget_battery_part),
summary = stringResource(R.string.preference_clockwidget_battery_part_summary),
icon = R.drawable.battery_full_24px,
value = parts?.battery == true,
onValueChanged = {
viewModel.setBatteryPart(it)
}
)
}
}
}
}

View File

@@ -5,6 +5,7 @@ import android.content.Intent
import android.provider.AlarmClock
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.AlarmPartProvider
@@ -13,6 +14,7 @@ import de.mm20.launcher2.ui.launcher.widgets.clock.parts.DatePartProvider
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.FavoritesPartProvider
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.MusicPartProvider
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.SmartspacerPartProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.channelFlow
@@ -26,12 +28,18 @@ import org.koin.core.component.inject
class ClockWidgetVM : ViewModel(), KoinComponent {
private val settings: ClockWidgetSettings by inject()
private val partProviders = settings.parts.map {
private val partProviders = settings.parts.combine(settings.useSmartspacer) { p, s ->
p to s
}.map { (parts, smartspacer) ->
if (smartspacer && isAtLeastApiLevel(29)) {
return@map listOf(SmartspacerPartProvider())
}
val providers = mutableListOf<PartProvider>()
if (it.date) providers += DatePartProvider()
if (it.music) providers += MusicPartProvider()
if (it.battery) providers += BatteryPartProvider()
if (it.alarm) providers += AlarmPartProvider()
if (parts.date) providers += DatePartProvider()
if (parts.music) providers += MusicPartProvider()
if (parts.battery) providers += BatteryPartProvider()
if (parts.alarm) providers += AlarmPartProvider()
providers
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())

View File

@@ -0,0 +1,255 @@
package de.mm20.launcher2.ui.launcher.widgets.clock.parts
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.DropdownMenuPopup
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MenuDefaults
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.graphics.toArgb
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.kieronquinn.app.smartspacer.sdk.client.SmartspacerClient
import com.kieronquinn.app.smartspacer.sdk.client.views.BcSmartspaceView
import com.kieronquinn.app.smartspacer.sdk.client.views.popup.Popup
import com.kieronquinn.app.smartspacer.sdk.client.views.popup.PopupFactory
import com.kieronquinn.app.smartspacer.sdk.model.SmartspaceTarget
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.flow.flow
@RequiresApi(29)
class SmartspacerPartProvider : PartProvider {
override fun getRanking(context: Context) = flow {
emit(Int.MAX_VALUE)
}
@Composable
override fun Component(compactLayout: Boolean) {
val contentColor = LocalContentColor.current
val popupFactory = remember {
ComposePopupFactory()
}
Box(
modifier = Modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Box(modifier = Modifier.align(Alignment.TopCenter)) {
popupFactory.Popup()
}
AndroidView(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = if (compactLayout) 0.dp else 12.dp)
.height(104.dp),
factory = {
val view = LayoutInflater
.from(it)
.inflate(R.layout.smartspacer, null, false) as BcSmartspaceView
view.setApplyShadowIfRequired(false)
view.setTintColour(contentColor.toArgb())
view.popupFactory = popupFactory
view
},
update = { view ->
view.setTintColour(contentColor.toArgb())
},
onRelease = {
SmartspacerClient.close()
}
)
}
}
private data class PopupState(
val target: SmartspaceTarget,
val launchIntent: (Intent?) -> Unit,
val dismissAction: ((SmartspaceTarget) -> Unit)?,
val aboutIntent: Intent?,
val feedbackIntent: Intent?,
val settingsIntent: Intent?
)
private class ComposePopupFactory : PopupFactory {
var state by mutableStateOf<PopupState?>(null)
override fun createPopup(
context: Context,
anchorView: View,
target: SmartspaceTarget,
backgroundColor: Int,
textColour: Int,
launchIntent: (Intent?) -> Unit,
dismissAction: ((SmartspaceTarget) -> Unit)?,
aboutIntent: Intent?,
feedbackIntent: Intent?,
settingsIntent: Intent?
): Popup {
state = PopupState(
target = target,
launchIntent = launchIntent,
dismissAction = dismissAction,
aboutIntent = aboutIntent,
feedbackIntent = feedbackIntent,
settingsIntent = settingsIntent
)
return object : Popup {
override fun dismiss() {
state = null
}
}
}
@Composable
fun Popup() {
val state = state
if (state != null) {
DropdownMenuPopup(
expanded = true,
onDismissRequest = {
this.state = null
}
) {
val itemCount = (if (state.settingsIntent != null) 1 else 0) +
(if (state.feedbackIntent != null) 1 else 0) +
(if (state.aboutIntent != null) 1 else 0) +
(if (state.dismissAction != null) 1 else 0)
var item = 0
DropdownMenuGroup(
shapes = MenuDefaults.groupShapes(),
) {
if (state.settingsIntent != null) {
DropdownMenuItem(
shape = when {
item == 0 && itemCount - 1 == item -> MenuDefaults.standaloneItemShape
item == 0 -> MenuDefaults.leadingItemShape
item == itemCount - 1 -> MenuDefaults.trailingItemShape
else -> MenuDefaults.middleItemShape
},
text = {
Text(stringResource(R.string.smartspace_long_press_popup_settings))
},
leadingIcon = {
Icon(
painterResource(R.drawable.settings_24px),
null,
)
},
onClick = {
state.launchIntent(state.settingsIntent)
}
)
item++
}
if (state.feedbackIntent != null) {
DropdownMenuItem(
shape = when {
item == 0 && itemCount - 1 == item -> MenuDefaults.standaloneItemShape
item == 0 -> MenuDefaults.leadingItemShape
item == itemCount - 1 -> MenuDefaults.trailingItemShape
else -> MenuDefaults.middleItemShape
},
text = {
Text(stringResource(R.string.smartspace_long_press_popup_feedback))
},
leadingIcon = {
Icon(
painterResource(R.drawable.feedback_24px),
null,
)
},
onClick = {
state.launchIntent(state.feedbackIntent)
}
)
item++
}
if (state.aboutIntent != null) {
DropdownMenuItem(
shape = when {
item == 0 && itemCount - 1 == item -> MenuDefaults.standaloneItemShape
item == 0 -> MenuDefaults.leadingItemShape
item == itemCount - 1 -> MenuDefaults.trailingItemShape
else -> MenuDefaults.middleItemShape
},
text = {
Text(stringResource(R.string.smartspace_long_press_popup_about))
},
leadingIcon = {
Icon(
painterResource(R.drawable.info_24px),
null,
)
},
onClick = {
state.launchIntent(state.aboutIntent)
}
)
item++
}
if (state.dismissAction != null) {
DropdownMenuItem(
shape = when {
item == 0 && itemCount - 1 == item -> MenuDefaults.standaloneItemShape
item == 0 -> MenuDefaults.leadingItemShape
item == itemCount - 1 -> MenuDefaults.trailingItemShape
else -> MenuDefaults.middleItemShape
},
text = {
Text(stringResource(R.string.smartspace_long_press_popup_dismiss))
},
leadingIcon = {
Icon(
painterResource(R.drawable.close_24px),
null,
)
},
onClick = {
state.dismissAction(state.target)
}
)
item++
}
}
}
}
}
}
}

View File

@@ -114,6 +114,8 @@ import de.mm20.launcher2.ui.settings.shapes.ShapeSchemeSettingsRoute
import de.mm20.launcher2.ui.settings.shapes.ShapeSchemeSettingsScreen
import de.mm20.launcher2.ui.settings.shapes.ShapeSchemesSettingsRoute
import de.mm20.launcher2.ui.settings.shapes.ShapeSchemesSettingsScreen
import de.mm20.launcher2.ui.settings.smartspacer.SmartspacerSettingsRoute
import de.mm20.launcher2.ui.settings.smartspacer.SmartspacerSettingsScreen
import de.mm20.launcher2.ui.settings.tags.TagsSettingsRoute
import de.mm20.launcher2.ui.settings.tags.TagsSettingsScreen
import de.mm20.launcher2.ui.settings.tasks.TasksIntegrationSettingsRoute
@@ -300,6 +302,9 @@ class SettingsActivity : BaseActivity() {
entry<AppSearchSettingsRoute> {
AppSearchSettingsScreen()
}
entry<SmartspacerSettingsRoute> {
SmartspacerSettingsScreen()
}
}

View File

@@ -106,4 +106,12 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
fun setAlignment(alignment: ClockWidgetAlignment) {
settings.setAlignment(alignment)
}
val useSmartspacer = settings.useSmartspacer
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun disableSmartspacer() {
settings.setUseSmartspacer(false)
}
}

View File

@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
@@ -13,13 +14,14 @@ import de.mm20.launcher2.ui.settings.breezyweather.BreezyWeatherSettingsRoute
import de.mm20.launcher2.ui.settings.media.MediaIntegrationSettingsRoute
import de.mm20.launcher2.ui.settings.nextcloud.NextcloudSettingsRoute
import de.mm20.launcher2.ui.settings.owncloud.OwncloudSettingsRoute
import de.mm20.launcher2.ui.settings.smartspacer.SmartspacerSettingsRoute
import de.mm20.launcher2.ui.settings.tasks.TasksIntegrationSettingsRoute
import de.mm20.launcher2.ui.settings.weather.WeatherIntegrationSettingsRoute
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsRoute
import kotlinx.serialization.Serializable
@Serializable
data object IntegrationsSettingsRoute: NavKey
data object IntegrationsSettingsRoute : NavKey
@Composable
fun IntegrationsSettingsScreen() {
@@ -68,6 +70,10 @@ fun IntegrationsSettingsScreen() {
backStack.add(WikipediaSettingsRoute)
}
)
}
}
item {
PreferenceCategory {
Preference(
title = stringResource(R.string.preference_tasks_integration),
icon = R.drawable.check_24px_sharp,
@@ -82,6 +88,15 @@ fun IntegrationsSettingsScreen() {
backStack.add(BreezyWeatherSettingsRoute)
}
)
if (isAtLeastApiLevel(29)) {
Preference(
title = stringResource(R.string.preference_smartspacer_integration),
icon = R.drawable.smartspacer,
onClick = {
backStack.add(SmartspacerSettingsRoute)
}
)
}
}
}
}

View File

@@ -0,0 +1,88 @@
package de.mm20.launcher2.ui.settings.smartspacer
import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import kotlinx.serialization.Serializable
@Serializable
data object SmartspacerSettingsRoute : NavKey
@Composable
fun SmartspacerSettingsScreen() {
val viewModel = viewModel<SmartspacerSettingsScreenVM>()
val activity = LocalActivity.current
val isSmartspacerAppInstalled by viewModel.isSmartspacerAppInstalled.collectAsStateWithLifecycle(
null
)
val isSmartspacerEnabled by viewModel.isSmartspacerEnabled.collectAsStateWithLifecycle(null)
PreferenceScreen(
title = stringResource(R.string.preference_smartspacer_integration)
) {
if (isSmartspacerAppInstalled == false) {
item {
PreferenceCategory {
Banner(
text = stringResource(
R.string.preference_smartspacer_integration_description,
stringResource(R.string.app_name)
),
icon = R.drawable.info_24px,
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp),
primaryAction = {
Button(onClick = {
viewModel.downloadSmartspacerApp(activity as AppCompatActivity)
}) {
Text(stringResource(R.string.action_install))
}
}
)
}
}
}
if (isSmartspacerAppInstalled == true) {
item {
PreferenceCategory {
SwitchPreference(
icon = R.drawable.smartspacer,
title = stringResource(R.string.preference_smartspacer_enable),
value = isSmartspacerEnabled == true,
onValueChanged = {
viewModel.setSmartspacerEnabled(it)
}
)
Preference(
title = stringResource(R.string.preference_launch_smartspacer_app),
icon = R.drawable.open_in_new_24px,
onClick = {
viewModel.launchSmartspacerApp(activity as AppCompatActivity)
}
)
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
package de.mm20.launcher2.ui.settings.smartspacer
import android.content.Intent
import android.os.Process
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.getValue
class SmartspacerSettingsScreenVM: ViewModel(), KoinComponent {
private val appRepository: AppRepository by inject()
private val clockWidgetSettings: ClockWidgetSettings by inject()
val smartspacerApp = appRepository.findOne("com.kieronquinn.app.smartspacer", Process.myUserHandle())
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
val isSmartspacerAppInstalled = smartspacerApp
.map { it != null }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
fun downloadSmartspacerApp(activity: AppCompatActivity) {
activity.startActivity(
Intent(Intent.ACTION_VIEW).apply {
data = "https://github.com/KieronQuinn/Smartspacer".toUri()
}
)
}
fun launchSmartspacerApp(activity: AppCompatActivity) {
viewModelScope.launch {
smartspacerApp.first()?.launch(activity, null)
}
}
val isSmartspacerEnabled = clockWidgetSettings.useSmartspacer
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
fun setSmartspacerEnabled(enabled: Boolean) {
clockWidgetSettings.setUseSmartspacer(enabled)
}
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<com.kieronquinn.app.smartspacer.sdk.client.views.BcSmartspaceView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bc_smartspace_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="12px">
<androidx.viewpager.widget.ViewPager
android:id="@+id/smartspace_card_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<com.kieronquinn.app.smartspacer.sdk.client.views.PageIndicator
android:id="@+id/smartspace_page_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|left|center_vertical|center_horizontal|center|start"
android:importantForAccessibility="no"
android:visibility="visible" />
</com.kieronquinn.app.smartspacer.sdk.client.views.BcSmartspaceView>

View File

@@ -197,5 +197,12 @@ val OpenSourceLicenses = arrayOf(
licenseText = R.raw.license_apache_2,
url = "https://chrisbanes.github.io/haze",
copyrightNote = "Copyright 2024 Chris Banes",
),
OpenSourceLibrary(
name = "Smartspacer Client SDK",
description = "A library to integrate Smartspacer in your app",
licenseName = R.string.apache_license_name,
licenseText = R.raw.license_apache_2,
url = "https://chrisbanes.github.io/haze",
)
)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,600Q497,600 508.5,588.5Q520,577 520,560Q520,543 508.5,531.5Q497,520 480,520Q463,520 451.5,531.5Q440,543 440,560Q440,577 451.5,588.5Q463,600 480,600ZM480,440Q497,440 508.5,428.5Q520,417 520,400L520,240Q520,223 508.5,211.5Q497,200 480,200Q463,200 451.5,211.5Q440,223 440,240L440,400Q440,417 451.5,428.5Q463,440 480,440ZM240,720L148,812Q129,831 104.5,820.5Q80,810 80,783L80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L240,720ZM206,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,685L206,640ZM160,640L160,640L160,160Q160,160 160,160Q160,160 160,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640Z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="96dp"
android:height="96dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.6"
android:scaleY="0.6">
<path
android:fillColor="@android:color/black"
android:pathData="M3.5,11.8h2c0.6,0 1,-0.4 1,-1v-2c0,-0.6 -0.4,-1 -1,-1h-2c-0.5,0 -1,0.5 -1,1v2C2.5,11.4 3,11.8 3.5,11.8z" />
<path
android:fillColor="@android:color/black"
android:pathData="M4.5,14.7m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
<path
android:fillColor="@android:color/black"
android:pathData="M7.5,14.7c0,0.6 0.4,1 1,1h12c0.5,0 1,-0.4 1,-1s-0.5,-1 -1,-1h-12C7.9,13.7 7.5,14.1 7.5,14.7z" />
<path
android:fillColor="@android:color/black"
android:pathData="M7.5,9.8c0,0.6 0.4,1 1,1h12c0.5,0 1,-0.4 1,-1s-0.5,-1 -1,-1h-12C7.9,8.8 7.5,9.3 7.5,9.8z" />
</group>
</vector>

View File

@@ -592,6 +592,7 @@
<string name="preference_clock_widget_alignment_center">Center</string>
<string name="preference_clock_widget_alignment_bottom">Bottom</string>
<string name="preference_clockwidget_dynamic_zone">Dynamic zone</string>
<string name="preference_clockwidget_smartspacer">Managed by Smartspacer</string>
<string name="preference_clockwidget_date_part">Date</string>
<string name="preference_clockwidget_date_part_summary">Show the current date</string>
<string name="preference_clockwidget_favorites_part">Dock</string>
@@ -695,6 +696,10 @@
<string name="preference_breezyweather_integration_description">Breezy Weather is a free and open source weather app. If installed, %1$s can use Breezy Weather as a data source for weather data.</string>
<string name="preference_breezyweather_integration_instructions">To use Breezy Weather as weather provider:\n\n1. Go to Breezy Weather &gt; Settings &gt; Widgets &amp; Live wallpaper &gt; Send Gadgetbridge data &gt; Enable %1$s\n\n2. In %1$s, select Breezy Weather as weather provider</string>
<string name="preference_launch_breezyweather_app">Open Breezy Weather</string>
<string name="preference_smartspacer_integration">Smartspacer</string>
<string name="preference_smartspacer_integration_description">Smartspacer is a free and open source At a Glance widget. If installed, it can be integrated into the dynamic zone of %1$s\'s clock widget</string>
<string name="preference_smartspacer_enable">Enable Smartspacer integration</string>
<string name="preference_launch_smartspacer_app">Open Smartspacer</string>
<string name="preference_contacts_call_on_tap">Tap to call</string>
<string name="preference_contacts_call_on_tap_summary">Start a call without confirmation when tapping a phone number</string>
<!-- Used in an info banner if a specific feature requires a Nextcloud account -->

View File

@@ -37,6 +37,8 @@ data class LauncherSettingsData internal constructor(
val mediaDenyList: Set<String> = emptySet(),
val clockWidgetCompact: Boolean = false,
val clockWidgetSmartspacer: Boolean = false,
@Deprecated("")
@SerialName("clockWidgetStyle")
val _clockWidgetStyle: ClockWidgetStyle = ClockWidgetStyle.Digital1(),

View File

@@ -153,6 +153,16 @@ class ClockWidgetSettings internal constructor(
it.copy(clockWidgetUseThemeColor = enabled)
}
}
val useSmartspacer
get() = launcherDataStore.data.map { it.clockWidgetSmartspacer }
fun setUseSmartspacer(enabled: Boolean) {
launcherDataStore.update {
it.copy(clockWidgetSmartspacer = enabled)
}
}
}
internal val ClockWidgetStyle.enumValue

View File

@@ -134,6 +134,8 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
osmopeninghours = { group = "de.westnordost", name = "osm-opening-hours", version.ref = "osmOpeningHours" }
smartspacer = { group = "com.kieronquinn.smartspacer", name = "sdk-client", version = "1.1.2" }
[bundles]
kotlin = ["kotlin-stdlib", "kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-collections-immutable", "kotlinx-serialization-json"]
androidx-lifecycle = ["androidx-lifecycle-viewmodel", "androidx-lifecycle-common", "androidx-lifecycle-runtime", "androidx-lifecycle-viewmodelcompose", "androidx-lifecycle-runtimecompose"]