Initial commit

This commit is contained in:
MM20
2021-09-18 23:37:52 +02:00
commit 749e4e3073
938 changed files with 50475 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.ui">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application>
<activity
android:name=".legacy.activity.LauncherActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:exported="true"
android:theme="@style/LauncherTheme"
android:windowSoftInputMode="stateHidden">
<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=".activity.ComposeActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:exported="true"
android:resizeableActivity="false"
android:theme="@style/LauncherTheme"
android:windowSoftInputMode="stateHidden">
</activity>
</application>
</manifest>
@@ -0,0 +1,235 @@
package de.mm20.launcher2.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.mm20.launcher2.ui.locals.LocalColorScheme
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
val lightPalette = lightColors(
primary = Color(0, 114, 255)
)
val darkPalette = darkColors(
primary = Color(0, 114, 255)
)
val Inter = FontFamily(
Font(R.font.inter_thin, FontWeight.Thin),
Font(R.font.inter_extralight, FontWeight.ExtraLight),
Font(R.font.inter_light, FontWeight.Light),
Font(R.font.inter_regular),
Font(R.font.inter_medium, FontWeight.Medium),
Font(R.font.inter_semibold, FontWeight.SemiBold),
Font(R.font.inter_bold, FontWeight.Bold),
Font(R.font.inter_extrabold, FontWeight.ExtraBold),
Font(R.font.inter_black, FontWeight.Black),
)
val typography = Typography(
h1 = TextStyle(
fontSize = 18.sp,
fontWeight = FontWeight.ExtraBold,
fontFamily = Inter
),
h2 = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
fontFamily = Inter
),
h3 = TextStyle(
fontSize = 13.sp,
fontFamily = Inter
),
caption = TextStyle(
fontFamily = Inter,
fontSize = 13.sp
),
body1 = TextStyle(
fontSize = 13.sp
),
body2 = TextStyle(
fontSize = 13.sp
)
)
val shapes = Shapes(
medium = RoundedCornerShape(8.dp)
)
val Colors.red: Color
get() = if (isLight) Color(0xFFE53935) else Color(0xFFE57373)
val Colors.pink: Color
get() = if (isLight) Color(0xFFD81B60) else Color(0xFFF06292)
val Colors.purple: Color
get() = if (isLight) Color(0xFF8E24AA) else Color(0xFFBA68C8)
val Colors.deepPurple: Color
get() = if (isLight) Color(0xFF5E35B1) else Color(0xFF9575CD)
val Colors.indigo: Color
get() = if (isLight) Color(0xFF3949AB) else Color(0xFF7986CB)
val Colors.blue: Color
get() = if (isLight) Color(0xFF039BE5) else Color(0xFF4FC3F7)
val Colors.lightBlue: Color
get() = if (isLight) Color(0xFF1E88E5) else Color(0xFF64B5F6)
val Colors.cyan: Color
get() = if (isLight) Color(0xFF00ACC1) else Color(0xFF4DD0E1)
val Colors.teal: Color
get() = if (isLight) Color(0xFF00897B) else Color(0xFF4DB6AC)
val Colors.green: Color
get() = if (isLight) Color(0xFF388E3C) else Color(0xFF81C784)
val Colors.lightGreen: Color
get() = if (isLight) Color(0xFF7CB342) else Color(0xFFAED581)
val Colors.lime: Color
get() = if (isLight) Color(0xFFC0CA33) else Color(0xFFDCE775)
val Colors.yellow: Color
get() = if (isLight) Color(0xFFFDD835) else Color(0xFFFFF176)
val Colors.amber: Color
get() = if (isLight) Color(0xFFFFB300) else Color(0xFFFFD54F)
val Colors.orange: Color
get() = if (isLight) Color(0xFFFB8C00) else Color(0xFFFFB74D)
val Colors.deepOrange: Color
get() = if (isLight) Color(0xFFF4511E) else Color(0xFFFF8A65)
val Colors.brown: Color
get() = if (isLight) Color(0xFF6D4C41) else Color(0xFFA1887F)
val Colors.gray: Color
get() = if (isLight) Color(0xFF757575) else Color(0xFFE0E0E0)
val Colors.blueGray: Color
get() = if (isLight) Color(0xFF546E7A) else Color(0xFF90A4AE)
val Colors.androidGreen: Color
get() = if (isLight) Color(0xFF00A55B) else Color(0xFF00DE7A)
val Colors.weatherSkyClear: Color
get() = Color(0xff4482ac)
val Colors.weatherSkyClearNight: Color
get() = deepPurple
val Colors.weatherSkyCloudy: Color
get() = gray
val Colors.weatherSkyCloudyNight: Color
get() = gray
val Colors.weatherSkyThunder: Color
get() = gray
val Colors.weatherSkyThunderNight: Color
get() = gray
val Colors.weatherCloudLight1: Color
get() = Color(0xFFECEFF1)
val Colors.weatherCloudLight2: Color
get() = if (isLight) Color(0xFF90A4AE) else Color(0xFFCFD8DC)
val Colors.weatherCloudMedium1: Color
get() = if (isLight) Color(0xFF546E7A) else Color(0xFF78909C)
val Colors.weatherCloudMedium2: Color
get() = if (isLight) Color(0xFF455a64) else Color(0xFF607D8B)
val Colors.weatherCloudDark1: Color
get() = if (isLight) Color(0xFF37474F) else Color(0xFF546E7A)
val Colors.weatherCloudDark2: Color
get() = if (isLight) Color(0xFF263238) else Color(0xFF455A64)
val Colors.weatherSun: Color
get() = amber
val Colors.weatherMoon: Color
get() = if (isLight) Color(0xFF9E9E9E) else Color(0xFFE0E0E0)
val Colors.weatherBolt: Color
get() = amber
val Colors.weatherHot: Color
get() = red
val Colors.weatherCold: Color
get() = lightBlue
val Colors.weatherWind: Color
get() = if (isLight) Color(0xFF90A4AE) else Color(0xFFCFD8DC)
val Colors.weatherWindDark: Color
get() = if (isLight) Color(0xFF546E7A) else Color(0xFF78909C)
val Colors.weatherRain: Color
get() = blue
val Colors.weatherHail: Color
get() = if (isLight) Color(0xFFBBDEFB) else Color(0xFFE3F2FD)
val Colors.weatherSnow: Color
get() = if (isLight) Color(0xFFE0E0E0) else Color(0xFFF5F5F5)
val Colors.weatherFog: Color
get() = weatherCloudLight2
@Composable
fun LauncherTheme(content: @Composable () -> Unit) {
val colorScheme = LocalColorScheme.current
val colors = if (isSystemInDarkTheme()) {
darkColors(
onSurface = colorScheme.neutral2.shade10,
surface = colorScheme.neutral2.shade900,
onBackground = colorScheme.neutral2.shade10,
background = colorScheme.neutral2.shade900,
primary = colorScheme.accent1.shade300,
primaryVariant = colorScheme.accent1.shade400,
secondary = colorScheme.accent2.shade300,
secondaryVariant = colorScheme.accent3.shade300,
)
} else {
lightColors(
surface = colorScheme.neutral1.shade0,
onSurface = colorScheme.neutral2.shade1000,
onBackground = colorScheme.neutral2.shade1000,
background = colorScheme.neutral1.shade0,
primary = colorScheme.accent1.shade600,
primaryVariant = colorScheme.accent1.shade700,
secondary = colorScheme.accent2.shade600,
secondaryVariant = colorScheme.accent3.shade600,
)
}
MaterialTheme(
colors = colors,
typography = typography,
shapes = shapes,
content = content
)
}
@@ -0,0 +1,72 @@
package de.mm20.launcher2.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.locals.LocalColorScheme
import de.mm20.launcher2.ui.theme.colors.ColorSwatch
@Composable
fun ColorSchemeTest() {
val colorScheme = LocalColorScheme.current
Card {
Column {
SwatchRow(swatch = colorScheme.neutral1)
SwatchRow(swatch = colorScheme.neutral2)
SwatchRow(swatch = colorScheme.accent1)
SwatchRow(swatch = colorScheme.accent2)
SwatchRow(swatch = colorScheme.accent3)
}
}
}
@Composable
fun SwatchRow(swatch: ColorSwatch) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade0))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade10))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade50))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade100))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade200))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade300))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade400))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade500))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade600))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade700))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade800))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade900))
Box(modifier = Modifier
.height(24.dp).weight(1f)
.background(swatch.shade1000))
}
}
@@ -0,0 +1,9 @@
package de.mm20.launcher2.ui
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,35 @@
package de.mm20.launcher2.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.theme.divider
@Composable
fun InformationText(
text: String,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null
) {
Card(
elevation = 0.dp,
border = BorderStroke(
width = 1.dp,
color = LocalContentColor.current.copy(alpha = ContentAlpha.divider)),
modifier = modifier.fillMaxWidth()
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
text = text,
style = MaterialTheme.typography.body2,
modifier = (if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier).padding(12.dp)
)
}
}
}
@@ -0,0 +1,8 @@
package de.mm20.launcher2.ui
import androidx.compose.runtime.Composable
@Composable
fun LauncherCard() {
}
@@ -0,0 +1,55 @@
package de.mm20.launcher2.ui
import android.graphics.Matrix
import android.graphics.Path
import android.graphics.RectF
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Build
import android.util.Log
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.asComposePath
import androidx.core.graphics.flatten
import de.mm20.launcher2.ktx.isAtLeastApiLevel
val LocalLauncherIconShape = compositionLocalOf { LauncherIconShape.circle }
object LauncherIconShape {
val circle: Shape = CircleShape
val square: Shape = RectangleShape
val roundedSquare: Shape = RoundedCornerShape(13)
val hexagon: Shape = 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()
}
val platformDefault: Shape = run {
val platformShape = getSystemShape()
if (platformShape == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return@run CircleShape
GenericShape { size, _ ->
Log.d("MM20", "GenericShape {}")
val matrix = Matrix()
val bounds = RectF()
platformShape.computeBounds(bounds, true)
matrix.setRectToRect(bounds, RectF(0f, 0f, size.width, size.height), Matrix.ScaleToFit.CENTER)
platformShape.transform(matrix)
addPath(platformShape.asComposePath())
}
}
private fun getSystemShape(): Path? {
return if (isAtLeastApiLevel(Build.VERSION_CODES.O)) {
AdaptiveIconDrawable(null, null).iconMask
} else {
null
}
}
}
@@ -0,0 +1,140 @@
package de.mm20.launcher2.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.accompanist.insets.systemBarsPadding
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.ui.locals.LocalWindowSize
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@OptIn(
ExperimentalMaterialApi::class,
ExperimentalAnimationApi::class,
ExperimentalPagerApi::class,
InternalCoroutinesApi::class
)
@Composable
fun LauncherMainScreen() {
val systemUiController = rememberSystemUiController()
val pagerState = rememberPagerState(pageCount = 2)
val searchColumnState = rememberLazyListState()
val widgetColumnState = rememberScrollState()
val isLightTheme = MaterialTheme.colors.isLight
val windowHeight = LocalWindowSize.current.height
LaunchedEffect(pagerState) {
val offsetFlow = snapshotFlow { pagerState.currentPageOffset + pagerState.currentPage }
val scrollFlow = snapshotFlow { widgetColumnState.value }
offsetFlow.combine(scrollFlow) { pageOffset, scrollValue ->
pageOffset > 0.5f || scrollValue > windowHeight / 2
}.collect { proposeDarkIcons ->
if (proposeDarkIcons) {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
isNavigationBarContrastEnforced = false,
darkIcons = isLightTheme
)
} else {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = false //TODO Add preference to control that
)
systemUiController.setNavigationBarColor(
color = Color.Transparent,
navigationBarContrastEnforced = false,
darkIcons = false //TODO Add preference to control that
)
}
}
}
var searchBarOffset by remember { mutableStateOf(0f) }
var lastWidgetScrollPosition by remember { mutableStateOf(0) }
searchBarOffset = run {
val lastScrollPos = lastWidgetScrollPosition
val scrollPos = widgetColumnState.value
lastWidgetScrollPosition = scrollPos
(searchBarOffset - (lastScrollPos - scrollPos) / 100.dp.toPixels()).coerceIn(0f, 1f)
}
val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
) {
BackHandler {
scope.launch {
if (pagerState.currentPage > 0) {
pagerState.animateScrollToPage(0)
} else if (widgetColumnState.value > 0) {
widgetColumnState.animateScrollTo(0)
}
}
}
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
when (page) {
0 -> WidgetColumn(
modifier = Modifier.fillMaxSize(),
scrollState = widgetColumnState
)
1 -> SearchColumn(
modifier = Modifier.fillMaxSize(),
listState = searchColumnState
)
}
}
val scope = rememberCoroutineScope()
SearchBar(
modifier = Modifier
.systemBarsPadding()
.padding(8.dp),
pagerState = pagerState,
widgetColumnState = widgetColumnState,
offScreen = searchBarOffset,
onFocus = {
scope.launch {
pagerState.animateScrollToPage(1)
}
}
)
}
}
enum class Page {
Home, Search
}
enum class SwipeState {
Initial,
Swiping
}
@@ -0,0 +1,43 @@
package de.mm20.launcher2.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Typography
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val legacyTypography = Typography(
h1 = TextStyle(
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
),
h2 = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
),
h3 = TextStyle(
fontSize = 13.sp,
),
caption = TextStyle(
fontSize = 13.sp
),
body1 = TextStyle(
fontSize = 13.sp
),
body2 = TextStyle(
fontSize = 13.sp
)
)
@Composable
fun LegacyLauncherTheme(content: @Composable () -> Unit) {
MaterialTheme(
typography = legacyTypography,
content = content,
colors = if(isSystemInDarkTheme()) darkColors() else lightColors()
)
}
@@ -0,0 +1,23 @@
package de.mm20.launcher2.ui
import android.content.res.Resources
import androidx.annotation.PluralsRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
@Composable
fun pluralResource(@PluralsRes id: Int, quantity: Int): String {
return resources().getQuantityString(id, quantity)
}
@Composable
fun pluralResource(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): String {
return resources().getQuantityString(id, quantity, *formatArgs)
}
@Composable
private fun resources(): Resources {
LocalConfiguration.current
return LocalContext.current.resources
}
@@ -0,0 +1,120 @@
package de.mm20.launcher2.ui
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Search
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.ExperimentalGraphicsApi
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.ui.locals.LocalWindowSize
/**
* Search bar
* @param pageTransition 0..1 how much the search bar should be shown (this will be 0 on widget page, 1 on
* search page, and anything in between while swiping between those two pages
*/
@OptIn(ExperimentalAnimationGraphicsApi::class)
@ExperimentalPagerApi
@Composable
fun SearchBar(
modifier: Modifier = Modifier,
pagerState: PagerState,
widgetColumnState: ScrollState,
offScreen: Float,
onFocus: () -> Unit = {}
) {
var searchQuery by remember { mutableStateOf(TextFieldValue()) }
val viewModel: SearchViewModel = viewModel()
LaunchedEffect(searchQuery) {
viewModel.search(searchQuery.text)
}
val pageTransition = (pagerState.currentPage + pagerState.currentPageOffset).coerceIn(0f, 1f)
val elevationTransition = (2 * widgetColumnState.value / LocalWindowSize.current.height).coerceIn(0f, 1f)
Card(
modifier = modifier
.offset(y = (-100.dp * offScreen * (1 - pageTransition))),
elevation = 8.dp * (pageTransition + elevationTransition).coerceIn(0f, 1f),
) {
val textStyle = TextStyle(
color = LocalContentColor.current,
fontSize = 16.sp
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(48.dp)
.fillMaxWidth()
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Icon(
imageVector = Icons.Rounded.Search,
modifier = Modifier.padding(horizontal = 12.dp),
contentDescription = null
)
}
Box(
modifier = Modifier.weight(1f)
) {
BasicTextField(
value = searchQuery,
onValueChange = {
searchQuery = it
},
cursorBrush = SolidColor(LocalContentColor.current),
textStyle = textStyle,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) onFocus()
}
)
if (searchQuery.text.isEmpty()) {
BasicText(
text = stringResource(id = R.string.edit_text_search_hint),
style = textStyle,
modifier = Modifier.alpha(ContentAlpha.medium)
)
}
}
IconButton(
onClick = {
searchQuery = TextFieldValue()
},
modifier = Modifier.size(48.dp)
) {
val menuClearIcon = animatedVectorResource(R.drawable.anim_ic_menu_clear)
Icon(painter = menuClearIcon.painterFor(atEnd = searchQuery.text.isNotEmpty()), null)
}
}
}
}
@@ -0,0 +1,72 @@
package de.mm20.launcher2.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.Divider
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.insets.navigationBarsWithImePadding
import com.google.accompanist.insets.statusBarsPadding
import de.mm20.launcher2.ui.search.*
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchColumn(
modifier: Modifier = Modifier,
listState: LazyListState
) {
Box(
modifier = modifier
.background(MaterialTheme.colors.surface.copy(alpha = 0.8f))
.fillMaxHeight()
.statusBarsPadding()
.navigationBarsWithImePadding()
) {
val apps = applicationResults()
val favorites = favoriteResults()
val files = fileResults()
val calculator = calculatorItem()
val wikipedia = wikipediaResult()
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onSurface) {
LazyColumn(
contentPadding = PaddingValues(8.dp),
state = listState
) {
item {
// Search bar space
Spacer(
modifier = Modifier.requiredHeight(
72.dp
)
)
}
favorites(listState)
apps(listState)
calculator()
wikipedia()
files()
}
}
}
}
fun LazyListScope.SectionDivider() {
item {
Divider(
modifier = Modifier.padding(vertical = 16.dp)
)
}
}
@@ -0,0 +1,202 @@
package de.mm20.launcher2.ui
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.*
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.graphics.compositeOver
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.icons.PlaceholderIcon
import de.mm20.launcher2.ui.icons.getPlaceholderIcon
import de.mm20.launcher2.ui.ktx.conditional
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun ShapedLauncherIcon(
modifier: Modifier = Modifier,
size: Dp = 64.dp,
item: Searchable,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null
) {
val context = LocalContext.current
val iconSize = size.toPixels().toInt()
var icon by remember {
mutableStateOf<LauncherIcon?>(null)
}
LaunchedEffect(item) {
icon = withContext(Dispatchers.IO) {
item.loadIconAsync(context, iconSize)
}
}
val placeholderIcon = item.getPlaceholderIcon()
ShapedLauncherIcon(
modifier = modifier,
size = size,
icon = icon,
placeholder = placeholderIcon,
onClick = onClick,
onLongClick = onLongClick
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ShapedLauncherIcon(
modifier: Modifier = Modifier,
size: Dp = 64.dp,
icon: LauncherIcon?,
placeholder: PlaceholderIcon,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null
) {
val iconShape = LocalLauncherIconShape.current
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val fgScale by animateFloatAsState(if (isPressed) 0.75f else 1f)
val bgScale by animateFloatAsState(if (isPressed) 1.25f else 1f)
Surface(
shape = iconShape,
elevation = animateDpAsState(if (isPressed) 4.dp else 1.dp).value,
modifier = modifier
.requiredSize(size)
) {
Box(
modifier = Modifier
.requiredSize(size)
.background(
color = if (icon == null) {
placeholder.color.copy(alpha = 0.4f).compositeOver(MaterialTheme.colors.surface)
} else {
Color.Gray
}
)
.conditional(
onClick != null || onLongClick != null, Modifier.combinedClickable(
onClick = {
onClick?.invoke()
},
onLongClick = {
onLongClick?.invoke()
},
interactionSource = interactionSource,
indication = LocalIndication.current
)
)
) {
if (icon == null) {
Icon(
imageVector = placeholder.icon, contentDescription = null,
tint = placeholder.color,
modifier = Modifier
.scale(fgScale)
.align(Alignment.Center)
)
} else {
val fg = icon.foreground
val bg = icon.background
Canvas(
modifier = Modifier
.size(size)
.align(Alignment.Center)
) {
drawIntoCanvas {
val actualSize = size.toPx() * icon.backgroundScale * bgScale
val offset = (size.toPx() - actualSize) / 2
bg?.setBounds(
offset.toInt(),
offset.toInt(),
(offset + actualSize).toInt(),
(offset + actualSize).toInt()
)
bg?.draw(it.nativeCanvas)
}
}
Canvas(
modifier = Modifier
.size(size)
.align(Alignment.Center)
) {
drawIntoCanvas {
val actualSize = size.toPx() * icon.foregroundScale * fgScale
val offset = (size.toPx() - actualSize) / 2
fg.setBounds(
offset.toInt(),
offset.toInt(),
(offset + actualSize).toInt(),
(offset + actualSize).toInt()
)
fg.draw(it.nativeCanvas)
}
}
}
}
}
}
/*private fun getSystemShape(): AndroidPath? {
return if (isAtLeastApiLevel(Build.VERSION_CODES.O)) {
AdaptiveIconDrawable(null, null).iconMask
} else {
null
}
}
private fun getIconShape(shape: LauncherIconShape): Shape {
return when (shape) {
LauncherIconShape.Circle -> CircleShape
LauncherIconShape.Square -> RectangleShape
LauncherIconShape.RoundedSquare -> RoundedCornerShape(13)
LauncherIconShape.Hexagon -> GenericShape {
moveTo(it.width * 0.25f, it.height * 0.933f)
lineTo(it.width * 0.75f, it.height * 0.933f)
lineTo(it.width * 1.0f, it.height * 0.5f)
lineTo(it.width * 0.75f, it.height * 0.067f)
lineTo(it.width * 0.25f, it.height * 0.067f)
lineTo(0f, it.height * 0.5f)
close()
}
LauncherIconShape.PlatformDefault -> {
val platformShape = getSystemShape() ?: return CircleShape
GenericShape {
val matrix = AndroidMatrix()
val bounds = RectF()
platformShape.computeBounds(bounds, true)
matrix.setRectToRect(bounds, RectF(0f, 0f, it.width, it.height), AndroidMatrix.ScaleToFit.CENTER)
platformShape.transform(matrix)
addPath(platformShape.asComposePath())
}
}
else -> CircleShape
}
}*/
@@ -0,0 +1,103 @@
package de.mm20.launcher2.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.component.NavBarSpacer
import de.mm20.launcher2.ui.widget.WidgetCard
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetViewModel
@OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class, ExperimentalAnimationGraphicsApi::class
)
@Composable
fun WidgetColumn(
modifier: Modifier = Modifier,
scrollState: ScrollState
) {
val systemUiController = rememberSystemUiController()
var widgets by remember { mutableStateOf(listOf<Widget>()) }
val viewModel: WidgetViewModel = viewModel()
var editMode by remember { mutableStateOf(false) }
LaunchedEffect(null) {
widgets = viewModel.getWidgets()
}
val isLightTheme = MaterialTheme.colors.isLight
val windowHeight = LocalWindowSize.current.height
val background = 1f - (scrollState.value * 2 / windowHeight).coerceIn(0f, 1f)
Box(
modifier = modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
background to Color.Transparent,
background to MaterialTheme.colors.surface.copy(alpha = 0.8f)
)
)
) {
Column(
Modifier
.padding(horizontal = 8.dp)
.verticalScroll(scrollState)
.navigationBarsPadding()
) {
ClockWidget(transparentBackground = background > 0.75f)
AnimatedVisibility(visible = scrollState.value == 0) {
NavBarSpacer()
}
for (widget in widgets) {
WidgetCard(widget = widget)
}
ColorSchemeTest()
val icon = animatedVectorResource(id = R.drawable.anim_ic_edit_add)
ExtendedFloatingActionButton(
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterHorizontally),
text = {
Text(
modifier = Modifier.animateContentSize(),
text = stringResource(if (editMode) R.string.widget_add_widget else R.string.menu_edit_widgets)
)
},
icon = {
Icon(painter = icon.painterFor(atEnd = editMode), contentDescription = null)
},
backgroundColor = MaterialTheme.colors.surface,
onClick = {
editMode = !editMode
})
}
}
}
@@ -0,0 +1,97 @@
package de.mm20.launcher2.ui.activity
import android.app.WallpaperManager
import android.appwidget.AppWidgetHost
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.*
import androidx.compose.ui.geometry.Size
import androidx.core.view.WindowCompat
import androidx.core.view.doOnLayout
import androidx.lifecycle.lifecycleScope
import com.google.accompanist.insets.ProvideWindowInsets
import de.mm20.launcher2.ui.LauncherMainScreen
import de.mm20.launcher2.ui.LauncherTheme
import de.mm20.launcher2.ui.locals.LocalAppWidgetHost
import de.mm20.launcher2.ui.locals.LocalColorScheme
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.theme.WallpaperColors
import de.mm20.launcher2.ui.theme.colors.DefaultColorScheme
import de.mm20.launcher2.ui.theme.colors.WallpaperColorScheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ComposeActivity : AppCompatActivity() {
private lateinit var widgetHost: AppWidgetHost
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//WindowCompat.setDecorFitsSystemWindows(window, false)
widgetHost = AppWidgetHost(applicationContext, 0xacac)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
var windowSize by remember { mutableStateOf(Size(0f, 0f)) }
findViewById<View>(android.R.id.content).doOnLayout {
windowSize = Size(it.width.toFloat(), it.height.toFloat())
}
var wallpaperColors by remember { mutableStateOf<WallpaperColors?>(null) }
LaunchedEffect(null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
val wallpaperManager = WallpaperManager.getInstance(this@ComposeActivity)
wallpaperManager.addOnColorsChangedListener({ colors, which ->
if (colors != null && which or WallpaperManager.FLAG_SYSTEM != 0) {
wallpaperColors = WallpaperColors.fromPlatformType(colors)
}
}, Handler(Looper.getMainLooper()))
lifecycleScope.launch {
val colors = withContext(Dispatchers.IO) {
wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
} ?: return@launch
wallpaperColors = WallpaperColors.fromPlatformType(colors)
}
}
}
if (windowSize.height <= 0 || windowSize.width <= 0) return@setContent
val colorScheme = wallpaperColors?.let { WallpaperColorScheme(it) } ?: DefaultColorScheme()
ProvideWindowInsets {
CompositionLocalProvider(
LocalAppWidgetHost provides widgetHost,
LocalWindowSize provides windowSize,
LocalColorScheme provides colorScheme,
) {
LauncherTheme {
LauncherMainScreen()
}
}
}
}
}
override fun onStart() {
super.onStart()
widgetHost.startListening()
}
override fun onStop() {
super.onStop()
widgetHost.stopListening()
}
}
@@ -0,0 +1,23 @@
package de.mm20.launcher2.ui.compat
import androidx.annotation.DrawableRes
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import de.mm20.launcher2.ui.R
@Composable
fun animatedVectorResource(@DrawableRes id: Int): AnimatedVectorResourceStub {
return AnimatedVectorResourceStub(id)
}
class AnimatedVectorResourceStub(
val res: Int
) {
@Composable
fun painterFor(atEnd: Boolean): Painter {
return ColorPainter(MaterialTheme.colors.onSurface)
}
}
@@ -0,0 +1,23 @@
package de.mm20.launcher2.ui.component
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun Chip(
content: @Composable RowScope.() -> Unit
) {
Row(
modifier = Modifier
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.1f), shape = MaterialTheme.shapes.large)
.padding(horizontal = 16.dp, vertical = 8.dp),
content = content
)
}
@@ -0,0 +1,19 @@
package de.mm20.launcher2.ui.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
@Composable
fun ChipGroup(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
FlowRow(
modifier = modifier,
content = content,
mainAxisSpacing = 16.dp,
crossAxisSpacing = 8.dp
)
}
@@ -0,0 +1,184 @@
package de.mm20.launcher2.ui.component
import android.text.format.DateFormat
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import de.mm20.launcher2.ui.locals.LocalColorScheme
import java.text.SimpleDateFormat
import java.util.*
@Composable
fun DigitalClock(time: Long) {
val format = SimpleDateFormat(
if (DateFormat.is24HourFormat(LocalContext.current))
"HH\nmm" else "hh\nmm",
Locale.getDefault()
)
Text(
modifier = Modifier.padding(4.dp),
text = format.format(time),
style = MaterialTheme.typography.h1.copy(
fontSize = 100.sp,
fontWeight = FontWeight.Black,
textAlign = TextAlign.Center,
lineHeight = 0.8.em,
letterSpacing = -0.1.em
)
)
}
@Composable
fun BinaryClock(time: Long) {
val date = Calendar.getInstance()
date.timeInMillis = time
val minute = date[Calendar.MINUTE]
var hour = date[Calendar.HOUR]
if (hour == 0) hour = 12
Row(
modifier = Modifier.padding(bottom = 24.dp)
) {
for (i in 0 until 10) {
val active = if (i < 4) {
hour and (1 shl (3 - i)) != 0
} else {
minute and (1 shl (9 - i)) != 0
}
Box(
modifier = Modifier
.padding(4.dp)
.size(12.dp)
.background(
LocalContentColor.current.copy(
if (active) 1f else 0.45f
)
)
)
if (i == 3) {
Box(Modifier.size(8.dp))
}
}
}
}
@Composable
fun AnalogClock(time: Long) {
val date = Calendar.getInstance()
date.timeInMillis = time
val minute = date[Calendar.MINUTE]
val hour = date[Calendar.HOUR]
val dark = !MaterialTheme.colors.isLight
val cs = LocalColorScheme.current
val bgColor = if (dark) cs.accent1.shade800 else cs.accent1.shade200
val hourColor = if (dark) cs.accent1.shade300 else cs.accent1.shade600
val minuteColor = if (dark) cs.accent1.shade200 else cs.accent1.shade700
val textColor = if (dark) cs.accent1.shade500 else cs.accent1.shade400
val hourAngle = 30f * hour + 0.5f * minute
val minuteAngle = 6f * minute
Surface(
modifier = Modifier
.padding(bottom = 24.dp)
.size(156.dp),
shape = CircleShape,
color = bgColor
) {
Box(
modifier = Modifier.fillMaxSize()
) {
Text(
text = "12",
style = MaterialTheme.typography.h1.copy(
fontSize = 32.sp,
lineHeight = 32.sp,
),
color = textColor,
modifier = Modifier
.padding(10.dp, 2.dp)
.align(Alignment.TopCenter),
)
Text(
text = "3",
style = MaterialTheme.typography.h1.copy(
fontSize = 32.sp
),
color = textColor,
modifier = Modifier
.padding(10.dp, 2.dp)
.align(Alignment.CenterEnd),
)
Text(
text = "6",
style = MaterialTheme.typography.h1.copy(
fontSize = 32.sp
),
color = textColor,
modifier = Modifier
.padding(10.dp, 2.dp)
.align(Alignment.BottomCenter),
)
Text(
text = "9",
style = MaterialTheme.typography.h1.copy(
fontSize = 32.sp
),
color = textColor,
modifier = Modifier
.padding(10.dp, 2.dp)
.align(Alignment.CenterStart),
)
}
Canvas(
modifier = Modifier
.fillMaxSize()
) {
rotate(
degrees = hourAngle - 180f
) {
drawLine(
strokeWidth = 12.dp.toPx(),
start = size.center.plus(Offset(0f, size.width / 5f)),
end = size.center,
color = hourColor,
cap = StrokeCap.Round
)
}
rotate(
degrees = minuteAngle - 180f
) {
drawLine(
strokeWidth = 12.dp.toPx(),
start = size.center.plus(Offset(0f, size.width / 3f)),
end = size.center,
color = minuteColor,
cap = StrokeCap.Round
)
}
}
}
PointMode.Polygon
}
@@ -0,0 +1,157 @@
package de.mm20.launcher2.ui.component
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarBorder
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.theme.divider
import kotlin.math.roundToInt
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@Composable
fun DefaultSwipeActions(
item: Searchable,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable RowScope.() -> Unit
) {
val viewModel: FavoritesViewModel = viewModel()
val isPinned by viewModel.isPinned(item).observeAsState()
val isHidden by viewModel.isHidden(item).observeAsState()
val state = rememberSwipeableState(
SwipeAction.Default,
confirmStateChange = {
if (it == SwipeAction.Favorites) {
if (isPinned == true) {
viewModel.unpinItem(item)
} else {
viewModel.pinItem(item)
}
}
false
}
)
val bgColor =
if (state.offset.value > 0f) colorResource(id = R.color.amber)
else colorResource(id = R.color.blue)
val isDismissing =
state.targetValue == SwipeAction.Favorites || state.targetValue == SwipeAction.Hide
BoxWithConstraints(modifier) {
val width = constraints.maxWidth.toFloat()
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val anchors = mapOf(
0f to SwipeAction.Default,
width to SwipeAction.Favorites,
-width to SwipeAction.Hide
)
val thresholds = { _: SwipeAction, _: SwipeAction ->
FractionalThreshold(0.5f)
}
Box(
Modifier.swipeable(
state = state,
anchors = anchors,
thresholds = thresholds,
orientation = Orientation.Horizontal,
enabled = enabled && state.currentValue == SwipeAction.Default,
reverseDirection = isRtl,
velocityThreshold = 10000.dp
)
) {
if (enabled) {
Row(
modifier = Modifier.matchParentSize()
) {
Card(
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.divider),
modifier = Modifier.fillMaxSize(),
elevation = 0.dp
) {
Box(
contentAlignment = if (state.offset.value > 0f) {
Alignment.CenterStart
} else {
Alignment.CenterEnd
}
) {
Box(
modifier = Modifier
.background(bgColor)
.fillMaxWidth(
animateFloatAsState(
if (isDismissing) 1f else 0f,
tween(200)
).value
)
.fillMaxHeight()
)
Icon(
imageVector = if (state.offset.value > 0f) {
if (isPinned == true) {
Icons.Rounded.StarBorder
} else {
Icons.Rounded.Star
}
} else {
if (isHidden == true) {
Icons.Rounded.Visibility
} else {
Icons.Rounded.VisibilityOff
}
},
tint = animateColorAsState(if (isDismissing) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSurface).value,
modifier = Modifier
.padding(horizontal = 16.dp)
.scale(animateFloatAsState(if (isDismissing) 1.2f else 1f).value),
contentDescription = null
)
}
}
}
}
Row(
content = content,
modifier = Modifier.offset { IntOffset(state.offset.value.roundToInt(), 0) }
)
}
}
}
enum class SwipeAction {
Default,
Favorites,
Hide
}
@@ -0,0 +1,28 @@
package de.mm20.launcher2.ui.component
/*
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.LocalContentAlpha
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieAnimationSpec
import com.airbnb.lottie.compose.LottieAnimationState
import com.airbnb.lottie.compose.rememberLottieAnimationState
@Composable
fun LottieIcon(
spec: LottieAnimationSpec,
modifier: Modifier = Modifier,
animationState: LottieAnimationState = rememberLottieAnimationState(autoPlay = true),
) {
LottieAnimation(
spec = spec,
modifier = modifier.alpha(LocalContentAlpha.current).requiredSize(24.dp),
animationState = animationState
)
}*/
@@ -0,0 +1,13 @@
package de.mm20.launcher2.ui.component
import androidx.compose.foundation.layout.Spacer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.google.accompanist.insets.navigationBarsHeight
import com.google.accompanist.insets.statusBarsHeight
@Composable
fun NavBarSpacer() = Spacer(modifier = Modifier.navigationBarsHeight())
@Composable
fun StatusBarSpacer() = Spacer(modifier = Modifier.statusBarsHeight())
@@ -0,0 +1,160 @@
package de.mm20.launcher2.ui.component
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.text.format.DateUtils
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import java.text.DateFormat
import java.util.*
@Composable
fun TextClock(
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
format: DateFormat
) {
TextClock(
modifier,
color,
fontSize,
fontStyle,
fontWeight,
fontFamily,
letterSpacing,
textDecoration,
textAlign,
lineHeight,
overflow,
softWrap,
maxLines,
onTextLayout,
style,
formatFunction = { format.format(Date(it)) }
)
}
@Composable
fun TextClock(
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
formatFlags: Int = DateUtils.FORMAT_SHOW_TIME
) {
val context = LocalContext.current
TextClock(
modifier,
color,
fontSize,
fontStyle,
fontWeight,
fontFamily,
letterSpacing,
textDecoration,
textAlign,
lineHeight,
overflow,
softWrap,
maxLines,
onTextLayout,
style,
formatFunction = { DateUtils.formatDateTime(context, it, formatFlags)}
)
}
@Composable
fun TextClock(
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
formatFunction: (time: Long) -> String
) {
var time by remember { mutableStateOf(System.currentTimeMillis()) }
val context = LocalContext.current
DisposableEffect(null) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
time = System.currentTimeMillis()
}
}
val filter = IntentFilter(Intent.ACTION_TIME_TICK).also {
it.addAction(Intent.ACTION_TIME_CHANGED)
it.addAction(Intent.ACTION_TIMEZONE_CHANGED)
}
context.registerReceiver(receiver, filter)
onDispose {
context.unregisterReceiver(receiver)
}
}
Text(
text = formatFunction(time),
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style
)
}
@@ -0,0 +1,194 @@
package de.mm20.launcher2.ui.component
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.integerResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
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 = "")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
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 ToggleToolbarAction -> {
IconToggleButton(action.isChecked, action.onCheckedChange) {
Icon(action.icon, contentDescription = action.label)
}
}
is SubmenuToolbarAction -> {
Box {
var showMenu by remember { mutableStateOf(false) }
IconButton({
showMenu = true
}) {
Icon(action.icon, contentDescription = action.label)
}
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(action.label, modifier = Modifier.weight(1f))
Icon(imageVector = Icons.Rounded.ArrowRight, contentDescription = null)
}
}
is ToggleToolbarAction -> {
DropdownMenuItem(
onClick = { action.onCheckedChange(!action.isChecked) },
) {
Text(action.label)
}
}
is DefaultToolbarAction -> {
DropdownMenuItem(onClick = {
action.action
onDismiss()
}) {
Text(action.label)
}
}
}
}
} 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
data class ToggleToolbarAction(
override val label: String,
override val icon: ImageVector,
val isChecked: Boolean,
val onCheckedChange: (Boolean) -> Unit
) : ToolbarAction
@Composable
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel = viewModel<FavoritesViewModel>()
val isPinned by viewModel.isPinned(item).observeAsState(false)
return ToggleToolbarAction(
label = stringResource(
if (isPinned) R.string.favorites_menu_unpin else R.string.favorites_menu_pin
),
icon = if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarBorder,
isChecked = isPinned,
onCheckedChange = {
if (it) {
viewModel.pinItem(item)
} else {
viewModel.unpinItem(item)
}
}
)
}
@Composable
fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel = viewModel<FavoritesViewModel>()
val isHidden by viewModel.isHidden(item).observeAsState(false)
return ToggleToolbarAction(
label = stringResource(
if (isHidden) R.string.menu_unhide else R.string.menu_hide
),
icon = if (isHidden) Icons.Rounded.Visibility else Icons.Rounded.VisibilityOff,
isChecked = isHidden,
onCheckedChange = {
if (it) {
viewModel.hideItem(item)
} else {
viewModel.unhideItem(item)
}
}
)
}
@@ -0,0 +1,620 @@
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()
}
}
@@ -0,0 +1,145 @@
package de.mm20.launcher2.ui.icons
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.*
data class PlaceholderIcon(
val color: Color,
val icon: ImageVector
)
@Composable
fun Searchable.getPlaceholderIcon(): PlaceholderIcon {
return when (this) {
is Application -> getPlaceholderIcon()
is File -> getPlaceholderIcon()
else -> PlaceholderIcon(
Color.LightGray,
Icons.Rounded.Circle
)
}
}
@Composable
fun Application.getPlaceholderIcon(): PlaceholderIcon {
return PlaceholderIcon(
MaterialTheme.colors.androidGreen,
Icons.Rounded.Android
)
}
@Composable
fun File.getPlaceholderIcon(): PlaceholderIcon {
return when {
isDirectory -> PlaceholderIcon(
MaterialTheme.colors.lightBlue,
Icons.Rounded.Folder
)
mimeType.startsWith("image/") -> PlaceholderIcon(
MaterialTheme.colors.teal,
Icons.Rounded.Image
)
mimeType.startsWith("audio/") -> PlaceholderIcon(
MaterialTheme.colors.orange,
Icons.Rounded.Audiotrack
)
mimeType.startsWith("video/") -> PlaceholderIcon(
MaterialTheme.colors.purple,
Icons.Rounded.Movie
)
/*
else -> when (mimeType) {
"application/vnd.google-apps.drawing" -> R.drawable.ic_file_picture to R.color.teal
}*/
else -> when (mimeType) {
"application/pdf" -> PlaceholderIcon(
MaterialTheme.colors.red,
Icons.Rounded.Pdf
)
"application/zip",
"application/x-gtar",
"application/x-tar",
"application/java-archive",
"application/x-7z-compressed",
"application/x-compressed-tar",
"application/x-zip-compressed",
"application/x-gzip",
"application/x-bzip2" -> PlaceholderIcon(
MaterialTheme.colors.brown,
Icons.Rounded.Archive
)
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"text/plain",
"application/x-iwork-pages-sffpages",
"application/vnd.apple.pages",
"application/vnd.google-apps.document" -> PlaceholderIcon(
MaterialTheme.colors.blue,
Icons.Rounded.Notes
)
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel",
"application/x-iwork-numbers-sffnumbers",
"application/vnd.apple.numbers",
"application/vnd.google-apps.spreadsheet" -> PlaceholderIcon(
MaterialTheme.colors.lightGreen,
Icons.Rounded.BorderAll
)
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint",
"application/x-iwork-keynote-sffkey",
"application/vnd.apple.keynote",
"application/vnd.google-apps.presentation" -> PlaceholderIcon(
MaterialTheme.colors.amber,
Icons.Rounded.Slideshow
)
"application/vnd.android.package-archive" -> PlaceholderIcon(
MaterialTheme.colors.androidGreen,
Icons.Rounded.Android
)
"text/x-asm",
"text/x-c",
"text/x-java-source",
"text/x-script.phyton",
"text/x-pascal",
"text/x-script.perl",
"text/javascript",
"application/json" -> PlaceholderIcon(
MaterialTheme.colors.pink,
Icons.Rounded.Code
)
"text/xml",
"text/html" -> PlaceholderIcon(
MaterialTheme.colors.deepOrange,
Icons.Rounded.Code
)
"application/vnd.google-apps.form" -> PlaceholderIcon(
MaterialTheme.colors.deepPurple,
Icons.Rounded.ViewList
)
"application/epub+zip" -> PlaceholderIcon(
MaterialTheme.colors.blue,
Icons.Rounded.Book
)
else -> PlaceholderIcon(
MaterialTheme.colors.blueGray,
Icons.Rounded.InsertDriveFile
)
}
}
}
@@ -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,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,811 @@
package de.mm20.launcher2.ui.legacy.activity
import android.Manifest
import android.animation.AnimatorSet
import android.animation.LayoutTransition
import android.animation.ObjectAnimator
import android.app.Activity
import android.app.WallpaperManager
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.Point
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.ViewGroupOverlay
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.core.animation.doOnEnd
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.view.*
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.afollestad.materialdialogs.LayoutMode
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.list.listItems
import com.jmedeisis.draglinearlayout.DragLinearLayout
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.icons.DynamicIconController
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.isBrightColor
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.transition.OneShotLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.component.EditFavoritesView
import de.mm20.launcher2.ui.legacy.component.WidgetView
import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur
import de.mm20.launcher2.ui.legacy.search.SearchGridView
import de.mm20.launcher2.ui.legacy.widget.LauncherWidget
import de.mm20.launcher2.weather.WeatherViewModel
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType
import de.mm20.launcher2.widgets.WidgetViewModel
import kotlinx.android.synthetic.main.activity_launcher.*
import kotlinx.coroutines.*
import java.util.*
import kotlin.math.roundToInt
class LauncherActivity : AppCompatActivity() {
/**
* True if the search result list is visible
*/
private var searchVisibility = false
private lateinit var widgetHost: AppWidgetHost
private val widgets = mutableListOf<Widget>()
private lateinit var overlayView: ViewGroupOverlay
private lateinit var searchViewModel: SearchViewModel
private lateinit var widgetViewModel: WidgetViewModel
private val preferences = LauncherPreferences.instance
private var widgetEditMode = false
set(value) {
field = value
if (value) {
widgetSpacer.visibility = View.GONE
smartWidget.visibility = View.GONE
searchBar.setRightIcon(R.drawable.ic_done)
scrollView.setOnTouchListener(null)
for (v in widgetList.iterator()) {
if (v is WidgetView) {
v.editMode = true
v.onResizeModeChange = {
OneShotLayoutTransition.run(widgetList)
OneShotLayoutTransition.run(widgetContainer)
}
}
}
OneShotLayoutTransition.run(widgetList)
OneShotLayoutTransition.run(widgetContainer)
OneShotLayoutTransition.run(scrollContainer)
fabEditWidget.apply {
setIconResource(R.drawable.ic_add)
setText(R.string.widget_add_widget)
setOnClickListener {
addWidget()
}
}
val statusBarColor = TypedValue().also {
theme.resolveAttribute(
R.attr.colorSurface,
it,
true
)
}.data
window.statusBarColor = statusBarColor
if (statusBarColor.isBrightColor()) {
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
insetsController.isAppearanceLightStatusBars = true
}
searchBar.visibility = View.INVISIBLE
editWidgetToolbar
.animate()
.translationY(0f)
.alpha(1f)
.withStartAction {
editWidgetToolbar.visibility = View.VISIBLE
}
.start()
} else {
widgetViewModel.saveWidgets(widgets)
widgetSpacer.visibility = View.VISIBLE
widgetList.layoutTransition = ChangingLayoutTransition()
widgetContainer.layoutTransition = ChangingLayoutTransition()
scrollContainer.layoutTransition = ChangingLayoutTransition()
searchBar.setRightIcon(R.drawable.ic_more_vert)
scrollView.setOnTouchListener(scrollViewOnTouchListener)
smartWidget.visibility = View.VISIBLE
for (v in widgetList.iterator()) {
if (v is WidgetView) {
v.editMode = false
v.layoutTransition = ChangingLayoutTransition()
}
}
fabEditWidget.apply {
setIconResource(R.drawable.ic_edit)
setText(R.string.menu_edit_widgets)
setOnClickListener {
widgetEditMode = true
}
}
window.statusBarColor = Color.TRANSPARENT
updateSystemBarAppearance()
searchBar.visibility = View.VISIBLE
editWidgetToolbar
.animate()
.translationY(-editWidgetToolbar.height.toFloat())
.alpha(0f)
.withEndAction {
editWidgetToolbar.visibility = View.GONE
}
.start()
}
}
private fun updateSystemBarAppearance() {
val allowLightSystemBars = allowsLightSystemBars()
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
insetsController.isAppearanceLightNavigationBars =
allowLightSystemBars && preferences.lightNavBar
insetsController.isAppearanceLightStatusBars =
allowLightSystemBars && preferences.lightStatusBar
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (LauncherPreferences.instance.firstRunVersion < 1) {
ActivityCompat.requestPermissions(
this, arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
), PermissionsManager.ALL
)
LauncherPreferences.instance.firstRunVersion = 1
}
WindowCompat.setDecorFitsSystemWindows(window, false)
setContentView(R.layout.activity_launcher)
overlayView = rootView.overlay
searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
widgetViewModel = ViewModelProvider(this)[WidgetViewModel::class.java]
scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
searchContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
widgetContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val params = widgetSpacer.layoutParams as LinearLayout.LayoutParams
params.topMargin = Point().also { windowManager.defaultDisplay.getSize(it) }.y
widgetSpacer.layoutParams = params
container.doOnNextLayout {
adjustWidgetSpace()
}
initWidgets()
if (preferences.blurCards && preferences.cardOpacity < 0xFF) {
container.viewTreeObserver.addOnPreDrawListener {
blurView.invalidate()
true
}
}
scrollView.setOnTouchListener(scrollViewOnTouchListener)
scrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, oldScrollY: Int ->
when {
/* Hide searchbar*/
scrollY > oldScrollY && ((searchVisibility && scrollY > searchBar.height) || widgetEditMode ||
scrollY > widgetSpacer.height + searchBar.height
+ (widgetSpacer.layoutParams as LinearLayout.LayoutParams).topMargin) -> {
var newTransY = searchBar.translationY - scrollY + oldScrollY
if (newTransY < -searchBar.height.toFloat() * 1.5f) {
newTransY = -searchBar.height.toFloat() * 1.5f
}
searchBar.translationY = newTransY
}
/* Show searchbar*/
scrollY < oldScrollY -> {
var newTransY = searchBar.translationY - scrollY + oldScrollY
if (newTransY > 0f) {
newTransY = 0f
}
searchBar.translationY = newTransY
}
}
if (scrollY > 0 && (searchVisibility || widgetEditMode ||
scrollY > widgetSpacer.height
+ (widgetSpacer.layoutParams as LinearLayout.LayoutParams).topMargin)
) {
searchBar.raise()
} else searchBar.drop()
if (scrollY == 0) {
smartWidget.translucent = true
if (!searchVisibility) searchBar.hide()
} else {
smartWidget.translucent = false
searchBar.show()
}
}
searchBar.onRightIconClick = onRightIconClick@{
if (widgetEditMode) widgetEditMode = false
else {
val menu = PopupMenu(this, it)
menu.inflate(R.menu.menu_launcher)
menu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_item_settings -> {
finish()
startActivity(Intent().also {
it.component = ComponentName(packageName, "de.mm20.launcher2.activity.SettingsActivity")
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}
R.id.menu_item_wallpaper -> {
startActivity(
Intent.createChooser(
Intent(Intent.ACTION_SET_WALLPAPER),
null
)
)
}
R.id.menu_item_hidden -> {
val layout = NestedScrollView(this)
layout.clipChildren = false
layout.layoutParams = ViewGroup.LayoutParams(
MATCH_PARENT,
WRAP_CONTENT
)
val hiddenItemsGrid = SearchGridView(this)
hiddenItemsGrid.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT,
WRAP_CONTENT
).apply {
setMargins((8 * dp).toInt())
}
hiddenItemsGrid.columnCount =
resources.getInteger(R.integer.config_columnCount)
val hiddenItems =
ViewModelProvider(this)[FavoritesViewModel::class.java].hiddenItems
hiddenItems.observe(this) {
hiddenItemsGrid.submitItems(it)
}
layout.addView(hiddenItemsGrid)
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
.show {
title(R.string.menu_hidden_items)
customView(view = layout)
negativeButton(R.string.close) { dismiss() }
}
//hiddenAppsActivated = true
}
R.id.menu_item_edit_favs -> {
val view = EditFavoritesView(this@LauncherActivity)
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT)).show {
customView(view = view)
title(res = R.string.menu_item_edit_favs)
positiveButton(res = R.string.close) {
it.dismiss()
}
onDismiss {
view.save()
}
}
}
}
true
}
menu.show()
}
}
searchBar.setOnTouchListener { _, _ ->
if (!searchVisibility) showSearch()
false
}
searchBar.onSearchQueryChanged = {
search(it)
}
widgetList.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
fabEditWidget.setOnClickListener {
widgetEditMode = true
}
editWidgetToolbar.apply {
navigationIcon =
ContextCompat.getDrawable(this@LauncherActivity, R.drawable.ic_done)?.apply {
setTint(ContextCompat.getColor(this@LauncherActivity, R.color.icon_color))
}
setNavigationOnClickListener {
widgetEditMode = false
}
}
lifecycle.addObserver(DynamicIconController.getInstance(this))
lifecycleScope.launch {
widgets.addAll(widgetViewModel.getWidgets())
initWidgets()
}
}
private fun initWidgets() {
widgetHost = AppWidgetHost(applicationContext, 0xacab)
widgetList.removeAllViews()
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
params.topMargin = (8 * dp).roundToInt()
for (w in widgets) {
val view = WidgetView(this)
view.layoutTransition = ChangingLayoutTransition()
view.layoutParams = params
if (view.setWidget(w, widgetHost)) {
widgetList.addDragView(view, view.getDragHandle())
view.onRemove = {
OneShotLayoutTransition.run(widgetList)
OneShotLayoutTransition.run(widgetContainer)
widgetList.removeDragView(view)
removeWidget(view.widget)
}
view.onResizeModeChange = {
OneShotLayoutTransition.run(widgetList)
OneShotLayoutTransition.run(widgetContainer)
}
}
}
widgetList.setOnViewSwapListener { _, firstPosition, _, secondPosition ->
Collections.swap(widgets, firstPosition, secondPosition)
}
updateWidgets()
}
private fun addWidget() {
val viewModel = ViewModelProvider(this)[WidgetViewModel::class.java]
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
val internalWidgets =
viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
if (internalWidgets.isNotEmpty()) {
MaterialDialog(this).show {
val widgetList =
this@LauncherActivity.findViewById<DragLinearLayout>(R.id.widgetList)
val widgetContainer =
this@LauncherActivity.findViewById<LinearLayout>(R.id.widgetContainer)
title(R.string.widget_add_widget)
listItems(items = internalWidgets.map { it.label }) { dialog, index, _ ->
val widget = internalWidgets[index]
val view = WidgetView(this@LauncherActivity)
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
params.topMargin = (8 * dp).roundToInt()
view.layoutParams = params
if (view.setWidget(widget, widgetHost)) {
view.editMode = true
widgetList.addDragView(view, view.getDragHandle())
view.onRemove = {
OneShotLayoutTransition.run(widgetList)
OneShotLayoutTransition.run(widgetContainer)
widgetList.removeDragView(view)
removeWidget(view.widget)
}
view.onResizeModeChange = {
OneShotLayoutTransition.run(widgetList)
OneShotLayoutTransition.run(widgetContainer)
}
widgets.add(widget)
}
dialog.dismiss()
}
@Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged.
neutralButton(R.string.widget_add_external) {
val appWidgetId = widgetHost.allocateAppWidgetId()
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
it.dismiss()
}
}
} else {
val appWidgetId = widgetHost.allocateAppWidgetId()
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
}
}
private fun removeWidget(widget: Widget?) {
widget ?: return
widgets.remove(widget)
val id = widget.data.toIntOrNull() ?: return
widgetHost.deleteAppWidgetId(id)
}
private fun adjustWidgetSpace() {
val firstWidget = smartWidget
if (firstWidget == null) {
val m = scrollContainer.paddingTop
val params = widgetSpacer.layoutParams as LinearLayout.LayoutParams
params.topMargin =
scrollView.height - m - widgetContainer.paddingTop - widgetSpacer.height
widgetSpacer.layoutParams = params
return
}
val m = scrollContainer.paddingTop +
(firstWidget.layoutParams as LinearLayout.LayoutParams).run { topMargin + bottomMargin }
val params = widgetSpacer.layoutParams as LinearLayout.LayoutParams
params.topMargin =
scrollView.height - firstWidget.measuredHeight - m - widgetContainer.paddingTop - widgetSpacer.height
widgetSpacer.layoutParams = params
}
private fun search(text: String) {
searchViewModel.search(text)
if (webSearchViewSpacer.tag != "measured" || webSearchViewSpacer.height == 0) {
val webSearchView = searchBar.getWebSearchView()
webSearchView.doOnNextLayout {
webSearchViewSpacer.layoutParams = webSearchViewSpacer.layoutParams
.apply { height = webSearchView.height }
webSearchViewSpacer.tag = "measured"
}
}
webSearchViewSpacer.visibility = if (text.isBlank()) View.GONE else View.VISIBLE
}
private fun toggleSearch() {
if (searchVisibility) {
hideSearch()
} else {
showSearch()
}
}
private fun hideSearch() {
searchVisibility = false
val set = AnimatorSet()
set.duration = 300
set.doOnEnd {
searchContainer.visibility = View.GONE
widgetContainer.visibility = View.VISIBLE
}
set.playTogether(
ObjectAnimator.ofFloat(widgetContainer, "translationY", 0f),
ObjectAnimator.ofInt(scrollView, "scrollY", 0),
ObjectAnimator.ofFloat(
searchContainer, "translationY", 0f,
if (scrollView.scrollY > searchContainer.height / 2f) -searchContainer.height.toFloat() else scrollView.height.toFloat()
)
)
set.start()
scrollView.scrollTo(0, 0)
searchBar.hide()
if (!searchBar.getSearchQuery().isEmpty()) searchBar.setSearchQuery("")
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.hideSoftInputFromWindow(searchBar.windowToken, 0)
}
private fun showSearch() {
searchVisibility = true
searchBar.show()
searchContainer.visibility = View.VISIBLE
widgetContainer.visibility = View.GONE
val set = AnimatorSet()
set.duration = 300
set.doOnEnd {
search("")
}
set.playTogether(
ObjectAnimator.ofFloat(widgetContainer, "translationY", scrollView.height.toFloat()),
ObjectAnimator.ofInt(scrollView, "scrollY", 0),
ObjectAnimator.ofFloat(searchContainer, "translationY", scrollView.height.toFloat(), 0f)
)
set.start()
}
override fun onBackPressed() {
if (widgetEditMode) widgetEditMode = false
if (searchVisibility) hideSearch()
else ObjectAnimator.ofInt(scrollView, "scrollY", 0).setDuration(200).start()
}
override fun onResume() {
super.onResume()
ActivityStarter.resume()
ActivityStarter.create(rootView)
activityStartOverlay.visibility = View.INVISIBLE
val widgetViewModel by viewModels<WidgetViewModel>()
widgetViewModel.requestCalendarUpdate()
search(searchBar.getSearchQuery())
updateWidgets()
updateSystemBarAppearance()
container.doOnNextLayout {
WallpaperManager.getInstance(this).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f)
}
if (!LauncherPreferences.instance.hasRequestedNotificationPermission && !hasNotificationListenerPermission()) {
try {
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
} catch (e: ActivityNotFoundException) {
Toast.makeText(
this,
R.string.notification_permission_activity_not_found,
Toast.LENGTH_LONG
).show()
}
LauncherPreferences.instance.hasRequestedNotificationPermission = true
}
//getSystemService(Context.INPUT_METHOD_SERVICE)
// .castTo<InputMethodManager>()
// .hideSoftInputFromWindow(currentFocus?.windowToken, 0)
//overridePendingTransition(R.anim.app_to_launcher_in, R.anim.app_to_launcher_out)
}
private fun allowsLightSystemBars(): Boolean {
val dimWallpaper = LauncherPreferences.instance.dimWallpaper
val isDarkTheme = resources.getBoolean(R.bool.is_dark_theme)
return !(isDarkTheme && dimWallpaper)
}
private fun hasNotificationListenerPermission(): Boolean {
val listeners = NotificationManagerCompat.getEnabledListenerPackages(this)
for (listener in listeners) {
if (listener == packageName) return true
}
return false
}
private val themeListener = { key: String ->
recreate()
}
override fun onStart() {
super.onStart()
if (preferences.blurCards && preferences.cardOpacity < 0xFF) {
lifecycleScope.launch {
val wallpaper = withContext(Dispatchers.IO) {
WallpaperBlur.getCachedBitmap(this@LauncherActivity)
}
WallpaperBlur.blurredWallpaper = wallpaper
}
}
preferences.doOnPreferenceChange("is_light_wallpaper", action = themeListener)
widgetHost.startListening()
}
override fun onPause() {
super.onPause()
ActivityStarter.pause()
}
override fun onStop() {
super.onStop()
WallpaperBlur.blurredWallpaper?.takeIf { !it.isRecycled }?.recycle()
WallpaperBlur.blurredWallpaper = null
try {
widgetHost.stopListening()
} catch (e: NullPointerException) {
Log.e("MM20", Log.getStackTraceString(e))
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
onBackPressed()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
search(searchBar.getSearchQuery())
when (requestCode) {
PermissionsManager.LOCATION -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
}
PermissionsManager.CALENDAR -> {
ViewModelProvider(this)[WidgetViewModel::class.java].requestCalendarUpdate()
}
PermissionsManager.ALL -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
ViewModelProvider(this)[WidgetViewModel::class.java].requestCalendarUpdate()
search(searchBar.getSearchQuery())
}
}
}
private fun updateWidgets() {
var topWidget: LauncherWidget? = null
var topWidgetRanking = 0
var topWidgetView: WidgetView? = null
for (widget in widgetList.iterator()) {
if (widget is WidgetView) {
widget.update()
if (topWidgetRanking < widget.widgetView?.compactViewRanking ?: 0) {
topWidget = widget.widgetView
topWidgetRanking = widget.widgetView?.compactViewRanking ?: 0
topWidgetView = widget
}
}
}
val compactView = topWidget?.compactView
compactView?.update()
compactView?.goToParent = {
ObjectAnimator.ofFloat(
scrollView, "scrollY", topWidgetView?.top?.toFloat()
?: 0f
).start()
}
smartWidget.compactView = compactView
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (data == null) return
if (requestCode == REQUEST_PICK_APPWIDGET) {
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (widgetId == -1) return
if (resultCode == Activity.RESULT_OK) {
val appWidget = AppWidgetManager.getInstance(applicationContext)
.getAppWidgetInfo(widgetId) ?: return
if (appWidget.configure != null) {
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
intent.component = appWidget.configure
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
startActivityForResult(intent, REQUEST_BIND_APPWIDGET)
} else {
onActivityResult(REQUEST_BIND_APPWIDGET, Activity.RESULT_OK, data)
}
} else {
widgetHost.deleteAppWidgetId(widgetId)
}
}
if (requestCode == REQUEST_CREATE_APPWIDGET) {
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (widgetId == -1) return
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
startActivityForResult(intent, REQUEST_BIND_APPWIDGET)
}
if (requestCode == REQUEST_BIND_APPWIDGET && resultCode == Activity.RESULT_OK) {
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (widgetId == -1) return
val appWidget = AppWidgetManager.getInstance(applicationContext)
.getAppWidgetInfo(widgetId) ?: return
val widget = Widget(
type = WidgetType.THIRD_PARTY,
data = widgetId.toString(),
height = appWidget.minHeight
)
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
params.topMargin = (8 * dp).roundToInt()
val view = WidgetView(this)
view.layoutParams = params
if (view.setWidget(widget, widgetHost)) {
view.editMode = true
widgetList.addDragView(view, view.getDragHandle())
view.onRemove = {
OneShotLayoutTransition.run(widgetList)
OneShotLayoutTransition.run(widgetContainer)
widgetList.removeDragView(view)
removeWidget(view.widget)
}
view.onResizeModeChange = {
OneShotLayoutTransition.run(widgetList)
OneShotLayoutTransition.run(widgetContainer)
}
widgets.add(widget)
}
}
}
private val scrollViewOnTouchListener: (View, MotionEvent) -> Boolean = onTouch@{ _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> true
MotionEvent.ACTION_MOVE -> {
when {
scrollView.scrollY == 0 -> {
if (container.translationY >= searchBar.height) {
return@onTouch false
}
if (event.historySize > 0) {
val dY = event.y - event.getHistoricalY(0)
val newTransY = 0.4f * dY + container.translationY
if (newTransY > 0) {
container.translationY = newTransY
searchBar.show()
} else {
container.translationY = 0f
}
if (container.translationY == 0f) return@onTouch false
}
}
scrollView.scrollY == scrollContainer.height - scrollView.height && searchVisibility -> {
if (container.translationY <= -searchBar.height) {
return@onTouch false
}
if (event.historySize > 0) {
val dY = event.y - event.getHistoricalY(0)
val newTransY = 0.4f * dY + container.translationY
container.translationY =
if (newTransY <= 0) newTransY
else 0f
if (container.translationY == 0f) return@onTouch false
}
}
else -> return@onTouch false
}
true
}
MotionEvent.ACTION_UP -> {
if (container.translationY >= searchBar.height) toggleSearch()
if (container.translationY <= -searchBar.height) hideSearch()
container.animate().translationY(0f).setDuration(200).start()
if (!searchVisibility && scrollView.scrollY == 0) searchBar.hide()
false
}
else -> false
}
}
override fun onDestroy() {
super.onDestroy()
ActivityStarter.destroy()
}
companion object {
const val REQUEST_PICK_APPWIDGET = 4412
const val REQUEST_CREATE_APPWIDGET = 4460
const val REQUEST_BIND_APPWIDGET = 4124
}
}
@@ -0,0 +1,34 @@
package de.mm20.launcher2.ui.legacy.component
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.*
import de.mm20.launcher2.applications.AppViewModel
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.view_application.view.*
class ApplicationView : FrameLayout {
private val applications: LiveData<List<Application>>
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
View.inflate(context, R.layout.view_application, this)
layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
applications = ViewModelProvider(context as AppCompatActivity).get(AppViewModel::class.java).applications
applications.observe(context as AppCompatActivity, Observer<List<Application>> {
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
applicationGrid.submitItems(it)
})
}
}
@@ -0,0 +1,73 @@
package de.mm20.launcher2.ui.legacy.component
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.calculator.CalculatorViewModel
import de.mm20.launcher2.search.data.Calculator
import kotlinx.android.synthetic.main.view_calculator.view.*
import kotlin.math.round
class CalculatorView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val calculator: LiveData<Calculator?>
init {
View.inflate(context, R.layout.view_calculator, this)
calculator = ViewModelProvider(context as AppCompatActivity).get(CalculatorViewModel::class.java).calculator
calculator.observe(context as AppCompatActivity, Observer {
if (it == null) visibility = View.GONE
else {
visibility = View.VISIBLE
bind(it)
}
})
}
private fun bind(calc: Calculator) {
calculatorTerm.text = beautifyTerm(calc.term)
calculatorSolution.text = context.getString(R.string.calculator_solution, calc.formattedString)
if (calc.solution == round(calc.solution) && calc.term.matches(Regex("[0-9]+"))) {
val binHexOct = StringBuilder()
binHexOct.append(calc.formattedBinaryString).append("\n")
.append(calc.formattedOctString).append("\n")
.append(calc.formattedHexString)
calculatorSolutionHexBinOct.text = binHexOct.toString()
calculatorSolutionHexBinOct.visibility = View.VISIBLE
calculatorLabelHexBinOct.visibility = View.VISIBLE
} else {
calculatorSolutionHexBinOct.visibility = GONE
calculatorLabelHexBinOct.visibility = GONE
}
}
private fun beautifyTerm(term: String): String {
return term.replace(Regex("\\s+"), "")
.replace("pi", " \u03C0 ", ignoreCase = true)
.replace("*", " \u00D7 ")
.replace("-", " \u2212 ")
.replace("/", " \u2215 ")
.replace("+", " + ")
.replace(Regex("&{1,2}"), " \u2227 ")
.replace(Regex("\\|{1,2}"), " \u2228 ")
.replace("!=", " \u2260 ")
.replace("<>", " \u2260 ")
.replace(">=", " \u2265 ")
.replace("<=", " \u2264 ")
.replace("=", " = ")
.replace("<", " < ")
.replace(">", " > ")
}
}
@@ -0,0 +1,55 @@
package de.mm20.launcher2.ui.legacy.component
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.legacy.search.SearchListView
class CalendarView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val calendarEvents: LiveData<List<CalendarEvent>?>
init {
View.inflate(context, R.layout.view_search_category_list, this)
layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list)
calendarEvents = ViewModelProvider(context as AppCompatActivity).get(CalendarViewModel::class.java).calendarEvents
calendarEvents.observe(context as AppCompatActivity, {
if (it == null) {
visibility = View.GONE
return@observe
}
if (it.isEmpty() && LauncherPreferences.instance.searchCalendars && !PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) {
visibility = View.VISIBLE
list.submitItems(listOf(
MissingPermission(
context.getString(R.string.permission_calendar_search),
PermissionsManager.CALENDAR
)
))
return@observe
}
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
list.submitItems(it)
})
}
}
@@ -0,0 +1,54 @@
package de.mm20.launcher2.ui.legacy.component
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.contacts.ContactViewModel
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.search.SearchListView
class ContactView : FrameLayout {
private val contacts: LiveData<List<Contact>?>
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
View.inflate(context, R.layout.view_search_category_list, this)
layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
contacts = ViewModelProvider(context as AppCompatActivity).get(ContactViewModel::class.java).contacts
val list = findViewById<SearchListView>(R.id.list)
contacts.observe(context as AppCompatActivity, {
if (it == null) {
visibility = View.GONE
return@observe
}
if (it.isEmpty() && LauncherPreferences.instance.searchContacts && !PermissionsManager.checkPermission(context, PermissionsManager.CONTACTS)) {
visibility = View.VISIBLE
list.submitItems(listOf(
MissingPermission(
context.getString(R.string.permission_contact_search),
PermissionsManager.CONTACTS
)
))
return@observe
}
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
list.submitItems(it)
})
}
}
@@ -0,0 +1,32 @@
package de.mm20.launcher2.ui.legacy.component
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import de.mm20.launcher2.favorites.FavoritesItem
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.edit_favorites_row.view.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class EditFavoritesRow @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, val favoritesItem: FavoritesItem
) : LinearLayout(context, attrs, defStyleAttr) {
init {
View.inflate(context, R.layout.edit_favorites_row, this)
label.text = favoritesItem.searchable?.label
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(favoritesItem.searchable!!, (48*dp).toInt()).collect{
icon.icon = it
}
}
}
fun getDragHandle(): View {
return dragHandle
}
}
@@ -0,0 +1,141 @@
package de.mm20.launcher2.ui.legacy.component
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateMargins
import androidx.core.widget.TextViewCompat
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.favorites.FavoritesItem
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.setPadding
import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.dialog_edit_favorites.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class EditFavoritesView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
init {
View.inflate(context, R.layout.dialog_edit_favorites, this)
lifecycleScope.launch {
initView()
}
}
private lateinit var favorites: MutableList<FavoritesItem>
suspend fun initView() {
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
favorites = withContext(Dispatchers.IO) {
viewModel.getAllFavoriteItems().toMutableList()
}
progressBar.visibility = View.GONE
itemList.addView(getLabel(R.string.edit_favorites_dialog_stage0))
itemList.setContainerScrollView(scrollView)
var stage = 0
for (favorite in favorites) {
if (favorite.pinPosition <= 1 && stage == 0) {
getLabel(R.string.edit_favorites_dialog_stage1).let {
it.tag = "stage1"
itemList.addDragView(it, it.getChildAt(1))
}
stage++
}
if (favorite.pinPosition == 0 && stage == 1) {
getLabel(R.string.edit_favorites_dialog_stage2).let {
it.tag = "stage2"
itemList.addDragView(it, it.getChildAt(1))
}
stage++
}
val view = EditFavoritesRow(context, favoritesItem = favorite)
itemList.addDragView(view, view.getDragHandle())
}
if (stage == 0) {
getLabel(R.string.edit_favorites_dialog_stage1).let {
it.tag = "stage1"
itemList.addDragView(it, it.getChildAt(1))
}
stage++
}
if (stage == 1) {
getLabel(R.string.edit_favorites_dialog_stage2).let {
it.tag = "stage2"
itemList.addDragView(it, it.getChildAt(1))
}
}
itemList.setOnViewSwapListener { firstView, firstPosition, secondView, secondPosition ->
if (firstView is EditFavoritesRow && secondView is EditFavoritesRow) {
val firstItem = firstView.favoritesItem
val secondItem = secondView.favoritesItem
val i = firstItem.pinPosition
firstItem.pinPosition = secondItem.pinPosition
secondItem.pinPosition = i
return@setOnViewSwapListener
}
val fw = if (firstPosition > secondPosition) secondView else firstView
val sw = if (firstPosition > secondPosition) firstView else secondView
if (fw.tag == "stage1" && sw is EditFavoritesRow) {
favorites.forEach {
if (it.pinPosition > 1) {
it.pinPosition++
}
}
sw.favoritesItem.pinPosition = 2
return@setOnViewSwapListener
}
if (sw.tag == "stage1" && fw is EditFavoritesRow) {
favorites.forEach {
if (it.pinPosition > 1) {
it.pinPosition--
}
}
return@setOnViewSwapListener
}
if (fw.tag == "stage2" && sw is EditFavoritesRow) {
sw.favoritesItem.pinPosition = 1
return@setOnViewSwapListener
}
if (sw.tag == "stage2" && fw is EditFavoritesRow) {
fw.favoritesItem.pinPosition = 0
return@setOnViewSwapListener
}
}
}
fun save() {
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
viewModel.saveFavorites(favorites)
}
private fun getLabel(@StringRes label: Int): FrameLayout {
return FrameLayout(context).also {
it.addView(TextView(context).also {
TextViewCompat.setTextAppearance(it, R.style.TextAppearance_EditFavorites)
it.setText(label)
it.setPadding((8 * dp).toInt(), (4 * dp).toInt())
it.layoutParams = MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).also {
it.updateMargins(top = (2 * dp).toInt())
}
it.setBackgroundColor(ContextCompat.getColor(context, R.color.color_divider))
})
it.addView(View(context).also {
it.visibility = View.GONE
})
}
}
}
@@ -0,0 +1,36 @@
package de.mm20.launcher2.ui.legacy.component
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.*
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.view_favorites.view.*
class FavoritesView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val favorites: LiveData<List<Searchable>>
init {
View.inflate(context, R.layout.view_favorites, this)
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
favorites = viewModel.getFavorites(context.resources.getInteger(R.integer.config_columnCount))
favorites.observe(context as AppCompatActivity, Observer {
visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE
favoritesGrid.submitItems(it)
})
layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
favoritesCard.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
}
}
@@ -0,0 +1,53 @@
package de.mm20.launcher2.ui.legacy.component
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.files.FilesViewModel
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.search.SearchListView
class FileView : FrameLayout {
private val files: LiveData<List<File>?>
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
View.inflate(context, R.layout.view_search_category_list, this)
layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list)
files = ViewModelProvider(context as AppCompatActivity).get(FilesViewModel::class.java).files
files.observe(context as AppCompatActivity, {
if (it == null) {
visibility = View.GONE
return@observe
}
if (it.isEmpty() && !PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) {
visibility = View.VISIBLE
list.submitItems(listOf(
MissingPermission(
context.getString(R.string.permission_files_search),
PermissionsManager.EXTERNAL_STORAGE
)
))
return@observe
}
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
list.submitItems(it)
})
}
}
@@ -0,0 +1,262 @@
package de.mm20.launcher2.ui.legacy.component
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Color
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.postDelayed
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.SearchStyles
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
import kotlinx.android.synthetic.main.view_search_bar.view.*
class SearchBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.materialCardViewStyle
) : LauncherCardView(context, attrs, defStyleAttr) {
private var raised = false
private var visible = true
private var currentAnimator: Animator? = null
private val rightDrawable = LottieDrawable().apply {
composition = LottieCompositionFactory.fromRawResSync(context, R.raw.ic_menu_to_clear).value
repeatMode = LottieDrawable.REVERSE
}
init {
View.inflate(context, R.layout.view_search_bar, this)
overflowMenu.setImageDrawable(rightDrawable)
searchEdit.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val text = searchEdit.text.toString()
onSearchQueryChanged?.invoke(searchEdit.text.toString())
if (text.isEmpty()) {
if (rightDrawable.frame > rightDrawable.minFrame.toInt()) {
rightDrawable.speed = -1f
rightDrawable.resumeAnimation()
}
} else {
if (rightDrawable.frame < rightDrawable.maxFrame.toInt()) {
rightDrawable.speed = 1f
rightDrawable.resumeAnimation()
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
ViewModelProvider(context as AppCompatActivity)[SearchViewModel::class.java].isSearching.observe(context, Observer {
searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE
})
overflowMenu.setOnClickListener {
if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it)
else (setSearchQuery(""))
}
postDelayed(1) {
hide()
}
layoutTransition = ChangingLayoutTransition()
}
fun setRightIcon(iconRes: Int) {
overflowMenu.setImageResource(iconRes)
}
var onSearchQueryChanged: ((String) -> Unit)? = null
var onRightIconClick: ((View) -> Unit)? = null
override fun setOnTouchListener(l: OnTouchListener?) {
searchEdit.setOnTouchListener(l)
}
fun setSearchQuery(text: String) {
searchEdit.setText(text)
}
fun getSearchQuery(): String {
return searchEdit.text.toString()
}
/**
* Elevates the search bar to be higher than the other cards
*/
fun raise() {
if (raised) return
currentAnimator?.takeIf { it.isStarted }?.end()
currentAnimator = AnimatorSet().apply {
duration = 200
playTogether(
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", elevation, 7 * dp).apply {
interpolator = AccelerateInterpolator(3f)
},
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", 0xFF).apply {
interpolator = DecelerateInterpolator(3f)
}
)
}
currentAnimator?.start()
raised = true
}
/**
* Drops the search bar back down to the other cards niveau
*/
fun drop() {
if (!raised) return
currentAnimator?.takeIf { it.isStarted }?.end()
currentAnimator = AnimatorSet().apply {
duration = 200
playTogether(
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", 0f).apply {
interpolator = DecelerateInterpolator(3f)
},
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", LauncherPreferences.instance.cardOpacity).apply {
interpolator = AccelerateInterpolator(3f)
}
)
}
currentAnimator?.start()
raised = false
}
fun show() {
if (visible) return
currentAnimator?.takeIf { it.isStarted }?.end()
currentAnimator = getShowAnimator()
currentAnimator?.start()
visible = true
}
fun hide() {
if (!visible) return
currentAnimator?.takeIf { it.isStarted }?.end()
currentAnimator = getHideAnimator()
currentAnimator?.start()
visible = false
}
fun getWebSearchView(): View {
return webSearchView
}
private fun getHideAnimator(): AnimatorSet {
val searchStyle = LauncherPreferences.instance.searchStyle
return when (searchStyle) {
SearchStyles.NO_BG -> {
val iconColor = ContextCompat.getColor(context, R.color.icon_color)
val cardElevation = resources.getDimension(R.dimen.card_elevation)
val shadowY = resources.getDimension(R.dimen.elevation_shadow_1dp_y)
val shadowR = resources.getDimension(R.dimen.elevation_shadow_1dp_radius)
val shadowC = Color.argb(66, 0, 0, 0)
searchEdit.setShadowLayer(shadowR, 0f, shadowY, shadowC)
AnimatorSet().apply {
duration = 200
playTogether(
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", 0).apply {
interpolator = AccelerateInterpolator(3f)
},
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", -elevation).apply {
interpolator = DecelerateInterpolator(3f)
duration = 150
},
ObjectAnimator.ofArgb(searchEdit, "hintTextColor", searchEdit.hintTextColors.defaultColor, Color.WHITE),
ObjectAnimator.ofArgb(searchIcon, "colorFilter", iconColor, Color.WHITE),
ObjectAnimator.ofArgb(overflowMenu, "colorFilter", iconColor, Color.WHITE),
ObjectAnimator.ofFloat(searchIcon, "alpha", 1f),
ObjectAnimator.ofFloat(overflowMenu, "alpha", 1f),
ObjectAnimator.ofFloat(searchIcon, "elevation", cardElevation),
ObjectAnimator.ofFloat(overflowMenu, "elevation", cardElevation)
)
}
}
// Solid style
SearchStyles.SOLID -> {
AnimatorSet()
}
// Hidden style
else -> {
AnimatorSet().apply {
duration = 200
playTogether(
ObjectAnimator.ofFloat(this@SearchBar, "alpha", 0f)
)
}
}
}
}
private fun getShowAnimator(): AnimatorSet? {
return when (LauncherPreferences.instance.searchStyle) {
// Transparent style
SearchStyles.NO_BG -> {
val hint = ContextCompat.getColor(context, R.color.text_color_primary_disabled)
val iconAttrs = context.obtainStyledAttributes(R.style.LauncherTheme_IconStyle, intArrayOf(android.R.attr.alpha))
val iconAlpha = iconAttrs.getFloat(0, 0f)
iconAttrs.recycle()
val iconColor = ContextCompat.getColor(context, R.color.icon_color)
searchEdit.setShadowLayer(0f, 0f, 0f, 0)
AnimatorSet().apply {
duration = 200
playTogether(
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", 0f).apply {
interpolator = AccelerateInterpolator(3f)
},
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", LauncherPreferences.instance.cardOpacity).apply {
interpolator = DecelerateInterpolator(3f)
},
ObjectAnimator.ofArgb(searchEdit, "hintTextColor", Color.WHITE, hint),
ObjectAnimator.ofArgb(searchIcon, "colorFilter", Color.WHITE, iconColor),
ObjectAnimator.ofArgb(overflowMenu, "colorFilter", Color.WHITE, iconColor),
ObjectAnimator.ofFloat(searchIcon, "alpha", iconAlpha),
ObjectAnimator.ofFloat(overflowMenu, "alpha", iconAlpha),
ObjectAnimator.ofFloat(searchIcon, "elevation", 0f),
ObjectAnimator.ofFloat(overflowMenu, "elevation", 0f)
)
}
}
// Solid style
SearchStyles.SOLID -> {
null
}
// Hidden style
else -> {
AnimatorSet().apply {
duration = 200
playTogether(
ObjectAnimator.ofFloat(this@SearchBar, "alpha", 1f)
)
}
}
}
}
}
@@ -0,0 +1,191 @@
package de.mm20.launcher2.ui.legacy.component
import android.content.Context
import android.net.Uri
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.xwray.groupie.ExpandableGroup
import com.xwray.groupie.ExpandableItem
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import de.mm20.launcher2.ktx.sp
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.UnitConverterViewModel
import de.mm20.launcher2.unitconverter.UnitValue
import kotlinx.android.synthetic.main.view_unitconverter.view.*
import java.text.DateFormat
import java.util.*
import kotlin.math.min
class UnitConverterView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val unitConverter: LiveData<UnitConverter?>
private val adapter: GroupAdapter<GroupieViewHolder>
init {
View.inflate(context, R.layout.view_unitconverter, this)
unitConverter = ViewModelProvider(context as AppCompatActivity).get(UnitConverterViewModel::class.java).unitConverter
unitConverter.observe(context as AppCompatActivity, Observer {
if (it == null) visibility = View.GONE
else {
visibility = View.VISIBLE
bind(it)
}
})
adapter = GroupAdapter()
unitConverterValues.also {
it.adapter = adapter
it.addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
}
}
private fun bind(converter: UnitConverter) {
val title = converter.inputValue.formattedValue + " " + converter.inputValue.formattedName
unitConverterInput.text = title
/*val sb = StringBuilder()
for (unit in converter.values) {
sb.append("${unit.formatted}\n")
}
sb.removeSuffix("\n")
unitConverterValues.text = sb.toString()
unitConverterIcon.setImageResource(when (converter.dimension) {
Dimension.LENGTH -> R.drawable.ic_unit_length
Dimension.MASS -> R.drawable.ic_unit_mass
Dimension.TIME -> R.drawable.ic_unit_time
Dimension.DATA -> R.drawable.ic_unit_datasize
Dimension.VELOCITY -> R.drawable.ic_unit_velocity
else -> 0
})*/
adapter.clear()
val maxValueLength = converter.values.maxByOrNull { it.formattedValue.length }?.formattedValue?.length
?: 0
val section = Section().apply {
addAll(converter.values.subList(0, min(converter.values.size, 5)).map {
ValueItem(it, maxValueLength * 8f * sp)
})
}.also {
adapter.add(it)
}
if (converter.values.size > 5) {
showAllButton.visibility = View.VISIBLE
showAllButton.setOnClickListener {
section.addAll(converter.values.subList( 5, converter.values.size).map {
ValueItem(it, maxValueLength * 8f * sp)
})
showAllButton.visibility = View.GONE
showAllButton.setOnClickListener(null)
}
} else {
showAllButton.visibility = View.GONE
}
if (converter is CurrencyUnitConverter) {
val df = DateFormat.getDateInstance(DateFormat.SHORT)
val date = Date().apply {
time = converter.updateTimestamp
}
val infoText = SpannableStringBuilder()
.append("European Central Bank (${df.format(date)})", object : ClickableSpan() {
override fun onClick(widget: View) {
CustomTabsIntent
.Builder()
.setToolbarColor(0xFF003299.toInt())
.build()
.launchUrl(context,
Uri.parse("https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html"))
}
}, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
.append("")
.append(context.getString(R.string.disclaimer), object : ClickableSpan() {
override fun onClick(widget: View) {
MaterialDialog(context).show {
title(res = R.string.disclaimer)
message(res = R.string.disclaimer_currency_converter)
positiveButton(res = R.string.close) {
dismiss()
}
}
}
}, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
unitConverterInfo.apply {
text = infoText
visibility = View.VISIBLE
movementMethod = LinkMovementMethod.getInstance()
}
} else {
unitConverterInfo.visibility = View.GONE
}
}
}
class ValueItem(private val value: UnitValue, val valueWidth: Float) : Item() {
override fun getLayout(): Int {
return R.layout.unit_converter_row
}
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.itemView.findViewById<TextView>(R.id.value).let {
it.text = value.formattedValue
it.layoutParams = it.layoutParams.apply {
width = valueWidth.toInt()
}
}
viewHolder.itemView.findViewById<TextView>(R.id.name).text = value.formattedName
viewHolder.itemView.findViewById<TextView>(R.id.symbol).text = value.symbol
}
}
class ExpandItem : Item(), ExpandableItem {
private lateinit var expandableGroup: ExpandableGroup
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.itemView.visibility = if (expandableGroup.isExpanded) View.GONE else View.VISIBLE
viewHolder.itemView.setOnClickListener {
expandableGroup.onToggleExpanded()
viewHolder.itemView.visibility = View.GONE
}
}
override fun getLayout(): Int {
return R.layout.unit_converter_show_all
}
override fun setExpandableGroup(onToggleListener: ExpandableGroup) {
expandableGroup = onToggleListener
}
}
@@ -0,0 +1,73 @@
package de.mm20.launcher2.ui.legacy.component
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.Chip
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.WebsearchViewModel
import de.mm20.launcher2.search.data.Websearch
import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.view_websearch.view.*
class WebSearchView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val websearches: LiveData<List<Websearch>>
init {
View.inflate(context, R.layout.view_websearch, this)
val viewModel = ViewModelProvider(context as AppCompatActivity)[WebsearchViewModel::class.java]
websearches = viewModel.websearches
websearches.observe(context as AppCompatActivity, Observer {
updateWebsearches(it)
})
}
private fun updateWebsearches(websearches: List<Websearch>) {
visibility = if (websearches.isEmpty()) View.GONE else View.VISIBLE
webSearchList.removeAllViews()
for (search in websearches) {
val chip = Chip(context)
chip.text = search.label
if (search.icon != null) {
Glide.with(context)
.load(search.icon)
.into(object : SimpleTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
chip.chipIcon = resource
}
})
} else {
chip.chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_search)
chip.chipIconTint = ColorStateList.valueOf(search.color)
}
chip.chipStrokeWidth = 1 * dp
chip.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
chip.chipBackgroundColor = ContextCompat.getColorStateList(context, R.color.chip_background)
chip.setTextAppearanceResource(R.style.ChipTextAppearance)
chip.setOnClickListener {
ActivityStarter.start(context, chip, intent = search.getLaunchIntent())
}
webSearchList.addView(chip)
}
}
}
@@ -0,0 +1,42 @@
package de.mm20.launcher2.ui.legacy.component
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.websites.WebsiteViewModel
class WebsiteView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val website: LiveData<Website?>
init {
View.inflate(context, R.layout.view_search_category_single_item, this)
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST)
val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val card = findViewById<ViewGroup>(R.id.card)
websiteView.layoutParams = params
card.addView(websiteView)
website = ViewModelProvider(context as AppCompatActivity)[WebsiteViewModel::class.java].website
website.observe(context as AppCompatActivity, Observer {
visibility = if (it == null) View.GONE else View.VISIBLE
card.setOnClickListener { _ ->
ActivityStarter.start(context, websiteView, item = it)
}
websiteView.searchable = it
})
}
}
@@ -0,0 +1,156 @@
package de.mm20.launcher2.ui.legacy.component
import android.animation.LayoutTransition
import android.appwidget.AppWidgetHost
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.get
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.transition.OneShotLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
import de.mm20.launcher2.ui.legacy.widget.*
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType
import kotlinx.android.synthetic.main.view_widget.view.*
class WidgetView : LauncherCardView {
var onRemove: (() -> Unit)? = null
var widget: Widget? = null
var widgetView: LauncherWidget? = null
var editMode = false
set(value) {
if (value) {
widgetControlPanel.visibility = View.VISIBLE
val widget = widgetWrapper[2]
widget.visibility = View.GONE
widgetName.visibility = View.VISIBLE
visibility = View.VISIBLE
layoutTransition = OneShotLayoutTransition(this)
widgetView?.layoutTransition = null
widgetWrapper.layoutTransition = null
} else {
resizeMode = false
widgetControlPanel.visibility = View.GONE
val widget = widgetWrapper[2] as LauncherWidget
widget.visibility = View.VISIBLE
widgetName.visibility = View.GONE
visibility = if (widget.show) View.VISIBLE else View.GONE
layoutTransition = ChangingLayoutTransition()
widgetView?.layoutTransition = ChangingLayoutTransition()
widgetWrapper.layoutTransition = ChangingLayoutTransition()
}
field = value
}
private var resizeMode = false
set(value) {
if (value == field) return
onResizeModeChange?.invoke(value)
if (value) {
widgetResizeDragHandle.visibility = View.VISIBLE
val widget = widgetWrapper[2]
widget.visibility = View.VISIBLE
widgetName.visibility = View.GONE
} else {
widgetResizeDragHandle.visibility = View.GONE
if (editMode) {
val widget = widgetWrapper[2]
widget.visibility = View.GONE
widgetName.visibility = View.VISIBLE
}
}
layoutTransition = OneShotLayoutTransition(this)
field = value
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
View.inflate(context, R.layout.view_widget, this)
widgetActionResize.setOnClickListener {
resizeMode = !resizeMode
}
widgetActionRemove.setOnClickListener {
onRemove?.invoke()
}
layoutTransition = LayoutTransition().apply {
enableTransitionType(LayoutTransition.CHANGING)
}
elevation = if (backgroundOpacity < 255) 0f else resources.getDimension(R.dimen.card_elevation)
TooltipCompat.setTooltipText(widgetActionResize, context.getString(R.string.widget_action_adjust_height))
TooltipCompat.setTooltipText(widgetActionRemove, context.getString(R.string.widget_action_remove))
TooltipCompat.setTooltipText(widgetActionSettings, context.getString(R.string.widget_action_settings))
}
var onResizeModeChange: ((Boolean) -> Unit)? = null
fun setWidget(widget: Widget, widgetHost: AppWidgetHost): Boolean {
if (widget.type == WidgetType.INTERNAL) {
widgetView = when (widget.data) {
CalendarWidget.ID -> CalendarWidget(context)
WeatherWidget.ID -> WeatherWidget(context)
MusicWidget.ID -> MusicWidget(context)
else -> return false
}
widgetActionResize.visibility = View.GONE
widgetActionSettings.visibility = if (widgetView?.hasSettings == true) View.VISIBLE else View.GONE
widgetResizeDragHandle.resizeView = widgetView
widgetWrapper.addView(widgetView, 2)
widgetName.text = widgetView?.name
visibility = if (widgetView?.show == true) View.VISIBLE else View.GONE
widgetActionSettings.setOnClickListener {
widgetView?.openSettings()
/*(context as? Activity)?.finish()
context.startActivity(Intent(context, SettingsActivity::class.java).apply {
putExtra(SettingsActivity.FRAGMENT, widgetView?.settingsFragment)
})*/
}
} else {
widgetView = ExternalWidget(context, widget, widgetHost)
widgetResizeDragHandle.resizeView = widgetView
widgetResizeDragHandle.onResize = {
widget.height = (it / dp).toInt()
}
widgetWrapper.addView(widgetView, 2)
widgetName.text = widgetView?.name
widgetActionResize.visibility = View.VISIBLE
widgetActionSettings.visibility = View.GONE
visibility = if (widgetView?.show == true) View.VISIBLE else View.GONE
}
widgetView?.onVisibilityChanged = {
visibility = if (it) View.VISIBLE else View.GONE
}
widgetView?.layoutTransition = LayoutTransition().apply {
enableTransitionType(LayoutTransition.CHANGING)
}
this.widget = widget
return true
}
fun getDragHandle(): View {
return widgetDragHandle
}
fun update() {
val widget = widgetWrapper[2] as? LauncherWidget ?: return
widget.update()
visibility = if (widget.show) View.VISIBLE else View.GONE
}
}
@@ -0,0 +1,42 @@
package de.mm20.launcher2.ui.legacy.component
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.wikipedia.WikipediaViewModel
class WikipediaView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
val wikipedia: LiveData<Wikipedia?>
init {
View.inflate(context, R.layout.view_search_category_single_item, this)
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST)
val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val card = findViewById<ViewGroup>(R.id.card)
websiteView.layoutParams = params
card.addView(websiteView)
wikipedia = ViewModelProvider(context as AppCompatActivity)[WikipediaViewModel::class.java].wikipedia
wikipedia.observe(context as AppCompatActivity, Observer {
visibility = if (it == null) View.GONE else View.VISIBLE
card.setOnClickListener { _ ->
ActivityStarter.start(context, websiteView, item = it)
}
websiteView.searchable = it
})
}
}
@@ -0,0 +1,30 @@
package de.mm20.launcher2.ui.legacy.data
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import androidx.core.content.ContextCompat
import com.amulyakhare.textdrawable.TextDrawable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
/**
* A helper searchable that is used to display text inside [de.mm20.launcher2.ui.search2.SearchGridViews]
*/
class InformationText(
override val label: String,
val clickAction: (() -> Unit)? = null
) : Searchable() {
override val key: String
get() = "text://${label}"
override fun getPlaceholderIcon(context: Context): LauncherIcon {
return LauncherIcon(
foreground = TextDrawable.builder()
.buildRect("i", Color.WHITE),
background = ColorDrawable(ContextCompat.getColor(context, R.color.grey))
)
}
}
@@ -0,0 +1,35 @@
package de.mm20.launcher2.ui.legacy.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
class SearchableBottomSheet(val searchable: Searchable) : BottomSheetDialogFragment() {
private var view: SearchableView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(BottomSheetDialogFragment.STYLE_NORMAL, R.style.TransparentBottomSheetTheme)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
view = SearchableView(requireContext(), SearchableView.REPRESENTATION_FULL)
view?.searchable = searchable
view?.onBack = {
dismiss()
}
return view
}
override fun onDestroyView() {
super.onDestroyView()
view?.searchable = null
view = null
}
}
@@ -0,0 +1,209 @@
package de.mm20.launcher2.legacy.helper
import android.animation.AnimatorSet
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroupOverlay
import android.view.animation.AccelerateInterpolator
import android.widget.Toast
import androidx.core.app.ActivityOptionsCompat
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.AppStartAnimation
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import java.lang.ref.WeakReference
object ActivityStarter {
private var initialized = false
private lateinit var overlayView: WeakReference<ViewGroupOverlay>
private lateinit var rootView: WeakReference<ViewGroup>
const val ANIM_SPLASH1 = 0
const val ANIM_SPLASH2 = 1
const val ANIM_M = 2
const val ANIM_FADE = 3
const val ANIM_SLIDE_BOTTOM = 4
private lateinit var animationStyle: AppStartAnimation
fun create(rootView: ViewGroup) {
this.rootView = WeakReference(rootView)
this.overlayView = WeakReference(rootView.overlay)
onResumeCallback = null
animationStyle = LauncherPreferences.instance.appStartAnim
initialized = true
}
fun start(context: Context, transitionView: View, item: Searchable? = null, intent: Intent? = null, pendingIntent: PendingIntent? = null): Boolean {
if (!initialized) throw IllegalStateException("Item starter has not been initialized properly.")
if (!startActivity(context, item, intent, pendingIntent, transitionView)) return false
if (animationStyle == AppStartAnimation.SLIDE_BOTTOM || animationStyle == AppStartAnimation.FADE ||
animationStyle == AppStartAnimation.M) {
return true
}
val rootView = rootView.get() ?: return true
val background = rootView.findViewById<View>(R.id.activityStartOverlay)
background.pivotX = background.width * 0.5f
background.pivotY = background.height * 0.5f
val searchView = rootView.findViewById<View>(R.id.container)
val parent = transitionView.parent as ViewGroup
val index = parent.indexOfChild(transitionView)
overlayView.get()?.add(transitionView)
val bounds = Rect()
transitionView.getGlobalVisibleRect(bounds)
val scale = (rootView.width).toFloat() / transitionView.width
//val x = bounds.left.toFloat()
//val y = bounds.top.toFloat()
val x = (rootView.width - transitionView.width) * 0.5f - transitionView.x
val y = (rootView.height - transitionView.height) * 0.5f - transitionView.y
background.visibility = View.VISIBLE
background.scaleX = transitionView.width.toFloat() / background.width
background.scaleY = transitionView.height.toFloat() / background.height
background.translationX = bounds.exactCenterX() - background.width * 0.5f
background.translationY = bounds.exactCenterY() - background.height * 0.5f
AnimatorSet().apply {
playTogether(
ViewPropertyObjectAnimator.animate(background)
.scaleX(1f)
.scaleY(1f)
.translationX(0f)
.translationY(0f)
.setDuration(200)
.setInterpolator(AccelerateInterpolator(0.8f))
.get(),
ViewPropertyObjectAnimator.animate(transitionView)
.scaleX(scale)
.scaleY(scale)
.alpha(0f)
.translationX(x)
.translationY(y)
.setDuration(200)
.setInterpolator(AccelerateInterpolator(0.8f))
.get(),
ViewPropertyObjectAnimator.animate(searchView)
.scaleX(0.8f)
.scaleY(0.8f)
.alpha(0f)
.get()
)
}.start()
onResumeCallback = {
transitionView.translationX = 0f
transitionView.translationY = 0f
transitionView.scaleX = 1f
transitionView.scaleY = 1f
transitionView.alpha = 1f
searchView.scaleX = 1f
searchView.scaleY = 1f
searchView.alpha = 1f
background.scaleX = 1f
background.scaleY = 1f
background.translationX = 0f
background.translationY = 0f
overlayView.get()?.remove(transitionView)
background.visibility = View.INVISIBLE
parent.addView(transitionView, index)
}
return true
}
private var onResumeCallback: (() -> Unit)? = null
private fun startActivity(context: Context, item: Searchable? = null, intent: Intent? = null, pendingIntent: PendingIntent? = null, sourceView: View): Boolean {
val pos = intArrayOf(0, 0)
sourceView.getLocationOnScreen(pos)
val sourceBounds = Rect(pos[0], pos[1], pos[0] + sourceView.width, pos[1] + sourceView.height)
val bundle = getActivityOptions(context, sourceView, sourceBounds)?.toBundle()
if (pendingIntent != null) {
return try {
pendingIntent.send()
true
} catch (e: ActivityNotFoundException) {
false
}
}
if (item != null) {
if (item.launch(context, bundle)) {
FavoritesRepository.getInstance(context).incrementLaunchCount(item)
return true
}
return false
}
val i = intent ?: return false
return if (i.resolveActivity(context.packageManager) != null) {
context.startActivity(i, bundle)
true
} else {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
false
}
}
private fun getActivityOptions(context: Context, sourceView: View, sourceBounds: Rect?): ActivityOptionsCompat? {
return when (animationStyle) {
AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_fade_enter, R.anim.activity_start_fade_exit)
AppStartAnimation.SLIDE_BOTTOM -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_slide_bottom_enter, R.anim.activity_start_slide_bottom_exit)
AppStartAnimation.M -> sourceBounds?.let { ActivityOptionsCompat.makeClipRevealAnimation(sourceView, 0, 0, sourceView.width, sourceView.height) }
else -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_splash2_enter, R.anim.activity_start_splash2_exit)
}
}
fun pause() {
}
fun resume() {
onResumeCallback?.invoke()
onResumeCallback = null
val it = callbacks.iterator()
while (it.hasNext()) {
val callbackRef = it.next()
val callback = callbackRef.get()
if (callback == null) {
it.remove()
continue
}
callback.onResume()
}
}
fun destroy() {
onResumeCallback = null
}
/**
* Returns true when an animation is running
*/
fun isStarting(): Boolean {
return onResumeCallback != null
}
private val callbacks = mutableSetOf<WeakReference<ActivityStarterCallback>>()
fun registerCallback(callback: ActivityStarterCallback) {
callbacks.add(WeakReference(callback))
}
}
interface ActivityStarterCallback {
fun onResume()
}
@@ -0,0 +1,22 @@
package de.mm20.launcher2.ui.legacy.helper
import android.graphics.*
import android.util.LruCache
import androidx.annotation.MainThread
/**
* Helper object to store temporary bitmaps to draw on so they don't have to be allocated every
* time and can be reused by different classes and methods.
*/
object BitmapHolder {
private val cache = LruCache<Int, Pair<Bitmap, Canvas>>(8)
@MainThread
fun getBitmapAndCanvas(size: Int): Pair<Bitmap, Canvas> {
return cache[size]?.apply { second.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) }
?: Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).let {
Pair(it, Canvas(it)).also { pair -> cache.put(size, pair) }
}
}
}
@@ -0,0 +1,93 @@
package de.mm20.launcher2.ui.legacy.helper
import android.Manifest
import android.app.WallpaperManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
object WallpaperBlur {
fun requestBlur(context: Context) {
val wm = WallpaperManager.getInstance(context)
val lastId = context.getSharedPreferences("wallpaper", Context.MODE_PRIVATE)
.getInt("last_wallpaper_id", 0)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || wm.getWallpaperId(WallpaperManager.FLAG_SYSTEM) != lastId) {
blurredWallpaper?.takeIf { !it.isRecycled }?.recycle()
blurredWallpaper = null
File(context.cacheDir, "wallpaper").takeIf { it.exists() }?.delete()
if (wm.wallpaperInfo != null) return
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) return
val wallpaper = wm.drawable.toBitmap()
Glide.with(context)
.asBitmap()
.load(wallpaper)
.apply(
RequestOptions.bitmapTransform(
BlurTransformation(
(20 * context.dp).toInt(),
1
)
)
)
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
GlobalScope.launch {
withContext(Dispatchers.IO) {
val out = FileOutputStream(File(context.cacheDir, "wallpaper"))
resource.compress(Bitmap.CompressFormat.PNG, 100, out)
out.close()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.getSharedPreferences("wallpaper", Context.MODE_PRIVATE)
.edit()
.putInt(
"last_wallpaper_id", wm.getWallpaperId(
WallpaperManager.FLAG_SYSTEM
)
)
.apply()
}
if (LauncherPreferences.instance.blurCards && LauncherPreferences.instance.cardOpacity < 0xFF) {
blurredWallpaper = resource
} else {
resource.recycle()
}
}
}
}
})
}
}
fun getCachedBitmap(context: Context): Bitmap? {
return BitmapFactory.decodeFile(File(context.cacheDir, "wallpaper").absolutePath)
}
var blurredWallpaper: Bitmap? = null
}
@@ -0,0 +1,393 @@
package de.mm20.launcher2.ui.legacy.search
import android.app.Notification
import android.app.ProgressDialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.LauncherApps
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Process
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.core.graphics.alpha
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.transition.Scene
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.castToOrNull
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.getBadgeIcon
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.notifications.NotificationService
import de.mm20.launcher2.search.data.AppInstallation
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
import kotlin.math.roundToInt
class ApplicationDetailRepresentation : Representation {
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val application = searchable as Application
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_application_detail, context)
scene.setEnterAction {
with(rootView) {
setOnClickListener(null)
setOnLongClickListener(null)
findViewById<TextView>(R.id.appName).text = application.label
findViewById<LauncherIconView>(R.id.icon).apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(application.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = IconRepository.getInstance(context).getIconIfCached(application)
lifecycleScope.launch {
IconRepository.getInstance(context)
.getIcon(application, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
}
findViewById<SwipeCardView>(R.id.appCard).also {
it.leftAction = FavoriteSwipeAction(context, application)
it.rightAction = HideSwipeAction(context, application)
}
val appInfo = findViewById<TextView>(R.id.appInfo)
appInfo.text = if (application !is AppInstallation) {
context.getString(
R.string.app_info,
application.version ?: "",
application.`package`
)
} else {
val callback = object : PackageInstaller.SessionCallback() {
override fun onActiveChanged(p0: Int, p1: Boolean) {
}
override fun onFinished(sessionId: Int, success: Boolean) {
if (sessionId == application.session.sessionId) {
context.packageManager.packageInstaller.unregisterSessionCallback(
this
)
}
}
override fun onBadgingChanged(p0: Int) {
}
override fun onCreated(p0: Int) {
}
override fun onProgressChanged(sessionId: Int, progress: Float) {
if (sessionId == application.session.sessionId) {
appInfo.text = context.getString(
R.string.installation_in_progress,
(progress * 100).roundToInt()
)
}
}
}
context.packageManager.packageInstaller.registerSessionCallback(callback)
context.getString(
R.string.installation_in_progress,
(application.session.progress * 100).roundToInt()
)
}
val appShortcuts = findViewById<ChipGroup>(R.id.appShortcuts)
appShortcuts.layoutTransition = ChangingLayoutTransition()
setupShortcuts(appShortcuts, application)
val toolbar = findViewById<ToolbarView>(R.id.appToolbar)
setupToolbar(this, toolbar, application)
}
}
return scene
}
private fun setupToolbar(rootView: SearchableView, toolbar: ToolbarView, app: Application) {
val context = rootView.context
toolbar.clear()
val backAction =
ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
toolbar.addAction(backAction, ToolbarView.PLACEMENT_START)
if (app !is AppInstallation) {
val favAction = FavoriteToolbarAction(context, app)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
}
if (app !is AppInstallation) {
val infoAction =
ToolbarAction(R.drawable.ic_info_outline, context.getString(R.string.menu_app_info))
infoAction.clickAction = {
val launcherApps = context.getSystemService<LauncherApps>()!!
launcherApps.startAppDetailsActivity(
ComponentName(app.`package`, app.activity),
app.castToOrNull<LauncherApp>()?.getUser() ?: Process.myUserHandle(),
null,
null
)
}
toolbar.addAction(infoAction, ToolbarView.PLACEMENT_END)
}
val shareAction = ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
val storeDetails = app.getStoreDetails(context)
if (app !is AppInstallation) {
if (storeDetails == null) {
shareAction.clickAction = {
shareApk(context, app)
}
} else {
shareAction.subActions.add(ToolbarSubaction(
context.getString(R.string.share_menu_store_link, storeDetails.label)
) { shareLink(context, storeDetails.url) })
shareAction.subActions.add(ToolbarSubaction(
context.getString(R.string.share_menu_apk_file)
) { shareApk(context, app) })
}
toolbar.addAction(shareAction, ToolbarView.PLACEMENT_END)
} else {
if (storeDetails != null) {
shareAction.clickAction = {
shareLink(context, storeDetails.url)
}
toolbar.addAction(shareAction, ToolbarView.PLACEMENT_END)
}
}
if (app !is AppInstallation) {
if (app.flags and ApplicationInfo.FLAG_SYSTEM == 0) {
val uninstallAction =
ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_uninstall))
uninstallAction.clickAction = {
val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:" + app.`package`)
context.startActivity(intent)
rootView.back()
}
toolbar.addAction(uninstallAction, ToolbarView.PLACEMENT_END)
}
val hideAction = VisibilityToolbarAction(context, app)
toolbar.addAction(hideAction, ToolbarView.PLACEMENT_END)
}
}
private fun setupShortcuts(appShortcuts: ChipGroup, app: Application) {
val context = appShortcuts.context
appShortcuts.removeAllViews()
val ns = NotificationService.getInstance()
val notifications = ns?.getNotifications(app.`package`)
notifications?.forEach {
var title = it.notification.tickerText
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TITLE)
}
if (title.isNullOrBlank()) {
title = it.notification.extras.getCharSequence(Notification.EXTRA_TEXT)
}
if (title == null) title = ""
if (!NotificationCompat.isGroupSummary(it.notification)) {
val view = Chip(context)
view.text = title
view.chipIcon = getNotificationChipIcon(context, it.notification)
view.chipStrokeWidth = 1 * context.dp
view.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
view.chipBackgroundColor =
ContextCompat.getColorStateList(context, R.color.chip_background)
view.setTextAppearanceResource(R.style.ChipTextAppearance)
view.closeIconTint = ColorStateList.valueOf(
ContextCompat.getColor(
context,
R.color.text_color_secondary
)
)
view.isCloseIconVisible = it.isClearable
view.setOnClickListener { _ ->
it.notification.contentIntent?.send()
}
view.setOnCloseIconClickListener { _ ->
ns.cancelNotification(it.key)
appShortcuts.removeView(view)
}
appShortcuts.addView(view)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val launcherApps =
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (launcherApps.hasShortcutHostPermission()) {
val shortcuts = app.shortcuts
val viewModel =
ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
for (si in shortcuts) {
val view = Chip(context)
view.text = si.label
view.chipIcon = launcherApps.getShortcutBadgedIconDrawable(
si.launcherShortcut,
context.resources.displayMetrics.densityDpi
)
view.chipStrokeWidth = 1 * context.dp
view.chipStrokeColor =
ContextCompat.getColorStateList(context, R.color.chip_stroke)
view.chipBackgroundColor =
ContextCompat.getColorStateList(context, R.color.chip_background)
view.setTextAppearanceResource(R.style.ChipTextAppearance)
view.closeIcon = context.getDrawable(R.drawable.ic_star_solid)
view.closeIconTint = ColorStateList.valueOf(
ContextCompat.getColor(
context,
R.color.text_color_primary
)
)
val isPinned = viewModel.isPinned(si)
isPinned.observe(context, Observer {
view.isCloseIconVisible = isPinned.value == true
})
view.setOnClickListener {
ActivityStarter.start(context, view)
launcherApps.startShortcut(si.launcherShortcut, null, null)
}
view.setOnLongClickListener {
if (isPinned.value == true) {
viewModel.unpinItem(si)
} else {
viewModel.pinItem(si)
}
true
}
view.setOnCloseIconClickListener {
viewModel.unpinItem(si)
view.isCloseIconVisible = false
}
appShortcuts.addView(view)
}
}
}
}
private fun getNotificationChipIcon(context: Context, notification: Notification): Drawable? {
return notification.getBadgeIcon(context, context.packageName)?.let {
val _4dp = (4 * context.dp).roundToInt()
return@let LayerDrawable(arrayOf(
ShapeDrawable(
OvalShape()
).apply {
colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(context, R.color.shortcut_icon_background),
PorterDuff.Mode.SRC_ATOP
)
},
it.apply {
colorFilter = PorterDuffColorFilter(
notification.color.takeIf { it.alpha > 0 }
?: ContextCompat.getColor(context, R.color.text_color_secondary),
PorterDuff.Mode.SRC_ATOP
)
}
)).apply {
setLayerInset(1, _4dp, _4dp, _4dp, _4dp)
}
}
}
private fun shareLink(context: Context, storeUrl: String) {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, storeUrl)
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}
private fun shareApk(context: Context, app: Application) {
val handler = Handler()
val progressDialog = ProgressDialog(context)
progressDialog.setMessage(context.getString(R.string.dialog_wait))
progressDialog.show()
val executor = Executors.newSingleThreadExecutor()
executor.execute {
try {
val info = context.packageManager
.getApplicationInfo(app.`package`, 0)
val file = java.io.File(info.publicSourceDir)
val fileCopy = java.io.File(
context.cacheDir,
"${app.`package`}-${app.version}.apk"
)
try {
file.copyTo(fileCopy, false)
} catch (e: FileAlreadyExistsException) {
// Do nothing. If the file is already there we don't have to copy it again.
}
handler.post {
progressDialog.hide()
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"
context.startActivity(Intent.createChooser(shareIntent, null))
}
} catch (e: PackageManager.NameNotFoundException) {
CrashReporter.logException(e)
}
}
}
}
@@ -0,0 +1,58 @@
package de.mm20.launcher2.ui.legacy.search
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.transition.Scene
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class BasicGridRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_basic_grid, rootView.context)
scene.setEnterAction {
with(rootView) {
val text = findViewById<TextView>(R.id.label)
text.text = searchable.label
/*text.alpha = 0f
text.animate()
.setStartDelay(300)
.setDuration(200)
.alpha(1f)
.start()*/
findViewById<LauncherIconView>(R.id.icon).apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(searchable.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
setOnClickListener {
if (!ActivityStarter.start(context, rootView.findViewById(R.id.card), item = searchable)) {
rootView.representation = SearchableView.REPRESENTATION_FULL
}
}
icon = IconRepository.getInstance(context).getIconIfCached(searchable)
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(searchable, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
setOnLongClickListener {
rootView.representation = SearchableView.REPRESENTATION_FULL
true
}
}
}
}
return scene
}
}
@@ -0,0 +1,127 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.CalendarContract
import android.text.format.DateUtils
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.transition.Scene
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ktx.setStartCompoundDrawable
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import java.net.URLEncoder
class CalendarDetailRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val calendarEvent = searchable as CalendarEvent
val scene = Scene.getSceneForLayout(rootView, R.layout.view_calendar_detail, rootView.context)
scene.setEnterAction {
with(rootView) {
findViewById<TextView>(R.id.calendarLabel).text = calendarEvent.label
findViewById<View>(R.id.calendarColor).setBackgroundColor(CalendarEvent.getDisplayColor(context, calendarEvent.color))
findViewById<SwipeCardView>(R.id.calendarEventCard).also {
it.leftAction = FavoriteSwipeAction(context, calendarEvent)
it.rightAction = HideSwipeAction(context, calendarEvent)
it.setOnClickListener {
rootView.representation = SearchableView.REPRESENTATION_FULL
}
}
val toolbar = findViewById<ToolbarView>(R.id.calendarToolbar)
/*toolbar.alpha = 0f
toolbar.animate()
.setStartDelay(100)
.setDuration(200)
.alpha(1f)
.start()*/
setupMenu(rootView, toolbar, calendarEvent)
addShortcuts(rootView, calendarEvent)
}
}
return scene
}
private fun setupMenu(rootView: SearchableView, toolbar: ToolbarView, event: CalendarEvent) {
val context = rootView.context
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
toolbar.addAction(backAction, ToolbarView.PLACEMENT_START)
val favAction = FavoriteToolbarAction(context, event)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val hideAction = VisibilityToolbarAction(context, event)
toolbar.addAction(hideAction, ToolbarView.PLACEMENT_END)
val openAction = ToolbarAction(R.drawable.ic_open_external, context.getString(R.string.calendar_menu_open_externally))
openAction.clickAction = {
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, event.id)
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
ActivityStarter.start(context, rootView, intent = intent)
}
toolbar.addAction(openAction, ToolbarView.PLACEMENT_END)
}
private fun addShortcuts(rootView: SearchableView, event: CalendarEvent) {
val context = rootView.context
val shortcutContainer = rootView.findViewById<LinearLayout>(R.id.calendarShortcuts)
val timeView = (View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_time)
it.text = formatTime(context, event)
}
shortcutContainer.addView(timeView)
if (event.description.isNotEmpty()) {
val descriptionView = (View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_description)
it.text = HtmlCompat.fromHtml(event.description, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
shortcutContainer.addView(descriptionView)
}
if (event.location.isNotEmpty()) {
val locationView = (View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_location)
it.text = event.location
it.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("geo:0,0?q=${URLEncoder.encode(event.location, "utf8")}")
ActivityStarter.start(context, rootView, intent = intent)
}
}
shortcutContainer.addView(locationView)
}
if (event.attendees.isNotEmpty()) {
val attendeesView = (View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_attendees)
it.text = event.attendees.joinToString { it }
}
}
}
private fun formatTime(context: Context, event: CalendarEvent): String {
if (event.allDay) return DateUtils.formatDateRange(context, event.startTime, event.endTime, DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY)
return DateUtils.formatDateRange(context, event.startTime, event.endTime, DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_WEEKDAY)
}
}
@@ -0,0 +1,110 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.text.format.DateUtils
import android.view.View
import android.widget.TextView
import androidx.transition.Scene
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class CalendarListRepresentation : Representation {
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val calendarEvent = searchable as CalendarEvent
val scene = Scene.getSceneForLayout(rootView, R.layout.view_calendar_list, rootView.context)
scene.setEnterAction {
with(rootView) {
findViewById<TextView>(R.id.calendarLabel).text = calendarEvent.label
findViewById<View>(R.id.calendarColor).setBackgroundColor(
CalendarEvent.getDisplayColor(
context,
calendarEvent.color
)
)
findViewById<SwipeCardView>(R.id.calendarEventCard).also {
it.leftAction = FavoriteSwipeAction(context, calendarEvent)
it.rightAction = HideSwipeAction(context, calendarEvent)
it.setOnClickListener {
rootView.representation = SearchableView.REPRESENTATION_FULL
}
}
val isToday =
DateUtils.isToday(calendarEvent.startTime) && DateUtils.isToday(calendarEvent.endTime)
findViewById<TextView>(R.id.eventDateTime).text = 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
)
}
}
}
}
return scene
}
private fun formatTime(context: Context, event: CalendarEvent): String {
val df = DateFormat.getTimeInstance(DateFormat.SHORT)
return when {
event.startTime == event.endTime -> {
df.format(Date(event.startTime))
}
event.allDay -> {
context.getString(R.string.calendar_event_allday)
}
else -> {
DateUtils.formatDateRange(
context, event.startTime, event.endTime,
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_MONTH
)
}
}
}
private fun formatDate(event: CalendarEvent): Pair<String, String> {
val calendar = Calendar.getInstance()
calendar.timeInMillis = event.startTime
val today = Calendar.getInstance()
today.timeInMillis = System.currentTimeMillis()
val line1 =
if (calendar[Calendar.YEAR] == today[Calendar.YEAR] && calendar[Calendar.MONTH] == calendar[Calendar.MONTH]) {
SimpleDateFormat("EEE").format(event.startTime)
} else SimpleDateFormat("MMM").format(event.startTime)
val line2 = calendar[Calendar.DAY_OF_MONTH].toString()
return line1 to line2
}
}
@@ -0,0 +1,352 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.ActivityNotFoundException
import android.content.ContentUris
import android.content.Intent
import android.net.Uri
import android.provider.ContactsContract
import android.view.Gravity
import android.view.Menu
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.setStartCompoundDrawable
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.net.URLEncoder
class ContactDetailRepresentation : Representation {
override fun getScene(
rootView: SearchableView,
searchable: Searchable,
previousRepresentation: Int?
): Scene {
val contact = searchable as Contact
val context = rootView.context as AppCompatActivity
val scene =
Scene.getSceneForLayout(rootView, R.layout.view_contact_detail, rootView.context)
scene.setEnterAction {
with(rootView) {
findViewById<LauncherIconView>(R.id.icon).apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(contact.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = IconRepository.getInstance(context).getIconIfCached(contact)
lifecycleScope.launch {
IconRepository.getInstance(context)
.getIcon(contact, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
}
findViewById<TextView>(R.id.contactName).text = contact.displayName
findViewById<SwipeCardView>(R.id.contactCard).also {
it.leftAction = FavoriteSwipeAction(context, contact)
it.rightAction = HideSwipeAction(context, contact)
}
val toolbar = findViewById<ToolbarView>(R.id.contactToolbar)
setupMenu(this, toolbar, contact)
addShortcuts(rootView, contact)
}
}
return scene
}
private fun setupMenu(rootView: SearchableView, toolbar: ToolbarView, contact: Contact) {
val context = rootView.context
val backAction =
ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
toolbar.addAction(backAction, ToolbarView.PLACEMENT_START)
val favAction = FavoriteToolbarAction(context, contact)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val hideAction = VisibilityToolbarAction(context, contact)
toolbar.addAction(hideAction, ToolbarView.PLACEMENT_END)
val openAction = ToolbarAction(
R.drawable.ic_open_external,
context.getString(R.string.contacts_menu_open_externally)
)
openAction.clickAction = {
try {
val uri =
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contact.id)
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
ActivityStarter.start(context, rootView, intent = intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
}
}
toolbar.addAction(openAction, ToolbarView.PLACEMENT_END)
}
private fun addShortcuts(rootView: SearchableView, contact: Contact) {
val context = rootView.context
val shortcutContainer = rootView.findViewById<LinearLayout>(R.id.contactShortcuts)
if (contact.phones.isNotEmpty()) {
val callView = (View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_call)
if (contact.phones.size == 1) {
it.text = contact.phones.first()
it.setOnClickListener {
call(rootView, contact.phones.first())
}
} else {
it.text =
context.getString(R.string.contact_multiple_numbers, contact.phones.size)
it.setOnClickListener {
val menu = PopupMenu(context, it, Gravity.START)
val phones = contact.phones.toList()
for ((i, phone) in phones.withIndex()) {
menu.menu.add(Menu.NONE, i, Menu.NONE, phone)
}
menu.setOnMenuItemClickListener {
call(rootView, phones[it.itemId])
true
}
menu.show()
}
}
}
shortcutContainer.addView(callView)
}
if (contact.phones.isNotEmpty()) {
val messageView =
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_message)
if (contact.phones.size == 1) {
it.text = contact.phones.first()
it.setOnClickListener {
message(rootView, contact.phones.first())
}
} else {
it.text = context.getString(
R.string.contact_multiple_numbers,
contact.phones.size
)
it.setOnClickListener {
val menu = PopupMenu(context, it, Gravity.START)
val phones = contact.phones.toList()
for ((i, phone) in phones.withIndex()) {
menu.menu.add(Menu.NONE, i, Menu.NONE, phone)
}
menu.setOnMenuItemClickListener {
message(rootView, phones[it.itemId])
true
}
menu.show()
}
}
}
shortcutContainer.addView(messageView)
}
if (contact.emails.isNotEmpty()) {
val emailView =
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_mail)
if (contact.emails.size == 1) {
it.text = contact.emails.first()
it.setOnClickListener {
email(rootView, contact.emails.first())
}
} else {
it.text =
context.getString(R.string.contact_multiple_emails, contact.emails.size)
it.setOnClickListener {
val menu = PopupMenu(context, it, Gravity.START)
val emails = contact.emails.toList()
for ((i, email) in emails.withIndex()) {
menu.menu.add(Menu.NONE, i, Menu.NONE, email)
}
menu.setOnMenuItemClickListener {
email(rootView, emails[it.itemId])
true
}
menu.show()
}
}
}
shortcutContainer.addView(emailView)
}
if (contact.telegram.isNotEmpty()) {
val telegramView =
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_telegram)
if (contact.telegram.size == 1) {
it.text = contact.telegram.first().substringAfter('$')
it.setOnClickListener {
telegram(rootView, contact.telegram.first().substringBefore('$'))
}
} else {
it.text = context.getString(
R.string.contact_multiple_numbers,
contact.telegram.size
)
it.setOnClickListener {
val menu = PopupMenu(context, it, Gravity.START)
val phones = contact.telegram.toList()
for ((i, phone) in phones.withIndex()) {
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.substringAfter('$'))
}
menu.setOnMenuItemClickListener {
telegram(rootView, phones[it.itemId].substringBefore('$'))
true
}
menu.show()
}
}
}
shortcutContainer.addView(telegramView)
}
if (contact.whatsapp.isNotEmpty()) {
val whatsappView =
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_whatsapp)
if (contact.whatsapp.size == 1) {
it.text = contact.whatsapp.first().substringAfter("$")
it.setOnClickListener {
whatsapp(rootView, contact.whatsapp.first().substringBefore("$"))
}
} else {
it.text = context.getString(
R.string.contact_multiple_numbers,
contact.whatsapp.size
)
it.setOnClickListener {
val menu = PopupMenu(context, it, Gravity.START)
val phones = contact.whatsapp.toList()
for ((i, phone) in phones.withIndex()) {
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.substringAfter('$'))
}
menu.setOnMenuItemClickListener {
whatsapp(rootView, phones[it.itemId].substringBefore('$'))
true
}
menu.show()
}
}
}
shortcutContainer.addView(whatsappView)
}
if (contact.postals.isNotEmpty()) {
val locationView =
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
it.setStartCompoundDrawable(R.drawable.ic_location)
if (contact.postals.size == 1) {
it.text = contact.postals.first()
it.setOnClickListener {
navigate(rootView, contact.postals.first())
}
} else {
it.text = context.getString(
R.string.contact_multiple_postals,
contact.postals.size
)
it.setOnClickListener {
val menu = PopupMenu(context, it, Gravity.START)
val postals = contact.postals.toList()
for ((i, postal) in postals.withIndex()) {
menu.menu.add(Menu.NONE, i, Menu.NONE, postal)
}
menu.setOnMenuItemClickListener {
navigate(rootView, postals[it.itemId])
true
}
menu.show()
}
}
}
shortcutContainer.addView(locationView)
}
}
private fun call(rootView: SearchableView, number: String) {
val context = rootView.context
try {
val callIntent = Intent(Intent.ACTION_DIAL)
callIntent.data = Uri.parse("tel:$number")
ActivityStarter.start(context, rootView, intent = callIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
}
}
private fun message(rootView: SearchableView, number: String) {
val context = rootView.context
try {
val messageIntent = Intent(Intent.ACTION_VIEW)
messageIntent.data = Uri.parse("sms:$number")
ActivityStarter.start(context, rootView, intent = messageIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
}
}
private fun email(rootView: SearchableView, address: String) {
val context = rootView.context
try {
val mailIntent = Intent(Intent.ACTION_VIEW)
mailIntent.data = Uri.parse("mailto:$address")
ActivityStarter.start(context, rootView, intent = mailIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
}
}
private fun whatsapp(rootView: SearchableView, number: String) {
val context = rootView.context
try {
val whatsappIntent = Intent(Intent.ACTION_VIEW)
whatsappIntent.data = Uri.withAppendedPath(ContactsContract.Data.CONTENT_URI, number)
ActivityStarter.start(context, rootView, intent = whatsappIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
}
}
private fun telegram(rootView: SearchableView, userId: String) {
val context = rootView.context
try {
val telegramIntent = Intent(Intent.ACTION_VIEW)
telegramIntent.data = Uri.parse("tg:openmessage?user_id=$userId")
ActivityStarter.start(context, rootView, intent = telegramIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
}
}
private fun navigate(rootView: SearchableView, location: String) {
val context = rootView.context
try {
val mapsIntent = Intent(Intent.ACTION_VIEW)
mapsIntent.data = Uri.parse("geo:0,0?q=${URLEncoder.encode(location, "utf8")}")
ActivityStarter.start(context, rootView, intent = mapsIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
}
}
}
@@ -0,0 +1,61 @@
package de.mm20.launcher2.ui.legacy.search
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.transition.Scene
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class ContactListRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val contact = searchable as Contact
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_contact_list, rootView.context)
scene.setEnterAction {
with(rootView) {
findViewById<LauncherIconView>(R.id.icon).apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(contact.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = IconRepository.getInstance(context).getIconIfCached(contact)
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(contact, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
}
findViewById<TextView>(R.id.contactName).text = contact.displayName
val contactSummary = findViewById<TextView>(R.id.contactSummary)
contactSummary.text = contact.summary
findViewById<SwipeCardView>(R.id.contactCard).also {
it.leftAction = FavoriteSwipeAction(context, contact)
it.rightAction = HideSwipeAction(context, contact)
it.setOnClickListener {
rootView.representation = SearchableView.REPRESENTATION_FULL
}
}
contactSummary.alpha = 0f
contactSummary.animate()
.setStartDelay(100)
.setDuration(200)
.alpha(1f)
.start()
}
}
return scene
}
}
@@ -0,0 +1,150 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.content.Intent
import android.provider.MediaStore
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModelProvider
import androidx.transition.Scene
import com.afollestad.materialdialogs.MaterialDialog
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.files.FilesViewModel
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.GDriveFile
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.text.DecimalFormat
class FileDetailRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val file = searchable as File
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_file_detail, rootView.context)
scene.setEnterAction {
with(rootView) {
findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = getInfo(context, file)
findViewById<LauncherIconView>(R.id.icon).apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(file.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = IconRepository.getInstance(context).getIconIfCached(file)
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(file, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
}
findViewById<SwipeCardView>(R.id.fileCard).also {
it.leftAction = FavoriteSwipeAction(context, file)
it.rightAction = HideSwipeAction(context, file)
}
setupMenu(rootView, findViewById(R.id.fileToolbar), file)
}
}
return scene
}
private fun setupMenu(rootView: SearchableView, toolbar: ToolbarView, file: File) {
val context = toolbar.context
toolbar.clear()
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
toolbar.addAction(backAction, ToolbarView.PLACEMENT_START)
val favAction = FavoriteToolbarAction(context, file)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val jFile = java.io.File(file.path)
if (jFile.canWrite() && jFile.parentFile.canWrite()) {
val deleteAction = ToolbarAction(R.drawable.ic_delete, context.getString(R.string.menu_delete))
deleteAction.clickAction = {
delete(context, file, jFile)
}
toolbar.addAction(deleteAction, ToolbarView.PLACEMENT_END)
}
val hideAction = VisibilityToolbarAction(context, file)
toolbar.addAction(hideAction, ToolbarView.PLACEMENT_END)
if (file !is GDriveFile) {
val shareAction = ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
shareAction.clickAction = {
share(context, file)
}
toolbar.addAction(shareAction, ToolbarView.PLACEMENT_END)
}
}
private fun delete(context: Context, file: File, jFile: java.io.File) {
MaterialDialog(context).show {
message(text = context.getString(
if (file.isDirectory) R.string.alert_delete_directory
else R.string.alert_delete_file,
file.path))
positiveButton(android.R.string.yes) {
Thread { jFile.deleteRecursively() }.start()
context.contentResolver.delete(
MediaStore.Files.getContentUri("external"),
"${MediaStore.Files.FileColumns._ID} = ?",
arrayOf(file.id.toString()))
it.dismiss()
val fileViewModel = ViewModelProvider(context as AppCompatActivity)[FilesViewModel::class.java]
fileViewModel.removeFile(file)
}
negativeButton(android.R.string.no) {
it.dismiss()
}
}
}
private fun share(context: Context, fileDetail: File) {
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(fileDetail.path))
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
shareIntent.type = fileDetail.mimeType
context.startActivity(Intent.createChooser(shareIntent, null))
}
private fun getInfo(context: Context, file: File): String {
val sb = StringBuilder()
sb.append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_type), file.mimeType))
for ((k, v) in file.metaData) {
sb.append("\n")
.append(context.getString(R.string.file_meta_data_entry, context.getString(k), v))
}
if (!file.isDirectory) {
sb.append("\n").append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_size), formatFileSize(file.size)))
}
if (file.path.isNotEmpty()) {
sb.append("\n").append(context.getString(R.string.file_meta_data_entry, context.getString(R.string.file_meta_path), file.path))
}
return sb.toString()
}
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,93 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.transition.Scene
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteSwipeAction
import de.mm20.launcher2.ui.legacy.view.HideSwipeAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.SwipeCardView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class FileListRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val file = searchable as File
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_file_list, rootView.context)
scene.setEnterAction {
with(rootView) {
findViewById<TextView>(R.id.fileLabel).text = file.label
findViewById<TextView>(R.id.fileInfo).text = getFileType(context, file)
findViewById<LauncherIconView>(R.id.icon).apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(file.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = IconRepository.getInstance(context).getIconIfCached(file)
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(file, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
}
findViewById<SwipeCardView>(R.id.fileCard).apply {
setOnClickListener {
ActivityStarter.start(context, rootView, item = file)
}
setOnLongClickListener {
rootView.representation = SearchableView.REPRESENTATION_FULL
true
}
leftAction = FavoriteSwipeAction(context, file)
rightAction = HideSwipeAction(context, file)
}
}
}
return scene
}
fun getFileType(context: Context, file: File): String {
if (file.isDirectory) return context.getString(R.string.file_type_directory)
val mimeType = file.mimeType
val resource = when (mimeType) {
"application/zip", "application/x-gtar", "application/x-tar",
"application/java-archive", "application/x-7z-compressed" -> R.string.file_type_archive
"application/x-gzip", "application/x-bzip2" -> R.string.file_type_compressed
"application/vnd.android.package-archive" -> R.string.file_type_android
"text/x-asm", "text/x-c", "text/x-java-source", "text/x-script.phyton", "text/x-pascal",
"text/x-script.perl", "text/javascript", "application/json" ->
R.string.file_type_source_code
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword", "application/vnd.google-apps.document" -> R.string.file_type_document
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel", "application/vnd.google-apps.spreadsheet" -> R.string.file_type_spreadsheet
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint", "application/vnd.google-apps.presentation" -> R.string.file_type_presentation
"text/plain" -> R.string.file_type_text
"application/vnd.google-apps.drawing" -> R.string.file_type_drawing
"application/vnd.google-apps.form" -> R.string.file_type_form
else -> when {
mimeType.startsWith("image/") -> R.string.file_type_image
mimeType.startsWith("video/") -> R.string.file_type_video
mimeType.startsWith("audio/") -> R.string.file_type_music
else -> R.string.file_type_none
}
}
if (resource == R.string.file_type_none && file.label.matches(Regex(".+\\..+"))) {
val extension = file.label.substringAfterLast(".").uppercase()
return context.getString(R.string.file_type_generic, extension)
}
return context.getString(resource)
}
}
@@ -0,0 +1,25 @@
package de.mm20.launcher2.ui.legacy.search
import android.widget.TextView
import androidx.transition.Scene
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.data.InformationText
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.InnerCardView
class InformationListRepresentation: Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val informationText = searchable as InformationText
val scene = Scene.getSceneForLayout(rootView, R.layout.view_information_list, rootView.context)
scene.setEnterAction {
rootView.findViewById<TextView>(R.id.informationText).text = informationText.label
if (informationText.clickAction != null) {
rootView.findViewById<InnerCardView>(R.id.card).setOnClickListener {
informationText.clickAction.invoke()
}
}
}
return scene
}
}
@@ -0,0 +1,28 @@
package de.mm20.launcher2.ui.legacy.search
import android.app.Activity
import android.widget.TextView
import androidx.transition.Scene
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.InnerCardView
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
class PermissionListRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val missingPermission = searchable as MissingPermission
val context = rootView.context
val scene = Scene.getSceneForLayout(rootView, R.layout.view_permission_list, rootView.context)
scene.setEnterAction {
rootView.findViewById<TextView>(R.id.permissionText).text = missingPermission.label
rootView.findViewById<LauncherIconView>(R.id.permissionIcon).icon = missingPermission.getPlaceholderIcon(context)
rootView.findViewById<InnerCardView>(R.id.card).setOnClickListener {
PermissionsManager.requestPermission(context as Activity, missingPermission.permissionGroup)
}
}
return scene
}
}
@@ -0,0 +1,9 @@
package de.mm20.launcher2.ui.legacy.search
import androidx.transition.Scene
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
interface Representation {
fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?) : Scene
}
@@ -0,0 +1,304 @@
package de.mm20.launcher2.ui.legacy.search
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import de.mm20.launcher2.ktx.ceilToInt
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.legacy.helper.ActivityStarterCallback
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.math.min
class SearchGridView : ViewGroup, ActivityStarterCallback {
override fun onResume() {
while (postponedDiffs.isNotEmpty()) {
postponedDiffs.poll()?.let { applyDiff(it, true) }
}
}
var columnCount: Int = 1
set(value) {
if (value > 0) field = value
else throw IllegalArgumentException("columnCount must be positive (is $value)")
}
@ObsoleteCoroutinesApi
private val updateActor = lifecycleScope
.actor<List<Searchable>>(Dispatchers.Main, capacity = Channel.CONFLATED) {
for (newItems in channel) {
val oldItems = currentItems
val diffResult = withContext(Dispatchers.Default) {
SearchDiffUtil.calculateDiff(oldItems, newItems)
}
currentItems = newItems
applyDiff(diffResult)
}
}
@ObsoleteCoroutinesApi
fun submitItems(items: List<Searchable>?) {
if (items == null) return
if (items.getOrNull(expandedItem)?.key != currentItems.getOrNull(expandedItem)?.key) expandedItem = -1
lifecycleScope.launch {
updateActor.send(items)
}
}
private var expandedItem = -1
set(value) {
(getChildAt(field) as? SearchableView)?.back()
requestLayout()
field = value
}
private var currentItems = listOf<Searchable>()
private val postponedDiffs = ArrayDeque<Queue<DiffAction>>()
/**
* The height of each row. An absolute pixel size or [ROW_HEIGHT_AUTO]
*/
var rowHeight: Int = ROW_HEIGHT_AUTO
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) {
attrs?.let {
val ta = context.theme.obtainStyledAttributes(it, R.styleable.SearchGridView, 0, defStyleRes)
columnCount = ta.getInt(R.styleable.SearchGridView_columnCount, 1)
rowHeight = ta.getDimensionPixelSize(R.styleable.SearchGridView_rowHeight, -1)
ta.recycle()
}
layoutTransition = LayoutTransition().also {
it.enableTransitionType(LayoutTransition.CHANGING)
}
clipChildren = false
ActivityStarter.registerCallback(this)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpec = MeasureSpec.makeMeasureSpec(
(MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight) / columnCount,
MeasureSpec.EXACTLY
)
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
val colWidth = 0
children.forEachIndexed { i, v ->
if (i == expandedItem) {
v.measure(widthMeasureSpec, heightSpec)
} else {
v.measure(widthSpec, heightSpec)
}
}
val rowHeight = if (rowHeight != ROW_HEIGHT_AUTO) rowHeight else {
children.maxByOrNull {
if (indexOfChild(it) == expandedItem) return@maxByOrNull 0
it.measuredHeight
}?.measuredHeight ?: 0
}
val width = when (MeasureSpec.getMode(widthMeasureSpec)) {
MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec)
MeasureSpec.AT_MOST -> min(colWidth * columnCount + paddingLeft + paddingRight, MeasureSpec.getSize(widthMeasureSpec))
MeasureSpec.UNSPECIFIED -> colWidth * columnCount + paddingLeft + paddingRight
else -> colWidth * columnCount
}
val visibleChildCount = children.count { it.visibility != View.GONE }
val rowCount = (visibleChildCount / columnCount.toFloat()).ceilToInt()
var height = rowHeight * rowCount + (getChildAt(expandedItem)?.measuredHeight
?: 0) + paddingTop + paddingBottom
if (expandedItem == childCount - 1 && (childCount % columnCount == 1) || expandedItem != -1 && columnCount == 1) {
height -= rowHeight
}
setMeasuredDimension(View.resolveSize(width, widthMeasureSpec), View.resolveSize(height, heightMeasureSpec))
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val rowHeight = if (rowHeight != ROW_HEIGHT_AUTO) rowHeight else {
children.maxByOrNull {
if (indexOfChild(it) == expandedItem) return@maxByOrNull 0
it.measuredHeight
}?.measuredHeight ?: 0
}
val width = measuredWidth
val colWidth = (width - paddingLeft - paddingRight) / columnCount
val visibleChildCount = children.count { it.visibility != View.GONE }
val rowCount = (visibleChildCount / columnCount.toFloat()).ceilToInt()
var x: Int
var y = paddingTop
var i = 0
for (row in 0 until rowCount) {
x = paddingLeft
if (row * columnCount <= expandedItem && expandedItem < (row + 1) * columnCount) {
if (row == 0) y = 0
val child = getChildAt(expandedItem) ?: continue
child.layout(0, y, x + child.measuredWidth, y + child.measuredHeight)
y += child.measuredHeight
if (columnCount == 1) {
i++
continue
}
}
for (col in 0 until columnCount) {
if (i == expandedItem) {
x += colWidth
i++
continue
}
val child = getChildAt(i) ?: break
child.layout(x, y, x + colWidth, y + rowHeight)
x += colWidth
i++
}
y += rowHeight
}
}
/**
* Applies a diff queue. Enqueues to postponedDiffs if an activity is starting (leaving this view
* in an unstable state) or if postponedDiffs is not empty and [force] is not set.
*/
private fun applyDiff(diff: Queue<DiffAction>, force: Boolean = false) {
if (ActivityStarter.isStarting() || (postponedDiffs.isNotEmpty() && !force)) {
postponedDiffs.push(diff)
return
}
val representation = if (columnCount == 1) SearchableView.REPRESENTATION_LIST else SearchableView.REPRESENTATION_GRID
while (diff.isNotEmpty()) {
val action = diff.poll() ?: continue
if (action.action == DiffAction.ACTION_INSERT) {
val searchableView = SearchableView.getView(context, action.item, representation)
searchableView.representation = representation
searchableView.searchable = action.item
searchableView.onRepresentationChange = { _, newRepr ->
expandedItem = if (newRepr == SearchableView.REPRESENTATION_FULL) {
(getChildAt(expandedItem) as? SearchableView)?.back()
indexOfChild(searchableView)
} else -1
}
val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
searchableView.layoutParams = params
addView(searchableView, action.position)
}
if (action.action == DiffAction.ACTION_DELETE) {
removeViewAt(action.position)
}
}
}
companion object {
/**
* Row height is automatically set to match the largest children
*/
const val ROW_HEIGHT_AUTO = -1
}
}
class QueueUpdateCallback : ListUpdateCallback {
val operations = mutableListOf<DiffAction>()
override fun onChanged(position: Int, count: Int, payload: Any?) {
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
operations += DiffAction(action = DiffAction.ACTION_DELETE, position = fromPosition)
operations += DiffAction(action = DiffAction.ACTION_INSERT, position = toPosition)
}
override fun onInserted(position: Int, count: Int) {
for (i in 0 until count) {
operations += DiffAction(action = DiffAction.ACTION_INSERT, position = position + i)
}
}
override fun onRemoved(position: Int, count: Int) {
for (i in 1..count) {
operations += DiffAction(action = DiffAction.ACTION_DELETE, position = position + (count - i))
}
}
}
object SearchDiffUtil {
fun calculateDiff(oldItems: List<Searchable>, newItems: List<Searchable>): Queue<DiffAction> {
if (oldItems.isEmpty() && newItems.isEmpty()) return ArrayDeque()
val callback = object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldItems[oldItemPosition].key == newItems[newItemPosition].key
}
override fun getOldListSize(): Int {
return oldItems.size
}
override fun getNewListSize(): Int {
return newItems.size
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return areItemsTheSame(oldItemPosition, newItemPosition)
}
}
val diffResult = DiffUtil.calculateDiff(callback, false)
val updateCallback = QueueUpdateCallback()
diffResult.dispatchUpdatesTo(updateCallback)
val result = ArrayDeque<DiffAction>()
val mutableNewItems = mutableListOf<Searchable?>()
mutableNewItems.addAll(newItems)
for (i in updateCallback.operations.asReversed()) {
if (i.action == DiffAction.ACTION_INSERT) {
i.item = mutableNewItems[i.position]
mutableNewItems.removeAt(i.position)
} else {
mutableNewItems.add(i.position, null)
}
}
result.addAll(updateCallback.operations)
return result
}
}
data class DiffAction(val action: Int, val position: Int, var item: Searchable? = null) {
companion object {
const val ACTION_INSERT = 1
const val ACTION_DELETE = -1
}
}
@@ -0,0 +1,101 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.legacy.helper.ActivityStarterCallback
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
class SearchListView : LinearLayout, ActivityStarterCallback {
override fun onResume() {
while (postponedDiffs.isNotEmpty()) {
postponedDiffs.poll()?.let { applyDiff(it, true) }
}
}
@ObsoleteCoroutinesApi
private val updateActor = lifecycleScope
.actor<List<Searchable>>(Dispatchers.Main, capacity = Channel.CONFLATED) {
for (newItems in channel) {
val oldItems = currentItems
val diffResult = withContext(Dispatchers.Default) {
SearchDiffUtil.calculateDiff(oldItems, newItems)
}
currentItems = newItems
applyDiff(diffResult)
}
}
@ObsoleteCoroutinesApi
fun submitItems(items: List<Searchable>?) {
if (items == null) return
if (items.getOrNull(expandedItem)?.key != currentItems.getOrNull(expandedItem)?.key) expandedItem = -1
lifecycleScope.launch {
updateActor.send(items)
}
}
private var expandedItem = -1
set(value) {
(getChildAt(field) as? SearchableView)?.back()
requestLayout()
field = value
}
private var currentItems = listOf<Searchable>()
private val postponedDiffs = ArrayDeque<Queue<DiffAction>>()
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) {
layoutTransition = ChangingLayoutTransition()
clipChildren = false
orientation = VERTICAL
ActivityStarter.registerCallback(this)
}
/**
* Applies a diff queue. Enqueues to postponedDiffs if an activity is starting (leaving this view
* in an unstable state) or if postponedDiffs is not empty and [force] is not set.
*/
private fun applyDiff(diff: Queue<DiffAction>, force: Boolean = false) {
if (ActivityStarter.isStarting() || (postponedDiffs.isNotEmpty() && !force)) {
postponedDiffs.push(diff)
return
}
val representation = SearchableView.REPRESENTATION_LIST
while (diff.isNotEmpty()) {
val action = diff.poll() ?: continue
if (action.action == DiffAction.ACTION_INSERT) {
val searchableView = SearchableView.getView(context, action.item, representation)
searchableView.representation = representation
searchableView.searchable = action.item
searchableView.onRepresentationChange = { _, newRepr ->
expandedItem = if (newRepr == SearchableView.REPRESENTATION_FULL) {
(getChildAt(expandedItem) as? SearchableView)?.back()
indexOfChild(searchableView)
} else -1
}
val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
searchableView.layoutParams = params
addView(searchableView, action.position)
}
if (action.action == DiffAction.ACTION_DELETE) {
removeViewAt(action.position)
}
}
}
}
@@ -0,0 +1,67 @@
package de.mm20.launcher2.ui.legacy.search
import android.os.Build
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.transition.Scene
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@RequiresApi(Build.VERSION_CODES.N_MR1)
class AppShortcutDetailRepresentation: Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val appShortcut = searchable as AppShortcut
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_application_detail, context)
scene.setEnterAction {
with(rootView) {
setOnClickListener(null)
setOnLongClickListener(null)
findViewById<TextView>(R.id.appName).text = appShortcut.label
findViewById<LauncherIconView>(R.id.icon).apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(appShortcut.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = IconRepository.getInstance(context).getIconIfCached(appShortcut)
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(appShortcut, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
}
val appName = appShortcut.appName
findViewById<TextView>(R.id.appInfo).text = context.getString(R.string.shortcut_summary, appName)
val toolbar = findViewById<ToolbarView>(R.id.appToolbar)
setupToolbar(this, toolbar, appShortcut)
}
}
return scene
}
private fun setupToolbar(searchableView: SearchableView, toolbar: ToolbarView, shortcut: AppShortcut) {
val context = searchableView.context
val favAction = FavoriteToolbarAction(context, shortcut)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
searchableView.back()
}
toolbar.addAction(backAction, ToolbarView.PLACEMENT_START)
}
}
@@ -0,0 +1,115 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.content.Intent
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.transition.Scene
import com.bumptech.glide.Glide
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class WebsiteDetailRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val website = searchable as Website
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_website_detail, rootView.context)
scene.setEnterAction {
with(rootView) {
if (!hasBack()) {
scene.sceneRoot.elevation = 0f
scene.sceneRoot.setBackgroundColor(0)
scene.sceneRoot.setOnClickListener {
ActivityStarter.start(context, rootView, website)
}
}
val label = findViewById<TextView>(R.id.websiteTitle)
label.text = website.label
findViewById<TextView>(R.id.websiteDescription).text = website.description
val websiteImage = findViewById<ImageView>(R.id.websiteImage)
val websiteFavIcon = findViewById<LauncherIconView>(R.id.websiteFavIcon)
when {
website.image.isNotBlank() -> {
websiteImage.visibility = View.VISIBLE
websiteFavIcon.visibility = FrameLayout.GONE
Glide.with(context).load(website.image).into(websiteImage)
websiteImage.transitionName = "icon"
label.transitionName = "label"
websiteFavIcon.transitionName = null
}
website.favicon.isNotBlank() -> {
websiteFavIcon.visibility = View.VISIBLE
websiteImage.visibility = FrameLayout.GONE
websiteImage.transitionName = null
label.transitionName = null
websiteFavIcon.transitionName = "icon"
websiteFavIcon.apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(website.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = IconRepository.getInstance(context).getIconIfCached(website)
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(website, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
}
}
else -> {
websiteFavIcon.visibility = View.GONE
websiteImage.visibility = FrameLayout.GONE
websiteImage.transitionName = null
websiteFavIcon.transitionName = null
label.transitionName = null
}
}
val toolbar = findViewById<ToolbarView>(R.id.websiteToolbar)
setupMenu(rootView, toolbar, website)
}
}
return scene
}
private fun setupMenu(rootView: SearchableView, toolbar: ToolbarView, searchable: Website) {
val context = rootView.context
toolbar.clear()
if (rootView.hasBack()) {
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
toolbar.addAction(backAction, ToolbarView.PLACEMENT_START)
}
val favAction = FavoriteToolbarAction(context, searchable)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val shareAction = ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
shareAction.clickAction = {
share(context, searchable)
}
toolbar.addAction(shareAction, ToolbarView.PLACEMENT_END)
}
private fun share(context: Context, website: Website) {
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,108 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.content.Intent
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.transition.Scene
import com.bumptech.glide.Glide
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class WebsiteListRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val website = searchable as Website
val context = rootView.context as AppCompatActivity
val scene = Scene.getSceneForLayout(rootView, R.layout.view_website_list, rootView.context)
scene.setEnterAction {
with(rootView) {
if (!hasBack()) {
scene.sceneRoot.elevation = 0f
scene.sceneRoot.setBackgroundColor(0)
scene.sceneRoot.setOnClickListener {
ActivityStarter.start(context, rootView, website)
}
}
val label = findViewById<TextView>(R.id.websiteTitle)
label.text = website.label
findViewById<TextView>(R.id.websiteDescription).text = website.description
val websiteImage = findViewById<ImageView>(R.id.websiteImage)
val websiteFavIcon = findViewById<LauncherIconView>(R.id.websiteFavIcon)
when {
website.image.isNotBlank() -> {
websiteImage.visibility = View.VISIBLE
websiteFavIcon.visibility = FrameLayout.GONE
Glide.with(context).load(website.image).into(websiteImage)
websiteImage.transitionName = "icon"
label.transitionName = "label"
websiteFavIcon.transitionName = null
}
website.favicon.isNotBlank() -> {
websiteFavIcon.visibility = View.VISIBLE
websiteImage.visibility = FrameLayout.GONE
websiteImage.transitionName = null
label.transitionName = null
websiteFavIcon.transitionName = "icon"
websiteFavIcon.apply {
badge = BadgeProvider.getInstance(context).getLiveBadge(website.badgeKey)
shape = LauncherIconView.getDefaultShape(context)
icon = IconRepository.getInstance(context).getIconIfCached(website)
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(website, (84 * rootView.dp).toInt()).collect {
icon = it
}
}
}
}
else -> {
websiteFavIcon.visibility = View.GONE
websiteImage.visibility = FrameLayout.GONE
websiteImage.transitionName = null
websiteFavIcon.transitionName = null
label.transitionName = null
}
}
val toolbar = findViewById<ToolbarView>(R.id.websiteToolbar)
setupMenu(rootView, toolbar, website)
}
}
return scene
}
private fun setupMenu(rootView: SearchableView, toolbar: ToolbarView, searchable: Website) {
val context = rootView.context
toolbar.clear()
val favAction = FavoriteToolbarAction(context, searchable)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val shareAction = ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
shareAction.clickAction = {
share(context, searchable)
}
toolbar.addAction(shareAction, ToolbarView.PLACEMENT_END)
}
private fun share(context: Context, website: Website) {
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,76 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.content.Intent
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.transition.Scene
import com.bumptech.glide.Glide
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
class WikipediaDetailRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val wikipedia = searchable as Wikipedia
val scene = Scene.getSceneForLayout(rootView, R.layout.view_wikipedia_detail, rootView.context)
scene.setEnterAction {
with(rootView) {
findViewById<TextView>(R.id.wikipediaTitle).text = wikipedia.label
findViewById<TextView>(R.id.wikipediaText).text = HtmlCompat.fromHtml(wikipedia.text, HtmlCompat.FROM_HTML_MODE_LEGACY)
findViewById<ImageView>(R.id.wikipediaImage).also {
if (wikipedia.image.isNullOrBlank()) {
it.visibility = View.GONE
it.setImageDrawable(null)
} else {
if (wikipedia.image?.endsWith(".png") == true) {
it.scaleType = ImageView.ScaleType.CENTER_INSIDE
} else {
it.scaleType = ImageView.ScaleType.CENTER_CROP
}
it.visibility = View.VISIBLE
Glide.with(context).load(wikipedia.image).into(it)
}
}
val toolbar = findViewById<ToolbarView>(R.id.wikipediaToolbar)
setupMenu(rootView, toolbar, wikipedia)
}
}
return scene
}
private fun setupMenu(rootView: SearchableView, toolbar: ToolbarView, wikipedia: Wikipedia) {
val context = rootView.context
if (rootView.hasBack()) {
val backAction = ToolbarAction(R.drawable.ic_arrow_back, context.getString(R.string.menu_back))
backAction.clickAction = {
rootView.back()
}
toolbar.addAction(backAction, ToolbarView.PLACEMENT_START)
}
val favAction = FavoriteToolbarAction(context, wikipedia)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val shareAction = ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
shareAction.clickAction = {
share(context, wikipedia)
}
toolbar.addAction(shareAction, ToolbarView.PLACEMENT_END)
}
private fun share(context: Context, wikipedia: Wikipedia) {
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" +
"${context.getString(R.string.wikipedia_url)}/wiki?curid=${wikipedia.id}")
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}
}
@@ -0,0 +1,71 @@
package de.mm20.launcher2.ui.legacy.search
import android.content.Context
import android.content.Intent
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.transition.Scene
import com.bumptech.glide.Glide
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.FavoriteToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarAction
import de.mm20.launcher2.ui.legacy.view.ToolbarView
class WikipediaListRepresentation : Representation {
override fun getScene(rootView: SearchableView, searchable: Searchable, previousRepresentation: Int?): Scene {
val wikipedia = searchable as Wikipedia
val scene = Scene.getSceneForLayout(rootView, R.layout.view_wikipedia_list, rootView.context)
scene.setEnterAction {
with(rootView) {
findViewById<TextView>(R.id.wikipediaTitle).text = wikipedia.label
findViewById<TextView>(R.id.wikipediaText).text = HtmlCompat.fromHtml(wikipedia.text, HtmlCompat.FROM_HTML_MODE_LEGACY)
findViewById<ImageView>(R.id.wikipediaImage).also {
if (wikipedia.image.isNullOrBlank()) {
it.visibility = View.GONE
it.setImageDrawable(null)
} else {
if (wikipedia.image?.endsWith(".png") == true) {
it.scaleType = ImageView.ScaleType.CENTER_INSIDE
} else {
it.scaleType = ImageView.ScaleType.CENTER_CROP
}
it.visibility = View.VISIBLE
Glide.with(context).load(wikipedia.image).into(it)
}
}
val toolbar = findViewById<ToolbarView>(R.id.wikipediaToolbar)
setupMenu(rootView, toolbar, wikipedia)
}
}
return scene
}
private fun setupMenu(rootView: SearchableView, toolbar: ToolbarView, wikipedia: Wikipedia) {
val context = rootView.context
toolbar.clear()
val favAction = FavoriteToolbarAction(context, wikipedia)
toolbar.addAction(favAction, ToolbarView.PLACEMENT_END)
val shareAction = ToolbarAction(R.drawable.ic_share, context.getString(R.string.menu_share))
shareAction.clickAction = {
share(context, wikipedia)
}
toolbar.addAction(shareAction, ToolbarView.PLACEMENT_END)
}
private fun share(context: Context, wikipedia: Wikipedia) {
val text = wikipedia.text.toString()
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, "${wikipedia.label}\n\n" +
"${text.substring(0, 200)}\n\n" +
"${context.getString(R.string.wikipedia_url)}/wiki?curid=${wikipedia.id}")
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
}
}
@@ -0,0 +1,144 @@
package de.mm20.launcher2.ui.legacy.searchable
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout
import androidx.transition.*
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.transition.TextResize
import de.mm20.launcher2.ui.legacy.data.InformationText
import de.mm20.launcher2.ui.legacy.search.*
import de.mm20.launcher2.ui.legacy.transition.LauncherCards
import de.mm20.launcher2.ui.legacy.transition.LauncherIconViewTransition
import de.mm20.launcher2.ui.legacy.view.AspectRationImageView
@SuppressLint("ViewConstructor")
open class SearchableView(context: Context, representation: Int) : FrameLayout(context) {
var searchable: Searchable? = null
set(value) {
field = value
updateRepresentation(null)
}
private var defaultRepresentation = representation
var representation = representation
set(value) {
val oldVal = field
field = value
if (oldVal != value) {
onRepresentationChange(oldVal, value)
}
updateRepresentation(oldVal)
}
var onRepresentationChange: (Int, Int) -> Unit = { _, _ -> }
init {
clipChildren = false
updateRepresentation(null)
}
internal open fun updateRepresentation(previousRepresentation: Int?) {
when (representation) {
REPRESENTATION_FULL -> setFullRepresentation(previousRepresentation)
REPRESENTATION_LIST -> setListRepresentation(previousRepresentation)
REPRESENTATION_GRID -> setGridRepresentation(previousRepresentation)
else -> throw IllegalArgumentException("Must be REPRESENTATION_GRID, REPRESENTATION_LIST or REPRESENTATION_FULL")
}
}
private fun setGridRepresentation(previousRepresentation: Int?) {
val searchable = searchable
if (searchable == null) {
removeAllViews()
return
}
val scene = BasicGridRepresentation().getScene(this, searchable, null)
applyScene(scene)
}
private fun setListRepresentation(previousRepresentation: Int?) {
val searchable = searchable
if (searchable == null) {
removeAllViews()
return
}
val representation = when (searchable) {
is File -> FileListRepresentation()
is Contact -> ContactListRepresentation()
is CalendarEvent -> CalendarListRepresentation()
is Website -> WebsiteListRepresentation()
is Wikipedia -> WikipediaListRepresentation()
is InformationText -> InformationListRepresentation()
is MissingPermission -> PermissionListRepresentation()
else -> return
}
applyScene(representation.getScene(this, searchable, previousRepresentation))
}
private fun setFullRepresentation(previousRepresentation: Int?) {
val searchable = searchable
if (searchable == null) {
removeAllViews()
return
}
val representation = when (searchable) {
is Application -> ApplicationDetailRepresentation()
is Website -> WebsiteDetailRepresentation()
is File -> FileDetailRepresentation()
is Contact -> ContactDetailRepresentation()
is CalendarEvent -> CalendarDetailRepresentation()
is Wikipedia -> WikipediaDetailRepresentation()
is AppShortcut -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
AppShortcutDetailRepresentation()
} else {
return
}
else -> return
}
applyScene(representation.getScene(this, searchable, previousRepresentation))
}
private fun applyScene(scene: Scene) {
val transition = TransitionSet().apply {
addTransition(ChangeBounds().setInterpolator(DecelerateInterpolator()).excludeTarget(
AspectRationImageView::class.java, true))
addTransition(LauncherIconViewTransition())
addTransition(TextResize())
addTransition(LauncherCards())
ordering = TransitionSet.ORDERING_TOGETHER
setMatchOrder(Transition.MATCH_NAME, Transition.MATCH_ID)
}
TransitionManager.go(scene, transition)
}
var onBack: (() -> Unit)? = null
fun back() {
if (!hasBack()) {
onBack?.invoke()
return
}
representation = defaultRepresentation
}
fun hasBack(): Boolean {
return defaultRepresentation != REPRESENTATION_FULL
}
companion object {
const val REPRESENTATION_GRID = 0
const val REPRESENTATION_LIST = 1
const val REPRESENTATION_FULL = 2
fun getView(context: Context, searchable: Searchable?, representation: Int): SearchableView {
return SearchableView(context, representation)
}
}
}
@@ -0,0 +1,72 @@
package de.mm20.launcher2.ui.legacy.transition
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.transition.Transition
import androidx.transition.TransitionValues
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
class LauncherCards : Transition() {
override fun captureStartValues(transitionValues: TransitionValues) {
val view = transitionValues.view
if (view is LauncherCardView) {
transitionValues.values[PROP_ELEVATION] = view.cardElevation
transitionValues.values[PROP_BG_OPACITY] = view.backgroundOpacity
}
}
override fun captureEndValues(transitionValues: TransitionValues) {
val view = transitionValues.view
if (view is LauncherCardView) {
transitionValues.values[PROP_ELEVATION] = view.cardElevation
transitionValues.values[PROP_BG_OPACITY] = view.backgroundOpacity
}
}
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues == null || endValues == null) return null
if (startValues.view !is LauncherCardView) return null
val endView = endValues.view as? LauncherCardView
?: return null
val startElevation = startValues.values[PROP_ELEVATION] as Float
val endElevation = endValues.values[PROP_ELEVATION] as Float
val startBgOpacity = startValues.values[PROP_BG_OPACITY] as Int
val endBgOpacity = endValues.values[PROP_BG_OPACITY] as Int
if(startBgOpacity < endBgOpacity) {
return AnimatorSet().apply {
playTogether(
ObjectAnimator.ofFloat(endView, "cardElevation", startElevation, startElevation, endElevation).apply {
interpolator = AccelerateInterpolator()
},
ObjectAnimator.ofInt(endView, "backgroundOpacity", startBgOpacity, endBgOpacity).apply {
interpolator = DecelerateInterpolator()
}
)
}
}
return AnimatorSet().apply {
playTogether(
ObjectAnimator.ofFloat(endView, "cardElevation", startElevation, endElevation, endElevation).apply {
interpolator = DecelerateInterpolator()
},
ObjectAnimator.ofInt(endView, "backgroundOpacity", startBgOpacity, endBgOpacity).apply {
interpolator = AccelerateInterpolator()
}
)
}
}
companion object {
private const val PROP_ELEVATION = "mm20:app:elevation"
private const val PROP_BG_OPACITY = "mm20:app:bg_opacity"
}
}
@@ -0,0 +1,47 @@
package de.mm20.launcher2.ui.legacy.transition
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.ViewGroup
import androidx.transition.Transition
import androidx.transition.TransitionValues
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
class LauncherIconViewTransition : Transition() {
override fun captureStartValues(transitionValues: TransitionValues) {
if (transitionValues.view is LauncherIconView) {
transitionValues.values[PROP_FG_SCALE] = (transitionValues.view as LauncherIconView).foregroundScale
transitionValues.values[PROP_BG_SCALE] = (transitionValues.view as LauncherIconView).backgroundScale
}
}
override fun captureEndValues(transitionValues: TransitionValues) {
if (transitionValues.view is LauncherIconView) {
transitionValues.values[PROP_FG_SCALE] = (transitionValues.view as LauncherIconView).foregroundScale
transitionValues.values[PROP_BG_SCALE] = (transitionValues.view as LauncherIconView).backgroundScale
}
}
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues == null || endValues == null) return null
if(startValues.view !is LauncherIconView || endValues.view !is LauncherIconView) return null
val startFg = startValues.values[PROP_FG_SCALE] as Float
val endFg = endValues.values[PROP_FG_SCALE] as Float
val startBg = startValues.values[PROP_BG_SCALE] as Float
val endBg = endValues.values[PROP_BG_SCALE] as Float
return AnimatorSet().apply {
playTogether(
ObjectAnimator.ofFloat(startValues.view as LauncherIconView, "foregroundScale", startFg, endFg),
ObjectAnimator.ofFloat(startValues.view as LauncherIconView, "backgroundScale", startBg, endBg)
)
}
}
companion object {
private const val PROP_FG_SCALE = "mm20:app:launcherIconFgScale"
private const val PROP_BG_SCALE = "mm20:app:launcherIconBgScale"
private const val PROP_SIZE = "mm20:app:launcherIconSize"
}
}
@@ -0,0 +1,32 @@
package de.mm20.launcher2.ui.legacy.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import de.mm20.launcher2.ui.R
class AspectRationImageView : AppCompatImageView {
var aspectRatio = 1f
var fixedHeight = false
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) {
attrs?.let {
val ta = context.theme.obtainStyledAttributes(it, R.styleable.AspectRatioImageView, 0, defStyleRes)
aspectRatio = ta.getFloat(R.styleable.AspectRatioImageView_aspectRatio, 1f)
fixedHeight = ta.getBoolean(R.styleable.AspectRatioImageView_fixedHeight, false)
ta.recycle()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (fixedHeight) {
setMeasuredDimension((measuredHeight / aspectRatio).toInt(), measuredHeight)
} else {
setMeasuredDimension(measuredWidth, (measuredWidth * aspectRatio).toInt())
}
}
}
@@ -0,0 +1,139 @@
package de.mm20.launcher2.ui.legacy.view
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.BatteryManager
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import de.mm20.launcher2.ktx.dp
import java.util.*
class BatteryChargingView : View, LifecycleObserver {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private var animating = false
private val activity = context as AppCompatActivity
init {
activity.lifecycle.addObserver(this)
}
private val batteryReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != Intent.ACTION_BATTERY_CHANGED) return
update(intent, true)
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
val intent = activity.registerReceiver(batteryReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
start()
intent?.let { update(it, true) }
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
stop()
try {
activity.unregisterReceiver(batteryReceiver)
} catch (e: IllegalArgumentException) {
}
}
private fun update(intent: Intent, retryOnZeroCurrent: Boolean = false) {
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
start()
//Workaround for delayed current updates
if (retryOnZeroCurrent) postDelayed({ update(intent) }, 1000)
return
}
intensity = Math.round(current / 100000f).takeIf { it > 0 } ?: 1
start()
} else {
intensity = 0
}
}
var intensity = 0
set(value) {
if (field == 0 && value > 0) start()
if (value == 0) stop()
field = when {
value > 100 -> 100
value < 0 -> 0
else -> value
}
for (i in field until bubbles.size) {
bubbles.pop()
}
for (i in bubbles.size until field) {
bubbles.push(FloatArray(6) { 0f })
}
}
fun start() {
if (animating || intensity == 0) return
animating = true
invalidate()
}
fun stop() {
animating = false
}
/**
* 0: Pos X
* 1: Pos Y
* 2: Delta X
* 3: Delta Y
* 4: Radius
* 5: Lifetime left
*/
private var bubbles = ArrayDeque<FloatArray>()
private val paint = Paint()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (!animating) return
for (b in bubbles) {
if (b[5] <= 0f) {
b[0] = (Math.random() * width).toFloat()
b[1] = height.toFloat()
b[2] = ((Math.random() - 0.5) * width / 120f).toFloat() * dp
b[3] = -(Math.random() * height / 90f).toFloat() * dp
b[4] = (Math.random() * 2 + 2).toFloat() * dp
b[5] = (Math.random() * 80 + 40).toInt().toFloat()
}
paint.color = Color.argb((b[5] / 120f * 120).toInt(), 255, 255, 255)
canvas.drawCircle(b[0], b[1], b[4], paint)
b[0] += b[2]
b[1] += b[3]
b[5]--
}
postInvalidate()
}
}
@@ -0,0 +1,96 @@
package de.mm20.launcher2.ui.legacy.view
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.toRect
import androidx.core.view.isVisible
import androidx.core.view.iterator
import de.mm20.launcher2.ktx.copyTo
import de.mm20.launcher2.ktx.scale
import de.mm20.launcher2.ktx.toRectF
import de.mm20.launcher2.ktx.translate
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.ui.R
import kotlin.math.min
class BlurView : View {
private val globalRect = Rect()
private val blurPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) }
private val maskPaint = Paint().apply { color = Color.BLACK }
private val viewRect = RectF()
private val wallpaperRect = RectF()
private val windowRect = Rect()
private val dimWallpaper = LauncherPreferences.instance.dimWallpaper
private val dimPaint = Paint().apply { color = ContextCompat.getColor(context, R.color.wallpaper_dim) }
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
/*val blurredWallpaper = LauncherApplication.instance.blurredWallpaper
blurredWallpaper ?: return drawWallpaperDim(canvas)
if (blurredWallpaper.isRecycled) return drawWallpaperDim(canvas)
val parent = parent as? ViewGroup ?: return drawWallpaperDim(canvas)
drawMasks(parent, canvas)
getGlobalVisibleRect(globalRect)
globalRect.toRectF(viewRect)
getWindowVisibleDisplayFrame(windowRect)
/*canvas.drawBitmap(blurredWallpaper,
-(blurredWallpaper.width - viewRect.width()) / 2f,
-(blurredWallpaper.height - viewRect.height()) / 2f,
blurPaint)*/
if (blurredWallpaper.width >= width && blurredWallpaper.height >= height) {
viewRect.copyTo(wallpaperRect)
wallpaperRect.translate((blurredWallpaper.width - wallpaperRect.width()) / 2f, (blurredWallpaper.height - wallpaperRect.height()) / 2f)
} else {
val scale = min(blurredWallpaper.width / width.toFloat(), blurredWallpaper.height / height.toFloat())
viewRect.copyTo(wallpaperRect)
wallpaperRect.scale(scale)
wallpaperRect.translate((blurredWallpaper.width - wallpaperRect.width()) / 2, (blurredWallpaper.height - wallpaperRect.height()) / 2)
}
if (viewRect.top > 0f) {
wallpaperRect.translate(0f, viewRect.top)
}
canvas.drawBitmap(blurredWallpaper, wallpaperRect.toRect(), viewRect, blurPaint)
drawWallpaperDim(canvas)*/
}
private fun drawWallpaperDim(canvas: Canvas) {
if (dimWallpaper) {
canvas.drawRect(Rect(0, 0, canvas.width, canvas.height), dimPaint)
}
}
private var viewBounds = RectF()
private fun drawMasks(parent: ViewGroup, canvas: Canvas) {
loop@ for (view in parent.iterator()) {
when {
!view.isVisible || view.alpha == 0f -> {
}
view is LauncherCardView -> {
if (view.backgroundOpacity == 0 || view.backgroundOpacity == 0xFF) {
continue@loop
}
if (!view.getGlobalVisibleRect(globalRect)) continue@loop
globalRect.toRectF(viewBounds)
if (viewRect.top > 0f) {
viewBounds.translate(0f, -viewRect.top)
}
canvas.drawRoundRect(viewBounds, view.radius, view.radius, maskPaint)
}
view is ViewGroup -> {
drawMasks(view, canvas)
}
}
}
}
}
@@ -0,0 +1,22 @@
package de.mm20.launcher2.ui.legacy.view
import android.content.Context
import android.util.AttributeSet
import androidx.core.content.ContextCompat
import com.google.android.material.card.MaterialCardView
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.ui.R
open class InnerCardView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.materialCardViewStyle
) : MaterialCardView(context, attrs, defStyleAttr) {
init {
radius = LauncherPreferences.instance.cardRadius * dp
strokeColor = ContextCompat.getColor(context, R.color.color_divider)
strokeWidth = (1 * dp).toInt()
cardElevation = 2 * dp
outlineProvider = null
}
}
@@ -0,0 +1,68 @@
package de.mm20.launcher2.ui.legacy.view
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import com.google.android.material.card.MaterialCardView
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.CardBackground
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.ui.R
import kotlin.math.roundToInt
/**
* A card view implementation that solves the following issues of MaterialCardView:
* (1) Content clipping in transitions
* (2) Elevation overlay color
*/
open class LauncherCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.materialCardViewStyle
) : MaterialCardView(context, attrs, defStyleAttr) {
private val isDarkTheme = resources.getBoolean(R.bool.is_dark_theme)
var backgroundOpacity: Int = LauncherPreferences.instance.cardOpacity
set(value) {
setCardBackgroundColor(cardBackgroundColor.defaultColor.let {
ColorStateList.valueOf((it and 0xFFFFFF) or (value shl 24))
})
field = value
}
var strokeOpacity: Int = if (LauncherPreferences.instance.cardStrokeWidth > 0) 0xFF else 0
set(value) {
setStrokeColor(strokeColorStateList?.defaultColor?.let {
ColorStateList.valueOf((it and 0xFFFFFF) or (value shl 24))
})
field = value
}
init {
val cardColor = when (LauncherPreferences.instance.cardBackground) {
CardBackground.DEFAULT-> context.getColor(R.color.cardview_background)
CardBackground.BLACK -> context.getColor(R.color.cardview_background_black)
}
setCardBackgroundColor(cardColor)
strokeColor = cardColor
strokeWidth = (LauncherPreferences.instance.cardStrokeWidth * dp).roundToInt()
radius = LauncherPreferences.instance.cardRadius * dp
context.theme.obtainStyledAttributes(
attrs,
R.styleable.LauncherCardView,
0, 0).apply {
try {
backgroundOpacity = getInt(R.styleable.LauncherCardView_backgroundOpacity, LauncherPreferences.instance.cardOpacity)
} finally {
recycle()
}
}
strokeOpacity = if (backgroundOpacity == 0) 0 else 0xFF
elevation = if (backgroundOpacity == 255) elevation else 0f
cardElevation = elevation
}
}
@@ -0,0 +1,460 @@
package de.mm20.launcher2.ui.legacy.view
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Build
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.toRectF
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.helper.BitmapHolder
import java.lang.Math.pow
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.roundToInt
class LauncherIconView : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
var shape: IconShape
set(value) {
if (value == IconShape.PLATFORM_DEFAULT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
platformShape = getSystemShape()
transformMatrix = Matrix()
platformShapeBounds = RectF()
field = value
} else field = IconShape.CIRCLE
} else {
platformShape = null
transformMatrix = null
platformShapeBounds = null
field = value
}
}
private var platformShape: Path? = null
private var transformMatrix: Matrix? = null
private var platformShapeBounds: RectF? = null
@RequiresApi(Build.VERSION_CODES.O)
private fun getSystemShape(): Path {
return AdaptiveIconDrawable(null, null).iconMask
}
var icon: LauncherIcon? = null
set(value) {
field = value
foregroundScale = value?.foregroundScale ?: 1f
backgroundScale = value?.backgroundScale ?: 1f
value?.registerCallback(iconObserver)
invalidate()
}
var badge: LiveData<Badge>? = null
set(value) {
field = value
value?.observe(context as AppCompatActivity, badgeObserver)
invalidate()
}
private val badgeObserver = Observer<Badge> {
invalidate()
}
private val iconObserver: (LauncherIcon) -> Unit = {
foregroundScale = it.foregroundScale
backgroundScale = it.backgroundScale
// Implicit invalidate
}
var foregroundScale = 1f
set(value) {
field = value
postInvalidate()
}
var backgroundScale = 1f
set(value) {
field = value
postInvalidate()
}
init {
shape = IconShape.CIRCLE
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun setElevation(elevation: Float) {
super.setElevation(elevation)
shadowPaint = updateShadowPaint()
badgeShadowPaint = updateBadgeShadowPaing()
}
override fun setTranslationZ(translationZ: Float) {
super.setTranslationZ(translationZ)
shadowPaint = updateShadowPaint()
badgeShadowPaint = updateBadgeShadowPaing()
}
private var shadowPaint: Paint = updateShadowPaint()
private var badgeShadowPaint = updateBadgeShadowPaing()
private fun updateShadowPaint(): Paint {
return Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER)
color = Color.TRANSPARENT
isAntiAlias = true
setShadowLayer(0.5f * z, 0f, 0.5f * z, 0x40000000)
}
}
private fun updateBadgeShadowPaing(): Paint {
return Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
color = Color.TRANSPARENT
isAntiAlias = true
setShadowLayer(0.5f * z, 0f, 0.5f * z, 0x40000000)
}
}
private val drawRect = Rect()
private val bmpDrawRect = Rect()
private val maskPaint = Paint().apply {
style = Paint.Style.FILL
color = 0xFF000000.toInt()
isAntiAlias = true
}
private val bitmapPaint = Paint().apply {
color = 0xFF000000.toInt()
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
isAntiAlias = true
isFilterBitmap = true
}
private val badgePaint = Paint().apply {
color = ContextCompat.getColor(context, R.color.badge)
isAntiAlias = true
}
private val badgeTextPaint = Paint().apply {
color = ContextCompat.getColor(context, R.color.badge_text)
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val badgeProgressPaint = Paint().apply {
color = 0x30000000
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private var path: Path = Path()
private val badgeRect = RectF()
private val textBounds = Rect()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val fg = icon?.foreground ?: return
val bg = icon?.background
canvas.getClipBounds(drawRect)
drawRect.left += paddingLeft
drawRect.top += paddingTop
drawRect.right -= paddingRight
drawRect.bottom -= paddingBottom
val (bmp, c) = BitmapHolder.getBitmapAndCanvas((drawRect.width() * 1.8).toInt())
c.getClipBounds(bmpDrawRect)
fg.bounds = bmpDrawRect
if (bg != null) {
bg.bounds = bmpDrawRect
when (shape) {
IconShape.PLATFORM_DEFAULT -> {
path.rewind()
val matrix = transformMatrix!!
val bounds = platformShapeBounds!!
val shape = platformShape!!
shape.computeBounds(bounds, true)
matrix.setRectToRect(bounds, badgeRect.also { drawRect.toRectF(it) }, Matrix.ScaleToFit.CENTER)
path.rewind()
shape.transform(matrix, path)
canvas.drawPath(path, maskPaint)
}
IconShape.CIRCLE -> {
canvas.drawOval(drawRect.left.toFloat(), drawRect.top.toFloat(), drawRect.right.toFloat(), drawRect.bottom.toFloat(), maskPaint)
}
IconShape.SQUARE -> {
canvas.drawRect(drawRect, maskPaint)
}
IconShape.ROUNDED_SQUARE -> {
canvas.drawRoundRect(drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
width * 0.125f,
height * 0.125f,
maskPaint
)
}
IconShape.TRIANGLE -> {
path.rewind()
var cx = drawRect.left.toFloat()
var cy = drawRect.top + drawRect.height().toFloat() * 0.86f
val r = drawRect.width()
path.moveTo(cx, cy)
path.arcTo(cx - r, cy - r, cx + r, cy + r, 300f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 300f, 60f, true, maskPaint)
cx = drawRect.right.toFloat()
cy = drawRect.top + drawRect.height().toFloat() * 0.86f
path.lineTo(cx, cy)
path.arcTo(cx - r, cy - r, cx + r, cy + r, 180f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 180f, 60f, true, maskPaint)
cx = drawRect.left + drawRect.width() * 0.5f
cy = drawRect.top.toFloat()
path.lineTo(cx, cy)
path.close()
path.arcTo(cx - r, cy - r, cx + r, cy + r, 60f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 60f, 60f, true, maskPaint)
}
IconShape.SQUIRCLE -> {
path.rewind()
val radius = drawRect.width() / 2
val radiusToPow = pow(radius.toDouble(), 3.0)
path.moveTo(-radius.toFloat(), 0f)
for (x in -radius..radius)
path.lineTo(x.toFloat(), Math.cbrt(radiusToPow - Math.abs(x * x * x)).toFloat())
for (x in radius downTo -radius)
path.lineTo(x.toFloat(), (-Math.cbrt(radiusToPow - Math.abs(x * x * x))).toFloat())
path.close()
canvas.save()
canvas.translate(width / 2f, height / 2f)
canvas.drawPath(path, maskPaint)
canvas.restore()
}
IconShape.HEXAGON -> {
path.rewind()
path.moveTo(drawRect.left + drawRect.width() * 0.25f, drawRect.top + drawRect.height() * 0.933f)
path.lineTo(drawRect.left + drawRect.width() * 0.75f, drawRect.top + drawRect.height() * 0.933f)
path.lineTo(drawRect.left + drawRect.width() * 1.0f, drawRect.top + drawRect.height() * 0.5f)
path.lineTo(drawRect.left + drawRect.width() * 0.75f, drawRect.top + drawRect.height() * 0.067f)
path.lineTo(drawRect.left + drawRect.width() * 0.25f, drawRect.top + drawRect.height() * 0.067f)
path.lineTo(drawRect.left.toFloat(), drawRect.top + drawRect.height() * 0.5f)
path.close()
canvas.drawPath(path, maskPaint)
}
IconShape.HEART -> {
path.rewind()
path.moveTo(0.49999999f * drawRect.width() + drawRect.left, 1f * drawRect.height() + drawRect.top)
path.lineTo(0.42749999f * drawRect.width() + drawRect.left, 0.9339999999999999f * drawRect.height() + drawRect.top)
path.cubicTo(0.16999998f * drawRect.width() + drawRect.left, 0.7005004f * drawRect.height() + drawRect.top, 0f + drawRect.left, 0.5460004f * drawRect.height() + drawRect.top, 0f + drawRect.left, 0.3575003f * drawRect.height() + drawRect.top)
path.cubicTo(0f + drawRect.left, 0.2030004f * drawRect.height() + drawRect.top, 0.12100002f * drawRect.width() + drawRect.left, 0.0825004f * drawRect.height() + drawRect.top, 0.275f * drawRect.width() + drawRect.left, 0.0825004f * drawRect.height() + drawRect.top)
path.cubicTo(0.362f * drawRect.width() + drawRect.left, 0.0825004f * drawRect.height() + drawRect.top, 0.4455f * drawRect.width() + drawRect.left, 0.123f * drawRect.height() + drawRect.top, 0.5f * drawRect.width() + drawRect.left, 0.1865003f * drawRect.height() + drawRect.top)
path.cubicTo(0.55449999f * drawRect.width() + drawRect.left, 0.123f * drawRect.height() + drawRect.top, 0.638f * drawRect.width() + drawRect.left, 0.0825f * drawRect.height() + drawRect.top, 0.725f * drawRect.width() + drawRect.left, 0.0825f * drawRect.height() + drawRect.top)
path.cubicTo(0.87900006f * drawRect.width() + drawRect.left, 0.0825004f * drawRect.height() + drawRect.top, 1f * drawRect.width() + drawRect.left, 0.2030004f * drawRect.height() + drawRect.top, 1f * drawRect.width() + drawRect.left, 0.3575003f * drawRect.height() + drawRect.top)
path.cubicTo(1f * drawRect.width() + drawRect.left, 0.5460004f * drawRect.height() + drawRect.top, 0.82999999f * drawRect.width() + drawRect.left, 0.7005004f * drawRect.height() + drawRect.top, 0.57250001f * drawRect.width() + drawRect.left, 0.9340004f * drawRect.height() + drawRect.top)
path.close()
canvas.drawPath(path, maskPaint)
}
IconShape.PENTAGON -> {
path.rewind()
path.moveTo(0.49997027f * drawRect.width() + drawRect.left, 0.0060308f * drawRect.height() + drawRect.top)
path.lineTo(0.99994053f * drawRect.width() + drawRect.left, 0.36928048f * drawRect.height() + drawRect.top)
path.lineTo(0.80896887f * drawRect.width() + drawRect.left, 0.95703078f * drawRect.height() + drawRect.top)
path.lineTo(0.19097162f * drawRect.width() + drawRect.left, 0.95703076f * drawRect.height() + drawRect.top)
path.lineTo(drawRect.left.toFloat(), 0.36928045f * drawRect.height() + drawRect.top)
path.close()
canvas.drawPath(path, maskPaint)
}
}
c.save()
c.scale(backgroundScale, backgroundScale, bmpDrawRect.centerX().toFloat(), bmpDrawRect.centerY().toFloat())
bg.draw(c)
c.restore()
}
c.save()
c.scale(foregroundScale, foregroundScale, bmpDrawRect.centerX().toFloat(), bmpDrawRect.centerY().toFloat())
fg.draw(c)
c.restore()
if (bg != null) {
canvas.drawBitmap(bmp, bmpDrawRect, drawRect, bitmapPaint)
} else {
canvas.drawBitmap(bmp, bmpDrawRect, drawRect, maskPaint)
}
if (bg != null) {
when (shape) {
IconShape.CIRCLE -> {
canvas.drawOval(drawRect.left.toFloat(), drawRect.top.toFloat(), drawRect.right.toFloat(), drawRect.bottom.toFloat(), shadowPaint)
}
IconShape.SQUARE -> {
canvas.drawRect(drawRect, shadowPaint)
}
IconShape.ROUNDED_SQUARE -> {
canvas.drawRoundRect(drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
width * 0.125f,
height * 0.125f,
shadowPaint
)
}
IconShape.TRIANGLE, IconShape.HEXAGON, IconShape.HEART, IconShape.PENTAGON, IconShape.PLATFORM_DEFAULT -> {
canvas.drawPath(path, shadowPaint)
}
IconShape.SQUIRCLE -> {
canvas.save()
canvas.translate(width / 2f, height / 2f)
canvas.drawPath(path, shadowPaint)
canvas.restore()
}
}
}
val badgeSize = drawRect.width() * 0.30f
badgeRect.left = drawRect.right - badgeSize
badgeRect.top = drawRect.bottom - badgeSize
badgeRect.right = drawRect.right.toFloat()
badgeRect.bottom = drawRect.bottom.toFloat()
val badge = badge?.value ?: return
val badgeNumber = badge.number
val badgeProgress = badge.progress
val badgeIcon = badge.icon ?: badge.iconRes?.let { ContextCompat.getDrawable(context, it) }
if (badgeNumber == null && badgeProgress == null && badgeIcon == null) return
badgePaint.color = icon?.badgeColor ?: 0
canvas.drawOval(badgeRect, badgeShadowPaint)
canvas.drawOval(badgeRect, badgePaint)
badgeProgress?.let {
canvas.drawArc(badgeRect, 270f, it * 360, true, badgeProgressPaint)
}
badgeIcon?.let {
it.setBounds((drawRect.right - badgeSize * 0.9f).toInt(),
(drawRect.bottom - badgeSize * 0.9f).toInt(),
(drawRect.right - badgeSize * 0.1f).toInt(),
(drawRect.bottom - badgeSize * 0.1f).toInt()
)
it.setBounds(badgeRect.left.roundToInt(), badgeRect.top.roundToInt(), badgeRect.right.roundToInt(), badgeRect.bottom.roundToInt())
it.draw(canvas)
return
}
badgeNumber?.takeIf { it in 1..99 }?.let {
val text = it.toString()
val textSize = (1f - 0.1f - text.length * 0.1f) * badgeSize
badgeTextPaint.textSize = textSize
badgeTextPaint.getTextBounds(text, 0, text.length, textBounds)
canvas.drawText(it.toString(), badgeRect.centerX(), badgeRect.centerY() - textBounds.exactCenterY(), badgeTextPaint)
}
}
private var longClicked = false
private val longClickRunnable = Runnable {
longClicked = true
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
performLongClick()
}
private var downX = 0f
private var downY = 0f
override fun onTouchEvent(ev: MotionEvent): Boolean {
if (!hasOnClickListeners()) return false
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
animateTouchDown()
downX = ev.rawX
downY = ev.rawY
longClicked = false
handler?.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout().toLong())
return true
}
MotionEvent.ACTION_MOVE -> {
if (abs(hypot(downX - ev.rawX, downY - ev.rawY)) > width * 0.25f) {
handler?.removeCallbacks(longClickRunnable)
animateTouchUp()
return false
}
}
MotionEvent.ACTION_UP -> {
animateTouchUp()
if (ev.x > 0 && ev.x < width && ev.y > 0 && ev.y < height && !longClicked) {
performClick()
}
handler?.removeCallbacks(longClickRunnable)
return false
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_OUTSIDE -> {
animateTouchUp()
handler?.removeCallbacks(longClickRunnable)
return false
}
}
return true
}
private fun animateTouchUp() {
AnimatorSet().also {
it.playTogether(
ViewPropertyObjectAnimator.animate(this).translationZ(0f).get(),
ObjectAnimator.ofFloat(this, "foregroundScale", icon?.foregroundScale ?: 1f),
ObjectAnimator.ofFloat(this, "backgroundScale", icon?.backgroundScale ?: 1f)
)
it.duration = 300
it.start()
}
}
private fun animateTouchDown() {
AnimatorSet().also {
it.playTogether(
ViewPropertyObjectAnimator.animate(this).translationZ(2 * dp).get(),
ObjectAnimator.ofFloat(this, "foregroundScale", (icon?.foregroundScale
?: 1f) * 0.8f),
ObjectAnimator.ofFloat(this, "backgroundScale", (icon?.backgroundScale
?: 1f) * 1.2f)
)
it.duration = 250
it.start()
}
}
companion object {
fun getDefaultShape(context: Context): IconShape {
if (LauncherPreferences.instance.easterEggEnabled) return IconShape.HEART
return LauncherPreferences.instance.iconShape.let {
return@let if (it != IconShape.HEART) it else IconShape.PLATFORM_DEFAULT
}
}
}
}
@@ -0,0 +1,446 @@
package de.mm20.launcher2.ui.legacy.view
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.*
import android.view.animation.AccelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.card.MaterialCardView
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R
import kotlin.math.abs
class SwipeCardView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MaterialCardView(context, attrs, defStyleAttr) {
private val backdrop = FrameLayout(context)
private val icon = ImageView(context)
private val content = MaterialCardView(context)
private val iconColor = ContextCompat.getColor(context, R.color.swipe_card_icon_color)
private val iconColorActive =
ContextCompat.getColor(context, R.color.swipe_card_icon_color_active)
init {
super.addView(backdrop)
super.addView(icon, LayoutParams((40 * dp).toInt(), (24 * dp).toInt()))
icon.setColorFilter(iconColor)
super.addView(content)
content.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
content.radius = radius
content.transitionName = "SwipeCardView/content"
radius = LauncherPreferences.instance.cardRadius * dp
//content.setCardBackgroundColor(cardBackgroundColor)
super.setCardBackgroundColor(
ContextCompat.getColor(
context,
R.color.swipe_cardview_background
)
)
content.layoutTransition = ChangingLayoutTransition()
val ta = context.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
content.foreground = ta.getDrawable(0)
ta.recycle()
}
var leftAction: SwipeAction? = null
var rightAction: SwipeAction? = null
private var leftThreshold = false
set(value) {
if (value == field) return
if (value) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
leftAction?.color?.let { backdrop.setBackgroundColor(it) }
AnimatorSet().also {
it.playTogether(
ViewAnimationUtils.createCircularReveal(
backdrop,
(28 * dp).toInt(),
(height * 0.5).toInt(),
0f,
width.toFloat()
)
.setDuration(300),
ObjectAnimator.ofArgb(icon, "colorFilter", iconColor, iconColorActive)
.apply {
duration = 150
startDelay = 100
},
ObjectAnimator.ofFloat(icon, "scaleX", 1.2f).apply {
duration = 200
},
ObjectAnimator.ofFloat(icon, "scaleY", 1.2f).apply {
duration = 200
}
)
}.start()
} else {
AnimatorSet().also {
it.playTogether(
ViewAnimationUtils.createCircularReveal(
backdrop,
(28 * dp).toInt(),
(height * 0.5).toInt(),
width.toFloat(),
0f
).apply {
doOnEnd {
if (!rightThreshold && !leftThreshold) backdrop.setBackgroundColor(0)
}
duration = 300
},
ObjectAnimator.ofArgb(icon, "colorFilter", iconColorActive, iconColor)
.apply {
duration = 150
startDelay = 100
},
ObjectAnimator.ofFloat(icon, "scaleX", 1f).apply {
duration = 200
},
ObjectAnimator.ofFloat(icon, "scaleY", 1f).apply {
duration = 200
}
)
}.start()
}
field = value
}
private var rightThreshold = false
set(value) {
if (value == field) return
if (value) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
rightAction?.color?.let { backdrop.setBackgroundColor(it) }
AnimatorSet().also {
it.playTogether(
ViewAnimationUtils.createCircularReveal(
backdrop,
(width - 28 * dp).toInt(),
(height * 0.5).toInt(),
0f,
width.toFloat()
)
.setDuration(300),
ObjectAnimator.ofArgb(icon, "colorFilter", iconColor, iconColorActive)
.apply {
duration = 150
startDelay = 100
},
ObjectAnimator.ofFloat(icon, "scaleX", 1.2f).apply {
duration = 200
},
ObjectAnimator.ofFloat(icon, "scaleY", 1.2f).apply {
duration = 200
}
)
}.start()
} else {
AnimatorSet().also {
it.playTogether(
ViewAnimationUtils.createCircularReveal(
backdrop,
(width - 28 * dp).toInt(),
(height * 0.5).toInt(),
width.toFloat(),
0f
).apply {
doOnEnd {
if (!rightThreshold && !leftThreshold) backdrop.setBackgroundColor(0)
}
duration = 300
},
ObjectAnimator.ofArgb(icon, "colorFilter", iconColorActive, iconColor)
.apply {
duration = 150
startDelay = 100
},
ObjectAnimator.ofFloat(icon, "scaleX", 1f).apply {
duration = 200
},
ObjectAnimator.ofFloat(icon, "scaleY", 1f).apply {
duration = 200
}
)
}.start()
}
field = value
}
private var swipeDirectionLeft: Boolean? = null
set(value) {
if (field == value) return
backdrop.setBackgroundColor(0)
if (value == true) {
leftAction?.icon?.let { icon.setImageResource(it) }
icon.setPadding((16 * dp).toInt(), 0, 0, 0)
icon.layoutParams = (icon.layoutParams as LayoutParams).also {
it.gravity = Gravity.CENTER_VERTICAL or Gravity.START
}
icon.pivotX = 28 * dp
} else if (value == false) {
rightAction?.icon?.let { icon.setImageResource(it) }
icon.setPadding(0, 0, (16 * dp).toInt(), 0)
icon.layoutParams = (icon.layoutParams as LayoutParams).also {
it.gravity = Gravity.CENTER_VERTICAL or Gravity.END
}
icon.pivotX = 12 * dp
}
field = value
}
override fun setCardBackgroundColor(color: Int) {
content.setCardBackgroundColor(color)
}
override fun setCardBackgroundColor(color: ColorStateList?) {
content.setCardBackgroundColor(color)
}
override fun addView(child: View?) {
content.addView(child)
}
override fun addView(child: View?, params: ViewGroup.LayoutParams?) {
content.addView(child, params)
}
override fun addView(child: View?, width: Int, height: Int) {
content.addView(child, width, height)
}
override fun setRadius(radius: Float) {
super.setRadius(radius)
content?.radius = radius
}
override fun removeAllViews() {
content.removeAllViews()
}
override fun removeAllViewsInLayout() {
content.removeAllViewsInLayout()
}
override fun removeView(view: View?) {
content.removeView(view)
}
override fun removeViewAt(index: Int) {
content.removeViewAt(index)
}
override fun removeViewInLayout(view: View?) {
content.removeViewInLayout(view)
}
override fun removeViews(start: Int, count: Int) {
content.removeViews(start, count)
}
override fun removeViewsInLayout(start: Int, count: Int) {
content.removeViewsInLayout(start, count)
}
private var downX = 0f
private var downY = 0f
private var isClick = false
private var isLongClick = false
private val longClickRunnable = Runnable {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
performLongClick()
isClick = false
isLongClick = true
content.foreground?.state = intArrayOf(android.R.attr.state_enabled)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
return when (event?.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
isClick = true
isLongClick = false
handler?.postDelayed(
longClickRunnable,
ViewConfiguration.getLongPressTimeout().toLong()
)
content.foreground?.setHotspot(event.x, event.y)
content.foreground?.state =
intArrayOf(android.R.attr.state_pressed, android.R.attr.state_enabled)
true
}
MotionEvent.ACTION_MOVE -> {
if (abs(event.x - downX) > abs(event.y - downY)) parent.requestDisallowInterceptTouchEvent(
true
)
if (isLongClick) return false
swipeDirectionLeft = event.x - downX > 0
if (isClick && abs(event.x - downX) < 4 * dp) {
return true
}
isClick = false
handler?.removeCallbacks(longClickRunnable)
content.translationX = event.x - downX
leftThreshold = content.translationX > 0.5f * width
rightThreshold = content.translationX < -0.5f * width
content.foreground?.state = intArrayOf(android.R.attr.state_enabled)
true
}
MotionEvent.ACTION_UP -> {
when {
isClick -> {
performClick()
content.foreground?.state = intArrayOf(android.R.attr.state_enabled)
return false
}
leftThreshold -> {
if (leftAction?.action?.invoke() == true) {
content.animate().translationX(width.toFloat())
.setDuration(200)
.setInterpolator(AccelerateInterpolator())
.start()
} else {
content.animate().translationX(0f)
.setDuration(300)
.start()
}
}
rightThreshold -> {
if (rightAction?.action?.invoke() == true) {
content.animate().translationX(-width.toFloat())
.setDuration(200)
.setInterpolator(AccelerateInterpolator())
.start()
} else {
content.animate().translationX(0f)
.setDuration(300)
.start()
}
}
else -> {
content.animate().translationX(0f).setDuration(300).start()
}
}
true
}
MotionEvent.ACTION_CANCEL -> {
content.animate().translationX(0f).setDuration(300).start()
handler?.removeCallbacks(longClickRunnable)
content.foreground?.state = intArrayOf(android.R.attr.state_enabled)
false
}
MotionEvent.ACTION_OUTSIDE -> {
handler?.removeCallbacks(longClickRunnable)
content.foreground?.state = intArrayOf(android.R.attr.state_enabled)
true
}
else -> false
}
}
open class SwipeAction(
@DrawableRes var icon: Int,
var color: Int,
/**
* Action that is performed after a swipe.
* returns true if the card should be animated out or false if it should be animated back.
*/
var action: () -> Boolean
)
}
class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
SwipeCardView.SwipeAction(
R.drawable.ic_star_solid,
ContextCompat.getColor(context, R.color.amber),
{ false }
) {
val pinned =
ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java].isPinned(
searchable
)
init {
pinned.observe(context as LifecycleOwner) {
setPinned(it)
}
}
private fun setPinned(pinned: Boolean) {
if (pinned) {
icon = R.drawable.ic_star_outline
action = {
ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java].unpinItem(
searchable
)
false
}
} else {
icon = R.drawable.ic_star_solid
action = {
ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java].pinItem(
searchable
)
false
}
}
}
}
class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeCardView.SwipeAction(
R.drawable.ic_visibility_off,
ContextCompat.getColor(context, R.color.blue),
{ false }
) {
val hidden =
ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java].isHidden(
searchable
)
init {
hidden.observe(context as LifecycleOwner) {
setHidden(it)
}
}
private fun setHidden(hidden: Boolean) {
if (hidden) {
icon = R.drawable.ic_visibility
action = {
ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java].unhideItem(
searchable
)
true
}
} else {
icon = R.drawable.ic_visibility_off
action = {
ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java].hideItem(
searchable
)
true
}
}
}
}
@@ -0,0 +1,273 @@
package de.mm20.launcher2.ui.legacy.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.setPadding
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.search.data.Searchable
class ToolbarView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val slots = context.resources.getInteger(R.integer.config_toolbarSlots)
private var leftOverflowIcon: ImageView? = null
private var rightOverflowIcon: ImageView? = null
private val leftActions = mutableListOf<ToolbarAction>()
private val rightActions = mutableListOf<ToolbarAction>()
var iconStyle = R.style.LauncherTheme_IconStyle
init {
orientation = HORIZONTAL
clipChildren = false
clipToPadding = false
val spacer = View(context)
spacer.layoutParams = LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)
addView(spacer)
}
fun addAction(action: ToolbarAction, placement: Int) {
val useOverflowMenu = (leftActions.size >= slots && placement == PLACEMENT_START) ||
(rightActions.size >= slots && placement == PLACEMENT_END)
if (useOverflowMenu) {
if (placement == PLACEMENT_START) {
if (leftOverflowIcon == null) {
val overflowMenuIcon = ImageView(context, null, R.attr.iconStyle)
overflowMenuIcon.isClickable = true
overflowMenuIcon.isFocusable = true
overflowMenuIcon.setPadding((12 * dp).toInt())
overflowMenuIcon.layoutParams = LayoutParams((48 * dp).toInt(), (48 * dp).toInt())
overflowMenuIcon.setImageResource(R.drawable.ic_more_vert)
removeViewAt(leftActions.size - 1)
addView(overflowMenuIcon, leftActions.size - 1)
leftOverflowIcon = overflowMenuIcon
}
leftActions.add(action)
val popup = PopupMenu(context, leftOverflowIcon!!)
for (i in slots - 1 until leftActions.size) {
if (leftActions[i].subActions.isNotEmpty()) {
val submenu = popup.menu.addSubMenu(leftActions[i].title)
for ((j, sa) in leftActions[i].subActions.withIndex()) {
submenu.add(i, j, 0, sa.title)
}
} else {
popup.menu.add(i, 0, 0, leftActions[i].title)
}
}
popup.setOnMenuItemClickListener {
if (leftActions[it.groupId].subActions.isEmpty()) {
leftActions[it.groupId].clickAction?.invoke()
} else {
leftActions[it.groupId].subActions[it.itemId].clickAction.invoke()
}
true
}
leftOverflowIcon?.setOnClickListener {
popup.show()
}
} else {
if (rightOverflowIcon == null) {
val overflowMenuIcon = ImageView(context, null, R.attr.iconStyle)
overflowMenuIcon.isClickable = true
overflowMenuIcon.isFocusable = true
overflowMenuIcon.setPadding((12 * dp).toInt())
overflowMenuIcon.layoutParams = LayoutParams((48 * dp).toInt(), (48 * dp).toInt())
overflowMenuIcon.setImageResource(R.drawable.ic_more_vert)
removeViewAt(childCount - 1)
addView(overflowMenuIcon)
rightOverflowIcon = overflowMenuIcon
}
rightActions.add(action)
val popup = PopupMenu(context, rightOverflowIcon!!)
for (i in slots - 1 until rightActions.size) {
if (rightActions[i].subActions.isNotEmpty()) {
val submenu = popup.menu.addSubMenu(i, -1, 0, rightActions[i].title)
for ((j, sa) in rightActions[i].subActions.withIndex()) {
submenu.add(i, j, 0, sa.title)
}
} else {
val item = popup.menu.add(i, -1, 0, rightActions[i].title)
rightActions[i].titleChanged = {
item.title = rightActions[i].title
}
}
}
popup.setOnMenuItemClickListener {
if (rightActions[it.groupId].subActions.isEmpty()) {
rightActions[it.groupId].clickAction?.invoke()
} else if (it.itemId != -1) {
rightActions[it.groupId].subActions[it.itemId].clickAction.invoke()
}
true
}
rightOverflowIcon?.setOnClickListener {
popup.show()
}
}
} else {
val imageView = getIconView(action)
if (placement == PLACEMENT_START) {
addView(imageView, leftActions.size)
leftActions.add(action)
} else {
addView(imageView)
rightActions.add(action)
}
}
}
private fun getIconView(action: ToolbarAction): ImageView {
val imageView = ImageView(context, null, 0, iconStyle)
imageView.setImageResource(action.icon)
imageView.isClickable = true
imageView.isFocusable = true
action.iconChanged = {
imageView.setImageResource(action.icon)
}
action.titleChanged = {
TooltipCompat.setTooltipText(imageView, action.title)
}
TooltipCompat.setTooltipText(imageView, action.title)
val submenu = if (action.subActions.isEmpty()) null else PopupMenu(context, imageView).apply {
for ((i, subAction) in action.subActions.withIndex()) {
menu.add(0, i, 0, subAction.title)
}
setOnMenuItemClickListener {
action.subActions[it.itemId].clickAction.invoke()
true
}
}
imageView.setOnClickListener { _ ->
if (submenu != null) {
submenu.show()
} else {
action.clickAction?.invoke()
}
}
imageView.setPadding((12 * dp).toInt())
imageView.layoutParams = LayoutParams((48 * dp).toInt(), (48 * dp).toInt())
return imageView
}
fun clear() {
removeAllViews()
leftActions.clear()
rightActions.clear()
leftOverflowIcon = null
rightOverflowIcon = null
val spacer = View(context)
spacer.layoutParams = LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)
addView(spacer)
}
companion object {
val PLACEMENT_START = 0
val PLACEMENT_END = 1
}
}
open class ToolbarAction(icon: Int, title: String) {
@DrawableRes
var icon: Int = icon
set(@DrawableRes value) {
field = value
iconChanged?.invoke()
}
var title = title
set(value) {
field = value
titleChanged?.invoke()
}
var subActions: MutableList<ToolbarSubaction> = mutableListOf()
var clickAction: (() -> Unit)? = null
internal var iconChanged: (() -> Unit)? = null
internal var titleChanged: (() -> Unit)? = null
}
open class ToolbarSubaction(val title: String, var clickAction: (() -> Unit)) {
}
class FavoriteToolbarAction(val context: Context, val item: Searchable)
: ToolbarAction(
R.drawable.ic_star_outline,
context.getString(R.string.favorites_menu_pin)
) {
private val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
private val isPinned = viewModel.isPinned(item)
init {
isPinned.observe(context as AppCompatActivity, Observer {
it ?: return@Observer
if (it) {
title = context.getString(R.string.favorites_menu_unpin)
icon = R.drawable.ic_star_solid
} else {
title = context.getString(R.string.favorites_menu_pin)
icon = R.drawable.ic_star_outline
}
})
clickAction = {
if (isPinned.value == true) {
viewModel.unpinItem(item)
} else {
viewModel.pinItem(item)
}
}
}
}
class VisibilityToolbarAction(val context: Context, val item: Searchable)
: ToolbarAction(
R.drawable.ic_visibility,
context.getString(R.string.menu_hide)
) {
private val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
private val isHidden = viewModel.isHidden(item)
init {
isHidden.observe(context as AppCompatActivity, Observer {
if (it) {
title = context.getString(R.string.menu_unhide)
icon = R.drawable.ic_visibility
} else {
title = context.getString(R.string.menu_hide)
icon = R.drawable.ic_visibility_off
}
})
clickAction = {
if (isHidden.value == true) {
viewModel.unhideItem(item)
} else {
viewModel.hideItem(item)
}
}
}
}
@@ -0,0 +1,43 @@
package de.mm20.launcher2.ui.legacy.view
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.MotionEvent.INVALID_POINTER_ID
import android.view.View
import android.widget.ImageView
class WidgetResizeDragView : ImageView {
var resizeView: View? = null
private var lastY = 0f
var onResize: ((Int) -> Unit)? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
val y = event.y
lastY = y
}
MotionEvent.ACTION_MOVE -> {
val y = event.y
val dY = y - lastY
val view = resizeView ?: return false
val params = view.layoutParams
val newHeight = (view.height + dY).toInt()
params.height = newHeight
onResize?.invoke(newHeight)
view.layoutParams = params
}
}
return true
}
}
@@ -0,0 +1,203 @@
package de.mm20.launcher2.ui.legacy.widget
import android.animation.LayoutTransition
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.provider.CalendarContract
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.Menu
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.ui.legacy.data.InformationText
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import kotlinx.android.synthetic.main.view_calendar_widget.view.*
import java.util.*
import kotlin.math.max
import kotlin.math.min
class CalendarWidget : LauncherWidget {
override val canResize: Boolean
get() = false
override val settingsFragment: String?
get() = "calendar"
override val compactView: CompactView?
get() = null
override val compactViewRanking: Int
get() {
return -1
}
private val calendarEvents: LiveData<List<CalendarEvent>>
private val pinnedCalendarEvents: LiveData<List<CalendarEvent>>
private val zoneOffset = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis())
private var selectedDay = 0L
set(value) {
field = value
calendarDate.text = formatDay(value)
updateEventList()
}
private var availableDays: List<Long> = emptyList()
set(value) {
if (value.indexOf(selectedDay) == -1) selectedDay = 0L
field = value
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
override fun update() {
}
private fun formatDay(day: Long): String {
return when (day) {
0L -> context.getString(R.string.date_today)
1L -> context.getString(R.string.date_tomorrow)
else -> DateUtils.formatDateTime(context, (getToday() + day) * (1000 * 60 * 60 * 24), DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY)
}
}
init {
clipToPadding = false
clipChildren = false
View.inflate(context, R.layout.view_calendar_widget, this)
calendarNewEvent.setOnClickListener {
val intent = Intent(Intent.ACTION_EDIT)
intent.data = CalendarContract.Events.CONTENT_URI
ActivityStarter.start(context, this, intent = intent)
}
calendarDate.setOnClickListener {
val menu = PopupMenu(context, calendarDate)
for (d in availableDays) {
menu.menu.add(
Menu.NONE,
d.toInt(),
Menu.NONE,
formatDay(d)
)
}
menu.setOnMenuItemClickListener {
selectedDay = it.itemId.toLong()
true
}
menu.show()
}
calendarOpenApp.setOnClickListener {
val startMillis = System.currentTimeMillis()
val builder = CalendarContract.CONTENT_URI.buildUpon()
builder.appendPath("time")
ContentUris.appendId(builder, startMillis)
val intent = Intent(Intent.ACTION_VIEW)
.setData(builder.build())
ActivityStarter.start(context, calendarWidgetRoot, intent = intent)
}
calendarDateNext.setOnClickListener {
val i = min(availableDays.lastIndex - 1, availableDays.indexOf(selectedDay))
selectedDay = availableDays[i + 1]
}
calendarDatePrev.setOnClickListener {
val i = max(1, availableDays.indexOf(selectedDay))
selectedDay = availableDays[i - 1]
}
val viewModel = ViewModelProvider(context as AppCompatActivity)[CalendarViewModel::class.java]
val favViewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
calendarEvents = viewModel.upcomingCalendarEvents
pinnedCalendarEvents = favViewModel.pinnedCalendarEvents
calendarEvents.observe(context as AppCompatActivity, {
if (!PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) {
calendarWidgetList.submitItems(listOf(
MissingPermission(
context.getString(R.string.permission_calendar_widget),
PermissionsManager.CALENDAR
)
))
return@observe
}
val today = getToday()
availableDays = it
.map { ((it.startTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today }
.union(it.map { ((it.endTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today })
.union(listOf(0L))
.toSet().toList().sorted()
updateEventList()
})
pinnedCalendarEvents.observe(context as AppCompatActivity) {
val today = getToday()
calendarWidgetPinnedList.submitItems(it.filter {
it.endTime > System.currentTimeMillis() &&
(it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) != today &&
(it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) != today
}.sortedBy { it.startTime })
if (it.isEmpty()) {
calendarWidgetPinnedList.visibility = View.GONE
calendarUpcomingEventsTitle.visibility = View.GONE
} else {
calendarWidgetPinnedList.visibility = View.VISIBLE
calendarUpcomingEventsTitle.visibility = View.VISIBLE
}
}
calendarWidgetRoot.layoutTransition = LayoutTransition().apply {
enableTransitionType(LayoutTransition.CHANGING)
}
}
private fun getToday(): Long {
return (System.currentTimeMillis() + zoneOffset) / (1000 * 60 * 60 * 24)
}
private fun updateEventList(includePastEvents: Boolean = false) {
val today = getToday()
val events: MutableList<Searchable> = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay || (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay }
?.toMutableList() ?: mutableListOf()
if (events.isEmpty()) {
events.add(
InformationText(context.getString(R.string.calendar_widget_no_events))
)
}
val pastEvents = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) < today + selectedDay && (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) > today + selectedDay }
if (pastEvents?.isNotEmpty() == true) {
if (includePastEvents) {
events.addAll(pastEvents)
} else {
events.add(InformationText(resources.getQuantityString(R.plurals.calendar_widget_running_events, pastEvents.size, pastEvents.size)) {
updateEventList(true)
})
}
}
calendarWidgetList.submitItems(events)
}
override val name: String
get() = resources.getString(R.string.widget_name_calendar)
companion object {
const val ID = "calendar"
}
}
@@ -0,0 +1,9 @@
package de.mm20.launcher2.ui.legacy.widget
interface CompactView {
fun setTranslucent(translucent: Boolean)
fun update() {}
var goToParent: (() -> Unit)?
}
@@ -0,0 +1,66 @@
package de.mm20.launcher2.ui.legacy.widget
import android.annotation.SuppressLint
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.ScrollView
import androidx.core.view.get
import androidx.core.view.iterator
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.widgets.Widget
@SuppressLint("ViewConstructor")
class ExternalWidget(
context: Context,
val widget: Widget,
host: AppWidgetHost
) : LauncherWidget(context) {
val widgetInfo: AppWidgetProviderInfo?
init {
val id = widget.data.toInt()
widgetInfo = AppWidgetManager.getInstance(context.applicationContext).getAppWidgetInfo(id)
show = widgetInfo != null
val widgetView = host.createView(context.applicationContext, id, widgetInfo)
?: View(context)
if (widgetView is AppWidgetHostView && widgetView.childCount > 0) {
enableNestedScroll(widgetView[0])
}
val h = widget.height * dp
val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, h.toInt())
val p = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
layoutParams = params
widgetView.layoutParams = p
addView(widgetView)
}
private fun enableNestedScroll(view: View) {
if (view is ViewGroup) {
for (child in view.iterator()) {
enableNestedScroll(child)
}
}
if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true
}
override fun update() {}
override val compactViewRanking: Int
get() = -1
override val compactView: CompactView?
get() = null
override val settingsFragment: String?
get() = null
override val canResize: Boolean
get() = true
override val name: String
get() = widgetInfo?.loadLabel(context.packageManager) ?: ""
}
@@ -0,0 +1,43 @@
package de.mm20.launcher2.ui.legacy.widget
import android.animation.LayoutTransition
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
abstract class LauncherWidget : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
layoutTransition = LayoutTransition().apply {
enableTransitionType(LayoutTransition.CHANGING)
}
}
abstract fun update()
fun updateCompactView() {
compactView?.update()
}
abstract val compactViewRanking: Int
abstract val compactView: CompactView?
abstract val settingsFragment: String?
open val hasSettings = false
abstract val canResize: Boolean
abstract val name: String
var show: Boolean = true
set(value) {
onVisibilityChanged?.invoke(value)
field = value
}
var onVisibilityChanged: ((Boolean) -> Unit)? = null
open fun startResize() {}
open fun endResize() {}
open fun openSettings() {}
}
@@ -0,0 +1,176 @@
package de.mm20.launcher2.ui.legacy.widget
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.AnimatedVectorDrawable
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.alpha
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.music.MusicViewModel
import de.mm20.launcher2.music.PlaybackState
import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.widget.MusicWidget
import de.mm20.launcher2.ui.widget.WeatherWidget
import kotlinx.android.synthetic.main.compact_music.view.*
class MusicWidget : LauncherWidget {
override val compactViewRanking: Int
get() = if (viewModel.hasActiveSession) 1 else -1
override val compactView: CompactView?
get() {
return MusicCompactView(context)
}
override val settingsFragment: String?
get() = null
override val canResize: Boolean
get() = false
override val name: String
get() = context.getString(R.string.widget_name_music)
private val viewModel = ViewModelProvider(context as AppCompatActivity)[MusicViewModel::class.java]
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
val composeView = ComposeView(context)
composeView.id = FrameLayout.generateViewId()
composeView.setContent {
LegacyLauncherTheme {
// TODO: Temporary solution until parent widget card is rewritten in Compose
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onSurface) {
Column {
MusicWidget()
}
}
}
}
addView(composeView)
}
override fun update() {
}
companion object {
const val ID = "music"
}
}
class MusicCompactView : FrameLayout, CompactView {
private val viewModel = ViewModelProvider(context as AppCompatActivity)[MusicViewModel::class.java]
override fun setTranslucent(translucent: Boolean) {
if (translucent) {
musicCompactTitle.setTextColor(Color.WHITE)
musicCompactArtist.setTextColor(Color.WHITE)
musicCompactNext.elevation = 2 * dp
musicCompactPlay.elevation = 2 * dp
musicCompactNext.alpha = 1f
musicCompactPlay.alpha = 1f
musicCompactNext.imageTintList = ColorStateList.valueOf(Color.WHITE)
musicCompactPlay.imageTintList = ColorStateList.valueOf(Color.WHITE)
val shadowY = resources.getDimension(R.dimen.elevation_shadow_1dp_y)
val shadowR = resources.getDimension(R.dimen.elevation_shadow_1dp_radius)
val shadowC = Color.argb(66, 0, 0, 0)
musicCompactTitle.setShadowLayer(shadowR, 0f, shadowY, shadowC)
musicCompactArtist.setShadowLayer(shadowR, 0f, shadowY, shadowC)
} else {
val primaryColor = ContextCompat.getColorStateList(context, R.color.text_color_primary)!!
musicCompactTitle.setTextColor(primaryColor)
musicCompactArtist.setTextColor(ContextCompat.getColorStateList(context, R.color.text_color_secondary))
musicCompactNext.elevation = 0f
musicCompactPlay.elevation = 0f
musicCompactNext.alpha = primaryColor.defaultColor.alpha / 255f
musicCompactPlay.alpha = primaryColor.defaultColor.alpha / 255f
musicCompactNext.imageTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.icon_color))
musicCompactPlay.imageTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.icon_color))
musicCompactTitle.setShadowLayer(0f, 0f, 0f, 0)
musicCompactArtist.setShadowLayer(0f, 0f, 0f, 0)
}
}
private var playPauseIcon = if (viewModel.playbackState.value == PlaybackState.Playing) R.drawable.ic_pause else R.drawable.ic_play
set(value) {
if (value != field) {
val icon = context.getDrawable(value)
musicCompactPlay.setImageDrawable(icon)
(icon as? AnimatedVectorDrawable)?.start()
field = value
}
}
override var goToParent: (() -> Unit)? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
View.inflate(context, R.layout.compact_music, this)
clipChildren = false
musicCompactNext.setOnClickListener {
viewModel.next()
(musicCompactNext.drawable as AnimatedVectorDrawable).start()
}
musicCompactPlay.setOnClickListener { _ ->
viewModel.togglePause()
}
musicCompactMeta.setOnClickListener {
ActivityStarter.start(context, this, pendingIntent = viewModel.getLaunchIntent(context))
}
viewModel.title.observe(context as AppCompatActivity, Observer {
musicCompactTitle.text = it
})
viewModel.artist.observe(context as AppCompatActivity, Observer {
musicCompactArtist.text = it
})
viewModel.playbackState.observe(context as AppCompatActivity, Observer {
if (it == PlaybackState.Playing) {
playPauseIcon = R.drawable.ic_play_to_pause
musicCompactPlay.setOnClickListener {
viewModel.pause()
}
musicCompactTitle.isSelected = true
musicCompactArtist.isSelected = true
} else {
playPauseIcon = R.drawable.ic_pause_to_play
musicCompactPlay.setOnClickListener {
viewModel.play()
}
musicCompactTitle.isSelected = false
musicCompactArtist.isSelected = false
}
})
}
override fun update() {
musicCompactTitle.text = viewModel.title.value
musicCompactArtist.text = viewModel.artist.value
playPauseIcon = if (viewModel.playbackState.value == PlaybackState.Playing) R.drawable.ic_pause else R.drawable.ic_play
}
}
@@ -0,0 +1,279 @@
package de.mm20.launcher2.ui.legacy.widget
import android.animation.AnimatorSet
import android.animation.LayoutTransition
import android.animation.ObjectAnimator
import android.content.ActivityNotFoundException
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.provider.AlarmClock
import android.provider.CalendarContract
import android.text.format.DateFormat
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextClock
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.postDelayed
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.fragment.SearchableBottomSheet
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
import kotlinx.android.synthetic.main.view_date_time.view.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.util.*
class SmartWidget : LauncherCardView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
View.inflate(context, R.layout.view_date_time, this)
clipToPadding = false
clipChildren = false
layoutTransition = LayoutTransition()
dateTimeTimeView.format12Hour = "hh:mm"
dateTimeTimeView.format24Hour = "HH:mm"
dateTimeTimeView.setOnClickListener {
try {
val intent = Intent(AlarmClock.ACTION_SHOW_ALARMS)
ActivityStarter.start(context, this, intent = intent)
} catch (_: ActivityNotFoundException) {
// Ignore
}
}
postDelayed(1) {
translucent = true
}
}
private val translucentDisableRunnable = Runnable@{
if (translucent) return@Runnable
dateTimeTimeView.setShadowLayer(0f, 0f, 0f, 0)
val textColor = ContextCompat.getColorStateList(context, R.color.text_color_primary)
val dividerColor = ContextCompat.getColor(context, R.color.color_divider)
dateTimeTimeView.setTextColor(textColor)
bottomPadding.setBackgroundColor(dividerColor)
bottomPadding.elevation = 0f
compactView?.setTranslucent(false)
}
private val translucentEnableRunnable = Runnable@{
if (!translucent) return@Runnable
val textColor = Color.argb(255, 255, 255, 255)
val shadowY = resources.getDimension(R.dimen.elevation_shadow_1dp_y)
val shadowR = resources.getDimension(R.dimen.elevation_shadow_1dp_radius)
val shadowC = Color.argb(66, 0, 0, 0)
dateTimeTimeView.setTextColor(textColor)
dateTimeTimeView.setShadowLayer(shadowR, 0f, shadowY, shadowC)
bottomPadding.setBackgroundColor(textColor)
bottomPadding.elevation = 1f
compactView?.setTranslucent(true)
}
var translucent: Boolean = false
set(value) {
if (value == field) return
if (value) {
removeCallbacks(translucentDisableRunnable)
postDelayed(translucentEnableRunnable, 100)
AnimatorSet().apply {
duration = 200
playTogether(
ObjectAnimator.ofInt(this@SmartWidget, "backgroundOpacity", 0).apply {
interpolator = AccelerateInterpolator(3f)
},
ObjectAnimator.ofFloat(this@SmartWidget, "translationZ", -elevation).apply {
interpolator = DecelerateInterpolator(3f)
}
)
}.start()
} else {
removeCallbacks(translucentEnableRunnable)
postDelayed(translucentDisableRunnable, 70)
AnimatorSet().apply {
duration = 200
playTogether(
ObjectAnimator.ofFloat(this@SmartWidget, "translationZ", 0f).apply {
interpolator = AccelerateInterpolator(3f)
},
ObjectAnimator.ofInt(this@SmartWidget, "backgroundOpacity", LauncherPreferences.instance.cardOpacity).apply {
interpolator = DecelerateInterpolator(3f)
}
)
}.start()
}
field = value
}
var compactView: CompactView? = getDefaultCompactView()
set(value) {
smartWidgetContainer.removeView(field as? View)
if (value == null) {
field = getDefaultCompactView()
} else {
field = value
}
(field as? View)?.let {
it.layoutParams = getCompactViewLayoutParams()
smartWidgetContainer.addView(it)
}
field?.setTranslucent(translucent)
}
private fun getDefaultCompactView(): CompactView {
return DateCompactView(context)
}
private fun getCompactViewLayoutParams(): RelativeLayout.LayoutParams {
val params = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT)
params.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE)
params.addRule(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE)
params.addRule(RelativeLayout.START_OF, R.id.smartWidgetDivider)
params.marginStart = (16 * dp).toInt()
params.marginEnd = (16 * dp).toInt()
return params
}
}
class DateCompactView : TextClock, CompactView {
override fun setTranslucent(translucent: Boolean) {
if (translucent) {
val textColor = Color.argb(255, 255, 255, 255)
val shadowY = resources.getDimension(R.dimen.elevation_shadow_1dp_y)
val shadowR = resources.getDimension(R.dimen.elevation_shadow_1dp_radius)
val shadowC = Color.argb(66, 0, 0, 0)
setShadowLayer(shadowR, 0f, shadowY, shadowC)
setTextColor(textColor)
} else {
val textColor = ContextCompat.getColorStateList(context, R.color.text_color_primary)
setShadowLayer(0f, 0f, 0f, 0)
setTextColor(textColor)
}
}
override var goToParent: (() -> Unit)? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
isClickable = true
elevation = 2 * dp
isFocusable = true
setPadding(0, (16 * dp).toInt(), 0, (16 * dp).toInt())
textSize = 20f
setTextColor(ContextCompat.getColorStateList(context, R.color.text_color_primary))
setOnClickListener {
val startMillis = System.currentTimeMillis()
val builder = CalendarContract.CONTENT_URI.buildUpon()
builder.appendPath("time")
ContentUris.appendId(builder, startMillis)
val intent = Intent(Intent.ACTION_VIEW)
.setData(builder.build())
ActivityStarter.start(context, this, intent = intent)
}
val dayFormat = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMMdyyyy")
val dayOfWeekFormat = DateFormat.getBestDateTimePattern(Locale.getDefault(), "EEEE")
val dateFormat = context.getString(R.string.date_format_clock_widget, dayOfWeekFormat, dayFormat)
format12Hour = dateFormat
format24Hour = dateFormat
val outValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, outValue, true)
foreground = context.getDrawable(outValue.resourceId)
}
}
class RecommendedAppsCompactView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr), CompactView {
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
val items = viewModel.getTopFavorites(4)
init {
clipChildren = false
clipToPadding = false
layoutTransition = LayoutTransition()
items.observe(context as LifecycleOwner, Observer {
removeAllViews()
for (s in it) {
val searchableView = LauncherIconView(context)
searchableView.icon = s.getPlaceholderIcon(context)
lifecycleScope.launch {
IconRepository.getInstance(context).getIcon(s, (48 * dp).toInt()).collect {
searchableView.icon = it
}
}
val frameLayout = FrameLayout(context)
frameLayout.clipChildren = false
frameLayout.clipToPadding = false
searchableView.layoutParams = FrameLayout.LayoutParams(
(48 * dp).toInt(),
(48 * dp).toInt()
).also {
it.gravity = Gravity.CENTER
}
searchableView.badge = BadgeProvider.getInstance(context).getLiveBadge(s.badgeKey)
searchableView.elevation = 2 * dp
searchableView.setOnClickListener {
if(!ActivityStarter.start(context, searchableView, s)) {
searchableView.performLongClick()
}
}
searchableView.setOnLongClickListener {
SearchableBottomSheet(s).show((context as AppCompatActivity).supportFragmentManager, null)
return@setOnLongClickListener true
}
frameLayout.addView(searchableView)
frameLayout.layoutParams = LayoutParams(
0,
(48 * dp).toInt()
).also {
it.weight = 1f
}
addView(frameLayout)
}
})
}
override fun setTranslucent(translucent: Boolean) {
}
override var goToParent: (() -> Unit)? = null
}
@@ -0,0 +1,74 @@
package de.mm20.launcher2.ui.legacy.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.compose.foundation.layout.Column
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.widget.WeatherWidget
class WeatherWidget : LauncherWidget {
override fun update() {
}
override val canResize: Boolean
get() = false
override val settingsFragment: String?
get() = "weather"
override val compactView: CompactView?
get() = WeatherCompactView(context)
override val compactViewRanking: Int
get() = -1
override val hasSettings = true
override val name: String
get() = resources.getString(R.string.widget_name_weather)
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
init {
val composeView = ComposeView(context)
composeView.id = FrameLayout.generateViewId()
composeView.setContent {
LegacyLauncherTheme {
// TODO: Temporary solution until parent widget card is rewritten in Compose
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onSurface) {
Column {
WeatherWidget()
}
}
}
}
addView(composeView)
}
companion object {
const val ID = "weather"
}
}
class WeatherCompactView(context: Context) : FrameLayout(context), CompactView {
override var goToParent: (() -> Unit)? = null
init {
View.inflate(context, R.layout.compact_weather, this)
}
override fun setTranslucent(translucent: Boolean) {
}
}
@@ -0,0 +1,16 @@
package de.mm20.launcher2.ui.locals
import android.appwidget.AppWidgetHost
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.geometry.Size
import de.mm20.launcher2.ui.theme.WallpaperColors
import de.mm20.launcher2.ui.theme.colors.ColorScheme
import de.mm20.launcher2.ui.theme.colors.DefaultColorScheme
val LocalWindowSize = compositionLocalOf { Size(0f, 0f) }
val LocalAppWidgetHost = compositionLocalOf<AppWidgetHost?>(defaultFactory = { null })
val LocalWallpaperColors = compositionLocalOf<WallpaperColors?> { null }
val LocalColorScheme = compositionLocalOf<ColorScheme> { DefaultColorScheme() }
@@ -0,0 +1,163 @@
package de.mm20.launcher2.ui.search
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
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 de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.*
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun ApplicationItem(
modifier: Modifier = Modifier,
app: Application,
representation: Representation,
initialRepresentation: Representation,
onRepresentationChange: ((Representation) -> Unit)
) {
val padding by animateDpAsState(
if (representation == Representation.Grid) 0.dp else 16.dp
)
val iconSize by animateDpAsState(
if (representation == Representation.Grid) 52.dp else 84.dp
)
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(padding)
) {
Column(
modifier = Modifier
.weight(1f, true)
) {
AnimatedVisibility(
representation == Representation.Full,
enter = expandIn() + fadeIn(),
exit = shrinkOut() + fadeOut(),
) {
Column {
Text(
text = app.label,
style = MaterialTheme.typography.h1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
app.version?.let {
Text(
text = it,
style = MaterialTheme.typography.body1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Text(
text = app.`package`,
style = MaterialTheme.typography.body1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
val width by animateDpAsState(
if (representation == Representation.Grid) LocalGridColumnWidth.current else iconSize,
spring(Spring.StiffnessHigh)
)
Column(
modifier = Modifier
.widthIn(max = width)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
ShapedLauncherIcon(
item = app,
size = iconSize,
onLongClick = {
onRepresentationChange(Representation.Full)
}
)
}
}
AnimatedVisibility(representation == Representation.Full) {
val leftActions = listOf(
DefaultToolbarAction(
stringResource(id = R.string.menu_back),
Icons.Rounded.ArrowBack
) { onRepresentationChange(initialRepresentation) }
)
val storeDetails = app.getStoreDetails(LocalContext.current)
val rightActions = listOf(
favoritesToolbarAction(app),
DefaultToolbarAction(
stringResource(id = R.string.menu_app_info),
Icons.Rounded.Info
) { },
DefaultToolbarAction(
stringResource(id = R.string.menu_uninstall),
Icons.Rounded.Delete
) { },
if (storeDetails == null) {
DefaultToolbarAction(
stringResource(id = R.string.menu_share),
Icons.Rounded.Share,
{}
)
} else {
SubmenuToolbarAction(
stringResource(id = R.string.menu_share),
Icons.Rounded.Share,
listOf(
DefaultToolbarAction(
stringResource(
id = R.string.share_menu_store_link,
storeDetails.label
),
Icons.Rounded.Share,
{}
),
DefaultToolbarAction(
stringResource(id = R.string.share_menu_apk_file),
Icons.Rounded.Share,
{}
)
)
)
},
hideToolbarAction(app),
)
Toolbar(
modifier = Modifier.fillMaxWidth(),
leftActions = leftActions,
rightActions = rightActions
)
}
AnimatedVisibility(representation == Representation.Grid) {
GridItemLabel(app)
}
}
}
@@ -0,0 +1,18 @@
package de.mm20.launcher2.ui.search
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.applications.AppViewModel
import de.mm20.launcher2.ui.SectionDivider
@Composable
fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit {
val apps by viewModel<AppViewModel>().applications.observeAsState(emptyList())
return {
LegacySearchableGrid(items = apps, listState = it)
}
}
@@ -0,0 +1,48 @@
package de.mm20.launcher2.ui.search
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.ShapedLauncherIcon
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun BasicGridItem(
modifier: Modifier,
item: Searchable,
iconSize: Dp,
showLabel: Boolean = true,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
ShapedLauncherIcon(
item = item,
size = iconSize,
onClick = onClick,
onLongClick = onLongClick
)
AnimatedVisibility(
showLabel
) {
GridItemLabel(
item
)
}
}
}
@@ -0,0 +1,67 @@
package de.mm20.launcher2.ui.search
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.calculator.CalculatorViewModel
import de.mm20.launcher2.ui.SectionDivider
@Composable
fun calculatorItem(): LazyListScope.() -> Unit {
val calculator by viewModel<CalculatorViewModel>().calculator.observeAsState()
return {
calculator?.let {
item {
Column(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(it.getBeatifiedTerm())
}
Text(
text = "= ${it.formattedString}",
style = MaterialTheme.typography.h1,
modifier = Modifier.align(Alignment.End),
)
if (it.term.matches(Regex("(0x|0b)?[0-9]+"))) {
Text(
it.formattedBinaryString,
fontFamily = FontFamily.Monospace,
modifier = Modifier
.align(Alignment.End)
.padding(top = 8.dp),
)
Text(
it.formattedHexString,
fontFamily = FontFamily.Monospace,
modifier = Modifier.align(Alignment.End),
)
Text(
it.formattedOctString,
fontFamily = FontFamily.Monospace,
modifier = Modifier.align(Alignment.End),
)
}
}
}
SectionDivider()
}
}
}
@@ -0,0 +1,17 @@
package de.mm20.launcher2.ui.search
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
@Composable
fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit {
val favorites by viewModel<FavoritesViewModel>().getFavorites(5).observeAsState(emptyList())
return {
LegacySearchableGrid(items = favorites, listState = it)
}
}
@@ -0,0 +1,177 @@
package de.mm20.launcher2.ui.search
import android.text.format.Formatter
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
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 de.mm20.launcher2.search.data.File
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.Toolbar
import de.mm20.launcher2.ui.component.favoritesToolbarAction
import de.mm20.launcher2.ui.component.hideToolbarAction
@OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class)
@Composable
fun FileItem(
modifier: Modifier = Modifier,
file: File,
representation: Representation,
initialRepresentation: Representation,
onRepresentationChange: ((Representation) -> Unit)
) {
val iconSize = 52.dp
val padding by animateDpAsState(
if (representation == Representation.Grid) 0.dp else 16.dp
)
Column(
modifier = Modifier
.combinedClickable(
enabled = representation == Representation.List,
onClick = {},
onLongClick = {
onRepresentationChange(Representation.Full)
}
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(padding),
) {
Column(
modifier = Modifier
.weight(1f, true)
.padding(end = 8.dp)
) {
AnimatedVisibility(
representation != Representation.Grid
) {
Column {
Text(
text = file.label,
style = MaterialTheme.typography.h2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
AnimatedVisibility(
representation == Representation.List
) {
Text(
text = file.getFileType(LocalContext.current),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
AnimatedVisibility(representation == Representation.Full) {
Column {
Text(
text = "${stringResource(R.string.file_meta_type)}: ${file.mimeType}",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (file.path.isNotBlank()) {
Text(
text = "${stringResource(R.string.file_meta_path)}: ${file.path}",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (!file.isDirectory) {
Text(
text = "${stringResource(R.string.file_meta_size)}: ${
Formatter.formatShortFileSize(
LocalContext.current, file.size
)
}",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
for ((k, v) in file.metaData) {
Text(
text = "${stringResource(k)}: ${v}",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
val width by animateDpAsState(
if (representation == Representation.Grid) LocalGridColumnWidth.current else iconSize,
spring(Spring.StiffnessHigh)
)
Column(
modifier = Modifier
.widthIn(max = width)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
ShapedLauncherIcon(
item = file,
size = iconSize,
onLongClick = {
onRepresentationChange(Representation.Full)
}
)
}
}
AnimatedVisibility(representation == Representation.Full) {
val leftActions = listOf(
DefaultToolbarAction(
stringResource(id = R.string.menu_back),
Icons.Rounded.ArrowBack
) { onRepresentationChange(initialRepresentation) }
)
val rightActions = listOf(
favoritesToolbarAction(file),
DefaultToolbarAction(
stringResource(id = R.string.menu_delete),
Icons.Rounded.Delete
) { },
hideToolbarAction(file),
DefaultToolbarAction(
stringResource(id = R.string.menu_share),
Icons.Rounded.Share
) {}
)
Toolbar(
modifier = Modifier.fillMaxWidth(),
leftActions = leftActions,
rightActions = rightActions
)
}
AnimatedVisibility(representation == Representation.Grid) {
GridItemLabel(file)
}
}
}
@@ -0,0 +1,16 @@
package de.mm20.launcher2.ui.search
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.files.FilesViewModel
@Composable
fun fileResults(): LazyListScope.() -> Unit {
val files by viewModel<FilesViewModel>().files.observeAsState(emptyList())
return {
files?.let { SearchableList(items = it) }
}
}
@@ -0,0 +1,34 @@
package de.mm20.launcher2.ui.search
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.Searchable
@Composable
fun ColumnScope.GridItemLabel(
item: Searchable
) {
Text(
text = item.label,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body1,
softWrap = false,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(
top = 8.dp, bottom = 4.dp
)
)
}
@@ -0,0 +1,228 @@
package de.mm20.launcher2.ui.search
import android.view.ViewGroup
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import com.google.accompanist.insets.LocalWindowInsets
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.SectionDivider
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.legacy.search.SearchGridView
import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.toPixels
fun LazyListScope.LegacySearchableGrid(
items: List<Searchable>,
columns: Int = 5,
listState: LazyListState
) {
item {
AndroidView(
{
SearchGridView(it).apply {
columnCount = columns
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
}, modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.animateContentSize()
) {
it.submitItems(items)
}
}
if (items.isNotEmpty()) {
SectionDivider()
}
}
@OptIn(ExperimentalAnimationApi::class)
fun LazyListScope.NotSoLazySearchableGrid(
items: List<Searchable>,
columns: Int = 5,
listState: LazyListState
) {
val rows = (items.size + columns - 1) / columns
item {
for (rowIndex in 0 until rows) {
var focusedItem by remember { mutableStateOf(-1) }
if (focusedItem != -1 && listState.isScrollInProgress) focusedItem = -1
Row(
modifier = Modifier
.requiredHeight(100.dp)
.zIndex(
animateFloatAsState(
if (focusedItem != -1 && rowIndex == focusedItem / columns) 100f else 0f
).value
)
) {
for (colIndex in 0 until columns) {
val itemIndex = rowIndex * columns + colIndex
if (itemIndex < items.size) {
GridItem(
item = items[itemIndex],
column = colIndex,
totalColumns = columns,
hasFocus = itemIndex == focusedItem,
requestFocus = {
focusedItem = if (it) itemIndex else -1
})
} else {
Spacer(Modifier.weight(1f, fill = true))
}
}
}
}
}
}
@OptIn(ExperimentalAnimationApi::class)
fun LazyListScope.SearchableGrid(
items: List<Searchable>,
columns: Int = 5,
listState: LazyListState
) {
val rows = (items.size + columns - 1) / columns
items(rows) { rowIndex ->
var focusedItem by remember { mutableStateOf(-1) }
if (focusedItem != -1 && listState.isScrollInProgress) focusedItem = -1
Row(
modifier = Modifier
.requiredHeight(100.dp)
.zIndex(
animateFloatAsState(
if (focusedItem != -1 && rowIndex == focusedItem / columns) 100f else 0f
).value
)
) {
for (colIndex in 0 until columns) {
val itemIndex = rowIndex * columns + colIndex
if (itemIndex < items.size) {
GridItem(
item = items[itemIndex],
column = colIndex,
totalColumns = columns,
hasFocus = itemIndex == focusedItem,
requestFocus = {
focusedItem = if (it) itemIndex else -1
})
} else {
Spacer(Modifier.weight(1f, fill = true))
}
}
}
}
if (items.isNotEmpty()) {
SectionDivider()
}
}
@Composable
fun RowScope.GridItem(
item: Searchable,
column: Int,
totalColumns: Int,
hasFocus: Boolean,
requestFocus: (Boolean) -> Unit
) {
val insets = LocalWindowInsets.current.systemBars
val topSpace = insets.top + 64.dp.toPixels()
val gridWidth =
LocalWindowSize.current.width.toDp() - 16.dp - (insets.left + insets.right).toDp()
val representation = if (hasFocus) Representation.Full else Representation.Grid
val offsetX by animateDpAsState(
if (representation == Representation.Grid) 0.dp
else gridWidth / totalColumns * ((totalColumns - 1) / 2 - column)
)
var calculatedYOffset by remember { mutableStateOf(0f) }
val offsetY by animateDpAsState(
if (representation == Representation.Grid) 0.dp
else calculatedYOffset.toDp()
)
val width by animateDpAsState(
if (representation == Representation.Grid) gridWidth / totalColumns
else gridWidth
)
val z by animateFloatAsState(
if (representation == Representation.Grid) 0f
else 100f
)
val windowSize = LocalWindowSize.current
Box(
modifier = Modifier
.weight(1f, fill = true)
.fillMaxHeight()
.zIndex(z)
.onGloballyPositioned {
calculatedYOffset = if (representation == Representation.Full) {
val position = it.positionInWindow()
val size = it.size
val topOffset = -position.y + topSpace + size.height / 2
if (topOffset > 0) {
topOffset
} else {
val bottom = position.y + size.height
val bottomOffset =
-(bottom - windowSize.height) - size.height / 2 - insets.bottom
if (bottomOffset < 0) {
bottomOffset
} else {
0f
}
}
} else {
0f
}
}
) {
key(item.key) {
CompositionLocalProvider(LocalGridColumnWidth provides width) {
SearchableItem(
item = item,
modifier = Modifier
.offset(offsetX, offsetY)
.requiredWidth(width)
.wrapContentHeight(unbounded = true)
.align(Alignment.BottomCenter),
representation = representation,
initialRepresentation = Representation.Grid,
onRepresentationChange = {
requestFocus(it == Representation.Full)
}
)
}
}
}
}
val LocalGridColumnWidth = compositionLocalOf { 0.dp }
@@ -0,0 +1,113 @@
package de.mm20.launcher2.ui.search
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.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.component.DefaultSwipeActions
@Composable
fun SearchableItem(
modifier: Modifier = Modifier,
item: Searchable,
initialRepresentation: Representation = Representation.List
) {
var representation by remember { mutableStateOf(initialRepresentation) }
SearchableItem(
modifier = modifier,
item = item,
representation = representation,
initialRepresentation = initialRepresentation,
onRepresentationChange = { representation = it }
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchableItem(
modifier: Modifier = Modifier,
item: Searchable,
representation: Representation,
initialRepresentation: Representation,
onRepresentationChange: ((Representation) -> Unit)
) {
DefaultSwipeActions(
modifier = modifier
.padding(vertical = 4.dp, horizontal = 4.dp),
item = item,
enabled = representation == Representation.List
) {
val transition = updateTransition(representation, label = "SearchableItem")
val cardElevation by transition.animateDp(
label = "cardElevation",
transitionSpec = {
if (targetState == Representation.Full) tween(200, delayMillis = 200)
else tween(200)
}) {
if (it == Representation.Full) 4.dp else 0.dp
}
val cardAlpha by transition.animateFloat(
label = "cardAlpha",
transitionSpec = {
if (targetState == Representation.Full) tween(300)
else tween(300, delayMillis = 100)
}) {
if (it == Representation.Grid) 0f else 1f
}
Card(
backgroundColor = MaterialTheme.colors.surface.copy(alpha = cardAlpha),
elevation = cardElevation
) {
when (item) {
is Application -> {
ApplicationItem(
app = item,
representation = representation,
initialRepresentation = initialRepresentation,
onRepresentationChange = onRepresentationChange
)
}
is File -> {
FileItem(
file = item,
representation = representation,
initialRepresentation = initialRepresentation,
onRepresentationChange = onRepresentationChange
)
}
is Wikipedia -> {
WikipediaItem(
wikipedia = item,
representation = representation,
initialRepresentation = initialRepresentation,
onRepresentationChange = onRepresentationChange)
}
}
}
}
}
enum class Representation {
Grid,
List,
Full
}
@@ -0,0 +1,23 @@
package de.mm20.launcher2.ui.search
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.SectionDivider
fun LazyListScope.SearchableList(
items: List<Searchable>
) {
items(items) {
ListItem(it)
}
if (items.isNotEmpty()) {
SectionDivider()
}
}
@Composable
fun ListItem(item: Searchable) {
SearchableItem(item = item)
}
@@ -0,0 +1,76 @@
package de.mm20.launcher2.ui.search
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.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
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.favoritesToolbarAction
@Composable
fun WikipediaItem(
wikipedia: Wikipedia,
representation: Representation,
initialRepresentation: Representation,
onRepresentationChange: ((Representation) -> Unit)
) {
val hPadding = if (initialRepresentation == Representation.Full) 8.dp else 16.dp
Column(
modifier = Modifier
) {
Column(
modifier = Modifier.padding(start = hPadding, end = hPadding, top = 16.dp)
) {
Text(
text = wikipedia.label,
style = MaterialTheme.typography.h1,
)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
modifier = Modifier.padding(vertical = 4.dp),
text = stringResource(R.string.wikipedia_source),
)
}
Text(
text = wikipedia.text
)
}
val leftActions = if (initialRepresentation == Representation.Full) {
emptyList()
} else {
listOf(
DefaultToolbarAction(
stringResource(id = R.string.menu_back),
Icons.Rounded.ArrowBack
) { onRepresentationChange(initialRepresentation) }
)
}
val rightActions = listOf(
favoritesToolbarAction(wikipedia),
DefaultToolbarAction(
stringResource(id = R.string.menu_share),
Icons.Rounded.Share
) {
}
)
Toolbar(
modifier = Modifier.fillMaxWidth(),
leftActions = leftActions,
rightActions = rightActions
)
}
}

Some files were not shown because too many files have changed in this diff Show More