Migrate search UI to Jetpack Compose
This commit is contained in:
parent
b997a7cb62
commit
665664df70
@ -1,15 +1,10 @@
|
||||
package de.mm20.launcher2.badges
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.badges.providers.*
|
||||
import de.mm20.launcher2.badges.providers.BadgeProvider
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
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 {
|
||||
badgeProviders.collectLatest { providers ->
|
||||
if (providers.isEmpty()) {
|
||||
send(null)
|
||||
return@collectLatest
|
||||
}
|
||||
combine(providers.map { it.getBadge(badgeKey) }) { badges ->
|
||||
if (badges.all { it == null }) {
|
||||
return@combine null
|
||||
withContext(Dispatchers.Default) {
|
||||
badgeProviders.collectLatest { providers ->
|
||||
if (providers.isEmpty()) {
|
||||
send(null)
|
||||
return@collectLatest
|
||||
}
|
||||
val badge = Badge()
|
||||
var progresses = 0
|
||||
badges.filterNotNull().forEach {
|
||||
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
|
||||
combine(providers.map { it.getBadge(badgeKey) }) { badges ->
|
||||
if (badges.all { it == null }) {
|
||||
return@combine null
|
||||
}
|
||||
val badge = Badge()
|
||||
var progresses = 0
|
||||
badges.filterNotNull().forEach {
|
||||
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 ->
|
||||
badge.progress?.let {
|
||||
b -> badge.progress = a + b
|
||||
} ?: run {
|
||||
badge.progress = a
|
||||
}
|
||||
progresses++
|
||||
if (progresses > 0) {
|
||||
badge.progress?.let { badge.progress = it / 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="colorSurfaceInverse">@android:color/system_neutral2_800</item>
|
||||
<item name="colorOnSurfaceInverse">@android:color/system_neutral2_50</item>
|
||||
<item name="elevationOverlayEnabled">false</item>
|
||||
<item name="elevationOverlayEnabled">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@ -56,6 +56,6 @@
|
||||
<item name="colorOnSurfaceVariant">@android:color/black</item>
|
||||
<item name="colorSurfaceInverse">@android:color/black</item>
|
||||
<item name="colorOnSurfaceInverse">@android:color/white</item>
|
||||
<item name="elevationOverlayEnabled">false</item>
|
||||
<item name="elevationOverlayEnabled">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@ -1,6 +1,5 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -10,7 +9,6 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.provider.CalendarContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.database.getStringOrNull
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.green
|
||||
@ -18,14 +16,8 @@ import androidx.core.graphics.red
|
||||
import de.mm20.launcher2.calendar.R
|
||||
import de.mm20.launcher2.graphics.TextDrawable
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.checkPermission
|
||||
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.util.*
|
||||
|
||||
class CalendarEvent(
|
||||
override val label: String,
|
||||
@ -53,19 +45,19 @@ class CalendarEvent(
|
||||
TextDrawable(
|
||||
day,
|
||||
color = Color.WHITE,
|
||||
fontSize = 40 * context.dp,
|
||||
fontSize = 20 * context.dp,
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
),
|
||||
TextDrawable(
|
||||
month,
|
||||
color = Color.WHITE,
|
||||
fontSize = 26 * context.dp,
|
||||
fontSize = 13 * context.dp,
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
)
|
||||
)
|
||||
val foreground = LayerDrawable(fgLayers)
|
||||
foreground.setLayerInset(0, 0, 0, 0, (26 * context.dp).toInt())
|
||||
foreground.setLayerInset(1, 0, (40 * context.dp).toInt(), 0, 0)
|
||||
foreground.setLayerInset(0, 0, 0, 0, (13 * context.dp).toInt())
|
||||
foreground.setLayerInset(1, 0, (20 * context.dp).toInt(), 0, 0)
|
||||
val background = ColorDrawable(getDisplayColor(context, color))
|
||||
return LauncherIcon(
|
||||
foreground = foreground,
|
||||
@ -74,8 +66,9 @@ class CalendarEvent(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getLaunchIntent(context: Context): Intent? {
|
||||
return null
|
||||
override fun getLaunchIntent(context: Context): Intent {
|
||||
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
|
||||
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
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.ktx.asBitmap
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import java.net.URLEncoder
|
||||
|
||||
class Contact(
|
||||
val id: Long,
|
||||
@ -29,11 +27,11 @@ class Contact(
|
||||
val lastName: String,
|
||||
val displayName: String,
|
||||
val lookupKey: String,
|
||||
val phones: Set<String>,
|
||||
val emails: Set<String>,
|
||||
val telegram: Set<String>,
|
||||
val whatsapp: Set<String>,
|
||||
val postals: Set<String>
|
||||
val phones: Set<ContactInfo>,
|
||||
val emails: Set<ContactInfo>,
|
||||
val telegram: Set<ContactInfo>,
|
||||
val whatsapp: Set<ContactInfo>,
|
||||
val postals: Set<ContactInfo>,
|
||||
) : Searchable() {
|
||||
override val key: String
|
||||
get() = "contact://$id"
|
||||
@ -42,7 +40,7 @@ class Contact(
|
||||
|
||||
val summary: String
|
||||
get() {
|
||||
return phones.union(emails).joinToString(separator = ", ")
|
||||
return phones.union(emails).joinToString(separator = ", ") { it.label }
|
||||
}
|
||||
|
||||
override fun getPlaceholderIcon(context: Context): LauncherIcon {
|
||||
@ -52,17 +50,22 @@ class Contact(
|
||||
foreground = TextDrawable(
|
||||
iconText,
|
||||
Color.WHITE,
|
||||
fontSize = 40 * context.sp,
|
||||
fontSize = 20 * context.sp,
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
),
|
||||
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 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)
|
||||
?.asBitmap()
|
||||
} ?: return null
|
||||
@ -73,10 +76,13 @@ class Contact(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getLaunchIntent(context: Context): Intent? {
|
||||
return null
|
||||
override fun getLaunchIntent(context: Context): Intent {
|
||||
val uri =
|
||||
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id)
|
||||
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
|
||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||
@ -92,11 +98,11 @@ class Contact(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
null, s, null, null
|
||||
) ?: return null
|
||||
val phones = mutableSetOf<String>()
|
||||
val emails = mutableSetOf<String>()
|
||||
val telegram = mutableSetOf<String>()
|
||||
val whatsapp = mutableSetOf<String>()
|
||||
val postals = mutableSetOf<String>()
|
||||
val phones = mutableSetOf<ContactInfo>()
|
||||
val emails = mutableSetOf<ContactInfo>()
|
||||
val telegram = mutableSetOf<ContactInfo>()
|
||||
val whatsapp = mutableSetOf<ContactInfo>()
|
||||
val postals = mutableSetOf<ContactInfo>()
|
||||
var firstName = ""
|
||||
var lastName = ""
|
||||
var displayName = ""
|
||||
@ -119,13 +125,28 @@ class Contact(
|
||||
loop@ while (dataCursor.moveToNext()) {
|
||||
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
||||
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 ->
|
||||
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 ->
|
||||
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 -> {
|
||||
firstName = dataCursor.getStringOrNull(givenNameColumn) ?: ""
|
||||
lastName = dataCursor.getStringOrNull(familyNameColumn) ?: ""
|
||||
@ -136,13 +157,23 @@ class Contact(
|
||||
?: continue@loop
|
||||
val data3 = dataCursor.getStringOrNull(data3Column)
|
||||
?: continue@loop
|
||||
telegram.add("$data1$$data3")
|
||||
telegram.add(
|
||||
ContactInfo(data3.substringAfterLast(" "), "tg:openmessage?user_id=$data1")
|
||||
)
|
||||
}
|
||||
"vnd.android.cursor.item/vnd.com.whatsapp.profile" -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
whatsapp.add("$dataId$+${data1.substringBefore('@')}")
|
||||
whatsapp.add(
|
||||
ContactInfo(
|
||||
"+${data1.substringBefore('@')}",
|
||||
Uri.withAppendedPath(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataId.toString()
|
||||
).toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -176,4 +207,9 @@ 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 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.nextcloud.NextcloudApiHelper
|
||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.data.*
|
||||
import kotlinx.coroutines.*
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface FileRepository {
|
||||
fun search(query: String): Flow<List<File>>
|
||||
@ -70,28 +69,16 @@ internal class FileRepositoryImpl(
|
||||
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 ->
|
||||
if (providers.isEmpty()) {
|
||||
send(emptyList())
|
||||
return@collectLatest
|
||||
}
|
||||
hiddenItems.collectLatest { hiddenItems ->
|
||||
if (providers.first() is LocalFileProvider) {
|
||||
val localFiles = providers.first().takeIf { it is LocalFileProvider }?.search(query) ?: emptyList()
|
||||
delay(300)
|
||||
if (providers.size > 1) {
|
||||
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)
|
||||
val results = mutableListOf<File>()
|
||||
for (provider in providers) {
|
||||
results.addAll(provider.search(query))
|
||||
send(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
<string name="menu_uninstall">Deinstallieren</string>
|
||||
<string name="menu_share">Teilen</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="wallpaper">Hintergrundbild</string>
|
||||
<string name="title_activity_settings">Einstellungen</string>
|
||||
@ -74,22 +75,22 @@
|
||||
<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_file">Die Datei %1$s wird unwideruflich gelöscht. Fortfahren?</string>
|
||||
<string name="file_meta_title">Titel</string>
|
||||
<string name="file_meta_artist">Künstler</string>
|
||||
<string name="file_meta_album">Album</string>
|
||||
<string name="file_meta_duration">Länge</string>
|
||||
<string name="file_meta_year">Jahr</string>
|
||||
<string name="file_meta_data_entry">%1$s: %2$s</string>
|
||||
<string name="file_meta_size">Größe</string>
|
||||
<string name="file_meta_path">Pfad</string>
|
||||
<string name="file_meta_type">Typ</string>
|
||||
<string name="file_meta_dimensions">Abmessungen</string>
|
||||
<string name="file_meta_app_name">App-Name</string>
|
||||
<string name="file_meta_app_version">Version</string>
|
||||
<string name="file_meta_app_pkgname">Paketname</string>
|
||||
<string name="file_meta_app_min_sdk">Min-SDK-Version</string>
|
||||
<string name="file_meta_title">Titel: %1$s</string>
|
||||
<string name="file_meta_artist">Künstler: %1$s</string>
|
||||
<string name="file_meta_album">Album: %1$s</string>
|
||||
<string name="file_meta_duration">Länge: %1$s</string>
|
||||
<string name="file_meta_year">Jahr: %1$s</string>
|
||||
<string name="file_meta_size">Größe: %1$s</string>
|
||||
<string name="file_meta_path">Pfad: %1$s</string>
|
||||
<string name="file_meta_type">Typ: %1$s</string>
|
||||
<string name="file_meta_dimensions">Abmessungen: %1$s</string>
|
||||
<string name="file_meta_app_name">App-Name: %1$s</string>
|
||||
<string name="file_meta_app_version">Version: %1$s</string>
|
||||
<string name="file_meta_app_pkgname">Paketname: %1$s</string>
|
||||
<string name="file_meta_app_min_sdk">Min-SDK-Version: %1$s</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="file_meta_location">Ort</string>
|
||||
<string name="websearch_google">Google</string>
|
||||
<string name="websearch_youtube">YouTube</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_postals">%1$d Postadressen</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_category_services_google">Google</string>
|
||||
<string name="preference_signin_user">Angemeldet als %1$s</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_ebook">E-Book</string>
|
||||
<string name="file_type_form">Formular</string>
|
||||
|
||||
@ -108,22 +108,22 @@
|
||||
<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_file">Le fichier %1$s va être définitivement supprimé. Procéder ?</string>
|
||||
<string name="file_meta_title">Titre</string>
|
||||
<string name="file_meta_album">Album</string>
|
||||
<string name="file_meta_duration">Durée</string>
|
||||
<string name="file_meta_year">Année</string>
|
||||
<string name="file_meta_data_entry">%1$s: %2$s</string>
|
||||
<string name="file_meta_size">Taille</string>
|
||||
<string name="file_meta_path">Chemin d\'accès</string>
|
||||
<string name="file_meta_type">Type</string>
|
||||
<string name="file_meta_dimensions">Dimensions</string>
|
||||
<string name="file_meta_app_name">Nom de l\'application</string>
|
||||
<string name="file_meta_app_version">Version</string>
|
||||
<string name="file_meta_app_pkgname">Nom du paquet</string>
|
||||
<string name="file_meta_app_min_sdk">Version min du SDK</string>
|
||||
<string name="file_meta_title">Titre: %1$s</string>
|
||||
<string name="file_meta_album">Album: %1$s</string>
|
||||
<string name="file_meta_duration">Durée: %1$s</string>
|
||||
<string name="file_meta_year">Année: %1$s</string>
|
||||
<string name="file_meta_size">Taille: %1$s</string>
|
||||
<string name="file_meta_path">Chemin d\'accès: %1$s</string>
|
||||
<string name="file_meta_type">Type: %1$s</string>
|
||||
<string name="file_meta_dimensions">Dimensions: %1$s</string>
|
||||
<string name="file_meta_app_name">Nom de l\'application: %1$s</string>
|
||||
<string name="file_meta_app_version">Version: %1$s</string>
|
||||
<string name="file_meta_app_pkgname">Nom du paquet: %1$s</string>
|
||||
<string name="file_meta_app_min_sdk">Version min du SDK: %1$s</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_theme_system">Suivre le système</string>
|
||||
<string name="file_meta_location">Position</string>
|
||||
<string name="websearch_google">Google</string>
|
||||
<string name="websearch_youtube">YouTube</string>
|
||||
<string name="websearch_playstore">Google Play</string>
|
||||
@ -166,7 +166,6 @@
|
||||
<string name="preference_category_services_google">Google</string>
|
||||
<string name="preference_signin_user">Connecté en tant que %1$s</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="file_type_ebook">E-book</string>
|
||||
<string name="file_type_drawing">Dessin</string>
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
<string name="menu_share">Share</string>
|
||||
<!-- %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">Version %1$s</string>
|
||||
<!-- Menu entry for launcher settings -->
|
||||
<string name="settings">Settings</string>
|
||||
<!-- Set the device's wallpaper -->
|
||||
@ -108,23 +109,23 @@
|
||||
<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_file">The file %1$s will be deleted permanently. Proceed?</string>
|
||||
<string name="file_meta_title">Title</string>
|
||||
<string name="file_meta_artist">Artist</string>
|
||||
<string name="file_meta_album">Album</string>
|
||||
<string name="file_meta_duration">Duration</string>
|
||||
<string name="file_meta_year">Year</string>
|
||||
<string name="file_meta_data_entry">%1$s: %2$s</string>
|
||||
<string name="file_meta_size">Size</string>
|
||||
<string name="file_meta_path">Path</string>
|
||||
<string name="file_meta_type">Type</string>
|
||||
<string name="file_meta_dimensions">Dimensions</string>
|
||||
<string name="file_meta_app_name">App name</string>
|
||||
<string name="file_meta_app_version">Version</string>
|
||||
<string name="file_meta_app_pkgname">Package name</string>
|
||||
<string name="file_meta_app_min_sdk">Min SDK version</string>
|
||||
<string name="file_meta_title">Title: %1$s</string>
|
||||
<string name="file_meta_artist">Artist: %1$s</string>
|
||||
<string name="file_meta_album">Album: %1$s</string>
|
||||
<string name="file_meta_duration">Duration: %1$s</string>
|
||||
<string name="file_meta_year">Year: %1$s</string>
|
||||
<string name="file_meta_size">Size: %1$s</string>
|
||||
<string name="file_meta_path">Path: %1$s</string>
|
||||
<string name="file_meta_type">Type: %1$s</string>
|
||||
<string name="file_meta_dimensions">Dimensions: %1$s</string>
|
||||
<string name="file_meta_app_name">App name: %1$s</string>
|
||||
<string name="file_meta_app_version">Version: %1$s</string>
|
||||
<string name="file_meta_app_pkgname">Package name: %1$s</string>
|
||||
<string name="file_meta_app_min_sdk">Min SDK version: %1$s</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_theme_system">Follow system</string>
|
||||
<string name="file_meta_location">Location</string>
|
||||
<string name="websearch_google">Google</string>
|
||||
<string name="websearch_youtube">YouTube</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_postals">%1$d postal addresses</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_category_services_google">Google</string>
|
||||
<string name="preference_signin_user">Signed in as %1$s</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="file_type_ebook">E-book</string>
|
||||
<string name="file_type_drawing">Drawing</string>
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
<activity
|
||||
android:name=".launcher.LauncherActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LauncherTheme"
|
||||
android:windowSoftInputMode="stateHidden">
|
||||
<intent-filter>
|
||||
@ -32,9 +32,9 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".launcher.LauncherActivity"
|
||||
android:screenOrientation="portrait"
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.integerResource
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.ktx.toDp
|
||||
import de.mm20.launcher2.ui.locals.LocalWindowPosition
|
||||
import kotlin.math.min
|
||||
|
||||
@Composable
|
||||
@ -39,13 +42,19 @@ fun Icons(actions: List<ToolbarAction>, slots: Int) {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(Icons.Rounded.MoreVert, contentDescription = "")
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.animateContentSize()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.offset(0.dp, LocalWindowPosition.current.toDp())
|
||||
) {
|
||||
OverflowMenuItems(items = actions.subList(slots - 1, actions.size)) {
|
||||
showMenu = false
|
||||
DropdownMenu(
|
||||
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)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.animateContentSize()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.offset(0.dp, LocalWindowPosition.current.toDp())
|
||||
) {
|
||||
OverflowMenuItems(items = action.children) {
|
||||
showMenu = false
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.animateContentSize()
|
||||
) {
|
||||
OverflowMenuItems(items = action.children) {
|
||||
showMenu = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -806,4 +806,54 @@ val Icons.Rounded.Wikipedia
|
||||
curveTo(15.4f, 18.95f, 14.97f, 18.95f, 14.97f, 18.95f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.WhatsApp
|
||||
get() = materialIcon("Icons.Rounded.WhatsApp") {
|
||||
materialPath {
|
||||
moveTo(12.04f, 2f)
|
||||
curveTo(6.58f, 2f, 2.13f, 6.45f, 2.13f, 11.91f)
|
||||
curveTo(2.13f, 13.66f, 2.59f, 15.36f, 3.45f, 16.86f)
|
||||
lineTo(2.05f, 22f)
|
||||
lineTo(7.3f, 20.62f)
|
||||
curveTo(8.75f, 21.41f, 10.38f, 21.83f, 12.04f, 21.83f)
|
||||
curveTo(17.5f, 21.83f, 21.95f, 17.38f, 21.95f, 11.92f)
|
||||
curveTo(21.95f, 9.27f, 20.92f, 6.78f, 19.05f, 4.91f)
|
||||
curveTo(17.18f, 3.03f, 14.69f, 2f, 12.04f, 2f)
|
||||
moveTo(12.05f, 3.67f)
|
||||
curveTo(14.25f, 3.67f, 16.31f, 4.53f, 17.87f, 6.09f)
|
||||
curveTo(19.42f, 7.65f, 20.28f, 9.72f, 20.28f, 11.92f)
|
||||
curveTo(20.28f, 16.46f, 16.58f, 20.15f, 12.04f, 20.15f)
|
||||
curveTo(10.56f, 20.15f, 9.11f, 19.76f, 7.85f, 19f)
|
||||
lineTo(7.55f, 18.83f)
|
||||
lineTo(4.43f, 19.65f)
|
||||
lineTo(5.26f, 16.61f)
|
||||
lineTo(5.06f, 16.29f)
|
||||
curveTo(4.24f, 15f, 3.8f, 13.47f, 3.8f, 11.91f)
|
||||
curveTo(3.81f, 7.37f, 7.5f, 3.67f, 12.05f, 3.67f)
|
||||
moveTo(8.53f, 7.33f)
|
||||
curveTo(8.37f, 7.33f, 8.1f, 7.39f, 7.87f, 7.64f)
|
||||
curveTo(7.65f, 7.89f, 7f, 8.5f, 7f, 9.71f)
|
||||
curveTo(7f, 10.93f, 7.89f, 12.1f, 8f, 12.27f)
|
||||
curveTo(8.14f, 12.44f, 9.76f, 14.94f, 12.25f, 16f)
|
||||
curveTo(12.84f, 16.27f, 13.3f, 16.42f, 13.66f, 16.53f)
|
||||
curveTo(14.25f, 16.72f, 14.79f, 16.69f, 15.22f, 16.63f)
|
||||
curveTo(15.7f, 16.56f, 16.68f, 16.03f, 16.89f, 15.45f)
|
||||
curveTo(17.1f, 14.87f, 17.1f, 14.38f, 17.04f, 14.27f)
|
||||
curveTo(16.97f, 14.17f, 16.81f, 14.11f, 16.56f, 14f)
|
||||
curveTo(16.31f, 13.86f, 15.09f, 13.26f, 14.87f, 13.18f)
|
||||
curveTo(14.64f, 13.1f, 14.5f, 13.06f, 14.31f, 13.3f)
|
||||
curveTo(14.15f, 13.55f, 13.67f, 14.11f, 13.53f, 14.27f)
|
||||
curveTo(13.38f, 14.44f, 13.24f, 14.46f, 13f, 14.34f)
|
||||
curveTo(12.74f, 14.21f, 11.94f, 13.95f, 11f, 13.11f)
|
||||
curveTo(10.26f, 12.45f, 9.77f, 11.64f, 9.62f, 11.39f)
|
||||
curveTo(9.5f, 11.15f, 9.61f, 11f, 9.73f, 10.89f)
|
||||
curveTo(9.84f, 10.78f, 10f, 10.6f, 10.1f, 10.45f)
|
||||
curveTo(10.23f, 10.31f, 10.27f, 10.2f, 10.35f, 10.04f)
|
||||
curveTo(10.43f, 9.87f, 10.39f, 9.73f, 10.33f, 9.61f)
|
||||
curveTo(10.27f, 9.5f, 9.77f, 8.26f, 9.56f, 7.77f)
|
||||
curveTo(9.36f, 7.29f, 9.16f, 7.35f, 9f, 7.34f)
|
||||
curveTo(8.86f, 7.34f, 8.7f, 7.33f, 8.53f, 7.33f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
@ -108,8 +108,6 @@ class LauncherScaffoldView @JvmOverloads constructor(
|
||||
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0).setDuration(200).start()
|
||||
}
|
||||
|
||||
binding.scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
binding.scrollView.scrollY = viewModel.scrollY
|
||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
||||
|
||||
@ -173,7 +171,6 @@ class LauncherScaffoldView @JvmOverloads constructor(
|
||||
viewModel.setStatusBarColor(colorSurface.data)
|
||||
} else {
|
||||
binding.widgetContainer.layoutTransition = ChangingLayoutTransition()
|
||||
binding.scrollContainer.layoutTransition = ChangingLayoutTransition()
|
||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
||||
|
||||
binding.searchBar.visibility = View.VISIBLE
|
||||
|
||||
@ -1,20 +1,92 @@
|
||||
package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||
import de.mm20.launcher2.ui.databinding.ViewSearchBinding
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
val binding = ViewSearchBinding.inflate(LayoutInflater.from(context), this)
|
||||
) : FrameLayout(context, attrs), KoinComponent {
|
||||
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
layoutTransition = ChangingLayoutTransition()
|
||||
val view = ComposeView(context)
|
||||
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)
|
||||
}
|
||||
|
||||
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.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 {
|
||||
it.setStartCompoundDrawable(R.drawable.ic_call)
|
||||
if (contact.phones.size == 1) {
|
||||
it.text = contact.phones.first()
|
||||
it.text = contact.phones.first().label
|
||||
it.setOnClickListener {
|
||||
call(rootView, contact.phones.first())
|
||||
call(rootView, contact.phones.first().data)
|
||||
}
|
||||
} else {
|
||||
it.text =
|
||||
@ -146,10 +146,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
val menu = PopupMenu(context, it, Gravity.START)
|
||||
val phones = contact.phones.toList()
|
||||
for ((i, phone) in phones.withIndex()) {
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone)
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.label)
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
call(rootView, phones[it.itemId])
|
||||
call(rootView, phones[it.itemId].data)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
@ -164,9 +164,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||
it.setStartCompoundDrawable(R.drawable.ic_message)
|
||||
if (contact.phones.size == 1) {
|
||||
it.text = contact.phones.first()
|
||||
it.text = contact.phones.first().label
|
||||
it.setOnClickListener {
|
||||
message(rootView, contact.phones.first())
|
||||
message(rootView, contact.phones.first().data)
|
||||
}
|
||||
} else {
|
||||
it.text = context.getString(
|
||||
@ -177,10 +177,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
val menu = PopupMenu(context, it, Gravity.START)
|
||||
val phones = contact.phones.toList()
|
||||
for ((i, phone) in phones.withIndex()) {
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone)
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.label)
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
message(rootView, phones[it.itemId])
|
||||
message(rootView, phones[it.itemId].data)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
@ -195,9 +195,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||
it.setStartCompoundDrawable(R.drawable.ic_mail)
|
||||
if (contact.emails.size == 1) {
|
||||
it.text = contact.emails.first()
|
||||
it.text = contact.emails.first().label
|
||||
it.setOnClickListener {
|
||||
email(rootView, contact.emails.first())
|
||||
email(rootView, contact.emails.first().data)
|
||||
}
|
||||
} else {
|
||||
it.text =
|
||||
@ -206,10 +206,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
val menu = PopupMenu(context, it, Gravity.START)
|
||||
val emails = contact.emails.toList()
|
||||
for ((i, email) in emails.withIndex()) {
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, email)
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, email.label)
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
email(rootView, emails[it.itemId])
|
||||
email(rootView, emails[it.itemId].data)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
@ -224,9 +224,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||
it.setStartCompoundDrawable(R.drawable.ic_telegram)
|
||||
if (contact.telegram.size == 1) {
|
||||
it.text = contact.telegram.first().substringAfter('$')
|
||||
it.text = contact.telegram.first().label
|
||||
it.setOnClickListener {
|
||||
telegram(rootView, contact.telegram.first().substringBefore('$'))
|
||||
telegram(rootView, contact.telegram.first().data)
|
||||
}
|
||||
} else {
|
||||
it.text = context.getString(
|
||||
@ -237,10 +237,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
val menu = PopupMenu(context, it, Gravity.START)
|
||||
val phones = contact.telegram.toList()
|
||||
for ((i, phone) in phones.withIndex()) {
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.substringAfter('$'))
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.label)
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
telegram(rootView, phones[it.itemId].substringBefore('$'))
|
||||
telegram(rootView, phones[it.itemId].data)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
@ -254,9 +254,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||
it.setStartCompoundDrawable(R.drawable.ic_whatsapp)
|
||||
if (contact.whatsapp.size == 1) {
|
||||
it.text = contact.whatsapp.first().substringAfter("$")
|
||||
it.text = contact.whatsapp.first().label
|
||||
it.setOnClickListener {
|
||||
whatsapp(rootView, contact.whatsapp.first().substringBefore("$"))
|
||||
whatsapp(rootView, contact.whatsapp.first().data)
|
||||
}
|
||||
} else {
|
||||
it.text = context.getString(
|
||||
@ -267,10 +267,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
val menu = PopupMenu(context, it, Gravity.START)
|
||||
val phones = contact.whatsapp.toList()
|
||||
for ((i, phone) in phones.withIndex()) {
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.substringAfter('$'))
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, phone.label)
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
whatsapp(rootView, phones[it.itemId].substringBefore('$'))
|
||||
whatsapp(rootView, phones[it.itemId].data)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
@ -284,9 +284,9 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
(View.inflate(context, R.layout.view_list_item, null) as TextView).also {
|
||||
it.setStartCompoundDrawable(R.drawable.ic_location)
|
||||
if (contact.postals.size == 1) {
|
||||
it.text = contact.postals.first()
|
||||
it.text = contact.postals.first().label
|
||||
it.setOnClickListener {
|
||||
navigate(rootView, contact.postals.first())
|
||||
navigate(rootView, contact.postals.first().data)
|
||||
}
|
||||
} else {
|
||||
it.text = context.getString(
|
||||
@ -297,10 +297,10 @@ class ContactDetailRepresentation : Representation, KoinComponent {
|
||||
val menu = PopupMenu(context, it, Gravity.START)
|
||||
val postals = contact.postals.toList()
|
||||
for ((i, postal) in postals.withIndex()) {
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, postal)
|
||||
menu.menu.add(Menu.NONE, i, Menu.NONE, postal.label)
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
navigate(rootView, postals[it.itemId])
|
||||
navigate(rootView, postals[it.itemId].data)
|
||||
true
|
||||
}
|
||||
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
|
||||
try {
|
||||
val callIntent = Intent(Intent.ACTION_DIAL)
|
||||
callIntent.data = Uri.parse("tel:$number")
|
||||
callIntent.data = Uri.parse(data)
|
||||
ActivityStarter.start(context, rootView, intent = callIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun message(rootView: SearchableView, number: String) {
|
||||
private fun message(rootView: SearchableView, data: String) {
|
||||
val context = rootView.context
|
||||
try {
|
||||
val messageIntent = Intent(Intent.ACTION_VIEW)
|
||||
messageIntent.data = Uri.parse("sms:$number")
|
||||
messageIntent.data = Uri.parse(data)
|
||||
ActivityStarter.start(context, rootView, intent = messageIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun email(rootView: SearchableView, address: String) {
|
||||
private fun email(rootView: SearchableView, data: String) {
|
||||
val context = rootView.context
|
||||
try {
|
||||
val mailIntent = Intent(Intent.ACTION_VIEW)
|
||||
mailIntent.data = Uri.parse("mailto:$address")
|
||||
mailIntent.data = Uri.parse(data)
|
||||
ActivityStarter.start(context, rootView, intent = mailIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun whatsapp(rootView: SearchableView, number: String) {
|
||||
private fun whatsapp(rootView: SearchableView, data: String) {
|
||||
val context = rootView.context
|
||||
try {
|
||||
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)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.activity_not_found, Toast.LENGTH_SHORT).show()
|
||||
|
||||
@ -162,21 +162,19 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
||||
|
||||
sb.append(
|
||||
context.getString(
|
||||
R.string.file_meta_data_entry,
|
||||
context.getString(R.string.file_meta_type),
|
||||
R.string.file_meta_type,
|
||||
file.mimeType
|
||||
)
|
||||
)
|
||||
|
||||
for ((k, v) in file.metaData) {
|
||||
sb.append("\n")
|
||||
.append(context.getString(R.string.file_meta_data_entry, context.getString(k), v))
|
||||
.append(context.getString(k, v))
|
||||
}
|
||||
if (!file.isDirectory) {
|
||||
sb.append("\n").append(
|
||||
context.getString(
|
||||
R.string.file_meta_data_entry,
|
||||
context.getString(R.string.file_meta_size),
|
||||
R.string.file_meta_size,
|
||||
formatFileSize(file.size)
|
||||
)
|
||||
)
|
||||
@ -184,8 +182,7 @@ class FileDetailRepresentation : Representation, KoinComponent {
|
||||
if (file.path.isNotEmpty()) {
|
||||
sb.append("\n").append(
|
||||
context.getString(
|
||||
R.string.file_meta_data_entry,
|
||||
context.getString(R.string.file_meta_path),
|
||||
R.string.file_meta_path,
|
||||
file.path
|
||||
)
|
||||
)
|
||||
|
||||
@ -50,6 +50,8 @@ open class LauncherCardView @JvmOverloads constructor(
|
||||
|
||||
private var overrideBackgroundOpacity = false
|
||||
|
||||
private var actualElevation: Float
|
||||
|
||||
init {
|
||||
strokeColor = cardBackgroundColor.defaultColor
|
||||
strokeWidth = (currentCardStyle.borderWidth * dp).roundToInt()
|
||||
@ -71,8 +73,8 @@ open class LauncherCardView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
strokeOpacity = if (backgroundOpacity == 0) 0 else 0xFF
|
||||
elevation = if (backgroundOpacity == 255) elevation else 0f
|
||||
cardElevation = elevation
|
||||
actualElevation = context.resources.getDimension(R.dimen.card_elevation)
|
||||
cardElevation = if (backgroundOpacity == 255) actualElevation else 0f
|
||||
}
|
||||
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
@ -88,8 +90,7 @@ open class LauncherCardView @JvmOverloads constructor(
|
||||
if (!overrideBackgroundOpacity) {
|
||||
backgroundOpacity = (it.opacity * 255).roundToInt()
|
||||
strokeOpacity = if (backgroundOpacity == 0) 0 else 0xFF
|
||||
elevation = if (backgroundOpacity == 255) elevation else 0f
|
||||
cardElevation = elevation
|
||||
cardElevation = if (backgroundOpacity == 255) actualElevation else 0f
|
||||
}
|
||||
strokeWidth = (it.borderWidth * dp).roundToInt()
|
||||
radius = it.radius * dp
|
||||
|
||||
@ -12,4 +12,12 @@ val LocalAppWidgetHost = compositionLocalOf<AppWidgetHost?>(defaultFactory = { n
|
||||
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<de.mm20.launcher2.ui.launcher.search.SearchView
|
||||
android:id="@+id/searchContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
@ -38,7 +37,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical" />
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="8dp" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user