Feature: Lock Single Tag for Favorites Widget (#1355)

* single tag for favorites

* fixing

* don't need this one

* cleanup

* new features - select for favorites/tags customization

* bug fix for tags

* bug fix to show pinned tags

* make sure pinned tag works on load

* better way to work with viewmodel (learning as i go)

* back to working state.. can be improved

* adding some comments

* cleanup

---------

Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
rac146
2025-12-21 12:01:06 -05:00
committed by GitHub
parent 97922805aa
commit d6214ef9fa
9 changed files with 437 additions and 120 deletions

View File

@@ -46,6 +46,7 @@ fun FavoritesTagSelector(
compact: Boolean, compact: Boolean,
expanded: Boolean, expanded: Boolean,
onExpand: (Boolean) -> Unit, onExpand: (Boolean) -> Unit,
showFavorites: Boolean
) { ) {
val sheetManager = LocalBottomSheetManager.current val sheetManager = LocalBottomSheetManager.current
@@ -70,40 +71,42 @@ fun FavoritesTagSelector(
.horizontalScroll(scrollState) .horizontalScroll(scrollState)
.padding(end = 12.dp), .padding(end = 12.dp),
) { ) {
FilterChip( if (showFavorites) {
modifier = Modifier FilterChip(
.padding(start = 16.dp), modifier = Modifier
selected = selectedTag == null, .padding(start = 16.dp),
onClick = { onSelectTag(null) }, selected = selectedTag == null,
leadingIcon = if (compact) null else { onClick = { onSelectTag(null) },
{ leadingIcon = if (compact) null else {
Icon( {
painter = painterResource(R.drawable.star_20px_filled), Icon(
contentDescription = null, painter = painterResource(R.drawable.star_20px_filled),
modifier = Modifier.size(FilterChipDefaults.IconSize), contentDescription = null,
) modifier = Modifier.size(FilterChipDefaults.IconSize),
)
}
},
label = {
if (compact) {
Icon(
painter = painterResource(R.drawable.star_20px_filled),
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize),
)
} else {
Text(stringResource(R.string.favorites))
}
} }
}, )
label = { }
if (compact) { for ((i, tag) in tags.withIndex()) {
Icon(
painter = painterResource(R.drawable.star_20px_filled),
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize),
)
} else {
Text(stringResource(R.string.favorites))
}
}
)
for (tag in tags) {
TagChip( TagChip(
modifier = Modifier modifier = Modifier
.padding(start = 8.dp), .padding(start = if (!showFavorites && i == 0) 16.dp else 8.dp),
tag = tag, tag = tag,
selected = selectedTag == tag.tag, selected = selectedTag == tag.tag,
onClick = { onClick = {
if (selectedTag == tag.tag) { if (selectedTag == tag.tag && showFavorites) {
onSelectTag(null) onSelectTag(null)
} else { } else {
onSelectTag(tag.tag) onSelectTag(tag.tag)
@@ -184,7 +187,7 @@ fun FavoritesTagSelector(
compact = compact, compact = compact,
selected = selectedTag == tag.tag, selected = selectedTag == tag.tag,
onClick = { onClick = {
if (selectedTag == tag.tag) { if (selectedTag == tag.tag && showFavorites) {
onSelectTag(null) onSelectTag(null)
} else { } else {
onSelectTag(tag.tag) onSelectTag(tag.tag)

View File

@@ -44,7 +44,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
minPinnedLevel = PinnedLevel.AutomaticallySorted, minPinnedLevel = PinnedLevel.AutomaticallySorted,
).map { ).map {
it.filterIsInstance<Tag>() it.filterIsInstance<Tag>()
} }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
open val favorites: Flow<List<SavableSearchable>> = selectedTag.flatMapLatest { tag -> open val favorites: Flow<List<SavableSearchable>> = selectedTag.flatMapLatest { tag ->
if (tag == null) { if (tag == null) {

View File

@@ -73,6 +73,7 @@ fun LazyListScope.SearchFavorites(
expanded = tagsExpanded, expanded = tagsExpanded,
compact = compactTags, compact = compactTags,
onExpand = onExpandTags, onExpand = onExpandTags,
showFavorites = true
) )
} }
} }

View File

@@ -12,33 +12,56 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.DropdownMenuPopup
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.ToggleButton
import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -52,20 +75,24 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isUnspecified import androidx.compose.ui.unit.isUnspecified
import androidx.compose.ui.zIndex
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import de.mm20.launcher2.calendar.CalendarRepository import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.plugin.PluginRepository import de.mm20.launcher2.plugin.PluginRepository
import de.mm20.launcher2.plugin.PluginType import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.search.Tag
import de.mm20.launcher2.search.calendar.CalendarListType import de.mm20.launcher2.search.calendar.CalendarListType
import de.mm20.launcher2.themes.colors.atTone import de.mm20.launcher2.themes.colors.atTone
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
@@ -74,6 +101,9 @@ import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DragResizeHandle import de.mm20.launcher2.ui.component.DragResizeHandle
import de.mm20.launcher2.ui.component.LargeMessage import de.mm20.launcher2.ui.component.LargeMessage
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.dragndrop.DraggableItem
import de.mm20.launcher2.ui.component.dragndrop.LazyDragAndDropColumn
import de.mm20.launcher2.ui.component.dragndrop.rememberLazyDragAndDropListState
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@@ -89,8 +119,10 @@ import de.mm20.launcher2.widgets.MusicWidget
import de.mm20.launcher2.widgets.NotesWidget import de.mm20.launcher2.widgets.NotesWidget
import de.mm20.launcher2.widgets.WeatherWidget import de.mm20.launcher2.widgets.WeatherWidget
import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.Widget
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koin.compose.koinInject import org.koin.compose.koinInject
import java.text.Collator
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -157,15 +189,16 @@ fun ColumnScope.ConfigureWeatherWidget(
bottom = 8.dp, bottom = 8.dp,
), ),
onClick = { onClick = {
context.startActivity(Intent( context.startActivity(
context, Intent(
SettingsActivity::class.java context,
).apply { SettingsActivity::class.java
putExtra( ).apply {
SettingsActivity.EXTRA_ROUTE, putExtra(
SettingsActivity.ROUTE_WEATHER_INTEGRATION SettingsActivity.EXTRA_ROUTE,
) SettingsActivity.ROUTE_WEATHER_INTEGRATION
}) )
})
}) { }) {
Text(stringResource(R.string.widget_config_weather_integration_settings)) Text(stringResource(R.string.widget_config_weather_integration_settings))
Icon( Icon(
@@ -182,49 +215,342 @@ fun ColumnScope.ConfigureFavoritesWidget(
widget: FavoritesWidget, widget: FavoritesWidget,
onWidgetUpdated: (FavoritesWidget) -> Unit, onWidgetUpdated: (FavoritesWidget) -> Unit,
) { ) {
val customAttrRepository = koinInject<CustomAttributesRepository>()
val allTags by remember {
customAttrRepository
.getAllTags()
.map {
val collator = Collator.getInstance().apply { strength = Collator.SECONDARY }
it
.sortedWith { el1, el2 ->
collator.compare(el1, el2)
}
}
}.collectAsState(emptyList())
val tagsListState = rememberLazyListState()
val bottomSheetManager = LocalBottomSheetManager.current val bottomSheetManager = LocalBottomSheetManager.current
OutlinedCard {
Column( var createTag by remember { mutableStateOf(false) }
modifier = Modifier.fillMaxWidth()
Row(
horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
) {
ToggleButton(
modifier = Modifier.weight(1f),
checked = !widget.config.customTags,
onCheckedChange = {
if (it) {
onWidgetUpdated(widget.copy(config = widget.config.copy(customTags = false)))
}
},
shapes = ButtonGroupDefaults.connectedLeadingButtonShapes(),
) { ) {
SwitchPreference( Icon(
title = stringResource(R.string.preference_edit_button), painterResource(
iconPadding = false, if (!widget.config.customTags) R.drawable.check_20px else R.drawable.star_20px
value = widget.config.editButton, ),
onValueChanged = { contentDescription = null,
onWidgetUpdated(widget.copy(config = widget.config.copy(editButton = it))) modifier = Modifier
} .padding(end = ToggleButtonDefaults.IconSpacing)
.size(ToggleButtonDefaults.IconSize)
) )
SwitchPreference(
title = stringResource(R.string.preference_compact_tags), Text(stringResource(R.string.favorites))
iconPadding = false, }
value = widget.config.compactTags, ToggleButton(
onValueChanged = { modifier = Modifier.weight(1f),
onWidgetUpdated(widget.copy(config = widget.config.copy(compactTags = it))) checked = widget.config.customTags,
onCheckedChange = {
if (it) {
onWidgetUpdated(
widget.copy(
config = widget.config.copy(customTags = true)
)
)
} }
},
shapes = ButtonGroupDefaults.connectedTrailingButtonShapes(),
) {
Icon(
painterResource(
if (widget.config.customTags) R.drawable.check_20px else R.drawable.tag_20px
),
contentDescription = null,
modifier = Modifier
.padding(end = ToggleButtonDefaults.IconSpacing)
.size(ToggleButtonDefaults.IconSize)
) )
Text(stringResource(R.string.preference_screen_tags))
} }
} }
TextButton(
modifier = Modifier AnimatedContent(widget.config.customTags) { showTags ->
.padding(top = 8.dp) Column(
.align(Alignment.End),
contentPadding = PaddingValues(
end = 16.dp,
top = 8.dp,
start = 24.dp,
bottom = 8.dp,
),
onClick = {
bottomSheetManager.showEditFavoritesSheet()
}) {
Text(stringResource(R.string.menu_item_edit_favs))
Icon(
modifier = Modifier modifier = Modifier
.padding(start = ButtonDefaults.IconSpacing) .fillMaxWidth(),
.requiredSize(ButtonDefaults.IconSize), ) {
painter = painterResource(R.drawable.open_in_new_20px), contentDescription = null if (showTags) {
) var tagList by remember {
mutableStateOf(widget.config.tagList.toImmutableList())
}
val availableTags by remember {
derivedStateOf { allTags - tagList }
}
LaunchedEffect(tagList) {
onWidgetUpdated(
widget.copy(
config = widget.config.copy(
tagList = tagList
)
)
)
}
OutlinedCard(
modifier = Modifier.padding(top = 16.dp)
) {
val rowState = rememberLazyDragAndDropListState(
listState = tagsListState,
onItemMove = { from, to ->
val newTagList = tagList.toMutableList()
val tag = newTagList.removeAt(from.index)
newTagList.add(to.index, tag)
tagList = newTagList.toImmutableList()
},
)
LazyDragAndDropColumn(
state = rowState,
modifier = Modifier
.heightIn(max = 9999.dp)
.fillMaxWidth()
.animateContentSize(MaterialTheme.motionScheme.defaultSpatialSpec()),
bidirectionalDrag = false,
) {
items(
tagList,
key = { it }
) { tag ->
DraggableItem(state = rowState, key = tag) {
val elevation by animateDpAsState(if (it) 4.dp else 0.dp)
Surface(
shadowElevation = elevation,
tonalElevation = elevation,
modifier = Modifier.zIndex(if (it) 1f else 0f)
) {
Preference(
title = tag,
icon = R.drawable.tag_24px,
controls = {
IconButton(
modifier = Modifier.offset(x = 8.dp),
onClick = {
tagList = (tagList - tag).toImmutableList()
}
) {
Icon(
painterResource(R.drawable.close_24px),
stringResource(R.string.menu_remove),
)
}
}
)
}
}
}
}
}
var showAddMenu by remember { mutableStateOf(false) }
Box {
FilledTonalButton(
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
onClick = {
showAddMenu = true
},
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
shapes = ButtonDefaults.shapes()
) {
Icon(
painterResource(R.drawable.add_20px),
null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.select_tag))
}
DropdownMenuPopup(
expanded = showAddMenu,
onDismissRequest = { showAddMenu = false }) {
if (availableTags.isNotEmpty()) {
DropdownMenuGroup(
shapes = MenuDefaults.groupShape(0, 2),
) {
for ((i, tag) in availableTags.withIndex()) {
DropdownMenuItem(
shape = if (availableTags.size == 1) MenuDefaults.standaloneItemShape
else when (i) {
0 -> MenuDefaults.leadingItemShape
availableTags.lastIndex -> MenuDefaults.trailingItemShape
else -> MenuDefaults.middleItemShape
},
leadingIcon = {
Icon(
painterResource(R.drawable.tag_24px),
null
)
},
text = { Text(tag) },
onClick = {
tagList = (tagList + tag).toImmutableList()
showAddMenu = false
})
}
}
Spacer(
modifier = Modifier.height(MenuDefaults.GroupSpacing)
)
}
DropdownMenuGroup(
shapes = MenuDefaults.groupShape(
if (availableTags.isEmpty()) 0 else 1,
if (availableTags.isEmpty()) 1 else 2,
)
) {
DropdownMenuItem(
shape = MenuDefaults.standaloneItemShape,
leadingIcon = {
Icon(painterResource(R.drawable.add_24px), null)
},
text = {
Text(
stringResource(R.string.edit_favorites_dialog_new_tag),
)
},
onClick = {
createTag = true
showAddMenu = false
}
)
}
}
}
if (createTag) {
EditTagSheet(
tag = null,
onTagSaved = { tag ->
val newTag = Tag(tag)
tagList = (tagList + newTag.tag).toImmutableList()
},
onDismiss = {
createTag = false
}
)
}
OutlinedCard(
modifier = Modifier.padding(top = 16.dp)
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
SwitchPreference(
title = stringResource(R.string.preference_compact_tags),
iconPadding = false,
value = widget.config.compactTags,
onValueChanged = {
onWidgetUpdated(
widget.copy(
config = widget.config.copy(
compactTags = it
)
)
)
},
enabled = widget.config.tagList.size > 1,
)
}
}
} else {
Column(
modifier = Modifier.fillMaxWidth()
) {
OutlinedCard(
modifier = Modifier.padding(top = 16.dp)
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
SwitchPreference(
title = stringResource(R.string.preference_edit_button),
iconPadding = false,
value = widget.config.editButton,
onValueChanged = {
onWidgetUpdated(
widget.copy(
config = widget.config.copy(
editButton = it
)
)
)
}
)
SwitchPreference(
title = stringResource(R.string.customize_item_tags),
iconPadding = false,
value = widget.config.compactTags,
onValueChanged = {
onWidgetUpdated(
widget.copy(
config = widget.config.copy(
compactTags = it
)
)
)
}
)
}
}
TextButton(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.End),
contentPadding = PaddingValues(
end = 16.dp,
top = 8.dp,
start = 24.dp,
bottom = 8.dp,
),
onClick = {
bottomSheetManager.showEditFavoritesSheet()
}) {
Text(stringResource(R.string.menu_item_edit_favs))
Icon(
modifier = Modifier
.padding(start = ButtonDefaults.IconSpacing)
.requiredSize(ButtonDefaults.IconSize),
painter = painterResource(R.drawable.open_in_new_20px),
contentDescription = null
)
}
}
}
}
} }
} }
@@ -232,7 +558,6 @@ fun ColumnScope.ConfigureFavoritesWidget(
fun ColumnScope.ConfigureMusicWidget( fun ColumnScope.ConfigureMusicWidget(
widget: MusicWidget, widget: MusicWidget,
onWidgetUpdated: (MusicWidget) -> Unit, onWidgetUpdated: (MusicWidget) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -261,15 +586,16 @@ fun ColumnScope.ConfigureMusicWidget(
bottom = 8.dp, bottom = 8.dp,
), ),
onClick = { onClick = {
context.startActivity(Intent( context.startActivity(
context, Intent(
SettingsActivity::class.java context,
).apply { SettingsActivity::class.java
putExtra( ).apply {
SettingsActivity.EXTRA_ROUTE, putExtra(
SettingsActivity.ROUTE_MEDIA_INTEGRATION, SettingsActivity.EXTRA_ROUTE,
) SettingsActivity.ROUTE_MEDIA_INTEGRATION,
}) )
})
}) { }) {
Text(stringResource(R.string.widget_config_music_integration_settings)) Text(stringResource(R.string.widget_config_music_integration_settings))
Icon( Icon(
@@ -504,7 +830,8 @@ fun ColumnScope.ConfigureAppWidget(
modifier = Modifier modifier = Modifier
.padding(start = ButtonDefaults.IconSpacing) .padding(start = ButtonDefaults.IconSpacing)
.requiredSize(ButtonDefaults.IconSize), .requiredSize(ButtonDefaults.IconSize),
painter = painterResource(R.drawable.open_in_new_20px), contentDescription = null painter = painterResource(R.drawable.open_in_new_20px),
contentDescription = null
) )
} }
} }
@@ -670,6 +997,7 @@ fun ConfigureNotesWidget(
onWidgetUpdated: (NotesWidget) -> Unit onWidgetUpdated: (NotesWidget) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val resources = LocalResources.current
val linkFileLauncher = rememberLauncherForActivityResult( val linkFileLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("text/markdown") ActivityResultContracts.CreateDocument("text/markdown")
) { ) {
@@ -733,7 +1061,7 @@ fun ConfigureNotesWidget(
icon = R.drawable.link_24px, icon = R.drawable.link_24px,
onClick = { onClick = {
linkFileLauncher.launch( linkFileLauncher.launch(
context.getString( resources.getString(
R.string.notes_widget_export_filename, R.string.notes_widget_export_filename,
ZonedDateTime.now().format( ZonedDateTime.now().format(
DateTimeFormatter.ISO_INSTANT DateTimeFormatter.ISO_INSTANT

View File

@@ -45,7 +45,7 @@ fun AppWidget(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val lightBackground = (!LocalDarkTheme.current && widget.config.background) || LocalPreferDarkContentOverWallpaper.current val lightBackground = if (widget.config.background) !LocalDarkTheme.current else LocalPreferDarkContentOverWallpaper.current
val widgetInfo = remember(widget.config.widgetId) { val widgetInfo = remember(widget.config.widgetId) {
AppWidgetManager.getInstance(context) AppWidgetManager.getInstance(context)

View File

@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.Tag
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.FavoritesTagSelector import de.mm20.launcher2.ui.common.FavoritesTagSelector
import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.Banner
@@ -24,10 +25,12 @@ fun FavoritesWidget(widget: FavoritesWidget) {
val favorites by remember { viewModel.favorites }.collectAsState(emptyList()) val favorites by remember { viewModel.favorites }.collectAsState(emptyList())
val pinnedTags by viewModel.pinnedTags.collectAsState(emptyList()) val pinnedTags by viewModel.pinnedTags.collectAsState(emptyList())
val selectedTag by viewModel.selectedTag.collectAsState(null) val selectedTag by viewModel.selectedTag.collectAsState(null)
val compactTags by viewModel.compactTags.collectAsState(false)
val favoritesEditButton = widget.config.editButton
val tagsExpanded by viewModel.tagsExpanded.collectAsState(false) val customTags = widget.config.customTags && widget.config.tagList.isNotEmpty()
val favoritesEditButton = !customTags && widget.config.editButton
val compactTags = widget.config.compactTags
val tagsExpanded = widget.config.tagsMultiline
LaunchedEffect(widget) { LaunchedEffect(widget) {
viewModel.updateWidget(widget) viewModel.updateWidget(widget)
@@ -47,9 +50,9 @@ fun FavoritesWidget(widget: FavoritesWidget) {
icon = if (selectedTag == null) R.drawable.star_24px else R.drawable.tag_24px, icon = if (selectedTag == null) R.drawable.star_24px else R.drawable.tag_24px,
) )
} }
if (pinnedTags.isNotEmpty() || favoritesEditButton) { if (favoritesEditButton || (customTags && widget.config.tagList.size > 1)) {
FavoritesTagSelector( FavoritesTagSelector(
tags = pinnedTags, tags = if (customTags) widget.config.tagList.map { Tag(it) } else pinnedTags,
selectedTag = selectedTag, selectedTag = selectedTag,
editButton = favoritesEditButton, editButton = favoritesEditButton,
reverse = false, reverse = false,
@@ -57,7 +60,8 @@ fun FavoritesWidget(widget: FavoritesWidget) {
scrollState = rememberScrollState(), scrollState = rememberScrollState(),
expanded = tagsExpanded, expanded = tagsExpanded,
compact = compactTags, compact = compactTags,
onExpand = { viewModel.setTagsExpanded(it) } onExpand = { viewModel.setTagsExpanded(it) },
showFavorites = !customTags
) )
} }
} }

View File

@@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.launcher.widgets.favorites package de.mm20.launcher2.ui.launcher.widgets.favorites
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.ui.GridSettings import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.preferences.ui.UiSettings import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.services.widgets.WidgetsService import de.mm20.launcher2.services.widgets.WidgetsService
@@ -10,40 +9,16 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.inject import org.koin.core.component.inject
class FavoritesWidgetVM : FavoritesVM() { class FavoritesWidgetVM : FavoritesVM() {
private val widgetsService: WidgetsService by inject() private val widgetsService: WidgetsService by inject()
private val uiSettings: UiSettings by inject()
private val widget = MutableStateFlow<FavoritesWidget?>(null) private val widget = MutableStateFlow<FavoritesWidget?>(null)
override val tagsExpanded = widget.map { it?.config?.tagsMultiline == true } override val tagsExpanded = widget.map { it?.config?.tagsMultiline == true }
override val compactTags: Flow<Boolean> = widget.map { it?.config?.compactTags == true } override val compactTags: Flow<Boolean> = widget.map { it?.config?.compactTags == true }
private val isTopWidget = widgetsService.isFavoritesWidgetFirst()
private val clockWidgetFavSlots =
combine(uiSettings.dock, isTopWidget, uiSettings.gridSettings) { (dock, isTop, grid) ->
dock as Boolean
isTop as Boolean
grid as GridSettings
if (!isTop || !dock) 0
else {
grid.columnCount
}
}
override val favorites = super.favorites.combine(clockWidgetFavSlots) { favs, slots ->
if (selectedTag.value == null) {
if (favs.lastIndex < slots) emptyList()
else favs.subList(slots, favs.size)
} else {
favs
}
}
override fun setTagsExpanded(expanded: Boolean) { override fun setTagsExpanded(expanded: Boolean) {
val widget = this.widget.value ?: return val widget = this.widget.value ?: return
widgetsService.updateWidget( widgetsService.updateWidget(
@@ -54,7 +29,11 @@ class FavoritesWidgetVM : FavoritesVM() {
} }
fun updateWidget(widget: FavoritesWidget) { fun updateWidget(widget: FavoritesWidget) {
selectTag(null)
if (widget.config.customTags) {
selectTag(widget.config.tagList.firstOrNull())
}
this.widget.value = widget this.widget.value = widget
} }
} }

View File

@@ -1070,4 +1070,5 @@
<string name="preference_transliteration">Preferred transliteration</string> <string name="preference_transliteration">Preferred transliteration</string>
<string name="preference_transliteration_auto">Automatic</string> <string name="preference_transliteration_auto">Automatic</string>
<string name="preference_transliteration_disabled">Disabled</string> <string name="preference_transliteration_disabled">Disabled</string>
<string name="select_tag">Select tag</string>
</resources> </resources>

View File

@@ -2,15 +2,16 @@ package de.mm20.launcher2.widgets
import de.mm20.launcher2.database.entities.PartialWidgetEntity import de.mm20.launcher2.database.entities.PartialWidgetEntity
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.UUID import java.util.UUID
@Serializable @Serializable
data class FavoritesWidgetConfig( data class FavoritesWidgetConfig(
val customTags: Boolean = false,
val editButton: Boolean = true, val editButton: Boolean = true,
val tagsMultiline: Boolean = false, val tagsMultiline: Boolean = false,
val compactTags: Boolean = false, val compactTags: Boolean = false,
val tagList: List<String> = emptyList()
) )
data class FavoritesWidget( data class FavoritesWidget(