Add Smartspacer integration
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-keep class com.kieronquinn.app.smartspacer.sdk.** { *; }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
23
app/ui/src/main/res/layout-v29/smartspacer.xml
Normal file
23
app/ui/src/main/res/layout-v29/smartspacer.xml
Normal 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>
|
||||
@@ -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",
|
||||
)
|
||||
)
|
||||
10
core/base/src/main/res/drawable/feedback_24px.xml
Normal file
10
core/base/src/main/res/drawable/feedback_24px.xml
Normal 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>
|
||||
25
core/base/src/main/res/drawable/smartspacer.xml
Normal file
25
core/base/src/main/res/drawable/smartspacer.xml
Normal 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>
|
||||
@@ -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 > Settings > Widgets & Live wallpaper > Send Gadgetbridge data > 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 -->
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user