Migrate search UI to Jetpack Compose
This commit is contained in:
parent
b997a7cb62
commit
665664df70
@ -1,15 +1,10 @@
|
|||||||
package de.mm20.launcher2.badges
|
package de.mm20.launcher2.badges
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import de.mm20.launcher2.badges.providers.*
|
import de.mm20.launcher2.badges.providers.*
|
||||||
import de.mm20.launcher2.badges.providers.BadgeProvider
|
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -47,40 +42,42 @@ internal class BadgeRepositoryImpl(private val context: Context) : BadgeReposito
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getBadge(badgeKey: String): Flow<Badge?> = channelFlow {
|
override fun getBadge(badgeKey: String): Flow<Badge?> = channelFlow {
|
||||||
badgeProviders.collectLatest { providers ->
|
withContext(Dispatchers.Default) {
|
||||||
if (providers.isEmpty()) {
|
badgeProviders.collectLatest { providers ->
|
||||||
send(null)
|
if (providers.isEmpty()) {
|
||||||
return@collectLatest
|
send(null)
|
||||||
}
|
return@collectLatest
|
||||||
combine(providers.map { it.getBadge(badgeKey) }) { badges ->
|
|
||||||
if (badges.all { it == null }) {
|
|
||||||
return@combine null
|
|
||||||
}
|
}
|
||||||
val badge = Badge()
|
combine(providers.map { it.getBadge(badgeKey) }) { badges ->
|
||||||
var progresses = 0
|
if (badges.all { it == null }) {
|
||||||
badges.filterNotNull().forEach {
|
return@combine null
|
||||||
if (it.icon != null && badge.icon == null) badge.icon = it.icon
|
}
|
||||||
if (it.iconRes != null && badge.iconRes == null) badge.iconRes = it.iconRes
|
val badge = Badge()
|
||||||
it.number?.let { a ->
|
var progresses = 0
|
||||||
badge.number?.let { b -> badge.number = a + b } ?: run {
|
badges.filterNotNull().forEach {
|
||||||
badge.number = a
|
if (it.icon != null && badge.icon == null) badge.icon = it.icon
|
||||||
|
if (it.iconRes != null && badge.iconRes == null) badge.iconRes = it.iconRes
|
||||||
|
it.number?.let { a ->
|
||||||
|
badge.number?.let { b -> badge.number = a + b } ?: run {
|
||||||
|
badge.number = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.progress?.let { a ->
|
||||||
|
badge.progress?.let { b ->
|
||||||
|
badge.progress = a + b
|
||||||
|
} ?: run {
|
||||||
|
badge.progress = a
|
||||||
|
}
|
||||||
|
progresses++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
it.progress?.let { a ->
|
if (progresses > 0) {
|
||||||
badge.progress?.let {
|
badge.progress?.let { badge.progress = it / progresses }
|
||||||
b -> badge.progress = a + b
|
|
||||||
} ?: run {
|
|
||||||
badge.progress = a
|
|
||||||
}
|
|
||||||
progresses++
|
|
||||||
}
|
}
|
||||||
|
return@combine badge
|
||||||
|
}.collectLatest {
|
||||||
|
send(it)
|
||||||
}
|
}
|
||||||
if (progresses > 0) {
|
|
||||||
badge.progress?.let { badge.progress = it / progresses }
|
|
||||||
}
|
|
||||||
return@combine badge
|
|
||||||
}.collectLatest {
|
|
||||||
send(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,6 @@
|
|||||||
<item name="colorOnSurfaceVariant">@android:color/system_neutral2_700</item>
|
<item name="colorOnSurfaceVariant">@android:color/system_neutral2_700</item>
|
||||||
<item name="colorSurfaceInverse">@android:color/system_neutral2_800</item>
|
<item name="colorSurfaceInverse">@android:color/system_neutral2_800</item>
|
||||||
<item name="colorOnSurfaceInverse">@android:color/system_neutral2_50</item>
|
<item name="colorOnSurfaceInverse">@android:color/system_neutral2_50</item>
|
||||||
<item name="elevationOverlayEnabled">false</item>
|
<item name="elevationOverlayEnabled">true</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@ -56,6 +56,6 @@
|
|||||||
<item name="colorOnSurfaceVariant">@android:color/black</item>
|
<item name="colorOnSurfaceVariant">@android:color/black</item>
|
||||||
<item name="colorSurfaceInverse">@android:color/black</item>
|
<item name="colorSurfaceInverse">@android:color/black</item>
|
||||||
<item name="colorOnSurfaceInverse">@android:color/white</item>
|
<item name="colorOnSurfaceInverse">@android:color/white</item>
|
||||||
<item name="elevationOverlayEnabled">false</item>
|
<item name="elevationOverlayEnabled">true</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package de.mm20.launcher2.search.data
|
package de.mm20.launcher2.search.data
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -10,7 +9,6 @@ import android.graphics.drawable.ColorDrawable
|
|||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.database.getStringOrNull
|
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.graphics.blue
|
import androidx.core.graphics.blue
|
||||||
import androidx.core.graphics.green
|
import androidx.core.graphics.green
|
||||||
@ -18,14 +16,8 @@ import androidx.core.graphics.red
|
|||||||
import de.mm20.launcher2.calendar.R
|
import de.mm20.launcher2.calendar.R
|
||||||
import de.mm20.launcher2.graphics.TextDrawable
|
import de.mm20.launcher2.graphics.TextDrawable
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.checkPermission
|
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.get
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class CalendarEvent(
|
class CalendarEvent(
|
||||||
override val label: String,
|
override val label: String,
|
||||||
@ -53,19 +45,19 @@ class CalendarEvent(
|
|||||||
TextDrawable(
|
TextDrawable(
|
||||||
day,
|
day,
|
||||||
color = Color.WHITE,
|
color = Color.WHITE,
|
||||||
fontSize = 40 * context.dp,
|
fontSize = 20 * context.dp,
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
),
|
),
|
||||||
TextDrawable(
|
TextDrawable(
|
||||||
month,
|
month,
|
||||||
color = Color.WHITE,
|
color = Color.WHITE,
|
||||||
fontSize = 26 * context.dp,
|
fontSize = 13 * context.dp,
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val foreground = LayerDrawable(fgLayers)
|
val foreground = LayerDrawable(fgLayers)
|
||||||
foreground.setLayerInset(0, 0, 0, 0, (26 * context.dp).toInt())
|
foreground.setLayerInset(0, 0, 0, 0, (13 * context.dp).toInt())
|
||||||
foreground.setLayerInset(1, 0, (40 * context.dp).toInt(), 0, 0)
|
foreground.setLayerInset(1, 0, (20 * context.dp).toInt(), 0, 0)
|
||||||
val background = ColorDrawable(getDisplayColor(context, color))
|
val background = ColorDrawable(getDisplayColor(context, color))
|
||||||
return LauncherIcon(
|
return LauncherIcon(
|
||||||
foreground = foreground,
|
foreground = foreground,
|
||||||
@ -74,8 +66,9 @@ class CalendarEvent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
override fun getLaunchIntent(context: Context): Intent {
|
||||||
return null
|
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
|
||||||
|
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
package de.mm20.launcher2.search.data
|
package de.mm20.launcher2.search.data
|
||||||
|
|
||||||
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.net.Uri
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
@ -14,14 +16,10 @@ import de.mm20.launcher2.graphics.TextDrawable
|
|||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.asBitmap
|
import de.mm20.launcher2.ktx.asBitmap
|
||||||
import de.mm20.launcher2.ktx.sp
|
import de.mm20.launcher2.ktx.sp
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
|
||||||
import de.mm20.launcher2.preferences.Settings
|
|
||||||
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
|
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import java.net.URLEncoder
|
||||||
import org.koin.core.component.get
|
|
||||||
|
|
||||||
class Contact(
|
class Contact(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@ -29,11 +27,11 @@ class Contact(
|
|||||||
val lastName: String,
|
val lastName: String,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val lookupKey: String,
|
val lookupKey: String,
|
||||||
val phones: Set<String>,
|
val phones: Set<ContactInfo>,
|
||||||
val emails: Set<String>,
|
val emails: Set<ContactInfo>,
|
||||||
val telegram: Set<String>,
|
val telegram: Set<ContactInfo>,
|
||||||
val whatsapp: Set<String>,
|
val whatsapp: Set<ContactInfo>,
|
||||||
val postals: Set<String>
|
val postals: Set<ContactInfo>,
|
||||||
) : Searchable() {
|
) : Searchable() {
|
||||||
override val key: String
|
override val key: String
|
||||||
get() = "contact://$id"
|
get() = "contact://$id"
|
||||||
@ -42,7 +40,7 @@ class Contact(
|
|||||||
|
|
||||||
val summary: String
|
val summary: String
|
||||||
get() {
|
get() {
|
||||||
return phones.union(emails).joinToString(separator = ", ")
|
return phones.union(emails).joinToString(separator = ", ") { it.label }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): LauncherIcon {
|
override fun getPlaceholderIcon(context: Context): LauncherIcon {
|
||||||
@ -52,17 +50,22 @@ class Contact(
|
|||||||
foreground = TextDrawable(
|
foreground = TextDrawable(
|
||||||
iconText,
|
iconText,
|
||||||
Color.WHITE,
|
Color.WHITE,
|
||||||
fontSize = 40 * context.sp,
|
fontSize = 20 * context.sp,
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
),
|
),
|
||||||
background = ColorDrawable(ContextCompat.getColor(context, R.color.blue))
|
background = ColorDrawable(ContextCompat.getColor(context, R.color.blue))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadIcon(context: Context, size: Int, legacyIconBackground: LegacyIconBackground): LauncherIcon? {
|
override suspend fun loadIcon(
|
||||||
|
context: Context,
|
||||||
|
size: Int,
|
||||||
|
legacyIconBackground: LegacyIconBackground
|
||||||
|
): LauncherIcon? {
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
val bmp = withContext(Dispatchers.IO) {
|
val bmp = withContext(Dispatchers.IO) {
|
||||||
val uri = ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return@withContext null
|
val uri =
|
||||||
|
ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return@withContext null
|
||||||
ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false)
|
ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false)
|
||||||
?.asBitmap()
|
?.asBitmap()
|
||||||
} ?: return null
|
} ?: return null
|
||||||
@ -73,10 +76,13 @@ class Contact(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
override fun getLaunchIntent(context: Context): Intent {
|
||||||
return null
|
val uri =
|
||||||
|
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id)
|
||||||
|
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
|
internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
|
||||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||||
@ -92,11 +98,11 @@ class Contact(
|
|||||||
ContactsContract.Data.CONTENT_URI,
|
ContactsContract.Data.CONTENT_URI,
|
||||||
null, s, null, null
|
null, s, null, null
|
||||||
) ?: return null
|
) ?: return null
|
||||||
val phones = mutableSetOf<String>()
|
val phones = mutableSetOf<ContactInfo>()
|
||||||
val emails = mutableSetOf<String>()
|
val emails = mutableSetOf<ContactInfo>()
|
||||||
val telegram = mutableSetOf<String>()
|
val telegram = mutableSetOf<ContactInfo>()
|
||||||
val whatsapp = mutableSetOf<String>()
|
val whatsapp = mutableSetOf<ContactInfo>()
|
||||||
val postals = mutableSetOf<String>()
|
val postals = mutableSetOf<ContactInfo>()
|
||||||
var firstName = ""
|
var firstName = ""
|
||||||
var lastName = ""
|
var lastName = ""
|
||||||
var displayName = ""
|
var displayName = ""
|
||||||
@ -119,13 +125,28 @@ class Contact(
|
|||||||
loop@ while (dataCursor.moveToNext()) {
|
loop@ while (dataCursor.moveToNext()) {
|
||||||
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
||||||
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
||||||
dataCursor.getStringOrNull(emailAddressColumn)?.let { emails.add(it) }
|
dataCursor.getStringOrNull(emailAddressColumn)?.let {
|
||||||
|
emails.add(ContactInfo(it, "mailto:$it"))
|
||||||
|
}
|
||||||
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
|
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
|
||||||
dataCursor.getStringOrNull(numberColumn)?.let {
|
dataCursor.getStringOrNull(numberColumn)?.let {
|
||||||
phones.add(it.replace(Regex("[^+0-9]"), ""))
|
val phone = it.replace(Regex("[^+0-9]"), "")
|
||||||
|
phones.add(
|
||||||
|
ContactInfo(
|
||||||
|
phone,
|
||||||
|
"tel:$phone"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ->
|
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ->
|
||||||
dataCursor.getStringOrNull(addressColumn)?.let { postals.add(it) }
|
dataCursor.getStringOrNull(addressColumn)?.let {
|
||||||
|
postals.add(
|
||||||
|
ContactInfo(
|
||||||
|
it.replace("\n", ", "),
|
||||||
|
"geo:0,0?q=${URLEncoder.encode(it, "utf8")}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||||
firstName = dataCursor.getStringOrNull(givenNameColumn) ?: ""
|
firstName = dataCursor.getStringOrNull(givenNameColumn) ?: ""
|
||||||
lastName = dataCursor.getStringOrNull(familyNameColumn) ?: ""
|
lastName = dataCursor.getStringOrNull(familyNameColumn) ?: ""
|
||||||
@ -136,13 +157,23 @@ class Contact(
|
|||||||
?: continue@loop
|
?: continue@loop
|
||||||
val data3 = dataCursor.getStringOrNull(data3Column)
|
val data3 = dataCursor.getStringOrNull(data3Column)
|
||||||
?: continue@loop
|
?: continue@loop
|
||||||
telegram.add("$data1$$data3")
|
telegram.add(
|
||||||
|
ContactInfo(data3.substringAfterLast(" "), "tg:openmessage?user_id=$data1")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"vnd.android.cursor.item/vnd.com.whatsapp.profile" -> {
|
"vnd.android.cursor.item/vnd.com.whatsapp.profile" -> {
|
||||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||||
?: continue@loop
|
?: continue@loop
|
||||||
val dataId = dataCursor.getLong(idColumn)
|
val dataId = dataCursor.getLong(idColumn)
|
||||||
whatsapp.add("$dataId$+${data1.substringBefore('@')}")
|
whatsapp.add(
|
||||||
|
ContactInfo(
|
||||||
|
"+${data1.substringBefore('@')}",
|
||||||
|
Uri.withAppendedPath(
|
||||||
|
ContactsContract.Data.CONTENT_URI,
|
||||||
|
dataId.toString()
|
||||||
|
).toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,3 +208,8 @@ class Contact(
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ContactInfo(
|
||||||
|
val label: String,
|
||||||
|
val data: String
|
||||||
|
)
|
||||||
@ -2,18 +2,17 @@ package de.mm20.launcher2.files
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.mm20.launcher2.files.providers.*
|
import de.mm20.launcher2.files.providers.*
|
||||||
import de.mm20.launcher2.files.providers.GDriveFileProvider
|
|
||||||
import de.mm20.launcher2.files.providers.LocalFileProvider
|
|
||||||
import de.mm20.launcher2.files.providers.NextcloudFileProvider
|
|
||||||
import de.mm20.launcher2.files.providers.OwncloudFileProvider
|
|
||||||
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
||||||
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
||||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.File
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
interface FileRepository {
|
interface FileRepository {
|
||||||
fun search(query: String): Flow<List<File>>
|
fun search(query: String): Flow<List<File>>
|
||||||
@ -70,28 +69,16 @@ internal class FileRepositoryImpl(
|
|||||||
return@channelFlow
|
return@channelFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO SearchListView crashes if we send too many updates at once. Rewrite this code
|
|
||||||
// once SearchListView has been replaced with a Jetpack Compose version of itself
|
|
||||||
providers.collectLatest { providers ->
|
providers.collectLatest { providers ->
|
||||||
if (providers.isEmpty()) {
|
if (providers.isEmpty()) {
|
||||||
send(emptyList())
|
send(emptyList())
|
||||||
return@collectLatest
|
return@collectLatest
|
||||||
}
|
}
|
||||||
hiddenItems.collectLatest { hiddenItems ->
|
hiddenItems.collectLatest { hiddenItems ->
|
||||||
if (providers.first() is LocalFileProvider) {
|
val results = mutableListOf<File>()
|
||||||
val localFiles = providers.first().takeIf { it is LocalFileProvider }?.search(query) ?: emptyList()
|
for (provider in providers) {
|
||||||
delay(300)
|
results.addAll(provider.search(query))
|
||||||
if (providers.size > 1) {
|
send(results)
|
||||||
val cloudFiles = providers.subList(1, providers.size).map {
|
|
||||||
async { it.search(query) }
|
|
||||||
}.awaitAll().flatten()
|
|
||||||
send(localFiles + cloudFiles)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val files = providers.map {
|
|
||||||
async { it.search(query) }
|
|
||||||
}.awaitAll().flatten()
|
|
||||||
send(files)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
<string name="menu_uninstall">Deinstallieren</string>
|
<string name="menu_uninstall">Deinstallieren</string>
|
||||||
<string name="menu_share">Teilen</string>
|
<string name="menu_share">Teilen</string>
|
||||||
<string name="app_info">Version %1$s\n%2$s</string>
|
<string name="app_info">Version %1$s\n%2$s</string>
|
||||||
|
<string name="app_info_version">Version %1$s</string>
|
||||||
<string name="settings">Einstellungen</string>
|
<string name="settings">Einstellungen</string>
|
||||||
<string name="wallpaper">Hintergrundbild</string>
|
<string name="wallpaper">Hintergrundbild</string>
|
||||||
<string name="title_activity_settings">Einstellungen</string>
|
<string name="title_activity_settings">Einstellungen</string>
|
||||||
@ -74,22 +75,22 @@
|
|||||||
<string name="file_type_text">Text-Datei</string>
|
<string name="file_type_text">Text-Datei</string>
|
||||||
<string name="alert_delete_directory">Das Verzeichnis %1$s wird samt Inhalt unwideruflich gelöscht. Fortfahren?</string>
|
<string name="alert_delete_directory">Das Verzeichnis %1$s wird samt Inhalt unwideruflich gelöscht. Fortfahren?</string>
|
||||||
<string name="alert_delete_file">Die Datei %1$s wird unwideruflich gelöscht. Fortfahren?</string>
|
<string name="alert_delete_file">Die Datei %1$s wird unwideruflich gelöscht. Fortfahren?</string>
|
||||||
<string name="file_meta_title">Titel</string>
|
<string name="file_meta_title">Titel: %1$s</string>
|
||||||
<string name="file_meta_artist">Künstler</string>
|
<string name="file_meta_artist">Künstler: %1$s</string>
|
||||||
<string name="file_meta_album">Album</string>
|
<string name="file_meta_album">Album: %1$s</string>
|
||||||
<string name="file_meta_duration">Länge</string>
|
<string name="file_meta_duration">Länge: %1$s</string>
|
||||||
<string name="file_meta_year">Jahr</string>
|
<string name="file_meta_year">Jahr: %1$s</string>
|
||||||
<string name="file_meta_data_entry">%1$s: %2$s</string>
|
<string name="file_meta_size">Größe: %1$s</string>
|
||||||
<string name="file_meta_size">Größe</string>
|
<string name="file_meta_path">Pfad: %1$s</string>
|
||||||
<string name="file_meta_path">Pfad</string>
|
<string name="file_meta_type">Typ: %1$s</string>
|
||||||
<string name="file_meta_type">Typ</string>
|
<string name="file_meta_dimensions">Abmessungen: %1$s</string>
|
||||||
<string name="file_meta_dimensions">Abmessungen</string>
|
<string name="file_meta_app_name">App-Name: %1$s</string>
|
||||||
<string name="file_meta_app_name">App-Name</string>
|
<string name="file_meta_app_version">Version: %1$s</string>
|
||||||
<string name="file_meta_app_version">Version</string>
|
<string name="file_meta_app_pkgname">Paketname: %1$s</string>
|
||||||
<string name="file_meta_app_pkgname">Paketname</string>
|
<string name="file_meta_app_min_sdk">Min-SDK-Version: %1$s</string>
|
||||||
<string name="file_meta_app_min_sdk">Min-SDK-Version</string>
|
<string name="file_meta_location">Ort: %1$s</string>
|
||||||
|
<string name="file_meta_owner">Eigentümer: %1$s</string>
|
||||||
<string name="preference_screen_services">Dienste</string>
|
<string name="preference_screen_services">Dienste</string>
|
||||||
<string name="file_meta_location">Ort</string>
|
|
||||||
<string name="websearch_google">Google</string>
|
<string name="websearch_google">Google</string>
|
||||||
<string name="websearch_youtube">YouTube</string>
|
<string name="websearch_youtube">YouTube</string>
|
||||||
<string name="websearch_playstore">Google Play</string>
|
<string name="websearch_playstore">Google Play</string>
|
||||||
@ -124,11 +125,12 @@
|
|||||||
<string name="contact_multiple_emails">%1$d E-Mail-Adressen</string>
|
<string name="contact_multiple_emails">%1$d E-Mail-Adressen</string>
|
||||||
<string name="contact_multiple_postals">%1$d Postadressen</string>
|
<string name="contact_multiple_postals">%1$d Postadressen</string>
|
||||||
<string name="menu_app_info">App-Info</string>
|
<string name="menu_app_info">App-Info</string>
|
||||||
|
<string name="menu_launch">Starten</string>
|
||||||
|
<string name="menu_open_file">Öffnen</string>
|
||||||
<string name="preference_screen_services_summary">Verbundene Accounts und Dienste verwalten</string>
|
<string name="preference_screen_services_summary">Verbundene Accounts und Dienste verwalten</string>
|
||||||
<string name="preference_category_services_google">Google</string>
|
<string name="preference_category_services_google">Google</string>
|
||||||
<string name="preference_signin_user">Angemeldet als %1$s</string>
|
<string name="preference_signin_user">Angemeldet als %1$s</string>
|
||||||
<string name="preference_signin_logout">Abmelden</string>
|
<string name="preference_signin_logout">Abmelden</string>
|
||||||
<string name="file_meta_owner">Eigentümer</string>
|
|
||||||
<string name="file_type_drawing">Zeichnung</string>
|
<string name="file_type_drawing">Zeichnung</string>
|
||||||
<string name="file_type_ebook">E-Book</string>
|
<string name="file_type_ebook">E-Book</string>
|
||||||
<string name="file_type_form">Formular</string>
|
<string name="file_type_form">Formular</string>
|
||||||
|
|||||||
@ -108,22 +108,22 @@
|
|||||||
<string name="file_type_text">Fichier texte</string>
|
<string name="file_type_text">Fichier texte</string>
|
||||||
<string name="alert_delete_directory">Le dossier %1$s et ce qu\'il contient vont être définitivement supprimés. Procéder ?</string>
|
<string name="alert_delete_directory">Le dossier %1$s et ce qu\'il contient vont être définitivement supprimés. Procéder ?</string>
|
||||||
<string name="alert_delete_file">Le fichier %1$s va être définitivement supprimé. Procéder ?</string>
|
<string name="alert_delete_file">Le fichier %1$s va être définitivement supprimé. Procéder ?</string>
|
||||||
<string name="file_meta_title">Titre</string>
|
<string name="file_meta_title">Titre: %1$s</string>
|
||||||
<string name="file_meta_album">Album</string>
|
<string name="file_meta_album">Album: %1$s</string>
|
||||||
<string name="file_meta_duration">Durée</string>
|
<string name="file_meta_duration">Durée: %1$s</string>
|
||||||
<string name="file_meta_year">Année</string>
|
<string name="file_meta_year">Année: %1$s</string>
|
||||||
<string name="file_meta_data_entry">%1$s: %2$s</string>
|
<string name="file_meta_size">Taille: %1$s</string>
|
||||||
<string name="file_meta_size">Taille</string>
|
<string name="file_meta_path">Chemin d\'accès: %1$s</string>
|
||||||
<string name="file_meta_path">Chemin d\'accès</string>
|
<string name="file_meta_type">Type: %1$s</string>
|
||||||
<string name="file_meta_type">Type</string>
|
<string name="file_meta_dimensions">Dimensions: %1$s</string>
|
||||||
<string name="file_meta_dimensions">Dimensions</string>
|
<string name="file_meta_app_name">Nom de l\'application: %1$s</string>
|
||||||
<string name="file_meta_app_name">Nom de l\'application</string>
|
<string name="file_meta_app_version">Version: %1$s</string>
|
||||||
<string name="file_meta_app_version">Version</string>
|
<string name="file_meta_app_pkgname">Nom du paquet: %1$s</string>
|
||||||
<string name="file_meta_app_pkgname">Nom du paquet</string>
|
<string name="file_meta_app_min_sdk">Version min du SDK: %1$s</string>
|
||||||
<string name="file_meta_app_min_sdk">Version min du SDK</string>
|
<string name="file_meta_location">Position: %1$s</string>
|
||||||
|
<string name="file_meta_owner">Propriétaire: %1$s</string>
|
||||||
<string name="preference_screen_services">Services</string>
|
<string name="preference_screen_services">Services</string>
|
||||||
<string name="preference_theme_system">Suivre le système</string>
|
<string name="preference_theme_system">Suivre le système</string>
|
||||||
<string name="file_meta_location">Position</string>
|
|
||||||
<string name="websearch_google">Google</string>
|
<string name="websearch_google">Google</string>
|
||||||
<string name="websearch_youtube">YouTube</string>
|
<string name="websearch_youtube">YouTube</string>
|
||||||
<string name="websearch_playstore">Google Play</string>
|
<string name="websearch_playstore">Google Play</string>
|
||||||
@ -166,7 +166,6 @@
|
|||||||
<string name="preference_category_services_google">Google</string>
|
<string name="preference_category_services_google">Google</string>
|
||||||
<string name="preference_signin_user">Connecté en tant que %1$s</string>
|
<string name="preference_signin_user">Connecté en tant que %1$s</string>
|
||||||
<string name="preference_signin_logout">Se déconnecter</string>
|
<string name="preference_signin_logout">Se déconnecter</string>
|
||||||
<string name="file_meta_owner">Propriétaire</string>
|
|
||||||
<string name="preference_summary_not_logged_in">Vous n\'êtes pas connecté</string>
|
<string name="preference_summary_not_logged_in">Vous n\'êtes pas connecté</string>
|
||||||
<string name="file_type_ebook">E-book</string>
|
<string name="file_type_ebook">E-book</string>
|
||||||
<string name="file_type_drawing">Dessin</string>
|
<string name="file_type_drawing">Dessin</string>
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
<string name="menu_share">Share</string>
|
<string name="menu_share">Share</string>
|
||||||
<!-- %1$s is the app version, %2$s the package name of an app -->
|
<!-- %1$s is the app version, %2$s the package name of an app -->
|
||||||
<string name="app_info">Version %1$s\n%2$s</string>
|
<string name="app_info">Version %1$s\n%2$s</string>
|
||||||
|
<string name="app_info_version">Version %1$s</string>
|
||||||
<!-- Menu entry for launcher settings -->
|
<!-- Menu entry for launcher settings -->
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<!-- Set the device's wallpaper -->
|
<!-- Set the device's wallpaper -->
|
||||||
@ -108,23 +109,23 @@
|
|||||||
<string name="file_type_text">Text file</string>
|
<string name="file_type_text">Text file</string>
|
||||||
<string name="alert_delete_directory">The directory %1$s and all its content will be deleted permanently. Proceed?</string>
|
<string name="alert_delete_directory">The directory %1$s and all its content will be deleted permanently. Proceed?</string>
|
||||||
<string name="alert_delete_file">The file %1$s will be deleted permanently. Proceed?</string>
|
<string name="alert_delete_file">The file %1$s will be deleted permanently. Proceed?</string>
|
||||||
<string name="file_meta_title">Title</string>
|
<string name="file_meta_title">Title: %1$s</string>
|
||||||
<string name="file_meta_artist">Artist</string>
|
<string name="file_meta_artist">Artist: %1$s</string>
|
||||||
<string name="file_meta_album">Album</string>
|
<string name="file_meta_album">Album: %1$s</string>
|
||||||
<string name="file_meta_duration">Duration</string>
|
<string name="file_meta_duration">Duration: %1$s</string>
|
||||||
<string name="file_meta_year">Year</string>
|
<string name="file_meta_year">Year: %1$s</string>
|
||||||
<string name="file_meta_data_entry">%1$s: %2$s</string>
|
<string name="file_meta_size">Size: %1$s</string>
|
||||||
<string name="file_meta_size">Size</string>
|
<string name="file_meta_path">Path: %1$s</string>
|
||||||
<string name="file_meta_path">Path</string>
|
<string name="file_meta_type">Type: %1$s</string>
|
||||||
<string name="file_meta_type">Type</string>
|
<string name="file_meta_dimensions">Dimensions: %1$s</string>
|
||||||
<string name="file_meta_dimensions">Dimensions</string>
|
<string name="file_meta_app_name">App name: %1$s</string>
|
||||||
<string name="file_meta_app_name">App name</string>
|
<string name="file_meta_app_version">Version: %1$s</string>
|
||||||
<string name="file_meta_app_version">Version</string>
|
<string name="file_meta_app_pkgname">Package name: %1$s</string>
|
||||||
<string name="file_meta_app_pkgname">Package name</string>
|
<string name="file_meta_app_min_sdk">Min SDK version: %1$s</string>
|
||||||
<string name="file_meta_app_min_sdk">Min SDK version</string>
|
<string name="file_meta_owner">Owner: %1$s</string>
|
||||||
|
<string name="file_meta_location">Location: %1$s</string>
|
||||||
<string name="preference_screen_services">Services</string>
|
<string name="preference_screen_services">Services</string>
|
||||||
<string name="preference_theme_system">Follow system</string>
|
<string name="preference_theme_system">Follow system</string>
|
||||||
<string name="file_meta_location">Location</string>
|
|
||||||
<string name="websearch_google">Google</string>
|
<string name="websearch_google">Google</string>
|
||||||
<string name="websearch_youtube">YouTube</string>
|
<string name="websearch_youtube">YouTube</string>
|
||||||
<string name="websearch_playstore">Google Play</string>
|
<string name="websearch_playstore">Google Play</string>
|
||||||
@ -163,11 +164,12 @@
|
|||||||
<string name="contact_multiple_emails">%1$d email addresses</string>
|
<string name="contact_multiple_emails">%1$d email addresses</string>
|
||||||
<string name="contact_multiple_postals">%1$d postal addresses</string>
|
<string name="contact_multiple_postals">%1$d postal addresses</string>
|
||||||
<string name="menu_app_info">App info</string>
|
<string name="menu_app_info">App info</string>
|
||||||
|
<string name="menu_launch">Launch</string>
|
||||||
|
<string name="menu_open_file">Open</string>
|
||||||
<string name="preference_screen_services_summary">Manage connected accounts and services</string>
|
<string name="preference_screen_services_summary">Manage connected accounts and services</string>
|
||||||
<string name="preference_category_services_google">Google</string>
|
<string name="preference_category_services_google">Google</string>
|
||||||
<string name="preference_signin_user">Signed in as %1$s</string>
|
<string name="preference_signin_user">Signed in as %1$s</string>
|
||||||
<string name="preference_signin_logout">Log out</string>
|
<string name="preference_signin_logout">Log out</string>
|
||||||
<string name="file_meta_owner">Owner</string>
|
|
||||||
<string name="preference_summary_not_logged_in">You are currently not logged in</string>
|
<string name="preference_summary_not_logged_in">You are currently not logged in</string>
|
||||||
<string name="file_type_ebook">E-book</string>
|
<string name="file_type_ebook">E-book</string>
|
||||||
<string name="file_type_drawing">Drawing</string>
|
<string name="file_type_drawing">Drawing</string>
|
||||||
|
|||||||
@ -8,8 +8,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".launcher.LauncherActivity"
|
android:name=".launcher.LauncherActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/LauncherTheme"
|
android:theme="@style/LauncherTheme"
|
||||||
android:windowSoftInputMode="stateHidden">
|
android:windowSoftInputMode="stateHidden">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -32,9 +32,9 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
|
android:exported="true"
|
||||||
android:label="@string/title_activity_settings"
|
android:label="@string/title_activity_settings"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true"
|
|
||||||
android:parentActivityName=".launcher.LauncherActivity"
|
android:parentActivityName=".launcher.LauncherActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:taskAffinity="de.mm20.launcher2.settings"
|
android:taskAffinity="de.mm20.launcher2.settings"
|
||||||
|
|||||||
56
ui/src/main/java/de/mm20/launcher2/ui/animation/TextStyle.kt
Normal file
56
ui/src/main/java/de/mm20/launcher2/ui/animation/TextStyle.kt
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package de.mm20.launcher2.ui.animation
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.animateInt
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.BaselineShift
|
||||||
|
import androidx.compose.ui.text.style.TextGeometricTransform
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun animateTextStyleAsState(
|
||||||
|
textStyle: TextStyle
|
||||||
|
): State<TextStyle> {
|
||||||
|
val state = remember { mutableStateOf(textStyle) }
|
||||||
|
|
||||||
|
val transition = updateTransition(textStyle, label = "animateTextStyleAsState")
|
||||||
|
|
||||||
|
val color by transition.animateColor(label = "color") {
|
||||||
|
it.color
|
||||||
|
}
|
||||||
|
val fontWeight by transition.animateInt(label = "fontWeight") {
|
||||||
|
it.fontWeight?.weight ?: 400
|
||||||
|
}
|
||||||
|
val fontSize by transition.animateTextUnit {
|
||||||
|
it.fontSize
|
||||||
|
}
|
||||||
|
val letterSpacing by transition.animateTextUnit {
|
||||||
|
it.letterSpacing
|
||||||
|
}
|
||||||
|
val baselineShift by transition.animateFloat(label = "baselineShift") {
|
||||||
|
it.baselineShift?.multiplier ?: 0f
|
||||||
|
}
|
||||||
|
val background by transition.animateColor(label = "background") {
|
||||||
|
it.background
|
||||||
|
}
|
||||||
|
val lineHeight by transition.animateTextUnit {
|
||||||
|
it.lineHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
state.value = textStyle.copy(
|
||||||
|
color = color,
|
||||||
|
fontSize = fontSize,
|
||||||
|
fontWeight = FontWeight(fontWeight),
|
||||||
|
letterSpacing = letterSpacing,
|
||||||
|
baselineShift = BaselineShift(baselineShift),
|
||||||
|
background = background,
|
||||||
|
lineHeight = lineHeight,
|
||||||
|
)
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
44
ui/src/main/java/de/mm20/launcher2/ui/animation/TextUnit.kt
Normal file
44
ui/src/main/java/de/mm20/launcher2/ui/animation/TextUnit.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package de.mm20.launcher2.ui.animation
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.ExperimentalUnitApi
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
|
|
||||||
|
object TextUnitConverter : TwoWayConverter<TextUnit, AnimationVector2D> {
|
||||||
|
@OptIn(ExperimentalUnitApi::class)
|
||||||
|
override val convertFromVector: (AnimationVector2D) -> TextUnit
|
||||||
|
get() = {
|
||||||
|
TextUnit(
|
||||||
|
it.v1,
|
||||||
|
if (it.v2 > 0.5f) TextUnitType.Em else TextUnitType.Sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override val convertToVector: (TextUnit) -> AnimationVector2D
|
||||||
|
get() = {
|
||||||
|
AnimationVector(
|
||||||
|
it.value,
|
||||||
|
if (it.isEm) 1f else 0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun <S> Transition<S>.animateTextUnit(
|
||||||
|
noinline transitionSpec:
|
||||||
|
@Composable Transition.Segment<S>.() -> FiniteAnimationSpec<TextUnit> = { spring() },
|
||||||
|
label: String = "ValueAnimation",
|
||||||
|
targetValueByState: @Composable (state: S) -> TextUnit
|
||||||
|
): State<TextUnit> {
|
||||||
|
return animateValue(
|
||||||
|
typeConverter = TextUnitConverter,
|
||||||
|
label = label,
|
||||||
|
transitionSpec = transitionSpec,
|
||||||
|
targetValueByState = targetValueByState
|
||||||
|
)
|
||||||
|
}
|
||||||
94
ui/src/main/java/de/mm20/launcher2/ui/component/Chip.kt
Normal file
94
ui/src/main/java/de/mm20/launcher2/ui/component/Chip.kt
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package de.mm20.launcher2.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Chip(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
content: @Composable RowScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(32.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalTextStyle provides MaterialTheme.typography.labelMedium,
|
||||||
|
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Chip(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
icon: Painter? = null,
|
||||||
|
rightIcon: ImageVector? = null,
|
||||||
|
rightAction: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Chip(
|
||||||
|
modifier = modifier.width(IntrinsicSize.Max),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
if (icon != null) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp).size(20.dp),
|
||||||
|
painter = icon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f, false)
|
||||||
|
.padding(
|
||||||
|
start = if (icon == null) 12.dp else 4.dp,
|
||||||
|
end = if (rightIcon == null) 12.dp else 4.dp,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
text = text)
|
||||||
|
if (rightIcon != null) {
|
||||||
|
if (rightAction != null) {
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
onClick = rightAction
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = rightIcon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp).size(20.dp),
|
||||||
|
imageVector = rightIcon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
ui/src/main/java/de/mm20/launcher2/ui/component/InnerCard.kt
Normal file
47
ui/src/main/java/de/mm20/launcher2/ui/component/InnerCard.kt
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package de.mm20.launcher2.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.animateDp
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InnerCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
raised: Boolean = false,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val transition = updateTransition(raised, label = "InnerCard")
|
||||||
|
|
||||||
|
val elevation by transition.animateDp(label = "elevation", transitionSpec = {
|
||||||
|
tween(250, if (targetState) 250 else 0)
|
||||||
|
}) {
|
||||||
|
if(it) 4.dp else 0.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
val borderWidth by transition.animateDp(label = "borderWidth", transitionSpec = { tween(500) }) {
|
||||||
|
if (it) 0.dp else 1.dp
|
||||||
|
}
|
||||||
|
val borderColor by transition.animateColor(label = "borderColor", transitionSpec = { tween(500) }) {
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (it) 0f else 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shadowElevation = elevation,
|
||||||
|
tonalElevation = elevation,
|
||||||
|
shape = RoundedCornerShape(LocalCardStyle.current.radius.dp),
|
||||||
|
border = BorderStroke(borderWidth, borderColor),
|
||||||
|
content = content,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,336 @@
|
|||||||
|
package de.mm20.launcher2.ui.component
|
||||||
|
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.GenericShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.badges.Badge
|
||||||
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
|
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ShapedLauncherIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: Dp,
|
||||||
|
icon: LauncherIcon? = null,
|
||||||
|
badge: Badge? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
shape: Shape = LocalIconShape.current
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
clip = true
|
||||||
|
this.shape = shape
|
||||||
|
}
|
||||||
|
.combinedClickable(
|
||||||
|
enabled = onClick != null || onLongClick != null,
|
||||||
|
onClick = {
|
||||||
|
onClick?.invoke()
|
||||||
|
},
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (icon != null) {
|
||||||
|
|
||||||
|
val fgScale = icon.foregroundScale
|
||||||
|
val bgScale = icon.backgroundScale
|
||||||
|
|
||||||
|
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val fg = icon.foreground
|
||||||
|
val bg = icon.background
|
||||||
|
drawIntoCanvas {
|
||||||
|
val paddingFg = (size * (1 - fgScale) * 0.5f).toPx()
|
||||||
|
val paddingBg = (size * (1 - bgScale) * 0.5f).toPx()
|
||||||
|
bg?.setBounds(
|
||||||
|
paddingBg.toInt(),
|
||||||
|
paddingBg.toInt(),
|
||||||
|
(this.size.width - paddingBg).toInt(),
|
||||||
|
(this.size.height - paddingBg).toInt()
|
||||||
|
)
|
||||||
|
bg?.draw(it.nativeCanvas)
|
||||||
|
fg.setBounds(
|
||||||
|
paddingFg.toInt(),
|
||||||
|
paddingFg.toInt(),
|
||||||
|
(this.size.width - paddingFg).toInt(),
|
||||||
|
(this.size.height - paddingFg).toInt()
|
||||||
|
)
|
||||||
|
fg.draw(it.nativeCanvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (badge != null) {
|
||||||
|
Surface(
|
||||||
|
shadowElevation = 1.dp,
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size * 0.33f)
|
||||||
|
.align(Alignment.BottomEnd),
|
||||||
|
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
shape = CircleShape
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val badgeIconRes = badge.iconRes
|
||||||
|
val badgeIcon = badge.icon
|
||||||
|
|
||||||
|
val number = badge.number
|
||||||
|
if (badgeIconRes != null) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
painter = painterResource(badgeIconRes),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
} else if (badgeIcon != null) {
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
badgeIcon.setBounds(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.size.width.roundToInt(),
|
||||||
|
this.size.height.roundToInt()
|
||||||
|
)
|
||||||
|
drawIntoCanvas {
|
||||||
|
badgeIcon.draw(it.nativeCanvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (number != null && number > 0 && number < 100) {
|
||||||
|
Text(
|
||||||
|
number.toString(),
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(
|
||||||
|
fontSize = with(LocalDensity.current) {
|
||||||
|
size.toSp() * 0.2f
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalIconShape = compositionLocalOf<Shape> { CircleShape }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProvideIconShape(iconShape: IconShape, content: @Composable () -> Unit) {
|
||||||
|
val shape = when (iconShape) {
|
||||||
|
IconShape.PlatformDefault -> PlatformShape
|
||||||
|
IconShape.Circle -> CircleShape
|
||||||
|
IconShape.Square -> RectangleShape
|
||||||
|
IconShape.RoundedSquare -> RoundedCornerShape(25)
|
||||||
|
IconShape.Triangle -> TriangleShape
|
||||||
|
IconShape.Squircle -> SquircleShape
|
||||||
|
IconShape.Hexagon -> HexagonShape
|
||||||
|
IconShape.Pentagon -> PentagonShape
|
||||||
|
IconShape.EasterEgg -> EasterEggShape
|
||||||
|
IconShape.UNRECOGNIZED -> CircleShape
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalIconShape provides shape,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TriangleShape: Shape
|
||||||
|
get() = GenericShape { size, _ ->
|
||||||
|
var cx = 0f
|
||||||
|
var cy = size.height * 0.86f
|
||||||
|
val r = size.width
|
||||||
|
arcTo(Rect(cx - r, cy - r, cx + r, cy + r), 300f, 60f, false)
|
||||||
|
cx = size.width
|
||||||
|
cy = size.height * 0.86f
|
||||||
|
arcTo(Rect(cx - r, cy - r, cx + r, cy + r), 180f, 60f, false)
|
||||||
|
cx = size.width * 0.5f
|
||||||
|
cy = 0f
|
||||||
|
arcTo(Rect(cx - r, cy - r, cx + r, cy + r), 60f, 60f, false)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SquircleShape: Shape
|
||||||
|
get() = GenericShape { size, _ ->
|
||||||
|
val radius = size.width / 2f
|
||||||
|
val radiusToPow = radius.pow(3f).toDouble()
|
||||||
|
moveTo(-radius, 0f)
|
||||||
|
for (x in -radius.roundToInt()..radius.roundToInt())
|
||||||
|
lineTo(
|
||||||
|
x.toFloat(),
|
||||||
|
Math.cbrt(radiusToPow - Math.abs(x * x * x)).toFloat()
|
||||||
|
)
|
||||||
|
for (x in radius.roundToInt() downTo -radius.roundToInt())
|
||||||
|
lineTo(
|
||||||
|
x.toFloat(),
|
||||||
|
(-Math.cbrt(radiusToPow - Math.abs(x * x * x))).toFloat()
|
||||||
|
)
|
||||||
|
translate(Offset(size.width / 2f, size.height / 2f))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val HexagonShape: Shape
|
||||||
|
get() = GenericShape { size, _ ->
|
||||||
|
moveTo(
|
||||||
|
size.width * 0.25f,
|
||||||
|
size.height * 0.933f
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
size.width * 0.75f,
|
||||||
|
size.height * 0.933f
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
size.width * 1.0f,
|
||||||
|
size.height * 0.5f
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
size.width * 0.75f,
|
||||||
|
size.height * 0.067f
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
size.width * 0.25f,
|
||||||
|
size.height * 0.067f
|
||||||
|
)
|
||||||
|
lineTo(0f, size.height * 0.5f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val PentagonShape: Shape
|
||||||
|
get() = GenericShape { size, _ ->
|
||||||
|
moveTo(
|
||||||
|
0.49997027f * size.width,
|
||||||
|
0.0060308f * size.height
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
0.99994053f * size.width,
|
||||||
|
0.36928048f * size.height
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
0.80896887f * size.width,
|
||||||
|
0.95703078f * size.height
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
0.19097162f * size.width,
|
||||||
|
0.95703076f * size.height
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
0f,
|
||||||
|
0.36928045f * size.height
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val EasterEggShape: Shape
|
||||||
|
get() = GenericShape { size, _ ->
|
||||||
|
moveTo(
|
||||||
|
0.49999999f * size.width,
|
||||||
|
1f * size.height
|
||||||
|
)
|
||||||
|
lineTo(
|
||||||
|
0.42749999f * size.width,
|
||||||
|
0.9339999999999999f * size.height
|
||||||
|
)
|
||||||
|
cubicTo(
|
||||||
|
0.16999998f * size.width,
|
||||||
|
0.7005004f * size.height,
|
||||||
|
0f,
|
||||||
|
0.5460004f * size.height,
|
||||||
|
0f,
|
||||||
|
0.3575003f * size.height
|
||||||
|
)
|
||||||
|
cubicTo(
|
||||||
|
0f,
|
||||||
|
0.2030004f * size.height,
|
||||||
|
0.12100002f * size.width,
|
||||||
|
0.0825004f * size.height,
|
||||||
|
0.275f * size.width,
|
||||||
|
0.0825004f * size.height
|
||||||
|
)
|
||||||
|
cubicTo(
|
||||||
|
0.362f * size.width,
|
||||||
|
0.0825004f * size.height,
|
||||||
|
0.4455f * size.width,
|
||||||
|
0.123f * size.height,
|
||||||
|
0.5f * size.width,
|
||||||
|
0.1865003f * size.height
|
||||||
|
)
|
||||||
|
cubicTo(
|
||||||
|
0.55449999f * size.width,
|
||||||
|
0.123f * size.height,
|
||||||
|
0.638f * size.width,
|
||||||
|
0.0825f * size.height,
|
||||||
|
0.725f * size.width,
|
||||||
|
0.0825f * size.height
|
||||||
|
)
|
||||||
|
cubicTo(
|
||||||
|
0.87900006f * size.width,
|
||||||
|
0.0825004f * size.height,
|
||||||
|
1f * size.width,
|
||||||
|
0.2030004f * size.height,
|
||||||
|
1f * size.width,
|
||||||
|
0.3575003f * size.height
|
||||||
|
)
|
||||||
|
cubicTo(
|
||||||
|
1f * size.width,
|
||||||
|
0.5460004f * size.height,
|
||||||
|
0.82999999f * size.width,
|
||||||
|
0.7005004f * size.height,
|
||||||
|
0.57250001f * size.width,
|
||||||
|
0.9340004f * size.height
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val PlatformShape: Shape
|
||||||
|
get() {
|
||||||
|
val drawable = AdaptiveIconDrawable(null, null)
|
||||||
|
|
||||||
|
val pathBounds = RectF()
|
||||||
|
drawable.iconMask.computeBounds(pathBounds, true)
|
||||||
|
|
||||||
|
return GenericShape { size, _ ->
|
||||||
|
val path = Path(drawable.iconMask)
|
||||||
|
val transformMatrix = Matrix()
|
||||||
|
transformMatrix.setScale(
|
||||||
|
size.width / pathBounds.width(),
|
||||||
|
size.height / pathBounds.height()
|
||||||
|
)
|
||||||
|
path.transform(transformMatrix)
|
||||||
|
addPath(path.asComposePath())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,8 +9,11 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.integerResource
|
import androidx.compose.ui.res.integerResource
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalWindowPosition
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -39,13 +42,19 @@ fun Icons(actions: List<ToolbarAction>, slots: Int) {
|
|||||||
IconButton(onClick = { showMenu = true }) {
|
IconButton(onClick = { showMenu = true }) {
|
||||||
Icon(Icons.Rounded.MoreVert, contentDescription = "")
|
Icon(Icons.Rounded.MoreVert, contentDescription = "")
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
Box(
|
||||||
expanded = showMenu,
|
modifier = Modifier
|
||||||
onDismissRequest = { showMenu = false },
|
.size(48.dp)
|
||||||
modifier = Modifier.animateContentSize()
|
.offset(0.dp, LocalWindowPosition.current.toDp())
|
||||||
) {
|
) {
|
||||||
OverflowMenuItems(items = actions.subList(slots - 1, actions.size)) {
|
DropdownMenu(
|
||||||
showMenu = false
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false },
|
||||||
|
modifier = Modifier.animateContentSize()
|
||||||
|
) {
|
||||||
|
OverflowMenuItems(items = actions.subList(slots - 1, actions.size)) {
|
||||||
|
showMenu = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,13 +74,19 @@ fun Icons(actions: List<ToolbarAction>, slots: Int) {
|
|||||||
}) {
|
}) {
|
||||||
Icon(action.icon, contentDescription = action.label)
|
Icon(action.icon, contentDescription = action.label)
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
Box(
|
||||||
expanded = showMenu,
|
modifier = Modifier
|
||||||
onDismissRequest = { showMenu = false },
|
.size(48.dp)
|
||||||
modifier = Modifier.animateContentSize()
|
.offset(0.dp, LocalWindowPosition.current.toDp())
|
||||||
) {
|
) {
|
||||||
OverflowMenuItems(items = action.children) {
|
DropdownMenu(
|
||||||
showMenu = false
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false },
|
||||||
|
modifier = Modifier.animateContentSize()
|
||||||
|
) {
|
||||||
|
OverflowMenuItems(items = action.children) {
|
||||||
|
showMenu = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -807,3 +807,53 @@ val Icons.Rounded.Wikipedia
|
|||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Icons.Rounded.WhatsApp
|
||||||
|
get() = materialIcon("Icons.Rounded.WhatsApp") {
|
||||||
|
materialPath {
|
||||||
|
moveTo(12.04f, 2f)
|
||||||
|
curveTo(6.58f, 2f, 2.13f, 6.45f, 2.13f, 11.91f)
|
||||||
|
curveTo(2.13f, 13.66f, 2.59f, 15.36f, 3.45f, 16.86f)
|
||||||
|
lineTo(2.05f, 22f)
|
||||||
|
lineTo(7.3f, 20.62f)
|
||||||
|
curveTo(8.75f, 21.41f, 10.38f, 21.83f, 12.04f, 21.83f)
|
||||||
|
curveTo(17.5f, 21.83f, 21.95f, 17.38f, 21.95f, 11.92f)
|
||||||
|
curveTo(21.95f, 9.27f, 20.92f, 6.78f, 19.05f, 4.91f)
|
||||||
|
curveTo(17.18f, 3.03f, 14.69f, 2f, 12.04f, 2f)
|
||||||
|
moveTo(12.05f, 3.67f)
|
||||||
|
curveTo(14.25f, 3.67f, 16.31f, 4.53f, 17.87f, 6.09f)
|
||||||
|
curveTo(19.42f, 7.65f, 20.28f, 9.72f, 20.28f, 11.92f)
|
||||||
|
curveTo(20.28f, 16.46f, 16.58f, 20.15f, 12.04f, 20.15f)
|
||||||
|
curveTo(10.56f, 20.15f, 9.11f, 19.76f, 7.85f, 19f)
|
||||||
|
lineTo(7.55f, 18.83f)
|
||||||
|
lineTo(4.43f, 19.65f)
|
||||||
|
lineTo(5.26f, 16.61f)
|
||||||
|
lineTo(5.06f, 16.29f)
|
||||||
|
curveTo(4.24f, 15f, 3.8f, 13.47f, 3.8f, 11.91f)
|
||||||
|
curveTo(3.81f, 7.37f, 7.5f, 3.67f, 12.05f, 3.67f)
|
||||||
|
moveTo(8.53f, 7.33f)
|
||||||
|
curveTo(8.37f, 7.33f, 8.1f, 7.39f, 7.87f, 7.64f)
|
||||||
|
curveTo(7.65f, 7.89f, 7f, 8.5f, 7f, 9.71f)
|
||||||
|
curveTo(7f, 10.93f, 7.89f, 12.1f, 8f, 12.27f)
|
||||||
|
curveTo(8.14f, 12.44f, 9.76f, 14.94f, 12.25f, 16f)
|
||||||
|
curveTo(12.84f, 16.27f, 13.3f, 16.42f, 13.66f, 16.53f)
|
||||||
|
curveTo(14.25f, 16.72f, 14.79f, 16.69f, 15.22f, 16.63f)
|
||||||
|
curveTo(15.7f, 16.56f, 16.68f, 16.03f, 16.89f, 15.45f)
|
||||||
|
curveTo(17.1f, 14.87f, 17.1f, 14.38f, 17.04f, 14.27f)
|
||||||
|
curveTo(16.97f, 14.17f, 16.81f, 14.11f, 16.56f, 14f)
|
||||||
|
curveTo(16.31f, 13.86f, 15.09f, 13.26f, 14.87f, 13.18f)
|
||||||
|
curveTo(14.64f, 13.1f, 14.5f, 13.06f, 14.31f, 13.3f)
|
||||||
|
curveTo(14.15f, 13.55f, 13.67f, 14.11f, 13.53f, 14.27f)
|
||||||
|
curveTo(13.38f, 14.44f, 13.24f, 14.46f, 13f, 14.34f)
|
||||||
|
curveTo(12.74f, 14.21f, 11.94f, 13.95f, 11f, 13.11f)
|
||||||
|
curveTo(10.26f, 12.45f, 9.77f, 11.64f, 9.62f, 11.39f)
|
||||||
|
curveTo(9.5f, 11.15f, 9.61f, 11f, 9.73f, 10.89f)
|
||||||
|
curveTo(9.84f, 10.78f, 10f, 10.6f, 10.1f, 10.45f)
|
||||||
|
curveTo(10.23f, 10.31f, 10.27f, 10.2f, 10.35f, 10.04f)
|
||||||
|
curveTo(10.43f, 9.87f, 10.39f, 9.73f, 10.33f, 9.61f)
|
||||||
|
curveTo(10.27f, 9.5f, 9.77f, 8.26f, 9.56f, 7.77f)
|
||||||
|
curveTo(9.36f, 7.29f, 9.16f, 7.35f, 9f, 7.34f)
|
||||||
|
curveTo(8.86f, 7.34f, 8.7f, 7.33f, 8.53f, 7.33f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -108,8 +108,6 @@ class LauncherScaffoldView @JvmOverloads constructor(
|
|||||||
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0).setDuration(200).start()
|
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0).setDuration(200).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
|
||||||
|
|
||||||
binding.scrollView.scrollY = viewModel.scrollY
|
binding.scrollView.scrollY = viewModel.scrollY
|
||||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
||||||
|
|
||||||
@ -173,7 +171,6 @@ class LauncherScaffoldView @JvmOverloads constructor(
|
|||||||
viewModel.setStatusBarColor(colorSurface.data)
|
viewModel.setStatusBarColor(colorSurface.data)
|
||||||
} else {
|
} else {
|
||||||
binding.widgetContainer.layoutTransition = ChangingLayoutTransition()
|
binding.widgetContainer.layoutTransition = ChangingLayoutTransition()
|
||||||
binding.scrollContainer.layoutTransition = ChangingLayoutTransition()
|
|
||||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
||||||
|
|
||||||
binding.searchBar.visibility = View.VISIBLE
|
binding.searchBar.visibility = View.VISIBLE
|
||||||
|
|||||||
@ -1,20 +1,92 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search
|
package de.mm20.launcher2.ui.launcher.search
|
||||||
|
|
||||||
import android.animation.LayoutTransition
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
import androidx.compose.foundation.layout.*
|
||||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import de.mm20.launcher2.ui.databinding.ViewSearchBinding
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
import de.mm20.launcher2.ui.MdcLauncherTheme
|
||||||
|
import de.mm20.launcher2.ui.component.ProvideIconShape
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.apps.AppResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.contacts.ContactResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.favorites.FavoritesResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.files.FileResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.website.WebsiteResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaResults
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class SearchView @JvmOverloads constructor(
|
class SearchView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null
|
context: Context, attrs: AttributeSet? = null
|
||||||
) : LinearLayout(context, attrs) {
|
) : FrameLayout(context, attrs), KoinComponent {
|
||||||
val binding = ViewSearchBinding.inflate(LayoutInflater.from(context), this)
|
|
||||||
|
private val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
orientation = VERTICAL
|
val view = ComposeView(context)
|
||||||
layoutTransition = ChangingLayoutTransition()
|
view.layoutParams = LayoutParams(
|
||||||
|
LayoutParams.MATCH_PARENT,
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
view.setContent {
|
||||||
|
MdcLauncherTheme {
|
||||||
|
val cardStyle by remember {
|
||||||
|
dataStore.data.map { it.cards }.distinctUntilChanged()
|
||||||
|
}.collectAsState(
|
||||||
|
Settings.CardSettings.getDefaultInstance()
|
||||||
|
)
|
||||||
|
val iconShape by remember {
|
||||||
|
dataStore.data.map {
|
||||||
|
if (it.easterEgg) Settings.IconSettings.IconShape.EasterEgg
|
||||||
|
else it.icons.shape
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
}.collectAsState(Settings.IconSettings.IconShape.Circle)
|
||||||
|
|
||||||
|
val favoritesEnabled by remember {
|
||||||
|
dataStore.data.map { it.favorites.enabled }.distinctUntilChanged()
|
||||||
|
}.collectAsState(true)
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalCardStyle provides cardStyle,
|
||||||
|
LocalFavoritesEnabled provides favoritesEnabled
|
||||||
|
) {
|
||||||
|
ProvideIconShape(iconShape) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
FavoritesResults()
|
||||||
|
AppResults()
|
||||||
|
UnitConverterResults()
|
||||||
|
CalculatorResults()
|
||||||
|
CalendarResults()
|
||||||
|
ContactResults()
|
||||||
|
FileResults()
|
||||||
|
WikipediaResults()
|
||||||
|
WebsiteResults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addView(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,341 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.apps
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
|
import de.mm20.launcher2.search.data.Application
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.*
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
import de.mm20.launcher2.ui.modifier.scale
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AppItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
app: Application,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val viewModel = remember { AppItemVM(app) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = app.label, style = MaterialTheme.typography.titleMedium)
|
||||||
|
app.version?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_info_version, it),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = app.`package`,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.animateContentSize(),
|
||||||
|
mainAxisSpacing = 16.dp,
|
||||||
|
crossAxisSpacing = 8.dp
|
||||||
|
) {
|
||||||
|
val notifications by viewModel.notifications.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
|
for (not in notifications) {
|
||||||
|
val title =
|
||||||
|
not.notification.extras.getString(NotificationCompat.EXTRA_TITLE, null)
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: not.notification.extras.getString(
|
||||||
|
NotificationCompat.EXTRA_TEXT,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: continue
|
||||||
|
|
||||||
|
val icon =
|
||||||
|
remember { not.notification.smallIcon?.loadDrawable(context) }?.let {
|
||||||
|
rememberImagePainter(
|
||||||
|
it,
|
||||||
|
builder = {
|
||||||
|
crossfade(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip(
|
||||||
|
text = title,
|
||||||
|
icon = icon,
|
||||||
|
rightIcon = Icons.Rounded.Clear,
|
||||||
|
rightAction = {
|
||||||
|
viewModel.clearNotification(not)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.openNotification(not)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (shortcut in app.shortcuts.subList(0, min(app.shortcuts.size, 5))) {
|
||||||
|
val title =
|
||||||
|
shortcut.launcherShortcut.shortLabel
|
||||||
|
?: shortcut.launcherShortcut.longLabel
|
||||||
|
?: continue
|
||||||
|
val isPinned by remember(shortcut) { viewModel.isShortcutPinned(shortcut) }.collectAsState(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
val icon =
|
||||||
|
remember {
|
||||||
|
viewModel.getShortcutIcon(
|
||||||
|
context,
|
||||||
|
shortcut.launcherShortcut
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?.let {
|
||||||
|
rememberImagePainter(it,
|
||||||
|
builder = {
|
||||||
|
crossfade(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip(
|
||||||
|
text = title.toString(),
|
||||||
|
icon = icon,
|
||||||
|
rightIcon = if (LocalFavoritesEnabled.current) {
|
||||||
|
if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline
|
||||||
|
} else null,
|
||||||
|
rightAction = {
|
||||||
|
if (isPinned) {
|
||||||
|
viewModel.unpinShortcut(shortcut)
|
||||||
|
} else {
|
||||||
|
viewModel.pinShortcut(shortcut)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.launchShortcut(context, shortcut)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val badge by viewModel.badge.collectAsState(null)
|
||||||
|
val iconSize = 84.dp.toPixels().toInt()
|
||||||
|
val icon by remember(app) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
||||||
|
ShapedLauncherIcon(
|
||||||
|
size = 84.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp),
|
||||||
|
badge = badge,
|
||||||
|
icon = icon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||||
|
|
||||||
|
if (LocalFavoritesEnabled.current) {
|
||||||
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
|
val favAction = if (isPinned) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_unpin),
|
||||||
|
icon = Icons.Rounded.Star,
|
||||||
|
action = {
|
||||||
|
viewModel.unpin()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_pin),
|
||||||
|
icon = Icons.Rounded.StarOutline,
|
||||||
|
action = {
|
||||||
|
viewModel.pin()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(favAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_app_info),
|
||||||
|
icon = Icons.Rounded.Info
|
||||||
|
) {
|
||||||
|
viewModel.openAppInfo(context as AppCompatActivity)
|
||||||
|
})
|
||||||
|
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_launch),
|
||||||
|
icon = Icons.Rounded.OpenInNew,
|
||||||
|
action = {
|
||||||
|
viewModel.launch(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val storeDetails = remember(app) { app.getStoreDetails(context) }
|
||||||
|
val shareAction = if (storeDetails == null) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_share),
|
||||||
|
icon = Icons.Rounded.Share
|
||||||
|
) {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.shareApkFile(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SubmenuToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_share),
|
||||||
|
icon = Icons.Rounded.Share,
|
||||||
|
children = listOf(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.share_menu_store_link, storeDetails.label),
|
||||||
|
icon = Icons.Rounded.Share,
|
||||||
|
action = {
|
||||||
|
viewModel.shareStoreLink(context as AppCompatActivity, storeDetails.url)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.share_menu_apk_file),
|
||||||
|
icon = Icons.Rounded.Share
|
||||||
|
) {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.shareApkFile(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
toolbarActions.add(shareAction)
|
||||||
|
|
||||||
|
if (viewModel.canUninstall) {
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_uninstall),
|
||||||
|
icon = Icons.Rounded.Delete,
|
||||||
|
) {
|
||||||
|
viewModel.uninstall(context as AppCompatActivity)
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||||
|
val hideAction = if (isHidden) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_unhide),
|
||||||
|
icon = Icons.Rounded.Visibility,
|
||||||
|
action = {
|
||||||
|
viewModel.unhide()
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_hide),
|
||||||
|
icon = Icons.Rounded.VisibilityOff,
|
||||||
|
action = {
|
||||||
|
viewModel.hide()
|
||||||
|
onBack()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarActions.add(hideAction)
|
||||||
|
|
||||||
|
Toolbar(
|
||||||
|
leftActions = listOf(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(id = R.string.menu_back),
|
||||||
|
icon = Icons.Rounded.ArrowBack
|
||||||
|
) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
rightActions = toolbarActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AppItemGridPopup(
|
||||||
|
app: Application,
|
||||||
|
show: Boolean,
|
||||||
|
animationProgress: Float,
|
||||||
|
origin: Rect,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = show,
|
||||||
|
transitionSpec = {
|
||||||
|
slideInHorizontally(
|
||||||
|
tween(300),
|
||||||
|
initialOffsetX = { -it + origin.width.roundToInt() }) with
|
||||||
|
slideOutHorizontally(
|
||||||
|
tween(300),
|
||||||
|
targetOffsetX = { -it + origin.width.roundToInt() }) + fadeOut(snap(400)) using
|
||||||
|
SizeTransform { _, _ ->
|
||||||
|
tween(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { targetState ->
|
||||||
|
if (targetState) {
|
||||||
|
AppItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.scale(
|
||||||
|
1 - (1 - 48.dp / 84.dp) * (1 - animationProgress),
|
||||||
|
transformOrigin = TransformOrigin(1f, 0f)
|
||||||
|
)
|
||||||
|
.offset(
|
||||||
|
x = 16.dp * (1 - animationProgress).pow(10),
|
||||||
|
y = -16.dp * (1 - animationProgress),
|
||||||
|
),
|
||||||
|
app = app,
|
||||||
|
onBack = onDismiss
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(origin.width.toDp())
|
||||||
|
.requiredHeight(origin.height.toDp())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.apps
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.*
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import de.mm20.launcher2.badges.BadgeRepository
|
||||||
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.notifications.NotificationRepository
|
||||||
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
|
import de.mm20.launcher2.search.data.Application
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class AppItemVM(
|
||||||
|
private val app: Application
|
||||||
|
) : SearchableItemVM(app) {
|
||||||
|
private val notificationRepository: NotificationRepository by inject()
|
||||||
|
|
||||||
|
|
||||||
|
val notifications =
|
||||||
|
notificationRepository.notifications.map { it.filter { it.packageName == app.`package` } }
|
||||||
|
|
||||||
|
fun clearNotification(notification: StatusBarNotification) {
|
||||||
|
notificationRepository.cancelNotification(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openAppInfo(activity: AppCompatActivity) {
|
||||||
|
activity.tryStartActivity(
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.parse("package:${app.`package`}")
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun shareApkFile(activity: AppCompatActivity) {
|
||||||
|
val fileCopy = java.io.File(
|
||||||
|
activity.cacheDir,
|
||||||
|
"${app.`package`}-${app.version}.apk"
|
||||||
|
)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val info = activity.packageManager
|
||||||
|
.getApplicationInfo(app.`package`, 0)
|
||||||
|
val file = java.io.File(info.publicSourceDir)
|
||||||
|
|
||||||
|
try {
|
||||||
|
file.copyTo(fileCopy, false)
|
||||||
|
} catch (e: FileAlreadyExistsException) {
|
||||||
|
// Do nothing. If the file is already there we don't have to copy it again.
|
||||||
|
}
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
CrashReporter.logException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||||
|
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
activity,
|
||||||
|
activity.applicationContext.packageName + ".fileprovider",
|
||||||
|
fileCopy
|
||||||
|
)
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
shareIntent.type = "application/vnd.android.package-archive"
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
activity.startActivity(Intent.createChooser(shareIntent, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shareStoreLink(activity: AppCompatActivity, url: String) {
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_TEXT, url)
|
||||||
|
shareIntent.type = "text/plain"
|
||||||
|
activity.startActivity(Intent.createChooser(shareIntent, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
val canUninstall = app.flags and ApplicationInfo.FLAG_SYSTEM == 0
|
||||||
|
|
||||||
|
fun uninstall(activity: AppCompatActivity) {
|
||||||
|
val intent = Intent(Intent.ACTION_DELETE)
|
||||||
|
intent.data = Uri.parse("package:" + app.`package`)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun openNotification(notification: StatusBarNotification) {
|
||||||
|
try {
|
||||||
|
notification.notification.contentIntent.send()
|
||||||
|
} catch (e: PendingIntent.CanceledException) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getShortcutIcon(context: Context, shortcut: ShortcutInfo) : Drawable? {
|
||||||
|
val launcherApps = context.getSystemService<LauncherApps>() ?: return null
|
||||||
|
return launcherApps.getShortcutIconDrawable(shortcut, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
|
||||||
|
return favoritesRepository.isPinned(shortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pinShortcut(shortcut: AppShortcut) {
|
||||||
|
favoritesRepository.pinItem(shortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unpinShortcut(shortcut: AppShortcut) {
|
||||||
|
favoritesRepository.unpinItem(shortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchShortcut(context: Context, shortcut: AppShortcut) {
|
||||||
|
shortcut.launch(context, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.apps
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchResultGrid
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.AppResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val apps by viewModel.appResults.observeAsState(emptyList())
|
||||||
|
|
||||||
|
AnimatedVisibility(apps.isNotEmpty()) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
SearchResultGrid(items = apps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.calculator
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.search.CalculatorItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.CalculatorResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val calculator by viewModel.calculatorResult.observeAsState(null)
|
||||||
|
|
||||||
|
AnimatedVisibility(calculator != null) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
calculator?.let { CalculatorItem(calculator = it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,260 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.calendar
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.*
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||||
|
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||||
|
import de.mm20.launcher2.ui.component.Toolbar
|
||||||
|
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CalendarItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
calendar: CalendarEvent,
|
||||||
|
showDetails: Boolean,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewModel = remember(calendar.key) { CalendarItemVM(calendar) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(Color(calendar.color), Offset.Zero, this.size.copy(width = 8.dp.toPx()))
|
||||||
|
}
|
||||||
|
.padding(start = 8.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
val padding by animateDpAsState(if (showDetails) 16.dp else 12.dp)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
top = padding,
|
||||||
|
start = padding,
|
||||||
|
bottom = 12.dp,
|
||||||
|
end = padding
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val textStyle by animateTextStyleAsState(
|
||||||
|
if (showDetails) MaterialTheme.typography.titleMedium
|
||||||
|
else MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
Text(text = calendar.label, style = textStyle)
|
||||||
|
AnimatedVisibility(!showDetails) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.getSummary(context),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimatedVisibility(showDetails) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||||
|
imageVector = Icons.Rounded.Schedule,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = viewModel.formatTime(context),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (calendar.description.isNotBlank()) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||||
|
imageVector = Icons.Rounded.Notes,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = calendar.description,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (calendar.attendees.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||||
|
imageVector = Icons.Rounded.People,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = calendar.attendees.joinToString(),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (calendar.location.isNotBlank()) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
viewModel.openLocation(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||||
|
imageVector = Icons.Rounded.Place,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = calendar.location,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||||
|
|
||||||
|
if (LocalFavoritesEnabled.current) {
|
||||||
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
|
val favAction = if (isPinned) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_unpin),
|
||||||
|
icon = Icons.Rounded.Star,
|
||||||
|
action = {
|
||||||
|
viewModel.unpin()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_pin),
|
||||||
|
icon = Icons.Rounded.StarOutline,
|
||||||
|
action = {
|
||||||
|
viewModel.pin()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(favAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||||
|
val hideAction = if (isHidden) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_unhide),
|
||||||
|
icon = Icons.Rounded.Visibility,
|
||||||
|
action = {
|
||||||
|
viewModel.unhide()
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_hide),
|
||||||
|
icon = Icons.Rounded.VisibilityOff,
|
||||||
|
action = {
|
||||||
|
viewModel.hide()
|
||||||
|
onBack()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(hideAction)
|
||||||
|
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.calendar_menu_open_externally),
|
||||||
|
icon = Icons.Rounded.OpenInNew,
|
||||||
|
action = {
|
||||||
|
viewModel.launch(context as AppCompatActivity)
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Toolbar(
|
||||||
|
leftActions = listOf(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(id = R.string.menu_back),
|
||||||
|
icon = Icons.Rounded.ArrowBack
|
||||||
|
) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
rightActions = toolbarActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun CalendarItemGridPopup(
|
||||||
|
calendar: CalendarEvent,
|
||||||
|
show: Boolean,
|
||||||
|
animationProgress: Float,
|
||||||
|
origin: Rect,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = show,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(snap()) with
|
||||||
|
fadeOut(snap(400)) using
|
||||||
|
SizeTransform { _, _ ->
|
||||||
|
tween(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { targetState ->
|
||||||
|
if (targetState) {
|
||||||
|
CalendarItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(calendar.color).copy(alpha = 1f - animationProgress)),
|
||||||
|
calendar = calendar,
|
||||||
|
showDetails = true,
|
||||||
|
onBack = onDismiss
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(origin.width.toDp())
|
||||||
|
.requiredHeight(origin.height.toDp())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.calendar
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
class CalendarItemVM(
|
||||||
|
private val calendarEvent: CalendarEvent
|
||||||
|
) : SearchableItemVM(calendarEvent) {
|
||||||
|
|
||||||
|
fun getSummary(context: Context): String {
|
||||||
|
val isToday =
|
||||||
|
DateUtils.isToday(calendarEvent.startTime) && DateUtils.isToday(calendarEvent.endTime)
|
||||||
|
return if (isToday) {
|
||||||
|
if (calendarEvent.allDay) {
|
||||||
|
context.getString(R.string.calendar_event_allday)
|
||||||
|
} else {
|
||||||
|
DateUtils.formatDateRange(
|
||||||
|
context,
|
||||||
|
calendarEvent.startTime,
|
||||||
|
calendarEvent.endTime,
|
||||||
|
DateUtils.FORMAT_SHOW_TIME
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (calendarEvent.allDay) {
|
||||||
|
DateUtils.formatDateRange(
|
||||||
|
context,
|
||||||
|
calendarEvent.startTime,
|
||||||
|
calendarEvent.endTime,
|
||||||
|
DateUtils.FORMAT_SHOW_DATE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DateUtils.formatDateRange(
|
||||||
|
context,
|
||||||
|
calendarEvent.startTime,
|
||||||
|
calendarEvent.endTime,
|
||||||
|
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatTime(context: Context): String {
|
||||||
|
if (calendarEvent.allDay) return DateUtils.formatDateRange(context, calendarEvent.startTime, calendarEvent.endTime, DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY)
|
||||||
|
return DateUtils.formatDateRange(context, calendarEvent.startTime, calendarEvent.endTime, DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_WEEKDAY)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openLocation(context: AppCompatActivity) {
|
||||||
|
context.tryStartActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW)
|
||||||
|
.setData(Uri.parse("geo:0,0?q=${URLEncoder.encode(calendarEvent.location, "utf8")}"))
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.calendar
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.CalendarResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val calendarEvents by viewModel.calendarResults.observeAsState(emptyList())
|
||||||
|
|
||||||
|
AnimatedVisibility(calendarEvents.isNotEmpty()) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth()
|
||||||
|
) {
|
||||||
|
SearchResultList(items = calendarEvents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.common
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
|
import de.mm20.launcher2.badges.BadgeRepository
|
||||||
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
abstract class SearchableItemVM(
|
||||||
|
private val searchable: Searchable
|
||||||
|
) : KoinComponent {
|
||||||
|
protected val favoritesRepository: FavoritesRepository by inject()
|
||||||
|
protected val badgeRepository: BadgeRepository by inject()
|
||||||
|
protected val iconRepository: IconRepository by inject()
|
||||||
|
|
||||||
|
val isPinned = favoritesRepository.isPinned(searchable)
|
||||||
|
fun pin() {
|
||||||
|
favoritesRepository.pinItem(searchable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unpin() {
|
||||||
|
favoritesRepository.unpinItem(searchable)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isHidden = favoritesRepository.isHidden(searchable)
|
||||||
|
fun hide() {
|
||||||
|
favoritesRepository.hideItem(searchable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unhide() {
|
||||||
|
favoritesRepository.unhideItem(searchable)
|
||||||
|
}
|
||||||
|
|
||||||
|
val badge = badgeRepository.getBadge(searchable.badgeKey)
|
||||||
|
|
||||||
|
fun getIcon(size: Int): Flow<LauncherIcon> {
|
||||||
|
return iconRepository.getIcon(searchable, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun launch(context: AppCompatActivity, bounds: Rect? = null): Boolean {
|
||||||
|
val options = if (bounds != null) {
|
||||||
|
ActivityOptionsCompat.makeClipRevealAnimation(
|
||||||
|
context.window.decorView,
|
||||||
|
bounds.left.toInt(),
|
||||||
|
bounds.top.toInt(),
|
||||||
|
bounds.width.toInt(),
|
||||||
|
bounds.height.toInt()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ActivityOptionsCompat.makeBasic()
|
||||||
|
}
|
||||||
|
if (searchable.launch(context, options.toBundle())) {
|
||||||
|
favoritesRepository.incrementLaunchCounter(searchable)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.common
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Popup
|
||||||
|
import androidx.compose.ui.window.PopupProperties
|
||||||
|
import de.mm20.launcher2.search.data.*
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.apps.AppItemGridPopup
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItemGridPopup
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItemVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.contacts.ContactItemGridPopup
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.files.FileItemGridPopup
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.shortcut.ShortcutItemGridPopup
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItemGridPopup
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItemGridPopup
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalWindowPosition
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun GridItem(modifier: Modifier = Modifier, item: Searchable) {
|
||||||
|
val viewModel = remember(item.key) { GridItemVM(item) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var showPopup by remember { mutableStateOf(false) }
|
||||||
|
var bounds by remember { mutableStateOf(Rect.Zero) }
|
||||||
|
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
val badge by viewModel.badge.collectAsState(null)
|
||||||
|
val iconSize = 48.dp.toPixels()
|
||||||
|
val icon by remember(item.key) { viewModel.getIcon(iconSize.toInt()) }.collectAsState(null)
|
||||||
|
|
||||||
|
// If item is one of these types, try to launch them on click; show details otherwise
|
||||||
|
val launchOnPress =
|
||||||
|
item is File || item is Application || item is AppShortcut || item is Website || item is Wikipedia
|
||||||
|
|
||||||
|
ShapedLauncherIcon(
|
||||||
|
modifier = Modifier
|
||||||
|
.onGloballyPositioned {
|
||||||
|
bounds = it.boundsInWindow()
|
||||||
|
},
|
||||||
|
size = 48.dp,
|
||||||
|
badge = badge,
|
||||||
|
icon = icon,
|
||||||
|
onClick = {
|
||||||
|
if (!launchOnPress || !viewModel.launch(context as AppCompatActivity, bounds)) {
|
||||||
|
showPopup = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
showPopup = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp),
|
||||||
|
text = item.label,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (showPopup) {
|
||||||
|
ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit) {
|
||||||
|
var show by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(null) {
|
||||||
|
show = true
|
||||||
|
}
|
||||||
|
LaunchedEffect(show) {
|
||||||
|
if (!show) {
|
||||||
|
delay(300L)
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackHandler {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val animationProgress by animateFloatAsState(if (show) 1f else 0f, tween(300))
|
||||||
|
Popup(
|
||||||
|
properties = PopupProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
dismissOnBackPress = true
|
||||||
|
),
|
||||||
|
alignment = Alignment.TopCenter,
|
||||||
|
onDismissRequest = {
|
||||||
|
show = false
|
||||||
|
},
|
||||||
|
offset = IntOffset(-origin.left.toInt(), 0)
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalWindowPosition provides origin.top) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
LauncherCard(
|
||||||
|
elevation = 8.dp * animationProgress,
|
||||||
|
backgroundOpacity = 1f,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.absoluteOffset(
|
||||||
|
x = ((1 - animationProgress) * origin.left).toDp() - 16.dp * (1 - animationProgress),
|
||||||
|
)
|
||||||
|
.wrapContentSize()
|
||||||
|
) {
|
||||||
|
when (searchable) {
|
||||||
|
is Application -> {
|
||||||
|
AppItemGridPopup(
|
||||||
|
app = searchable,
|
||||||
|
show = show,
|
||||||
|
animationProgress = animationProgress,
|
||||||
|
origin = origin,
|
||||||
|
onDismiss = {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Website -> {
|
||||||
|
WebsiteItemGridPopup(
|
||||||
|
website = searchable,
|
||||||
|
show = show,
|
||||||
|
animationProgress = animationProgress,
|
||||||
|
origin = origin,
|
||||||
|
onDismiss = {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Wikipedia -> {
|
||||||
|
WikipediaItemGridPopup(
|
||||||
|
wikipedia = searchable,
|
||||||
|
show = show,
|
||||||
|
animationProgress = animationProgress,
|
||||||
|
origin = origin,
|
||||||
|
onDismiss = {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Contact -> {
|
||||||
|
ContactItemGridPopup(
|
||||||
|
contact = searchable,
|
||||||
|
show = show,
|
||||||
|
animationProgress = animationProgress,
|
||||||
|
origin = origin,
|
||||||
|
onDismiss = {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is File -> {
|
||||||
|
FileItemGridPopup(
|
||||||
|
file = searchable,
|
||||||
|
show = show,
|
||||||
|
animationProgress = animationProgress,
|
||||||
|
origin = origin,
|
||||||
|
onDismiss = {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CalendarEvent -> {
|
||||||
|
CalendarItemGridPopup(
|
||||||
|
calendar = searchable,
|
||||||
|
show = show,
|
||||||
|
animationProgress = animationProgress,
|
||||||
|
origin = origin,
|
||||||
|
onDismiss = {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is AppShortcut -> {
|
||||||
|
ShortcutItemGridPopup(
|
||||||
|
shortcut = searchable,
|
||||||
|
show = show,
|
||||||
|
animationProgress = animationProgress,
|
||||||
|
origin = origin,
|
||||||
|
onDismiss = {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.common.grid
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.toAndroidRect
|
||||||
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
|
import de.mm20.launcher2.badges.BadgeRepository
|
||||||
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class GridItemVM(
|
||||||
|
private val searchable: Searchable
|
||||||
|
): SearchableItemVM(searchable)
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.common
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Popup
|
||||||
|
import androidx.compose.ui.window.PopupProperties
|
||||||
|
import de.mm20.launcher2.search.data.Application
|
||||||
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.apps.AppItemGridPopup
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchResultGrid(items: List<Searchable>) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.animateContentSize().fillMaxWidth().padding(4.dp)
|
||||||
|
) {
|
||||||
|
for (i in 0 until ceil(items.size / 5f).toInt()) {
|
||||||
|
Row {
|
||||||
|
for (j in 0 until 5) {
|
||||||
|
val item = items.getOrNull(i * 5 + j)
|
||||||
|
if (item != null) {
|
||||||
|
GridItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(4.dp, 8.dp), item = item
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,179 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.common.list
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
import androidx.compose.material.icons.rounded.StarOutline
|
||||||
|
import androidx.compose.material.icons.rounded.Visibility
|
||||||
|
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
|
import de.mm20.launcher2.search.data.Contact
|
||||||
|
import de.mm20.launcher2.search.data.File
|
||||||
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
|
import de.mm20.launcher2.ui.component.InnerCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.contacts.ContactItem
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.files.FileItem
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ListItem(modifier: Modifier = Modifier, item: Searchable) {
|
||||||
|
var showDetails by remember { mutableStateOf(false) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val viewModel = remember(item.key) { ListItemVM(item) }
|
||||||
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
|
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||||
|
|
||||||
|
val dismissState = rememberDismissState(
|
||||||
|
confirmStateChange = {
|
||||||
|
when (it) {
|
||||||
|
DismissValue.Default -> {}
|
||||||
|
DismissValue.DismissedToEnd -> {
|
||||||
|
if (isPinned) viewModel.unpin()
|
||||||
|
else viewModel.pin()
|
||||||
|
}
|
||||||
|
DismissValue.DismissedToStart -> {
|
||||||
|
if (isHidden) viewModel.unhide()
|
||||||
|
else viewModel.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it == DismissValue.DismissedToStart
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val swipeDirections = when {
|
||||||
|
showDetails -> emptySet()
|
||||||
|
LocalFavoritesEnabled.current -> setOf(
|
||||||
|
DismissDirection.StartToEnd,
|
||||||
|
DismissDirection.EndToStart
|
||||||
|
)
|
||||||
|
else -> setOf(DismissDirection.EndToStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeToDismiss(
|
||||||
|
modifier = modifier,
|
||||||
|
state = dismissState,
|
||||||
|
directions = swipeDirections,
|
||||||
|
background = {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
shape = RoundedCornerShape(LocalCardStyle.current.radius.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(
|
||||||
|
alpha = (dismissState.progress.fraction * 2f).coerceAtMost(
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
contentAlignment = if (dismissState.dismissDirection == DismissDirection.EndToStart) Alignment.CenterEnd else Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
val overThreshold =
|
||||||
|
dismissState.progress.fraction >= 0.5f && dismissState.dismissDirection != null
|
||||||
|
val iconScale by animateFloatAsState(
|
||||||
|
if (overThreshold) 1.25f else 1f
|
||||||
|
)
|
||||||
|
val iconColor by animateColorAsState(
|
||||||
|
if (overThreshold) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
val pinIcon = if (isPinned) Icons.Rounded.StarOutline else Icons.Rounded.Star
|
||||||
|
val hideIcon =
|
||||||
|
if (isHidden) Icons.Rounded.Visibility else Icons.Rounded.VisibilityOff
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.scale(iconScale),
|
||||||
|
imageVector = if (dismissState.dismissDirection == DismissDirection.EndToStart) {
|
||||||
|
hideIcon
|
||||||
|
} else {
|
||||||
|
pinIcon
|
||||||
|
},
|
||||||
|
tint = iconColor,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
var bounds by remember { mutableStateOf(Rect.Zero) }
|
||||||
|
InnerCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned {
|
||||||
|
bounds = it.boundsInWindow()
|
||||||
|
},
|
||||||
|
raised = showDetails
|
||||||
|
) {
|
||||||
|
when (item) {
|
||||||
|
is Contact -> {
|
||||||
|
ContactItem(
|
||||||
|
modifier = Modifier.combinedClickable(
|
||||||
|
enabled = !showDetails,
|
||||||
|
onClick = { showDetails = true },
|
||||||
|
onLongClick = { showDetails = true }
|
||||||
|
),
|
||||||
|
contact = item,
|
||||||
|
showDetails = showDetails,
|
||||||
|
onBack = { showDetails = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is File -> {
|
||||||
|
FileItem(
|
||||||
|
modifier = Modifier.combinedClickable(
|
||||||
|
enabled = !showDetails,
|
||||||
|
onClick = {
|
||||||
|
if (!viewModel.launch(context as AppCompatActivity, bounds)) {
|
||||||
|
showDetails = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { showDetails = true }
|
||||||
|
),
|
||||||
|
file = item,
|
||||||
|
showDetails = showDetails,
|
||||||
|
onBack = { showDetails = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CalendarEvent -> {
|
||||||
|
CalendarItem(
|
||||||
|
modifier = Modifier.combinedClickable(
|
||||||
|
enabled = !showDetails,
|
||||||
|
onClick = { showDetails = true },
|
||||||
|
onLongClick = { showDetails = true }
|
||||||
|
),
|
||||||
|
calendar = item,
|
||||||
|
showDetails = showDetails,
|
||||||
|
onBack = { showDetails = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.common.list
|
||||||
|
|
||||||
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
|
||||||
|
class ListItemVM(
|
||||||
|
private val searchable: Searchable
|
||||||
|
): SearchableItemVM(searchable) {
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.common.list
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchResultList(items: List<Searchable>) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
for (item in items) {
|
||||||
|
key(item.key) {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp),
|
||||||
|
item = item
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,325 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.contacts
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.animateDp
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.*
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.ExperimentalUnitApi
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.search.data.Contact
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||||
|
import de.mm20.launcher2.ui.component.*
|
||||||
|
import de.mm20.launcher2.ui.icons.Telegram
|
||||||
|
import de.mm20.launcher2.ui.icons.WhatsApp
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUnitApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ContactItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contact: Contact,
|
||||||
|
showDetails: Boolean,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewModel = remember(contact) { ContactItemVM(contact) }
|
||||||
|
|
||||||
|
val transition = updateTransition(showDetails, label = "ContactItem")
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val iconSize = 48.dp.toPixels().toInt()
|
||||||
|
val icon by remember(contact) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
||||||
|
val padding by transition.animateDp(label = "iconPadding") {
|
||||||
|
if (it) 16.dp else 8.dp
|
||||||
|
}
|
||||||
|
ShapedLauncherIcon(
|
||||||
|
size = 48.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = padding, top = padding, bottom = padding),
|
||||||
|
icon = icon,
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
val textStyle by animateTextStyleAsState(
|
||||||
|
if (showDetails) MaterialTheme.typography.titleLarge
|
||||||
|
else MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = contact.label,
|
||||||
|
style = textStyle,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
AnimatedVisibility(!showDetails) {
|
||||||
|
Text(
|
||||||
|
contact.summary,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(showDetails) {
|
||||||
|
Column {
|
||||||
|
|
||||||
|
if (contact.phones.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Call, contentDescription = null)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
items(contact.phones.toList()) {
|
||||||
|
Chip(
|
||||||
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
text = it.label,
|
||||||
|
onClick = {
|
||||||
|
viewModel.contact(context as AppCompatActivity, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contact.emails.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Email, contentDescription = null)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
items(contact.emails.toList()) {
|
||||||
|
Chip(
|
||||||
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
text = it.label,
|
||||||
|
onClick = {
|
||||||
|
viewModel.contact(context as AppCompatActivity, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contact.telegram.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Telegram, contentDescription = null)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
items(contact.telegram.toList()) {
|
||||||
|
Chip(
|
||||||
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
text = it.label,
|
||||||
|
onClick = {
|
||||||
|
viewModel.contact(context as AppCompatActivity, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contact.whatsapp.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.WhatsApp, contentDescription = null)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
items(contact.whatsapp.toList()) {
|
||||||
|
Chip(
|
||||||
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
text = it.label,
|
||||||
|
onClick = {
|
||||||
|
viewModel.contact(context as AppCompatActivity, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contact.postals.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(Icons.Rounded.Place, contentDescription = null)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
items(contact.postals.toList()) {
|
||||||
|
Chip(
|
||||||
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
text = it.label,
|
||||||
|
onClick = {
|
||||||
|
viewModel.contact(context as AppCompatActivity, it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||||
|
|
||||||
|
if (LocalFavoritesEnabled.current) {
|
||||||
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
|
val favAction = if (isPinned) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_unpin),
|
||||||
|
icon = Icons.Rounded.Star,
|
||||||
|
action = {
|
||||||
|
viewModel.unpin()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_pin),
|
||||||
|
icon = Icons.Rounded.StarOutline,
|
||||||
|
action = {
|
||||||
|
viewModel.pin()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(favAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||||
|
val hideAction = if (isHidden) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_unhide),
|
||||||
|
icon = Icons.Rounded.Visibility,
|
||||||
|
action = {
|
||||||
|
viewModel.unhide()
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_hide),
|
||||||
|
icon = Icons.Rounded.VisibilityOff,
|
||||||
|
action = {
|
||||||
|
viewModel.hide()
|
||||||
|
onBack()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(hideAction)
|
||||||
|
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.calendar_menu_open_externally),
|
||||||
|
icon = Icons.Rounded.OpenInNew,
|
||||||
|
action = {
|
||||||
|
viewModel.launch(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Toolbar(
|
||||||
|
leftActions = listOf(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(id = R.string.menu_back),
|
||||||
|
icon = Icons.Rounded.ArrowBack
|
||||||
|
) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
rightActions = toolbarActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ContactItemGridPopup(
|
||||||
|
contact: Contact,
|
||||||
|
show: Boolean,
|
||||||
|
animationProgress: Float,
|
||||||
|
origin: Rect,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = show,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(snap()) with
|
||||||
|
fadeOut(snap(400)) using
|
||||||
|
SizeTransform { _, _ ->
|
||||||
|
tween(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { targetState ->
|
||||||
|
if (targetState) {
|
||||||
|
ContactItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.offset(
|
||||||
|
x = -16.dp * (1 - animationProgress),
|
||||||
|
y = -16.dp * (1 - animationProgress)
|
||||||
|
),
|
||||||
|
contact = contact,
|
||||||
|
showDetails = true,
|
||||||
|
onBack = onDismiss
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(origin.width.toDp())
|
||||||
|
.requiredHeight(origin.height.toDp())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.contacts
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.data.Contact
|
||||||
|
import de.mm20.launcher2.search.data.ContactInfo
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
|
class ContactItemVM(
|
||||||
|
val contact: Contact
|
||||||
|
): SearchableItemVM(contact) {
|
||||||
|
fun contact(context: AppCompatActivity, contactInfo: ContactInfo) {
|
||||||
|
context.tryStartActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setData(Uri.parse(contactInfo.data))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.contacts
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.ContactResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val contacts by viewModel.contactResults.observeAsState(emptyList())
|
||||||
|
|
||||||
|
AnimatedVisibility(contacts.isNotEmpty()) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth()
|
||||||
|
) {
|
||||||
|
SearchResultList(items = contacts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.favorites
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchResultGrid
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.FavoritesResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val favorites by viewModel.favorites.observeAsState(emptyList())
|
||||||
|
|
||||||
|
val hide by viewModel.hideFavorites.observeAsState(true)
|
||||||
|
|
||||||
|
AnimatedVisibility(!hide && favorites.isNotEmpty()) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
SearchResultGrid(items = favorites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,305 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.files
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.animateDp
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.*
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.ExperimentalUnitApi
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.search.data.File
|
||||||
|
import de.mm20.launcher2.search.data.LocalFile
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||||
|
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||||
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
|
import de.mm20.launcher2.ui.component.Toolbar
|
||||||
|
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUnitApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FileItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
file: File,
|
||||||
|
showDetails: Boolean,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewModel = remember(file.key) { FileItemVM(file) }
|
||||||
|
|
||||||
|
val transition = updateTransition(showDetails, label = "ContactItem")
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
val textPadding by transition.animateDp(label = "textPadding") {
|
||||||
|
if (it) 16.dp else 12.dp
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp, top = textPadding, end = 16.dp)
|
||||||
|
) {
|
||||||
|
val textStyle by animateTextStyleAsState(
|
||||||
|
if (showDetails) MaterialTheme.typography.titleMedium
|
||||||
|
else MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = file.label,
|
||||||
|
style = textStyle,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
AnimatedVisibility(!showDetails) {
|
||||||
|
Text(
|
||||||
|
file.getFileType(context),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(showDetails) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.file_meta_type,
|
||||||
|
file.mimeType
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
for ((k, v) in file.metaData) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(k, v),
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!file.isDirectory) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.file_meta_size,
|
||||||
|
formatFileSize(file.size)
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val iconSize = 48.dp.toPixels().toInt()
|
||||||
|
val icon by remember(file) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
||||||
|
val badge by viewModel.badge.collectAsState(null)
|
||||||
|
val padding by transition.animateDp(label = "iconPadding") {
|
||||||
|
if (it) 16.dp else 8.dp
|
||||||
|
}
|
||||||
|
ShapedLauncherIcon(
|
||||||
|
size = 48.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = padding, top = padding, bottom = padding),
|
||||||
|
icon = icon,
|
||||||
|
badge = badge
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(showDetails) {
|
||||||
|
Column {
|
||||||
|
|
||||||
|
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||||
|
|
||||||
|
if (LocalFavoritesEnabled.current) {
|
||||||
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
|
val favAction = if (isPinned) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_unpin),
|
||||||
|
icon = Icons.Rounded.Star,
|
||||||
|
action = {
|
||||||
|
viewModel.unpin()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_pin),
|
||||||
|
icon = Icons.Rounded.StarOutline,
|
||||||
|
action = {
|
||||||
|
viewModel.pin()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(favAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_open_file),
|
||||||
|
icon = Icons.Rounded.OpenInNew,
|
||||||
|
action = {
|
||||||
|
viewModel.launch(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (viewModel.canShare) {
|
||||||
|
toolbarActions.add(DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_share),
|
||||||
|
icon = Icons.Rounded.Share,
|
||||||
|
action = {
|
||||||
|
viewModel.share(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.canDelete) {
|
||||||
|
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
toolbarActions.add(DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_delete),
|
||||||
|
icon = Icons.Rounded.Delete,
|
||||||
|
action = {
|
||||||
|
showConfirmDialog = true
|
||||||
|
}
|
||||||
|
))
|
||||||
|
if (showConfirmDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showConfirmDialog = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.delete()
|
||||||
|
showConfirmDialog = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(android.R.string.ok), style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showConfirmDialog = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(android.R.string.cancel), style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
if (file.isDirectory) stringResource(
|
||||||
|
R.string.alert_delete_directory,
|
||||||
|
file.label
|
||||||
|
)
|
||||||
|
else stringResource(R.string.alert_delete_file, file.label)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isHidden by viewModel.isHidden.collectAsState(false)
|
||||||
|
val hideAction = if (isHidden) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_unhide),
|
||||||
|
icon = Icons.Rounded.Visibility,
|
||||||
|
action = {
|
||||||
|
viewModel.unhide()
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_hide),
|
||||||
|
icon = Icons.Rounded.VisibilityOff,
|
||||||
|
action = {
|
||||||
|
viewModel.hide()
|
||||||
|
onBack()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(hideAction)
|
||||||
|
|
||||||
|
Toolbar(
|
||||||
|
leftActions = listOf(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(id = R.string.menu_back),
|
||||||
|
icon = Icons.Rounded.ArrowBack
|
||||||
|
) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
rightActions = toolbarActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FileItemGridPopup(
|
||||||
|
file: File,
|
||||||
|
show: Boolean,
|
||||||
|
animationProgress: Float,
|
||||||
|
origin: Rect,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = show,
|
||||||
|
transitionSpec = {
|
||||||
|
slideInHorizontally(
|
||||||
|
tween(300),
|
||||||
|
initialOffsetX = { -it + origin.width.roundToInt() }) with
|
||||||
|
slideOutHorizontally(
|
||||||
|
tween(300),
|
||||||
|
targetOffsetX = { -it + origin.width.roundToInt() }) + fadeOut(snap(400)) using
|
||||||
|
SizeTransform { _, _ ->
|
||||||
|
tween(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { targetState ->
|
||||||
|
if (targetState) {
|
||||||
|
FileItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.offset(
|
||||||
|
x = 16.dp * (1 - animationProgress),
|
||||||
|
y = -16.dp * (1 - animationProgress)
|
||||||
|
),
|
||||||
|
file = file,
|
||||||
|
showDetails = true,
|
||||||
|
onBack = onDismiss
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(origin.width.toDp())
|
||||||
|
.requiredHeight(origin.height.toDp())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatFileSize(size: Long): String {
|
||||||
|
return when {
|
||||||
|
size < 1000L -> "$size Bytes"
|
||||||
|
size < 1000000L -> "${DecimalFormat("#,##0.#").format(size / 1000.0)} kB"
|
||||||
|
size < 1000000000L -> "${DecimalFormat("#,##0.#").format(size / 1000000.0)} MB"
|
||||||
|
size < 1000000000000L -> "${DecimalFormat("#,##0.#").format(size / 1000000000.0)} GB"
|
||||||
|
else -> "${DecimalFormat("#,##0.#").format(size / 1000000000000.0)} TB"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.files
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import de.mm20.launcher2.files.FileRepository
|
||||||
|
import de.mm20.launcher2.search.data.File
|
||||||
|
import de.mm20.launcher2.search.data.LocalFile
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class FileItemVM(
|
||||||
|
private val file: File
|
||||||
|
) : SearchableItemVM(file) {
|
||||||
|
private val fileRepository: FileRepository by inject()
|
||||||
|
|
||||||
|
val canShare = file is LocalFile
|
||||||
|
val canDelete = file.isDeletable
|
||||||
|
|
||||||
|
fun delete() {
|
||||||
|
fileRepository.deleteFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun share(context: AppCompatActivity) {
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||||
|
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
context.applicationContext.packageName + ".fileprovider",
|
||||||
|
java.io.File(file.path)
|
||||||
|
)
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
shareIntent.type = file.mimeType
|
||||||
|
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.files
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.FileResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val files by viewModel.fileResults.observeAsState(emptyList())
|
||||||
|
|
||||||
|
AnimatedVisibility(files.isNotEmpty()) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth()
|
||||||
|
) {
|
||||||
|
SearchResultList(items = files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.shortcut
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
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.AppShortcut
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.*
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
import de.mm20.launcher2.ui.modifier.scale
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AppItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shortcut: AppShortcut,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
val viewModel = remember { ShortcutItemVM(shortcut) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = shortcut.label, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.shortcut_summary, shortcut.appName),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val badge by viewModel.badge.collectAsState(null)
|
||||||
|
val iconSize = 84.dp.toPixels().toInt()
|
||||||
|
val icon by remember(shortcut.key) { viewModel.getIcon(iconSize) }.collectAsState(null)
|
||||||
|
ShapedLauncherIcon(
|
||||||
|
size = 84.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp),
|
||||||
|
badge = badge,
|
||||||
|
icon = icon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||||
|
|
||||||
|
if (LocalFavoritesEnabled.current) {
|
||||||
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
|
val favAction = if (isPinned) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_unpin),
|
||||||
|
icon = Icons.Rounded.Star,
|
||||||
|
action = {
|
||||||
|
viewModel.unpin()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_pin),
|
||||||
|
icon = Icons.Rounded.StarOutline,
|
||||||
|
action = {
|
||||||
|
viewModel.pin()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(favAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_app_info),
|
||||||
|
icon = Icons.Rounded.Info
|
||||||
|
) {
|
||||||
|
viewModel.openAppInfo(context as AppCompatActivity)
|
||||||
|
})
|
||||||
|
|
||||||
|
Toolbar(
|
||||||
|
leftActions = listOf(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(id = R.string.menu_back),
|
||||||
|
icon = Icons.Rounded.ArrowBack
|
||||||
|
) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
rightActions = toolbarActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ShortcutItemGridPopup(
|
||||||
|
shortcut: AppShortcut,
|
||||||
|
show: Boolean,
|
||||||
|
animationProgress: Float,
|
||||||
|
origin: Rect,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = show,
|
||||||
|
transitionSpec = {
|
||||||
|
slideInHorizontally(
|
||||||
|
tween(300),
|
||||||
|
initialOffsetX = { -it + origin.width.roundToInt() }) with
|
||||||
|
slideOutHorizontally(
|
||||||
|
tween(300),
|
||||||
|
targetOffsetX = { -it + origin.width.roundToInt() }) + fadeOut(snap(400)) using
|
||||||
|
SizeTransform { _, _ ->
|
||||||
|
tween(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { targetState ->
|
||||||
|
if (targetState) {
|
||||||
|
AppItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.scale(
|
||||||
|
1 - (1 - 48.dp / 84.dp) * (1 - animationProgress),
|
||||||
|
transformOrigin = TransformOrigin(1f, 0f)
|
||||||
|
)
|
||||||
|
.offset(
|
||||||
|
x = 16.dp * (1 - animationProgress).pow(10),
|
||||||
|
y = -16.dp * (1 - animationProgress),
|
||||||
|
),
|
||||||
|
shortcut = shortcut,
|
||||||
|
onBack = onDismiss
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(origin.width.toDp())
|
||||||
|
.requiredHeight(origin.height.toDp())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.shortcut
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
|
||||||
|
class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(shortcut) {
|
||||||
|
fun openAppInfo(context: AppCompatActivity) {
|
||||||
|
context.tryStartActivity(
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.parse("package:${shortcut.launcherShortcut.`package`}")
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.unitconverter
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.search.UnitConverterItem
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.UnitConverterResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val unitConverter by viewModel.unitConverterResult.observeAsState(null)
|
||||||
|
|
||||||
|
AnimatedVisibility(unitConverter != null) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
unitConverter?.let { UnitConverterItem(unitConverter = it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.website
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.Share
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
import androidx.compose.material.icons.rounded.StarOutline
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
|
import de.mm20.launcher2.search.data.Application
|
||||||
|
import de.mm20.launcher2.search.data.Website
|
||||||
|
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||||
|
import de.mm20.launcher2.ui.component.Toolbar
|
||||||
|
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.apps.AppItem
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WebsiteItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
website: Website,
|
||||||
|
onBack: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewModel = remember(website) { WebsiteItemVM(website) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (website.image.isNotBlank()) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||||
|
painter = rememberImagePainter(website.image),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = website.label,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
text = website.description,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||||
|
|
||||||
|
if (LocalFavoritesEnabled.current) {
|
||||||
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
|
val favAction = if (isPinned) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_unpin),
|
||||||
|
icon = Icons.Rounded.Star,
|
||||||
|
action = {
|
||||||
|
viewModel.unpin()
|
||||||
|
onBack?.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_pin),
|
||||||
|
icon = Icons.Rounded.StarOutline,
|
||||||
|
action = {
|
||||||
|
viewModel.pin()
|
||||||
|
onBack?.invoke()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(favAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_share),
|
||||||
|
icon= Icons.Rounded.Share,
|
||||||
|
action = {
|
||||||
|
viewModel.share(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Toolbar(
|
||||||
|
leftActions = if (onBack != null) listOf(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
stringResource(id = R.string.menu_back),
|
||||||
|
icon = Icons.Rounded.ArrowBack,
|
||||||
|
action = onBack
|
||||||
|
)
|
||||||
|
) else emptyList(),
|
||||||
|
rightActions = toolbarActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun WebsiteItemGridPopup(
|
||||||
|
website: Website,
|
||||||
|
show: Boolean,
|
||||||
|
animationProgress: Float,
|
||||||
|
origin: Rect,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = show,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(tween(200)) with
|
||||||
|
fadeOut(tween(200, 200)) using
|
||||||
|
SizeTransform { _, _ ->
|
||||||
|
tween(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { targetState ->
|
||||||
|
if (targetState) {
|
||||||
|
WebsiteItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
website = website,
|
||||||
|
onBack = onDismiss
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(origin.width.toDp())
|
||||||
|
.requiredHeight(origin.height.toDp())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.website
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
|
import de.mm20.launcher2.search.data.Website
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class WebsiteItemVM(
|
||||||
|
private val website: Website
|
||||||
|
): KoinComponent {
|
||||||
|
private val favoritesRepository: FavoritesRepository by inject()
|
||||||
|
|
||||||
|
val isPinned = favoritesRepository.isPinned(website)
|
||||||
|
fun pin() {
|
||||||
|
favoritesRepository.pinItem(website)
|
||||||
|
}
|
||||||
|
fun unpin() {
|
||||||
|
favoritesRepository.unpinItem(website)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun share(context: AppCompatActivity) {
|
||||||
|
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,28 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.website
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.WebsiteResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val website by viewModel.websiteResult.observeAsState(null)
|
||||||
|
|
||||||
|
AnimatedVisibility(website != null) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
website?.let { WebsiteItem(website = it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.wikipedia
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.Share
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
import androidx.compose.material.icons.rounded.StarOutline
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
|
import de.mm20.launcher2.search.data.Website
|
||||||
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||||
|
import de.mm20.launcher2.ui.component.Toolbar
|
||||||
|
import de.mm20.launcher2.ui.component.ToolbarAction
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WikipediaItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
wikipedia: Wikipedia,
|
||||||
|
onBack: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val viewModel = remember(wikipedia) { WikipediaItemVM(wikipedia) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (!wikipedia.image.isNullOrEmpty()) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(16f / 9f)
|
||||||
|
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||||
|
painter = rememberImagePainter(wikipedia.image),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = wikipedia.label,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
text = stringResource(id = R.string.wikipedia_source),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
text = wikipedia.text,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val toolbarActions = mutableListOf<ToolbarAction>()
|
||||||
|
|
||||||
|
if (LocalFavoritesEnabled.current) {
|
||||||
|
val isPinned by viewModel.isPinned.collectAsState(false)
|
||||||
|
val favAction = if (isPinned) {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_unpin),
|
||||||
|
icon = Icons.Rounded.Star,
|
||||||
|
action = {
|
||||||
|
viewModel.unpin()
|
||||||
|
onBack?.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.favorites_menu_pin),
|
||||||
|
icon = Icons.Rounded.StarOutline,
|
||||||
|
action = {
|
||||||
|
viewModel.pin()
|
||||||
|
onBack?.invoke()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toolbarActions.add(favAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarActions.add(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
label = stringResource(R.string.menu_share),
|
||||||
|
icon = Icons.Rounded.Share,
|
||||||
|
action = {
|
||||||
|
viewModel.share(context as AppCompatActivity)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Toolbar(
|
||||||
|
leftActions = if (onBack != null) listOf(
|
||||||
|
DefaultToolbarAction(
|
||||||
|
stringResource(id = R.string.menu_back),
|
||||||
|
icon = Icons.Rounded.ArrowBack,
|
||||||
|
action = onBack
|
||||||
|
)
|
||||||
|
) else emptyList(),
|
||||||
|
rightActions = toolbarActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun WikipediaItemGridPopup(
|
||||||
|
wikipedia: Wikipedia,
|
||||||
|
show: Boolean,
|
||||||
|
animationProgress: Float,
|
||||||
|
origin: Rect,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = show,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn(tween(200)) with
|
||||||
|
fadeOut(tween(200, 200)) using
|
||||||
|
SizeTransform { _, _ ->
|
||||||
|
tween(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { targetState ->
|
||||||
|
if (targetState) {
|
||||||
|
WikipediaItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
wikipedia = wikipedia,
|
||||||
|
onBack = onDismiss
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(origin.width.toDp())
|
||||||
|
.requiredHeight(origin.height.toDp())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.wikipedia
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class WikipediaItemVM(
|
||||||
|
private val wikipedia: Wikipedia
|
||||||
|
) : KoinComponent {
|
||||||
|
private val favoritesRepository: FavoritesRepository by inject()
|
||||||
|
|
||||||
|
val isPinned = favoritesRepository.isPinned(wikipedia)
|
||||||
|
fun pin() {
|
||||||
|
favoritesRepository.pinItem(wikipedia)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unpin() {
|
||||||
|
favoritesRepository.unpinItem(wikipedia)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun share(context: AppCompatActivity) {
|
||||||
|
val text = HtmlCompat.fromHtml(wikipedia.text, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||||
|
shareIntent.putExtra(
|
||||||
|
Intent.EXTRA_TEXT, "${wikipedia.label}\n\n" +
|
||||||
|
"${text.substring(0, 200)}…\n\n" +
|
||||||
|
"${wikipedia.wikipediaUrl}/wiki?curid=${wikipedia.id}"
|
||||||
|
)
|
||||||
|
shareIntent.type = "text/plain"
|
||||||
|
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search.wikipedia
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.WikipediaResults() {
|
||||||
|
val viewModel: SearchVM = viewModel()
|
||||||
|
val wikipedia by viewModel.wikipediaResult.observeAsState(null)
|
||||||
|
|
||||||
|
AnimatedVisibility(wikipedia != null) {
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
wikipedia?.let { WikipediaItem(wikipedia = it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -90,8 +90,6 @@ class WidgetView : LauncherCardView {
|
|||||||
enableTransitionType(LayoutTransition.CHANGING)
|
enableTransitionType(LayoutTransition.CHANGING)
|
||||||
}
|
}
|
||||||
|
|
||||||
elevation = if (backgroundOpacity < 255) 0f else resources.getDimension(R.dimen.card_elevation)
|
|
||||||
|
|
||||||
TooltipCompat.setTooltipText(binding.widgetActionResize, context.getString(R.string.widget_action_adjust_height))
|
TooltipCompat.setTooltipText(binding.widgetActionResize, context.getString(R.string.widget_action_adjust_height))
|
||||||
TooltipCompat.setTooltipText(binding.widgetActionRemove, context.getString(R.string.widget_action_remove))
|
TooltipCompat.setTooltipText(binding.widgetActionRemove, context.getString(R.string.widget_action_remove))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -135,9 +135,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
val callView = (View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
val callView = (View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||||
it.setStartCompoundDrawable(R.drawable.ic_call)
|
it.setStartCompoundDrawable(R.drawable.ic_call)
|
||||||
if (contact.phones.size == 1) {
|
if (contact.phones.size == 1) {
|
||||||
it.text = contact.phones.first()
|
it.text = contact.phones.first().label
|
||||||
it.setOnClickListener {
|
it.setOnClickListener {
|
||||||
call(rootView, contact.phones.first())
|
call(rootView, contact.phones.first().data)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
it.text =
|
it.text =
|
||||||
@ -146,10 +146,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
val menu = PopupMenu(context, it, Gravity.START)
|
val menu = PopupMenu(context, it, Gravity.START)
|
||||||
val phones = contact.phones.toList()
|
val phones = contact.phones.toList()
|
||||||
for ((i, phone) in phones.withIndex()) {
|
for ((i, phone) in phones.withIndex()) {
|
||||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone)
|
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.label)
|
||||||
}
|
}
|
||||||
menu.setOnMenuItemClickListener {
|
menu.setOnMenuItemClickListener {
|
||||||
call(rootView, phones[it.itemId])
|
call(rootView, phones[it.itemId].data)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
menu.show()
|
menu.show()
|
||||||
@ -164,9 +164,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||||
it.setStartCompoundDrawable(R.drawable.ic_message)
|
it.setStartCompoundDrawable(R.drawable.ic_message)
|
||||||
if (contact.phones.size == 1) {
|
if (contact.phones.size == 1) {
|
||||||
it.text = contact.phones.first()
|
it.text = contact.phones.first().label
|
||||||
it.setOnClickListener {
|
it.setOnClickListener {
|
||||||
message(rootView, contact.phones.first())
|
message(rootView, contact.phones.first().data)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
it.text = context.getString(
|
it.text = context.getString(
|
||||||
@ -177,10 +177,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
val menu = PopupMenu(context, it, Gravity.START)
|
val menu = PopupMenu(context, it, Gravity.START)
|
||||||
val phones = contact.phones.toList()
|
val phones = contact.phones.toList()
|
||||||
for ((i, phone) in phones.withIndex()) {
|
for ((i, phone) in phones.withIndex()) {
|
||||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone)
|
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.label)
|
||||||
}
|
}
|
||||||
menu.setOnMenuItemClickListener {
|
menu.setOnMenuItemClickListener {
|
||||||
message(rootView, phones[it.itemId])
|
message(rootView, phones[it.itemId].data)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
menu.show()
|
menu.show()
|
||||||
@ -195,9 +195,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||||
it.setStartCompoundDrawable(R.drawable.ic_mail)
|
it.setStartCompoundDrawable(R.drawable.ic_mail)
|
||||||
if (contact.emails.size == 1) {
|
if (contact.emails.size == 1) {
|
||||||
it.text = contact.emails.first()
|
it.text = contact.emails.first().label
|
||||||
it.setOnClickListener {
|
it.setOnClickListener {
|
||||||
email(rootView, contact.emails.first())
|
email(rootView, contact.emails.first().data)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
it.text =
|
it.text =
|
||||||
@ -206,10 +206,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
val menu = PopupMenu(context, it, Gravity.START)
|
val menu = PopupMenu(context, it, Gravity.START)
|
||||||
val emails = contact.emails.toList()
|
val emails = contact.emails.toList()
|
||||||
for ((i, email) in emails.withIndex()) {
|
for ((i, email) in emails.withIndex()) {
|
||||||
menu.menu.add(Menu.NONE, i, Menu.NONE, email)
|
menu.menu.add(Menu.NONE, i, Menu.NONE, email.label)
|
||||||
}
|
}
|
||||||
menu.setOnMenuItemClickListener {
|
menu.setOnMenuItemClickListener {
|
||||||
email(rootView, emails[it.itemId])
|
email(rootView, emails[it.itemId].data)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
menu.show()
|
menu.show()
|
||||||
@ -224,9 +224,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||||
it.setStartCompoundDrawable(R.drawable.ic_telegram)
|
it.setStartCompoundDrawable(R.drawable.ic_telegram)
|
||||||
if (contact.telegram.size == 1) {
|
if (contact.telegram.size == 1) {
|
||||||
it.text = contact.telegram.first().substringAfter('$')
|
it.text = contact.telegram.first().label
|
||||||
it.setOnClickListener {
|
it.setOnClickListener {
|
||||||
telegram(rootView, contact.telegram.first().substringBefore('$'))
|
telegram(rootView, contact.telegram.first().data)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
it.text = context.getString(
|
it.text = context.getString(
|
||||||
@ -237,10 +237,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
val menu = PopupMenu(context, it, Gravity.START)
|
val menu = PopupMenu(context, it, Gravity.START)
|
||||||
val phones = contact.telegram.toList()
|
val phones = contact.telegram.toList()
|
||||||
for ((i, phone) in phones.withIndex()) {
|
for ((i, phone) in phones.withIndex()) {
|
||||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.substringAfter('$'))
|
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.label)
|
||||||
}
|
}
|
||||||
menu.setOnMenuItemClickListener {
|
menu.setOnMenuItemClickListener {
|
||||||
telegram(rootView, phones[it.itemId].substringBefore('$'))
|
telegram(rootView, phones[it.itemId].data)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
menu.show()
|
menu.show()
|
||||||
@ -254,9 +254,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||||
it.setStartCompoundDrawable(R.drawable.ic_whatsapp)
|
it.setStartCompoundDrawable(R.drawable.ic_whatsapp)
|
||||||
if (contact.whatsapp.size == 1) {
|
if (contact.whatsapp.size == 1) {
|
||||||
it.text = contact.whatsapp.first().substringAfter("$")
|
it.text = contact.whatsapp.first().label
|
||||||
it.setOnClickListener {
|
it.setOnClickListener {
|
||||||
whatsapp(rootView, contact.whatsapp.first().substringBefore("$"))
|
whatsapp(rootView, contact.whatsapp.first().data)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
it.text = context.getString(
|
it.text = context.getString(
|
||||||
@ -267,10 +267,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
val menu = PopupMenu(context, it, Gravity.START)
|
val menu = PopupMenu(context, it, Gravity.START)
|
||||||
val phones = contact.whatsapp.toList()
|
val phones = contact.whatsapp.toList()
|
||||||
for ((i, phone) in phones.withIndex()) {
|
for ((i, phone) in phones.withIndex()) {
|
||||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.substringAfter('$'))
|
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.label)
|
||||||
}
|
}
|
||||||
menu.setOnMenuItemClickListener {
|
menu.setOnMenuItemClickListener {
|
||||||
whatsapp(rootView, phones[it.itemId].substringBefore('$'))
|
whatsapp(rootView, phones[it.itemId].data)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
menu.show()
|
menu.show()
|
||||||
@ -284,9 +284,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||||
it.setStartCompoundDrawable(R.drawable.ic_location)
|
it.setStartCompoundDrawable(R.drawable.ic_location)
|
||||||
if (contact.postals.size == 1) {
|
if (contact.postals.size == 1) {
|
||||||
it.text = contact.postals.first()
|
it.text = contact.postals.first().label
|
||||||
it.setOnClickListener {
|
it.setOnClickListener {
|
||||||
navigate(rootView, contact.postals.first())
|
navigate(rootView, contact.postals.first().data)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
it.text = context.getString(
|
it.text = context.getString(
|
||||||
@ -297,10 +297,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
val menu = PopupMenu(context, it, Gravity.START)
|
val menu = PopupMenu(context, it, Gravity.START)
|
||||||
val postals = contact.postals.toList()
|
val postals = contact.postals.toList()
|
||||||
for ((i, postal) in postals.withIndex()) {
|
for ((i, postal) in postals.withIndex()) {
|
||||||
menu.menu.add(Menu.NONE, i, Menu.NONE, postal)
|
menu.menu.add(Menu.NONE, i, Menu.NONE, postal.label)
|
||||||
}
|
}
|
||||||
menu.setOnMenuItemClickListener {
|
menu.setOnMenuItemClickListener {
|
||||||
navigate(rootView, postals[it.itemId])
|
navigate(rootView, postals[it.itemId].data)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
menu.show()
|
menu.show()
|
||||||
@ -311,44 +311,44 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun call(rootView: SearchableView, number: String) {
|
private fun call(rootView: SearchableView, data: String) {
|
||||||
val context = rootView.context
|
val context = rootView.context
|
||||||
try {
|
try {
|
||||||
val callIntent = Intent(Intent.ACTION_DIAL)
|
val callIntent = Intent(Intent.ACTION_DIAL)
|
||||||
callIntent.data = Uri.parse("tel:$number")
|
callIntent.data = Uri.parse(data)
|
||||||
ActivityStarter.start(context, rootView, intent = callIntent)
|
ActivityStarter.start(context, rootView, intent = callIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun message(rootView: SearchableView, number: String) {
|
private fun message(rootView: SearchableView, data: String) {
|
||||||
val context = rootView.context
|
val context = rootView.context
|
||||||
try {
|
try {
|
||||||
val messageIntent = Intent(Intent.ACTION_VIEW)
|
val messageIntent = Intent(Intent.ACTION_VIEW)
|
||||||
messageIntent.data = Uri.parse("sms:$number")
|
messageIntent.data = Uri.parse(data)
|
||||||
ActivityStarter.start(context, rootView, intent = messageIntent)
|
ActivityStarter.start(context, rootView, intent = messageIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun email(rootView: SearchableView, address: String) {
|
private fun email(rootView: SearchableView, data: String) {
|
||||||
val context = rootView.context
|
val context = rootView.context
|
||||||
try {
|
try {
|
||||||
val mailIntent = Intent(Intent.ACTION_VIEW)
|
val mailIntent = Intent(Intent.ACTION_VIEW)
|
||||||
mailIntent.data = Uri.parse("mailto:$address")
|
mailIntent.data = Uri.parse(data)
|
||||||
ActivityStarter.start(context, rootView, intent = mailIntent)
|
ActivityStarter.start(context, rootView, intent = mailIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun whatsapp(rootView: SearchableView, number: String) {
|
private fun whatsapp(rootView: SearchableView, data: String) {
|
||||||
val context = rootView.context
|
val context = rootView.context
|
||||||
try {
|
try {
|
||||||
val whatsappIntent = Intent(Intent.ACTION_VIEW)
|
val whatsappIntent = Intent(Intent.ACTION_VIEW)
|
||||||
whatsappIntent.data = Uri.withAppendedPath(ContactsContract.Data.CONTENT_URI, number)
|
whatsappIntent.data = Uri.parse(data)
|
||||||
ActivityStarter.start(context, rootView, intent = whatsappIntent)
|
ActivityStarter.start(context, rootView, intent = whatsappIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
||||||
|
|||||||
@ -162,21 +162,19 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
|||||||
|
|
||||||
sb.append(
|
sb.append(
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.file_meta_data_entry,
|
R.string.file_meta_type,
|
||||||
context.getString(R.string.file_meta_type),
|
|
||||||
file.mimeType
|
file.mimeType
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for ((k, v) in file.metaData) {
|
for ((k, v) in file.metaData) {
|
||||||
sb.append("\n")
|
sb.append("\n")
|
||||||
.append(context.getString(R.string.file_meta_data_entry, context.getString(k), v))
|
.append(context.getString(k, v))
|
||||||
}
|
}
|
||||||
if (!file.isDirectory) {
|
if (!file.isDirectory) {
|
||||||
sb.append("\n").append(
|
sb.append("\n").append(
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.file_meta_data_entry,
|
R.string.file_meta_size,
|
||||||
context.getString(R.string.file_meta_size),
|
|
||||||
formatFileSize(file.size)
|
formatFileSize(file.size)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -184,8 +182,7 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
|||||||
if (file.path.isNotEmpty()) {
|
if (file.path.isNotEmpty()) {
|
||||||
sb.append("\n").append(
|
sb.append("\n").append(
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.file_meta_data_entry,
|
R.string.file_meta_path,
|
||||||
context.getString(R.string.file_meta_path),
|
|
||||||
file.path
|
file.path
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -50,6 +50,8 @@ open class LauncherCardView @JvmOverloads constructor(
|
|||||||
|
|
||||||
private var overrideBackgroundOpacity = false
|
private var overrideBackgroundOpacity = false
|
||||||
|
|
||||||
|
private var actualElevation: Float
|
||||||
|
|
||||||
init {
|
init {
|
||||||
strokeColor = cardBackgroundColor.defaultColor
|
strokeColor = cardBackgroundColor.defaultColor
|
||||||
strokeWidth = (currentCardStyle.borderWidth * dp).roundToInt()
|
strokeWidth = (currentCardStyle.borderWidth * dp).roundToInt()
|
||||||
@ -71,8 +73,8 @@ open class LauncherCardView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
strokeOpacity = if (backgroundOpacity == 0) 0 else 0xFF
|
strokeOpacity = if (backgroundOpacity == 0) 0 else 0xFF
|
||||||
elevation = if (backgroundOpacity == 255) elevation else 0f
|
actualElevation = context.resources.getDimension(R.dimen.card_elevation)
|
||||||
cardElevation = elevation
|
cardElevation = if (backgroundOpacity == 255) actualElevation else 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dataStore: LauncherDataStore by inject()
|
private val dataStore: LauncherDataStore by inject()
|
||||||
@ -88,8 +90,7 @@ open class LauncherCardView @JvmOverloads constructor(
|
|||||||
if (!overrideBackgroundOpacity) {
|
if (!overrideBackgroundOpacity) {
|
||||||
backgroundOpacity = (it.opacity * 255).roundToInt()
|
backgroundOpacity = (it.opacity * 255).roundToInt()
|
||||||
strokeOpacity = if (backgroundOpacity == 0) 0 else 0xFF
|
strokeOpacity = if (backgroundOpacity == 0) 0 else 0xFF
|
||||||
elevation = if (backgroundOpacity == 255) elevation else 0f
|
cardElevation = if (backgroundOpacity == 255) actualElevation else 0f
|
||||||
cardElevation = elevation
|
|
||||||
}
|
}
|
||||||
strokeWidth = (it.borderWidth * dp).roundToInt()
|
strokeWidth = (it.borderWidth * dp).roundToInt()
|
||||||
radius = it.radius * dp
|
radius = it.radius * dp
|
||||||
|
|||||||
@ -13,3 +13,11 @@ val LocalAppWidgetHost = compositionLocalOf<AppWidgetHost?>(defaultFactory = { n
|
|||||||
val LocalNavController = compositionLocalOf<NavController?> { null }
|
val LocalNavController = compositionLocalOf<NavController?> { null }
|
||||||
|
|
||||||
val LocalCardStyle = compositionLocalOf<Settings.CardSettings> { Settings.CardSettings.getDefaultInstance() }
|
val LocalCardStyle = compositionLocalOf<Settings.CardSettings> { Settings.CardSettings.getDefaultInstance() }
|
||||||
|
|
||||||
|
val LocalFavoritesEnabled = compositionLocalOf { true }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workaround a bug in Jetpack Compose which incorrectly places popups
|
||||||
|
* that are nested inside other popups.
|
||||||
|
*/
|
||||||
|
val LocalWindowPosition = compositionLocalOf { 0f }
|
||||||
24
ui/src/main/java/de/mm20/launcher2/ui/modifier/Modifiers.kt
Normal file
24
ui/src/main/java/de/mm20/launcher2/ui/modifier/Modifiers.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package de.mm20.launcher2.ui.modifier
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
fun Modifier.scale(
|
||||||
|
scaleX: Float, scaleY: Float, transformOrigin: TransformOrigin
|
||||||
|
) = if (scaleX != 1.0f || scaleY != 1.0f) {
|
||||||
|
graphicsLayer(
|
||||||
|
scaleX = scaleX,
|
||||||
|
scaleY = scaleY,
|
||||||
|
transformOrigin = transformOrigin
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.scale(
|
||||||
|
scale: Float,
|
||||||
|
transformOrigin: TransformOrigin
|
||||||
|
) = scale(scale, scale, transformOrigin)
|
||||||
@ -19,16 +19,15 @@
|
|||||||
android:id="@+id/scrollContainer"
|
android:id="@+id/scrollContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
android:padding="8dp">
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.launcher.search.SearchView
|
<de.mm20.launcher2.ui.launcher.search.SearchView
|
||||||
android:id="@+id/searchContainer"
|
android:id="@+id/searchContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
||||||
@ -38,7 +37,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical" />
|
android:orientation="vertical"
|
||||||
|
android:layout_margin="8dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user