Reorganize and group modules
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".launcher.LauncherActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LauncherTheme"
|
||||
android:stateNotNeeded="true"
|
||||
android:resumeWhilePausing="true"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".assistant.AssistantActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LauncherTheme"
|
||||
android:stateNotNeeded="true"
|
||||
android:resumeWhilePausing="true"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ASSIST" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings"
|
||||
android:launchMode="singleTask"
|
||||
android:parentActivityName=".launcher.SharedLauncherActivity"
|
||||
android:taskAffinity="de.mm20.launcher2.settings"
|
||||
android:theme="@style/SettingsTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="de.mm20.launcher2.ui.launcher.SharedLauncherActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".launcher.widgets.picker.PickAppWidgetActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:parentActivityName=".launcher.SharedLauncherActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:taskAffinity="de.mm20.launcher2.settings"
|
||||
android:theme="@style/SettingsTheme.NoActionBar">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="de.mm20.launcher2.ui.launcher.SharedLauncherActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.mm20.launcher2.ui.animation
|
||||
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.animateInt
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.BaselineShift
|
||||
import androidx.compose.ui.text.style.TextGeometricTransform
|
||||
|
||||
@Composable
|
||||
fun animateTextStyleAsState(
|
||||
textStyle: TextStyle
|
||||
): State<TextStyle> {
|
||||
val state = remember { mutableStateOf(textStyle) }
|
||||
|
||||
val transition = updateTransition(textStyle, label = "animateTextStyleAsState")
|
||||
|
||||
val color by transition.animateColor(label = "color") {
|
||||
it.color
|
||||
}
|
||||
val fontWeight by transition.animateInt(label = "fontWeight") {
|
||||
it.fontWeight?.weight ?: 400
|
||||
}
|
||||
val fontSize by transition.animateTextUnit {
|
||||
it.fontSize
|
||||
}
|
||||
val letterSpacing by transition.animateTextUnit {
|
||||
it.letterSpacing
|
||||
}
|
||||
val baselineShift by transition.animateFloat(label = "baselineShift") {
|
||||
it.baselineShift?.multiplier ?: 0f
|
||||
}
|
||||
val background by transition.animateColor(label = "background") {
|
||||
it.background
|
||||
}
|
||||
val lineHeight by transition.animateTextUnit {
|
||||
it.lineHeight
|
||||
}
|
||||
|
||||
|
||||
state.value = textStyle.copy(
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
fontWeight = FontWeight(fontWeight),
|
||||
letterSpacing = letterSpacing,
|
||||
baselineShift = BaselineShift(baselineShift),
|
||||
background = background,
|
||||
lineHeight = lineHeight,
|
||||
)
|
||||
|
||||
return state
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.mm20.launcher2.ui.animation
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.unit.ExperimentalUnitApi
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
|
||||
object TextUnitConverter : TwoWayConverter<TextUnit, AnimationVector2D> {
|
||||
override val convertFromVector: (AnimationVector2D) -> TextUnit
|
||||
get() = {
|
||||
TextUnit(
|
||||
it.v1,
|
||||
if (it.v2 > 0.5f) TextUnitType.Em else TextUnitType.Sp
|
||||
)
|
||||
}
|
||||
override val convertToVector: (TextUnit) -> AnimationVector2D
|
||||
get() = {
|
||||
AnimationVector(
|
||||
it.value,
|
||||
if (it.isEm) 1f else 0f
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
inline fun <S> Transition<S>.animateTextUnit(
|
||||
noinline transitionSpec:
|
||||
@Composable Transition.Segment<S>.() -> FiniteAnimationSpec<TextUnit> = { spring() },
|
||||
label: String = "ValueAnimation",
|
||||
targetValueByState: @Composable (state: S) -> TextUnit
|
||||
): State<TextUnit> {
|
||||
return animateValue(
|
||||
typeConverter = TextUnitConverter,
|
||||
label = label,
|
||||
transitionSpec = transitionSpec,
|
||||
targetValueByState = targetValueByState
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.mm20.launcher2.ui.assistant
|
||||
|
||||
import de.mm20.launcher2.ui.launcher.SharedLauncherActivity
|
||||
|
||||
class AssistantActivity: SharedLauncherActivity(LauncherActivityMode.Assistant)
|
||||
@@ -0,0 +1,219 @@
|
||||
package de.mm20.launcher2.ui.assistant
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
|
||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Composable
|
||||
fun AssistantScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
darkStatusBarIcons: Boolean = false,
|
||||
darkNavBarIcons: Boolean = false,
|
||||
) {
|
||||
val viewModel: LauncherScaffoldVM = viewModel()
|
||||
|
||||
var searchBarFocused by remember { mutableStateOf(false) }
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(null) {
|
||||
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
searchBarFocused = false
|
||||
if (!viewModel.autoFocusSearch.first()) return@repeatOnLifecycle
|
||||
delay(100)
|
||||
searchBarFocused = true
|
||||
keyboardController?.show()
|
||||
}
|
||||
}
|
||||
|
||||
val bottomSearchBar by remember {
|
||||
viewModel.dataStore.data.map { it.appearance.layout != Settings.AppearanceSettings.Layout.PullDown }
|
||||
}.collectAsState(null)
|
||||
|
||||
val reverseResults by remember {
|
||||
viewModel.dataStore.data.map { it.appearance.layout != Settings.AppearanceSettings.Layout.PullDown }
|
||||
}.collectAsState(null)
|
||||
|
||||
|
||||
val searchState = rememberLazyListState()
|
||||
|
||||
val isSearchAtStart by remember {
|
||||
derivedStateOf {
|
||||
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
|
||||
}
|
||||
}
|
||||
|
||||
val isSearchAtEnd by remember {
|
||||
derivedStateOf {
|
||||
val lastItem =
|
||||
searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
|
||||
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
|
||||
}
|
||||
}
|
||||
|
||||
if (reverseResults == null || bottomSearchBar == null) return
|
||||
|
||||
|
||||
val searchBarLevel by remember {
|
||||
derivedStateOf {
|
||||
when {
|
||||
reverseResults == bottomSearchBar && isSearchAtStart -> SearchBarLevel.Active
|
||||
reverseResults != bottomSearchBar && isSearchAtEnd -> SearchBarLevel.Active
|
||||
else -> SearchBarLevel.Raised
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val showStatusBarScrim by remember {
|
||||
derivedStateOf {
|
||||
if (reverseResults == true) {
|
||||
!isSearchAtEnd
|
||||
} else {
|
||||
!isSearchAtStart
|
||||
}
|
||||
}
|
||||
}
|
||||
val showNavBarScrim by remember {
|
||||
derivedStateOf {
|
||||
if (reverseResults == true) {
|
||||
!isSearchAtStart
|
||||
} else {
|
||||
!isSearchAtEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val colorSurface = MaterialTheme.colorScheme.surface
|
||||
LaunchedEffect(darkStatusBarIcons, colorSurface, showStatusBarScrim) {
|
||||
if (showStatusBarScrim) {
|
||||
systemUiController.setStatusBarColor(
|
||||
colorSurface.copy(0.7f),
|
||||
)
|
||||
} else {
|
||||
systemUiController.setStatusBarColor(
|
||||
Color.Transparent,
|
||||
darkIcons = darkStatusBarIcons
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WallpaperBlur {
|
||||
true
|
||||
}
|
||||
|
||||
LaunchedEffect(darkNavBarIcons, showNavBarScrim) {
|
||||
if (showNavBarScrim) {
|
||||
systemUiController.setNavigationBarColor(
|
||||
colorSurface.copy(0.7f),
|
||||
)
|
||||
} else {
|
||||
systemUiController.setNavigationBarColor(
|
||||
Color.Transparent,
|
||||
darkIcons = darkNavBarIcons,
|
||||
navigationBarContrastEnforced = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val density = LocalDensity.current
|
||||
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
|
||||
var searchBarOffset by remember {
|
||||
mutableStateOf(0f)
|
||||
}
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val y = available.y * if (reverseResults == true) -1f else 1f
|
||||
searchBarOffset = (searchBarOffset + y).coerceIn(-maxSearchBarOffset, 0f)
|
||||
return super.onPreScroll(available, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchVM: SearchVM = viewModel()
|
||||
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||
val webSearchPadding by animateDpAsState(
|
||||
if (actions.isEmpty()) 0.dp else 48.dp
|
||||
)
|
||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
) {
|
||||
SearchColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
paddingValues = PaddingValues(
|
||||
top = (if (bottomSearchBar == true) 0.dp else 56.dp + webSearchPadding) + 4.dp + windowInsets.calculateTopPadding(),
|
||||
bottom = (if (bottomSearchBar == true) 56.dp + webSearchPadding else 0.dp) + 4.dp + windowInsets.calculateBottomPadding()
|
||||
),
|
||||
reverse = reverseResults == true,
|
||||
state = searchState
|
||||
)
|
||||
|
||||
val value by searchVM.searchQuery.observeAsState("")
|
||||
|
||||
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
|
||||
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
|
||||
|
||||
LauncherSearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.align(if (bottomSearchBar == true) Alignment.BottomCenter else Alignment.TopCenter)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(8.dp)
|
||||
.offset {
|
||||
if (searchBarFocused) IntOffset.Zero
|
||||
else IntOffset(
|
||||
0,
|
||||
searchBarOffset.toInt() * if (bottomSearchBar == true) -1 else 1
|
||||
)
|
||||
},
|
||||
level = { searchBarLevel },
|
||||
focused = searchBarFocused,
|
||||
onFocusChange = {
|
||||
if (it) viewModel.openSearch()
|
||||
viewModel.setSearchbarFocus(it)
|
||||
},
|
||||
actions = actions,
|
||||
showHiddenItemsButton = true,
|
||||
value = { value },
|
||||
onValueChange = { searchVM.search(it) },
|
||||
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
|
||||
style = searchBarStyle,
|
||||
reverse = bottomSearchBar == true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.mm20.launcher2.ui.base
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
permissionsManager.onResume()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.mm20.launcher2.ui.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class BaseActivityVM : ViewModel(), KoinComponent {
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
val theme = dataStore.data.map { it.appearance.theme }.asLiveData()
|
||||
|
||||
fun getTheme(): Settings.AppearanceSettings.Theme = runBlocking {
|
||||
dataStore.data.map { it.appearance.theme }.first()
|
||||
}
|
||||
|
||||
val colorScheme = dataStore.data.map { it.appearance.colorScheme }.asLiveData()
|
||||
|
||||
fun getColorScheme(): Settings.AppearanceSettings.ColorScheme = runBlocking {
|
||||
dataStore.data.map { it.appearance.colorScheme }.first()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.mm20.launcher2.ui.base
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun ProvideCurrentTime(content: @Composable () -> Unit) {
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
var time by remember { mutableStateOf(System.currentTimeMillis()) }
|
||||
|
||||
DisposableEffect(null) {
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
time = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
context.registerReceiver(receiver, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_TIME_TICK)
|
||||
addAction(Intent.ACTION_TIME_CHANGED)
|
||||
})
|
||||
onDispose {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalTime provides time,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
val LocalTime = compositionLocalOf { System.currentTimeMillis() }
|
||||
@@ -0,0 +1,58 @@
|
||||
package de.mm20.launcher2.ui.base
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.component.ProvideIconShape
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.widgets.WidgetRepository
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.androidx.compose.inject
|
||||
|
||||
@Composable
|
||||
fun ProvideSettings(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val dataStore: LauncherDataStore by inject()
|
||||
val widgetRepository: WidgetRepository by inject()
|
||||
|
||||
val cardStyle by remember {
|
||||
dataStore.data.map { it.cards }.distinctUntilChanged()
|
||||
}.collectAsState(
|
||||
Settings.CardSettings.getDefaultInstance()
|
||||
)
|
||||
val iconShape by remember {
|
||||
dataStore.data.map {
|
||||
if (it.easterEgg) Settings.IconSettings.IconShape.EasterEgg
|
||||
else it.icons.shape
|
||||
}.distinctUntilChanged()
|
||||
}.collectAsState(Settings.IconSettings.IconShape.Circle)
|
||||
|
||||
val favoritesEnabled by remember {
|
||||
combine(
|
||||
widgetRepository.isFavoritesWidgetEnabled(),
|
||||
dataStore.data.map { it.favorites.enabled },
|
||||
dataStore.data.map { it.clockWidget.favoritesPart },
|
||||
) { a, b, c -> a || b || c }.distinctUntilChanged()
|
||||
}.collectAsState(true)
|
||||
|
||||
val gridSettings by remember {
|
||||
dataStore.data.map { it.grid }.distinctUntilChanged()
|
||||
}.collectAsState(Settings.GridSettings.newBuilder().setColumnCount(5).setShowLabels(true).setIconSize(48).build())
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalCardStyle provides cardStyle,
|
||||
LocalFavoritesEnabled provides favoritesEnabled,
|
||||
LocalGridSettings provides gridSettings,
|
||||
) {
|
||||
ProvideIconShape(iconShape) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package de.mm20.launcher2.ui.common
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||
import de.mm20.launcher2.data.customattrs.utils.withCustomLabels
|
||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.Tag
|
||||
import de.mm20.launcher2.widgets.WidgetRepository
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
open class FavoritesVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val favoritesRepository: FavoritesRepository by inject()
|
||||
private val widgetRepository: WidgetRepository by inject()
|
||||
private val customAttributesRepository: CustomAttributesRepository by inject()
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
val selectedTag = MutableStateFlow<String?>(null)
|
||||
|
||||
val showEditButton = dataStore.data.map { it.favorites.editButton }
|
||||
|
||||
val pinnedTags = favoritesRepository.getFavorites(
|
||||
includeTypes = listOf("tag"),
|
||||
manuallySorted = true,
|
||||
automaticallySorted = true,
|
||||
).map {
|
||||
it.filterIsInstance<Tag>()
|
||||
}
|
||||
|
||||
val favorites: Flow<List<SavableSearchable>> = selectedTag.flatMapLatest { tag ->
|
||||
if (tag == null) {
|
||||
val columns = dataStore.data.map { it.grid.columnCount }
|
||||
val excludeCalendar = widgetRepository.isCalendarWidgetEnabled()
|
||||
val includeFrequentlyUsed = dataStore.data.map { it.favorites.frequentlyUsed }
|
||||
val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows }
|
||||
|
||||
combine(
|
||||
listOf(
|
||||
columns,
|
||||
excludeCalendar,
|
||||
includeFrequentlyUsed,
|
||||
frequentlyUsedRows
|
||||
)
|
||||
) { it }.transformLatest {
|
||||
|
||||
val columns = it[0] as Int
|
||||
val excludeCalendar = it[1] as Boolean
|
||||
val includeFrequentlyUsed = it[2] as Boolean
|
||||
val frequentlyUsedRows = it[3] as Int
|
||||
|
||||
val pinned = favoritesRepository.getFavorites(
|
||||
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
|
||||
manuallySorted = true,
|
||||
automaticallySorted = true,
|
||||
limit = 10 * columns,
|
||||
)
|
||||
if (includeFrequentlyUsed) {
|
||||
emitAll(pinned.flatMapLatest { pinned ->
|
||||
favoritesRepository.getFavorites(
|
||||
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
|
||||
frequentlyUsed = true,
|
||||
limit = frequentlyUsedRows * columns - pinned.size % columns,
|
||||
).map {
|
||||
pinned + it
|
||||
}
|
||||
.withCustomLabels(customAttributesRepository)
|
||||
})
|
||||
} else {
|
||||
emitAll(
|
||||
pinned.withCustomLabels(customAttributesRepository)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
customAttributesRepository
|
||||
.getItemsForTag(tag)
|
||||
.withCustomLabels(customAttributesRepository)
|
||||
.map { it.sortedBy { it } }
|
||||
}
|
||||
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
|
||||
|
||||
|
||||
fun selectTag(tag: String?) {
|
||||
selectedTag.value = tag
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package de.mm20.launcher2.ui.common
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.backup.BackupCompatibility
|
||||
import de.mm20.launcher2.backup.BackupComponent
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.component.LargeMessage
|
||||
import de.mm20.launcher2.ui.component.SmallMessage
|
||||
|
||||
@Composable
|
||||
fun RestoreBackupSheet(
|
||||
uri: Uri,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
val viewModel: RestoreBackupSheetVM = viewModel()
|
||||
|
||||
LaunchedEffect(uri) {
|
||||
viewModel.setInputUri(uri)
|
||||
}
|
||||
|
||||
val state by viewModel.state.observeAsState(RestoreBackupState.Parsing)
|
||||
val selectedComponents by viewModel.selectedComponents.observeAsState(emptySet())
|
||||
val compatibility by viewModel.compatibility.observeAsState(null)
|
||||
|
||||
BottomSheetDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(id = R.string.preference_restore),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
|
||||
if (state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) {
|
||||
Button(
|
||||
enabled = selectedComponents.isNotEmpty(),
|
||||
onClick = { viewModel.restore() }) {
|
||||
Text(stringResource(R.string.preference_restore))
|
||||
}
|
||||
} else if (state == RestoreBackupState.InvalidFile || state == RestoreBackupState.Restored || state == RestoreBackupState.Ready) {
|
||||
OutlinedButton(
|
||||
onClick = onDismissRequest
|
||||
) {
|
||||
Text(stringResource(R.string.close))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = if (state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) {
|
||||
{
|
||||
OutlinedButton(
|
||||
onClick = onDismissRequest
|
||||
) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
} else null
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (state) {
|
||||
RestoreBackupState.Parsing -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
RestoreBackupState.InvalidFile -> {
|
||||
LargeMessage(
|
||||
modifier = Modifier.aspectRatio(1f),
|
||||
icon = Icons.Rounded.ErrorOutline,
|
||||
text = stringResource(id = R.string.restore_invalid_file)
|
||||
)
|
||||
}
|
||||
RestoreBackupState.Ready -> {
|
||||
val metadata by viewModel.metadata.observeAsState(null)
|
||||
|
||||
if (metadata != null) {
|
||||
Column {
|
||||
SmallMessage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(bottom = 16.dp),
|
||||
icon = Icons.Rounded.Info,
|
||||
text = stringResource(
|
||||
R.string.restore_meta,
|
||||
DateUtils.formatDateTime(
|
||||
LocalContext.current,
|
||||
metadata!!.timestamp,
|
||||
DateUtils.FORMAT_ABBREV_ALL or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR
|
||||
),
|
||||
metadata!!.deviceName,
|
||||
stringResource(R.string.app_name) + " " + metadata!!.appVersionName,
|
||||
)
|
||||
)
|
||||
if (compatibility == BackupCompatibility.Incompatible) {
|
||||
LargeMessage(
|
||||
modifier = Modifier.aspectRatio(1f),
|
||||
icon = Icons.Rounded.ErrorOutline,
|
||||
text = stringResource(
|
||||
id = R.string.restore_incompatible_file,
|
||||
stringResource(R.string.app_name)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
if (compatibility == BackupCompatibility.PartiallyCompatible) {
|
||||
SmallMessage(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
icon = Icons.Rounded.Warning,
|
||||
text =
|
||||
stringResource(
|
||||
R.string.restore_different_minor_version,
|
||||
stringResource(R.string.app_name)
|
||||
)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.restore_select_components),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
|
||||
)
|
||||
val components by viewModel.availableComponents.observeAsState(
|
||||
emptyList()
|
||||
)
|
||||
for (component in components) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
viewModel.toggleComponent(
|
||||
component
|
||||
)
|
||||
}
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (component) {
|
||||
BackupComponent.Favorites -> Icons.Rounded.Star
|
||||
BackupComponent.Settings -> Icons.Rounded.Settings
|
||||
BackupComponent.SearchActions -> Icons.Rounded.ArrowOutward
|
||||
BackupComponent.Widgets -> Icons.Rounded.Widgets
|
||||
BackupComponent.Customizations -> Icons.Rounded.Edit
|
||||
},
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
when (component) {
|
||||
BackupComponent.Favorites -> R.string.backup_component_favorites
|
||||
BackupComponent.Settings -> R.string.backup_component_settings
|
||||
BackupComponent.SearchActions -> R.string.backup_component_searchactions
|
||||
BackupComponent.Widgets -> R.string.backup_component_widgets
|
||||
BackupComponent.Customizations -> R.string.backup_component_customizations
|
||||
}
|
||||
),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
Checkbox(
|
||||
checked = selectedComponents.contains(
|
||||
component
|
||||
),
|
||||
onCheckedChange = {
|
||||
viewModel.toggleComponent(component)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RestoreBackupState.Restoring -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
RestoreBackupState.Restored -> {
|
||||
LargeMessage(
|
||||
modifier = Modifier.aspectRatio(1f),
|
||||
icon = Icons.Rounded.CheckCircleOutline,
|
||||
text = stringResource(
|
||||
id = R.string.restore_complete
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package de.mm20.launcher2.ui.common
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.backup.BackupCompatibility
|
||||
import de.mm20.launcher2.backup.BackupComponent
|
||||
import de.mm20.launcher2.backup.BackupManager
|
||||
import de.mm20.launcher2.backup.BackupMetadata
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class RestoreBackupSheetVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val backupManager: BackupManager by inject()
|
||||
|
||||
private var restoreUri: Uri? = null
|
||||
|
||||
val state = MutableLiveData(RestoreBackupState.Parsing)
|
||||
val metadata = MutableLiveData<BackupMetadata?>(null)
|
||||
val compatibility = MutableLiveData<BackupCompatibility?>(null)
|
||||
val selectedComponents = MutableLiveData(setOf<BackupComponent>())
|
||||
|
||||
val availableComponents = MutableLiveData(emptyList<BackupComponent>())
|
||||
|
||||
fun setInputUri(uri: Uri) {
|
||||
restoreUri = uri
|
||||
state.value = RestoreBackupState.Parsing
|
||||
viewModelScope.launch {
|
||||
val metadata = backupManager.readBackupMeta(uri)
|
||||
if (metadata == null) {
|
||||
state.value = RestoreBackupState.InvalidFile
|
||||
availableComponents.value = emptyList()
|
||||
} else {
|
||||
state.value = RestoreBackupState.Ready
|
||||
compatibility.value = backupManager.checkCompatibility(metadata)
|
||||
availableComponents.value = metadata.components.toList().sortedBy { it.ordinal }
|
||||
}
|
||||
selectedComponents.value = metadata?.components ?: emptySet()
|
||||
this@RestoreBackupSheetVM.metadata.value = metadata
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleComponent(component: BackupComponent) {
|
||||
val components = selectedComponents.value ?: emptySet()
|
||||
if (components.contains(component)) {
|
||||
selectedComponents.value = components - component
|
||||
} else {
|
||||
selectedComponents.value = components + component
|
||||
}
|
||||
}
|
||||
|
||||
fun restore() {
|
||||
val components = selectedComponents.value ?: return
|
||||
val uri = restoreUri ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
state.value = RestoreBackupState.Restoring
|
||||
backupManager.restore(uri, components)
|
||||
state.value = RestoreBackupState.Restored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class RestoreBackupState {
|
||||
Parsing,
|
||||
InvalidFile,
|
||||
Ready,
|
||||
Restoring,
|
||||
Restored,
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package de.mm20.launcher2.ui.common
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Error
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.component.SmallMessage
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun WeatherLocationSearchDialog(
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val viewModel: WeatherLocationSearchDialogVM = viewModel()
|
||||
val isSearching by viewModel.isSearchingLocation.observeAsState(initial = false)
|
||||
val locations by viewModel.locationResults.observeAsState(emptyList())
|
||||
|
||||
BottomSheetDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.preference_location),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = onDismissRequest,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
var query by remember { mutableStateOf("") }
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
singleLine = true,
|
||||
value = query,
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
onValueChange = {
|
||||
query = it
|
||||
scope.launch {
|
||||
viewModel.searchLocation(it)
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Search, contentDescription = null)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
if (isSearching) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
} else if (locations.isNotEmpty()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
items(locations) {
|
||||
Text(
|
||||
text = it.name,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
viewModel.setLocation(it)
|
||||
onDismissRequest()
|
||||
}
|
||||
.padding(
|
||||
vertical = 16.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
} else if (query.isNotEmpty()) {
|
||||
SmallMessage(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
icon = Icons.Rounded.Error,
|
||||
text = stringResource(R.string.weather_location_search_no_result)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.mm20.launcher2.ui.common
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.WeatherRepository
|
||||
import kotlinx.coroutines.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class WeatherLocationSearchDialogVM: ViewModel(), KoinComponent {
|
||||
private val repository: WeatherRepository by inject()
|
||||
|
||||
val isSearchingLocation = MutableLiveData(false)
|
||||
val locationResults = MutableLiveData<List<WeatherLocation>>(emptyList())
|
||||
|
||||
private var debounceSearchJob: Job? = null
|
||||
suspend fun searchLocation(query: String) {
|
||||
debounceSearchJob?.cancelAndJoin()
|
||||
if (query.isBlank()) {
|
||||
locationResults.value = emptyList()
|
||||
isSearchingLocation.value = false
|
||||
return
|
||||
}
|
||||
withContext(coroutineContext) {
|
||||
debounceSearchJob = launch {
|
||||
delay(1000)
|
||||
isSearchingLocation.value = true
|
||||
locationResults.value = repository.lookupLocation(query)
|
||||
isSearchingLocation.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setLocation(location: WeatherLocation) {
|
||||
locationResults.postValue(emptyList())
|
||||
repository.setAutoLocation(false)
|
||||
repository.setLocation(location)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.R
|
||||
|
||||
@Composable
|
||||
fun ExperimentalBadge(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.background(MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small).padding(4.dp)) {
|
||||
Text(stringResource(id = R.string.experimental_feature), color = MaterialTheme.colorScheme.onPrimary, style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.flowlayout.FlowCrossAxisAlignment
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.google.accompanist.flowlayout.MainAxisAlignment
|
||||
|
||||
@Composable
|
||||
fun Banner(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
primaryAction: (@Composable () -> Unit)? = null,
|
||||
secondaryAction: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 16.dp)
|
||||
.padding(end = 16.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
if (secondaryAction != null || primaryAction != null) {
|
||||
FlowRow(
|
||||
Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(8.dp),
|
||||
crossAxisSpacing = 8.dp,
|
||||
crossAxisAlignment = FlowCrossAxisAlignment.End,
|
||||
mainAxisAlignment = MainAxisAlignment.End
|
||||
) {
|
||||
Box {
|
||||
secondaryAction?.invoke()
|
||||
}
|
||||
Box(modifier = Modifier.padding(start = 8.dp)) {
|
||||
primaryAction?.invoke()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.material.FixedThreshold
|
||||
import androidx.compose.material.FractionalThreshold
|
||||
import androidx.compose.material.SwipeableState
|
||||
import androidx.compose.material.swipeable
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun BottomSheetDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
title: @Composable () -> Unit,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
confirmButton: @Composable (() -> Unit)? = null,
|
||||
dismissButton: @Composable (() -> Unit)? = null,
|
||||
swipeToDismiss: () -> Boolean = { true },
|
||||
dismissOnBackPress: () -> Boolean = { true },
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val swipeState = remember {
|
||||
SwipeableState(
|
||||
initialValue = SwipeState.Dismiss,
|
||||
confirmStateChange = {
|
||||
if (it == SwipeState.Dismiss) {
|
||||
if (swipeToDismiss()) onDismissRequest()
|
||||
else return@SwipeableState false
|
||||
}
|
||||
return@SwipeableState true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.toFloat()
|
||||
return if (delta < 0 && source == NestedScrollSource.Drag) {
|
||||
swipeState.performDrag(delta).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
return if (source == NestedScrollSource.Drag) {
|
||||
swipeState.performDrag(available.toFloat()).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
val toFling = Offset(available.x, available.y).toFloat()
|
||||
return if (toFling < 0 && swipeState.offset.value > 0) {
|
||||
swipeState.performFling(velocity = toFling)
|
||||
// since we go to the anchor with tween settling, consume all for the best UX
|
||||
available
|
||||
} else {
|
||||
Velocity.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
swipeState.performFling(velocity = Offset(available.x, available.y).toFloat())
|
||||
return available
|
||||
}
|
||||
|
||||
private fun Float.toOffset(): Offset = Offset(0f, this)
|
||||
|
||||
private fun Offset.toFloat(): Float = this.y
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnClickOutside = true,
|
||||
dismissOnBackPress = dismissOnBackPress()
|
||||
),
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
propagateMinConstraints = true,
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
val maxHeightPx = maxHeight.toPixels()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(Alignment.Bottom)
|
||||
.clipToBounds(),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
var height by remember {
|
||||
mutableStateOf(maxHeightPx)
|
||||
}
|
||||
|
||||
LaunchedEffect(null) {
|
||||
swipeState.animateTo(SwipeState.Peek)
|
||||
}
|
||||
|
||||
val heightDp = height.toDp()
|
||||
val peekHeight = (height - maxHeightPx / 2).coerceAtLeast(0f)
|
||||
val anchors = mutableMapOf(
|
||||
peekHeight to SwipeState.Peek,
|
||||
height to SwipeState.Dismiss,
|
||||
).also {
|
||||
if (peekHeight > 0f) {
|
||||
it[0f] = SwipeState.Full
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.swipeable(
|
||||
swipeState,
|
||||
anchors = anchors,
|
||||
orientation = Orientation.Vertical,
|
||||
thresholds = { _, to ->
|
||||
if (to == SwipeState.Dismiss) {
|
||||
FixedThreshold(heightDp - 48.dp)
|
||||
} else {
|
||||
FractionalThreshold(0.5f)
|
||||
}
|
||||
},
|
||||
resistance = null
|
||||
)
|
||||
.animateContentSize()
|
||||
.onSizeChanged {
|
||||
height = it.height.toFloat()
|
||||
}
|
||||
.offset { IntOffset(0, swipeState.offset.value.roundToInt()) }
|
||||
.fillMaxWidth()
|
||||
.weight(1f, false),
|
||||
shape = MaterialTheme.shapes.extraLarge.copy(
|
||||
bottomStart = CornerSize(0),
|
||||
bottomEnd = CornerSize(0),
|
||||
),
|
||||
shadowElevation = 16.dp,
|
||||
) {
|
||||
Column {
|
||||
CenterAlignedTopAppBar(
|
||||
title = title,
|
||||
actions = actions,
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
propagateMinConstraints = true,
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmButton != null || dismissButton != null) {
|
||||
val elevation by animateDpAsState(if (swipeState.offset.value == 0f) 0.dp else 1.dp)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth(),
|
||||
tonalElevation = elevation,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
if (dismissButton != null) {
|
||||
dismissButton()
|
||||
}
|
||||
if (confirmButton != null && dismissButton != null) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
if (confirmButton != null) {
|
||||
confirmButton()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class SwipeState {
|
||||
Full, Peek, Dismiss
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun Chip(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
modifier = modifier,
|
||||
color = Color.Transparent,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(32.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.labelLarge,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onSurface
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Chip(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
onClick: () -> Unit = {},
|
||||
icon: Painter? = null,
|
||||
rightIcon: ImageVector? = null,
|
||||
rightAction: (() -> Unit)? = null
|
||||
) {
|
||||
Chip(
|
||||
modifier = modifier.width(IntrinsicSize.Max),
|
||||
onClick = onClick
|
||||
) {
|
||||
if (icon != null) {
|
||||
Image(
|
||||
modifier = Modifier.padding(horizontal = 6.dp).size(20.dp),
|
||||
painter = icon,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, false)
|
||||
.padding(
|
||||
start = if (icon == null) 12.dp else 4.dp,
|
||||
end = if (rightIcon == null) 12.dp else 4.dp,
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
text = text)
|
||||
if (rightIcon != null) {
|
||||
if (rightAction != null) {
|
||||
IconButton(
|
||||
modifier = Modifier.size(32.dp),
|
||||
onClick = rightAction
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = rightIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier.padding(horizontal = 6.dp).size(20.dp),
|
||||
imageVector = rightIcon,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
import kotlin.math.ln
|
||||
|
||||
@Composable
|
||||
fun InnerCard(
|
||||
modifier: Modifier = Modifier,
|
||||
raised: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val transition = updateTransition(raised, label = "InnerCard")
|
||||
|
||||
val absoluteTonalElevation = LocalAbsoluteTonalElevation.current
|
||||
|
||||
val elevation by transition.animateDp(label = "elevation", transitionSpec = {
|
||||
tween(250, if (targetState) 250 else 0)
|
||||
}) {
|
||||
if(it) 4.dp else 0.dp
|
||||
}
|
||||
|
||||
val borderWidth by transition.animateDp(label = "borderWidth", transitionSpec = { tween(500) }) {
|
||||
if (it) 0.dp else 1.dp
|
||||
}
|
||||
val borderColor by transition.animateColor(label = "borderColor", transitionSpec = { tween(500) }) {
|
||||
MaterialTheme.colorScheme.outline.copy(alpha = if (it) 0f else 0.7f)
|
||||
}
|
||||
val bgAlpha by transition.animateFloat(label = "bgAlpha", transitionSpec = {
|
||||
tween(250, if (targetState) 0 else 250)
|
||||
}) {
|
||||
if (it) 1f else 0f
|
||||
}
|
||||
|
||||
val shape = MaterialTheme.shapes.small
|
||||
|
||||
val bgColor = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteTonalElevation + elevation)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.border(BorderStroke(borderWidth, borderColor), shape)
|
||||
.shadow(elevation, shape, clip = false)
|
||||
.clip(shape)
|
||||
.drawBehind {
|
||||
drawRect(
|
||||
bgColor.copy(alpha = bgAlpha)
|
||||
)
|
||||
}
|
||||
,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalAbsoluteTonalElevation provides absoluteTonalElevation
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ColorScheme.surfaceColorAtElevation(
|
||||
elevation: Dp,
|
||||
): Color {
|
||||
if (elevation == 0.dp) return surface
|
||||
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
|
||||
return surfaceTint.copy(alpha = alpha).compositeOver(surface)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun LargeMessage(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
text: String
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 24.dp)
|
||||
.size(64.dp)
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.drawOutline
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
|
||||
@Composable
|
||||
fun LauncherCard(
|
||||
modifier: Modifier = Modifier,
|
||||
elevation: Dp = 2.dp,
|
||||
backgroundOpacity: Float = LocalCardStyle.current.opacity,
|
||||
shape: Shape = MaterialTheme.shapes.medium,
|
||||
content: @Composable () -> Unit = {}
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = shape,
|
||||
border = LocalCardStyle.current.borderWidth.takeIf { it > 0 }
|
||||
?.let { BorderStroke(it.dp, MaterialTheme.colorScheme.surface) },
|
||||
content = content,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
|
||||
shadowElevation = if (backgroundOpacity == 1f) elevation else 0.dp,
|
||||
tonalElevation = elevation,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PartialLauncherCard(
|
||||
modifier: Modifier = Modifier,
|
||||
isTop: Boolean = false,
|
||||
isBottom: Boolean = false,
|
||||
elevation: Dp = 2.dp,
|
||||
backgroundOpacity: Float = LocalCardStyle.current.opacity,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
|
||||
if (isTop && isBottom) {
|
||||
LauncherCard(modifier = modifier, content = content)
|
||||
} else if (!isTop && !isBottom) {
|
||||
CardMiddlePiece(modifier = modifier, elevation = elevation, content = content)
|
||||
} else {
|
||||
CardEndPiece(
|
||||
modifier = modifier,
|
||||
isTop = isTop,
|
||||
isBottom = isBottom,
|
||||
elevation = elevation,
|
||||
backgroundOpacity = backgroundOpacity,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardMiddlePiece(
|
||||
modifier: Modifier,
|
||||
elevation: Dp,
|
||||
backgroundOpacity: Float = LocalCardStyle.current.opacity,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val borderWidth = LocalCardStyle.current.borderWidth.dp
|
||||
val borderColor = MaterialTheme.colorScheme.surface
|
||||
|
||||
val absoluteElevation = LocalAbsoluteTonalElevation.current + elevation
|
||||
Box(
|
||||
modifier = modifier
|
||||
.shadow(if (backgroundOpacity < 1f) 0.dp else elevation, RectangleShape, true)
|
||||
.background(
|
||||
if (backgroundOpacity == 1f) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface.copy(
|
||||
alpha = backgroundOpacity.coerceIn(
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.drawBehind {
|
||||
if (borderWidth == 0.dp) return@drawBehind
|
||||
val border = borderWidth.toPx()
|
||||
drawRect(
|
||||
color = borderColor,
|
||||
topLeft = Offset.Zero,
|
||||
size = size.copy(width = border)
|
||||
)
|
||||
drawRect(
|
||||
color = borderColor,
|
||||
topLeft = Offset(size.width - border, 0f),
|
||||
size = size.copy(width = border)
|
||||
)
|
||||
},
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onSurface,
|
||||
LocalAbsoluteTonalElevation provides absoluteElevation,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardEndPiece(
|
||||
modifier: Modifier = Modifier,
|
||||
isTop: Boolean,
|
||||
isBottom: Boolean,
|
||||
elevation: Dp,
|
||||
backgroundOpacity: Float,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val shape = when {
|
||||
isTop -> MaterialTheme.shapes.medium.copy(
|
||||
bottomEnd = CornerSize(0),
|
||||
bottomStart = CornerSize(0),
|
||||
)
|
||||
isBottom -> MaterialTheme.shapes.medium.copy(
|
||||
topEnd = CornerSize(0),
|
||||
topStart = CornerSize(0),
|
||||
)
|
||||
else -> RectangleShape
|
||||
}
|
||||
|
||||
val borderWidth = LocalCardStyle.current.borderWidth.dp
|
||||
val borderColor = MaterialTheme.colorScheme.surface
|
||||
|
||||
val absoluteElevation = LocalAbsoluteTonalElevation.current + elevation
|
||||
Box(
|
||||
modifier = modifier
|
||||
.shadow(if (backgroundOpacity < 1f) 0.dp else elevation, shape, true)
|
||||
.background(
|
||||
if (backgroundOpacity == 1f) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface.copy(
|
||||
alpha = backgroundOpacity.coerceIn(
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.drawWithCache {
|
||||
val border = borderWidth.toPx()
|
||||
val outline = shape.createOutline(
|
||||
size.copy(height = size.height + border),
|
||||
layoutDirection,
|
||||
Density(density, fontScale)
|
||||
)
|
||||
onDrawBehind {
|
||||
if (borderWidth == 0.dp) return@onDrawBehind
|
||||
withTransform({
|
||||
translate(0f, if (isBottom) -border else 0f)
|
||||
}) {
|
||||
drawOutline(
|
||||
outline,
|
||||
borderColor,
|
||||
style = Stroke(width = border * 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onSurface,
|
||||
LocalAbsoluteTonalElevation provides absoluteElevation,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Lock
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.R
|
||||
|
||||
@Composable
|
||||
fun MissingPermissionBanner(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
secondaryAction: @Composable () -> Unit = {}
|
||||
) {
|
||||
Banner(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = Icons.Rounded.Lock,
|
||||
primaryAction = {
|
||||
Button(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
onClick = onClick
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.grant_permission),
|
||||
)
|
||||
}
|
||||
},
|
||||
secondaryAction = secondaryAction
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.BatteryManager
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.android.awaitFrame
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun NavBarEffects(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var intensity by remember { mutableStateOf(0) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DisposableEffect(null) {
|
||||
|
||||
suspend fun update(intent: Intent?, retryOnZeroCurrent: Boolean = false) {
|
||||
if (intent == null) return
|
||||
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||
val charging =
|
||||
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
||||
if (charging) {
|
||||
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
|
||||
val current = bm.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
|
||||
if (current <= 0) {
|
||||
intensity = 5
|
||||
//Workaround for delayed current updates
|
||||
if (retryOnZeroCurrent) {
|
||||
delay(1000)
|
||||
update(intent)
|
||||
}
|
||||
return
|
||||
}
|
||||
intensity = (current / 100000f).roundToInt().takeIf { it > 0 } ?: 1
|
||||
} else {
|
||||
intensity = 0
|
||||
}
|
||||
}
|
||||
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
scope.launch {
|
||||
update(intent, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||
|
||||
val intent = context.registerReceiver(receiver, intentFilter)
|
||||
scope.launch {
|
||||
update(intent, true)
|
||||
}
|
||||
|
||||
onDispose {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
var bubbles by remember { mutableStateOf(arrayOf<Bubble>()) }
|
||||
|
||||
val dp = LocalDensity.current.density
|
||||
|
||||
LaunchedEffect(intensity) {
|
||||
bubbles = Array(intensity) {
|
||||
FloatArray(6) { 0f }
|
||||
}
|
||||
if (intensity == 0) return@LaunchedEffect
|
||||
while (isActive) {
|
||||
val newBubbles = Array(intensity) { FloatArray(6) { 0f } }
|
||||
withInfiniteAnimationFrameMillis {}
|
||||
for (i in 0 until intensity) {
|
||||
val bubble = newBubbles[i]
|
||||
val oldBubble = bubbles[i]
|
||||
if (oldBubble.lifetime <= 0f) {
|
||||
bubble.posX = (Math.random() * 48 - 24).toFloat() * dp
|
||||
bubble.posY = 0f
|
||||
bubble.deltaX = (Math.random() - 0.5).toFloat() * 1f * dp
|
||||
bubble.deltaY = Math.random().toFloat() * 3f * dp
|
||||
bubble.radius = (Math.random() * 2 + 2).toFloat() * dp
|
||||
bubble.lifetime = (Math.random() * 80 + 40).toInt().toFloat()
|
||||
} else {
|
||||
bubble.posX = oldBubble.deltaX + oldBubble.posX
|
||||
bubble.posY = oldBubble.deltaY + oldBubble.posY
|
||||
bubble.lifetime = oldBubble.lifetime - 1
|
||||
bubble.deltaX = oldBubble.deltaX
|
||||
bubble.deltaY = oldBubble.deltaY
|
||||
bubble.radius = oldBubble.radius
|
||||
}
|
||||
}
|
||||
bubbles = newBubbles
|
||||
}
|
||||
}
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
for (bubble in bubbles) {
|
||||
val x = size.width / 2 + bubble.posX
|
||||
val y = size.height - bubble.posY
|
||||
drawCircle(
|
||||
color = Color.White,
|
||||
radius = bubble.radius,
|
||||
alpha = bubble.lifetime / 255,
|
||||
center = Offset(x, y)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias Bubble = FloatArray
|
||||
|
||||
inline var Bubble.posX: Float
|
||||
get() = this[0]
|
||||
set(value) {
|
||||
this[0] = value
|
||||
}
|
||||
|
||||
inline var Bubble.posY: Float
|
||||
get() = this[1]
|
||||
set(value) {
|
||||
this[1] = value
|
||||
}
|
||||
|
||||
inline var Bubble.deltaX: Float
|
||||
get() = this[2]
|
||||
set(value) {
|
||||
this[2] = value
|
||||
}
|
||||
|
||||
inline var Bubble.deltaY: Float
|
||||
get() = this[3]
|
||||
set(value) {
|
||||
this[3] = value
|
||||
}
|
||||
|
||||
inline var Bubble.radius: Float
|
||||
get() = this[4]
|
||||
set(value) {
|
||||
this[4] = value
|
||||
}
|
||||
|
||||
inline var Bubble.lifetime: Float
|
||||
get() = this[5]
|
||||
set(value) {
|
||||
this[5] = value
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Clear
|
||||
import androidx.compose.material.icons.rounded.Tag
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun OutlinedTagsInputField(
|
||||
modifier: Modifier = Modifier,
|
||||
tags: List<String>,
|
||||
onTagsChange: (tags: List<String>) -> Unit,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
textStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
textColor: Color = LocalContentColor.current,
|
||||
onAutocomplete: (suspend (query: String) -> List<String>)? = null
|
||||
) {
|
||||
var value by remember { mutableStateOf("") }
|
||||
var lastTagFocused by remember { mutableStateOf(false) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var completions by remember(onAutocomplete) { mutableStateOf<List<String>>(emptyList()) }
|
||||
|
||||
BasicTextField(
|
||||
modifier = modifier
|
||||
.onKeyEvent {
|
||||
if (it.key == Key.Backspace && value.isEmpty() && tags.isNotEmpty()) {
|
||||
if (!lastTagFocused) {
|
||||
lastTagFocused = true
|
||||
} else {
|
||||
onTagsChange(tags.dropLast(1))
|
||||
lastTagFocused = false
|
||||
}
|
||||
return@onKeyEvent true
|
||||
}
|
||||
lastTagFocused = false
|
||||
false
|
||||
}
|
||||
.onFocusChanged {
|
||||
if (!it.hasFocus && value.isNotBlank()) {
|
||||
onTagsChange((tags + value).toImmutableList())
|
||||
value = ""
|
||||
} else if (it.hasFocus) {
|
||||
scope.launch {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = value, onValueChange = {
|
||||
val newTags = it.split(",")
|
||||
if (newTags.size > 1) {
|
||||
onTagsChange(tags + newTags.dropLast(1).filter { it.isNotBlank() })
|
||||
}
|
||||
value = newTags.last()
|
||||
if (value.isNotBlank()) {
|
||||
onAutocomplete?.let {
|
||||
scope.launch {
|
||||
completions = it(value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completions = emptyList()
|
||||
}
|
||||
lastTagFocused = false
|
||||
},
|
||||
textStyle = textStyle.copy(
|
||||
color = textColor
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
singleLine = true,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
if (value.isNotBlank()) {
|
||||
onTagsChange(tags + value)
|
||||
value = ""
|
||||
}
|
||||
}),
|
||||
decorationBox = { innerTextField ->
|
||||
TextFieldDefaults.OutlinedTextFieldDecorationBox(
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
value = value,
|
||||
innerTextField = {
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
for ((i, tag) in tags.withIndex()) {
|
||||
InputChip(
|
||||
selected = i == tags.lastIndex && lastTagFocused,
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
onClick = { },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Tag,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(tag) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.clickable {
|
||||
onTagsChange(tags.filterIndexed { index, _ -> index != i })
|
||||
},
|
||||
imageVector = Icons.Rounded.Clear,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(56.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides textStyle,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
) {
|
||||
placeholder?.invoke()
|
||||
}
|
||||
}
|
||||
innerTextField()
|
||||
|
||||
}
|
||||
}
|
||||
if (completions.isNotEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
DropdownMenu(
|
||||
expanded = true,
|
||||
onDismissRequest = { completions = emptyList() },
|
||||
properties = PopupProperties(focusable = false)
|
||||
) {
|
||||
for (completion in completions) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(completion) },
|
||||
onClick = {
|
||||
onTagsChange(tags + completion)
|
||||
value = ""
|
||||
completions = emptyList()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = true,
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
},
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Alarm
|
||||
import androidx.compose.material.icons.rounded.Call
|
||||
import androidx.compose.material.icons.rounded.Email
|
||||
import androidx.compose.material.icons.rounded.Error
|
||||
import androidx.compose.material.icons.rounded.Event
|
||||
import androidx.compose.material.icons.rounded.FindInPage
|
||||
import androidx.compose.material.icons.rounded.Games
|
||||
import androidx.compose.material.icons.rounded.ImageSearch
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
import androidx.compose.material.icons.rounded.LocationOn
|
||||
import androidx.compose.material.icons.rounded.ManageSearch
|
||||
import androidx.compose.material.icons.rounded.Movie
|
||||
import androidx.compose.material.icons.rounded.MusicNote
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.PersonAdd
|
||||
import androidx.compose.material.icons.rounded.PersonSearch
|
||||
import androidx.compose.material.icons.rounded.Place
|
||||
import androidx.compose.material.icons.rounded.QueryStats
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.icons.rounded.Sms
|
||||
import androidx.compose.material.icons.rounded.SportsEsports
|
||||
import androidx.compose.material.icons.rounded.StickyNote2
|
||||
import androidx.compose.material.icons.rounded.Timer
|
||||
import androidx.compose.material.icons.rounded.Translate
|
||||
import androidx.compose.material.icons.rounded.TravelExplore
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.searchactions.actions.AppSearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import de.mm20.launcher2.searchactions.builders.AppSearchActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun SearchActionIcon(
|
||||
icon: SearchActionIcon,
|
||||
color: Int = 0,
|
||||
customIcon: String? = null,
|
||||
componentName: ComponentName? = null,
|
||||
size: Dp = 20.dp
|
||||
) {
|
||||
val tint = when(color) {
|
||||
0 -> MaterialTheme.colorScheme.primary
|
||||
1 -> Color.Unspecified
|
||||
else -> Color(color)
|
||||
}
|
||||
if (icon != SearchActionIcon.Custom || customIcon == null && componentName == null) {
|
||||
Icon(
|
||||
modifier = Modifier.size(size),
|
||||
imageVector = getSearchActionIconVector(icon),
|
||||
contentDescription = null,
|
||||
tint = tint,
|
||||
)
|
||||
} else if (customIcon == null && componentName != null) {
|
||||
val context = LocalContext.current
|
||||
var drawable by remember(componentName) { mutableStateOf<Drawable?>(null) }
|
||||
|
||||
LaunchedEffect(componentName) {
|
||||
drawable = withContext(Dispatchers.IO) {
|
||||
context.packageManager.getActivityIcon(componentName)
|
||||
}
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = drawable,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size),
|
||||
colorFilter = if (tint.isSpecified) ColorFilter.tint(tint) else null
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = customIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size),
|
||||
colorFilter = if (tint.isSpecified) ColorFilter.tint(tint) else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchActionIcon(action: SearchAction, size: Dp = 20.dp) {
|
||||
SearchActionIcon(
|
||||
icon = action.icon,
|
||||
color = action.iconColor,
|
||||
customIcon = action.customIcon,
|
||||
componentName = (action as? AppSearchAction)?.baseIntent?.component,
|
||||
size = size,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchActionIcon(builder: CustomizableSearchActionBuilder, size: Dp = 20.dp) {
|
||||
SearchActionIcon(
|
||||
icon = builder.icon,
|
||||
color = builder.iconColor,
|
||||
customIcon = builder.customIcon,
|
||||
componentName = (builder as? AppSearchActionBuilder)?.baseIntent?.component,
|
||||
size = size,
|
||||
)
|
||||
}
|
||||
|
||||
fun getSearchActionIconVector(icon: SearchActionIcon): ImageVector {
|
||||
return when (icon) {
|
||||
SearchActionIcon.Phone -> Icons.Rounded.Call
|
||||
SearchActionIcon.Website -> Icons.Rounded.Language
|
||||
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
|
||||
SearchActionIcon.Timer -> Icons.Rounded.Timer
|
||||
SearchActionIcon.Contact -> Icons.Rounded.PersonAdd
|
||||
SearchActionIcon.Email -> Icons.Rounded.Email
|
||||
SearchActionIcon.Message -> Icons.Rounded.Sms
|
||||
SearchActionIcon.Calendar -> Icons.Rounded.Event
|
||||
SearchActionIcon.Translate -> Icons.Rounded.Translate
|
||||
SearchActionIcon.Search -> Icons.Rounded.Search
|
||||
SearchActionIcon.Custom -> Icons.Rounded.Warning
|
||||
SearchActionIcon.WebSearch -> Icons.Rounded.TravelExplore
|
||||
SearchActionIcon.PersonSearch -> Icons.Rounded.PersonSearch
|
||||
SearchActionIcon.StatsSearch -> Icons.Rounded.QueryStats
|
||||
SearchActionIcon.SearchPage -> Icons.Rounded.FindInPage
|
||||
SearchActionIcon.SearchList -> Icons.Rounded.ManageSearch
|
||||
SearchActionIcon.ImageSearch -> Icons.Rounded.ImageSearch
|
||||
SearchActionIcon.Location -> Icons.Rounded.Place
|
||||
SearchActionIcon.Movie -> Icons.Rounded.Movie
|
||||
SearchActionIcon.Music -> Icons.Rounded.MusicNote
|
||||
SearchActionIcon.Game -> Icons.Rounded.SportsEsports
|
||||
SearchActionIcon.Note -> Icons.Rounded.StickyNote2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
modifier: Modifier = Modifier,
|
||||
style: Settings.SearchBarSettings.SearchBarStyle,
|
||||
level: SearchBarLevel,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
onFocus: () -> Unit = {},
|
||||
onUnfocus: () -> Unit = {},
|
||||
reverse: Boolean = false,
|
||||
darkColors: Boolean = false,
|
||||
menu: @Composable RowScope.() -> Unit = {},
|
||||
actions: @Composable ColumnScope.() -> Unit = {},
|
||||
) {
|
||||
|
||||
val transition = updateTransition(level, label = "Searchbar")
|
||||
|
||||
val elevation by transition.animateDp(
|
||||
label = "elevation",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
targetState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}
|
||||
) {
|
||||
when {
|
||||
it == SearchBarLevel.Resting && style != Settings.SearchBarSettings.SearchBarStyle.Solid -> 0.dp
|
||||
it == SearchBarLevel.Raised -> 8.dp
|
||||
else -> 2.dp
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundOpacity by transition.animateFloat(label = "backgroundOpacity",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
targetState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
else -> tween(durationMillis = 200)
|
||||
}
|
||||
}) {
|
||||
when {
|
||||
it == SearchBarLevel.Active -> LocalCardStyle.current.opacity
|
||||
style != Settings.SearchBarSettings.SearchBarStyle.Transparent -> 1f
|
||||
it == SearchBarLevel.Resting -> 0f
|
||||
else -> 1f
|
||||
}
|
||||
}
|
||||
|
||||
val contentColor by transition.animateColor(label = "textColor",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
targetState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}) {
|
||||
when {
|
||||
style != Settings.SearchBarSettings.SearchBarStyle.Transparent -> MaterialTheme.colorScheme.onSurface
|
||||
it == SearchBarLevel.Resting -> if (darkColors) Color(0, 0, 0, 180) else Color.White
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
}
|
||||
|
||||
val opacity by transition.animateFloat(label = "opacity") {
|
||||
if (style == Settings.SearchBarSettings.SearchBarStyle.Hidden && it == SearchBarLevel.Resting) 0f
|
||||
else 1f
|
||||
}
|
||||
|
||||
LauncherCard(
|
||||
modifier = modifier
|
||||
.alpha(opacity),
|
||||
backgroundOpacity = backgroundOpacity,
|
||||
elevation = elevation
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides contentColor
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.height(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
imageVector = androidx.compose.material.icons.Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = contentColor
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.search_bar_placeholder),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
LaunchedEffect(level) {
|
||||
if (level == SearchBarLevel.Resting) onUnfocus()
|
||||
}
|
||||
BasicTextField(
|
||||
modifier = Modifier
|
||||
.onFocusChanged {
|
||||
if (it.hasFocus) onFocus()
|
||||
}
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.titleMedium.copy(
|
||||
color = contentColor
|
||||
),
|
||||
singleLine = true,
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
menu()
|
||||
}
|
||||
}
|
||||
this.actions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SearchBarLevel {
|
||||
/**
|
||||
* The default, "hidden" state, when the launcher is in its initial state (scroll position is 0
|
||||
* and search is closed)
|
||||
*/
|
||||
Resting,
|
||||
|
||||
/**
|
||||
* When the search is open but there is no content behind the search bar (scroll position is 0)
|
||||
*/
|
||||
Active,
|
||||
|
||||
/**
|
||||
* When there is content below the search bar which requires the search bar to be raised above
|
||||
* this content (scroll position is not 0)
|
||||
*/
|
||||
Raised
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Path
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.GenericShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
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.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.toRect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.asComposePath
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.drawscope.scale
|
||||
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toAndroidRect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.icons.ClockLayer
|
||||
import de.mm20.launcher2.icons.ClockSublayer
|
||||
import de.mm20.launcher2.icons.ClockSublayerRole
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.DynamicLauncherIcon
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.icons.LauncherIconLayer
|
||||
import de.mm20.launcher2.icons.StaticIconLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TextLayer
|
||||
import de.mm20.launcher2.icons.TintedClockLayer
|
||||
import de.mm20.launcher2.icons.TintedIconLayer
|
||||
import de.mm20.launcher2.icons.TransparentLayer
|
||||
import de.mm20.launcher2.ktx.drawWithColorFilter
|
||||
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
|
||||
import de.mm20.launcher2.ui.base.LocalTime
|
||||
import de.mm20.launcher2.ui.locals.LocalDarkTheme
|
||||
import kotlinx.coroutines.launch
|
||||
import palettes.TonalPalette
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ShapedLauncherIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp,
|
||||
icon: () -> LauncherIcon? = { null },
|
||||
badge: () -> Badge? = { null },
|
||||
onClick: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
shape: Shape = LocalIconShape.current
|
||||
) {
|
||||
|
||||
val _icon = icon()
|
||||
|
||||
var currentIcon by remember(_icon) {
|
||||
mutableStateOf(
|
||||
when (_icon) {
|
||||
is DynamicLauncherIcon -> null
|
||||
is StaticLauncherIcon -> _icon
|
||||
else -> null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (_icon is DynamicLauncherIcon) {
|
||||
val time = LocalTime.current
|
||||
LaunchedEffect(time) {
|
||||
currentIcon = _icon.getIcon(time)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
clip = currentIcon?.backgroundLayer !is TransparentLayer
|
||||
this.shape = shape
|
||||
}
|
||||
.then(
|
||||
if (onClick != null || onLongClick != null) {
|
||||
Modifier.pointerInput(onClick, onLongClick) {
|
||||
detectTapGestures(
|
||||
onLongPress = { onLongClick?.invoke() },
|
||||
onTap = { onClick?.invoke() },
|
||||
)
|
||||
}
|
||||
} else Modifier
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
currentIcon?.let {
|
||||
IconLayer(
|
||||
it.backgroundLayer,
|
||||
size,
|
||||
colorTone = if (LocalDarkTheme.current) 30 else 90,
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
IconLayer(
|
||||
it.foregroundLayer,
|
||||
size,
|
||||
colorTone = if (LocalDarkTheme.current) 90 else 10,
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
val _badge = badge()
|
||||
if (_badge != null) {
|
||||
Surface(
|
||||
shadowElevation = 1.dp,
|
||||
tonalElevation = 1.dp,
|
||||
modifier = Modifier
|
||||
.size(size * 0.33f)
|
||||
.align(Alignment.BottomEnd)
|
||||
.then(
|
||||
if (onClick != null || onLongClick != null) {
|
||||
Modifier.pointerInput(onClick, onLongClick) {
|
||||
detectTapGestures(
|
||||
onLongPress = { onLongClick?.invoke() },
|
||||
onTap = { onClick?.invoke() },
|
||||
)
|
||||
}
|
||||
} else Modifier
|
||||
),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
shape = CircleShape
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
_badge.progress?.let {
|
||||
val progress by animateFloatAsState(it)
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
progress = progress,
|
||||
strokeWidth = size / 48,
|
||||
color = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
}
|
||||
val badgeIconRes = _badge.iconRes
|
||||
val badgeIcon = _badge.icon
|
||||
|
||||
val number = _badge.number
|
||||
if (badgeIconRes != null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(size / 48),
|
||||
painter = painterResource(badgeIconRes),
|
||||
contentDescription = null
|
||||
)
|
||||
} else if (badgeIcon != null) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(size / 48)
|
||||
) {
|
||||
badgeIcon.setBounds(
|
||||
0,
|
||||
0,
|
||||
this.size.width.roundToInt(),
|
||||
this.size.height.roundToInt()
|
||||
)
|
||||
drawIntoCanvas {
|
||||
badgeIcon.draw(it.nativeCanvas)
|
||||
}
|
||||
}
|
||||
} else if (number != null && number > 0 && number < 100) {
|
||||
Text(
|
||||
number.toString(),
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontSize = with(LocalDensity.current) {
|
||||
size.toSp() * 0.2f
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Badge(
|
||||
badge: () -> Badge?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IconLayer(
|
||||
layer: LauncherIconLayer,
|
||||
size: Dp,
|
||||
colorTone: Int,
|
||||
defaultTintColor: Color
|
||||
) {
|
||||
when (layer) {
|
||||
is ClockLayer -> {
|
||||
ClockLayer(layer.sublayers, scale = layer.scale, tintColor = null)
|
||||
}
|
||||
|
||||
is TintedClockLayer -> {
|
||||
ClockLayer(
|
||||
layer.sublayers,
|
||||
scale = layer.scale,
|
||||
tintColor = if (layer.color == 0) defaultTintColor
|
||||
else Color(getTone(layer.color, colorTone))
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is ColorLayer -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (layer.color == 0) {
|
||||
defaultTintColor
|
||||
} else {
|
||||
Color(getTone(layer.color, colorTone))
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is StaticIconLayer -> {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
withTransform({
|
||||
this.scale(layer.scale)
|
||||
}) {
|
||||
drawIntoCanvas {
|
||||
layer.icon.bounds = this.size.toRect().toAndroidRect()
|
||||
layer.icon.draw(it.nativeCanvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TextLayer -> {
|
||||
Text(
|
||||
text = layer.text,
|
||||
style = MaterialTheme.typography.headlineSmall.copy(
|
||||
fontSize = 20.sp * (size / 48.dp)
|
||||
),
|
||||
color = if (layer.color == 0) {
|
||||
defaultTintColor
|
||||
} else {
|
||||
Color(getTone(layer.color, colorTone))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is TintedIconLayer -> {
|
||||
val color =
|
||||
if (layer.color == 0) defaultTintColor.toArgb()
|
||||
else getTone(layer.color, colorTone)
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
withTransform({
|
||||
this.scale(layer.scale)
|
||||
}) {
|
||||
drawIntoCanvas {
|
||||
layer.icon.bounds = this.size.toRect().toAndroidRect()
|
||||
layer.icon.drawWithColorFilter(
|
||||
it.nativeCanvas,
|
||||
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TransparentLayer -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTone(argb: Int, tone: Int): Int {
|
||||
return TonalPalette
|
||||
.fromInt(argb)
|
||||
.tone(tone)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClockLayer(
|
||||
sublayers: List<ClockSublayer>,
|
||||
scale: Float,
|
||||
tintColor: Color?,
|
||||
) {
|
||||
val time = remember(LocalTime.current) {
|
||||
Instant.ofEpochMilli(System.currentTimeMillis()).atZone(ZoneId.systemDefault())
|
||||
}
|
||||
|
||||
val second = remember {
|
||||
Animatable(time.second.toFloat())
|
||||
}
|
||||
|
||||
val minute = remember {
|
||||
Animatable(time.minute.toFloat() + time.second.toFloat() / 60f)
|
||||
}
|
||||
|
||||
val hour = remember {
|
||||
Animatable(time.hour.toFloat() + time.minute.toFloat() / 60f)
|
||||
}
|
||||
|
||||
LaunchedEffect(time) {
|
||||
val h = time.hour.toFloat() + time.minute.toFloat() / 60f
|
||||
val m = time.minute.toFloat() + time.second.toFloat() / 60f
|
||||
val s = time.second.toFloat() + (time.nano / 1000000f) / 1000f
|
||||
second.snapTo(s)
|
||||
hour.snapTo(h)
|
||||
minute.snapTo(m)
|
||||
launch {
|
||||
hour.animateTo(h + 1.5f / 60f, tween(90000, easing = LinearEasing))
|
||||
}
|
||||
launch {
|
||||
minute.animateTo(m + 1.5f, tween(90000, easing = LinearEasing))
|
||||
}
|
||||
launch {
|
||||
second.animateTo(s + 90f, tween(90000, easing = LinearEasing))
|
||||
}
|
||||
}
|
||||
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val colorFilter = tintColor?.let {
|
||||
PorterDuffColorFilter(tintColor.toArgb(), PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
withTransform({
|
||||
this.scale(scale)
|
||||
}) {
|
||||
for (sublayer in sublayers) {
|
||||
withTransform({
|
||||
when (sublayer.role) {
|
||||
ClockSublayerRole.Hour -> {
|
||||
rotate(hour.value / 12f * 360f)
|
||||
}
|
||||
|
||||
ClockSublayerRole.Minute -> {
|
||||
rotate(minute.value / 60f * 360f)
|
||||
}
|
||||
|
||||
ClockSublayerRole.Second -> {
|
||||
rotate(second.value / 60f * 360f)
|
||||
}
|
||||
|
||||
ClockSublayerRole.Static -> {}
|
||||
}
|
||||
}) {
|
||||
drawIntoCanvas {
|
||||
sublayer.drawable.bounds = this.size.toRect().toAndroidRect()
|
||||
sublayer.drawable.drawWithColorFilter(it.nativeCanvas, colorFilter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val LocalIconShape = compositionLocalOf<Shape> { CircleShape }
|
||||
|
||||
fun getShape(iconShape: IconShape): Shape {
|
||||
return when (iconShape) {
|
||||
IconShape.PlatformDefault -> PlatformShape
|
||||
IconShape.Circle -> CircleShape
|
||||
IconShape.Square -> RectangleShape
|
||||
IconShape.RoundedSquare -> RoundedCornerShape(25)
|
||||
IconShape.Triangle -> TriangleShape
|
||||
IconShape.Squircle -> SquircleShape
|
||||
IconShape.Hexagon -> HexagonShape
|
||||
IconShape.Pentagon -> PentagonShape
|
||||
IconShape.Teardrop -> TeardropShape
|
||||
IconShape.Pebble -> PebbleShape
|
||||
IconShape.EasterEgg -> EasterEggShape
|
||||
IconShape.UNRECOGNIZED -> CircleShape
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProvideIconShape(iconShape: IconShape, content: @Composable () -> Unit) {
|
||||
val shape = getShape(iconShape)
|
||||
CompositionLocalProvider(
|
||||
LocalIconShape provides shape,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
private val TriangleShape: Shape
|
||||
get() = GenericShape { size, _ ->
|
||||
var cx = 0f
|
||||
var cy = size.height * 0.86f
|
||||
val r = size.width
|
||||
arcTo(Rect(cx - r, cy - r, cx + r, cy + r), 300f, 60f, false)
|
||||
cx = size.width
|
||||
cy = size.height * 0.86f
|
||||
arcTo(Rect(cx - r, cy - r, cx + r, cy + r), 180f, 60f, false)
|
||||
cx = size.width * 0.5f
|
||||
cy = 0f
|
||||
arcTo(Rect(cx - r, cy - r, cx + r, cy + r), 60f, 60f, false)
|
||||
close()
|
||||
}
|
||||
|
||||
private val SquircleShape: Shape
|
||||
get() = GenericShape { size, _ ->
|
||||
val radius = size.width / 2f
|
||||
val radiusToPow = radius.pow(3f).toDouble()
|
||||
moveTo(-radius, 0f)
|
||||
for (x in -radius.roundToInt()..radius.roundToInt())
|
||||
lineTo(
|
||||
x.toFloat(),
|
||||
Math.cbrt(radiusToPow - abs(x * x * x)).toFloat()
|
||||
)
|
||||
for (x in radius.roundToInt() downTo -radius.roundToInt())
|
||||
lineTo(
|
||||
x.toFloat(),
|
||||
(-Math.cbrt(radiusToPow - abs(x * x * x))).toFloat()
|
||||
)
|
||||
translate(Offset(size.width / 2f, size.height / 2f))
|
||||
}
|
||||
|
||||
private val HexagonShape: Shape
|
||||
get() = GenericShape { size, _ ->
|
||||
moveTo(
|
||||
size.width * 0.25f,
|
||||
size.height * 0.933f
|
||||
)
|
||||
lineTo(
|
||||
size.width * 0.75f,
|
||||
size.height * 0.933f
|
||||
)
|
||||
lineTo(
|
||||
size.width * 1.0f,
|
||||
size.height * 0.5f
|
||||
)
|
||||
lineTo(
|
||||
size.width * 0.75f,
|
||||
size.height * 0.067f
|
||||
)
|
||||
lineTo(
|
||||
size.width * 0.25f,
|
||||
size.height * 0.067f
|
||||
)
|
||||
lineTo(0f, size.height * 0.5f)
|
||||
close()
|
||||
}
|
||||
|
||||
private val PentagonShape: Shape
|
||||
get() = GenericShape { size, _ ->
|
||||
moveTo(
|
||||
0.49997027f * size.width,
|
||||
0.0060308f * size.height
|
||||
)
|
||||
lineTo(
|
||||
0.99994053f * size.width,
|
||||
0.36928048f * size.height
|
||||
)
|
||||
lineTo(
|
||||
0.80896887f * size.width,
|
||||
0.95703078f * size.height
|
||||
)
|
||||
lineTo(
|
||||
0.19097162f * size.width,
|
||||
0.95703076f * size.height
|
||||
)
|
||||
lineTo(
|
||||
0f,
|
||||
0.36928045f * size.height
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
private val TeardropShape: Shape
|
||||
get() = GenericShape { size, _ ->
|
||||
moveTo(0.5f * size.width, 0f)
|
||||
cubicTo(
|
||||
0.776f * size.width, 0f,
|
||||
size.width, 0.224f * size.height,
|
||||
size.width, 0.5f * size.height,
|
||||
)
|
||||
lineTo(
|
||||
size.width, 0.88f * size.height,
|
||||
)
|
||||
cubicTo(
|
||||
size.width, 0.946f * size.height,
|
||||
0.946f * size.width, size.height,
|
||||
0.88f * size.width, size.height,
|
||||
)
|
||||
lineTo(0.5f * size.width, size.height)
|
||||
cubicTo(
|
||||
0.224f * size.width, size.height,
|
||||
0f, 0.776f * size.height,
|
||||
0f, 0.5f * size.height,
|
||||
)
|
||||
cubicTo(
|
||||
0f, 0.224f * size.height,
|
||||
0.224f * size.width, 0f,
|
||||
0.5f * size.width, 0f,
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
private val PebbleShape: Shape
|
||||
get() = GenericShape { size, _ ->
|
||||
moveTo(0.55f * size.width, 0f * size.height)
|
||||
cubicTo(
|
||||
0.25f * size.width,
|
||||
0f * size.height,
|
||||
0f * size.width,
|
||||
0.25f * size.height,
|
||||
0f * size.width,
|
||||
0.5f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
0f * size.width,
|
||||
0.78f * size.height,
|
||||
0.28f * size.width,
|
||||
1f * size.height,
|
||||
0.55f * size.width,
|
||||
1f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
0.85f * size.width,
|
||||
1f * size.height,
|
||||
1f * size.width,
|
||||
0.85f * size.height,
|
||||
1f * size.width,
|
||||
0.58f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
1f * size.width,
|
||||
0.3f * size.height,
|
||||
0.86f * size.width,
|
||||
0f * size.height,
|
||||
0.55f * size.width,
|
||||
0f * size.height
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
private val EasterEggShape: Shape
|
||||
get() = GenericShape { size, _ ->
|
||||
moveTo(
|
||||
0.49999999f * size.width,
|
||||
1f * size.height
|
||||
)
|
||||
lineTo(
|
||||
0.42749999f * size.width,
|
||||
0.9339999999999999f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
0.16999998f * size.width,
|
||||
0.7005004f * size.height,
|
||||
0f,
|
||||
0.5460004f * size.height,
|
||||
0f,
|
||||
0.3575003f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
0f,
|
||||
0.2030004f * size.height,
|
||||
0.12100002f * size.width,
|
||||
0.0825004f * size.height,
|
||||
0.275f * size.width,
|
||||
0.0825004f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
0.362f * size.width,
|
||||
0.0825004f * size.height,
|
||||
0.4455f * size.width,
|
||||
0.123f * size.height,
|
||||
0.5f * size.width,
|
||||
0.1865003f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
0.55449999f * size.width,
|
||||
0.123f * size.height,
|
||||
0.638f * size.width,
|
||||
0.0825f * size.height,
|
||||
0.725f * size.width,
|
||||
0.0825f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
0.87900006f * size.width,
|
||||
0.0825004f * size.height,
|
||||
1f * size.width,
|
||||
0.2030004f * size.height,
|
||||
1f * size.width,
|
||||
0.3575003f * size.height
|
||||
)
|
||||
cubicTo(
|
||||
1f * size.width,
|
||||
0.5460004f * size.height,
|
||||
0.82999999f * size.width,
|
||||
0.7005004f * size.height,
|
||||
0.57250001f * size.width,
|
||||
0.9340004f * size.height
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
private val PlatformShape: Shape
|
||||
get() {
|
||||
val drawable = AdaptiveIconDrawable(null, null)
|
||||
|
||||
val pathBounds = RectF()
|
||||
drawable.iconMask.computeBounds(pathBounds, true)
|
||||
|
||||
return GenericShape { size, _ ->
|
||||
val path = Path(drawable.iconMask)
|
||||
val transformMatrix = Matrix()
|
||||
transformMatrix.setScale(
|
||||
size.width / pathBounds.width(),
|
||||
size.height / pathBounds.height()
|
||||
)
|
||||
path.transform(transformMatrix)
|
||||
addPath(path.asComposePath())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun SmallMessage(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
color: Color = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = color
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = icon, contentDescription = null)
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ChevronRight
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.integerResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.locals.LocalWindowPosition
|
||||
import kotlin.math.min
|
||||
|
||||
@Composable
|
||||
fun Toolbar(
|
||||
modifier: Modifier = Modifier,
|
||||
leftActions: List<ToolbarAction> = emptyList(),
|
||||
rightActions: List<ToolbarAction> = emptyList()
|
||||
) {
|
||||
val slots = integerResource(R.integer.config_toolbarSlots)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(4.dp)
|
||||
) {
|
||||
Icons(leftActions, slots)
|
||||
Spacer(modifier = Modifier.weight(1f, fill = true))
|
||||
Icons(rightActions, slots)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Icons(actions: List<ToolbarAction>, slots: Int) {
|
||||
for (i in 0 until min(slots, actions.size)) {
|
||||
if (i == slots - 1 && slots != actions.size) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(Icons.Rounded.MoreVert, contentDescription = "")
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.offset(0.dp, LocalWindowPosition.current.toDp())
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.animateContentSize()
|
||||
) {
|
||||
OverflowMenuItems(items = actions.subList(slots - 1, actions.size)) {
|
||||
showMenu = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val action = actions[i]
|
||||
when (action) {
|
||||
is DefaultToolbarAction -> {
|
||||
IconButton(action.action) {
|
||||
Icon(action.icon, contentDescription = action.label)
|
||||
}
|
||||
}
|
||||
is SubmenuToolbarAction -> {
|
||||
Box {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
IconButton({
|
||||
showMenu = true
|
||||
}) {
|
||||
Icon(action.icon, contentDescription = action.label)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.offset(0.dp, LocalWindowPosition.current.toDp())
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.animateContentSize()
|
||||
) {
|
||||
OverflowMenuItems(items = action.children) {
|
||||
showMenu = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.OverflowMenuItems(items: List<ToolbarAction>, onDismiss: () -> Unit) {
|
||||
var selectedSubMenu by remember { mutableStateOf(-1) }
|
||||
if (selectedSubMenu == -1) {
|
||||
items.forEachIndexed { i, action ->
|
||||
when (action) {
|
||||
is SubmenuToolbarAction -> {
|
||||
DropdownMenuItem(
|
||||
onClick = { selectedSubMenu = i },
|
||||
text = {
|
||||
Text(
|
||||
action.label, modifier = Modifier.weight(1f),
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(action.icon, null)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(Icons.Rounded.ChevronRight, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
is DefaultToolbarAction -> {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
action.action()
|
||||
onDismiss()
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
action.label,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(action.icon, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val submenu = items[selectedSubMenu] as SubmenuToolbarAction
|
||||
OverflowMenuItems(items = submenu.children, onDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
interface ToolbarAction {
|
||||
val label: String
|
||||
val icon: ImageVector
|
||||
}
|
||||
|
||||
data class DefaultToolbarAction(
|
||||
override val label: String,
|
||||
override val icon: ImageVector,
|
||||
val action: () -> Unit
|
||||
) : ToolbarAction
|
||||
|
||||
data class SubmenuToolbarAction(
|
||||
override val label: String,
|
||||
override val icon: ImageVector,
|
||||
val children: List<ToolbarAction>
|
||||
) : ToolbarAction
|
||||
@@ -0,0 +1,95 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.godaddy.android.colorpicker.ClassicColorPicker
|
||||
import de.mm20.launcher2.ui.ktx.toHexString
|
||||
|
||||
@Composable
|
||||
fun ColorPreference(
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
value: Color?,
|
||||
onValueChanged: (Color?) -> Unit = {}
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
Preference(
|
||||
title = title,
|
||||
summary = summary,
|
||||
controls = {
|
||||
value?.let {
|
||||
Surface(
|
||||
color = it,
|
||||
shape = CircleShape,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
.size(36.dp)
|
||||
) {}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showDialog = true
|
||||
}
|
||||
)
|
||||
if (showDialog) {
|
||||
var color by remember(value) { mutableStateOf(value) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
|
||||
ClassicColorPicker(
|
||||
color = value ?: Color.Black,
|
||||
onColorChanged = {
|
||||
color = it.toColor()
|
||||
},
|
||||
showAlphaBar = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
)
|
||||
|
||||
var hexValue by remember(color) {
|
||||
mutableStateOf(
|
||||
color?.toHexString() ?: "#000000"
|
||||
)
|
||||
}
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
value = hexValue,
|
||||
onValueChange = {
|
||||
hexValue = it
|
||||
if (Regex("#[0-9a-fA-F]{6}").matches(it)) {
|
||||
val hex = it.substring(1).toIntOrNull(16) ?: return@TextField
|
||||
color = Color(hex).copy(alpha = 1f)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onValueChanged(color)
|
||||
showDialog = false
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
|
||||
@Composable
|
||||
fun <T> ListPreference(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
iconPadding: Boolean = true,
|
||||
items: List<ListPreferenceItem<T>>,
|
||||
value: T,
|
||||
summary: String? = items.firstOrNull { value == it.value }?.label,
|
||||
onValueChanged: (T) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
itemLabel: @Composable RowScope.(item: ListPreferenceItem<T>) -> Unit = {
|
||||
Text(
|
||||
text = it.label,
|
||||
)
|
||||
}
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
Preference(
|
||||
title = title,
|
||||
summary = summary,
|
||||
icon = icon,
|
||||
iconPadding = iconPadding,
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
showDialog = true
|
||||
}
|
||||
)
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { showDialog = false }) {
|
||||
Surface(
|
||||
tonalElevation = 16.dp,
|
||||
shadowElevation = 16.dp,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(
|
||||
start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp
|
||||
)
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
items(items) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onValueChanged(it.value)
|
||||
showDialog = false
|
||||
}
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 16.dp,
|
||||
bottom = 16.dp,
|
||||
end = 24.dp
|
||||
)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = it.value == value,
|
||||
onClick = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.titleMedium
|
||||
) {
|
||||
itemLabel(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias ListPreferenceItem<T> = Pair<String, T>
|
||||
|
||||
inline val <T>ListPreferenceItem<T>.label: String
|
||||
get() = this.first
|
||||
|
||||
inline val <T>ListPreferenceItem<T>.value: T
|
||||
get() = this.second
|
||||
@@ -0,0 +1,89 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun Preference(
|
||||
title: String,
|
||||
icon: @Composable (() -> Unit),
|
||||
iconPadding: Boolean = true,
|
||||
summary: String? = null,
|
||||
onClick: () -> Unit = {},
|
||||
controls: @Composable (() -> Unit)? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled, onClick = onClick)
|
||||
.padding(horizontal = 16.dp)
|
||||
.alpha(if (enabled) 1f else 0.38f),
|
||||
) {
|
||||
if (iconPadding) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(56.dp)
|
||||
.padding(start = 4.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
icon()
|
||||
}
|
||||
} else {
|
||||
Box(modifier = Modifier.size(0.dp))
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f).padding(vertical = 16.dp)
|
||||
) {
|
||||
Text(text = title, style = MaterialTheme.typography.titleMedium)
|
||||
if (summary != null) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (controls != null) {
|
||||
Box(
|
||||
modifier = Modifier.padding(start = 24.dp)
|
||||
) {
|
||||
controls()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Preference(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
iconPadding: Boolean = true,
|
||||
summary: String? = null,
|
||||
onClick: () -> Unit = {},
|
||||
controls: @Composable (() -> Unit)? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Preference(
|
||||
title,
|
||||
icon = {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}, iconPadding, summary, onClick, controls, enabled
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun PreferenceCategory(
|
||||
title: String? = null,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Column {
|
||||
if (title != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 16.dp, end = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 56.dp),
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
content()
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().height(0.5.dp).background(
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.HelpOutline
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||
|
||||
|
||||
@Composable
|
||||
fun PreferenceScreen(
|
||||
title: String,
|
||||
floatingActionButton: @Composable () -> Unit = {},
|
||||
topBarActions: @Composable RowScope.() -> Unit = {},
|
||||
helpUrl: String? = null,
|
||||
content: LazyListScope.() -> Unit,
|
||||
) {
|
||||
val navController = LocalNavController.current
|
||||
val systemUiController = rememberSystemUiController()
|
||||
systemUiController.setStatusBarColor(MaterialTheme.colorScheme.surface)
|
||||
systemUiController.setNavigationBarColor(Color.Black)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
val activity = LocalContext.current as? AppCompatActivity
|
||||
Scaffold(
|
||||
floatingActionButton = floatingActionButton,
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
if (navController?.navigateUp() != true) {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
}) {
|
||||
Icon(imageVector = Icons.Rounded.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (helpUrl != null) {
|
||||
IconButton(onClick = {
|
||||
CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(colorScheme.primaryContainer.toArgb())
|
||||
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
|
||||
.build()
|
||||
)
|
||||
.build().launchUrl(context, Uri.parse(helpUrl))
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.HelpOutline,
|
||||
contentDescription = "Help"
|
||||
)
|
||||
}
|
||||
}
|
||||
topBarActions()
|
||||
}
|
||||
)
|
||||
}) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun PreferenceWithSwitch(
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit = {},
|
||||
switchValue: Boolean,
|
||||
onSwitchChanged: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = (Alignment.CenterVertically)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Preference(
|
||||
title = title,
|
||||
summary = summary,
|
||||
icon = icon,
|
||||
enabled = enabled,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.width(1.dp)
|
||||
.alpha(0.38f)
|
||||
.background(LocalContentColor.current)
|
||||
)
|
||||
Switch(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
checked = switchValue,
|
||||
enabled = enabled,
|
||||
onCheckedChange = onSwitchChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.log
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun SliderPreference(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
value: Float,
|
||||
min: Float = 0f,
|
||||
max: Float = 1f,
|
||||
step: Float? = null,
|
||||
onValueChanged: (Float) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
var sliderValue by remember(value) { mutableStateOf(value) }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.alpha(if (enabled) 1f else 0.38f),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(48.dp)
|
||||
.padding(start = 4.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Slider(
|
||||
modifier = Modifier.weight(1f),
|
||||
value = sliderValue,
|
||||
onValueChange = {
|
||||
sliderValue = it
|
||||
},
|
||||
valueRange = min..max,
|
||||
steps = step?.let { ((max - min) / it).toInt() - 1 } ?: 0,
|
||||
onValueChangeFinished = {
|
||||
onValueChanged(sliderValue)
|
||||
}
|
||||
)
|
||||
val decimalPlaces = -log(step ?: 0.01f, 10f)
|
||||
val format = remember { DecimalFormat().apply {
|
||||
maximumFractionDigits = floor(decimalPlaces).toInt()
|
||||
minimumFractionDigits = 0
|
||||
} }
|
||||
Text(
|
||||
modifier = Modifier.width(56.dp).padding(start = 24.dp),
|
||||
text = format.format(sliderValue),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SliderPreference(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
value: Int,
|
||||
min: Int = 0,
|
||||
max: Int = 100,
|
||||
step: Int = 1,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
SliderPreference(
|
||||
title = title,
|
||||
icon = icon,
|
||||
value = value.toFloat(),
|
||||
enabled = enabled,
|
||||
min = min.toFloat(),
|
||||
max = max.toFloat(),
|
||||
step = step.toFloat(),
|
||||
onValueChanged = {
|
||||
onValueChanged(it.roundToInt())
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
@Composable
|
||||
fun SwitchPreference(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
summary: String? = null,
|
||||
value: Boolean,
|
||||
onValueChanged: (Boolean) -> Unit,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Preference(
|
||||
title = title,
|
||||
icon = icon,
|
||||
summary = summary,
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
onValueChanged(!value)
|
||||
},
|
||||
controls = {
|
||||
Switch(
|
||||
enabled = enabled, checked = value, onCheckedChange = onValueChanged,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.mm20.launcher2.ui.component.preferences
|
||||
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
@Composable
|
||||
fun TextPreference(
|
||||
title: String,
|
||||
value: String,
|
||||
summary: String? = value,
|
||||
onValueChanged: (String) -> Unit,
|
||||
placeholder: String? = null
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
Preference(
|
||||
title = title,
|
||||
summary = summary,
|
||||
onClick = { showDialog = true }
|
||||
)
|
||||
|
||||
if (showDialog) {
|
||||
var textFieldValue by remember { mutableStateOf(value) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
title = {
|
||||
Text(text = title, style = MaterialTheme.typography.titleLarge)
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = textFieldValue,
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
onValueChange = { textFieldValue = it },
|
||||
placeholder = placeholder?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onValueChanged(textFieldValue)
|
||||
showDialog = false
|
||||
}) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
showDialog = false
|
||||
}) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
package de.mm20.launcher2.ui.component.weather
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.AcUnit
|
||||
import androidx.compose.material.icons.rounded.Air
|
||||
import androidx.compose.material.icons.rounded.Bolt
|
||||
import androidx.compose.material.icons.rounded.Thermostat
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.icons.*
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
|
||||
|
||||
@Composable
|
||||
fun AnimatedWeatherIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: WeatherIcon,
|
||||
night: Boolean = false
|
||||
) {
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(8.dp)
|
||||
.size(64.dp)
|
||||
.clipToBounds(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SunMoon(icon, night)
|
||||
Precipitation(icon)
|
||||
LightningBolt(icon)
|
||||
LightningBolt2(icon)
|
||||
Cloud1(icon)
|
||||
Wind2(icon)
|
||||
Cloud2(icon)
|
||||
Cloud3(icon)
|
||||
Fog(icon)
|
||||
Hot(icon)
|
||||
Cold(icon)
|
||||
Wind1(icon)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SunMoon(icon: WeatherIcon, night: Boolean) {
|
||||
val sunMoonIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_weather_sun_moon)
|
||||
val transition = updateTransition(targetState = icon, "AnimatedWeatherIcon")
|
||||
|
||||
val color by animateColorAsState(
|
||||
if (night) colorResource(id = R.color.weather_moon) else colorResource(id = R.color.weather_sun)
|
||||
)
|
||||
val scale by transition.animateFloat(label = "sunScale") {
|
||||
when (it) {
|
||||
WeatherIcon.Clear,
|
||||
WeatherIcon.PartlyCloudy,
|
||||
WeatherIcon.BrokenClouds,
|
||||
WeatherIcon.Haze -> 1f
|
||||
WeatherIcon.MostlyCloudy -> 0.8f
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
val offset by transition.animateOffset(label = "sunOffset") {
|
||||
when (it) {
|
||||
WeatherIcon.Clear,
|
||||
WeatherIcon.BrokenClouds,
|
||||
WeatherIcon.Haze -> Offset.Zero
|
||||
WeatherIcon.MostlyCloudy -> Offset(6f, -4f)
|
||||
WeatherIcon.PartlyCloudy -> Offset(3f, -2f)
|
||||
else -> Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
rememberAnimatedVectorPainter(sunMoonIcon, atEnd = night),
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(offset.x.dp, offset.y.dp)
|
||||
.scale(scale),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LightningBolt(icon: WeatherIcon) {
|
||||
val transition = updateTransition(targetState = icon, "AnimatedWeatherIconBolt")
|
||||
|
||||
val scale by transition.animateFloat(label = "scale") {
|
||||
when (it) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm -> 0.6f
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
val offset by transition.animateOffset(label = "offset") {
|
||||
when (it) {
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.Thunderstorm -> Offset(1f, 8f)
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm -> Offset(6f, 8f)
|
||||
else -> Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
Icons.Rounded.Bolt,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(offset.x.dp, offset.y.dp)
|
||||
.scale(scale),
|
||||
tint = colorResource(id = R.color.weather_lightning_bolt)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LightningBolt2(icon: WeatherIcon) {
|
||||
val transition = updateTransition(targetState = icon, "AnimatedWeatherIconBolt2")
|
||||
|
||||
val scale by transition.animateFloat(label = "scale") {
|
||||
when (it) {
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm -> 0.5f
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
val offset by transition.animateOffset(label = "offset") {
|
||||
when (it) {
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm -> Offset(-9f, 5f)
|
||||
else -> Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
Icons.Rounded.Bolt,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(offset.x.dp, offset.y.dp)
|
||||
.scale(scale),
|
||||
tint = colorResource(id = R.color.weather_lightning_bolt)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Cloud1(icon: WeatherIcon) {
|
||||
val transition = updateTransition(targetState = icon, "AnimatedWeatherIconCloud1")
|
||||
|
||||
val scale by transition.animateFloat(label = "scale") {
|
||||
when (it) {
|
||||
WeatherIcon.Clear -> 0f
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Wind,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Storm,
|
||||
WeatherIcon.Fog -> 1.4f
|
||||
WeatherIcon.MostlyCloudy,
|
||||
WeatherIcon.PartlyCloudy -> 1f
|
||||
WeatherIcon.BrokenClouds -> 0.9f
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
val offset by transition.animateOffset(label = "offset") {
|
||||
when (it) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> Offset(0f, -14f)
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Wind,
|
||||
WeatherIcon.Storm,
|
||||
WeatherIcon.Fog -> Offset(0f, -5f)
|
||||
WeatherIcon.MostlyCloudy -> Offset(-5f, 0f)
|
||||
WeatherIcon.PartlyCloudy -> Offset(-3f, 4f)
|
||||
WeatherIcon.BrokenClouds -> Offset(-5f, 7f)
|
||||
else -> Offset.Zero
|
||||
}
|
||||
}
|
||||
val color by transition.animateColor(label = "color") {
|
||||
when (it) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.Storm -> colorResource(id = R.color.weather_cloud_dark_2)
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Wind,
|
||||
WeatherIcon.Fog -> colorResource(id = R.color.weather_cloud_dark_1)
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Snow -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
WeatherIcon.MostlyCloudy -> colorResource(id = R.color.weather_cloud_light_2)
|
||||
WeatherIcon.PartlyCloudy,
|
||||
WeatherIcon.BrokenClouds -> colorResource(id = R.color.weather_cloud_light_1)
|
||||
else -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
Icons.Rounded.WeatherCloud,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(offset.x.dp, offset.y.dp)
|
||||
.scale(scale),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Cloud2(icon: WeatherIcon) {
|
||||
val transition = updateTransition(targetState = icon, "AnimatedWeatherIconCloud2")
|
||||
|
||||
val scale by transition.animateFloat(label = "scale") {
|
||||
when (it) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.MostlyCloudy,
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Wind,
|
||||
WeatherIcon.Storm,
|
||||
WeatherIcon.Fog -> 1.1f
|
||||
WeatherIcon.BrokenClouds -> 0.7f
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
val offset by transition.animateOffset(label = "offset") {
|
||||
when (it) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> Offset(-6f, -6f)
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Wind,
|
||||
WeatherIcon.Storm,
|
||||
WeatherIcon.Fog -> Offset(-6f, 3f)
|
||||
WeatherIcon.BrokenClouds -> Offset(9f, -9f)
|
||||
WeatherIcon.MostlyCloudy -> Offset(4f, 5f)
|
||||
else -> Offset.Zero
|
||||
}
|
||||
}
|
||||
val color by transition.animateColor(label = "color") {
|
||||
when (it) {
|
||||
WeatherIcon.BrokenClouds -> colorResource(id = R.color.weather_cloud_light_2)
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.Storm -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Wind,
|
||||
WeatherIcon.Fog -> colorResource(id = R.color.weather_cloud_medium_1)
|
||||
WeatherIcon.MostlyCloudy -> colorResource(id = R.color.weather_cloud_medium_1)
|
||||
else -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
Icons.Rounded.WeatherCloud,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(offset.x.dp, offset.y.dp)
|
||||
.scale(scale),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Cloud3(icon: WeatherIcon) {
|
||||
val transition = updateTransition(targetState = icon, "AnimatedWeatherIconCloud3")
|
||||
|
||||
val scale by transition.animateFloat(label = "scale") {
|
||||
when (it) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Wind,
|
||||
WeatherIcon.Storm -> 1f
|
||||
else -> 0f
|
||||
}
|
||||
}
|
||||
val offset by transition.animateOffset(label = "offset") {
|
||||
when (it) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> Offset(6f, -4f)
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Wind,
|
||||
WeatherIcon.Storm -> Offset(6f, 5f)
|
||||
else -> Offset.Zero
|
||||
}
|
||||
}
|
||||
val color by transition.animateColor(label = "color") {
|
||||
when (it) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.Storm -> colorResource(id = R.color.weather_cloud_dark_1)
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Wind -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Snow -> colorResource(id = R.color.weather_cloud_light_2)
|
||||
else -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
Icons.Rounded.WeatherCloud,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(offset.x.dp, offset.y.dp)
|
||||
.scale(scale),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Hot(icon: WeatherIcon) {
|
||||
val scale by animateFloatAsState(
|
||||
if (icon == WeatherIcon.Hot) {
|
||||
1f
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
Icon(
|
||||
Icons.Rounded.Thermostat,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.scale(scale)
|
||||
.size(32.dp),
|
||||
tint = colorResource(id = R.color.weather_hot)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Cold(icon: WeatherIcon) {
|
||||
val scale by animateFloatAsState(
|
||||
if (icon == WeatherIcon.Cold) {
|
||||
1f
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
Icon(
|
||||
Icons.Rounded.AcUnit,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.scale(scale)
|
||||
.size(32.dp),
|
||||
tint = colorResource(id = R.color.weather_cold)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Wind1(icon: WeatherIcon) {
|
||||
val scale by animateFloatAsState(
|
||||
if (icon == WeatherIcon.Wind || icon == WeatherIcon.Storm) {
|
||||
0.6f
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
Icon(
|
||||
Icons.Rounded.Air,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.scale(scale)
|
||||
.offset(12.dp, 11.dp)
|
||||
.size(32.dp),
|
||||
tint = colorResource(id = R.color.weather_wind)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Wind2(icon: WeatherIcon) {
|
||||
val scale by animateFloatAsState(
|
||||
if (icon == WeatherIcon.Storm) {
|
||||
0.6f
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
Icon(
|
||||
Icons.Rounded.Air,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.scale(scale)
|
||||
.offset(8.dp, -1.dp)
|
||||
.size(32.dp),
|
||||
tint = colorResource(id = R.color.weather_wind_dark)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AnimatedWeatherIconTestPanel() {
|
||||
var icon by remember { mutableStateOf(WeatherIcon.MostlyCloudy) }
|
||||
var night by remember { mutableStateOf(false) }
|
||||
Row {
|
||||
val icons = WeatherIcon.values()
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
AnimatedWeatherIcon(icon = icon, night = night)
|
||||
WeatherIcon(icon = icon, night = night)
|
||||
}
|
||||
|
||||
Column {
|
||||
Box {
|
||||
var menu by remember { mutableStateOf(false) }
|
||||
DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) {
|
||||
for (ic in icons) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
icon = ic
|
||||
menu = false
|
||||
},
|
||||
text = {
|
||||
Text(text = ic.name)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(onClick = { menu = true }) {
|
||||
Text(text = icon.name)
|
||||
}
|
||||
}
|
||||
Button(onClick = { night = !night }) {
|
||||
Text(text = if (night) "Night" else "Day")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Precipitation(icon: WeatherIcon) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clipToBounds()
|
||||
.rotate(10f)
|
||||
) {
|
||||
Crossfade(icon) {
|
||||
when (it) {
|
||||
WeatherIcon.Drizzle -> {
|
||||
val anim = rememberInfiniteTransition()
|
||||
val animProgress by anim.animateFloat(
|
||||
initialValue = 0f, targetValue = 1f, animationSpec = InfiniteRepeatableSpec(
|
||||
tween(300, easing = LinearEasing)
|
||||
)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(y = 8.dp + 11.dp * animProgress),
|
||||
imageVector = Icons.Rounded.WeatherLightRainAnimatable,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_rain)
|
||||
)
|
||||
}
|
||||
WeatherIcon.Hail -> {
|
||||
val anim = rememberInfiniteTransition()
|
||||
val animProgress by anim.animateFloat(
|
||||
initialValue = 0f, targetValue = 1f, animationSpec = InfiniteRepeatableSpec(
|
||||
tween(300, easing = LinearEasing)
|
||||
)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(y = 8.dp + 11.dp * animProgress),
|
||||
imageVector = Icons.Rounded.WeatherHailAnimatable,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_hail)
|
||||
)
|
||||
}
|
||||
WeatherIcon.Snow -> {
|
||||
val anim = rememberInfiniteTransition()
|
||||
val animProgress by anim.animateFloat(
|
||||
initialValue = 0f, targetValue = 1f, animationSpec = InfiniteRepeatableSpec(
|
||||
tween(1000, easing = LinearEasing)
|
||||
)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(
|
||||
x = sin(animProgress * 2 * PI).dp,
|
||||
y = 8.dp + 11.dp * animProgress
|
||||
),
|
||||
imageVector = Icons.Rounded.WeatherHailAnimatable,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_snow)
|
||||
)
|
||||
}
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> {
|
||||
val anim = rememberInfiniteTransition()
|
||||
val animProgress by anim.animateFloat(
|
||||
initialValue = 0f, targetValue = 1f, animationSpec = InfiniteRepeatableSpec(
|
||||
tween(300, easing = LinearEasing)
|
||||
)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(y = 8.dp + 11.dp * animProgress),
|
||||
imageVector = Icons.Rounded.WeatherRainAnimatable,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_rain)
|
||||
)
|
||||
}
|
||||
WeatherIcon.Sleet -> {
|
||||
val anim = rememberInfiniteTransition()
|
||||
val animProgress by anim.animateFloat(
|
||||
initialValue = 0f, targetValue = 1f, animationSpec = InfiniteRepeatableSpec(
|
||||
tween(300, easing = LinearEasing)
|
||||
)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(y = 8.dp + 11.dp * animProgress),
|
||||
imageVector = Icons.Rounded.WeatherSleetRainAnimatable,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_rain)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(y = 8.dp + 11.dp * animProgress),
|
||||
imageVector = Icons.Rounded.WeatherSleetSnowAnimatable,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_snow)
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Fog(icon: WeatherIcon) {
|
||||
val scale by animateFloatAsState(if (icon == WeatherIcon.Fog || icon == WeatherIcon.Haze) 1f else 0f)
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.scale(scale)
|
||||
.offset(x = 6.dp, y = 7.dp),
|
||||
imageVector = Icons.Rounded.WeatherFog,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_fog)
|
||||
)
|
||||
}
|
||||
|
||||
enum class WeatherIcon {
|
||||
None,
|
||||
Clear,
|
||||
Cloudy,
|
||||
Cold,
|
||||
Drizzle,
|
||||
Haze,
|
||||
Fog,
|
||||
Hail,
|
||||
HeavyThunderstorm,
|
||||
HeavyThunderstormWithRain,
|
||||
Hot,
|
||||
MostlyCloudy,
|
||||
PartlyCloudy,
|
||||
Showers,
|
||||
Sleet,
|
||||
Snow,
|
||||
Storm,
|
||||
Thunderstorm,
|
||||
ThunderstormWithRain,
|
||||
Wind,
|
||||
BrokenClouds
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package de.mm20.launcher2.ui.component.weather
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.*
|
||||
import de.mm20.launcher2.ui.icons.*
|
||||
|
||||
@Composable
|
||||
fun WeatherIcon(
|
||||
icon: WeatherIcon,
|
||||
night: Boolean = false
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SunMoon(icon, night)
|
||||
Precipitation(icon)
|
||||
LightningBolts(icon)
|
||||
Cloud1(icon)
|
||||
Cloud2(icon)
|
||||
Cloud3(icon)
|
||||
HotCold(icon)
|
||||
Wind(icon)
|
||||
Fog(icon)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SunMoon(icon: WeatherIcon, night: Boolean) {
|
||||
when (icon) {
|
||||
WeatherIcon.Clear,
|
||||
WeatherIcon.PartlyCloudy,
|
||||
WeatherIcon.BrokenClouds,
|
||||
WeatherIcon.Haze,
|
||||
WeatherIcon.MostlyCloudy -> {
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
|
||||
val color = if (night) colorResource(id = R.color.weather_moon) else colorResource(id = R.color.weather_sun)
|
||||
|
||||
val scale = when (icon) {
|
||||
WeatherIcon.Clear,
|
||||
WeatherIcon.PartlyCloudy,
|
||||
WeatherIcon.BrokenClouds,
|
||||
WeatherIcon.Haze -> 1f
|
||||
WeatherIcon.MostlyCloudy -> 0.8f
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
val offset = when (icon) {
|
||||
WeatherIcon.MostlyCloudy -> DpOffset(2.dp, -3.dp)
|
||||
WeatherIcon.PartlyCloudy -> DpOffset(2.dp, -2.dp)
|
||||
else -> DpOffset.Zero
|
||||
}
|
||||
|
||||
Icon(
|
||||
if (night) Icons.Rounded.DarkMode else Icons.Rounded.LightMode,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.offset(offset.x, offset.y)
|
||||
.scale(scale),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Cloud1(icon: WeatherIcon) {
|
||||
|
||||
when (icon) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog,
|
||||
WeatherIcon.MostlyCloudy,
|
||||
WeatherIcon.PartlyCloudy,
|
||||
WeatherIcon.BrokenClouds -> {
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
|
||||
val scale = when (icon) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog -> 1.4f
|
||||
WeatherIcon.MostlyCloudy,
|
||||
WeatherIcon.PartlyCloudy -> 1f
|
||||
WeatherIcon.BrokenClouds -> 0.9f
|
||||
else -> 0f
|
||||
}
|
||||
val offset = when (icon) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> DpOffset(0.dp, -7.dp)
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog -> DpOffset(0.dp, -2.5.dp)
|
||||
WeatherIcon.MostlyCloudy -> DpOffset(-2.5.dp, 0.dp)
|
||||
WeatherIcon.PartlyCloudy -> DpOffset(-1.5.dp, 2.dp)
|
||||
WeatherIcon.BrokenClouds -> DpOffset(-2.5.dp, 3.5.dp)
|
||||
else -> DpOffset.Zero
|
||||
}
|
||||
val color = when (icon) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> colorResource(id = R.color.weather_cloud_dark_2)
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog -> colorResource(id = R.color.weather_cloud_dark_1)
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Snow -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
WeatherIcon.MostlyCloudy -> colorResource(id = R.color.weather_cloud_light_2)
|
||||
WeatherIcon.PartlyCloudy,
|
||||
WeatherIcon.BrokenClouds -> colorResource(id = R.color.weather_cloud_light_1)
|
||||
else -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
}
|
||||
|
||||
Icon(
|
||||
Icons.Rounded.WeatherCloud,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.offset(offset.x, offset.y)
|
||||
.scale(scale),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Cloud2(icon: WeatherIcon) {
|
||||
|
||||
when (icon) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.MostlyCloudy,
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog,
|
||||
WeatherIcon.BrokenClouds -> {
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
|
||||
val scale = when (icon) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.MostlyCloudy,
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog -> 1.1f
|
||||
WeatherIcon.BrokenClouds -> 0.7f
|
||||
else -> 0f
|
||||
}
|
||||
val offset = when (icon) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> DpOffset(-3.dp, -3.dp)
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog -> DpOffset(-3.dp, 1.5.dp)
|
||||
WeatherIcon.BrokenClouds -> DpOffset(4.5.dp, -4.5.dp)
|
||||
WeatherIcon.MostlyCloudy -> DpOffset(2.dp, 2.5.dp)
|
||||
else -> DpOffset.Zero
|
||||
}
|
||||
val color = when (icon) {
|
||||
WeatherIcon.BrokenClouds -> colorResource(id = R.color.weather_cloud_light_2)
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog -> colorResource(id = R.color.weather_cloud_medium_1)
|
||||
WeatherIcon.MostlyCloudy -> colorResource(id = R.color.weather_cloud_medium_1)
|
||||
else -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
}
|
||||
|
||||
Icon(
|
||||
Icons.Rounded.WeatherCloud,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.offset(offset.x, offset.y)
|
||||
.scale(scale),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Cloud3(icon: WeatherIcon) {
|
||||
when (icon) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain,
|
||||
WeatherIcon.Cloudy -> {
|
||||
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
val offset = when (icon) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> DpOffset(3.dp, -2.dp)
|
||||
WeatherIcon.Cloudy,
|
||||
WeatherIcon.Fog -> DpOffset(3.dp, 2.5.dp)
|
||||
else -> DpOffset.Zero
|
||||
}
|
||||
val color = when (icon) {
|
||||
WeatherIcon.Thunderstorm,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstorm,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> colorResource(id = R.color.weather_cloud_dark_1)
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Sleet,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Cloudy -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Snow -> colorResource(id = R.color.weather_cloud_light_2)
|
||||
else -> colorResource(id = R.color.weather_cloud_medium_2)
|
||||
}
|
||||
|
||||
Icon(
|
||||
Icons.Rounded.WeatherCloud,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.offset(offset.x, offset.y),
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Precipitation(icon: WeatherIcon) {
|
||||
if (icon == WeatherIcon.Sleet) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.offset(y = 2.dp)
|
||||
.size(32.dp),
|
||||
imageVector = Icons.Rounded.WeatherSleetSnow,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_snow)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.offset(y = 2.dp)
|
||||
.size(32.dp),
|
||||
imageVector = Icons.Rounded.WeatherSleetRain,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_rain)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
when (icon) {
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Hail,
|
||||
WeatherIcon.Snow,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> {
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
val vector = when (icon) {
|
||||
WeatherIcon.Drizzle -> Icons.Rounded.WeatherLightRain
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> Icons.Rounded.WeatherRain
|
||||
WeatherIcon.Hail, WeatherIcon.Snow -> Icons.Rounded.WeatherHail
|
||||
else -> Icons.Rounded.WeatherLightRain
|
||||
}
|
||||
val color = when (icon) {
|
||||
WeatherIcon.Drizzle,
|
||||
WeatherIcon.Showers,
|
||||
WeatherIcon.ThunderstormWithRain,
|
||||
WeatherIcon.HeavyThunderstormWithRain -> colorResource(id = R.color.weather_rain)
|
||||
WeatherIcon.Hail -> colorResource(id = R.color.weather_hail)
|
||||
WeatherIcon.Snow -> colorResource(id = R.color.weather_snow)
|
||||
else -> Color.Unspecified
|
||||
}
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.offset(y = 2.dp)
|
||||
.size(32.dp),
|
||||
imageVector = vector,
|
||||
contentDescription = null,
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HotCold(icon: WeatherIcon) {
|
||||
if (icon == WeatherIcon.Hot) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp),
|
||||
imageVector = Icons.Rounded.Thermostat,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_hot)
|
||||
)
|
||||
}
|
||||
if (icon == WeatherIcon.Cold) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(32.dp),
|
||||
imageVector = Icons.Rounded.AcUnit,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_cold)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Wind(icon: WeatherIcon) {
|
||||
if (icon == WeatherIcon.Storm) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.offset(x = 4.dp, y = 0.dp)
|
||||
.size(24.dp),
|
||||
imageVector = Icons.Rounded.Air,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_wind_dark)
|
||||
)
|
||||
}
|
||||
if (icon == WeatherIcon.Wind || icon == WeatherIcon.Storm) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(if(icon == WeatherIcon.Wind) 32.dp else 24.dp),
|
||||
imageVector = Icons.Rounded.Air,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_wind)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Fog(icon: WeatherIcon) {
|
||||
|
||||
if (icon != WeatherIcon.Haze && icon != WeatherIcon.Fog) {
|
||||
return
|
||||
}
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.offset(x = 3.dp, y = 4.dp),
|
||||
imageVector = Icons.Rounded.WeatherFog,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_fog)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LightningBolts(icon: WeatherIcon) {
|
||||
|
||||
if (icon != WeatherIcon.ThunderstormWithRain
|
||||
&& icon != WeatherIcon.HeavyThunderstormWithRain
|
||||
&& icon != WeatherIcon.Thunderstorm
|
||||
&& icon != WeatherIcon.HeavyThunderstorm
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
val isHeavy = icon == WeatherIcon.HeavyThunderstormWithRain || icon == WeatherIcon.HeavyThunderstorm
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.offset(x = if (isHeavy) 4.dp else 1.dp, y = 6.dp),
|
||||
imageVector = Icons.Rounded.Bolt,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_lightning_bolt)
|
||||
)
|
||||
|
||||
if (icon == WeatherIcon.HeavyThunderstorm || icon == WeatherIcon.HeavyThunderstormWithRain) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.offset(x = -3.dp, y = 6.dp),
|
||||
imageVector = Icons.Rounded.Bolt,
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.weather_lightning_bolt)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
package de.mm20.launcher2.ui.icons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.ui.graphics.vector.group
|
||||
|
||||
val Icons.Rounded.Battery0Bar
|
||||
get() = materialIcon("Icons.Rounded.Battery0Bar") {
|
||||
materialPath {
|
||||
moveTo(17f, 5f)
|
||||
verticalLineToRelative(16f)
|
||||
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
|
||||
horizontalLineTo(8f)
|
||||
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
|
||||
verticalLineTo(5f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
verticalLineTo(3f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
|
||||
verticalLineToRelative(1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
|
||||
close()
|
||||
moveTo(15f, 6f)
|
||||
horizontalLineTo(9f)
|
||||
verticalLineToRelative(14f)
|
||||
horizontalLineToRelative(6f)
|
||||
verticalLineTo(6f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Battery1Bar
|
||||
get() = materialIcon("Icons.Rounded.Battery1Bar") {
|
||||
materialPath {
|
||||
moveTo(17f, 5f)
|
||||
verticalLineToRelative(16f)
|
||||
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
|
||||
horizontalLineTo(8f)
|
||||
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
|
||||
verticalLineTo(5f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
verticalLineTo(3f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
|
||||
verticalLineToRelative(1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
|
||||
close()
|
||||
moveTo(15f, 6f)
|
||||
horizontalLineTo(9f)
|
||||
verticalLineToRelative(12f)
|
||||
horizontalLineToRelative(6f)
|
||||
verticalLineTo(6f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Battery2Bar
|
||||
get() = materialIcon("Icons.Rounded.Battery2Bar") {
|
||||
materialPath {
|
||||
moveTo(17f, 5f)
|
||||
verticalLineToRelative(16f)
|
||||
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
|
||||
horizontalLineTo(8f)
|
||||
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
|
||||
verticalLineTo(5f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
verticalLineTo(3f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
|
||||
verticalLineToRelative(1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
|
||||
close()
|
||||
moveTo(15f, 6f)
|
||||
horizontalLineTo(9f)
|
||||
verticalLineToRelative(10f)
|
||||
horizontalLineToRelative(6f)
|
||||
verticalLineTo(6f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Battery3Bar
|
||||
get() = materialIcon("Icons.Rounded.Battery3Bar") {
|
||||
materialPath {
|
||||
moveTo(17f, 5f)
|
||||
verticalLineToRelative(16f)
|
||||
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
|
||||
horizontalLineTo(8f)
|
||||
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
|
||||
verticalLineTo(5f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
verticalLineTo(3f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
|
||||
verticalLineToRelative(1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
|
||||
close()
|
||||
moveTo(15f, 6f)
|
||||
horizontalLineTo(9f)
|
||||
verticalLineToRelative(8f)
|
||||
horizontalLineToRelative(6f)
|
||||
verticalLineTo(6f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Battery4Bar
|
||||
get() = materialIcon("Icons.Rounded.Battery4Bar") {
|
||||
materialPath {
|
||||
moveTo(17f, 5f)
|
||||
verticalLineToRelative(16f)
|
||||
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
|
||||
horizontalLineTo(8f)
|
||||
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
|
||||
verticalLineTo(5f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
verticalLineTo(3f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
|
||||
verticalLineToRelative(1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
|
||||
close()
|
||||
moveTo(15f, 6f)
|
||||
horizontalLineTo(9f)
|
||||
verticalLineToRelative(6f)
|
||||
horizontalLineToRelative(6f)
|
||||
verticalLineTo(6f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Battery5Bar
|
||||
get() = materialIcon("Icons.Rounded.Battery5Bar") {
|
||||
materialPath {
|
||||
moveTo(17f, 5f)
|
||||
verticalLineToRelative(16f)
|
||||
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
|
||||
horizontalLineTo(8f)
|
||||
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
|
||||
verticalLineTo(5f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
verticalLineTo(3f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
|
||||
verticalLineToRelative(1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
|
||||
close()
|
||||
moveTo(15f, 6f)
|
||||
horizontalLineTo(9f)
|
||||
verticalLineToRelative(4f)
|
||||
horizontalLineToRelative(6f)
|
||||
verticalLineTo(6f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Battery6Bar
|
||||
get() = materialIcon("Icons.Rounded.Battery6Bar") {
|
||||
materialPath {
|
||||
moveTo(17f, 5f)
|
||||
verticalLineToRelative(16f)
|
||||
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
|
||||
horizontalLineTo(8f)
|
||||
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
|
||||
verticalLineTo(5f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
verticalLineTo(3f)
|
||||
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
|
||||
verticalLineToRelative(1f)
|
||||
horizontalLineToRelative(2f)
|
||||
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
|
||||
close()
|
||||
moveTo(15f, 6f)
|
||||
horizontalLineTo(9f)
|
||||
verticalLineToRelative(2f)
|
||||
horizontalLineToRelative(6f)
|
||||
verticalLineTo(6f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.BatteryCharging0Bar
|
||||
get() = materialIcon("Icons.Rounded.BatteryCharging0Bar") {
|
||||
group(
|
||||
scaleX = 0.26458333f,
|
||||
scaleY = 0.26458333f,
|
||||
) {
|
||||
materialPath {
|
||||
moveTo(41.574219f, 7.5585938f)
|
||||
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
|
||||
lineTo(37.794922f, 15.117188f)
|
||||
lineTo(30.236328f, 15.117188f)
|
||||
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
|
||||
lineTo(26.457031f, 79.369141f)
|
||||
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
|
||||
lineTo(60.472656f, 83.150391f)
|
||||
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
|
||||
lineTo(64.251953f, 18.898438f)
|
||||
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
|
||||
lineTo(52.914062f, 15.117188f)
|
||||
lineTo(52.914062f, 11.337891f)
|
||||
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
|
||||
lineTo(41.574219f, 7.5585938f)
|
||||
close()
|
||||
moveTo(34.015625f, 22.677734f)
|
||||
lineTo(56.693359f, 22.677734f)
|
||||
lineTo(56.693359f, 75.589844f)
|
||||
lineTo(34.015625f, 75.589844f)
|
||||
lineTo(34.015625f, 22.677734f)
|
||||
close()
|
||||
moveTo(47.253906f, 32.126953f)
|
||||
curveTo(46.605486f, 32.112843f, 45.950535f, 32.41843f, 45.582031f, 33.109375f)
|
||||
lineTo(35.490234f, 52.005859f)
|
||||
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
|
||||
lineTo(41.574219f, 54.802734f)
|
||||
lineTo(41.574219f, 68.03125f)
|
||||
curveTo(41.574219f, 69.996604f, 44.219867f, 70.640241f, 45.126953f, 68.939453f)
|
||||
lineTo(55.21875f, 50.041016f)
|
||||
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
|
||||
lineTo(49.134766f, 47.244141f)
|
||||
lineTo(49.134766f, 34.015625f)
|
||||
curveTo(49.134766f, 32.848696f, 48.201598f, 32.147575f, 47.253906f, 32.126953f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.BatteryCharging1Bar
|
||||
get() = materialIcon("Icons.Rounded.BatteryCharging1Bar") {
|
||||
group(
|
||||
scaleX = 0.26458333f,
|
||||
scaleY = 0.26458333f,
|
||||
) {
|
||||
materialPath {
|
||||
moveTo(41.574219f, 7.5585938f)
|
||||
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
|
||||
lineTo(37.794922f, 15.117188f)
|
||||
lineTo(30.236328f, 15.117188f)
|
||||
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
|
||||
lineTo(26.457031f, 79.369141f)
|
||||
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
|
||||
lineTo(60.472656f, 83.150391f)
|
||||
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
|
||||
lineTo(64.251953f, 18.898438f)
|
||||
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
|
||||
lineTo(52.914062f, 15.117188f)
|
||||
lineTo(52.914062f, 11.337891f)
|
||||
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
|
||||
lineTo(41.574219f, 7.5585938f)
|
||||
close()
|
||||
moveTo(34.015625f, 22.677734f)
|
||||
lineTo(56.693359f, 22.677734f)
|
||||
lineTo(56.693359f, 68.03125f)
|
||||
lineTo(45.611328f, 68.03125f)
|
||||
lineTo(45.126953f, 68.939453f)
|
||||
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
|
||||
lineTo(34.015625f, 68.03125f)
|
||||
lineTo(34.015625f, 22.677734f)
|
||||
close()
|
||||
moveTo(41.574219f, 68.03125f)
|
||||
lineTo(45.611328f, 68.03125f)
|
||||
lineTo(55.21875f, 50.041016f)
|
||||
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
|
||||
lineTo(49.134766f, 47.244141f)
|
||||
lineTo(49.134766f, 34.015625f)
|
||||
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
|
||||
lineTo(35.490234f, 52.005859f)
|
||||
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
|
||||
lineTo(41.574219f, 54.802734f)
|
||||
lineTo(41.574219f, 68.03125f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.BatteryCharging2Bar
|
||||
get() = materialIcon("Icons.Rounded.BatteryCharging2Bar") {
|
||||
group(
|
||||
scaleX = 0.26458333f,
|
||||
scaleY = 0.26458333f,
|
||||
) {
|
||||
materialPath {
|
||||
moveTo(41.574219f, 7.5585938f)
|
||||
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
|
||||
lineTo(37.794922f, 15.117188f)
|
||||
lineTo(30.236328f, 15.117188f)
|
||||
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
|
||||
lineTo(26.457031f, 79.369141f)
|
||||
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
|
||||
lineTo(60.472656f, 83.150391f)
|
||||
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
|
||||
lineTo(64.251953f, 18.898438f)
|
||||
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
|
||||
lineTo(52.914062f, 15.117188f)
|
||||
lineTo(52.914062f, 11.337891f)
|
||||
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
|
||||
lineTo(41.574219f, 7.5585938f)
|
||||
close()
|
||||
moveTo(34.015625f, 22.677734f)
|
||||
lineTo(56.693359f, 22.677734f)
|
||||
lineTo(56.693359f, 60.472656f)
|
||||
lineTo(49.648438f, 60.472656f)
|
||||
lineTo(45.126953f, 68.939453f)
|
||||
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
|
||||
lineTo(41.574219f, 60.472656f)
|
||||
lineTo(34.015625f, 60.472656f)
|
||||
lineTo(34.015625f, 22.677734f)
|
||||
close()
|
||||
moveTo(41.574219f, 60.472656f)
|
||||
lineTo(49.648438f, 60.472656f)
|
||||
lineTo(55.21875f, 50.041016f)
|
||||
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
|
||||
lineTo(49.134766f, 47.244141f)
|
||||
lineTo(49.134766f, 34.015625f)
|
||||
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
|
||||
lineTo(35.490234f, 52.005859f)
|
||||
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
|
||||
lineTo(41.574219f, 54.802734f)
|
||||
lineTo(41.574219f, 60.472656f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.BatteryCharging3Bar
|
||||
get() = materialIcon("Icons.Rounded.BatteryCharging3Bar") {
|
||||
group(
|
||||
scaleX = 0.26458333f,
|
||||
scaleY = 0.26458333f,
|
||||
) {
|
||||
materialPath {
|
||||
moveTo(41.574219f, 7.5585938f)
|
||||
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
|
||||
lineTo(37.794922f, 15.117188f)
|
||||
lineTo(30.236328f, 15.117188f)
|
||||
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
|
||||
lineTo(26.457031f, 79.369141f)
|
||||
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
|
||||
lineTo(60.472656f, 83.150391f)
|
||||
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
|
||||
lineTo(64.251953f, 18.898438f)
|
||||
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
|
||||
lineTo(52.914062f, 15.117188f)
|
||||
lineTo(52.914062f, 11.337891f)
|
||||
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
|
||||
lineTo(41.574219f, 7.5585938f)
|
||||
close()
|
||||
moveTo(34.015625f, 22.677734f)
|
||||
lineTo(56.693359f, 22.677734f)
|
||||
lineTo(56.693359f, 52.914062f)
|
||||
lineTo(53.683594f, 52.914062f)
|
||||
lineTo(45.126953f, 68.939453f)
|
||||
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
|
||||
lineTo(41.574219f, 54.802734f)
|
||||
lineTo(37.152344f, 54.802734f)
|
||||
curveTo(36.048819f, 54.802734f, 35.258483f, 53.910469f, 35.259766f, 52.914062f)
|
||||
lineTo(34.015625f, 52.914062f)
|
||||
lineTo(34.015625f, 22.677734f)
|
||||
close()
|
||||
moveTo(35.259766f, 52.914062f)
|
||||
lineTo(53.683594f, 52.914062f)
|
||||
lineTo(55.21875f, 50.041016f)
|
||||
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
|
||||
lineTo(49.134766f, 47.244141f)
|
||||
lineTo(49.134766f, 34.015625f)
|
||||
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
|
||||
lineTo(35.490234f, 52.005859f)
|
||||
curveTo(35.332642f, 52.303535f, 35.260152f, 52.613661f, 35.259766f, 52.914062f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.BatteryCharging4Bar
|
||||
get() = materialIcon("Icons.Rounded.BatteryCharging4Bar") {
|
||||
group(
|
||||
scaleX = 0.26458333f,
|
||||
scaleY = 0.26458333f,
|
||||
) {
|
||||
materialPath {
|
||||
moveTo(41.574219f, 7.5585938f)
|
||||
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
|
||||
lineTo(37.794922f, 15.117188f)
|
||||
lineTo(30.236328f, 15.117188f)
|
||||
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
|
||||
lineTo(26.457031f, 79.369141f)
|
||||
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
|
||||
lineTo(60.472656f, 83.150391f)
|
||||
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
|
||||
lineTo(64.251953f, 18.898438f)
|
||||
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
|
||||
lineTo(52.914062f, 15.117188f)
|
||||
lineTo(52.914062f, 11.337891f)
|
||||
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
|
||||
lineTo(41.574219f, 7.5585938f)
|
||||
close()
|
||||
moveTo(34.015625f, 22.677734f)
|
||||
lineTo(56.693359f, 22.677734f)
|
||||
lineTo(56.693359f, 45.353516f)
|
||||
lineTo(49.134766f, 45.353516f)
|
||||
lineTo(49.134766f, 47.244141f)
|
||||
lineTo(53.556641f, 47.244141f)
|
||||
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
|
||||
lineTo(45.126953f, 68.939453f)
|
||||
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
|
||||
lineTo(41.574219f, 54.802734f)
|
||||
lineTo(37.152344f, 54.802734f)
|
||||
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
|
||||
lineTo(39.042969f, 45.353516f)
|
||||
lineTo(34.015625f, 45.353516f)
|
||||
lineTo(34.015625f, 22.677734f)
|
||||
close()
|
||||
moveTo(39.042969f, 45.353516f)
|
||||
lineTo(49.134766f, 45.353516f)
|
||||
lineTo(49.134766f, 34.015625f)
|
||||
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
|
||||
lineTo(39.042969f, 45.353516f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.BatteryCharging5Bar
|
||||
get() = materialIcon("Icons.Rounded.BatteryCharging5Bar") {
|
||||
group(
|
||||
scaleX = 0.26458333f,
|
||||
scaleY = 0.26458333f,
|
||||
) {
|
||||
materialPath {
|
||||
moveTo(41.574219f, 7.5585938f)
|
||||
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
|
||||
lineTo(37.794922f, 15.117188f)
|
||||
lineTo(30.236328f, 15.117188f)
|
||||
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
|
||||
lineTo(26.457031f, 79.369141f)
|
||||
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
|
||||
lineTo(60.472656f, 83.150391f)
|
||||
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
|
||||
lineTo(64.251953f, 18.898438f)
|
||||
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
|
||||
lineTo(52.914062f, 15.117188f)
|
||||
lineTo(52.914062f, 11.337891f)
|
||||
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
|
||||
lineTo(41.574219f, 7.5585938f)
|
||||
close()
|
||||
moveTo(34.015625f, 22.677734f)
|
||||
lineTo(56.693359f, 22.677734f)
|
||||
lineTo(56.693359f, 37.794922f)
|
||||
lineTo(49.134766f, 37.794922f)
|
||||
lineTo(49.134766f, 47.244141f)
|
||||
lineTo(53.556641f, 47.244141f)
|
||||
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
|
||||
lineTo(45.126953f, 68.939453f)
|
||||
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
|
||||
lineTo(41.574219f, 54.802734f)
|
||||
lineTo(37.152344f, 54.802734f)
|
||||
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
|
||||
lineTo(43.080078f, 37.794922f)
|
||||
lineTo(34.015625f, 37.794922f)
|
||||
lineTo(34.015625f, 22.677734f)
|
||||
close()
|
||||
moveTo(43.080078f, 37.794922f)
|
||||
lineTo(49.134766f, 37.794922f)
|
||||
lineTo(49.134766f, 34.015625f)
|
||||
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
|
||||
lineTo(43.080078f, 37.794922f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.BatteryCharging6Bar
|
||||
get() = materialIcon("Icons.Rounded.BatteryCharging6Bar") {
|
||||
group(
|
||||
scaleX = 0.26458333f,
|
||||
scaleY = 0.26458333f,
|
||||
) {
|
||||
materialPath {
|
||||
moveTo(41.574219f, 7.5585938f)
|
||||
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
|
||||
lineTo(37.794922f, 15.117188f)
|
||||
lineTo(30.236328f, 15.117188f)
|
||||
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
|
||||
lineTo(26.457031f, 79.369141f)
|
||||
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
|
||||
lineTo(60.472656f, 83.150391f)
|
||||
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
|
||||
lineTo(64.251953f, 18.898438f)
|
||||
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
|
||||
lineTo(52.914062f, 15.117188f)
|
||||
lineTo(52.914062f, 11.337891f)
|
||||
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
|
||||
lineTo(41.574219f, 7.5585938f)
|
||||
close()
|
||||
moveTo(34.015625f, 22.677734f)
|
||||
lineTo(56.693359f, 22.677734f)
|
||||
lineTo(56.693359f, 30.236328f)
|
||||
lineTo(34.015625f, 30.236328f)
|
||||
lineTo(34.015625f, 22.677734f)
|
||||
close()
|
||||
moveTo(47.253906f, 32.126953f)
|
||||
curveTo(48.201598f, 32.147575f, 49.134766f, 32.848696f, 49.134766f, 34.015625f)
|
||||
lineTo(49.134766f, 47.244141f)
|
||||
lineTo(53.556641f, 47.244141f)
|
||||
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
|
||||
lineTo(45.126953f, 68.939453f)
|
||||
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
|
||||
lineTo(41.574219f, 54.802734f)
|
||||
lineTo(37.152344f, 54.802734f)
|
||||
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
|
||||
lineTo(45.582031f, 33.109375f)
|
||||
curveTo(45.950535f, 32.41843f, 46.605486f, 32.112843f, 47.253906f, 32.126953f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,859 @@
|
||||
package de.mm20.launcher2.ui.icons
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
val Icons.Rounded.Pdf: ImageVector
|
||||
get() = materialIcon("Icons.Rounded.Pdf") {
|
||||
materialPath {
|
||||
moveTo(11.129145f, 3.328324f)
|
||||
curveTo(8.6213132f, 3.3999763f, 9.5656688f, 7.7995412f, 9.9955824f, 9.1752658f)
|
||||
curveTo(9.9669215f, 9.2469181f, 9.5802228f, 11.696644f, 7.000739f, 15.89547f)
|
||||
curveTo(7.000739f, 15.89547f, 1.985188f, 18.489173f, 3.1746167f, 20.438116f)
|
||||
curveTo(4.1347578f, 21.957146f, 6.4999564f, 20.380906f, 8.5205521f, 16.611993f)
|
||||
curveTo(8.5205521f, 16.611993f, 11.126568f, 15.694396f, 14.580212f, 15.436448f)
|
||||
curveTo(14.580212f, 15.436448f, 20.113003f, 17.930845f, 20.858187f, 15.279709f)
|
||||
curveTo(21.603372f, 12.642903f, 16.487507f, 13.216122f, 15.599018f, 13.488401f)
|
||||
curveTo(15.599018f, 13.488401f, 12.675603f, 11.569238f, 12.016402f, 8.9037707f)
|
||||
curveTo(12.016402f, 8.9037707f, 13.636976f, 3.2566717f, 11.129145f, 3.328324f)
|
||||
close()
|
||||
|
||||
moveTo(11.157134f, 4.2015868f)
|
||||
curveTo(11.93098f, 4.2875695f, 11.157134f, 7.2115443f, 11.157134f, 7.2831966f)
|
||||
curveTo(11.142804f, 7.2115443f, 10.626907f, 4.1442649f, 11.157134f, 4.2015868f)
|
||||
moveTo(11.157134f, 10.681084f)
|
||||
curveTo(11.200126f, 10.681084f, 11.828763f, 12.39962f, 13.863687f, 14.190928f)
|
||||
curveTo(13.863687f, 14.190928f, 10.526593f, 14.850019f, 9.007564f, 15.50922f)
|
||||
curveTo(9.007564f, 15.50922f, 10.440611f, 13.00262f, 11.157134f, 10.681084f)
|
||||
close()
|
||||
|
||||
moveTo(17.642229f, 14.515604f)
|
||||
curveTo(18.690144f, 14.571358f, 20.077401f, 14.907787f, 20.012913f, 15.251718f)
|
||||
curveTo(19.89827f, 15.724625f, 16.774564f, 14.563186f, 16.774564f, 14.563186f)
|
||||
curveTo(16.982356f, 14.509446f, 17.292924f, 14.497019f, 17.642229f, 14.515604f)
|
||||
close()
|
||||
|
||||
moveTo(6.6564719f, 17.227756f)
|
||||
curveTo(5.8969573f, 18.961742f, 4.6069915f, 20.066196f, 4.2773909f, 20.051866f)
|
||||
curveTo(3.9477902f, 20.037535f, 5.2807473f, 17.757984f, 6.6564719f, 17.227756f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherCloud
|
||||
get() = materialIcon("Icons.Rounded.WeatherCloud") {
|
||||
materialPath {
|
||||
moveTo(15.240234f, 6.953125f)
|
||||
arcTo(4.2818656f, 4.2818656f, 0f, false, false, 11.722656f, 8.7949219f)
|
||||
arcTo(3.8614116f, 3.8614116f, 0f, false, false, 9.2871094f, 7.9296875f)
|
||||
arcTo(3.8614116f, 3.8614116f, 0f, false, false, 5.4257812f, 11.791016f)
|
||||
arcTo(3.8614116f, 3.8614116f, 0f, false, false, 5.4335938f, 12.035156f)
|
||||
arcTo(3.0096498f, 3.0096498f, 0f, false, false, 4.8671875f, 11.980469f)
|
||||
arcTo(3.0096498f, 3.0096498f, 0f, false, false, 1.8574219f, 14.990234f)
|
||||
arcTo(3.0096498f, 3.0096498f, 0f, false, false, 4.8671875f, 18f)
|
||||
arcTo(3.0096498f, 3.0096498f, 0f, false, false, 4.9316406f, 18f)
|
||||
lineTo(19.220703f, 18f)
|
||||
lineTo(19.220703f, 17.998047f)
|
||||
arcTo(2.5873528f, 2.5873528f, 0f, false, false, 21.742188f, 15.412109f)
|
||||
arcTo(2.5873528f, 2.5873528f, 0f, false, false, 19.214844f, 12.826172f)
|
||||
arcTo(4.2818656f, 4.2818656f, 0f, false, false, 19.523438f, 11.234375f)
|
||||
arcTo(4.2818656f, 4.2818656f, 0f, false, false, 15.240234f, 6.953125f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherLightRain
|
||||
get() = materialIcon("Icons.Rounded.WeatherLightRain") {
|
||||
materialPath {
|
||||
moveTo(7.7747158f, 16.298471f)
|
||||
curveTo(7.7747158f, 16.298471f, 6.3149844f, 17.454229f, 6.1717047f, 18.266809f)
|
||||
arcTo(1.237673f, 1.237673f, 0f, false, false, 7.1769077f, 19.699334f)
|
||||
arcTo(1.237673f, 1.237673f, 0f, false, false, 8.6094328f, 18.696345f)
|
||||
curveTo(8.7527126f, 17.883765f, 7.7747158f, 16.298471f, 7.7747158f, 16.298471f)
|
||||
close()
|
||||
moveTo(12.3092f, 16.298471f)
|
||||
curveTo(12.3092f, 16.298471f, 10.849468f, 17.454229f, 10.706189f, 18.266809f)
|
||||
arcTo(1.237673f, 1.237673f, 0f, false, false, 11.711392f, 19.699334f)
|
||||
arcTo(1.237673f, 1.237673f, 0f, false, false, 13.143917f, 18.696345f)
|
||||
curveTo(13.287198f, 17.883765f, 12.3092f, 16.298471f, 12.3092f, 16.298471f)
|
||||
close()
|
||||
moveTo(16.843685f, 16.298471f)
|
||||
curveTo(16.843685f, 16.298471f, 15.383952f, 17.454229f, 15.240673f, 18.266809f)
|
||||
arcTo(1.237673f, 1.237673f, 0f, false, false, 16.245876f, 19.699334f)
|
||||
arcTo(1.237673f, 1.237673f, 0f, false, false, 17.678401f, 18.696345f)
|
||||
curveTo(17.821682f, 17.883765f, 16.843685f, 16.298471f, 16.843685f, 16.298471f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherHail
|
||||
get() = materialIcon("Icons.Rounded.WeatherHail") {
|
||||
materialPath {
|
||||
moveTo(7.4093667f, 16.258438f)
|
||||
curveTo(6.6975174f, 16.235656f, 6.09006f, 16.947847f, 6.2243877f, 17.647106f)
|
||||
curveTo(6.3138584f, 18.353739f, 7.1127111f, 18.841403f, 7.7821731f, 18.599057f)
|
||||
curveTo(8.4660776f, 18.399808f, 8.8227568f, 17.533933f, 8.4775745f, 16.910825f)
|
||||
curveTo(8.2792236f, 16.515419f, 7.8517895f, 16.254332f, 7.4093667f, 16.258438f)
|
||||
close()
|
||||
moveTo(11.935705f, 16.258438f)
|
||||
curveTo(11.223856f, 16.235656f, 10.616399f, 16.947847f, 10.750726f, 17.647106f)
|
||||
curveTo(10.840183f, 18.35367f, 11.639079f, 18.841563f, 12.308486f, 18.598897f)
|
||||
curveTo(12.992113f, 18.399396f, 13.348471f, 17.533766f, 13.003396f, 16.910825f)
|
||||
curveTo(12.805121f, 16.51558f, 12.377952f, 16.254528f, 11.935705f, 16.258438f)
|
||||
close()
|
||||
moveTo(16.462044f, 16.258438f)
|
||||
curveTo(15.750195f, 16.235656f, 15.142737f, 16.947847f, 15.277065f, 17.647106f)
|
||||
curveTo(15.366522f, 18.35367f, 16.165418f, 18.841563f, 16.834825f, 18.598897f)
|
||||
curveTo(17.518452f, 18.399396f, 17.87481f, 17.533766f, 17.529735f, 16.910825f)
|
||||
curveTo(17.331459f, 16.51558f, 16.904291f, 16.254528f, 16.462044f, 16.258438f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherSleetSnow
|
||||
get() = materialIcon("Icons.Rounded.WeatherSleetSnow") {
|
||||
materialPath {
|
||||
moveTo(11.935705f, 16.258438f)
|
||||
curveTo(11.223856f, 16.235656f, 10.616399f, 16.947847f, 10.750726f, 17.647106f)
|
||||
curveTo(10.840183f, 18.35367f, 11.639079f, 18.841563f, 12.308486f, 18.598897f)
|
||||
curveTo(12.992113f, 18.399396f, 13.348471f, 17.533766f, 13.003396f, 16.910825f)
|
||||
curveTo(12.805121f, 16.51558f, 12.377952f, 16.254528f, 11.935705f, 16.258438f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherSleetRain
|
||||
get() = materialIcon("Icons.Rounded.WeatherSleetRain") {
|
||||
materialPath {
|
||||
moveTo(6.347656f, 15.96875f)
|
||||
curveTo(5.938469f, 15.896599f, 5.550666f, 16.166984f, 5.478515f, 16.576172f)
|
||||
lineTo(5.044922f, 19.039062f)
|
||||
curveTo(4.972771f, 19.44825f, 5.243156f, 19.836052f, 5.652344f, 19.908203f)
|
||||
curveTo(6.061531f, 19.980354f, 6.449333f, 19.708016f, 6.521484f, 19.298828f)
|
||||
lineTo(6.955078f, 16.837891f)
|
||||
curveTo(7.0272288f, 16.428703f, 6.7568437f, 16.040901f, 6.347656f, 15.96875f)
|
||||
close()
|
||||
moveTo(18.347656f, 15.96875f)
|
||||
curveTo(17.938469f, 15.896599f, 17.550666f, 16.166984f, 17.478516f, 16.576172f)
|
||||
lineTo(17.044922f, 19.039062f)
|
||||
curveTo(16.972771f, 19.44825f, 17.243156f, 19.836052f, 17.652344f, 19.908203f)
|
||||
curveTo(18.061531f, 19.980354f, 18.449333f, 19.708016f, 18.521484f, 19.298828f)
|
||||
lineTo(18.955078f, 16.837891f)
|
||||
curveTo(19.027229f, 16.428703f, 18.756844f, 16.040901f, 18.347656f, 15.96875f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherRain
|
||||
get() = materialIcon("Icons.Rounded.WeatherRain") {
|
||||
materialPath {
|
||||
moveTo(6.347656f, 15.96875f)
|
||||
curveTo(5.938469f, 15.896599f, 5.550666f, 16.166984f, 5.478515f, 16.576172f)
|
||||
lineTo(5.044922f, 19.039062f)
|
||||
curveTo(4.972771f, 19.44825f, 5.243156f, 19.836052f, 5.652344f, 19.908203f)
|
||||
curveTo(6.061531f, 19.980354f, 6.449333f, 19.708016f, 6.521484f, 19.298828f)
|
||||
lineTo(6.955078f, 16.837891f)
|
||||
curveTo(7.0272288f, 16.428703f, 6.7568437f, 16.040901f, 6.347656f, 15.96875f)
|
||||
close()
|
||||
moveTo(9.3476561f, 15.96875f)
|
||||
curveTo(8.9384685f, 15.896599f, 8.5506663f, 16.166984f, 8.4785155f, 16.576172f)
|
||||
lineTo(8.0449218f, 19.039062f)
|
||||
curveTo(7.9727709f, 19.44825f, 8.243156f, 19.836052f, 8.6523436f, 19.908203f)
|
||||
curveTo(9.0615312f, 19.980354f, 9.4493334f, 19.708016f, 9.5214842f, 19.298828f)
|
||||
lineTo(9.955078f, 16.837891f)
|
||||
curveTo(10.027229f, 16.428703f, 9.7568437f, 16.040901f, 9.3476561f, 15.96875f)
|
||||
close()
|
||||
moveTo(12.347656f, 15.96875f)
|
||||
curveTo(11.938469f, 15.896599f, 11.550666f, 16.166984f, 11.478516f, 16.576172f)
|
||||
lineTo(11.044922f, 19.039062f)
|
||||
curveTo(10.972771f, 19.44825f, 11.243156f, 19.836052f, 11.652344f, 19.908203f)
|
||||
curveTo(12.061531f, 19.980354f, 12.449333f, 19.708016f, 12.521484f, 19.298828f)
|
||||
lineTo(12.955078f, 16.837891f)
|
||||
curveTo(13.027229f, 16.428703f, 12.756844f, 16.040901f, 12.347656f, 15.96875f)
|
||||
close()
|
||||
moveTo(15.347656f, 15.96875f)
|
||||
curveTo(14.938469f, 15.896599f, 14.550666f, 16.166984f, 14.478516f, 16.576172f)
|
||||
lineTo(14.044922f, 19.039062f)
|
||||
curveTo(13.972771f, 19.44825f, 14.243156f, 19.836052f, 14.652344f, 19.908203f)
|
||||
curveTo(15.061531f, 19.980354f, 15.449333f, 19.708016f, 15.521484f, 19.298828f)
|
||||
lineTo(15.955078f, 16.837891f)
|
||||
curveTo(16.027229f, 16.428703f, 15.756844f, 16.040901f, 15.347656f, 15.96875f)
|
||||
close()
|
||||
moveTo(18.347656f, 15.96875f)
|
||||
curveTo(17.938469f, 15.896599f, 17.550666f, 16.166984f, 17.478516f, 16.576172f)
|
||||
lineTo(17.044922f, 19.039062f)
|
||||
curveTo(16.972771f, 19.44825f, 17.243156f, 19.836052f, 17.652344f, 19.908203f)
|
||||
curveTo(18.061531f, 19.980354f, 18.449333f, 19.708016f, 18.521484f, 19.298828f)
|
||||
lineTo(18.955078f, 16.837891f)
|
||||
curveTo(19.027229f, 16.428703f, 18.756844f, 16.040901f, 18.347656f, 15.96875f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherLightRainAnimatable
|
||||
get() = materialIcon("Icons.Rounded.WeatherLightRainAnimatable") {
|
||||
materialPath {
|
||||
moveTo(4.828129f, 0f)
|
||||
curveTo(4.828129f, 0f, 3.6375041f, 1.3394488f, 3.6375041f, 2.1332031f)
|
||||
curveTo(3.6375041f, 2.7907671f, 4.1705651f, 3.3238281f, 4.828129f, 3.3238281f)
|
||||
curveTo(5.485693f, 3.3238281f, 6.018754f, 2.7907671f, 6.018754f, 2.1332031f)
|
||||
curveTo(6.018754f, 1.3394488f, 4.828129f, 0f, 4.828129f, 0f)
|
||||
close()
|
||||
moveTo(12.765629f, 0f)
|
||||
curveTo(12.765629f, 0f, 11.575004f, 1.3394488f, 11.575004f, 2.1332031f)
|
||||
curveTo(11.575004f, 2.7907671f, 12.108065f, 3.3238281f, 12.765629f, 3.3238281f)
|
||||
curveTo(13.423193f, 3.3238281f, 13.956254f, 2.7907671f, 13.956254f, 2.1332031f)
|
||||
curveTo(13.956254f, 1.3394488f, 12.765629f, 0f, 12.765629f, 0f)
|
||||
close()
|
||||
moveTo(20.703129f, 0f)
|
||||
curveTo(20.703129f, 0f, 19.512504f, 1.3394488f, 19.512504f, 2.1332031f)
|
||||
curveTo(19.512504f, 2.7907671f, 20.045565f, 3.3238281f, 20.703129f, 3.3238281f)
|
||||
curveTo(21.360693f, 3.3238281f, 21.893754f, 2.7907671f, 21.893754f, 2.1332031f)
|
||||
curveTo(21.893754f, 1.3394488f, 20.703129f, 0f, 20.703129f, 0f)
|
||||
close()
|
||||
moveTo(8.796879f, 3.96875f)
|
||||
curveTo(8.796879f, 3.96875f, 7.606254f, 5.3081983f, 7.606254f, 6.101953f)
|
||||
curveTo(7.606254f, 6.759517f, 8.1393151f, 7.292578f, 8.796879f, 7.292578f)
|
||||
curveTo(9.4544429f, 7.292578f, 9.987504f, 6.759517f, 9.987504f, 6.101953f)
|
||||
curveTo(9.987504f, 5.3081983f, 8.796879f, 3.96875f, 8.796879f, 3.96875f)
|
||||
close()
|
||||
moveTo(16.734379f, 3.96875f)
|
||||
curveTo(16.734379f, 3.96875f, 16.659965f, 4.0524664f, 16.548344f, 4.1904419f)
|
||||
curveTo(16.21348f, 4.6043688f, 15.543754f, 5.5066369f, 15.543754f, 6.101953f)
|
||||
curveTo(15.54381f, 6.7411813f, 16.048624f, 7.2662972f, 16.687353f, 7.2915446f)
|
||||
curveTo(16.70302f, 7.2921986f, 16.718698f, 7.2925434f, 16.734379f, 7.292578f)
|
||||
curveTo(17.391943f, 7.292578f, 17.925004f, 6.759517f, 17.925004f, 6.101953f)
|
||||
curveTo(17.925004f, 5.3081983f, 16.734379f, 3.96875f, 16.734379f, 3.96875f)
|
||||
close()
|
||||
moveTo(4.828129f, 7.9374999f)
|
||||
curveTo(4.828129f, 7.9374999f, 3.6375041f, 9.2769485f, 3.6375041f, 10.070703f)
|
||||
curveTo(3.6375041f, 10.728267f, 4.1705651f, 11.261328f, 4.828129f, 11.261328f)
|
||||
curveTo(5.485693f, 11.261328f, 6.018754f, 10.728267f, 6.018754f, 10.070703f)
|
||||
curveTo(6.018754f, 9.2769485f, 4.828129f, 7.9374999f, 4.828129f, 7.9374999f)
|
||||
close()
|
||||
moveTo(12.765629f, 7.9374999f)
|
||||
curveTo(12.765629f, 7.9374999f, 12.691215f, 8.0212162f, 12.579594f, 8.1591919f)
|
||||
curveTo(12.24473f, 8.5731188f, 11.575004f, 9.4753868f, 11.575004f, 10.070703f)
|
||||
curveTo(11.57496f, 10.567853f, 11.883816f, 11.012697f, 12.349634f, 11.186397f)
|
||||
curveTo(12.364344f, 11.191857f, 12.379161f, 11.197026f, 12.394075f, 11.2019f)
|
||||
curveTo(12.499019f, 11.236323f, 12.608242f, 11.255976f, 12.718603f, 11.260295f)
|
||||
curveTo(12.73427f, 11.260949f, 12.749948f, 11.261293f, 12.765629f, 11.261328f)
|
||||
curveTo(13.423193f, 11.261328f, 13.956254f, 10.728267f, 13.956254f, 10.070703f)
|
||||
curveTo(13.956254f, 9.2769485f, 12.765629f, 7.9374999f, 12.765629f, 7.9374999f)
|
||||
close()
|
||||
moveTo(20.703129f, 7.9374999f)
|
||||
curveTo(20.703129f, 7.9374999f, 20.628715f, 8.0212162f, 20.517094f, 8.1591919f)
|
||||
curveTo(20.18223f, 8.5731188f, 19.512504f, 9.4753868f, 19.512504f, 10.070703f)
|
||||
curveTo(19.51256f, 10.709931f, 20.017374f, 11.235047f, 20.656103f, 11.260295f)
|
||||
curveTo(20.67177f, 11.260949f, 20.687448f, 11.261293f, 20.703129f, 11.261328f)
|
||||
curveTo(21.360693f, 11.261328f, 21.893754f, 10.728267f, 21.893754f, 10.070703f)
|
||||
curveTo(21.893754f, 9.2769485f, 20.703129f, 7.9374999f, 20.703129f, 7.9374999f)
|
||||
close()
|
||||
moveTo(8.796879f, 11.90625f)
|
||||
curveTo(8.796879f, 11.90625f, 7.606254f, 13.245698f, 7.606254f, 14.039453f)
|
||||
curveTo(7.606254f, 14.697017f, 8.1393151f, 15.230078f, 8.796879f, 15.230078f)
|
||||
curveTo(9.4544429f, 15.230078f, 9.987504f, 14.697017f, 9.987504f, 14.039453f)
|
||||
curveTo(9.987504f, 13.245698f, 8.796879f, 11.90625f, 8.796879f, 11.90625f)
|
||||
close()
|
||||
moveTo(16.734379f, 11.90625f)
|
||||
curveTo(16.734379f, 11.90625f, 15.543754f, 13.245698f, 15.543754f, 14.039453f)
|
||||
curveTo(15.543754f, 14.697017f, 16.076815f, 15.230078f, 16.734379f, 15.230078f)
|
||||
curveTo(17.391943f, 15.230078f, 17.925004f, 14.697017f, 17.925004f, 14.039453f)
|
||||
curveTo(17.925004f, 13.245698f, 16.734379f, 11.90625f, 16.734379f, 11.90625f)
|
||||
close()
|
||||
moveTo(4.828129f, 15.875f)
|
||||
curveTo(4.828129f, 15.875f, 3.6375041f, 17.214448f, 3.6375041f, 18.008203f)
|
||||
curveTo(3.6375041f, 18.665767f, 4.1705651f, 19.198828f, 4.828129f, 19.198828f)
|
||||
curveTo(5.485693f, 19.198828f, 6.018754f, 18.665767f, 6.018754f, 18.008203f)
|
||||
curveTo(6.018754f, 17.214448f, 4.828129f, 15.875f, 4.828129f, 15.875f)
|
||||
close()
|
||||
moveTo(12.765629f, 15.875f)
|
||||
curveTo(12.765629f, 15.875f, 11.575004f, 17.214448f, 11.575004f, 18.008203f)
|
||||
curveTo(11.575004f, 18.665767f, 12.108065f, 19.198828f, 12.765629f, 19.198828f)
|
||||
curveTo(13.423193f, 19.198828f, 13.956254f, 18.665767f, 13.956254f, 18.008203f)
|
||||
curveTo(13.956254f, 17.214448f, 12.765629f, 15.875f, 12.765629f, 15.875f)
|
||||
close()
|
||||
moveTo(20.703129f, 15.875f)
|
||||
curveTo(20.703129f, 15.875f, 19.512504f, 17.214448f, 19.512504f, 18.008203f)
|
||||
curveTo(19.512504f, 18.665767f, 20.045565f, 19.198828f, 20.703129f, 19.198828f)
|
||||
curveTo(21.360693f, 19.198828f, 21.893754f, 18.665767f, 21.893754f, 18.008203f)
|
||||
curveTo(21.893754f, 17.214448f, 20.703129f, 15.875f, 20.703129f, 15.875f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val Icons.Rounded.WeatherHailAnimatable
|
||||
get() = materialIcon("Icons.Rounded.WeatherHailAnimatable") {
|
||||
materialPath {
|
||||
moveTo(4.8281292f, 1.0583334f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 3.5971949f, 2.2892661f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 4.8281292f, 3.5201987f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 6.0585449f, 2.2892661f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 4.8281292f, 1.0583334f)
|
||||
close()
|
||||
moveTo(12.765629f, 1.0583334f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 11.534695f, 2.2892661f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 12.765629f, 3.5201987f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 13.996045f, 2.2892661f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 12.765629f, 1.0583334f)
|
||||
close()
|
||||
moveTo(20.703129f, 1.0583334f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 19.472195f, 2.2892661f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 20.703129f, 3.5201987f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 21.933545f, 2.2892661f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 20.703129f, 1.0583334f)
|
||||
close()
|
||||
moveTo(8.7968792f, 5.0270833f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 7.5659449f, 6.258016f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 8.7968792f, 7.4889487f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 10.027295f, 6.258016f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 8.7968792f, 5.0270833f)
|
||||
close()
|
||||
moveTo(16.734379f, 5.0270833f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 15.503445f, 6.258016f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 16.734379f, 7.4889487f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 17.964795f, 6.258016f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 16.734379f, 5.0270833f)
|
||||
close()
|
||||
moveTo(4.8281292f, 8.9958333f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 3.5971949f, 10.226766f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 4.8281292f, 11.457699f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 6.0585449f, 10.226766f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 4.8281292f, 8.9958333f)
|
||||
close()
|
||||
moveTo(12.765629f, 8.9958333f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 11.534695f, 10.226766f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 12.765629f, 11.457699f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 13.996045f, 10.226766f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 12.765629f, 8.9958333f)
|
||||
close()
|
||||
moveTo(20.703129f, 8.9958333f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 19.472195f, 10.226766f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 20.703129f, 11.457699f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 21.933545f, 10.226766f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 20.703129f, 8.9958333f)
|
||||
close()
|
||||
moveTo(8.7968792f, 12.964583f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 7.5659449f, 14.195516f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 8.7968792f, 15.426449f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 10.027295f, 14.195516f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 8.7968792f, 12.964583f)
|
||||
close()
|
||||
moveTo(16.734379f, 12.964583f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 15.503445f, 14.195516f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 16.734379f, 15.426449f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 17.964795f, 14.195516f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 16.734379f, 12.964583f)
|
||||
close()
|
||||
moveTo(4.8281292f, 16.933333f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 3.5971949f, 18.164266f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 4.8281292f, 19.395199f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 6.0585449f, 18.164266f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 4.8281292f, 16.933333f)
|
||||
close()
|
||||
moveTo(12.765629f, 16.933333f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 11.534695f, 18.164266f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 12.765629f, 19.395199f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 13.996045f, 18.164266f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 12.765629f, 16.933333f)
|
||||
close()
|
||||
moveTo(20.703129f, 16.933333f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 19.472195f, 18.164266f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 20.703129f, 19.395199f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 21.933545f, 18.164266f)
|
||||
arcTo(1.2307138f, 1.2307138f, 0f, false, false, 20.703129f, 16.933333f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val Icons.Rounded.WeatherRainAnimatable
|
||||
get() = materialIcon("Icons.Rounded.WeatherRainAnimatable") {
|
||||
materialPath {
|
||||
moveTo(4.828129f, 0.1133928f)
|
||||
curveTo(4.41263f, 0.1133928f, 4.0777866f, 0.4477181f, 4.0777866f, 0.8632184f)
|
||||
verticalLineTo(4.8634909f)
|
||||
curveTo(4.0777866f, 5.278991f, 4.41263f, 5.6133161f, 4.828129f, 5.6133161f)
|
||||
curveTo(5.243628f, 5.6133161f, 5.5779529f, 5.278991f, 5.5779529f, 4.8634909f)
|
||||
verticalLineTo(0.8632184f)
|
||||
curveTo(5.5779529f, 0.4477181f, 5.243628f, 0.1133928f, 4.828129f, 0.1133928f)
|
||||
close()
|
||||
moveTo(12.765629f, 0.1133928f)
|
||||
curveTo(12.35013f, 0.1133928f, 12.015286f, 0.4477181f, 12.015286f, 0.8632184f)
|
||||
verticalLineTo(4.8634909f)
|
||||
curveTo(12.015286f, 5.278991f, 12.35013f, 5.6133161f, 12.765629f, 5.6133161f)
|
||||
curveTo(13.181128f, 5.6133161f, 13.515453f, 5.278991f, 13.515453f, 4.8634909f)
|
||||
verticalLineTo(0.8632184f)
|
||||
curveTo(13.515453f, 0.4477181f, 13.181128f, 0.1133928f, 12.765629f, 0.1133928f)
|
||||
close()
|
||||
moveTo(20.703129f, 0.1133928f)
|
||||
curveTo(20.28763f, 0.1133928f, 19.952786f, 0.4477181f, 19.952786f, 0.8632184f)
|
||||
verticalLineTo(4.8634909f)
|
||||
curveTo(19.952786f, 5.278991f, 20.28763f, 5.6133161f, 20.703129f, 5.6133161f)
|
||||
curveTo(21.118628f, 5.6133161f, 21.452953f, 5.278991f, 21.452953f, 4.8634909f)
|
||||
verticalLineTo(0.8632184f)
|
||||
curveTo(21.452953f, 0.4477181f, 21.118628f, 0.1133928f, 20.703129f, 0.1133928f)
|
||||
close()
|
||||
moveTo(8.796879f, 4.0821433f)
|
||||
curveTo(8.3813799f, 4.0821433f, 8.0465365f, 4.4164684f, 8.0465365f, 4.8319684f)
|
||||
verticalLineTo(8.8322408f)
|
||||
curveTo(8.0465365f, 9.247741f, 8.3813799f, 9.582066f, 8.796879f, 9.582066f)
|
||||
curveTo(9.212378f, 9.582066f, 9.5467028f, 9.247741f, 9.5467028f, 8.8322408f)
|
||||
verticalLineTo(4.8319684f)
|
||||
curveTo(9.5467028f, 4.4164684f, 9.212378f, 4.0821433f, 8.796879f, 4.0821433f)
|
||||
close()
|
||||
moveTo(16.734379f, 4.0821433f)
|
||||
curveTo(16.31888f, 4.0821433f, 15.984036f, 4.4164684f, 15.984036f, 4.8319684f)
|
||||
verticalLineTo(8.8322408f)
|
||||
curveTo(15.984036f, 9.247741f, 16.31888f, 9.582066f, 16.734379f, 9.582066f)
|
||||
curveTo(17.149878f, 9.582066f, 17.484203f, 9.247741f, 17.484203f, 8.8322408f)
|
||||
verticalLineTo(4.8319684f)
|
||||
curveTo(17.484203f, 4.4164684f, 17.149878f, 4.0821433f, 16.734379f, 4.0821433f)
|
||||
close()
|
||||
moveTo(4.828129f, 8.0508932f)
|
||||
curveTo(4.41263f, 8.0508932f, 4.0777866f, 8.3852183f, 4.0777866f, 8.8007184f)
|
||||
verticalLineTo(12.800991f)
|
||||
curveTo(4.0777866f, 13.216491f, 4.41263f, 13.550816f, 4.828129f, 13.550816f)
|
||||
curveTo(5.243628f, 13.550816f, 5.5779529f, 13.216491f, 5.5779529f, 12.800991f)
|
||||
verticalLineTo(8.8007184f)
|
||||
curveTo(5.5779529f, 8.3852183f, 5.243628f, 8.0508932f, 4.828129f, 8.0508932f)
|
||||
close()
|
||||
moveTo(12.765629f, 8.0508932f)
|
||||
curveTo(12.35013f, 8.0508932f, 12.015286f, 8.3852183f, 12.015286f, 8.8007184f)
|
||||
verticalLineTo(12.800991f)
|
||||
curveTo(12.015286f, 13.216491f, 12.35013f, 13.550816f, 12.765629f, 13.550816f)
|
||||
curveTo(13.181128f, 13.550816f, 13.515453f, 13.216491f, 13.515453f, 12.800991f)
|
||||
verticalLineTo(8.8007184f)
|
||||
curveTo(13.515453f, 8.3852183f, 13.181128f, 8.0508932f, 12.765629f, 8.0508932f)
|
||||
close()
|
||||
moveTo(20.703129f, 8.0508932f)
|
||||
curveTo(20.28763f, 8.0508932f, 19.952786f, 8.3852183f, 19.952786f, 8.8007184f)
|
||||
verticalLineTo(12.800991f)
|
||||
curveTo(19.952786f, 13.216491f, 20.28763f, 13.550816f, 20.703129f, 13.550816f)
|
||||
curveTo(21.118628f, 13.550816f, 21.452953f, 13.216491f, 21.452953f, 12.800991f)
|
||||
verticalLineTo(8.8007184f)
|
||||
curveTo(21.452953f, 8.3852183f, 21.118628f, 8.0508932f, 20.703129f, 8.0508932f)
|
||||
close()
|
||||
moveTo(8.796879f, 12.019643f)
|
||||
curveTo(8.3813799f, 12.019643f, 8.0465365f, 12.353968f, 8.0465365f, 12.769468f)
|
||||
verticalLineTo(16.769741f)
|
||||
curveTo(8.0465365f, 17.185241f, 8.3813799f, 17.519566f, 8.796879f, 17.519566f)
|
||||
curveTo(9.212378f, 17.519566f, 9.5467028f, 17.185241f, 9.5467028f, 16.769741f)
|
||||
verticalLineTo(12.769468f)
|
||||
curveTo(9.5467028f, 12.353968f, 9.212378f, 12.019643f, 8.796879f, 12.019643f)
|
||||
close()
|
||||
moveTo(16.734379f, 12.019643f)
|
||||
curveTo(16.31888f, 12.019643f, 15.984036f, 12.353968f, 15.984036f, 12.769468f)
|
||||
verticalLineTo(16.769741f)
|
||||
curveTo(15.984036f, 17.185241f, 16.31888f, 17.519566f, 16.734379f, 17.519566f)
|
||||
curveTo(17.149878f, 17.519566f, 17.484203f, 17.185241f, 17.484203f, 16.769741f)
|
||||
verticalLineTo(12.769468f)
|
||||
curveTo(17.484203f, 12.353968f, 17.149878f, 12.019643f, 16.734379f, 12.019643f)
|
||||
close()
|
||||
moveTo(4.828129f, 15.988393f)
|
||||
curveTo(4.41263f, 15.988393f, 4.0777866f, 16.322718f, 4.0777866f, 16.738218f)
|
||||
verticalLineTo(20.738491f)
|
||||
curveTo(4.0777866f, 21.153991f, 4.41263f, 21.488316f, 4.828129f, 21.488316f)
|
||||
curveTo(5.243628f, 21.488316f, 5.5779529f, 21.153991f, 5.5779529f, 20.738491f)
|
||||
verticalLineTo(16.738218f)
|
||||
curveTo(5.5779529f, 16.322718f, 5.243628f, 15.988393f, 4.828129f, 15.988393f)
|
||||
close()
|
||||
moveTo(12.765629f, 15.988393f)
|
||||
curveTo(12.35013f, 15.988393f, 12.015286f, 16.322718f, 12.015286f, 16.738218f)
|
||||
verticalLineTo(20.738491f)
|
||||
curveTo(12.015286f, 21.153991f, 12.35013f, 21.488316f, 12.765629f, 21.488316f)
|
||||
curveTo(13.181128f, 21.488316f, 13.515453f, 21.153991f, 13.515453f, 20.738491f)
|
||||
verticalLineTo(16.738218f)
|
||||
curveTo(13.515453f, 16.322718f, 13.181128f, 15.988393f, 12.765629f, 15.988393f)
|
||||
close()
|
||||
moveTo(20.703129f, 15.988393f)
|
||||
curveTo(20.28763f, 15.988393f, 19.952786f, 16.322718f, 19.952786f, 16.738218f)
|
||||
verticalLineTo(20.738491f)
|
||||
curveTo(19.952786f, 21.153991f, 20.28763f, 21.488316f, 20.703129f, 21.488316f)
|
||||
curveTo(21.118628f, 21.488316f, 21.452953f, 21.153991f, 21.452953f, 20.738491f)
|
||||
verticalLineTo(16.738218f)
|
||||
curveTo(21.452953f, 16.322718f, 21.118628f, 15.988393f, 20.703129f, 15.988393f)
|
||||
close()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherSleetRainAnimatable
|
||||
get() = materialIcon("Icons.Rounded.WeatherSleetRainAnimatable") {
|
||||
materialPath {
|
||||
moveTo(4.8281293f, 0.11317139f)
|
||||
curveTo(4.4126296f, 0.11317139f, 4.0777866f, 0.44749611f, 4.0777866f, 0.86299642f)
|
||||
verticalLineTo(4.8632689f)
|
||||
curveTo(4.0777866f, 5.278769f, 4.4126296f, 5.6130941f, 4.8281293f, 5.6130941f)
|
||||
curveTo(5.2436283f, 5.6130941f, 5.5779543f, 5.278769f, 5.5779543f, 4.8632689f)
|
||||
verticalLineTo(0.86299642f)
|
||||
curveTo(5.5779543f, 0.44749611f, 5.2436283f, 0.11317139f, 4.8281293f, 0.11317139f)
|
||||
close()
|
||||
moveTo(12.765629f, 0.11317139f)
|
||||
curveTo(12.35013f, 0.11317139f, 12.015287f, 0.44749611f, 12.015287f, 0.86299642f)
|
||||
verticalLineTo(4.8632689f)
|
||||
curveTo(12.015287f, 5.278769f, 12.35013f, 5.6130941f, 12.765629f, 5.6130941f)
|
||||
curveTo(13.181128f, 5.6130941f, 13.515454f, 5.278769f, 13.515454f, 4.8632689f)
|
||||
verticalLineTo(0.86299642f)
|
||||
curveTo(13.515454f, 0.44749611f, 13.181128f, 0.11317139f, 12.765629f, 0.11317139f)
|
||||
close()
|
||||
moveTo(20.703129f, 0.11317139f)
|
||||
curveTo(20.287629f, 0.11317139f, 19.952787f, 0.44749611f, 19.952787f, 0.86299642f)
|
||||
verticalLineTo(4.8632689f)
|
||||
curveTo(19.952787f, 5.278769f, 20.287629f, 5.6130941f, 20.703129f, 5.6130941f)
|
||||
curveTo(21.118628f, 5.6130941f, 21.452953f, 5.278769f, 21.452953f, 4.8632689f)
|
||||
verticalLineTo(0.86299642f)
|
||||
curveTo(21.452953f, 0.44749611f, 21.118628f, 0.11317139f, 20.703129f, 0.11317139f)
|
||||
close()
|
||||
moveTo(4.8281293f, 8.0506712f)
|
||||
curveTo(4.4126296f, 8.0506712f, 4.0777866f, 8.3849963f, 4.0777866f, 8.8004964f)
|
||||
verticalLineTo(12.800769f)
|
||||
curveTo(4.0777866f, 13.216269f, 4.4126296f, 13.550594f, 4.8281293f, 13.550594f)
|
||||
curveTo(5.2436283f, 13.550594f, 5.5779543f, 13.216269f, 5.5779543f, 12.800769f)
|
||||
verticalLineTo(8.8004964f)
|
||||
curveTo(5.5779543f, 8.3849963f, 5.2436283f, 8.0506712f, 4.8281293f, 8.0506712f)
|
||||
close()
|
||||
moveTo(12.765629f, 8.0506712f)
|
||||
curveTo(12.35013f, 8.0506712f, 12.015287f, 8.3849963f, 12.015287f, 8.8004964f)
|
||||
verticalLineTo(12.800769f)
|
||||
curveTo(12.015287f, 13.216269f, 12.35013f, 13.550594f, 12.765629f, 13.550594f)
|
||||
curveTo(13.181128f, 13.550594f, 13.515454f, 13.216269f, 13.515454f, 12.800769f)
|
||||
verticalLineTo(8.8004964f)
|
||||
curveTo(13.515454f, 8.3849963f, 13.181128f, 8.0506712f, 12.765629f, 8.0506712f)
|
||||
close()
|
||||
moveTo(20.703129f, 8.0506712f)
|
||||
curveTo(20.287629f, 8.0506712f, 19.952787f, 8.3849963f, 19.952787f, 8.8004964f)
|
||||
verticalLineTo(12.800769f)
|
||||
curveTo(19.952787f, 13.216269f, 20.287629f, 13.550594f, 20.703129f, 13.550594f)
|
||||
curveTo(21.118628f, 13.550594f, 21.452953f, 13.216269f, 21.452953f, 12.800769f)
|
||||
verticalLineTo(8.8004964f)
|
||||
curveTo(21.452953f, 8.3849963f, 21.118628f, 8.0506712f, 20.703129f, 8.0506712f)
|
||||
close()
|
||||
moveTo(4.8281293f, 15.988171f)
|
||||
curveTo(4.4126296f, 15.988171f, 4.0777866f, 16.322496f, 4.0777866f, 16.737996f)
|
||||
verticalLineTo(20.738269f)
|
||||
curveTo(4.0777866f, 21.153769f, 4.4126296f, 21.488094f, 4.8281293f, 21.488094f)
|
||||
curveTo(5.2436283f, 21.488094f, 5.5779543f, 21.153769f, 5.5779543f, 20.738269f)
|
||||
verticalLineTo(16.737996f)
|
||||
curveTo(5.5779543f, 16.322496f, 5.2436283f, 15.988171f, 4.8281293f, 15.988171f)
|
||||
close()
|
||||
moveTo(12.765629f, 15.988171f)
|
||||
curveTo(12.35013f, 15.988171f, 12.015287f, 16.322496f, 12.015287f, 16.737996f)
|
||||
verticalLineTo(20.738269f)
|
||||
curveTo(12.015287f, 21.153769f, 12.35013f, 21.488094f, 12.765629f, 21.488094f)
|
||||
curveTo(13.181128f, 21.488094f, 13.515454f, 21.153769f, 13.515454f, 20.738269f)
|
||||
verticalLineTo(16.737996f)
|
||||
curveTo(13.515454f, 16.322496f, 13.181128f, 15.988171f, 12.765629f, 15.988171f)
|
||||
close()
|
||||
moveTo(20.703129f, 15.988171f)
|
||||
curveTo(20.287629f, 15.988171f, 19.952787f, 16.322496f, 19.952787f, 16.737996f)
|
||||
verticalLineTo(20.738269f)
|
||||
curveTo(19.952787f, 21.153769f, 20.287629f, 21.488094f, 20.703129f, 21.488094f)
|
||||
curveTo(21.118628f, 21.488094f, 21.452953f, 21.153769f, 21.452953f, 20.738269f)
|
||||
verticalLineTo(16.737996f)
|
||||
curveTo(21.452953f, 16.322496f, 21.118628f, 15.988171f, 20.703129f, 15.988171f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherSleetSnowAnimatable
|
||||
get() = materialIcon("Icons.Rounded.WeatherSleetSnowAnimatable") {
|
||||
materialPath {
|
||||
moveTo(8.796879f, 5.0270833f)
|
||||
curveTo(7.8632606f, 4.9745641f, 7.2031834f, 6.1917513f, 7.7668228f, 6.9446931f)
|
||||
curveTo(8.2182954f, 7.6432508f, 9.3919824f, 7.6787827f, 9.8262364f, 6.9412491f)
|
||||
curveTo(10.250965f, 6.3103634f, 9.9588313f, 5.3184227f, 9.2064902f, 5.0975184f)
|
||||
curveTo(9.0753076f, 5.0509595f, 8.9361544f, 5.0263693f, 8.796879f, 5.0270833f)
|
||||
close()
|
||||
moveTo(16.734379f, 5.0270833f)
|
||||
curveTo(15.800761f, 4.974564f, 15.140683f, 6.1917512f, 15.704322f, 6.9446931f)
|
||||
curveTo(16.155795f, 7.643251f, 17.329482f, 7.6787825f, 17.763736f, 6.9412491f)
|
||||
curveTo(18.188465f, 6.3103636f, 17.896331f, 5.3184225f, 17.14399f, 5.0975184f)
|
||||
curveTo(17.012808f, 5.0509596f, 16.873654f, 5.0263694f, 16.734379f, 5.0270833f)
|
||||
close()
|
||||
moveTo(8.796879f, 12.964583f)
|
||||
curveTo(7.8632606f, 12.912064f, 7.2031834f, 14.129251f, 7.7668228f, 14.882193f)
|
||||
curveTo(8.2182957f, 15.580751f, 9.3919822f, 15.616282f, 9.8262364f, 14.878749f)
|
||||
curveTo(10.250965f, 14.247863f, 9.9588308f, 13.255923f, 9.2064902f, 13.035018f)
|
||||
curveTo(9.0753077f, 12.988459f, 8.9361544f, 12.963869f, 8.796879f, 12.964583f)
|
||||
close()
|
||||
moveTo(16.734379f, 12.964583f)
|
||||
curveTo(15.800761f, 12.912064f, 15.140683f, 14.129251f, 15.704322f, 14.882193f)
|
||||
curveTo(16.155795f, 15.580751f, 17.329482f, 15.616282f, 17.763736f, 14.878749f)
|
||||
curveTo(18.188465f, 14.247863f, 17.896331f, 13.255923f, 17.14399f, 13.035018f)
|
||||
curveTo(17.012808f, 12.988459f, 16.873654f, 12.963869f, 16.734379f, 12.964583f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WeatherFog
|
||||
get() = materialIcon("Icons.Rounded.WeatherFog") {
|
||||
materialPath {
|
||||
moveTo(9.7558594f, 9.9609375f)
|
||||
curveTo(9.2018594f, 9.9609375f, 8.7558594f, 10.406937f, 8.7558594f, 10.960938f)
|
||||
curveTo(8.7558594f, 11.514938f, 9.2018594f, 11.960938f, 9.7558594f, 11.960938f)
|
||||
lineTo(19.662109f, 11.960938f)
|
||||
curveTo(20.216109f, 11.960937f, 20.662109f, 11.514938f, 20.662109f, 10.960938f)
|
||||
curveTo(20.662109f, 10.406937f, 20.216109f, 9.9609375f, 19.662109f, 9.9609375f)
|
||||
lineTo(9.7558594f, 9.9609375f)
|
||||
close()
|
||||
moveTo(2.390625f, 12.818359f)
|
||||
curveTo(1.836625f, 12.818359f, 1.390625f, 13.264359f, 1.390625f, 13.818359f)
|
||||
curveTo(1.390625f, 14.372359f, 1.836625f, 14.818359f, 2.390625f, 14.818359f)
|
||||
lineTo(11.5625f, 14.818359f)
|
||||
curveTo(12.1165f, 14.818359f, 12.5625f, 14.372359f, 12.5625f, 13.818359f)
|
||||
curveTo(12.5625f, 13.264359f, 12.1165f, 12.818359f, 11.5625f, 12.818359f)
|
||||
lineTo(2.390625f, 12.818359f)
|
||||
close()
|
||||
moveTo(14.847656f, 12.818359f)
|
||||
curveTo(14.293656f, 12.818359f, 13.847656f, 13.264359f, 13.847656f, 13.818359f)
|
||||
curveTo(13.847656f, 14.372359f, 14.293656f, 14.818359f, 14.847656f, 14.818359f)
|
||||
lineTo(21.183594f, 14.818359f)
|
||||
curveTo(21.737594f, 14.818359f, 22.183594f, 14.372359f, 22.183594f, 13.818359f)
|
||||
curveTo(22.183594f, 13.264359f, 21.737594f, 12.818359f, 21.183594f, 12.818359f)
|
||||
lineTo(14.847656f, 12.818359f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.NotificationBadge
|
||||
get() = materialIcon("Icons.Rounded.Badge") {
|
||||
materialPath {
|
||||
moveTo(11.826172f, 2.0507812f)
|
||||
curveTo(9.3350327f, 2.0955476f, 6.8902426f, 3.0711501f, 5.03125f, 4.8867188f)
|
||||
curveTo(2.0568619f, 7.7916288f, 1.1976573f, 12.247433f, 2.8769531f, 16.050781f)
|
||||
curveTo(4.5562487f, 19.854128f, 8.4275333f, 22.217552f, 12.578125f, 21.976562f)
|
||||
arcTo(1f, 1f, 0f, false, false, 13.517578f, 20.921875f)
|
||||
arcTo(1f, 1f, 0f, false, false, 12.462891f, 19.980469f)
|
||||
curveTo(9.1386756f, 20.173477f, 6.0519814f, 18.28634f, 4.7070312f, 15.240234f)
|
||||
curveTo(3.3620806f, 12.194127f, 4.0474958f, 8.6429527f, 6.4296875f, 6.3164062f)
|
||||
curveTo(8.8118791f, 3.9898597f, 12.379106f, 3.3881285f, 15.392578f, 4.8046875f)
|
||||
curveTo(18.406051f, 6.2212464f, 20.218777f, 9.3531497f, 19.947266f, 12.671875f)
|
||||
arcTo(1f, 1f, 0f, false, false, 20.861328f, 13.748047f)
|
||||
arcTo(1f, 1f, 0f, false, false, 21.941406f, 12.833984f)
|
||||
curveTo(22.11091f, 10.762115f, 21.627235f, 8.7461001f, 20.630859f, 7.0253906f)
|
||||
curveTo(19.634483f, 5.3046802f, 18.125441f, 3.8804465f, 16.244141f, 2.9960938f)
|
||||
curveTo(14.833165f, 2.332829f, 13.320855f, 2.0239214f, 11.826172f, 2.0507812f)
|
||||
close()
|
||||
moveTo(18f, 14f)
|
||||
arcTo(4f, 4f, 0f, false, false, 14f, 18f)
|
||||
arcTo(4f, 4f, 0f, false, false, 18f, 22f)
|
||||
arcTo(4f, 4f, 0f, false, false, 22f, 18f)
|
||||
arcTo(4f, 4f, 0f, false, false, 18f, 14f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.GitHub
|
||||
get() = materialIcon("Icons.Rounded.GitHub") {
|
||||
materialPath {
|
||||
moveTo(12f, 2f)
|
||||
arcTo(10f, 10f, 0f, false, false, 2f, 12f)
|
||||
curveTo(2f, 16.42f, 4.87f, 20.17f, 8.84f, 21.5f)
|
||||
curveTo(9.34f, 21.58f, 9.5f, 21.27f, 9.5f, 21f)
|
||||
curveTo(9.5f, 20.77f, 9.5f, 20.14f, 9.5f, 19.31f)
|
||||
curveTo(6.73f, 19.91f, 6.14f, 17.97f, 6.14f, 17.97f)
|
||||
curveTo(5.68f, 16.81f, 5.03f, 16.5f, 5.03f, 16.5f)
|
||||
curveTo(4.12f, 15.88f, 5.1f, 15.9f, 5.1f, 15.9f)
|
||||
curveTo(6.1f, 15.97f, 6.63f, 16.93f, 6.63f, 16.93f)
|
||||
curveTo(7.5f, 18.45f, 8.97f, 18f, 9.54f, 17.76f)
|
||||
curveTo(9.63f, 17.11f, 9.89f, 16.67f, 10.17f, 16.42f)
|
||||
curveTo(7.95f, 16.17f, 5.62f, 15.31f, 5.62f, 11.5f)
|
||||
curveTo(5.62f, 10.39f, 6f, 9.5f, 6.65f, 8.79f)
|
||||
curveTo(6.55f, 8.54f, 6.2f, 7.5f, 6.75f, 6.15f)
|
||||
curveTo(6.75f, 6.15f, 7.59f, 5.88f, 9.5f, 7.17f)
|
||||
curveTo(10.29f, 6.95f, 11.15f, 6.84f, 12f, 6.84f)
|
||||
curveTo(12.85f, 6.84f, 13.71f, 6.95f, 14.5f, 7.17f)
|
||||
curveTo(16.41f, 5.88f, 17.25f, 6.15f, 17.25f, 6.15f)
|
||||
curveTo(17.8f, 7.5f, 17.45f, 8.54f, 17.35f, 8.79f)
|
||||
curveTo(18f, 9.5f, 18.38f, 10.39f, 18.38f, 11.5f)
|
||||
curveTo(18.38f, 15.32f, 16.04f, 16.16f, 13.81f, 16.41f)
|
||||
curveTo(14.17f, 16.72f, 14.5f, 17.33f, 14.5f, 18.26f)
|
||||
curveTo(14.5f, 19.6f, 14.5f, 20.68f, 14.5f, 21f)
|
||||
curveTo(14.5f, 21.27f, 14.66f, 21.59f, 15.17f, 21.5f)
|
||||
curveTo(19.14f, 20.16f, 22f, 16.42f, 22f, 12f)
|
||||
arcTo(10f, 10f, 0f, false, false, 12f, 2f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Telegram
|
||||
get() = materialIcon("Icons.Rounded.Telegram") {
|
||||
materialPath {
|
||||
moveTo(9.78f, 18.65f)
|
||||
lineTo(10.06f, 14.42f)
|
||||
lineTo(17.74f, 7.5f)
|
||||
curveTo(18.08f, 7.19f, 17.67f, 7.04f, 17.22f, 7.31f)
|
||||
lineTo(7.74f, 13.3f)
|
||||
lineTo(3.64f, 12f)
|
||||
curveTo(2.76f, 11.75f, 2.75f, 11.14f, 3.84f, 10.7f)
|
||||
lineTo(19.81f, 4.54f)
|
||||
curveTo(20.54f, 4.21f, 21.24f, 4.72f, 20.96f, 5.84f)
|
||||
lineTo(18.24f, 18.65f)
|
||||
curveTo(18.05f, 19.56f, 17.5f, 19.78f, 16.74f, 19.36f)
|
||||
lineTo(12.6f, 16.3f)
|
||||
lineTo(10.61f, 18.23f)
|
||||
curveTo(10.38f, 18.46f, 10.19f, 18.65f, 9.78f, 18.65f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Fdroid
|
||||
get() = materialIcon("Icons.Rounded.Fdroid") {
|
||||
materialPath {
|
||||
moveTo(4.4570312f, 3.6269531f)
|
||||
arcTo(0.44109249f, 0.44109249f, 0f, false, false, 4.1757812f, 3.71875f)
|
||||
arcTo(0.44109249f, 0.44109249f, 0f, false, false, 4.0957031f, 4.3378906f)
|
||||
lineTo(5.3691406f, 5.984375f)
|
||||
curveTo(5.3240789f, 6.102208f, 5.2949219f, 6.2272726f, 5.2949219f, 6.3613281f)
|
||||
lineTo(5.2949219f, 6.5507812f)
|
||||
lineTo(5.2949219f, 7.4199219f)
|
||||
lineTo(5.2949219f, 7.7714844f)
|
||||
lineTo(5.2949219f, 8.640625f)
|
||||
lineTo(5.2949219f, 8.8300781f)
|
||||
curveTo(5.2949219f, 9.4165531f, 5.7670378f, 9.8886719f, 6.3535156f, 9.8886719f)
|
||||
lineTo(17.646484f, 9.8886719f)
|
||||
curveTo(18.232962f, 9.8886719f, 18.705078f, 9.4165531f, 18.705078f, 8.8300781f)
|
||||
lineTo(18.705078f, 8.640625f)
|
||||
lineTo(18.705078f, 6.5507812f)
|
||||
lineTo(18.705078f, 6.3613281f)
|
||||
curveTo(18.705078f, 6.2272726f, 18.675921f, 6.102208f, 18.630859f, 5.984375f)
|
||||
lineTo(19.904297f, 4.3378906f)
|
||||
arcTo(0.44109249f, 0.44109249f, 0f, false, false, 19.824219f, 3.71875f)
|
||||
arcTo(0.44109249f, 0.44109249f, 0f, false, false, 19.566406f, 3.6269531f)
|
||||
arcTo(0.44109249f, 0.44109249f, 0f, false, false, 19.207031f, 3.796875f)
|
||||
lineTo(17.990234f, 5.3710938f)
|
||||
curveTo(17.881129f, 5.3332803f, 17.768779f, 5.3027344f, 17.646484f, 5.3027344f)
|
||||
lineTo(6.3535156f, 5.3027344f)
|
||||
curveTo(6.2312214f, 5.3027344f, 6.1188708f, 5.3332803f, 6.0097656f, 5.3710938f)
|
||||
lineTo(4.7929688f, 3.796875f)
|
||||
arcTo(0.44109249f, 0.44109249f, 0f, false, false, 4.4570312f, 3.6269531f)
|
||||
close()
|
||||
moveTo(8.5664062f, 6.625f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 8.6035156f, 6.625f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 9.7949219f, 7.8164062f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 8.6035156f, 9.0078125f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 7.4121094f, 7.8164062f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 8.5664062f, 6.625f)
|
||||
close()
|
||||
moveTo(15.447266f, 6.625f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 15.484375f, 6.625f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 16.675781f, 7.8164062f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 15.484375f, 9.0078125f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 14.292969f, 7.8164062f)
|
||||
arcTo(1.1909523f, 1.1909465f, 0f, false, true, 15.447266f, 6.625f)
|
||||
close()
|
||||
moveTo(6.3535156f, 10.242188f)
|
||||
curveTo(5.7670378f, 10.242188f, 5.2949219f, 10.714306f, 5.2949219f, 11.300781f)
|
||||
lineTo(5.2949219f, 11.572266f)
|
||||
lineTo(5.2949219f, 13.771484f)
|
||||
lineTo(5.2949219f, 15.888672f)
|
||||
lineTo(5.2949219f, 18.087891f)
|
||||
lineTo(5.2949219f, 18.359375f)
|
||||
curveTo(5.2949219f, 18.94585f, 5.7670378f, 19.417969f, 6.3535156f, 19.417969f)
|
||||
lineTo(17.646484f, 19.417969f)
|
||||
curveTo(18.232962f, 19.417969f, 18.705078f, 18.94585f, 18.705078f, 18.359375f)
|
||||
lineTo(18.705078f, 18.087891f)
|
||||
lineTo(18.705078f, 11.572266f)
|
||||
lineTo(18.705078f, 11.300781f)
|
||||
curveTo(18.705078f, 10.714306f, 18.232962f, 10.242187f, 17.646484f, 10.242188f)
|
||||
lineTo(6.3535156f, 10.242188f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.Wikipedia
|
||||
get() = materialIcon("Icons.Rounded.Wikipedia") {
|
||||
materialPath {
|
||||
moveTo(14.97f, 18.95f)
|
||||
lineTo(12.41f, 12.92f)
|
||||
curveTo(11.39f, 14.91f, 10.27f, 17f, 9.31f, 18.95f)
|
||||
curveTo(9.3f, 18.96f, 8.84f, 18.95f, 8.84f, 18.95f)
|
||||
curveTo(7.37f, 15.5f, 5.85f, 12.1f, 4.37f, 8.68f)
|
||||
curveTo(4.03f, 7.84f, 2.83f, 6.5f, 2f, 6.5f)
|
||||
curveTo(2f, 6.4f, 2f, 6.18f, 2f, 6.05f)
|
||||
horizontalLineTo(7.06f)
|
||||
verticalLineTo(6.5f)
|
||||
curveTo(6.46f, 6.5f, 5.44f, 6.9f, 5.7f, 7.55f)
|
||||
curveTo(6.42f, 9.09f, 8.94f, 15.06f, 9.63f, 16.58f)
|
||||
curveTo(10.1f, 15.64f, 11.43f, 13.16f, 12f, 12.11f)
|
||||
curveTo(11.55f, 11.23f, 10.13f, 7.93f, 9.71f, 7.11f)
|
||||
curveTo(9.39f, 6.57f, 8.58f, 6.5f, 7.96f, 6.5f)
|
||||
curveTo(7.96f, 6.35f, 7.97f, 6.25f, 7.96f, 6.06f)
|
||||
lineTo(12.42f, 6.07f)
|
||||
verticalLineTo(6.47f)
|
||||
curveTo(11.81f, 6.5f, 11.24f, 6.71f, 11.5f, 7.29f)
|
||||
curveTo(12.1f, 8.53f, 12.45f, 9.42f, 13f, 10.57f)
|
||||
curveTo(13.17f, 10.23f, 14.07f, 8.38f, 14.5f, 7.41f)
|
||||
curveTo(14.76f, 6.76f, 14.37f, 6.5f, 13.29f, 6.5f)
|
||||
curveTo(13.3f, 6.38f, 13.3f, 6.17f, 13.3f, 6.07f)
|
||||
curveTo(14.69f, 6.06f, 16.78f, 6.06f, 17.15f, 6.05f)
|
||||
verticalLineTo(6.47f)
|
||||
curveTo(16.44f, 6.5f, 15.71f, 6.88f, 15.33f, 7.46f)
|
||||
lineTo(13.5f, 11.3f)
|
||||
curveTo(13.68f, 11.81f, 15.46f, 15.76f, 15.65f, 16.2f)
|
||||
lineTo(19.5f, 7.37f)
|
||||
curveTo(19.2f, 6.65f, 18.34f, 6.5f, 18f, 6.5f)
|
||||
curveTo(18f, 6.37f, 18f, 6.2f, 18f, 6.05f)
|
||||
lineTo(22f, 6.08f)
|
||||
verticalLineTo(6.1f)
|
||||
lineTo(22f, 6.5f)
|
||||
curveTo(21.12f, 6.5f, 20.57f, 7f, 20.25f, 7.75f)
|
||||
curveTo(19.45f, 9.54f, 17f, 15.24f, 15.4f, 18.95f)
|
||||
curveTo(15.4f, 18.95f, 14.97f, 18.95f, 14.97f, 18.95f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WhatsApp
|
||||
get() = materialIcon("Icons.Rounded.WhatsApp") {
|
||||
materialPath {
|
||||
moveTo(12.04f, 2f)
|
||||
curveTo(6.58f, 2f, 2.13f, 6.45f, 2.13f, 11.91f)
|
||||
curveTo(2.13f, 13.66f, 2.59f, 15.36f, 3.45f, 16.86f)
|
||||
lineTo(2.05f, 22f)
|
||||
lineTo(7.3f, 20.62f)
|
||||
curveTo(8.75f, 21.41f, 10.38f, 21.83f, 12.04f, 21.83f)
|
||||
curveTo(17.5f, 21.83f, 21.95f, 17.38f, 21.95f, 11.92f)
|
||||
curveTo(21.95f, 9.27f, 20.92f, 6.78f, 19.05f, 4.91f)
|
||||
curveTo(17.18f, 3.03f, 14.69f, 2f, 12.04f, 2f)
|
||||
moveTo(12.05f, 3.67f)
|
||||
curveTo(14.25f, 3.67f, 16.31f, 4.53f, 17.87f, 6.09f)
|
||||
curveTo(19.42f, 7.65f, 20.28f, 9.72f, 20.28f, 11.92f)
|
||||
curveTo(20.28f, 16.46f, 16.58f, 20.15f, 12.04f, 20.15f)
|
||||
curveTo(10.56f, 20.15f, 9.11f, 19.76f, 7.85f, 19f)
|
||||
lineTo(7.55f, 18.83f)
|
||||
lineTo(4.43f, 19.65f)
|
||||
lineTo(5.26f, 16.61f)
|
||||
lineTo(5.06f, 16.29f)
|
||||
curveTo(4.24f, 15f, 3.8f, 13.47f, 3.8f, 11.91f)
|
||||
curveTo(3.81f, 7.37f, 7.5f, 3.67f, 12.05f, 3.67f)
|
||||
moveTo(8.53f, 7.33f)
|
||||
curveTo(8.37f, 7.33f, 8.1f, 7.39f, 7.87f, 7.64f)
|
||||
curveTo(7.65f, 7.89f, 7f, 8.5f, 7f, 9.71f)
|
||||
curveTo(7f, 10.93f, 7.89f, 12.1f, 8f, 12.27f)
|
||||
curveTo(8.14f, 12.44f, 9.76f, 14.94f, 12.25f, 16f)
|
||||
curveTo(12.84f, 16.27f, 13.3f, 16.42f, 13.66f, 16.53f)
|
||||
curveTo(14.25f, 16.72f, 14.79f, 16.69f, 15.22f, 16.63f)
|
||||
curveTo(15.7f, 16.56f, 16.68f, 16.03f, 16.89f, 15.45f)
|
||||
curveTo(17.1f, 14.87f, 17.1f, 14.38f, 17.04f, 14.27f)
|
||||
curveTo(16.97f, 14.17f, 16.81f, 14.11f, 16.56f, 14f)
|
||||
curveTo(16.31f, 13.86f, 15.09f, 13.26f, 14.87f, 13.18f)
|
||||
curveTo(14.64f, 13.1f, 14.5f, 13.06f, 14.31f, 13.3f)
|
||||
curveTo(14.15f, 13.55f, 13.67f, 14.11f, 13.53f, 14.27f)
|
||||
curveTo(13.38f, 14.44f, 13.24f, 14.46f, 13f, 14.34f)
|
||||
curveTo(12.74f, 14.21f, 11.94f, 13.95f, 11f, 13.11f)
|
||||
curveTo(10.26f, 12.45f, 9.77f, 11.64f, 9.62f, 11.39f)
|
||||
curveTo(9.5f, 11.15f, 9.61f, 11f, 9.73f, 10.89f)
|
||||
curveTo(9.84f, 10.78f, 10f, 10.6f, 10.1f, 10.45f)
|
||||
curveTo(10.23f, 10.31f, 10.27f, 10.2f, 10.35f, 10.04f)
|
||||
curveTo(10.43f, 9.87f, 10.39f, 9.73f, 10.33f, 9.61f)
|
||||
curveTo(10.27f, 9.5f, 9.77f, 8.26f, 9.56f, 7.77f)
|
||||
curveTo(9.36f, 7.29f, 9.16f, 7.35f, 9f, 7.34f)
|
||||
curveTo(8.86f, 7.34f, 8.7f, 7.33f, 8.53f, 7.33f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.mm20.launcher2.ui.ktx
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Color.toHexString(): String {
|
||||
val red = (this.red * 255).roundToInt()
|
||||
val green = (this.green * 255).roundToInt()
|
||||
val blue = (this.blue * 255).roundToInt()
|
||||
return "#" +
|
||||
red.toString(16).run { if (length == 1) "0$this" else this } +
|
||||
green.toString(16).run { if (length == 1) "0$this" else this } +
|
||||
blue.toString(16).run { if (length == 1) "0$this" else this }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.mm20.launcher2.ui.ktx
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
@Composable
|
||||
fun Dp.toPixels(): Float {
|
||||
return value * LocalDensity.current.density
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.mm20.launcher2.ui.ktx
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Converts the given pixel size to a Dp value based on the current density
|
||||
*/
|
||||
@Composable
|
||||
fun Float.toDp(): Dp {
|
||||
return (this / LocalDensity.current.density).dp
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.mm20.launcher2.ui.ktx
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Converts the given pixel size to a Dp value based on the current density
|
||||
*/
|
||||
@Composable
|
||||
fun Int.toDp(): Dp {
|
||||
return (this / LocalDensity.current.density).dp
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.mm20.launcher2.ui.ktx
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
|
||||
fun Modifier.conditional(condition: Boolean, other: Modifier): Modifier {
|
||||
if (condition) {
|
||||
return this then other
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.mm20.launcher2.ui.ktx
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector
|
||||
import androidx.compose.animation.core.TwoWayConverter
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
suspend fun MutableState<Dp>.animateTo(targetValue: Dp) {
|
||||
animateTo(targetValue, Dp.VectorConverter)
|
||||
}
|
||||
|
||||
suspend fun MutableState<Float>.animateTo(targetValue: Float) {
|
||||
animateTo(targetValue, Float.VectorConverter)
|
||||
}
|
||||
|
||||
suspend inline fun <T, V: AnimationVector> MutableState<T>.animateTo(targetValue: T, converter: TwoWayConverter<T, V>) {
|
||||
val animatable = Animatable(this.value, converter)
|
||||
animatable.animateTo(targetValue) {
|
||||
this@animateTo.value = this.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.mm20.launcher2.ui.ktx
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Offset.toIntOffset(): IntOffset {
|
||||
return IntOffset(x.roundToInt(), y.roundToInt())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import android.content.Intent
|
||||
import com.android.launcher3.GestureNavContract
|
||||
|
||||
|
||||
class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
val navContract = intent?.let { GestureNavContract.fromIntent(it) }
|
||||
if (navContract != null) {
|
||||
homeTransitionManager.resolve(navContract, window)
|
||||
} else {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
homeTransitionManager.clear()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (onBackPressedDispatcher.hasEnabledCallbacks()) {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class LauncherActivityVM : ViewModel(), KoinComponent {
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
private var isSystemInDarkMode = MutableStateFlow(false)
|
||||
|
||||
private val dimBackgroundState = combine(
|
||||
dataStore.data.map { it.appearance.dimWallpaper },
|
||||
dataStore.data.map { it.appearance.theme },
|
||||
isSystemInDarkMode
|
||||
) { dim, theme, systemDarkMode ->
|
||||
dim && (theme == Theme.Dark || theme == Theme.System && systemDarkMode)
|
||||
}
|
||||
val dimBackground = dimBackgroundState.asLiveData()
|
||||
|
||||
val statusBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData()
|
||||
val navBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData()
|
||||
|
||||
val hideNavBar = dataStore.data.map { it.systemBars.hideNavBar }.asLiveData()
|
||||
val hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }.asLiveData()
|
||||
|
||||
fun setSystemInDarkMode(darkMode: Boolean) {
|
||||
isSystemInDarkMode.value = darkMode
|
||||
}
|
||||
|
||||
val layout = dataStore.data.map { it.appearance.layout }.asLiveData()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
||||
val isSearchOpen = MutableLiveData(false)
|
||||
val isWidgetEditMode = MutableLiveData(false)
|
||||
|
||||
val searchBarFocused = MutableLiveData(false)
|
||||
|
||||
val dataStore: LauncherDataStore by inject()
|
||||
|
||||
val autoFocusSearch = dataStore.data.map { it.searchBar.autoFocus }
|
||||
|
||||
fun setSearchbarFocus(focused: Boolean) {
|
||||
if (searchBarFocused.value != focused) searchBarFocused.value = focused
|
||||
}
|
||||
|
||||
fun openSearch() {
|
||||
if (isSearchOpen.value == true) return
|
||||
isSearchOpen.value = true
|
||||
viewModelScope.launch {
|
||||
if (autoFocusSearch.first()) setSearchbarFocus(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun closeSearch() {
|
||||
if (isSearchOpen.value == false) return
|
||||
isSearchOpen.value = false
|
||||
setSearchbarFocus(false)
|
||||
}
|
||||
|
||||
fun toggleSearch() {
|
||||
if (isSearchOpen.value == true) closeSearch()
|
||||
else openSearch()
|
||||
}
|
||||
|
||||
fun setWidgetEditMode(editMode: Boolean) {
|
||||
isSearchOpen.value = false
|
||||
isWidgetEditMode.value = editMode
|
||||
}
|
||||
|
||||
val wallpaperBlur = dataStore.data.map { it.appearance.blurWallpaper }.asLiveData()
|
||||
|
||||
val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }.asLiveData()
|
||||
val searchBarColor = dataStore.data.map { it.searchBar.color }.asLiveData()
|
||||
val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }.asLiveData()
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.FractionalThreshold
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material.rememberSwipeableState
|
||||
import androidx.compose.material.swipeable
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import de.mm20.launcher2.ui.utils.rememberNotificationShadeController
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun PagerScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
darkStatusBarIcons: Boolean = false,
|
||||
darkNavBarIcons: Boolean = false,
|
||||
reverse: Boolean = false,
|
||||
) {
|
||||
val viewModel: LauncherScaffoldVM = viewModel()
|
||||
val searchVM: SearchVM = viewModel()
|
||||
|
||||
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
||||
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
||||
|
||||
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||
|
||||
val widgetsScrollState = rememberScrollState()
|
||||
val searchState = rememberLazyListState()
|
||||
val swipeableState = rememberSwipeableState(if (isSearchOpen) Page.Search else Page.Widgets)
|
||||
|
||||
val isSearchAtStart by remember {
|
||||
derivedStateOf {
|
||||
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
|
||||
}
|
||||
}
|
||||
|
||||
val isSearchAtEnd by remember {
|
||||
derivedStateOf {
|
||||
val lastItem =
|
||||
searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
|
||||
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
|
||||
}
|
||||
}
|
||||
|
||||
val showStatusBarScrim by remember {
|
||||
derivedStateOf {
|
||||
if (isSearchOpen) {
|
||||
!isSearchAtEnd
|
||||
} else {
|
||||
widgetsScrollState.value > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val fillClockHeight by viewModel.fillClockHeight.observeAsState(true)
|
||||
|
||||
val showNavBarScrim by remember {
|
||||
derivedStateOf {
|
||||
if (isSearchOpen) {
|
||||
!isSearchAtStart
|
||||
} else {
|
||||
(widgetsScrollState.value > 0 || !fillClockHeight) && widgetsScrollState.value < widgetsScrollState.maxValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isWidgetsScrollZero by remember {
|
||||
derivedStateOf {
|
||||
widgetsScrollState.value == 0
|
||||
}
|
||||
}
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
|
||||
val colorSurface = MaterialTheme.colorScheme.surface
|
||||
LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim) {
|
||||
if (isWidgetEditMode) {
|
||||
systemUiController.setStatusBarColor(
|
||||
colorSurface
|
||||
)
|
||||
} else if (showStatusBarScrim) {
|
||||
systemUiController.setStatusBarColor(
|
||||
colorSurface.copy(0.7f),
|
||||
)
|
||||
} else {
|
||||
systemUiController.setStatusBarColor(
|
||||
Color.Transparent,
|
||||
darkIcons = darkStatusBarIcons
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(darkNavBarIcons, showNavBarScrim) {
|
||||
if (showNavBarScrim) {
|
||||
systemUiController.setNavigationBarColor(
|
||||
colorSurface.copy(0.7f),
|
||||
)
|
||||
} else {
|
||||
systemUiController.setNavigationBarColor(
|
||||
Color.Transparent,
|
||||
darkIcons = darkNavBarIcons,
|
||||
navigationBarContrastEnforced = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val blurEnabled by viewModel.wallpaperBlur.observeAsState(false)
|
||||
|
||||
val blurWallpaper by remember {
|
||||
derivedStateOf {
|
||||
blurEnabled && (
|
||||
isSearchOpen || swipeableState.progress.to == Page.Widgets && swipeableState.progress.fraction <= 0.5f ||
|
||||
swipeableState.progress.to == Page.Search && swipeableState.progress.fraction > 0.5f ||
|
||||
!isWidgetsScrollZero)
|
||||
}
|
||||
}
|
||||
|
||||
WallpaperBlur {
|
||||
blurWallpaper
|
||||
}
|
||||
|
||||
val currentPage = swipeableState.currentValue
|
||||
LaunchedEffect(currentPage) {
|
||||
if (currentPage == Page.Search) viewModel.openSearch()
|
||||
else viewModel.closeSearch()
|
||||
}
|
||||
|
||||
LaunchedEffect(isSearchOpen) {
|
||||
if (isSearchOpen) swipeableState.animateTo(Page.Search)
|
||||
else {
|
||||
swipeableState.animateTo(Page.Widgets)
|
||||
searchVM.search("")
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
BackHandler {
|
||||
when {
|
||||
isSearchOpen -> {
|
||||
viewModel.closeSearch()
|
||||
searchVM.search("")
|
||||
}
|
||||
|
||||
isWidgetEditMode -> {
|
||||
viewModel.setWidgetEditMode(false)
|
||||
}
|
||||
|
||||
widgetsScrollState.value != 0 -> {
|
||||
scope.launch {
|
||||
widgetsScrollState.animateScrollTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val notificationDragThreshold = with(LocalDensity.current) { 200.dp.toPx() }
|
||||
val notificationShadeController = rememberNotificationShadeController()
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
private var pullDownTotalY: Float? = 0f
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
if (!isWidgetsScrollZero) return Offset.Zero
|
||||
val diff = -available.y
|
||||
var totalY = pullDownTotalY ?: return available
|
||||
if (diff >= 0) return super.onPreScroll(available, source)
|
||||
|
||||
totalY += diff
|
||||
|
||||
if (totalY < -notificationDragThreshold) {
|
||||
notificationShadeController.expandNotifications()
|
||||
pullDownTotalY = null
|
||||
return available
|
||||
}
|
||||
pullDownTotalY = totalY
|
||||
|
||||
return super.onPreScroll(available, source)
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
if (pullDownTotalY == null) {
|
||||
pullDownTotalY = 0f
|
||||
return available
|
||||
}
|
||||
return super.onPreFling(available)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val searchNestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
if (source == NestedScrollSource.Drag && available.y.absoluteValue > available.x.absoluteValue * 2) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
return super.onPreScroll(available, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val insets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
) {
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val height by remember {
|
||||
derivedStateOf { maxHeight }
|
||||
}
|
||||
val width by remember {
|
||||
derivedStateOf { maxWidth }
|
||||
}
|
||||
|
||||
|
||||
val widthPx = width.toPixels()
|
||||
|
||||
val originalLayoutDirection = LocalLayoutDirection.current
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalOverscrollConfiguration provides null,
|
||||
LocalLayoutDirection provides if (reverse) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.requiredWidth(width * 2)
|
||||
.fillMaxHeight()
|
||||
.swipeable(
|
||||
swipeableState,
|
||||
orientation = Orientation.Horizontal,
|
||||
anchors = mapOf(
|
||||
-widthPx / 2f to Page.Search,
|
||||
widthPx / 2f to Page.Widgets,
|
||||
),
|
||||
thresholds = { _, _ ->
|
||||
FractionalThreshold(0.5f)
|
||||
},
|
||||
enabled = !isWidgetEditMode,
|
||||
reverseDirection = reverse,
|
||||
)
|
||||
.offset {
|
||||
IntOffset(swipeableState.offset.value.roundToInt(), 0)
|
||||
},
|
||||
) {
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides originalLayoutDirection
|
||||
) {
|
||||
|
||||
|
||||
val editModePadding by animateDpAsState(if (isWidgetEditMode) 56.dp else 0.dp)
|
||||
|
||||
val clockPadding by animateDpAsState(
|
||||
if (isWidgetsScrollZero && fillClockHeight) 64.dp + insets.calculateBottomPadding() else 0.dp
|
||||
)
|
||||
|
||||
val clockHeight by remember {
|
||||
derivedStateOf {
|
||||
if (fillClockHeight) {
|
||||
height - (64.dp + insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.requiredWidth(width)
|
||||
.fillMaxHeight()
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.verticalScroll(widgetsScrollState)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(top = 8.dp, bottom = 64.dp)
|
||||
.padding(top = editModePadding)
|
||||
) {
|
||||
|
||||
AnimatedVisibility(!isWidgetEditMode) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(clockHeight?.let { Modifier.height(it) } ?: Modifier)
|
||||
.padding(bottom = clockPadding),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
ClockWidget(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WidgetColumn(
|
||||
editMode = isWidgetEditMode,
|
||||
onEditModeChange = {
|
||||
viewModel.setWidgetEditMode(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val webSearchPadding by animateDpAsState(
|
||||
if (actions.isEmpty()) 0.dp else 48.dp
|
||||
)
|
||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
SearchColumn(
|
||||
modifier = Modifier
|
||||
.requiredWidth(width)
|
||||
.fillMaxHeight()
|
||||
.nestedScroll(searchNestedScrollConnection)
|
||||
.padding(
|
||||
start = windowInsets.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = windowInsets.calculateStartPadding(LocalLayoutDirection.current),
|
||||
),
|
||||
reverse = true,
|
||||
state = searchState,
|
||||
paddingValues = PaddingValues(
|
||||
top = 4.dp + windowInsets.calculateTopPadding(),
|
||||
bottom = 60.dp + webSearchPadding + windowInsets.calculateBottomPadding()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = isWidgetEditMode,
|
||||
enter = slideIn { IntOffset(0, -it.height) },
|
||||
exit = slideOut { IntOffset(0, -it.height) }
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
title = {
|
||||
Text(stringResource(R.string.menu_edit_widgets))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { viewModel.setWidgetEditMode(false) }) {
|
||||
Icon(imageVector = Icons.Rounded.Done, contentDescription = null)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val searchBarLevel by remember {
|
||||
derivedStateOf {
|
||||
when {
|
||||
swipeableState.direction != 0f -> SearchBarLevel.Raised
|
||||
!isSearchOpen && isWidgetsScrollZero && fillClockHeight -> SearchBarLevel.Resting
|
||||
isSearchOpen && isSearchAtStart -> SearchBarLevel.Active
|
||||
else -> SearchBarLevel.Raised
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val focusSearchBar by viewModel.searchBarFocused.observeAsState(false)
|
||||
|
||||
val widgetEditModeOffset by animateDpAsState(
|
||||
if (isWidgetEditMode) 128.dp else 0.dp
|
||||
)
|
||||
|
||||
val value by searchVM.searchQuery.observeAsState("")
|
||||
|
||||
val searchBarColor by viewModel.searchBarColor.observeAsState(SearchBarColors.Auto)
|
||||
val searchBarStyle by viewModel.searchBarStyle.observeAsState(SearchBarStyle.Transparent)
|
||||
|
||||
LauncherSearchBar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.imePadding()
|
||||
.offset(y = widgetEditModeOffset),
|
||||
level = { searchBarLevel },
|
||||
focused = focusSearchBar,
|
||||
onFocusChange = {
|
||||
if (it) viewModel.openSearch()
|
||||
viewModel.setSearchbarFocus(it)
|
||||
},
|
||||
actions = actions,
|
||||
showHiddenItemsButton = isSearchOpen,
|
||||
value = { value },
|
||||
onValueChange = { searchVM.search(it) },
|
||||
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
|
||||
style = searchBarStyle,
|
||||
reverse = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Page {
|
||||
Widgets,
|
||||
Search
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.ktx.animateTo
|
||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun PullDownScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
darkStatusBarIcons: Boolean = false,
|
||||
darkNavBarIcons: Boolean = false,
|
||||
) {
|
||||
val viewModel: LauncherScaffoldVM = viewModel()
|
||||
val searchVM: SearchVM = viewModel()
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||
|
||||
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
||||
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
||||
|
||||
val widgetsScrollState = rememberScrollState()
|
||||
val searchState = rememberLazyListState()
|
||||
|
||||
val isSearchAtStart by remember {
|
||||
derivedStateOf {
|
||||
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
|
||||
}
|
||||
}
|
||||
|
||||
val isSearchAtEnd by remember {
|
||||
derivedStateOf {
|
||||
val lastItem = searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
|
||||
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
|
||||
}
|
||||
}
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
|
||||
val isWidgetsAtStart by remember {
|
||||
derivedStateOf {
|
||||
widgetsScrollState.value == 0
|
||||
}
|
||||
}
|
||||
|
||||
val isWidgetsAtEnd by remember {
|
||||
derivedStateOf {
|
||||
widgetsScrollState.value >= widgetsScrollState.maxValue
|
||||
}
|
||||
}
|
||||
|
||||
val fillClockHeight by viewModel.fillClockHeight.observeAsState(true)
|
||||
|
||||
val showStatusBarScrim by remember {
|
||||
derivedStateOf {
|
||||
if (isSearchOpen) {
|
||||
!isSearchAtStart
|
||||
} else {
|
||||
widgetsScrollState.value > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
val showNavBarScrim by remember {
|
||||
derivedStateOf {
|
||||
if (isSearchOpen) {
|
||||
!isSearchAtEnd
|
||||
} else {
|
||||
(widgetsScrollState.value > 0 || !fillClockHeight) && widgetsScrollState.value < widgetsScrollState.maxValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val colorSurface = MaterialTheme.colorScheme.surface
|
||||
LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim) {
|
||||
if (isWidgetEditMode) {
|
||||
systemUiController.setStatusBarColor(
|
||||
colorSurface
|
||||
)
|
||||
} else if (showStatusBarScrim) {
|
||||
systemUiController.setStatusBarColor(
|
||||
colorSurface.copy(0.7f),
|
||||
)
|
||||
} else {
|
||||
systemUiController.setStatusBarColor(
|
||||
Color.Transparent,
|
||||
darkIcons = darkStatusBarIcons
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(darkNavBarIcons, showNavBarScrim) {
|
||||
if (showNavBarScrim) {
|
||||
systemUiController.setNavigationBarColor(
|
||||
colorSurface.copy(0.7f),
|
||||
)
|
||||
} else {
|
||||
systemUiController.setNavigationBarColor(
|
||||
Color.Transparent,
|
||||
darkIcons = darkNavBarIcons,
|
||||
navigationBarContrastEnforced = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val offsetY = remember { mutableStateOf(0f) }
|
||||
|
||||
val maxOffset = with(density) { 64.dp.toPx() }
|
||||
val toggleSearchThreshold = with(density) { 48.dp.toPx() }
|
||||
|
||||
val searchBarOffset = remember { mutableStateOf(0f) }
|
||||
|
||||
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
|
||||
|
||||
val blurEnabled by viewModel.wallpaperBlur.observeAsState(false)
|
||||
|
||||
val blurWallpaper by remember {
|
||||
derivedStateOf {
|
||||
blurEnabled && (isSearchOpen || offsetY.value > toggleSearchThreshold || widgetsScrollState.value > 0)
|
||||
}
|
||||
}
|
||||
|
||||
WallpaperBlur {
|
||||
blurWallpaper
|
||||
}
|
||||
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(isSearchOpen) {
|
||||
if (isSearchOpen) searchState.scrollToItem(0)
|
||||
if (!isSearchOpen) searchVM.search("")
|
||||
searchBarOffset.animateTo(0f)
|
||||
}
|
||||
|
||||
LaunchedEffect(isWidgetEditMode) {
|
||||
if (!isWidgetEditMode) searchBarOffset.value = 0f
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
when {
|
||||
isSearchOpen -> {
|
||||
viewModel.closeSearch()
|
||||
searchVM.search("")
|
||||
}
|
||||
isWidgetEditMode -> {
|
||||
viewModel.setWidgetEditMode(false)
|
||||
}
|
||||
widgetsScrollState.value != 0 -> {
|
||||
scope.launch {
|
||||
widgetsScrollState.animateScrollTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
if (isWidgetEditMode) return Offset.Zero
|
||||
if (source == NestedScrollSource.Drag && available.y.absoluteValue > available.x.absoluteValue * 2) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
val canPullDown = if (isSearchOpen) {
|
||||
isSearchAtStart
|
||||
} else {
|
||||
isWidgetsAtStart
|
||||
}
|
||||
val canPullUp = isSearchOpen && isSearchAtEnd
|
||||
|
||||
val consumed = when {
|
||||
canPullUp && available.y < 0 || offsetY.value < 0 -> {
|
||||
val consumed = available.y
|
||||
offsetY.value = (offsetY.value + (consumed * 0.5f)).coerceIn(-maxOffset, 0f)
|
||||
consumed
|
||||
}
|
||||
canPullDown && available.y > 0 || offsetY.value > 0 -> {
|
||||
val consumed = available.y
|
||||
offsetY.value = (offsetY.value + (consumed * 0.5f)).coerceIn(0f, maxOffset)
|
||||
consumed
|
||||
}
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
searchBarOffset.value =
|
||||
(searchBarOffset.value + (available.y - consumed)).coerceIn(
|
||||
-maxSearchBarOffset,
|
||||
0f
|
||||
)
|
||||
|
||||
return Offset(0f, consumed)
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
if (offsetY.value > toggleSearchThreshold || offsetY.value < -toggleSearchThreshold) {
|
||||
viewModel.toggleSearch()
|
||||
}
|
||||
if (offsetY.value != 0f) {
|
||||
offsetY.animateTo(0f)
|
||||
return available
|
||||
}
|
||||
return Velocity.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val insets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
Box(
|
||||
modifier = modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.offset { IntOffset(0, offsetY.value.toInt()) },
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val height by remember {
|
||||
derivedStateOf { maxHeight }
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalOverscrollConfiguration provides null
|
||||
) {
|
||||
val offset by animateFloatAsState(if (isSearchOpen) 1f else 0f)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(height * 2)
|
||||
.offset {
|
||||
IntOffset(
|
||||
0,
|
||||
((-0.5f + offset) * height.toPx()).roundToInt()
|
||||
)
|
||||
}
|
||||
) {
|
||||
val webSearchPadding by animateDpAsState(
|
||||
if (actions.isEmpty()) 0.dp else 48.dp
|
||||
)
|
||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
SearchColumn(
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
transformOrigin = TransformOrigin.Center
|
||||
scaleX = offset
|
||||
scaleY = offset
|
||||
alpha = offset
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(height)
|
||||
.padding(
|
||||
start = windowInsets.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = windowInsets.calculateStartPadding(LocalLayoutDirection.current),
|
||||
),
|
||||
paddingValues = PaddingValues(
|
||||
top = 60.dp + webSearchPadding + windowInsets.calculateTopPadding(),
|
||||
bottom = 4.dp + windowInsets.calculateBottomPadding()
|
||||
),
|
||||
state = searchState,
|
||||
|
||||
)
|
||||
val clockPadding by animateDpAsState(
|
||||
if (isWidgetsAtStart && fillClockHeight) insets.calculateBottomPadding() else 0.dp
|
||||
)
|
||||
val clockHeight by remember {
|
||||
derivedStateOf {
|
||||
if (fillClockHeight) {
|
||||
height - (insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding + 56.dp)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
transformOrigin = TransformOrigin.Center
|
||||
scaleX = 1 - offset
|
||||
scaleY = 1 - offset
|
||||
alpha = 1 - offset
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(height)
|
||||
.verticalScroll(widgetsScrollState)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(8.dp)
|
||||
.padding(top = 56.dp)
|
||||
) {
|
||||
AnimatedVisibility(!isWidgetEditMode) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(clockHeight?.let { Modifier.height(it) } ?: Modifier)
|
||||
.padding(bottom = clockPadding),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
ClockWidget(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
WidgetColumn(
|
||||
editMode = isWidgetEditMode,
|
||||
onEditModeChange = {
|
||||
viewModel.setWidgetEditMode(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
AnimatedVisibility(visible = isWidgetEditMode,
|
||||
enter = slideIn { IntOffset(0, -it.height) },
|
||||
exit = slideOut { IntOffset(0, -it.height) }
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
||||
title = {
|
||||
Text(stringResource(R.string.menu_edit_widgets))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { viewModel.setWidgetEditMode(false) }) {
|
||||
Icon(imageVector = Icons.Rounded.Done, contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val searchBarLevel by remember {
|
||||
derivedStateOf {
|
||||
when {
|
||||
offsetY.value != 0f -> SearchBarLevel.Raised
|
||||
isSearchOpen && isSearchAtStart -> SearchBarLevel.Active
|
||||
isSearchOpen && !isSearchAtStart -> SearchBarLevel.Raised
|
||||
!isWidgetsAtStart -> SearchBarLevel.Raised
|
||||
else -> SearchBarLevel.Resting
|
||||
}
|
||||
}
|
||||
}
|
||||
val searchBarFocused by viewModel.searchBarFocused.observeAsState(false)
|
||||
val editModeSearchBarOffset by animateDpAsState(
|
||||
if (isWidgetEditMode) -128.dp else 0.dp
|
||||
)
|
||||
|
||||
val value by searchVM.searchQuery.observeAsState("")
|
||||
|
||||
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
|
||||
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
|
||||
|
||||
LauncherSearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(8.dp)
|
||||
.offset { IntOffset(0,
|
||||
if (searchBarFocused) 0 else searchBarOffset.value.toInt()) }
|
||||
.offset {
|
||||
IntOffset(
|
||||
0,
|
||||
with(density) {
|
||||
editModeSearchBarOffset
|
||||
.toPx()
|
||||
.roundToInt()
|
||||
})
|
||||
},
|
||||
level = { searchBarLevel },
|
||||
focused = searchBarFocused,
|
||||
onFocusChange = {
|
||||
if (it) viewModel.openSearch()
|
||||
viewModel.setSearchbarFocus(it)
|
||||
},
|
||||
actions = actions,
|
||||
showHiddenItemsButton = isSearchOpen,
|
||||
value = { value },
|
||||
onValueChange = { searchVM.search(it) },
|
||||
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
|
||||
style = searchBarStyle,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import android.app.WallpaperManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
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.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.preferences.Settings.SystemBarsSettings.SystemBarColors
|
||||
import de.mm20.launcher2.ui.assistant.AssistantScaffold
|
||||
import de.mm20.launcher2.ui.base.BaseActivity
|
||||
import de.mm20.launcher2.ui.base.ProvideCurrentTime
|
||||
import de.mm20.launcher2.ui.base.ProvideSettings
|
||||
import de.mm20.launcher2.ui.component.NavBarEffects
|
||||
import de.mm20.launcher2.ui.ktx.animateTo
|
||||
import de.mm20.launcher2.ui.launcher.sheets.CustomizeSearchableSheet
|
||||
import de.mm20.launcher2.ui.launcher.sheets.EditFavoritesSheet
|
||||
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LauncherBottomSheetManager
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.launcher.transitions.HomeTransition
|
||||
import de.mm20.launcher2.ui.launcher.transitions.HomeTransitionManager
|
||||
import de.mm20.launcher2.ui.launcher.transitions.LocalHomeTransitionManager
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
||||
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
||||
import kotlin.math.pow
|
||||
|
||||
|
||||
abstract class SharedLauncherActivity(
|
||||
private val mode: LauncherActivityMode
|
||||
) : BaseActivity() {
|
||||
|
||||
private val viewModel: LauncherActivityVM by viewModels()
|
||||
|
||||
internal val homeTransitionManager = HomeTransitionManager()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val wallpaperManager = WallpaperManager.getInstance(this)
|
||||
|
||||
val windowSize = Resources.getSystem().displayMetrics.let {
|
||||
Size(it.widthPixels.toFloat(), it.heightPixels.toFloat())
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
viewModel.setSystemInDarkMode(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
|
||||
|
||||
val bottomSheetManager = LauncherBottomSheetManager()
|
||||
|
||||
setContent {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val wallpaperColors by wallpaperColorsAsState()
|
||||
val dimBackground by viewModel.dimBackground.observeAsState(false)
|
||||
CompositionLocalProvider(
|
||||
LocalHomeTransitionManager provides homeTransitionManager,
|
||||
LocalWindowSize provides windowSize,
|
||||
LocalSnackbarHostState provides snackbarHostState,
|
||||
LocalWallpaperColors provides wallpaperColors,
|
||||
LocalPreferDarkContentOverWallpaper provides (!dimBackground && wallpaperColors.supportsDarkText),
|
||||
LocalBottomSheetManager provides bottomSheetManager,
|
||||
) {
|
||||
LauncherTheme {
|
||||
ProvideCurrentTime {
|
||||
ProvideSettings {
|
||||
val statusBarColor by viewModel.statusBarColor.observeAsState(SystemBarColors.Auto)
|
||||
val navBarColor by viewModel.navBarColor.observeAsState(SystemBarColors.Auto)
|
||||
|
||||
val lightStatus = !dimBackground && (statusBarColor == SystemBarColors.Dark || statusBarColor == SystemBarColors.Auto && wallpaperColors.supportsDarkText)
|
||||
val lightNav = !dimBackground && (navBarColor == SystemBarColors.Dark || navBarColor == SystemBarColors.Auto && wallpaperColors.supportsDarkText)
|
||||
|
||||
val hideStatus by viewModel.hideStatusBar.observeAsState(false)
|
||||
val hideNav by viewModel.hideNavBar.observeAsState(false)
|
||||
val layout by viewModel.layout.observeAsState(null)
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
|
||||
val enterTransitionProgress = remember { mutableStateOf(1f) }
|
||||
var enterTransition by remember {
|
||||
mutableStateOf<HomeTransition?>(
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(null) {
|
||||
homeTransitionManager
|
||||
.currentTransition
|
||||
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
|
||||
.collect {
|
||||
enterTransitionProgress.value = 0f
|
||||
enterTransition = it
|
||||
enterTransitionProgress.animateTo(1f)
|
||||
enterTransition = null
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(hideStatus) {
|
||||
systemUiController.isStatusBarVisible = !hideStatus
|
||||
}
|
||||
LaunchedEffect(hideNav) {
|
||||
systemUiController.isNavigationBarVisible = !hideNav
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(null) {
|
||||
detectTapGestures {
|
||||
wallpaperManager?.sendWallpaperCommand(
|
||||
window.decorView.applicationWindowToken,
|
||||
WallpaperManager.COMMAND_TAP,
|
||||
it.x.toInt(),
|
||||
it.y.toInt(),
|
||||
0,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
.background(if (dimBackground) Color.Black.copy(alpha = 0.30f) else Color.Transparent),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
NavBarEffects(modifier = Modifier.fillMaxSize())
|
||||
if (mode == LauncherActivityMode.Assistant) {
|
||||
AssistantScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
darkStatusBarIcons = lightStatus,
|
||||
darkNavBarIcons = lightNav,
|
||||
)
|
||||
} else {
|
||||
when (layout) {
|
||||
Settings.AppearanceSettings.Layout.PullDown -> {
|
||||
PullDownScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX =
|
||||
0.5f + enterTransitionProgress.value * 0.5f
|
||||
scaleY =
|
||||
0.5f + enterTransitionProgress.value * 0.5f
|
||||
alpha = enterTransitionProgress.value
|
||||
},
|
||||
darkStatusBarIcons = lightStatus,
|
||||
darkNavBarIcons = lightNav,
|
||||
)
|
||||
}
|
||||
|
||||
Settings.AppearanceSettings.Layout.Pager,
|
||||
Settings.AppearanceSettings.Layout.PagerReversed -> {
|
||||
PagerScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX = enterTransitionProgress.value
|
||||
scaleY = enterTransitionProgress.value
|
||||
alpha = enterTransitionProgress.value
|
||||
},
|
||||
darkStatusBarIcons = lightStatus,
|
||||
darkNavBarIcons = lightNav,
|
||||
reverse = layout == Settings.AppearanceSettings.Layout.PagerReversed
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
SnackbarHost(
|
||||
snackbarHostState,
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
)
|
||||
enterTransition?.let {
|
||||
val dX = it.startBounds.center.x - it.targetBounds.center.x
|
||||
val dY = it.startBounds.center.y - it.targetBounds.center.y
|
||||
val s = (it.startBounds.minDimension / it.targetBounds.minDimension - 1f) * 0.5f
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.graphicsLayer {
|
||||
val p = (enterTransitionProgress.value).pow(2f)
|
||||
transformOrigin = TransformOrigin.Center
|
||||
translationX = it.targetBounds.left + dX * (1 - p)
|
||||
translationY = it.targetBounds.top + dY * (1 - p)
|
||||
alpha = enterTransitionProgress.value
|
||||
scaleX = 1f + s * (1 - p)
|
||||
scaleY = 1f + s * (1 - p)
|
||||
}) {
|
||||
it.icon?.invoke(Offset(dX, dY)) { enterTransitionProgress.value }
|
||||
}
|
||||
}
|
||||
|
||||
bottomSheetManager.customizeSearchableSheetShown.value?.let {
|
||||
CustomizeSearchableSheet(searchable = it, onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() })
|
||||
}
|
||||
if (bottomSheetManager.editFavoritesSheetShown.value) {
|
||||
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
val windowController = WindowCompat.getInsetsController(window, window.decorView.rootView)
|
||||
windowController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
|
||||
enum class LauncherActivityMode {
|
||||
Launcher,
|
||||
Assistant
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package de.mm20.launcher2.ui.launcher.helper
|
||||
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.zIndex
|
||||
import de.mm20.launcher2.ui.ktx.animateTo
|
||||
import de.mm20.launcher2.ui.ktx.toIntOffset
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.android.awaitFrame
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
/**
|
||||
* Create and remember a [LazyDragAndDropGridState]
|
||||
* @param gridState the [LazyGridState] to use with the LazyGrid
|
||||
* @param onDragStart callback that will be called when an item is picked up. If the return value
|
||||
* is false, the drag operation will be canceled.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberLazyDragAndDropGridState(
|
||||
gridState: LazyGridState = rememberLazyGridState(),
|
||||
onDragStart: (item: LazyGridItemInfo) -> Boolean = { true },
|
||||
onDrag: (item: LazyGridItemInfo, offset: Offset, position: Offset) -> Unit = {_, _, _ ->},
|
||||
onDragEnd: (item: LazyGridItemInfo) -> Unit = {},
|
||||
onDragCancel: (item: LazyGridItemInfo) -> Unit = {},
|
||||
onItemMove: (from: LazyGridItemInfo, to: LazyGridItemInfo) -> Unit
|
||||
): LazyDragAndDropGridState {
|
||||
return remember {
|
||||
LazyDragAndDropGridState(
|
||||
gridState,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
onDragEnd,
|
||||
onDragCancel,
|
||||
onItemMove
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class LazyDragAndDropGridState(
|
||||
val gridState: LazyGridState,
|
||||
val onDragStart: (item: LazyGridItemInfo) -> Boolean = { true },
|
||||
val onDrag: (item: LazyGridItemInfo, offset: Offset, position: Offset) -> Unit = {_, _, _ ->},
|
||||
val onDragEnd: (item: LazyGridItemInfo) -> Unit = {},
|
||||
val onDragCancel: (item: LazyGridItemInfo) -> Unit = {},
|
||||
val onItemMove: (from: LazyGridItemInfo, to: LazyGridItemInfo) -> Unit
|
||||
) {
|
||||
var draggedItem by mutableStateOf<LazyGridItemInfo?>(null)
|
||||
var draggedItemAbsolutePosition by mutableStateOf<Offset?>(null)
|
||||
val draggedItemOffset by derivedStateOf {
|
||||
val absPos = draggedItemAbsolutePosition ?: return@derivedStateOf null
|
||||
val key = draggedItem?.key ?: return@derivedStateOf null
|
||||
val draggedItem = gridState.layoutInfo.visibleItemsInfo.find {
|
||||
it.key == key
|
||||
} ?: return@derivedStateOf null
|
||||
return@derivedStateOf absPos - draggedItem.offset.toOffset()
|
||||
}
|
||||
|
||||
var droppedItemKey by mutableStateOf<Any?>(null)
|
||||
val droppedItemOffset = mutableStateOf<IntOffset>(IntOffset.Zero)
|
||||
|
||||
private var currentDropPosition: Int? = null
|
||||
private var dropJob: Job? = null
|
||||
|
||||
fun startDrag(draggedItem: LazyGridItemInfo): Boolean {
|
||||
if (!onDragStart(draggedItem)) return false
|
||||
this.draggedItem = draggedItem
|
||||
draggedItemAbsolutePosition = draggedItem.offset.toOffset()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the currently dragged item to the specified drop target if the dragged item is held
|
||||
* for at least 300ms over the drop target. The move operation is canceled if the dragged item
|
||||
* is released or moved out of the dropTarget area during the 300ms time frame.
|
||||
*/
|
||||
suspend fun attemptMove(dropTarget: LazyGridItemInfo) {
|
||||
if (currentDropPosition != dropTarget.index) {
|
||||
coroutineScope {
|
||||
dropJob?.cancelAndJoin()
|
||||
dropJob = launch {
|
||||
currentDropPosition = dropTarget.index
|
||||
delay(300)
|
||||
// Get a fresh copy of layout info because index in saved layout info might be outdated
|
||||
val dragged =
|
||||
gridState.layoutInfo.visibleItemsInfo.find { it.key == draggedItem?.key }
|
||||
if (dragged != null) {
|
||||
onItemMove(dragged, dropTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cancelDrag() {
|
||||
draggedItem?.let { onDragCancel(it) }
|
||||
afterDragEnded()
|
||||
}
|
||||
|
||||
suspend fun endDrag() {
|
||||
draggedItem?.let { onDragEnd(it) }
|
||||
afterDragEnded()
|
||||
}
|
||||
|
||||
private suspend fun afterDragEnded() {
|
||||
endScrolling()
|
||||
val key = draggedItem?.key
|
||||
val startOffset = draggedItemOffset
|
||||
draggedItem = null
|
||||
draggedItemAbsolutePosition = null
|
||||
currentDropPosition = null
|
||||
|
||||
if (key == null || startOffset == null) return
|
||||
droppedItemKey = key
|
||||
droppedItemOffset.value = startOffset.toIntOffset()
|
||||
droppedItemOffset.animateTo(IntOffset.Zero, IntOffset.VectorConverter)
|
||||
droppedItemKey = null
|
||||
}
|
||||
|
||||
private var scrollJob: Job? = null
|
||||
private var currentScrollDelta = 0.0f
|
||||
|
||||
/**
|
||||
* Scroll the lazy grid by `delta` px per second until [endScrolling] is called
|
||||
*/
|
||||
suspend fun enableScrolling(delta: Float) {
|
||||
if (currentScrollDelta == delta) return
|
||||
coroutineScope {
|
||||
scrollJob?.cancelAndJoin()
|
||||
scrollJob = launch {
|
||||
currentScrollDelta = delta
|
||||
delay(500)
|
||||
var lastFrame = awaitFrame()
|
||||
while (isActive) {
|
||||
val frame = awaitFrame()
|
||||
val timeDelta = frame - lastFrame
|
||||
gridState.scrollBy(delta * timeDelta / 1000_000_000f)
|
||||
lastFrame = frame
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel scrolling
|
||||
*/
|
||||
fun endScrolling() {
|
||||
currentScrollDelta = 0f
|
||||
scrollJob?.cancel()
|
||||
scrollJob = null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyVerticalDragAndDropGrid(
|
||||
state: LazyDragAndDropGridState,
|
||||
columns: GridCells,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
content: LazyGridScope.() -> Unit
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns,
|
||||
modifier.dragAndDrop(
|
||||
state,
|
||||
LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
LocalHapticFeedback.current
|
||||
),
|
||||
state.gridState,
|
||||
contentPadding,
|
||||
false,
|
||||
verticalArrangement,
|
||||
horizontalArrangement,
|
||||
flingBehavior,
|
||||
userScrollEnabled,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyHorizontalDragAndDropGrid(
|
||||
state: LazyDragAndDropGridState,
|
||||
rows: GridCells,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
content: LazyGridScope.() -> Unit
|
||||
) {
|
||||
LazyHorizontalGrid(
|
||||
rows,
|
||||
modifier.dragAndDrop(
|
||||
state,
|
||||
LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
LocalHapticFeedback.current
|
||||
),
|
||||
state.gridState,
|
||||
contentPadding,
|
||||
false,
|
||||
horizontalArrangement,
|
||||
verticalArrangement,
|
||||
flingBehavior,
|
||||
userScrollEnabled,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fun Modifier.dragAndDrop(
|
||||
state: LazyDragAndDropGridState,
|
||||
isRtl: Boolean,
|
||||
hapticFeedback: HapticFeedback
|
||||
) =
|
||||
this then pointerInput(null) {
|
||||
val scope = CoroutineScope(coroutineContext)
|
||||
val scrollEdgeSize = 32.dp.toPx()
|
||||
val scrollDelta = 128.dp.toPx()
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDragStart = { offset ->
|
||||
val draggedItem = state.gridState.layoutInfo.visibleItemsInfo.find {
|
||||
Rect(
|
||||
it.offset.toOffset().let {off ->
|
||||
if (isRtl) off.copy(x = state.gridState.layoutInfo.viewportSize.width - off.x - it.size.width)
|
||||
else off
|
||||
},
|
||||
it.size.toSize()
|
||||
).contains(offset)
|
||||
}
|
||||
if (draggedItem != null && state.startDrag(draggedItem)) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
val absPosition = state.draggedItemAbsolutePosition
|
||||
val draggedItem = state.draggedItem
|
||||
if (absPosition != null && draggedItem != null) {
|
||||
state.draggedItemAbsolutePosition = absPosition + dragAmount.let {
|
||||
if (isRtl) it.copy(x = -it.x)
|
||||
else it
|
||||
}
|
||||
val draggedCenter = Rect(absPosition, draggedItem.size.toSize()).center
|
||||
val dragOver = state.gridState.layoutInfo.visibleItemsInfo.find {
|
||||
Rect(
|
||||
it.offset.toOffset(),
|
||||
it.size.toSize()
|
||||
).contains(draggedCenter)
|
||||
}
|
||||
|
||||
if (dragOver != null && dragOver.key != draggedItem.key) {
|
||||
scope.launch {
|
||||
state.attemptMove(dragOver)
|
||||
}
|
||||
}
|
||||
|
||||
val toStart =
|
||||
if (state.gridState.layoutInfo.orientation == Orientation.Horizontal) draggedCenter.x else draggedCenter.y
|
||||
|
||||
|
||||
if (toStart - state.gridState.layoutInfo.viewportStartOffset < scrollEdgeSize) {
|
||||
scope.launch {
|
||||
state.enableScrolling(-scrollDelta)
|
||||
}
|
||||
} else if (state.gridState.layoutInfo.viewportEndOffset - toStart < scrollEdgeSize) {
|
||||
scope.launch {
|
||||
state.enableScrolling(scrollDelta)
|
||||
}
|
||||
} else {
|
||||
state.endScrolling()
|
||||
}
|
||||
|
||||
state.draggedItemOffset?.let { state.onDrag(draggedItem, it, absPosition) }
|
||||
}
|
||||
},
|
||||
onDragCancel = {
|
||||
scope.launch { state.cancelDrag() }
|
||||
},
|
||||
onDragEnd = {
|
||||
scope.launch { state.endDrag() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyGridItemScope.DraggableItem(
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyDragAndDropGridState,
|
||||
key: Any?,
|
||||
content: @Composable BoxScope.(isDragged: Boolean) -> Unit
|
||||
) {
|
||||
val isDragged = state.draggedItem?.key == key || state.droppedItemKey == key
|
||||
Box(
|
||||
modifier = modifier
|
||||
.then(if (isDragged) Modifier else Modifier.animateItemPlacement())
|
||||
.zIndex(if (isDragged) 1f else 0f)
|
||||
.offset {
|
||||
if (state.draggedItem?.key == key) {
|
||||
state.draggedItemOffset?.toIntOffset() ?: IntOffset.Zero
|
||||
} else if (state.droppedItemKey == key) {
|
||||
state.droppedItemOffset.value
|
||||
} else {
|
||||
IntOffset.Zero
|
||||
}
|
||||
},
|
||||
content = { content(isDragged) },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package de.mm20.launcher2.ui.launcher.helper
|
||||
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.lazy.grid.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.zIndex
|
||||
import de.mm20.launcher2.ui.ktx.animateTo
|
||||
import de.mm20.launcher2.ui.ktx.toIntOffset
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.android.awaitFrame
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
/**
|
||||
* Create and remember a [LazyDragAndDropGridState]
|
||||
* @param gridState the [LazyGridState] to use with the LazyGrid
|
||||
* @param onDragStart callback that will be called when an item is picked up. If the return value
|
||||
* is false, the drag operation will be canceled.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberLazyDragAndDropListState(
|
||||
listState: LazyListState = rememberLazyListState(),
|
||||
scrollEdgeSize: Dp = 32.dp,
|
||||
scrollDelta: Dp = 128.dp,
|
||||
onDragStart: (item: LazyListItemInfo) -> Boolean = { true },
|
||||
onDrag: (item: LazyListItemInfo, offset: Offset) -> Unit = { _, _ -> },
|
||||
onDragEnd: (item: LazyListItemInfo) -> Unit = {},
|
||||
onDragCancel: (item: LazyListItemInfo) -> Unit = {},
|
||||
onItemMove: (from: LazyListItemInfo, to: LazyListItemInfo) -> Unit,
|
||||
): LazyDragAndDropListState {
|
||||
val scrollDeltaPx = scrollDelta.toPixels()
|
||||
val scrollEdgeSizePx = scrollEdgeSize.toPixels()
|
||||
return remember {
|
||||
LazyDragAndDropListState(
|
||||
listState,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
onDragEnd,
|
||||
onDragCancel,
|
||||
onItemMove,
|
||||
scrollDeltaPx,
|
||||
scrollEdgeSizePx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class LazyDragAndDropListState(
|
||||
val listState: LazyListState,
|
||||
val onDragStart: (item: LazyListItemInfo) -> Boolean = { true },
|
||||
val onDrag: (item: LazyListItemInfo, offset: Offset) -> Unit = { _, _ -> },
|
||||
val onDragEnd: (item: LazyListItemInfo) -> Unit = {},
|
||||
val onDragCancel: (item: LazyListItemInfo) -> Unit = {},
|
||||
val onItemMove: (from: LazyListItemInfo, to: LazyListItemInfo) -> Unit,
|
||||
private val scrollDelta: Float,
|
||||
private val scrollEdgeSize: Float,
|
||||
) {
|
||||
var draggedItem by mutableStateOf<LazyListItemInfo?>(null)
|
||||
var draggedItemAbsolutePosition by mutableStateOf<Offset?>(null)
|
||||
val draggedItemOffset by derivedStateOf {
|
||||
val absPos = draggedItemAbsolutePosition ?: return@derivedStateOf null
|
||||
val key = draggedItem?.key ?: return@derivedStateOf null
|
||||
val draggedItem = listState.layoutInfo.visibleItemsInfo.find {
|
||||
it.key == key
|
||||
} ?: return@derivedStateOf null
|
||||
return@derivedStateOf absPos - draggedItem.offset.toOffset()
|
||||
}
|
||||
|
||||
var droppedItemKey by mutableStateOf<Any?>(null)
|
||||
val droppedItemOffset = mutableStateOf<IntOffset>(IntOffset.Zero)
|
||||
|
||||
private var currentDropPosition: Int? = null
|
||||
private var dropJob: Job? = null
|
||||
|
||||
fun startDrag(offset: Offset): Boolean {
|
||||
val draggedItem = listState.layoutInfo.visibleItemsInfo.find {
|
||||
Rect(
|
||||
(it.offset + listState.layoutInfo.beforeContentPadding).toOffset(),
|
||||
it.size.toSize()
|
||||
).contains(offset)
|
||||
} ?: return false
|
||||
|
||||
if (!onDragStart(draggedItem)) return false
|
||||
this.draggedItem = draggedItem
|
||||
draggedItemAbsolutePosition = draggedItem.offset.toOffset()
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun drag(dragAmount: Offset) {
|
||||
val absPosition = draggedItemAbsolutePosition
|
||||
val draggedItem = draggedItem
|
||||
if (absPosition != null && draggedItem != null) {
|
||||
draggedItemAbsolutePosition = absPosition + dragAmount
|
||||
val draggedCenter = Rect(absPosition, draggedItem.size.toSize()).center
|
||||
val dragOver = listState.layoutInfo.visibleItemsInfo.find {
|
||||
Rect(
|
||||
it.offset.toOffset(),
|
||||
it.size.toSize()
|
||||
).contains(draggedCenter)
|
||||
}
|
||||
|
||||
if (dragOver != null && dragOver.key != draggedItem.key) {
|
||||
attemptMove(dragOver)
|
||||
}
|
||||
|
||||
val toStart =
|
||||
if (listState.layoutInfo.orientation == Orientation.Horizontal) draggedCenter.x else draggedCenter.y
|
||||
|
||||
|
||||
if (toStart - listState.layoutInfo.viewportStartOffset < scrollEdgeSize) {
|
||||
enableScrolling(-scrollDelta)
|
||||
} else if (listState.layoutInfo.viewportEndOffset - toStart < scrollEdgeSize) {
|
||||
enableScrolling(scrollDelta)
|
||||
} else {
|
||||
endScrolling()
|
||||
}
|
||||
|
||||
draggedItemOffset?.let { onDrag(draggedItem, it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toOffset(): Offset {
|
||||
return if (listState.layoutInfo.orientation == Orientation.Horizontal) {
|
||||
Offset(this.toFloat(), 0f)
|
||||
} else {
|
||||
Offset(0f, this.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.toSize(): Size {
|
||||
return if (listState.layoutInfo.orientation == Orientation.Horizontal) {
|
||||
Size(this.toFloat(), listState.layoutInfo.viewportSize.height.toFloat())
|
||||
} else {
|
||||
Size(listState.layoutInfo.viewportSize.width.toFloat(), this.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the currently dragged item to the specified drop target if the dragged item is held
|
||||
* for at least 300ms over the drop target. The move operation is canceled if the dragged item
|
||||
* is released or moved out of the dropTarget area during the 300ms time frame.
|
||||
*/
|
||||
private suspend fun attemptMove(dropTarget: LazyListItemInfo) {
|
||||
if (currentDropPosition != dropTarget.index) {
|
||||
coroutineScope {
|
||||
dropJob?.cancelAndJoin()
|
||||
dropJob = launch {
|
||||
currentDropPosition = dropTarget.index
|
||||
delay(300)
|
||||
// Get a fresh copy of layout info because index in saved layout info might be outdated
|
||||
val dragged =
|
||||
listState.layoutInfo.visibleItemsInfo.find { it.key == draggedItem?.key }
|
||||
if (dragged != null) {
|
||||
onItemMove(dragged, dropTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cancelDrag() {
|
||||
draggedItem?.let { onDragCancel(it) }
|
||||
afterDragEnded()
|
||||
}
|
||||
|
||||
suspend fun endDrag() {
|
||||
draggedItem?.let { onDragEnd(it) }
|
||||
afterDragEnded()
|
||||
}
|
||||
|
||||
private suspend fun afterDragEnded() {
|
||||
endScrolling()
|
||||
val key = draggedItem?.key
|
||||
val startOffset = draggedItemOffset
|
||||
draggedItem = null
|
||||
draggedItemAbsolutePosition = null
|
||||
currentDropPosition = null
|
||||
|
||||
if (key == null || startOffset == null) return
|
||||
droppedItemKey = key
|
||||
droppedItemOffset.value = startOffset.toIntOffset()
|
||||
droppedItemOffset.animateTo(IntOffset.Zero, IntOffset.VectorConverter)
|
||||
droppedItemKey = null
|
||||
}
|
||||
|
||||
private var scrollJob: Job? = null
|
||||
private var currentScrollDelta = 0.0f
|
||||
|
||||
/**
|
||||
* Scroll the lazy grid by `delta` px per second until [endScrolling] is called
|
||||
*/
|
||||
suspend fun enableScrolling(delta: Float) {
|
||||
if (currentScrollDelta == delta) return
|
||||
coroutineScope {
|
||||
scrollJob?.cancelAndJoin()
|
||||
scrollJob = launch {
|
||||
currentScrollDelta = delta
|
||||
delay(500)
|
||||
var lastFrame = awaitFrame()
|
||||
while (isActive) {
|
||||
val frame = awaitFrame()
|
||||
val timeDelta = frame - lastFrame
|
||||
listState.scrollBy(delta * timeDelta / 1000_000_000f)
|
||||
lastFrame = frame
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel scrolling
|
||||
*/
|
||||
fun endScrolling() {
|
||||
currentScrollDelta = 0f
|
||||
scrollJob?.cancel()
|
||||
scrollJob = null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyDragAndDropRow(
|
||||
state: LazyDragAndDropListState,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
//reverseLayout: Boolean = false, //TODO: Fix reverse layout
|
||||
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
verticalAlignment: Alignment.Vertical = Alignment.Top,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
bidirectionalDrag: Boolean = true,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = modifier.dragAndDrop(
|
||||
state,
|
||||
isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
dragVertical = bidirectionalDrag,
|
||||
dragHorizontal = true,
|
||||
),
|
||||
state = state.listState,
|
||||
contentPadding = contentPadding,
|
||||
reverseLayout = false,
|
||||
verticalAlignment = verticalAlignment,
|
||||
horizontalArrangement = horizontalArrangement,
|
||||
flingBehavior = flingBehavior,
|
||||
userScrollEnabled = userScrollEnabled,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyDragAndDropColumn(
|
||||
state: LazyDragAndDropListState,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
//reverseLayout: Boolean = false, //TODO: Fix reverse layout
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
bidirectionalDrag: Boolean = true,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.dragAndDrop(
|
||||
state,
|
||||
isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
dragVertical = true,
|
||||
dragHorizontal = bidirectionalDrag,
|
||||
),
|
||||
state = state.listState,
|
||||
contentPadding = contentPadding,
|
||||
reverseLayout = false,
|
||||
verticalArrangement = verticalArrangement,
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
flingBehavior = flingBehavior,
|
||||
userScrollEnabled = userScrollEnabled,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
fun Modifier.dragAndDrop(
|
||||
state: LazyDragAndDropListState,
|
||||
isRtl: Boolean,
|
||||
dragVertical: Boolean = true,
|
||||
dragHorizontal: Boolean = true,
|
||||
hapticFeedback: HapticFeedback
|
||||
) =
|
||||
this then pointerInput(null) {
|
||||
val scope = CoroutineScope(coroutineContext)
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDragStart = { offset ->
|
||||
if (state.startDrag(offset)) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
},
|
||||
onDrag = { _, dragAmount ->
|
||||
scope.launch {
|
||||
state.drag(
|
||||
when {
|
||||
!dragVertical && !dragHorizontal -> Offset.Zero
|
||||
dragVertical && !dragHorizontal -> Offset(0f, dragAmount.y)
|
||||
!dragVertical && dragHorizontal && isRtl -> Offset(-dragAmount.x, 0f)
|
||||
!dragVertical && dragHorizontal && !isRtl -> Offset(dragAmount.x, 0f)
|
||||
dragVertical && dragHorizontal && isRtl -> Offset(-dragAmount.x, dragAmount.y)
|
||||
else -> dragAmount
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
onDragCancel = {
|
||||
scope.launch { state.cancelDrag() }
|
||||
},
|
||||
onDragEnd = {
|
||||
scope.launch { state.endDrag() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.DraggableItem(
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyDragAndDropListState,
|
||||
key: Any?,
|
||||
content: @Composable BoxScope.(isDragged: Boolean) -> Unit
|
||||
) {
|
||||
val isDragged = state.draggedItem?.key == key || state.droppedItemKey == key
|
||||
Box(
|
||||
modifier = modifier
|
||||
.then(if (isDragged) Modifier else Modifier.animateItemPlacement())
|
||||
.zIndex(if (isDragged) 1f else 0f)
|
||||
.offset {
|
||||
if (state.draggedItem?.key == key) {
|
||||
state.draggedItemOffset?.toIntOffset() ?: IntOffset.Zero
|
||||
} else if (state.droppedItemKey == key) {
|
||||
state.droppedItemOffset.value
|
||||
} else {
|
||||
IntOffset.Zero
|
||||
}
|
||||
},
|
||||
content = { content(isDragged) },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.mm20.launcher2.ui.launcher.helper
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
|
||||
@Composable
|
||||
fun WallpaperBlur(blur: () -> Boolean) {
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
val blurWallpaper = blur()
|
||||
LaunchedEffect(blurWallpaper) {
|
||||
if (!isAtLeastApiLevel(31)) return@LaunchedEffect
|
||||
(context as Activity).window.attributes = context.window.attributes.also {
|
||||
if (blurWallpaper) {
|
||||
it.blurBehindRadius = with(density) { 32.dp.toPx().toInt() }
|
||||
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
|
||||
} else {
|
||||
it.blurBehindRadius = 0
|
||||
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.Tag
|
||||
import androidx.compose.material.icons.rounded.Work
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
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.draw.clipToBounds
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.Banner
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.component.PartialLauncherCard
|
||||
import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorItem
|
||||
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
|
||||
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
|
||||
import de.mm20.launcher2.ui.launcher.search.favorites.SearchFavoritesVM
|
||||
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterItem
|
||||
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem
|
||||
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItem
|
||||
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlin.math.ceil
|
||||
|
||||
@Composable
|
||||
fun SearchColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
paddingValues: PaddingValues = PaddingValues(0.dp),
|
||||
state: LazyListState = rememberLazyListState(),
|
||||
reverse: Boolean = false,
|
||||
) {
|
||||
|
||||
val columns = LocalGridSettings.current.columnCount
|
||||
val context = LocalContext.current
|
||||
|
||||
val viewModel: SearchVM = viewModel()
|
||||
|
||||
val favoritesVM: SearchFavoritesVM = viewModel()
|
||||
val favorites by favoritesVM.favorites.collectAsState(emptyList())
|
||||
|
||||
var showWorkProfileApps by remember { mutableStateOf(false) }
|
||||
|
||||
val hideFavs by viewModel.hideFavorites.observeAsState(true)
|
||||
val favoritesEnabled by viewModel.favoritesEnabled.collectAsState(false)
|
||||
val apps by viewModel.appResults.observeAsState(emptyList())
|
||||
val workApps by viewModel.workAppResults.observeAsState(emptyList())
|
||||
val appShortcuts by viewModel.appShortcutResults.observeAsState(emptyList())
|
||||
val contacts by viewModel.contactResults.observeAsState(emptyList())
|
||||
val files by viewModel.fileResults.observeAsState(emptyList())
|
||||
val events by viewModel.calendarResults.observeAsState(emptyList())
|
||||
val unitConverter by viewModel.unitConverterResults.observeAsState(emptyList())
|
||||
val calculator by viewModel.calculatorResults.observeAsState(emptyList())
|
||||
val wikipedia by viewModel.wikipediaResults.observeAsState(emptyList())
|
||||
val website by viewModel.websiteResults.observeAsState(emptyList())
|
||||
val hiddenResults by viewModel.hiddenResults.observeAsState(emptyList())
|
||||
|
||||
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
|
||||
|
||||
val missingCalendarPermission by viewModel.missingCalendarPermission.collectAsState(false)
|
||||
val missingShortcutsPermission by viewModel.missingAppShortcutPermission.collectAsState(false)
|
||||
val missingContactsPermission by viewModel.missingContactsPermission.collectAsState(false)
|
||||
val missingFilesPermission by viewModel.missingFilesPermission.collectAsState(false)
|
||||
|
||||
val pinnedTags by favoritesVM.pinnedTags.collectAsState(emptyList())
|
||||
val selectedTag by favoritesVM.selectedTag.collectAsState(null)
|
||||
val tagsScrollState = rememberScrollState()
|
||||
val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false)
|
||||
|
||||
LazyColumn(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
contentPadding = paddingValues,
|
||||
reverseLayout = reverse,
|
||||
) {
|
||||
if (!hideFavs && favoritesEnabled) {
|
||||
GridResults(
|
||||
items = favorites.toImmutableList(),
|
||||
columns = columns,
|
||||
key = "favorites",
|
||||
reverse = reverse,
|
||||
before = if (favorites.isEmpty()) {
|
||||
{
|
||||
Banner(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = stringResource(
|
||||
if (selectedTag == null) R.string.favorites_empty else R.string.favorites_empty_tag
|
||||
),
|
||||
icon = if (selectedTag == null) Icons.Rounded.Star else Icons.Rounded.Tag,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
after = if (pinnedTags.isEmpty() && !favoritesEditButton) {
|
||||
null
|
||||
} else {
|
||||
{
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = if (reverse) 8.dp else 4.dp,
|
||||
bottom = if (reverse) 4.dp else 8.dp,
|
||||
end = if (favoritesEditButton) 8.dp else 0.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.horizontalScroll(tagsScrollState)
|
||||
.padding(end = 12.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
selected = selectedTag == null,
|
||||
onClick = { favoritesVM.selectTag(null) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Star,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(R.string.favorites)) }
|
||||
)
|
||||
for (tag in pinnedTags) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
selected = selectedTag == tag.tag,
|
||||
onClick = { favoritesVM.selectTag(tag.tag) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Tag,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(tag.label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (favoritesEditButton) {
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
SmallFloatingActionButton(
|
||||
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
|
||||
onClick = { sheetManager.showEditFavoritesSheet() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Edit,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
GridResults(
|
||||
items = if (showWorkProfileApps && workApps.isNotEmpty()) workApps.toImmutableList() else apps.toImmutableList(),
|
||||
columns = columns,
|
||||
reverse = reverse,
|
||||
key = "apps",
|
||||
before = if (workApps.isNotEmpty()) {
|
||||
{
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(
|
||||
top = if (reverse) 4.dp else 8.dp,
|
||||
bottom = if (reverse) 8.dp else 4.dp
|
||||
),
|
||||
) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
selected = !showWorkProfileApps,
|
||||
onClick = { showWorkProfileApps = false },
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Person, contentDescription = null)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
stringResource(R.string.apps_profile_main),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
)
|
||||
FilterChip(
|
||||
selected = showWorkProfileApps,
|
||||
onClick = { showWorkProfileApps = true },
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Work, contentDescription = null)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
stringResource(R.string.apps_profile_work),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null
|
||||
)
|
||||
ListResults(
|
||||
before = if (missingShortcutsPermission && !isSearchEmpty) {
|
||||
{
|
||||
MissingPermissionBanner(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = stringResource(
|
||||
R.string.missing_permission_appshortcuts_search,
|
||||
stringResource(R.string.app_name)
|
||||
),
|
||||
onClick = { viewModel.requestAppShortcutPermission(context as AppCompatActivity) },
|
||||
secondaryAction = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.disableAppShortcutSearch()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(R.string.turn_off),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
items = appShortcuts.toImmutableList(),
|
||||
reverse = reverse,
|
||||
key = "shortcuts"
|
||||
)
|
||||
for (conv in unitConverter) {
|
||||
SingleResult {
|
||||
UnitConverterItem(unitConverter = conv)
|
||||
}
|
||||
}
|
||||
for (calc in calculator) {
|
||||
SingleResult {
|
||||
CalculatorItem(calculator = calc)
|
||||
}
|
||||
}
|
||||
ListResults(
|
||||
before = if (missingCalendarPermission && !isSearchEmpty) {
|
||||
{
|
||||
MissingPermissionBanner(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = stringResource(R.string.missing_permission_calendar_search),
|
||||
onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity) },
|
||||
secondaryAction = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.disableCalendarSearch()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(R.string.turn_off),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
items = events.toImmutableList(),
|
||||
reverse = reverse,
|
||||
key = "events"
|
||||
)
|
||||
ListResults(
|
||||
before = if (missingContactsPermission && !isSearchEmpty) {
|
||||
{
|
||||
MissingPermissionBanner(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = stringResource(R.string.missing_permission_contact_search),
|
||||
onClick = { viewModel.requestContactsPermission(context as AppCompatActivity) },
|
||||
secondaryAction = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.disableContactsSearch()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(R.string.turn_off),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
items = contacts.toImmutableList(),
|
||||
reverse = reverse,
|
||||
key = "contacts"
|
||||
)
|
||||
for (wiki in wikipedia) {
|
||||
SingleResult {
|
||||
WikipediaItem(wikipedia = wiki)
|
||||
}
|
||||
}
|
||||
for (ws in website) {
|
||||
SingleResult {
|
||||
WebsiteItem(website = ws)
|
||||
}
|
||||
}
|
||||
ListResults(
|
||||
before = if (missingFilesPermission && !isSearchEmpty) {
|
||||
{
|
||||
MissingPermissionBanner(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = stringResource(R.string.missing_permission_files_search),
|
||||
onClick = { viewModel.requestFilesPermission(context as AppCompatActivity) },
|
||||
secondaryAction = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.disableFilesSearch()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(R.string.turn_off),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
items = files.toImmutableList(),
|
||||
reverse = reverse,
|
||||
key = "files"
|
||||
)
|
||||
}
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
if (sheetManager.hiddenItemsSheetShown.value) {
|
||||
HiddenItemsSheet(items = hiddenResults, onDismiss = { sheetManager.dismissHiddenItemsSheet() })
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.GridResults(
|
||||
items: ImmutableList<SavableSearchable>,
|
||||
columns: Int,
|
||||
reverse: Boolean,
|
||||
key: String,
|
||||
before: (@Composable () -> Unit)? = null,
|
||||
after: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
if (items.isEmpty() && before == null && after == null) return
|
||||
|
||||
if (before != null) {
|
||||
item(key = "$key-before") {
|
||||
PartialCardRow(
|
||||
isFirst = true,
|
||||
isLast = items.isEmpty() && after == null,
|
||||
reverse = reverse
|
||||
) {
|
||||
before()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val rows = ceil(items.size / columns.toFloat()).toInt()
|
||||
items(
|
||||
rows,
|
||||
key = {
|
||||
"$key-${items[it * columns].key}"
|
||||
}
|
||||
) {
|
||||
PartialCardRow(
|
||||
isFirst = it == 0 && before == null,
|
||||
isLast = it == rows - 1 && after == null,
|
||||
reverse = reverse
|
||||
) {
|
||||
GridRow(
|
||||
modifier = Modifier.padding(
|
||||
top = if (if (reverse) it == rows - 1 else it == 0) 4.dp else 0.dp,
|
||||
bottom = if (if (reverse) it == 0 else it == rows - 1) 2.dp else 0.dp,
|
||||
),
|
||||
items = items.subList(
|
||||
it * columns,
|
||||
(it * columns + columns).coerceAtMost(items.size)
|
||||
),
|
||||
columns = columns,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (after != null) {
|
||||
item(key = "$key-after") {
|
||||
PartialCardRow(
|
||||
isFirst = items.isEmpty() && before == null,
|
||||
isLast = true,
|
||||
reverse = reverse
|
||||
) {
|
||||
after()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GridRow(
|
||||
modifier: Modifier = Modifier,
|
||||
items: ImmutableList<SavableSearchable>,
|
||||
columns: Int,
|
||||
showLabels: Boolean = LocalGridSettings.current.showLabels,
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
) {
|
||||
for (item in items) {
|
||||
GridItem(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(4.dp, 8.dp),
|
||||
item = item,
|
||||
showLabels = showLabels
|
||||
)
|
||||
}
|
||||
for (i in 0 until columns - items.size) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.ListResults(
|
||||
items: ImmutableList<SavableSearchable>,
|
||||
reverse: Boolean,
|
||||
key: String,
|
||||
before: (@Composable () -> Unit)? = null,
|
||||
after: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
if (before != null) {
|
||||
item(key = "$key-before") {
|
||||
PartialCardRow(
|
||||
isFirst = true,
|
||||
isLast = items.isEmpty() && after == null,
|
||||
reverse = reverse
|
||||
) {
|
||||
before()
|
||||
}
|
||||
}
|
||||
}
|
||||
items(
|
||||
items.size,
|
||||
key = {
|
||||
"$key-${items[it].key}"
|
||||
}
|
||||
) {
|
||||
PartialCardRow(
|
||||
isFirst = it == 0 && before == null,
|
||||
isLast = it == items.lastIndex && after == null,
|
||||
reverse = reverse
|
||||
) {
|
||||
ListRow(
|
||||
modifier = Modifier.padding(
|
||||
top = if (if (reverse) it == items.size - 1 else it == 0) 8.dp else 4.dp,
|
||||
bottom = if (if (reverse) it == 0 else it == items.size - 1) 8.dp else 4.dp,
|
||||
),
|
||||
item = items[it],
|
||||
)
|
||||
}
|
||||
}
|
||||
if (after != null) {
|
||||
item(key = "$key-after") {
|
||||
PartialCardRow(
|
||||
isFirst = items.isEmpty() && before == null,
|
||||
isLast = true,
|
||||
reverse = reverse
|
||||
) {
|
||||
after()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListRow(
|
||||
modifier: Modifier = Modifier,
|
||||
item: SavableSearchable,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.padding(
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
item = item
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.SingleResult(content: @Composable (() -> Unit)?) {
|
||||
if (content == null) return
|
||||
item {
|
||||
LauncherCard(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 8.dp,
|
||||
vertical = 4.dp,
|
||||
)
|
||||
.animateItemPlacement()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyItemScope.PartialCardRow(
|
||||
modifier: Modifier = Modifier,
|
||||
isFirst: Boolean,
|
||||
isLast: Boolean,
|
||||
reverse: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val isTop = isFirst && !reverse || isLast && reverse
|
||||
val isBottom = isLast && !reverse || isFirst && reverse
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clipToBounds()
|
||||
.animateItemPlacement()
|
||||
) {
|
||||
PartialLauncherCard(
|
||||
modifier = Modifier.padding(
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
top = if (isTop) 4.dp else 0.dp,
|
||||
bottom = if (isBottom) 4.dp else 0.dp,
|
||||
),
|
||||
isTop = isTop,
|
||||
isBottom = isBottom,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchService
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.Calculator
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.data.UnitConverter
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
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
|
||||
|
||||
class SearchVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val favoritesRepository: FavoritesRepository by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
private val searchService: SearchService by inject()
|
||||
|
||||
val isSearching = MutableLiveData(false)
|
||||
val searchQuery = MutableLiveData<String>("")
|
||||
val isSearchEmpty = MutableLiveData(true)
|
||||
|
||||
val appResults = MutableLiveData<List<LauncherApp>>(emptyList())
|
||||
val workAppResults = MutableLiveData<List<LauncherApp>>(emptyList())
|
||||
val appShortcutResults = MutableLiveData<List<AppShortcut>>(emptyList())
|
||||
val fileResults = MutableLiveData<List<File>>(emptyList())
|
||||
val contactResults = MutableLiveData<List<Contact>>(emptyList())
|
||||
val calendarResults = MutableLiveData<List<CalendarEvent>>(emptyList())
|
||||
val wikipediaResults = MutableLiveData<List<Wikipedia>>(emptyList())
|
||||
val websiteResults = MutableLiveData<List<Website>>(emptyList())
|
||||
val calculatorResults = MutableLiveData<List<Calculator>>(emptyList())
|
||||
val unitConverterResults = MutableLiveData<List<UnitConverter>>(emptyList())
|
||||
val searchActionResults = MutableLiveData<List<SearchAction>>(emptyList())
|
||||
|
||||
val hiddenResults = MutableLiveData<List<SavableSearchable>>(emptyList())
|
||||
|
||||
val favoritesEnabled = dataStore.data.map { it.favorites.enabled }
|
||||
val hideFavorites = MutableLiveData(false)
|
||||
|
||||
private val hiddenItemKeys = favoritesRepository
|
||||
.getHiddenItemKeys()
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
|
||||
|
||||
init {
|
||||
search("", true)
|
||||
}
|
||||
|
||||
var searchJob: Job? = null
|
||||
fun search(query: String, forceRestart: Boolean = false) {
|
||||
if (searchQuery.value == query && !forceRestart) return
|
||||
searchQuery.value = query
|
||||
isSearchEmpty.value = query.isEmpty()
|
||||
hiddenResults.value = emptyList()
|
||||
|
||||
try {
|
||||
searchJob?.cancel()
|
||||
} catch (_: CancellationException) {
|
||||
}
|
||||
hideFavorites.postValue(query.isNotEmpty())
|
||||
searchJob = viewModelScope.launch {
|
||||
isSearching.postValue(true)
|
||||
|
||||
dataStore.data.collectLatest {
|
||||
searchService.search(
|
||||
query,
|
||||
calculator = it.calculatorSearch,
|
||||
unitConverter = it.unitConverterSearch,
|
||||
calendars = it.calendarSearch,
|
||||
contacts = it.contactsSearch,
|
||||
files = it.fileSearch,
|
||||
shortcuts = it.appShortcutSearch,
|
||||
websites = it.websiteSearch,
|
||||
wikipedia = it.wikipediaSearch,
|
||||
).collectLatest { results ->
|
||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
||||
val hidden = mutableListOf<SavableSearchable>()
|
||||
val apps = mutableListOf<LauncherApp>()
|
||||
val workApps = mutableListOf<LauncherApp>()
|
||||
val shortcuts = mutableListOf<AppShortcut>()
|
||||
val files = mutableListOf<File>()
|
||||
val contacts = mutableListOf<Contact>()
|
||||
val events = mutableListOf<CalendarEvent>()
|
||||
val unitConv = mutableListOf<UnitConverter>()
|
||||
val calc = mutableListOf<Calculator>()
|
||||
val wikipedia = mutableListOf<Wikipedia>()
|
||||
val website = mutableListOf<Website>()
|
||||
val actions = mutableListOf<SearchAction>()
|
||||
for (r in results) {
|
||||
when {
|
||||
r is SavableSearchable && hiddenKeys.contains(r.key) -> {
|
||||
hidden.add(r)
|
||||
}
|
||||
|
||||
r is LauncherApp && !r.isMainProfile -> workApps.add(r)
|
||||
r is LauncherApp -> apps.add(r)
|
||||
r is AppShortcut -> shortcuts.add(r)
|
||||
r is File -> files.add(r)
|
||||
r is Contact -> contacts.add(r)
|
||||
r is CalendarEvent -> events.add(r)
|
||||
r is UnitConverter -> unitConv.add(r)
|
||||
r is Calculator -> calc.add(r)
|
||||
r is Website -> website.add(r)
|
||||
r is Wikipedia -> wikipedia.add(r)
|
||||
r is SearchAction -> actions.add(r)
|
||||
}
|
||||
}
|
||||
searchActionResults.value = actions
|
||||
appResults.value = apps
|
||||
workAppResults.value = workApps
|
||||
appShortcutResults.value = shortcuts
|
||||
fileResults.value = files
|
||||
contactResults.value = contacts
|
||||
calendarResults.value = events
|
||||
wikipediaResults.value = wikipedia
|
||||
websiteResults.value = website
|
||||
calculatorResults.value = calc
|
||||
unitConverterResults.value = unitConv
|
||||
hiddenResults.value = hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
isSearching.postValue(false)
|
||||
}
|
||||
}
|
||||
|
||||
val missingCalendarPermission = combine(
|
||||
permissionsManager.hasPermission(PermissionGroup.Calendar),
|
||||
dataStore.data.map { it.calendarSearch.enabled }.distinctUntilChanged()
|
||||
) { perm, enabled -> !perm && enabled }
|
||||
|
||||
fun requestCalendarPermission(context: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(context, PermissionGroup.Calendar)
|
||||
}
|
||||
|
||||
fun disableCalendarSearch() {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setCalendarSearch(it.calendarSearch.toBuilder().setEnabled(false))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val missingContactsPermission = combine(
|
||||
permissionsManager.hasPermission(PermissionGroup.Contacts),
|
||||
dataStore.data.map { it.contactsSearch.enabled }.distinctUntilChanged()
|
||||
) { perm, enabled -> !perm && enabled }
|
||||
|
||||
fun requestContactsPermission(context: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(context, PermissionGroup.Contacts)
|
||||
}
|
||||
|
||||
fun disableContactsSearch() {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setContactsSearch(it.contactsSearch.toBuilder().setEnabled(false))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val missingFilesPermission = combine(
|
||||
permissionsManager.hasPermission(PermissionGroup.ExternalStorage),
|
||||
dataStore.data.map { it.fileSearch.localFiles }.distinctUntilChanged()
|
||||
) { perm, enabled -> !perm && enabled }
|
||||
|
||||
fun requestFilesPermission(context: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(context, PermissionGroup.ExternalStorage)
|
||||
}
|
||||
|
||||
fun disableFilesSearch() {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setFileSearch(it.fileSearch.toBuilder().setLocalFiles(false))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val missingAppShortcutPermission = combine(
|
||||
permissionsManager.hasPermission(PermissionGroup.AppShortcuts),
|
||||
dataStore.data.map { it.appShortcutSearch.enabled }.distinctUntilChanged()
|
||||
) { perm, enabled -> !perm && enabled }
|
||||
|
||||
fun requestAppShortcutPermission(context: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(context, PermissionGroup.AppShortcuts)
|
||||
}
|
||||
|
||||
fun disableAppShortcutSearch() {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setAppShortcutSearch(it.appShortcutSearch.toBuilder().setEnabled(false))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.apps
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.*
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import de.mm20.launcher2.ui.modifier.scale
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun AppItem(
|
||||
modifier: Modifier = Modifier,
|
||||
app: LauncherApp,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val viewModel = remember { AppItemVM(app) }
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Row {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = app.labelOverride ?: app.label,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
||||
if (tags.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
||||
text = tags.joinToString(separator = " #", prefix = "#"),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
app.version?.let {
|
||||
Text(
|
||||
text = stringResource(R.string.app_info_version, it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = app.`package`,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(top = 1.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp)
|
||||
.animateContentSize(),
|
||||
mainAxisSpacing = 12.dp,
|
||||
crossAxisSpacing = 0.dp
|
||||
) {
|
||||
val notifications by viewModel.notifications.collectAsState(initial = emptyList())
|
||||
|
||||
for (not in notifications) {
|
||||
val title =
|
||||
not.notification.extras.getString(NotificationCompat.EXTRA_TITLE, null)
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: not.notification.extras.getString(
|
||||
NotificationCompat.EXTRA_TEXT,
|
||||
null
|
||||
)
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: continue
|
||||
|
||||
val icon =
|
||||
remember { not.notification.smallIcon?.loadDrawable(context) }?.let {
|
||||
rememberAsyncImagePainter(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
Chip(
|
||||
text = title,
|
||||
icon = icon,
|
||||
rightIcon = Icons.Rounded.Clear,
|
||||
rightAction = {
|
||||
viewModel.clearNotification(not)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.openNotification(not)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val shortcuts by viewModel.shortcuts.collectAsState(emptyList())
|
||||
|
||||
for (shortcut in shortcuts) {
|
||||
val title =
|
||||
shortcut.launcherShortcut.shortLabel
|
||||
?: shortcut.launcherShortcut.longLabel
|
||||
?: continue
|
||||
val isPinned by remember(shortcut) { viewModel.isShortcutPinned(shortcut) }.collectAsState(
|
||||
false
|
||||
)
|
||||
|
||||
val icon =
|
||||
remember {
|
||||
viewModel.getShortcutIcon(
|
||||
context,
|
||||
shortcut.launcherShortcut
|
||||
)
|
||||
}
|
||||
?.let {
|
||||
rememberAsyncImagePainter(it)
|
||||
}
|
||||
|
||||
Chip(
|
||||
text = title.toString(),
|
||||
icon = icon,
|
||||
rightIcon = if (LocalFavoritesEnabled.current) {
|
||||
if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline
|
||||
} else null,
|
||||
rightAction = {
|
||||
if (isPinned) {
|
||||
viewModel.unpinShortcut(shortcut)
|
||||
} else {
|
||||
viewModel.pinShortcut(shortcut)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
viewModel.launchShortcut(context, shortcut)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val badge by viewModel.badge.collectAsState(null)
|
||||
val iconSize = 84.dp.toPixels().toInt()
|
||||
val icon by remember(app) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
||||
ShapedLauncherIcon(
|
||||
size = 84.dp,
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
badge = { badge },
|
||||
icon = { icon },
|
||||
)
|
||||
}
|
||||
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_app_info),
|
||||
icon = Icons.Rounded.Info
|
||||
) {
|
||||
viewModel.openAppInfo(context)
|
||||
})
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_launch),
|
||||
icon = Icons.Rounded.OpenInNew,
|
||||
action = {
|
||||
viewModel.launch(context)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(app) }
|
||||
))
|
||||
|
||||
val storeDetails = remember(app) { app.getStoreDetails(context) }
|
||||
val shareAction = if (storeDetails == null) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_share),
|
||||
icon = Icons.Rounded.Share
|
||||
) {
|
||||
scope.launch {
|
||||
viewModel.shareApkFile(context)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SubmenuToolbarAction(
|
||||
label = stringResource(R.string.menu_share),
|
||||
icon = Icons.Rounded.Share,
|
||||
children = listOf(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_share_store_link, storeDetails.label),
|
||||
icon = Icons.Rounded.Link,
|
||||
action = {
|
||||
viewModel.shareStoreLink(context, storeDetails.url)
|
||||
}
|
||||
),
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_share_apk_file),
|
||||
icon = Icons.Rounded.Android
|
||||
) {
|
||||
scope.launch {
|
||||
viewModel.shareApkFile(context)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
toolbarActions.add(shareAction)
|
||||
|
||||
if (viewModel.canUninstall) {
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_uninstall),
|
||||
icon = Icons.Rounded.Delete,
|
||||
) {
|
||||
viewModel.uninstall(context)
|
||||
onBack()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||
val hideAction = if (isHidden) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_unhide),
|
||||
icon = Icons.Rounded.Visibility,
|
||||
action = {
|
||||
viewModel.unhide()
|
||||
onBack()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_hide),
|
||||
icon = Icons.Rounded.VisibilityOff,
|
||||
action = {
|
||||
viewModel.hide()
|
||||
onBack()
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.msg_item_hidden, app.label),
|
||||
actionLabel = context.getString(R.string.action_undo),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
viewModel.unhide()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toolbarActions.add(hideAction)
|
||||
|
||||
Toolbar(
|
||||
leftActions = listOf(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(id = R.string.menu_back),
|
||||
icon = Icons.Rounded.ArrowBack
|
||||
) {
|
||||
onBack()
|
||||
}
|
||||
),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppItemGridPopup(
|
||||
app: LauncherApp,
|
||||
show: Boolean,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = show,
|
||||
transitionSpec = {
|
||||
slideInHorizontally(
|
||||
tween(300),
|
||||
initialOffsetX = { -it + origin.width.roundToInt() }) with
|
||||
slideOutHorizontally(
|
||||
tween(300),
|
||||
targetOffsetX = { -it + origin.width.roundToInt() }) + fadeOut(snap(400)) using
|
||||
SizeTransform { _, _ ->
|
||||
tween(300)
|
||||
}
|
||||
}
|
||||
) { targetState ->
|
||||
if (targetState) {
|
||||
AppItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scale(
|
||||
1 - (1 - LocalGridSettings.current.iconSize / 84f) * (1 - animationProgress),
|
||||
transformOrigin = TransformOrigin(1f, 0f)
|
||||
)
|
||||
.offset(
|
||||
x = 16.dp * (1 - animationProgress).pow(10),
|
||||
y = -16.dp * (1 - animationProgress),
|
||||
),
|
||||
app = app,
|
||||
onBack = onDismiss
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidth(origin.width.toDp())
|
||||
.requiredHeight(origin.height.toDp())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.apps
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Process
|
||||
import android.service.notification.StatusBarNotification
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.notifications.NotificationRepository
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class AppItemVM(
|
||||
private val app: LauncherApp
|
||||
) : SearchableItemVM(app) {
|
||||
private val notificationRepository: NotificationRepository by inject()
|
||||
private val appShortcutRepository: AppShortcutRepository by inject()
|
||||
|
||||
|
||||
val notifications =
|
||||
notificationRepository.notifications.map { it.filter { it.packageName == app.`package` } }
|
||||
|
||||
fun clearNotification(notification: StatusBarNotification) {
|
||||
notificationRepository.cancelNotification(notification)
|
||||
}
|
||||
|
||||
fun openAppInfo(context: Context) {
|
||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||
|
||||
launcherApps.startAppDetailsActivity(
|
||||
ComponentName(app.`package`, app.activity),
|
||||
app.getUser(),
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun shareApkFile(context: Context) {
|
||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||
val fileCopy = java.io.File(
|
||||
context.cacheDir,
|
||||
"${app.`package`}-${app.version}.apk"
|
||||
)
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val user = (app as? LauncherApp)?.getUser()
|
||||
val info = if (user != null) {
|
||||
launcherApps.getApplicationInfo(app.`package`, 0, user)
|
||||
} else {
|
||||
context.packageManager.getApplicationInfo(app.`package`, 0)
|
||||
}
|
||||
val file = java.io.File(info.publicSourceDir)
|
||||
|
||||
try {
|
||||
file.copyTo(fileCopy, false)
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
// Do nothing. If the file is already there we don't have to copy it again.
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
context.applicationContext.packageName + ".fileprovider",
|
||||
fileCopy
|
||||
)
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
shareIntent.type = "application/vnd.android.package-archive"
|
||||
withContext(Dispatchers.Main) {
|
||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun shareStoreLink(context: Context, url: String) {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, url)
|
||||
shareIntent.type = "text/plain"
|
||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
|
||||
val canUninstall = app.flags and ApplicationInfo.FLAG_SYSTEM == 0 && (app as? LauncherApp)?.getUser() == Process.myUserHandle()
|
||||
|
||||
fun uninstall(context: Context) {
|
||||
val intent = Intent(Intent.ACTION_DELETE)
|
||||
intent.data = Uri.parse("package:" + app.`package`)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
fun openNotification(notification: StatusBarNotification) {
|
||||
try {
|
||||
notification.notification.contentIntent?.send()
|
||||
} catch (e: PendingIntent.CanceledException) {}
|
||||
}
|
||||
|
||||
fun getShortcutIcon(context: Context, shortcut: ShortcutInfo) : Drawable? {
|
||||
val launcherApps = context.getSystemService<LauncherApps>() ?: return null
|
||||
return launcherApps.getShortcutIconDrawable(shortcut, 0)
|
||||
}
|
||||
|
||||
val shortcuts = flow {
|
||||
emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5))
|
||||
}
|
||||
|
||||
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
|
||||
return favoritesRepository.isPinned(shortcut)
|
||||
}
|
||||
|
||||
fun pinShortcut(shortcut: AppShortcut) {
|
||||
favoritesRepository.pinItem(shortcut)
|
||||
}
|
||||
|
||||
fun unpinShortcut(shortcut: AppShortcut) {
|
||||
favoritesRepository.unpinItem(shortcut)
|
||||
}
|
||||
|
||||
fun launchShortcut(context: Context, shortcut: AppShortcut) {
|
||||
shortcut.launch(context, null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.apps
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
||||
|
||||
@Composable
|
||||
fun AppResults(reverse: Boolean = false) {
|
||||
val viewModel: SearchVM = viewModel()
|
||||
val apps by viewModel.appResults.observeAsState(emptyList())
|
||||
|
||||
if (apps.isNotEmpty()) {
|
||||
LauncherCard(
|
||||
modifier = Modifier
|
||||
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
|
||||
) {
|
||||
SearchResultGrid(items = apps, reverse = reverse)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.appshortcuts
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.AppShortcutResults(reverse: Boolean = false) {
|
||||
val viewModel: SearchVM = viewModel()
|
||||
val shortcuts by viewModel.appShortcutResults.observeAsState(emptyList())
|
||||
val context = LocalContext.current
|
||||
|
||||
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
|
||||
val missingPermission by viewModel.missingAppShortcutPermission.collectAsState(false)
|
||||
|
||||
AnimatedVisibility(shortcuts.isNotEmpty() || (!isSearchEmpty && missingPermission)) {
|
||||
LauncherCard(
|
||||
modifier = Modifier
|
||||
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(!isSearchEmpty && missingPermission) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_appshortcuts_search, stringResource(R.string.app_name)),
|
||||
onClick = { viewModel.requestAppShortcutPermission(context as AppCompatActivity) },
|
||||
modifier = Modifier.padding(16.dp),
|
||||
secondaryAction = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.disableAppShortcutSearch()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(R.string.turn_off),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
SearchResultList(
|
||||
items = shortcuts,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
reverse = reverse
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.calculator
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.search.data.Calculator
|
||||
|
||||
@Composable
|
||||
fun CalculatorItem(
|
||||
calculator: Calculator
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||
Text(
|
||||
text = calculator.getBeatifiedTerm(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "= ${calculator.formattedString}",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
)
|
||||
if (calculator.term.matches(Regex("(0x|0b)?[0-9]+"))) {
|
||||
Text(
|
||||
calculator.formattedBinaryString,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(top = 8.dp),
|
||||
)
|
||||
Text(
|
||||
calculator.formattedHexString,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
)
|
||||
Text(
|
||||
calculator.formattedOctString,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.calendar
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.Toolbar
|
||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalDarkTheme
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import kotlinx.coroutines.launch
|
||||
import palettes.TonalPalette
|
||||
|
||||
@Composable
|
||||
fun CalendarItem(
|
||||
modifier: Modifier = Modifier,
|
||||
calendar: CalendarEvent,
|
||||
showDetails: Boolean,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val viewModel = remember(calendar.key) { CalendarItemVM(calendar) }
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
val darkMode = LocalDarkTheme.current
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.drawBehind {
|
||||
val color = TonalPalette.fromInt(calendar.color).tone(
|
||||
if (darkMode) 80 else 40
|
||||
)
|
||||
drawRect(Color(color), Offset.Zero, this.size.copy(width = 8.dp.toPx()))
|
||||
}
|
||||
.padding(start = 8.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Row {
|
||||
val padding by animateDpAsState(if (showDetails) 16.dp else 12.dp)
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
top = padding,
|
||||
start = padding,
|
||||
bottom = 12.dp,
|
||||
end = padding
|
||||
)
|
||||
) {
|
||||
val textStyle by animateTextStyleAsState(
|
||||
if (showDetails) MaterialTheme.typography.titleMedium
|
||||
else MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(text = calendar.labelOverride ?: calendar.label, style = textStyle)
|
||||
AnimatedVisibility(!showDetails) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
text = viewModel.getSummary(context),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(showDetails) {
|
||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
||||
if (tags.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
||||
text = tags.joinToString(separator = " #", prefix = "#"),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(showDetails) {
|
||||
Column {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
imageVector = Icons.Rounded.Schedule,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = viewModel.formatTime(context),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
if (calendar.description.isNotBlank()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
imageVector = Icons.Rounded.Notes,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = calendar.description,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
if (calendar.attendees.isNotEmpty()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
imageVector = Icons.Rounded.People,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = calendar.attendees.joinToString(),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
if (calendar.location.isNotBlank()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
viewModel.openLocation(context)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
imageVector = Icons.Rounded.Place,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = calendar.location,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||
val hideAction = if (isHidden) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_unhide),
|
||||
icon = Icons.Rounded.Visibility,
|
||||
action = {
|
||||
viewModel.unhide()
|
||||
onBack()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_hide),
|
||||
icon = Icons.Rounded.VisibilityOff,
|
||||
action = {
|
||||
viewModel.hide()
|
||||
onBack()
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.msg_item_hidden, calendar.label),
|
||||
actionLabel = context.getString(R.string.action_undo),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
if(result == SnackbarResult.ActionPerformed) {
|
||||
viewModel.unhide()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_calendar_open_externally),
|
||||
icon = Icons.Rounded.OpenInNew,
|
||||
action = {
|
||||
viewModel.launch(context)
|
||||
onBack()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(calendar) }
|
||||
))
|
||||
|
||||
toolbarActions.add(hideAction)
|
||||
|
||||
Toolbar(
|
||||
leftActions = listOf(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(id = R.string.menu_back),
|
||||
icon = Icons.Rounded.ArrowBack
|
||||
) {
|
||||
onBack()
|
||||
}
|
||||
),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CalendarItemGridPopup(
|
||||
calendar: CalendarEvent,
|
||||
show: Boolean,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = show,
|
||||
transitionSpec = {
|
||||
fadeIn(snap()) with
|
||||
fadeOut(snap(400)) using
|
||||
SizeTransform { _, _ ->
|
||||
tween(300)
|
||||
}
|
||||
}
|
||||
) { targetState ->
|
||||
if (targetState) {
|
||||
CalendarItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(calendar.color).copy(alpha = 1f - animationProgress)),
|
||||
calendar = calendar,
|
||||
showDetails = true,
|
||||
onBack = onDismiss
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidth(origin.width.toDp())
|
||||
.requiredHeight(origin.height.toDp())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.calendar
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.text.format.DateUtils
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
import java.net.URLEncoder
|
||||
|
||||
class CalendarItemVM(
|
||||
private val calendarEvent: CalendarEvent
|
||||
) : SearchableItemVM(calendarEvent) {
|
||||
|
||||
fun getSummary(context: Context): String {
|
||||
val isToday =
|
||||
DateUtils.isToday(calendarEvent.startTime) && DateUtils.isToday(calendarEvent.endTime)
|
||||
return if (isToday) {
|
||||
if (calendarEvent.allDay) {
|
||||
context.getString(R.string.calendar_event_allday)
|
||||
} else {
|
||||
DateUtils.formatDateRange(
|
||||
context,
|
||||
calendarEvent.startTime,
|
||||
calendarEvent.endTime,
|
||||
DateUtils.FORMAT_SHOW_TIME
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (calendarEvent.allDay) {
|
||||
DateUtils.formatDateRange(
|
||||
context,
|
||||
calendarEvent.startTime,
|
||||
calendarEvent.endTime,
|
||||
DateUtils.FORMAT_SHOW_DATE
|
||||
)
|
||||
} else {
|
||||
DateUtils.formatDateRange(
|
||||
context,
|
||||
calendarEvent.startTime,
|
||||
calendarEvent.endTime,
|
||||
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatTime(context: Context): String {
|
||||
if (calendarEvent.allDay) return DateUtils.formatDateRange(
|
||||
context,
|
||||
calendarEvent.startTime,
|
||||
calendarEvent.endTime,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
)
|
||||
return DateUtils.formatDateRange(
|
||||
context,
|
||||
calendarEvent.startTime,
|
||||
calendarEvent.endTime,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
fun openLocation(context: Context) {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(
|
||||
Uri.parse(
|
||||
"geo:0,0?q=${
|
||||
URLEncoder.encode(
|
||||
calendarEvent.location,
|
||||
"utf8"
|
||||
)
|
||||
}"
|
||||
)
|
||||
)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.calendar
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.CalendarResults(reverse: Boolean = false) {
|
||||
val viewModel: SearchVM = viewModel()
|
||||
val calendarEvents by viewModel.calendarResults.observeAsState(emptyList())
|
||||
val context = LocalContext.current
|
||||
|
||||
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
|
||||
val missingPermission by viewModel.missingCalendarPermission.collectAsState(false)
|
||||
AnimatedVisibility(calendarEvents.isNotEmpty() || (!isSearchEmpty && missingPermission)) {
|
||||
LauncherCard(
|
||||
modifier = Modifier
|
||||
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(!isSearchEmpty && missingPermission) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_calendar_search),
|
||||
onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity) },
|
||||
modifier = Modifier.padding(16.dp),
|
||||
secondaryAction = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.disableCalendarSearch()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(R.string.turn_off),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SearchResultList(
|
||||
items = calendarEvents,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
reverse = reverse
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.common
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import de.mm20.launcher2.badges.BadgeRepository
|
||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||
import de.mm20.launcher2.icons.IconRepository
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
abstract class SearchableItemVM(
|
||||
private val searchable: SavableSearchable
|
||||
) : KoinComponent {
|
||||
protected val favoritesRepository: FavoritesRepository by inject()
|
||||
protected val badgeRepository: BadgeRepository by inject()
|
||||
protected val iconRepository: IconRepository by inject()
|
||||
protected val customAttributesRepository: CustomAttributesRepository by inject()
|
||||
|
||||
val isPinned = favoritesRepository.isPinned(searchable)
|
||||
fun pin() {
|
||||
favoritesRepository.pinItem(searchable)
|
||||
}
|
||||
|
||||
fun unpin() {
|
||||
favoritesRepository.unpinItem(searchable)
|
||||
}
|
||||
|
||||
val isHidden = favoritesRepository.isHidden(searchable)
|
||||
fun hide() {
|
||||
favoritesRepository.hideItem(searchable)
|
||||
}
|
||||
|
||||
fun unhide() {
|
||||
favoritesRepository.unhideItem(searchable)
|
||||
}
|
||||
|
||||
val badge = badgeRepository.getBadge(searchable)
|
||||
|
||||
fun getIcon(size: Int): Flow<LauncherIcon> {
|
||||
return iconRepository.getIcon(searchable, size)
|
||||
}
|
||||
|
||||
fun getTags(): Flow<List<String>> {
|
||||
return customAttributesRepository.getTags(searchable)
|
||||
}
|
||||
|
||||
|
||||
open fun launch(context: Context, bounds: Rect? = null): Boolean {
|
||||
val view = (context as? AppCompatActivity)?.window?.decorView
|
||||
val options = if (bounds != null && view != null) {
|
||||
ActivityOptionsCompat.makeClipRevealAnimation(
|
||||
view,
|
||||
bounds.left.toInt(),
|
||||
bounds.top.toInt(),
|
||||
bounds.width.toInt(),
|
||||
bounds.height.toInt()
|
||||
)
|
||||
} else {
|
||||
ActivityOptionsCompat.makeBasic()
|
||||
}
|
||||
val bundle = options.toBundle()
|
||||
if (isAtLeastApiLevel(31)) {
|
||||
bundle?.putInt("android.activity.splashScreenStyle", 1)
|
||||
}
|
||||
if (searchable.launch(context, bundle)) {
|
||||
favoritesRepository.incrementLaunchCounter(searchable)
|
||||
return true
|
||||
} else if (searchable is LauncherApp || searchable is AppShortcut) {
|
||||
favoritesRepository.remove(searchable)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.common.grid
|
||||
|
||||
import android.content.ComponentName
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.data.*
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.search.apps.AppItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.contacts.ContactItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.files.FileItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.shortcut.ShortcutItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.transitions.HandleHomeTransition
|
||||
import de.mm20.launcher2.ui.launcher.transitions.HomeTransitionParams
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalWindowPosition
|
||||
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
@Composable
|
||||
fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels: Boolean = true) {
|
||||
val viewModel = remember(item.key) { GridItemVM(item) }
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
var showPopup by remember(item.key) { mutableStateOf(false) }
|
||||
var bounds by remember { mutableStateOf(Rect.Zero) }
|
||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
val badge by remember(item.key) { viewModel.badge }.collectAsState(null)
|
||||
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
|
||||
val icon by remember(item.key) { viewModel.getIcon(iconSize.toInt()) }.collectAsState(null)
|
||||
|
||||
val launchOnPress = !item.preferDetailsOverLaunch
|
||||
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
if (item is LauncherApp) {
|
||||
HandleHomeTransition {
|
||||
val cn = ComponentName(item.`package`, item.activity)
|
||||
if (
|
||||
it.componentName == cn &&
|
||||
bounds.right > 0f && bounds.left < windowSize.width &&
|
||||
bounds.bottom > 0f && bounds.top < windowSize.height
|
||||
) {
|
||||
return@HandleHomeTransition HomeTransitionParams(
|
||||
bounds
|
||||
) { _, _ ->
|
||||
ShapedLauncherIcon(size = LocalGridSettings.current.iconSize.dp, icon = { icon })
|
||||
}
|
||||
}
|
||||
return@HandleHomeTransition null
|
||||
}
|
||||
}
|
||||
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
ShapedLauncherIcon(
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned {
|
||||
bounds = it.boundsInWindow()
|
||||
},
|
||||
size = LocalGridSettings.current.iconSize.dp,
|
||||
badge = { badge },
|
||||
icon = { icon },
|
||||
onClick = {
|
||||
if (!launchOnPress || !viewModel.launch(context, bounds)) {
|
||||
showPopup = true
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
showPopup = true
|
||||
}
|
||||
)
|
||||
if (showLabels) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
text = item.labelOverride ?: item.label,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (showPopup) {
|
||||
ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit) {
|
||||
var show by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(null) {
|
||||
show = true
|
||||
}
|
||||
LaunchedEffect(show) {
|
||||
if (!show) {
|
||||
delay(300L)
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
BackHandler {
|
||||
show = false
|
||||
}
|
||||
|
||||
val animationProgress by animateFloatAsState(if (show) 1f else 0f, tween(300))
|
||||
Popup(
|
||||
properties = PopupProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true
|
||||
),
|
||||
alignment = Alignment.TopCenter,
|
||||
onDismissRequest = {
|
||||
show = false
|
||||
},
|
||||
offset = IntOffset(-origin.left.toInt(), 0)
|
||||
) {
|
||||
CompositionLocalProvider(LocalWindowPosition provides origin.top) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
LauncherCard(
|
||||
elevation = 8.dp * animationProgress,
|
||||
backgroundOpacity = 1f,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.absoluteOffset(
|
||||
x = ((1 - animationProgress) * origin.left).toDp() - 16.dp * (1 - animationProgress),
|
||||
)
|
||||
.wrapContentSize()
|
||||
) {
|
||||
when (searchable) {
|
||||
is LauncherApp -> {
|
||||
AppItemGridPopup(
|
||||
app = searchable,
|
||||
show = show,
|
||||
animationProgress = animationProgress,
|
||||
origin = origin,
|
||||
onDismiss = {
|
||||
show = false
|
||||
}
|
||||
)
|
||||
}
|
||||
is Website -> {
|
||||
WebsiteItemGridPopup(
|
||||
website = searchable,
|
||||
show = show,
|
||||
animationProgress = animationProgress,
|
||||
origin = origin,
|
||||
onDismiss = {
|
||||
show = false
|
||||
}
|
||||
)
|
||||
}
|
||||
is Wikipedia -> {
|
||||
WikipediaItemGridPopup(
|
||||
wikipedia = searchable,
|
||||
show = show,
|
||||
animationProgress = animationProgress,
|
||||
origin = origin,
|
||||
onDismiss = {
|
||||
show = false
|
||||
}
|
||||
)
|
||||
}
|
||||
is Contact -> {
|
||||
ContactItemGridPopup(
|
||||
contact = searchable,
|
||||
show = show,
|
||||
animationProgress = animationProgress,
|
||||
origin = origin,
|
||||
onDismiss = {
|
||||
show = false
|
||||
}
|
||||
)
|
||||
}
|
||||
is File -> {
|
||||
FileItemGridPopup(
|
||||
file = searchable,
|
||||
show = show,
|
||||
animationProgress = animationProgress,
|
||||
origin = origin,
|
||||
onDismiss = {
|
||||
show = false
|
||||
}
|
||||
)
|
||||
}
|
||||
is CalendarEvent -> {
|
||||
CalendarItemGridPopup(
|
||||
calendar = searchable,
|
||||
show = show,
|
||||
animationProgress = animationProgress,
|
||||
origin = origin,
|
||||
onDismiss = {
|
||||
show = false
|
||||
}
|
||||
)
|
||||
}
|
||||
is AppShortcut -> {
|
||||
ShortcutItemGridPopup(
|
||||
shortcut = searchable,
|
||||
show = show,
|
||||
animationProgress = animationProgress,
|
||||
origin = origin,
|
||||
onDismiss = {
|
||||
show = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.common.grid
|
||||
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
|
||||
class GridItemVM(
|
||||
searchable: SavableSearchable
|
||||
): SearchableItemVM(searchable)
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.common.grid
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import kotlin.math.ceil
|
||||
|
||||
@Composable
|
||||
fun SearchResultGrid(
|
||||
items: List<SavableSearchable>,
|
||||
modifier: Modifier = Modifier,
|
||||
showLabels: Boolean = LocalGridSettings.current.showLabels,
|
||||
columns: Int = LocalGridSettings.current.columnCount,
|
||||
reverse: Boolean = false
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
|
||||
) {
|
||||
for (i in 0 until ceil(items.size / columns.toFloat()).toInt()) {
|
||||
Row {
|
||||
for (j in 0 until columns) {
|
||||
val item = items.getOrNull(i * columns + j)
|
||||
if (item != null) {
|
||||
GridItem(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(4.dp, 8.dp),
|
||||
item = item,
|
||||
showLabels = showLabels
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.common.list
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.*
|
||||
import de.mm20.launcher2.ui.component.InnerCard
|
||||
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
|
||||
import de.mm20.launcher2.ui.launcher.search.contacts.ContactItem
|
||||
import de.mm20.launcher2.ui.launcher.search.files.FileItem
|
||||
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
|
||||
|
||||
@Composable
|
||||
fun ListItem(modifier: Modifier = Modifier, item: SavableSearchable) {
|
||||
var showDetails by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val viewModel = remember(item.key) { ListItemVM(item) }
|
||||
|
||||
var bounds by remember { mutableStateOf(Rect.Zero) }
|
||||
InnerCard(
|
||||
modifier = modifier
|
||||
.onGloballyPositioned {
|
||||
bounds = it.boundsInWindow()
|
||||
},
|
||||
raised = showDetails
|
||||
) {
|
||||
when (item) {
|
||||
is Contact -> {
|
||||
ContactItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
enabled = !showDetails,
|
||||
onClick = { showDetails = true },
|
||||
onLongClick = { showDetails = true }
|
||||
),
|
||||
contact = item,
|
||||
showDetails = showDetails,
|
||||
onBack = { showDetails = false }
|
||||
)
|
||||
}
|
||||
is File -> {
|
||||
FileItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
enabled = !showDetails,
|
||||
onClick = {
|
||||
if (!viewModel.launch(context, bounds)) {
|
||||
showDetails = true
|
||||
}
|
||||
},
|
||||
onLongClick = { showDetails = true }
|
||||
),
|
||||
file = item,
|
||||
showDetails = showDetails,
|
||||
onBack = { showDetails = false }
|
||||
)
|
||||
}
|
||||
is CalendarEvent -> {
|
||||
CalendarItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
enabled = !showDetails,
|
||||
onClick = { showDetails = true },
|
||||
onLongClick = { showDetails = true }
|
||||
),
|
||||
calendar = item,
|
||||
showDetails = showDetails,
|
||||
onBack = { showDetails = false }
|
||||
)
|
||||
}
|
||||
is AppShortcut -> {
|
||||
AppShortcutItem(
|
||||
shortcut = item,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
enabled = !showDetails,
|
||||
onClick = {
|
||||
if (!viewModel.launch(context, bounds)) {
|
||||
showDetails = true
|
||||
}
|
||||
},
|
||||
onLongClick = { showDetails = true }
|
||||
),
|
||||
showDetails = showDetails,
|
||||
onBack = { showDetails = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.common.list
|
||||
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
|
||||
class ListItemVM(
|
||||
searchable: SavableSearchable
|
||||
): SearchableItemVM(searchable)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.common.list
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||
|
||||
@Composable
|
||||
fun SearchResultList(
|
||||
items: List<SavableSearchable>,
|
||||
modifier: Modifier = Modifier,
|
||||
reverse: Boolean = false
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
|
||||
) {
|
||||
for (item in items) {
|
||||
key(item.key) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp),
|
||||
item = item
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.contacts
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.*
|
||||
import de.mm20.launcher2.ui.icons.Telegram
|
||||
import de.mm20.launcher2.ui.icons.WhatsApp
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import de.mm20.launcher2.ui.modifier.scale
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ContactItem(
|
||||
modifier: Modifier = Modifier,
|
||||
contact: Contact,
|
||||
showDetails: Boolean,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val viewModel = remember(contact) { ContactItemVM(contact) }
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
val transition = updateTransition(showDetails, label = "ContactItem")
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val iconSize = 48.dp.toPixels().toInt()
|
||||
val icon by remember(contact) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
||||
val padding by transition.animateDp(label = "iconPadding") {
|
||||
if (it) 16.dp else 8.dp
|
||||
}
|
||||
ShapedLauncherIcon(
|
||||
size = 48.dp,
|
||||
modifier = Modifier
|
||||
.padding(start = padding, top = padding, bottom = padding),
|
||||
icon = { icon },
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
val textStyle by animateTextStyleAsState(
|
||||
if (showDetails) MaterialTheme.typography.titleLarge
|
||||
else MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
text = contact.labelOverride ?: contact.label,
|
||||
style = textStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
AnimatedVisibility(!showDetails) {
|
||||
Text(
|
||||
contact.summary,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(showDetails) {
|
||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
||||
if (tags.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 1.dp),
|
||||
text = tags.joinToString(separator = " #", prefix = "#"),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(showDetails) {
|
||||
Column {
|
||||
|
||||
if (contact.phones.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Call, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.phones.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
viewModel.contact(context, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.emails.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Email, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.emails.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
viewModel.contact(context, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.telegram.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Telegram, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.telegram.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
viewModel.contact(context, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.whatsapp.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.WhatsApp, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.whatsapp.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
viewModel.contact(context, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.postals.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Place, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.postals.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
viewModel.contact(context, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||
val hideAction = if (isHidden) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_unhide),
|
||||
icon = Icons.Rounded.Visibility,
|
||||
action = {
|
||||
viewModel.unhide()
|
||||
onBack()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_hide),
|
||||
icon = Icons.Rounded.VisibilityOff,
|
||||
action = {
|
||||
viewModel.hide()
|
||||
onBack()
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.msg_item_hidden, contact.label),
|
||||
actionLabel = context.getString(R.string.action_undo),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
if(result == SnackbarResult.ActionPerformed) {
|
||||
viewModel.unhide()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_contacts_open_externally),
|
||||
icon = Icons.Rounded.OpenInNew,
|
||||
action = {
|
||||
viewModel.launch(context)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(contact) }
|
||||
))
|
||||
|
||||
toolbarActions.add(hideAction)
|
||||
|
||||
Toolbar(
|
||||
leftActions = listOf(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(id = R.string.menu_back),
|
||||
icon = Icons.Rounded.ArrowBack
|
||||
) {
|
||||
onBack()
|
||||
}
|
||||
),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactItemGridPopup(
|
||||
contact: Contact,
|
||||
show: Boolean,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = show,
|
||||
transitionSpec = {
|
||||
fadeIn(snap()) with
|
||||
fadeOut(snap(400)) using
|
||||
SizeTransform { _, _ ->
|
||||
tween(300)
|
||||
}
|
||||
}
|
||||
) { targetState ->
|
||||
if (targetState) {
|
||||
ContactItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scale(
|
||||
1 - (1 - LocalGridSettings.current.iconSize / 48f) * (1 - animationProgress),
|
||||
transformOrigin = TransformOrigin(0f, 0f)
|
||||
)
|
||||
.offset(
|
||||
x = -16.dp * (1 - animationProgress),
|
||||
y = -16.dp * (1 - animationProgress)
|
||||
),
|
||||
contact = contact,
|
||||
showDetails = true,
|
||||
onBack = onDismiss
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidth(origin.width.toDp())
|
||||
.requiredHeight(origin.height.toDp())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.contacts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.data.ContactInfo
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
|
||||
class ContactItemVM(
|
||||
val contact: Contact
|
||||
) : SearchableItemVM(contact) {
|
||||
fun contact(context: Context, contactInfo: ContactInfo) {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse(contactInfo.data))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.contacts
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.ContactResults(reverse: Boolean = false) {
|
||||
val viewModel: SearchVM = viewModel()
|
||||
val context = LocalContext.current
|
||||
val contacts by viewModel.contactResults.observeAsState(emptyList())
|
||||
|
||||
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
|
||||
val missingPermission by viewModel.missingContactsPermission.collectAsState(false)
|
||||
AnimatedVisibility(contacts.isNotEmpty() || (!isSearchEmpty && missingPermission)) {
|
||||
LauncherCard(
|
||||
modifier = Modifier
|
||||
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(!isSearchEmpty && missingPermission) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_contact_search),
|
||||
onClick = { viewModel.requestContactsPermission(context as AppCompatActivity) },
|
||||
modifier = Modifier.padding(16.dp),
|
||||
secondaryAction = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.disableContactsSearch()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(R.string.turn_off),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
SearchResultList(
|
||||
items = contacts,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
reverse = reverse
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.favorites
|
||||
|
||||
import de.mm20.launcher2.ui.common.FavoritesVM
|
||||
|
||||
class SearchFavoritesVM: FavoritesVM()
|
||||
@@ -0,0 +1,344 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.files
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.component.Toolbar
|
||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import de.mm20.launcher2.ui.modifier.scale
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun FileItem(
|
||||
modifier: Modifier = Modifier,
|
||||
file: File,
|
||||
showDetails: Boolean,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val viewModel = remember(file.key) { FileItemVM(file) }
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
val transition = updateTransition(showDetails, label = "ContactItem")
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
val textStyle by animateTextStyleAsState(
|
||||
if (showDetails) MaterialTheme.typography.titleMedium
|
||||
else MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
text = file.labelOverride ?: file.label,
|
||||
style = textStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
AnimatedVisibility(!showDetails) {
|
||||
Text(
|
||||
file.getFileType(context),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 2.dp)
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(showDetails) {
|
||||
Column {
|
||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
||||
if (tags.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
||||
text = tags.joinToString(separator = " #", prefix = "#"),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.file_meta_type,
|
||||
file.mimeType
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
for ((k, v) in file.metaData) {
|
||||
Text(
|
||||
text = stringResource(k, v),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.file_meta_path,
|
||||
file.path
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
if (!file.isDirectory) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.file_meta_size,
|
||||
formatFileSize(file.size)
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val iconSize = 48.dp.toPixels().toInt()
|
||||
val icon by remember(file) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
||||
val badge by viewModel.badge.collectAsState(null)
|
||||
val padding by transition.animateDp(label = "iconPadding") {
|
||||
if (it) 16.dp else 8.dp
|
||||
}
|
||||
ShapedLauncherIcon(
|
||||
size = 48.dp,
|
||||
modifier = Modifier
|
||||
.padding(end = padding, top = padding, bottom = padding),
|
||||
icon = { icon },
|
||||
badge = { badge }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
AnimatedVisibility(showDetails) {
|
||||
Column {
|
||||
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_open_file),
|
||||
icon = Icons.Rounded.OpenInNew,
|
||||
action = {
|
||||
viewModel.launch(context)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (viewModel.canShare) {
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_share),
|
||||
icon = Icons.Rounded.Share,
|
||||
action = {
|
||||
viewModel.share(context)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
if (viewModel.canDelete) {
|
||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_delete),
|
||||
icon = Icons.Rounded.Delete,
|
||||
action = {
|
||||
showConfirmDialog = true
|
||||
}
|
||||
))
|
||||
if (showConfirmDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showConfirmDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.delete()
|
||||
showConfirmDialog = false
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
showConfirmDialog = false
|
||||
}) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
if (file.isDirectory) stringResource(
|
||||
R.string.alert_delete_directory,
|
||||
file.label
|
||||
)
|
||||
else stringResource(R.string.alert_delete_file, file.label)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(file) }
|
||||
))
|
||||
|
||||
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||
val hideAction = if (isHidden) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_unhide),
|
||||
icon = Icons.Rounded.Visibility,
|
||||
action = {
|
||||
viewModel.unhide()
|
||||
onBack()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_hide),
|
||||
icon = Icons.Rounded.VisibilityOff,
|
||||
action = {
|
||||
viewModel.hide()
|
||||
onBack()
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.msg_item_hidden, file.label),
|
||||
actionLabel = context.getString(R.string.action_undo),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
if(result == SnackbarResult.ActionPerformed) {
|
||||
viewModel.unhide()
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
toolbarActions.add(hideAction)
|
||||
|
||||
Toolbar(
|
||||
leftActions = listOf(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(id = R.string.menu_back),
|
||||
icon = Icons.Rounded.ArrowBack
|
||||
) {
|
||||
onBack()
|
||||
}
|
||||
),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FileItemGridPopup(
|
||||
file: File,
|
||||
show: Boolean,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = show,
|
||||
transitionSpec = {
|
||||
slideInHorizontally(
|
||||
tween(300),
|
||||
initialOffsetX = { -it + origin.width.roundToInt() }) with
|
||||
slideOutHorizontally(
|
||||
tween(300),
|
||||
targetOffsetX = { -it + origin.width.roundToInt() }) + fadeOut(snap(400)) using
|
||||
SizeTransform { _, _ ->
|
||||
tween(300)
|
||||
}
|
||||
}
|
||||
) { targetState ->
|
||||
if (targetState) {
|
||||
FileItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scale(
|
||||
1 - (1 - LocalGridSettings.current.iconSize / 48f) * (1 - animationProgress),
|
||||
transformOrigin = TransformOrigin(1f, 0f)
|
||||
)
|
||||
.offset(
|
||||
x = 16.dp * (1 - animationProgress),
|
||||
y = -16.dp * (1 - animationProgress)
|
||||
),
|
||||
file = file,
|
||||
showDetails = true,
|
||||
onBack = onDismiss
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidth(origin.width.toDp())
|
||||
.requiredHeight(origin.height.toDp())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatFileSize(size: Long): String {
|
||||
return when {
|
||||
size < 1000L -> "$size Bytes"
|
||||
size < 1000000L -> "${DecimalFormat("#,##0.#").format(size / 1000.0)} kB"
|
||||
size < 1000000000L -> "${DecimalFormat("#,##0.#").format(size / 1000000.0)} MB"
|
||||
size < 1000000000000L -> "${DecimalFormat("#,##0.#").format(size / 1000000000.0)} GB"
|
||||
else -> "${DecimalFormat("#,##0.#").format(size / 1000000000000.0)} TB"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.files
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.FileProvider
|
||||
import de.mm20.launcher2.files.FileRepository
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.LocalFile
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class FileItemVM(
|
||||
private val file: File
|
||||
) : SearchableItemVM(file) {
|
||||
private val fileRepository: FileRepository by inject()
|
||||
|
||||
val canShare = file is LocalFile
|
||||
val canDelete = file.isDeletable
|
||||
|
||||
fun delete() {
|
||||
fileRepository.deleteFile(file)
|
||||
}
|
||||
|
||||
fun share(context: Context) {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
context.applicationContext.packageName + ".fileprovider",
|
||||
java.io.File(file.path)
|
||||
)
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
shareIntent.type = file.mimeType
|
||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.files
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.FileResults(reverse: Boolean = false) {
|
||||
val viewModel: SearchVM = viewModel()
|
||||
val files by viewModel.fileResults.observeAsState(emptyList())
|
||||
val context = LocalContext.current
|
||||
|
||||
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
|
||||
val missingPermission by viewModel.missingFilesPermission.collectAsState(false)
|
||||
AnimatedVisibility(files.isNotEmpty() || (!isSearchEmpty && missingPermission)) {
|
||||
LauncherCard(
|
||||
modifier = Modifier
|
||||
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Column {
|
||||
AnimatedVisibility(!isSearchEmpty && missingPermission) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_files_search),
|
||||
onClick = { viewModel.requestFilesPermission(context as AppCompatActivity) },
|
||||
modifier = Modifier.padding(16.dp),
|
||||
secondaryAction = {
|
||||
OutlinedButton(onClick = {
|
||||
viewModel.disableFilesSearch()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(R.string.turn_off),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
SearchResultList(
|
||||
items = files,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
reverse = reverse
|
||||
)}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.shortcut
|
||||
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.component.Toolbar
|
||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import de.mm20.launcher2.ui.modifier.scale
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun AppShortcutItem(
|
||||
modifier: Modifier = Modifier,
|
||||
shortcut: AppShortcut,
|
||||
showDetails: Boolean = false,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val viewModel = remember { ShortcutItemVM(shortcut) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
var requestDelete by remember { mutableStateOf(false) }
|
||||
|
||||
val transition = updateTransition(showDetails, label = "AppShortcutItem")
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Row {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
val titleStyle by animateTextStyleAsState(if (showDetails) MaterialTheme.typography.titleMedium else MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
text = shortcut.labelOverride ?: shortcut.label,
|
||||
style = titleStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
AnimatedVisibility(showDetails) {
|
||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
||||
if (tags.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 2.dp),
|
||||
text = tags.joinToString(separator = " #", prefix = "#"),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
val textSpace by transition.animateDp(label = "textSpace") {
|
||||
if (it) 4.dp else 2.dp
|
||||
}
|
||||
shortcut.appName?.let {
|
||||
Text(
|
||||
text = stringResource(R.string.shortcut_summary, it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(top = textSpace),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
val badge by viewModel.badge.collectAsState(null)
|
||||
val size by animateDpAsState(if (showDetails) 84.dp else 48.dp)
|
||||
val iconSize = 84.dp.toPixels().toInt()
|
||||
val icon by remember(shortcut.key) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
||||
|
||||
val padding by transition.animateDp(label = "iconPadding") {
|
||||
if (it) 16.dp else 8.dp
|
||||
}
|
||||
ShapedLauncherIcon(
|
||||
size = size,
|
||||
modifier = Modifier
|
||||
.padding(padding),
|
||||
badge = { badge },
|
||||
icon = { icon },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
AnimatedVisibility(showDetails) {
|
||||
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_app_info),
|
||||
icon = Icons.Rounded.Info
|
||||
) {
|
||||
viewModel.openAppInfo(context)
|
||||
})
|
||||
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(shortcut) }
|
||||
))
|
||||
|
||||
if (viewModel.canDelete) {
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_delete),
|
||||
icon = Icons.Rounded.Delete,
|
||||
action = { requestDelete = true }
|
||||
))
|
||||
}
|
||||
|
||||
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||
val hideAction = if (isHidden) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_unhide),
|
||||
icon = Icons.Rounded.Visibility,
|
||||
action = {
|
||||
viewModel.unhide()
|
||||
onBack()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_hide),
|
||||
icon = Icons.Rounded.VisibilityOff,
|
||||
action = {
|
||||
viewModel.hide()
|
||||
onBack()
|
||||
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = context.getString(
|
||||
R.string.msg_item_hidden,
|
||||
shortcut.label
|
||||
),
|
||||
actionLabel = context.getString(R.string.action_undo),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
viewModel.unhide()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
toolbarActions.add(hideAction)
|
||||
|
||||
Toolbar(
|
||||
leftActions = listOf(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(id = R.string.menu_back),
|
||||
icon = Icons.Rounded.ArrowBack
|
||||
) {
|
||||
onBack()
|
||||
}
|
||||
),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (requestDelete) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { requestDelete = false },
|
||||
text = { Text(stringResource(R.string.alert_delete_shortcut, shortcut.label)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.deleteShortcut()
|
||||
requestDelete = false
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
requestDelete = false
|
||||
}) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShortcutItemGridPopup(
|
||||
shortcut: AppShortcut,
|
||||
show: Boolean,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = show,
|
||||
transitionSpec = {
|
||||
slideInHorizontally(
|
||||
tween(300),
|
||||
initialOffsetX = { -it + origin.width.roundToInt() }) with
|
||||
slideOutHorizontally(
|
||||
tween(300),
|
||||
targetOffsetX = { -it + origin.width.roundToInt() }) + fadeOut(snap(400)) using
|
||||
SizeTransform { _, _ ->
|
||||
tween(300)
|
||||
}
|
||||
}
|
||||
) { targetState ->
|
||||
if (targetState) {
|
||||
AppShortcutItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.scale(
|
||||
1 - (1 - LocalGridSettings.current.iconSize / 84f) * (1 - animationProgress),
|
||||
transformOrigin = TransformOrigin(1f, 0f)
|
||||
)
|
||||
.offset(
|
||||
x = 16.dp * (1 - animationProgress).pow(10),
|
||||
y = -16.dp * (1 - animationProgress),
|
||||
),
|
||||
shortcut = shortcut,
|
||||
showDetails = true,
|
||||
onBack = onDismiss
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidth(origin.width.toDp())
|
||||
.requiredHeight(origin.height.toDp())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.shortcut
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(shortcut), KoinComponent {
|
||||
|
||||
private val shortcutRepository: AppShortcutRepository by inject()
|
||||
|
||||
val canDelete = shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned
|
||||
|
||||
fun openAppInfo(context: Context) {
|
||||
val packageName = when(shortcut) {
|
||||
is LegacyShortcut -> shortcut.intent.`package` ?: return
|
||||
is LauncherShortcut -> shortcut.launcherShortcut.`package`
|
||||
else -> return
|
||||
}
|
||||
context.tryStartActivity(
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.parse("package:$packageName")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteShortcut() {
|
||||
if (!canDelete) return
|
||||
if (shortcut is LauncherShortcut) shortcutRepository.removePinnedShortcut(shortcut)
|
||||
favoritesRepository.unpinItem(shortcut)
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.unitconverter
|
||||
|
||||
import android.icu.text.DateFormat
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.search.data.CurrencyUnitConverter
|
||||
import de.mm20.launcher2.search.data.UnitConverter
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.unitconverter.Dimension
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun UnitConverterItem(
|
||||
unitConverter: UnitConverter,
|
||||
) {
|
||||
var showAll by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = unitConverter.inputValue.let { "${it.formattedValue} ${it.formattedName}" },
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false
|
||||
)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.size(48.dp),
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = getDimensionIcon(unitConverter.dimension),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
Column {
|
||||
for ((i, unit) in unitConverter.values.withIndex()) {
|
||||
if (!showAll && i >= 5) break
|
||||
Text(
|
||||
text = unit.formattedValue,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, bottom = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Column {
|
||||
for ((i, unit) in unitConverter.values.withIndex()) {
|
||||
if (!showAll && i >= 5) break
|
||||
Text(
|
||||
text = "${unit.formattedName} (${unit.symbol})",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp, bottom = 12.dp, start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
var showDisclaimer by remember { mutableStateOf(false) }
|
||||
(unitConverter as? CurrencyUnitConverter)?.let {
|
||||
val df = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
Text(
|
||||
text = "${df.format(Date(it.updateTimestamp))} • ",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.disclaimer),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.clickable {
|
||||
showDisclaimer = true
|
||||
}
|
||||
)
|
||||
if (showDisclaimer) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDisclaimer = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showDisclaimer = false }) {
|
||||
Text(text = stringResource(id = R.string.close))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(id = R.string.disclaimer)) },
|
||||
text = { Text(stringResource(id = R.string.disclaimer_currency_converter, df.format(Date(it.updateTimestamp)))) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!showAll && unitConverter.values.size > 5) {
|
||||
TextButton(
|
||||
onClick = { showAll = true },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.unit_converter_show_all),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getDimensionIcon(dimension: Dimension): ImageVector {
|
||||
return when (dimension) {
|
||||
Dimension.Mass -> Icons.Rounded.FitnessCenter
|
||||
Dimension.Length -> Icons.Rounded.Straighten
|
||||
Dimension.Velocity -> Icons.Rounded.Speed
|
||||
Dimension.Volume -> TODO()
|
||||
Dimension.Area -> Icons.Rounded.SquareFoot
|
||||
Dimension.Currency -> Icons.Rounded.Toll
|
||||
Dimension.Data -> Icons.Rounded.Storage
|
||||
Dimension.Bitrate -> TODO()
|
||||
Dimension.Pressure -> TODO()
|
||||
Dimension.Energy -> Icons.Rounded.Bolt
|
||||
Dimension.Frequency -> TODO()
|
||||
Dimension.Temperature -> Icons.Rounded.Thermostat
|
||||
Dimension.Time -> Icons.Rounded.Schedule
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.website
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.Toolbar
|
||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
|
||||
@Composable
|
||||
fun WebsiteItem(
|
||||
modifier: Modifier = Modifier,
|
||||
website: Website,
|
||||
onBack: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val viewModel = remember(website) { WebsiteItemVM(website) }
|
||||
|
||||
Column(
|
||||
modifier = modifier.clickable {
|
||||
viewModel.launch(context)
|
||||
}
|
||||
) {
|
||||
if (website.image.isNotBlank()) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 9f)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
model = website.image,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = website.labelOverride ?: website.label,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
||||
if (tags.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 2.dp, bottom = 2.dp),
|
||||
text = tags.joinToString(separator = " #", prefix = "#"),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
text = website.description,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
onBack?.invoke()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
onBack?.invoke()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_share),
|
||||
icon= Icons.Rounded.Share,
|
||||
action = {
|
||||
viewModel.share(context)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(website) }
|
||||
))
|
||||
|
||||
Toolbar(
|
||||
leftActions = if (onBack != null) listOf(
|
||||
DefaultToolbarAction(
|
||||
stringResource(id = R.string.menu_back),
|
||||
icon = Icons.Rounded.ArrowBack,
|
||||
action = onBack
|
||||
)
|
||||
) else emptyList(),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WebsiteItemGridPopup(
|
||||
website: Website,
|
||||
show: Boolean,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = show,
|
||||
transitionSpec = {
|
||||
fadeIn(tween(200)) with
|
||||
fadeOut(tween(200, 200)) using
|
||||
SizeTransform { _, _ ->
|
||||
tween(300)
|
||||
}
|
||||
}
|
||||
) { targetState ->
|
||||
if (targetState) {
|
||||
WebsiteItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
website = website,
|
||||
onBack = onDismiss
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidth(origin.width.toDp())
|
||||
.requiredHeight(origin.height.toDp())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.website
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
|
||||
class WebsiteItemVM(
|
||||
private val website: Website
|
||||
) : SearchableItemVM(website) {
|
||||
|
||||
fun share(context: Context) {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
"${website.label}\n\n${website.description}\n\n${website.url}"
|
||||
)
|
||||
shareIntent.type = "text/plain"
|
||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.wikipedia
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.Toolbar
|
||||
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.utils.htmlToAnnotatedString
|
||||
|
||||
@Composable
|
||||
fun WikipediaItem(
|
||||
modifier: Modifier = Modifier,
|
||||
wikipedia: Wikipedia,
|
||||
onBack: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val viewModel = remember(wikipedia) { WikipediaItemVM(wikipedia) }
|
||||
|
||||
Column(
|
||||
modifier = modifier.clickable {
|
||||
viewModel.launch(context)
|
||||
}
|
||||
) {
|
||||
if (!wikipedia.image.isNullOrEmpty()) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 9f)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
model = wikipedia.image,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = wikipedia.label,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
||||
if (tags.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
|
||||
text = tags.joinToString(separator = " #", prefix = "#"),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
text = stringResource(id = R.string.wikipedia_source),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
text = htmlToAnnotatedString(wikipedia.text),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||
|
||||
if (LocalFavoritesEnabled.current) {
|
||||
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||
val favAction = if (isPinned) {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_unpin),
|
||||
icon = Icons.Rounded.Star,
|
||||
action = {
|
||||
viewModel.unpin()
|
||||
onBack?.invoke()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_favorites_pin),
|
||||
icon = Icons.Rounded.StarOutline,
|
||||
action = {
|
||||
viewModel.pin()
|
||||
onBack?.invoke()
|
||||
})
|
||||
}
|
||||
toolbarActions.add(favAction)
|
||||
}
|
||||
|
||||
toolbarActions.add(
|
||||
DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_share),
|
||||
icon = Icons.Rounded.Share,
|
||||
action = {
|
||||
viewModel.share(context)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(wikipedia) }
|
||||
))
|
||||
|
||||
Toolbar(
|
||||
leftActions = if (onBack != null) listOf(
|
||||
DefaultToolbarAction(
|
||||
stringResource(id = R.string.menu_back),
|
||||
icon = Icons.Rounded.ArrowBack,
|
||||
action = onBack
|
||||
)
|
||||
) else emptyList(),
|
||||
rightActions = toolbarActions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WikipediaItemGridPopup(
|
||||
wikipedia: Wikipedia,
|
||||
show: Boolean,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = show,
|
||||
transitionSpec = {
|
||||
fadeIn(tween(200)) with
|
||||
fadeOut(tween(200, 200)) using
|
||||
SizeTransform { _, _ ->
|
||||
tween(300)
|
||||
}
|
||||
}
|
||||
) { targetState ->
|
||||
if (targetState) {
|
||||
WikipediaItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
wikipedia = wikipedia,
|
||||
onBack = onDismiss
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidth(origin.width.toDp())
|
||||
.requiredHeight(origin.height.toDp())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.wikipedia
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.text.HtmlCompat
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||
|
||||
class WikipediaItemVM(
|
||||
private val wikipedia: Wikipedia
|
||||
) : SearchableItemVM(wikipedia) {
|
||||
|
||||
fun share(context: Context) {
|
||||
val text = HtmlCompat.fromHtml(wikipedia.text, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_TEXT, "${wikipedia.label}\n\n" +
|
||||
"${text.substring(0, 200)}…\n\n" +
|
||||
"${wikipedia.wikipediaUrl}/wiki?curid=${wikipedia.id}"
|
||||
)
|
||||
shareIntent.type = "text/plain"
|
||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package de.mm20.launcher2.ui.launcher.searchbar
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.ui.component.SearchBar
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
|
||||
@Composable
|
||||
fun LauncherSearchBar(
|
||||
modifier: Modifier = Modifier,
|
||||
style: Settings.SearchBarSettings.SearchBarStyle,
|
||||
level: () -> SearchBarLevel,
|
||||
value: () -> String,
|
||||
onValueChange: (String) -> Unit,
|
||||
focused: Boolean,
|
||||
onFocusChange: (Boolean) -> Unit,
|
||||
actions: List<SearchAction>,
|
||||
showHiddenItemsButton: Boolean = false,
|
||||
reverse: Boolean = false,
|
||||
darkColors: Boolean = false,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
|
||||
val searchVM: SearchVM = viewModel()
|
||||
|
||||
val hiddenItems by searchVM.hiddenResults.observeAsState(emptyList())
|
||||
|
||||
|
||||
LaunchedEffect(focused) {
|
||||
if (focused) focusRequester.requestFocus()
|
||||
else focusManager.clearFocus()
|
||||
}
|
||||
|
||||
val _value = value()
|
||||
|
||||
SearchBar(
|
||||
modifier = modifier,
|
||||
style = style, level = level(), value = _value, onValueChange = onValueChange,
|
||||
reverse = reverse,
|
||||
darkColors = darkColors,
|
||||
menu = {
|
||||
AnimatedVisibility(
|
||||
showHiddenItemsButton && hiddenItems.isNotEmpty(),
|
||||
enter = scaleIn(tween(100)),
|
||||
exit = scaleOut(tween(100))
|
||||
) {
|
||||
FilledIconButton(
|
||||
onClick = { sheetManager.showHiddenItemsSheet() },
|
||||
colors = if (sheetManager.hiddenItemsSheetShown.value) IconButtonDefaults.filledTonalIconButtonColors() else IconButtonDefaults.iconButtonColors()
|
||||
) {
|
||||
Icon(imageVector = Icons.Rounded.VisibilityOff, contentDescription = null)
|
||||
}
|
||||
}
|
||||
SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange)
|
||||
},
|
||||
actions = {
|
||||
SearchBarActions(actions = actions, reverse = reverse)
|
||||
},
|
||||
focusRequester = focusRequester,
|
||||
onFocus = { onFocusChange(true) },
|
||||
onUnfocus = { onFocusChange(false) },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.mm20.launcher2.ui.launcher.searchbar
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.ui.component.SearchActionIcon
|
||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.SearchBarActions(
|
||||
modifier: Modifier = Modifier,
|
||||
actions: List<SearchAction>,
|
||||
reverse: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
AnimatedVisibility(actions.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 4.dp)
|
||||
) {
|
||||
items(actions) {
|
||||
AssistChip(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
onClick = {
|
||||
it.start(context)
|
||||
},
|
||||
label = { Text(it.label) },
|
||||
leadingIcon = {
|
||||
SearchActionIcon(
|
||||
action = it
|
||||
)
|
||||
}
|
||||
/*leadingIcon = {
|
||||
val icon = it.icon
|
||||
if (icon == null) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = if (it.color == 0) MaterialTheme.colorScheme.primary else Color(
|
||||
it.color
|
||||
)
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
modifier = Modifier.size(24.dp),
|
||||
model = File(icon),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}*/
|
||||
)
|
||||
}
|
||||
item {
|
||||
SmallFloatingActionButton(
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
|
||||
onClick = {
|
||||
context.startActivity(
|
||||
Intent(context, SettingsActivity::class.java).apply {
|
||||
putExtra(SettingsActivity.EXTRA_ROUTE, "settings/search/searchactions")
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
||||
Icon(imageVector = Icons.Rounded.Edit, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package de.mm20.launcher2.ui.launcher.searchbar
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.HelpOutline
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Wallpaper
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||
|
||||
@Composable
|
||||
fun RowScope.SearchBarMenu(
|
||||
searchBarValue: String,
|
||||
onSearchBarValueChange: (newValue: String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
||||
|
||||
IconButton(onClick = {
|
||||
if (searchBarValue.isNotBlank()) onSearchBarValueChange("")
|
||||
else showOverflowMenu = true
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(
|
||||
rightIcon,
|
||||
atEnd = searchBarValue.isNotEmpty()
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = LocalContentColor.current
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = showOverflowMenu, onDismissRequest = { showOverflowMenu = false }) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_SET_WALLPAPER),
|
||||
null
|
||||
)
|
||||
)
|
||||
showOverflowMenu = false
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.wallpaper))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Wallpaper, contentDescription = null)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
context.startActivity(Intent(context, SettingsActivity::class.java))
|
||||
showOverflowMenu = false
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.settings))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Settings, contentDescription = null)
|
||||
}
|
||||
)
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(colorScheme.primaryContainer.toArgb())
|
||||
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
.launchUrl(context, Uri.parse("https://kvaesitso.mm20.de/docs/user-guide"))
|
||||
showOverflowMenu = false
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.help))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.HelpOutline, contentDescription = null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
package de.mm20.launcher2.ui.launcher.sheets
|
||||
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Clear
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.icons.CustomIconWithPreview
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.component.OutlinedTagsInputField
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun CustomizeSearchableSheet(
|
||||
searchable: SavableSearchable,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val viewModel: CustomizeSearchableSheetVM =
|
||||
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val pickIcon by viewModel.isIconPickerOpen.observeAsState(false)
|
||||
|
||||
BottomSheetDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(stringResource(if (pickIcon) R.string.icon_picker_title else R.string.menu_customize))
|
||||
},
|
||||
swipeToDismiss = { !pickIcon },
|
||||
dismissOnBackPress = { !pickIcon },
|
||||
confirmButton = {
|
||||
if (pickIcon) {
|
||||
OutlinedButton(onClick = { viewModel.closeIconPicker() }) {
|
||||
Text(stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
} else {
|
||||
OutlinedButton(onClick = onDismiss) {
|
||||
Text(stringResource(id = R.string.close))
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (!pickIcon) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
val iconSize = 64.dp
|
||||
val iconSizePx = iconSize.toPixels()
|
||||
val icon by remember { viewModel.getIcon(iconSizePx.toInt()) }.collectAsState(null)
|
||||
val primaryColor = MaterialTheme.colorScheme.onSecondary
|
||||
val badgeDrawable = remember {
|
||||
InsetDrawable(
|
||||
AppCompatResources.getDrawable(context, R.drawable.ic_edit),
|
||||
8
|
||||
).also {
|
||||
it.setTint(primaryColor.toArgb())
|
||||
}
|
||||
}
|
||||
|
||||
ShapedLauncherIcon(
|
||||
size = iconSize,
|
||||
icon = { icon },
|
||||
badge = {
|
||||
Badge(
|
||||
icon = badgeDrawable
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.openIconPicker()
|
||||
}
|
||||
)
|
||||
|
||||
var customLabelValue by remember {
|
||||
mutableStateOf(searchable.labelOverride ?: "")
|
||||
}
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp),
|
||||
value = customLabelValue,
|
||||
onValueChange = {
|
||||
customLabelValue = it
|
||||
},
|
||||
singleLine = true,
|
||||
placeholder = {
|
||||
Text(searchable.label)
|
||||
},
|
||||
)
|
||||
|
||||
var tags by remember { mutableStateOf(emptyList<String>()) }
|
||||
|
||||
LaunchedEffect(searchable.key) {
|
||||
tags = viewModel.getTags().first()
|
||||
}
|
||||
|
||||
OutlinedTagsInputField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
tags = tags, onTagsChange = { tags = it.distinct() },
|
||||
placeholder = {
|
||||
Text(stringResource(R.string.customize_tags_placeholder))
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
onAutocomplete = {
|
||||
viewModel.autocompleteTags(it).minus(tags.toSet())
|
||||
}
|
||||
)
|
||||
|
||||
DisposableEffect(searchable.key) {
|
||||
onDispose {
|
||||
viewModel.setCustomLabel(customLabelValue)
|
||||
viewModel.setTags(tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val iconSize = 48.dp
|
||||
val iconSizePx = iconSize.toPixels()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val suggestions by remember { viewModel.getIconSuggestions(iconSizePx.toInt()) }
|
||||
.observeAsState(emptyList())
|
||||
|
||||
val defaultIcon by remember {
|
||||
viewModel.getDefaultIcon(iconSizePx.toInt())
|
||||
}.observeAsState()
|
||||
|
||||
var query by remember { mutableStateOf("") }
|
||||
val isSearching by viewModel.isSearchingIcons.observeAsState(initial = false)
|
||||
val iconResults by viewModel.iconSearchResults.observeAsState(emptyList())
|
||||
|
||||
val columns = LocalGridSettings.current.columnCount
|
||||
|
||||
LazyVerticalGrid(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
columns = GridCells.Fixed(columns)
|
||||
) {
|
||||
|
||||
item(span = { GridItemSpan(columns) }) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (query.isNotEmpty()) {
|
||||
IconButton(onClick = {
|
||||
query = ""
|
||||
scope.launch {
|
||||
viewModel.searchIcon("")
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Clear,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = query,
|
||||
onValueChange = {
|
||||
query = it
|
||||
scope.launch {
|
||||
viewModel.searchIcon(query)
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(stringResource(R.string.icon_picker_search_icon))
|
||||
},
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
if (query.isEmpty()) {
|
||||
if (defaultIcon != null) {
|
||||
item(span = { GridItemSpan(columns) }) {
|
||||
Separator(stringResource(R.string.icon_picker_default_icon))
|
||||
}
|
||||
item {
|
||||
IconPreview(item = defaultIcon, iconSize = iconSize, onClick = {
|
||||
viewModel.pickIcon(null)
|
||||
})
|
||||
}
|
||||
}
|
||||
item(span = { GridItemSpan(columns) }) {
|
||||
Separator(stringResource(R.string.icon_picker_suggestions))
|
||||
}
|
||||
|
||||
items(suggestions) {
|
||||
IconPreview(
|
||||
it,
|
||||
iconSize,
|
||||
onClick = { viewModel.pickIcon(it.customIcon) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
items(iconResults) {
|
||||
IconPreview(
|
||||
it,
|
||||
iconSize,
|
||||
onClick = { viewModel.pickIcon(it.customIcon) }
|
||||
)
|
||||
}
|
||||
|
||||
if (isSearching) {
|
||||
item(span = { GridItemSpan(columns) }) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconPreview(
|
||||
item: CustomIconWithPreview?,
|
||||
iconSize: Dp,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
) {
|
||||
ShapedLauncherIcon(
|
||||
size = iconSize,
|
||||
icon = { item?.preview },
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Separator(label: String) {
|
||||
Text(
|
||||
label,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp, bottom = 8.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package de.mm20.launcher2.ui.launcher.sheets
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||
import de.mm20.launcher2.data.customattrs.CustomIcon
|
||||
import de.mm20.launcher2.icons.CustomIconWithPreview
|
||||
import de.mm20.launcher2.icons.IconRepository
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class CustomizeSearchableSheetVM(
|
||||
private val searchable: SavableSearchable
|
||||
) : KoinComponent {
|
||||
private val iconRepository: IconRepository by inject()
|
||||
private val customAttributesRepository: CustomAttributesRepository by inject()
|
||||
|
||||
val isIconPickerOpen = MutableLiveData(false)
|
||||
|
||||
fun getIcon(size: Int): Flow<LauncherIcon> {
|
||||
return iconRepository.getIcon(searchable, size)
|
||||
}
|
||||
|
||||
fun getIconSuggestions(size: Int) = liveData {
|
||||
emit(iconRepository.getCustomIconSuggestions(searchable, size))
|
||||
}
|
||||
|
||||
fun openIconPicker() {
|
||||
isIconPickerOpen.value = true
|
||||
}
|
||||
|
||||
fun closeIconPicker() {
|
||||
isIconPickerOpen.value = false
|
||||
}
|
||||
|
||||
fun pickIcon(icon: CustomIcon?) {
|
||||
iconRepository.setCustomIcon(searchable, icon)
|
||||
closeIconPicker()
|
||||
}
|
||||
|
||||
fun getDefaultIcon(size: Int) = liveData {
|
||||
emit(iconRepository.getUncustomizedDefaultIcon(searchable, size))
|
||||
}
|
||||
|
||||
val iconSearchResults = MutableLiveData(emptyList<CustomIconWithPreview>())
|
||||
val isSearchingIcons = MutableLiveData(false)
|
||||
|
||||
private var debounceSearchJob: Job? = null
|
||||
suspend fun searchIcon(query: String) {
|
||||
debounceSearchJob?.cancelAndJoin()
|
||||
if (query.isBlank()) {
|
||||
iconSearchResults.value = emptyList()
|
||||
isSearchingIcons.value = false
|
||||
return
|
||||
}
|
||||
withContext(coroutineContext) {
|
||||
debounceSearchJob = launch {
|
||||
delay(1000)
|
||||
isSearchingIcons.value = true
|
||||
iconSearchResults.value = iconRepository.searchCustomIcons(query)
|
||||
isSearchingIcons.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCustomLabel(label: String) {
|
||||
if (label.isBlank()) {
|
||||
customAttributesRepository.clearCustomLabel(searchable)
|
||||
} else {
|
||||
customAttributesRepository.setCustomLabel(searchable, label)
|
||||
}
|
||||
}
|
||||
|
||||
fun setTags(tags: List<String>) {
|
||||
customAttributesRepository.setTags(searchable, tags)
|
||||
}
|
||||
|
||||
fun getTags(): Flow<List<String>> {
|
||||
return customAttributesRepository.getTags(searchable)
|
||||
}
|
||||
|
||||
suspend fun autocompleteTags(query: String): List<String> {
|
||||
return customAttributesRepository.getAllTags(startsWith = query)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,752 @@
|
||||
package de.mm20.launcher2.ui.launcher.sheets
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
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.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.ArrowForward
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Create
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Tag
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
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.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.drawOutline
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.toOffset
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.helper.DraggableItem
|
||||
import de.mm20.launcher2.ui.launcher.helper.LazyDragAndDropRow
|
||||
import de.mm20.launcher2.ui.launcher.helper.LazyVerticalDragAndDropGrid
|
||||
import de.mm20.launcher2.ui.launcher.helper.rememberLazyDragAndDropGridState
|
||||
import de.mm20.launcher2.ui.launcher.helper.rememberLazyDragAndDropListState
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun EditFavoritesSheet(
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val viewModel: EditFavoritesSheetVM = viewModel()
|
||||
|
||||
LaunchedEffect(null) {
|
||||
viewModel.reload()
|
||||
}
|
||||
|
||||
val loading by viewModel.loading.observeAsState(true)
|
||||
val createShortcutTarget by viewModel.createShortcutTarget.observeAsState(null)
|
||||
|
||||
BottomSheetDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
if (createShortcutTarget == null) {
|
||||
stringResource(id = R.string.menu_item_edit_favs)
|
||||
} else {
|
||||
stringResource(id = R.string.create_app_shortcut)
|
||||
}
|
||||
)
|
||||
},
|
||||
swipeToDismiss = {
|
||||
createShortcutTarget == null
|
||||
},
|
||||
dismissOnBackPress = {
|
||||
createShortcutTarget == null
|
||||
},
|
||||
confirmButton = {
|
||||
if (createShortcutTarget != null) {
|
||||
OutlinedButton(onClick = { viewModel.cancelPickShortcut() }) {
|
||||
Text(stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
} else {
|
||||
OutlinedButton(onClick = onDismiss) {
|
||||
Text(stringResource(id = R.string.close))
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (loading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
} else if (createShortcutTarget != null) {
|
||||
ShortcutPicker(viewModel)
|
||||
} else {
|
||||
ReorderFavoritesGrid(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReorderFavoritesGrid(viewModel: EditFavoritesSheetVM) {
|
||||
val items by viewModel.gridItems.observeAsState(emptyList())
|
||||
val columns = LocalGridSettings.current.columnCount
|
||||
|
||||
val availableTags by viewModel.availableTags.observeAsState(emptyList())
|
||||
val pinnedTags by viewModel.pinnedTags.observeAsState(emptyList())
|
||||
|
||||
var contextMenuItemKey by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val contextMenuCloseDistance = 8.dp.toPixels()
|
||||
|
||||
var draggedItemKey by remember { mutableStateOf<String?>(null) }
|
||||
var hoveredTag by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val gridState = rememberLazyGridState()
|
||||
val tagsListState = rememberLazyListState()
|
||||
val tagsTitleSize = 48.dp.toPixels()
|
||||
val tagsSpacing = 12.dp.toPixels()
|
||||
val state = rememberLazyDragAndDropGridState(
|
||||
gridState = gridState,
|
||||
onDragStart = {
|
||||
val item = items.getOrNull(it.index)
|
||||
|
||||
if (item !is FavoritesSheetGridItem.Favorite) return@rememberLazyDragAndDropGridState false
|
||||
|
||||
draggedItemKey = item.item.key
|
||||
contextMenuItemKey = item.item.key
|
||||
true
|
||||
},
|
||||
onDrag = { item, offset, position ->
|
||||
if (offset.getDistanceSquared() > contextMenuCloseDistance) {
|
||||
contextMenuItemKey = null
|
||||
}
|
||||
val draggedCenter = Rect(position, item.size.toSize()).center
|
||||
val hoveredItem = gridState.layoutInfo.visibleItemsInfo.find {
|
||||
Rect(
|
||||
it.offset.toOffset(),
|
||||
it.size.toSize()
|
||||
).contains(draggedCenter)
|
||||
}
|
||||
if (hoveredItem != null
|
||||
&& items[hoveredItem.index] is FavoritesSheetGridItem.Tags
|
||||
&& hoveredItem.offset.y + tagsTitleSize < position.y
|
||||
) {
|
||||
val scroll = tagsListState.layoutInfo.viewportStartOffset
|
||||
val tag = tagsListState.layoutInfo.visibleItemsInfo.find {
|
||||
draggedCenter.x + scroll > it.offset && draggedCenter.x + scroll < it.offset + it.size - tagsSpacing
|
||||
}
|
||||
hoveredTag = tag?.index?.let { pinnedTags[it].tag }
|
||||
} else {
|
||||
hoveredTag = null
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
viewModel.addTag(draggedItemKey, hoveredTag)
|
||||
draggedItemKey = null
|
||||
hoveredTag = null
|
||||
},
|
||||
onDragCancel = {
|
||||
draggedItemKey = null
|
||||
hoveredTag = null
|
||||
}
|
||||
) { from, to ->
|
||||
viewModel.moveItem(from, to)
|
||||
}
|
||||
|
||||
val iconSize = 48.dp.toPixels()
|
||||
|
||||
LazyVerticalDragAndDropGrid(
|
||||
state = state,
|
||||
columns = GridCells.Fixed(columns),
|
||||
|
||||
) {
|
||||
items(
|
||||
items.size,
|
||||
key = { i ->
|
||||
val it = items[i]
|
||||
if (it is FavoritesSheetGridItem.Favorite) it.item.key else i
|
||||
},
|
||||
span = { i ->
|
||||
val it = items[i]
|
||||
when (it) {
|
||||
is FavoritesSheetGridItem.Favorite -> GridItemSpan(1)
|
||||
is FavoritesSheetGridItem.Divider -> GridItemSpan(columns)
|
||||
is FavoritesSheetGridItem.EmptySection -> GridItemSpan(columns)
|
||||
is FavoritesSheetGridItem.Spacer -> GridItemSpan(it.span)
|
||||
is FavoritesSheetGridItem.Tags -> GridItemSpan(columns)
|
||||
}
|
||||
}
|
||||
) { i ->
|
||||
when (val it = items[i]) {
|
||||
is FavoritesSheetGridItem.Favorite -> {
|
||||
val icon by remember(it.item.key) {
|
||||
viewModel.getIcon(
|
||||
it.item,
|
||||
iconSize.roundToInt()
|
||||
)
|
||||
}.collectAsState(null)
|
||||
val badge by remember(it.item.key) {
|
||||
viewModel.getBadge(
|
||||
it.item,
|
||||
)
|
||||
}.collectAsState(null)
|
||||
DraggableItem(state = state, key = it.item.key) { dragged ->
|
||||
GridItem(
|
||||
label = it.item.labelOverride ?: it.item.label,
|
||||
icon = icon,
|
||||
badge = badge
|
||||
)
|
||||
if (contextMenuItemKey == it.item.key) {
|
||||
DropdownMenu(
|
||||
expanded = true,
|
||||
onDismissRequest = { contextMenuItemKey = null }) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.menu_remove))
|
||||
}, onClick = {
|
||||
contextMenuItemKey?.let { viewModel.remove(it) }
|
||||
contextMenuItemKey = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is FavoritesSheetGridItem.Divider -> {
|
||||
val title = when (it.section) {
|
||||
FavoritesSheetSection.ManuallySorted -> R.string.edit_favorites_dialog_pinned_sorted
|
||||
FavoritesSheetSection.AutomaticallySorted -> R.string.edit_favorites_dialog_pinned_unsorted
|
||||
FavoritesSheetSection.FrequentlyUsed -> R.string.edit_favorites_dialog_unpinned
|
||||
}
|
||||
var showSettings by remember { mutableStateOf(false) }
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
text = stringResource(id = title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
if (it.section == FavoritesSheetSection.FrequentlyUsed) {
|
||||
FilledTonalIconToggleButton(
|
||||
modifier = Modifier.offset(x = 4.dp),
|
||||
checked = showSettings,
|
||||
onCheckedChange = { showSettings = it }) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Settings,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
FilledTonalIconButton(
|
||||
modifier = Modifier.offset(x = 4.dp),
|
||||
onClick = {
|
||||
viewModel.pickShortcut(it.section)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val enableFrequentlyUsed by viewModel.enableFrequentlyUsed.observeAsState(
|
||||
null
|
||||
)
|
||||
val frequentlyUsedRows by viewModel.frequentlyUsedRows.observeAsState(1)
|
||||
AnimatedVisibility(showSettings) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
text = stringResource(R.string.frequently_used_show_in_favorites),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Switch(
|
||||
checked = enableFrequentlyUsed == true,
|
||||
onCheckedChange = {
|
||||
viewModel.setFrequentlyUsed(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(enableFrequentlyUsed == true) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.frequently_used_rows),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Slider(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
value = frequentlyUsedRows.toFloat(),
|
||||
colors = SliderDefaults.colors(
|
||||
inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
onValueChange = {
|
||||
viewModel.setFrequentlyUsedRows(it.roundToInt())
|
||||
},
|
||||
steps = 2,
|
||||
valueRange = 1f..4f
|
||||
)
|
||||
Text(
|
||||
text = frequentlyUsedRows.toString(),
|
||||
modifier = Modifier
|
||||
.width(52.dp)
|
||||
.padding(4.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
is FavoritesSheetGridItem.EmptySection -> {
|
||||
val shape = MaterialTheme.shapes.medium
|
||||
val color = MaterialTheme.colorScheme.outline
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.drawBehind {
|
||||
drawOutline(
|
||||
outline = shape.createOutline(
|
||||
size,
|
||||
layoutDirection,
|
||||
Density(density, fontScale)
|
||||
),
|
||||
color = color,
|
||||
style = Stroke(
|
||||
2.dp.toPx(),
|
||||
pathEffect = PathEffect.dashPathEffect(
|
||||
intervals = floatArrayOf(
|
||||
4.dp.toPx(),
|
||||
4.dp.toPx(),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 24.dp,
|
||||
),
|
||||
text = stringResource(R.string.edit_favorites_dialog_empty_section),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is FavoritesSheetGridItem.Spacer -> {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
)
|
||||
}
|
||||
|
||||
is FavoritesSheetGridItem.Tags -> {
|
||||
var showAddMenu by remember { mutableStateOf(false) }
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
text = stringResource(R.string.edit_favorites_dialog_tags),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Box() {
|
||||
FilledTonalIconButton(
|
||||
modifier = Modifier.offset(x = 4.dp),
|
||||
onClick = {
|
||||
showAddMenu = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showAddMenu,
|
||||
onDismissRequest = { showAddMenu = false }) {
|
||||
for (tag in availableTags) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(Icons.Rounded.Tag, null)
|
||||
},
|
||||
text = { Text(tag.label) },
|
||||
onClick = {
|
||||
viewModel.pinTag(tag)
|
||||
showAddMenu = false
|
||||
})
|
||||
}
|
||||
if (availableTags.isNotEmpty()) {
|
||||
Divider()
|
||||
}
|
||||
var newTag by remember { mutableStateOf("") }
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(Icons.Rounded.Create, null)
|
||||
},
|
||||
contentPadding = PaddingValues(
|
||||
start = MenuDefaults.DropdownMenuItemContentPadding.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = MenuDefaults.DropdownMenuItemContentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
top = if(availableTags.isNotEmpty()) 8.dp else 0.dp,
|
||||
),
|
||||
text = {
|
||||
Box {
|
||||
if (newTag.isEmpty()) {
|
||||
Text(
|
||||
stringResource(R.string.edit_favorites_dialog_new_tag),
|
||||
color = LocalContentColor.current.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
BasicTextField(
|
||||
value = newTag,
|
||||
onValueChange = { newTag = it.replace(",", "") },
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
color = LocalContentColor.current
|
||||
),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
singleLine = true,
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
if (newTag.isBlank()) return@KeyboardActions
|
||||
viewModel.createNewTag(newTag)
|
||||
showAddMenu = false
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.clickable {
|
||||
if (newTag.isBlank()) return@clickable
|
||||
viewModel.createNewTag(newTag)
|
||||
showAddMenu = false
|
||||
},
|
||||
imageVector = Icons.Rounded.ArrowForward,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (pinnedTags.isNotEmpty()) {
|
||||
val rowState = rememberLazyDragAndDropListState(
|
||||
listState = tagsListState,
|
||||
) { from, to ->
|
||||
viewModel.moveTag(from, to)
|
||||
}
|
||||
LazyDragAndDropRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = rowState
|
||||
) {
|
||||
items(
|
||||
pinnedTags,
|
||||
key = { it.key }
|
||||
) { tag ->
|
||||
DraggableItem(state = rowState, key = tag.key) { dragged ->
|
||||
|
||||
FilterChip(
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.pointerInput(null) {
|
||||
val coroutineContext =
|
||||
currentCoroutineContext()
|
||||
|
||||
},
|
||||
selected = tag.tag == hoveredTag,
|
||||
onClick = {},
|
||||
label = { Text(tag.label) },
|
||||
leadingIcon = {
|
||||
Icon(Icons.Rounded.Tag, null)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.clickable {
|
||||
viewModel.unpinTag(tag)
|
||||
},
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
elevation = if (dragged) FilterChipDefaults.elevatedFilterChipElevation() else FilterChipDefaults.filterChipElevation(),
|
||||
colors = if (dragged) FilterChipDefaults.elevatedFilterChipColors()
|
||||
else FilterChipDefaults.filterChipColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
text = stringResource(R.string.edit_favorites_dialog_tag_section_empty),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GridItem(
|
||||
modifier: Modifier = Modifier,
|
||||
label: String,
|
||||
icon: LauncherIcon?,
|
||||
badge: Badge?
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ShapedLauncherIcon(
|
||||
size = 48.dp,
|
||||
icon = { icon },
|
||||
badge = { badge })
|
||||
Text(
|
||||
label,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShortcutPicker(viewModel: EditFavoritesSheetVM) {
|
||||
|
||||
val hasShortcutPermission by remember { viewModel.hasShortcutPermission }.collectAsState(null)
|
||||
|
||||
val shortcutActivities by remember(hasShortcutPermission) { viewModel.getShortcutActivities() }.collectAsState(
|
||||
emptyList()
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
val activityLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartIntentSenderForResult()) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
viewModel.cancelPickShortcut()
|
||||
}
|
||||
viewModel.createShortcut(context, it.data)
|
||||
|
||||
}
|
||||
|
||||
val iconSize = 48.dp.toPixels().roundToInt()
|
||||
val activity = LocalLifecycleOwner.current as AppCompatActivity
|
||||
LazyColumn {
|
||||
if (hasShortcutPermission == false) {
|
||||
item {
|
||||
MissingPermissionBanner(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
text = stringResource(
|
||||
R.string.missing_permission_appshortcuts_create,
|
||||
stringResource(R.string.app_name)
|
||||
),
|
||||
onClick = { viewModel.requestShortcutPermission(activity) })
|
||||
}
|
||||
}
|
||||
items(shortcutActivities) {
|
||||
val icon by remember(it.key) { viewModel.getIcon(it, iconSize) }.collectAsState(null)
|
||||
val badge by remember(it.key) { viewModel.getBadge(it) }.collectAsState(null)
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
onClick = {
|
||||
val launcherApps =
|
||||
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
val sender =
|
||||
launcherApps.getShortcutConfigActivityIntent(it.launcherActivityInfo)
|
||||
?: return@OutlinedCard
|
||||
activityLauncher.launch(IntentSenderRequest.Builder(sender).build(), null)
|
||||
}) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ShapedLauncherIcon(
|
||||
size = 48.dp,
|
||||
icon = { icon },
|
||||
badge = { badge },
|
||||
)
|
||||
Text(
|
||||
text = it.labelOverride ?: it.label,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface FavoritesSheetGridItem {
|
||||
class Favorite(val item: SavableSearchable) : FavoritesSheetGridItem
|
||||
class Divider(val section: FavoritesSheetSection) : FavoritesSheetGridItem
|
||||
class Spacer(val span: Int = 1) : FavoritesSheetGridItem
|
||||
object EmptySection : FavoritesSheetGridItem
|
||||
object Tags : FavoritesSheetGridItem
|
||||
}
|
||||
|
||||
enum class FavoritesSheetSection {
|
||||
ManuallySorted,
|
||||
AutomaticallySorted,
|
||||
FrequentlyUsed
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package de.mm20.launcher2.ui.launcher.sheets
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.badges.BadgeRepository
|
||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||
import de.mm20.launcher2.icons.IconRepository
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.normalize
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.data.Tag
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class EditFavoritesSheetVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val repository: FavoritesRepository by inject()
|
||||
private val shortcutRepository: AppShortcutRepository by inject()
|
||||
private val iconRepository: IconRepository by inject()
|
||||
private val badgeRepository: BadgeRepository by inject()
|
||||
private val customAttributesRepository: CustomAttributesRepository by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
val gridItems = MutableLiveData<List<FavoritesSheetGridItem>>(emptyList())
|
||||
|
||||
val loading = MutableLiveData(false)
|
||||
|
||||
val createShortcutTarget = MutableLiveData<FavoritesSheetSection?>(null)
|
||||
|
||||
private var manuallySorted: MutableList<SavableSearchable> = mutableListOf()
|
||||
private var automaticallySorted: MutableList<SavableSearchable> = mutableListOf()
|
||||
private var frequentlyUsed: MutableList<SavableSearchable> = mutableListOf()
|
||||
|
||||
val pinnedTags = MutableLiveData<List<Tag>>(emptyList())
|
||||
val availableTags = MutableLiveData<List<Tag>>(emptyList())
|
||||
|
||||
suspend fun reload(showLoadingIndicator: Boolean = true) {
|
||||
loading.value = showLoadingIndicator
|
||||
manuallySorted = mutableListOf()
|
||||
manuallySorted = repository.getFavorites(
|
||||
manuallySorted = true,
|
||||
excludeTypes = listOf("tag"),
|
||||
).first().toMutableList()
|
||||
automaticallySorted = repository.getFavorites(
|
||||
automaticallySorted = true,
|
||||
excludeTypes = listOf("tag"),
|
||||
).first().toMutableList()
|
||||
frequentlyUsed = repository.getFavorites(
|
||||
frequentlyUsed = true,
|
||||
excludeTypes = listOf("tag"),
|
||||
).first().toMutableList()
|
||||
val pinnedTags = repository.getFavorites(
|
||||
includeTypes = listOf("tag"),
|
||||
manuallySorted = true,
|
||||
automaticallySorted = true,
|
||||
).first().filterIsInstance<Tag>().toMutableList()
|
||||
availableTags.value =
|
||||
customAttributesRepository
|
||||
.getAllTags()
|
||||
.filter {t -> pinnedTags.none { it.tag == t } }
|
||||
.sortedBy { it.normalize() }
|
||||
.map { Tag(it) }
|
||||
this.pinnedTags.value = pinnedTags
|
||||
|
||||
buildItemList()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
private fun buildItemList() {
|
||||
val items = mutableListOf<FavoritesSheetGridItem>()
|
||||
|
||||
items.add(FavoritesSheetGridItem.Tags)
|
||||
|
||||
items.add(FavoritesSheetGridItem.Divider(FavoritesSheetSection.ManuallySorted))
|
||||
if (manuallySorted.isEmpty()) {
|
||||
items.add(FavoritesSheetGridItem.EmptySection)
|
||||
} else {
|
||||
items.addAll(manuallySorted.map { FavoritesSheetGridItem.Favorite(it) })
|
||||
items.add(FavoritesSheetGridItem.Spacer())
|
||||
}
|
||||
|
||||
items.add(FavoritesSheetGridItem.Divider(FavoritesSheetSection.AutomaticallySorted))
|
||||
if (automaticallySorted.isEmpty()) {
|
||||
items.add(FavoritesSheetGridItem.EmptySection)
|
||||
} else {
|
||||
items.addAll(automaticallySorted.map { FavoritesSheetGridItem.Favorite(it) })
|
||||
items.add(FavoritesSheetGridItem.Spacer())
|
||||
}
|
||||
|
||||
items.add(FavoritesSheetGridItem.Divider(FavoritesSheetSection.FrequentlyUsed))
|
||||
if (frequentlyUsed.isEmpty()) {
|
||||
items.add(FavoritesSheetGridItem.EmptySection)
|
||||
} else {
|
||||
items.addAll(frequentlyUsed.map { FavoritesSheetGridItem.Favorite(it) })
|
||||
items.add(FavoritesSheetGridItem.Spacer())
|
||||
}
|
||||
|
||||
gridItems.value = items
|
||||
}
|
||||
|
||||
fun moveItem(from: LazyGridItemInfo, to: LazyGridItemInfo) {
|
||||
gridItems.value?.getOrNull(from.index)?.takeIf { it is FavoritesSheetGridItem.Favorite }
|
||||
?: return
|
||||
gridItems.value?.getOrNull(to.index)
|
||||
?.takeIf {
|
||||
it is FavoritesSheetGridItem.Favorite ||
|
||||
it is FavoritesSheetGridItem.EmptySection ||
|
||||
it is FavoritesSheetGridItem.Spacer
|
||||
}
|
||||
?: return
|
||||
val manuallySortedSize = manuallySorted.size + 1
|
||||
val automaticallySortedSize = automaticallySorted.size + 1
|
||||
val item = when {
|
||||
from.index < manuallySortedSize + 2 -> {
|
||||
manuallySorted.removeAt(from.index - 2)
|
||||
}
|
||||
from.index < manuallySortedSize + automaticallySortedSize + 3 -> {
|
||||
automaticallySorted.removeAt(from.index - 3 - manuallySortedSize)
|
||||
}
|
||||
else -> {
|
||||
frequentlyUsed.removeAt(from.index - 4 - manuallySortedSize - automaticallySortedSize)
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
to.index < manuallySortedSize + 2 -> {
|
||||
manuallySorted.add((to.index - 2).coerceAtMost(manuallySorted.size), item)
|
||||
}
|
||||
to.index < manuallySortedSize + automaticallySortedSize + 3 -> {
|
||||
automaticallySorted.add(
|
||||
(to.index - 3 - manuallySortedSize).coerceAtMost(
|
||||
automaticallySorted.size
|
||||
), item
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
frequentlyUsed.add(
|
||||
(to.index - 4 - manuallySortedSize - automaticallySortedSize).coerceAtMost(
|
||||
frequentlyUsed.size
|
||||
),
|
||||
item
|
||||
)
|
||||
}
|
||||
}
|
||||
save()
|
||||
buildItemList()
|
||||
}
|
||||
|
||||
private fun save() {
|
||||
repository.updateFavorites(
|
||||
manuallySorted = buildList {
|
||||
pinnedTags.value?.let { addAll(it) }
|
||||
addAll(manuallySorted)
|
||||
},
|
||||
automaticallySorted = buildList {
|
||||
addAll(automaticallySorted)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> {
|
||||
return iconRepository.getIcon(searchable, size)
|
||||
}
|
||||
|
||||
fun getBadge(searchable: Searchable): Flow<Badge?> {
|
||||
return badgeRepository.getBadge(searchable)
|
||||
}
|
||||
|
||||
fun pickShortcut(section: FavoritesSheetSection) {
|
||||
createShortcutTarget.value = section
|
||||
}
|
||||
|
||||
fun cancelPickShortcut() {
|
||||
createShortcutTarget.value = null
|
||||
}
|
||||
|
||||
fun getShortcutActivities() = flow {
|
||||
emit(shortcutRepository.getShortcutsConfigActivities())
|
||||
}
|
||||
|
||||
val hasShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts)
|
||||
|
||||
fun requestShortcutPermission(context: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(context, PermissionGroup.AppShortcuts)
|
||||
}
|
||||
|
||||
fun createShortcut(context: Context, data: Intent?) {
|
||||
data ?: return cancelPickShortcut()
|
||||
|
||||
val shortcut = AppShortcut.fromPinRequestIntent(context, data)
|
||||
|
||||
if (shortcut == null) {
|
||||
cancelPickShortcut()
|
||||
return
|
||||
}
|
||||
|
||||
if (!manuallySorted.any { it.key == shortcut.key }
|
||||
&& !automaticallySorted.any { it.key == shortcut.key }
|
||||
&& !frequentlyUsed.any { it.key == shortcut.key }
|
||||
) {
|
||||
if (createShortcutTarget.value == FavoritesSheetSection.ManuallySorted) {
|
||||
manuallySorted.add(shortcut)
|
||||
} else {
|
||||
automaticallySorted.add(shortcut)
|
||||
}
|
||||
}
|
||||
save()
|
||||
buildItemList()
|
||||
createShortcutTarget.value = null
|
||||
}
|
||||
|
||||
fun remove(key: String) {
|
||||
val gridItems = gridItems.value?.toMutableList() ?: return
|
||||
val item =
|
||||
gridItems.find { it is FavoritesSheetGridItem.Favorite && it.item.key == key } as FavoritesSheetGridItem.Favorite?
|
||||
if (item != null) {
|
||||
repository.removeFromFavorites(item.item)
|
||||
automaticallySorted.removeAll { it.key == item.item.key }
|
||||
|| manuallySorted.removeAll { it.key == item.item.key }
|
||||
|| frequentlyUsed.removeAll { it.key == item.item.key }
|
||||
buildItemList()
|
||||
}
|
||||
}
|
||||
|
||||
val enableFrequentlyUsed = dataStore.data.map { it.favorites.frequentlyUsed }.asLiveData()
|
||||
fun setFrequentlyUsed(frequentlyUsed: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setFavorites(
|
||||
it.favorites
|
||||
.toBuilder()
|
||||
.setFrequentlyUsed(frequentlyUsed)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows }.asLiveData()
|
||||
fun setFrequentlyUsedRows(frequentlyUsedRows: Int) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setFavorites(
|
||||
it.favorites
|
||||
.toBuilder()
|
||||
.setFrequentlyUsedRows(frequentlyUsedRows)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pinTag(tag: Tag) {
|
||||
val pinned = pinnedTags.value?.toMutableList() ?: mutableListOf()
|
||||
pinned.add(tag)
|
||||
val available = availableTags.value ?: emptyList()
|
||||
availableTags.value = available.filter { it.tag != tag.tag }
|
||||
pinnedTags.value = pinned.distinctBy { it.tag }
|
||||
save()
|
||||
}
|
||||
|
||||
fun unpinTag(tag: Tag) {
|
||||
val pinned = pinnedTags.value?.toMutableList() ?: mutableListOf()
|
||||
val available = availableTags.value ?: emptyList()
|
||||
availableTags.value = (available + tag).sorted()
|
||||
pinnedTags.value = pinned.filter { it.tag != tag.tag }
|
||||
save()
|
||||
}
|
||||
|
||||
fun moveTag(from: LazyListItemInfo, to: LazyListItemInfo) {
|
||||
val pinned = pinnedTags.value?.toMutableList() ?: return
|
||||
val tag = pinned.removeAt(from.index)
|
||||
pinned.add(to.index, tag)
|
||||
pinnedTags.value = pinned
|
||||
save()
|
||||
}
|
||||
|
||||
fun addTag(key: String?, tag: String?) {
|
||||
val gridItems = gridItems.value?.toMutableList() ?: return
|
||||
if (key == null || tag == null) return
|
||||
val item =
|
||||
gridItems.find { it is FavoritesSheetGridItem.Favorite && it.item.key == key } as FavoritesSheetGridItem.Favorite?
|
||||
if (item != null) {
|
||||
automaticallySorted.removeAll { it.key == item.item.key }
|
||||
|| manuallySorted.removeAll { it.key == item.item.key }
|
||||
|| frequentlyUsed.removeAll { it.key == item.item.key }
|
||||
buildItemList()
|
||||
customAttributesRepository.addTag(item.item, tag)
|
||||
repository.unpinItem(item.item)
|
||||
viewModelScope.launch {
|
||||
frequentlyUsed = repository.getFavorites(
|
||||
frequentlyUsed = true,
|
||||
excludeTypes = listOf("tag"),
|
||||
).first().toMutableList()
|
||||
buildItemList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewTag(newTag: String) {
|
||||
pinTag(Tag(newTag))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.mm20.launcher2.ui.launcher.sheets
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
||||
|
||||
@Composable
|
||||
fun HiddenItemsSheet(
|
||||
items: List<SavableSearchable>,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val viewModel: HiddenItemsSheetVM = viewModel()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
BottomSheetDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.preference_hidden_items),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.showHiddenItems(context) }) {
|
||||
Icon(imageVector = Icons.Rounded.Edit, contentDescription = null)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
OutlinedButton(onClick = onDismiss) {
|
||||
Text(stringResource(id = R.string.close))
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
SearchResultGrid(
|
||||
items,
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.mm20.launcher2.ui.launcher.sheets
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.ViewModel
|
||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||
|
||||
class HiddenItemsSheetVM: ViewModel() {
|
||||
|
||||
fun showHiddenItems(context: Context) {
|
||||
context.startActivity(
|
||||
Intent(context, SettingsActivity::class.java).apply {
|
||||
putExtra(SettingsActivity.EXTRA_ROUTE, "settings/search/hiddenitems")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package de.mm20.launcher2.ui.launcher.sheets
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
|
||||
class LauncherBottomSheetManager {
|
||||
val customizeSearchableSheetShown = mutableStateOf<SavableSearchable?>(null)
|
||||
val editFavoritesSheetShown = mutableStateOf(false)
|
||||
val hiddenItemsSheetShown = mutableStateOf(false)
|
||||
|
||||
fun showCustomizeSearchableModal(item: SavableSearchable) {
|
||||
customizeSearchableSheetShown.value = item
|
||||
}
|
||||
fun dismissCustomizeSearchableModal() {
|
||||
customizeSearchableSheetShown.value = null
|
||||
}
|
||||
|
||||
fun showEditFavoritesSheet() {
|
||||
editFavoritesSheetShown.value = true
|
||||
}
|
||||
fun dismissEditFavoritesSheet() {
|
||||
editFavoritesSheetShown.value = false
|
||||
}
|
||||
|
||||
fun showHiddenItemsSheet() {
|
||||
hiddenItemsSheetShown.value = true
|
||||
}
|
||||
fun dismissHiddenItemsSheet() {
|
||||
hiddenItemsSheetShown.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val LocalBottomSheetManager = compositionLocalOf { LauncherBottomSheetManager() }
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.mm20.launcher2.ui.launcher.transitions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
|
||||
data class HomeTransition(
|
||||
val startBounds: Rect,
|
||||
val targetBounds: Rect,
|
||||
val icon: (@Composable (animVector: Offset, progress: () -> Float) -> Unit)? = null
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user