Initial commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
+393
@@ -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
Reference in New Issue
Block a user