Reorganize Searchable data types
This commit is contained in:
parent
a01b0aa03d
commit
bcda89c211
@ -45,7 +45,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.commons.text)
|
implementation(libs.commons.text)
|
||||||
|
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
|||||||
@ -12,17 +12,18 @@ import android.os.Process
|
|||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.mm20.launcher2.ktx.normalize
|
import de.mm20.launcher2.ktx.normalize
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.apache.commons.text.similarity.FuzzyScore
|
import org.apache.commons.text.similarity.FuzzyScore
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
interface AppRepository {
|
interface AppRepository: SearchableRepository<LauncherApp> {
|
||||||
fun search(query: String): Flow<List<Application>>
|
fun getAllInstalledApps(): Flow<List<LauncherApp>>
|
||||||
fun getAllInstalledApps(): Flow<List<Application>>
|
|
||||||
fun getSuspendedPackages(): Flow<List<String>>
|
fun getSuspendedPackages(): Flow<List<String>>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,11 +141,11 @@ internal class AppRepositoryImpl(
|
|||||||
return LauncherApp(context, launcherActivityInfo)
|
return LauncherApp(context, launcherActivityInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Flow<List<Application>> = channelFlow {
|
override fun search(query: String): Flow<ImmutableList<LauncherApp>> = channelFlow {
|
||||||
|
|
||||||
installedApps.collectLatest { apps ->
|
installedApps.collectLatest { apps ->
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val appResults = mutableListOf<Application>()
|
val appResults = mutableListOf<LauncherApp>()
|
||||||
if (query.isEmpty()) {
|
if (query.isEmpty()) {
|
||||||
appResults.addAll(apps)
|
appResults.addAll(apps)
|
||||||
} else {
|
} else {
|
||||||
@ -158,12 +159,12 @@ internal class AppRepositoryImpl(
|
|||||||
|
|
||||||
appResults.sort()
|
appResults.sort()
|
||||||
|
|
||||||
send(appResults)
|
send(appResults.toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllInstalledApps(): Flow<List<Application>> {
|
override fun getAllInstalledApps(): Flow<List<LauncherApp>> {
|
||||||
return installedApps
|
return installedApps
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +175,7 @@ internal class AppRepositoryImpl(
|
|||||||
fuzzyScore.fuzzyScore(normalizedLabel, query.normalize()) >= query.length * 1.5
|
fuzzyScore.fuzzyScore(normalizedLabel, query.normalize()) >= query.length * 1.5
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getActivityByComponentName(componentName: ComponentName?): Application? {
|
private fun getActivityByComponentName(componentName: ComponentName?): LauncherApp? {
|
||||||
componentName ?: return null
|
componentName ?: return null
|
||||||
val intent = Intent().setComponent(componentName)
|
val intent = Intent().setComponent(componentName)
|
||||||
val lai = launcherApps.resolveActivity(intent, Process.myUserHandle())
|
val lai = launcherApps.resolveActivity(intent, Process.myUserHandle())
|
||||||
|
|||||||
@ -1,73 +0,0 @@
|
|||||||
package de.mm20.launcher2.search.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.graphics.ColorMatrix
|
|
||||||
import android.graphics.ColorMatrixColorFilter
|
|
||||||
import android.graphics.drawable.BitmapDrawable
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import de.mm20.launcher2.applications.R
|
|
||||||
import de.mm20.launcher2.icons.*
|
|
||||||
|
|
||||||
class AppInstallation(
|
|
||||||
val session: PackageInstaller.SessionInfo
|
|
||||||
) : Application(
|
|
||||||
label = session.appLabel?.toString() ?: "",
|
|
||||||
`package` = session.appPackageName ?: "",
|
|
||||||
activity = "",
|
|
||||||
flags = 0,
|
|
||||||
version = null
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val key: String
|
|
||||||
get() = "installer://${session.installerPackageName}:${session.appPackageName}"
|
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
|
||||||
return session.createDetailsIntent()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
|
||||||
return StaticLauncherIcon(
|
|
||||||
foregroundLayer = TintedIconLayer(
|
|
||||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
|
|
||||||
scale = 0.5f,
|
|
||||||
color = 0xFF757575.toInt()
|
|
||||||
),
|
|
||||||
backgroundLayer = ColorLayer(0xFF757575.toInt())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun loadIcon(
|
|
||||||
context: Context,
|
|
||||||
size: Int,
|
|
||||||
themed: Boolean,
|
|
||||||
): LauncherIcon {
|
|
||||||
val icon = session.appIcon ?: return getPlaceholderIcon(context)
|
|
||||||
val foreground = BitmapDrawable(context.resources, icon)
|
|
||||||
foreground.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
|
|
||||||
setSaturation(0f)
|
|
||||||
})
|
|
||||||
return StaticLauncherIcon(
|
|
||||||
foregroundLayer = StaticIconLayer(
|
|
||||||
icon = foreground,
|
|
||||||
),
|
|
||||||
backgroundLayer = ColorLayer(0xFF757575.toInt())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStoreDetails(context: Context): StoreLink? {
|
|
||||||
return getStoreLinkForInstaller(session.installerPackageName, `package`)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun search(context: Context): List<AppInstallation> {
|
|
||||||
val installer = context.packageManager.packageInstaller
|
|
||||||
val sessions = installer.allSessions
|
|
||||||
val results = sessions.mapNotNull {
|
|
||||||
if (it.appLabel != null && it.isActive) AppInstallation(it) else null
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,20 +4,17 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
|
|
||||||
class LauncherAppSerializer : SearchableSerializer {
|
class LauncherAppSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as LauncherApp
|
searchable as LauncherApp
|
||||||
val json = JSONObject()
|
val json = JSONObject()
|
||||||
json.put("package", searchable.`package`)
|
json.put("package", searchable.`package`)
|
||||||
@ -31,7 +28,7 @@ class LauncherAppSerializer : SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
|
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable? {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||||
val userManager = context.getSystemService<UserManager>()!!
|
val userManager = context.getSystemService<UserManager>()!!
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
package de.mm20.launcher2.search.data
|
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Color
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import de.mm20.launcher2.applications.R
|
|
||||||
import de.mm20.launcher2.compat.PackageManagerCompat
|
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
|
||||||
import de.mm20.launcher2.icons.TintedIconLayer
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
abstract class Application(
|
|
||||||
override val label: String,
|
|
||||||
val `package`: String,
|
|
||||||
val activity: String,
|
|
||||||
val flags: Int,
|
|
||||||
val version: String?,
|
|
||||||
) : Searchable() {
|
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
val json = JSONObject()
|
|
||||||
json.put("package", `package`)
|
|
||||||
json.put("activity", activity)
|
|
||||||
return json.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
|
||||||
val intent = Intent()
|
|
||||||
intent.component = ComponentName(`package`, activity)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
|
||||||
return StaticLauncherIcon(
|
|
||||||
foregroundLayer = TintedIconLayer(
|
|
||||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
|
|
||||||
scale = 0.5f,
|
|
||||||
color = 0xff3dda84.toInt(),
|
|
||||||
),
|
|
||||||
backgroundLayer = ColorLayer(0xff3dda84.toInt())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getStoreDetails(context: Context): StoreLink? {
|
|
||||||
val pm = context.packageManager
|
|
||||||
return try {
|
|
||||||
val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`)
|
|
||||||
getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`)
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val key: String
|
|
||||||
get() = "app://$`package`:$activity"
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
internal fun getStoreLinkForInstaller(
|
|
||||||
installerPackage: String?,
|
|
||||||
packageName: String?
|
|
||||||
): StoreLink? {
|
|
||||||
if (packageName == null) return null
|
|
||||||
return when (installerPackage) {
|
|
||||||
"de.amazon.mShop.android", "com.amazon.venezia" -> {
|
|
||||||
StoreLink(
|
|
||||||
"Amazon App Shop",
|
|
||||||
"http://www.amazon.com/gp/mas/dl/android?p=${packageName}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"com.android.vending" -> {
|
|
||||||
StoreLink(
|
|
||||||
"Google Play Store",
|
|
||||||
"https://play.google.com/store/apps/details?id=${packageName}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"org.fdroid.fdroid", "com.aurora.adroid" -> {
|
|
||||||
StoreLink(
|
|
||||||
"F-Droid",
|
|
||||||
"https://f-droid.org/packages/${packageName}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class StoreLink(
|
|
||||||
val label: String,
|
|
||||||
val url: String
|
|
||||||
)
|
|
||||||
@ -10,37 +10,65 @@ import android.graphics.drawable.AdaptiveIconDrawable
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
import de.mm20.launcher2.applications.R
|
||||||
|
import de.mm20.launcher2.compat.PackageManagerCompat
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.ktx.getSerialNumber
|
import de.mm20.launcher2.ktx.getSerialNumber
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
data class LauncherApp(
|
||||||
* An [Application] based on an [android.content.pm.LauncherActivityInfo]
|
val launcherActivityInfo: LauncherActivityInfo,
|
||||||
*/
|
override val label: String,
|
||||||
class LauncherApp(
|
val `package`: String,
|
||||||
context: Context,
|
val activity: String,
|
||||||
val launcherActivityInfo: LauncherActivityInfo
|
val flags: Int,
|
||||||
) : Application(
|
val version: String?,
|
||||||
label = launcherActivityInfo.label.toString(),
|
internal val userSerialNumber: Long,
|
||||||
`package` = launcherActivityInfo.applicationInfo.packageName,
|
override val labelOverride: String? = null,
|
||||||
activity = launcherActivityInfo.name,
|
) : PinnableSearchable {
|
||||||
flags = launcherActivityInfo.applicationInfo.flags,
|
|
||||||
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
|
constructor(context: Context, launcherActivityInfo: LauncherActivityInfo): this(
|
||||||
) {
|
launcherActivityInfo,
|
||||||
|
label = launcherActivityInfo.label.toString(),
|
||||||
|
`package` = launcherActivityInfo.applicationInfo.packageName,
|
||||||
|
activity = launcherActivityInfo.name,
|
||||||
|
flags = launcherActivityInfo.applicationInfo.flags,
|
||||||
|
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
|
||||||
|
userSerialNumber = launcherActivityInfo.user.getSerialNumber(context)
|
||||||
|
)
|
||||||
|
|
||||||
internal val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context)
|
|
||||||
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
override val preferDetailsOverLaunch: Boolean = false
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): LauncherApp {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
override val key: String
|
override val key: String
|
||||||
get() = if (isMainProfile) "app://$`package`:$activity" else "app://$`package`:$activity:${userSerialNumber}"
|
get() = if (isMainProfile) "${domain}://$`package`:$activity" else "${domain}://$`package`:$activity:${userSerialNumber}"
|
||||||
|
|
||||||
fun getUser(): UserHandle? {
|
fun getUser(): UserHandle? {
|
||||||
return launcherActivityInfo.user
|
return launcherActivityInfo.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||||
|
return StaticLauncherIcon(
|
||||||
|
foregroundLayer = TintedIconLayer(
|
||||||
|
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
|
||||||
|
scale = 0.5f,
|
||||||
|
color = 0xff3dda84.toInt(),
|
||||||
|
),
|
||||||
|
backgroundLayer = ColorLayer(0xff3dda84.toInt())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun loadIcon(
|
override suspend fun loadIcon(
|
||||||
context: Context,
|
context: Context,
|
||||||
size: Int,
|
size: Int,
|
||||||
@ -107,7 +135,46 @@ class LauncherApp(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getStoreDetails(context: Context): StoreLink? {
|
||||||
|
val pm = context.packageManager
|
||||||
|
return try {
|
||||||
|
val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`)
|
||||||
|
getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private fun getStoreLinkForInstaller(
|
||||||
|
installerPackage: String?,
|
||||||
|
packageName: String?
|
||||||
|
): StoreLink? {
|
||||||
|
if (packageName == null) return null
|
||||||
|
return when (installerPackage) {
|
||||||
|
"de.amazon.mShop.android", "com.amazon.venezia" -> {
|
||||||
|
StoreLink(
|
||||||
|
"Amazon App Shop",
|
||||||
|
"http://www.amazon.com/gp/mas/dl/android?p=${packageName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"com.android.vending" -> {
|
||||||
|
StoreLink(
|
||||||
|
"Google Play Store",
|
||||||
|
"https://play.google.com/store/apps/details?id=${packageName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"org.fdroid.fdroid", "com.aurora.adroid" -> {
|
||||||
|
StoreLink(
|
||||||
|
"F-Droid",
|
||||||
|
"https://f-droid.org/packages/${packageName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getPackageVersionName(context: Context, packageName: String): String? {
|
fun getPackageVersionName(context: Context, packageName: String): String? {
|
||||||
return try {
|
return try {
|
||||||
@ -116,5 +183,12 @@ class LauncherApp(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val Domain = "app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class StoreLink(
|
||||||
|
val label: String,
|
||||||
|
val url: String
|
||||||
|
)
|
||||||
@ -44,7 +44,6 @@ dependencies {
|
|||||||
implementation(libs.commons.text)
|
implementation(libs.commons.text)
|
||||||
|
|
||||||
implementation(project(":applications"))
|
implementation(project(":applications"))
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":permissions"))
|
implementation(project(":permissions"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
|
|||||||
@ -15,9 +15,13 @@ import de.mm20.launcher2.ktx.normalize
|
|||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -27,7 +31,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.apache.commons.text.similarity.FuzzyScore
|
import org.apache.commons.text.similarity.FuzzyScore
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
interface AppShortcutRepository {
|
interface AppShortcutRepository: SearchableRepository<AppShortcut> {
|
||||||
suspend fun getShortcutsForActivity(
|
suspend fun getShortcutsForActivity(
|
||||||
launcherActivityInfo: LauncherActivityInfo,
|
launcherActivityInfo: LauncherActivityInfo,
|
||||||
count: Int = 5
|
count: Int = 5
|
||||||
@ -35,8 +39,6 @@ interface AppShortcutRepository {
|
|||||||
|
|
||||||
suspend fun getShortcutsConfigActivities(): List<LauncherApp>
|
suspend fun getShortcutsConfigActivities(): List<LauncherApp>
|
||||||
|
|
||||||
fun search(query: String): Flow<List<AppShortcut>>
|
|
||||||
|
|
||||||
fun removePinnedShortcut(shortcut: LauncherShortcut)
|
fun removePinnedShortcut(shortcut: LauncherShortcut)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,32 +74,31 @@ internal class AppShortcutRepositoryImpl(
|
|||||||
LauncherShortcut(
|
LauncherShortcut(
|
||||||
context,
|
context,
|
||||||
it,
|
it,
|
||||||
launcherActivityInfo.label.toString()
|
|
||||||
)
|
)
|
||||||
} ?: emptyList())
|
} ?: emptyList())
|
||||||
appShortcuts
|
appShortcuts
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String) = channelFlow<List<AppShortcut>> {
|
override fun search(query: String) = channelFlow<ImmutableList<AppShortcut>> {
|
||||||
if (query.length < 3) {
|
if (query.length < 3) {
|
||||||
send(emptyList())
|
send(persistentListOf())
|
||||||
return@channelFlow
|
return@channelFlow
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.AppShortcuts)) {
|
if (!permissionsManager.checkPermissionOnce(PermissionGroup.AppShortcuts)) {
|
||||||
send(emptyList())
|
send(persistentListOf())
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
dataStore.data.map { it.appShortcutSearch.enabled }.collectLatest { enabled ->
|
dataStore.data.map { it.appShortcutSearch.enabled }.collectLatest { enabled ->
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
send(emptyList())
|
send(persistentListOf())
|
||||||
return@collectLatest
|
return@collectLatest
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcutChangeEmitter.collectLatest {
|
shortcutChangeEmitter.collectLatest {
|
||||||
val launcherApps =
|
val launcherApps =
|
||||||
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
|
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
|
||||||
emptyList()
|
persistentListOf()
|
||||||
)
|
)
|
||||||
|
|
||||||
val shortcutQuery = LauncherApps.ShortcutQuery()
|
val shortcutQuery = LauncherApps.ShortcutQuery()
|
||||||
@ -124,17 +125,11 @@ internal class AppShortcutRepositoryImpl(
|
|||||||
|
|
||||||
send(
|
send(
|
||||||
shortcuts.mapNotNull {
|
shortcuts.mapNotNull {
|
||||||
val label = try {
|
|
||||||
pm.getApplicationInfo(it.`package`, 0).loadLabel(pm).toString()
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
LauncherShortcut(
|
LauncherShortcut(
|
||||||
context,
|
context,
|
||||||
it,
|
it
|
||||||
label
|
|
||||||
)
|
)
|
||||||
}
|
}.toImmutableList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,22 +5,22 @@ import android.content.Intent
|
|||||||
import android.content.Intent.ShortcutIconResource
|
import android.content.Intent.ShortcutIconResource
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
import de.mm20.launcher2.search.data.LegacyShortcut
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
|
|
||||||
class LauncherShortcutSerializer : SearchableSerializer {
|
class LauncherShortcutSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as LauncherShortcut
|
searchable as LauncherShortcut
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"packagename" to searchable.launcherShortcut.`package`,
|
"packagename" to searchable.launcherShortcut.`package`,
|
||||||
@ -38,7 +38,7 @@ class LauncherShortcutDeserializer(
|
|||||||
val context: Context
|
val context: Context
|
||||||
) : SearchableDeserializer, KoinComponent {
|
) : SearchableDeserializer, KoinComponent {
|
||||||
|
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable? {
|
||||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||||
if (!launcherApps.hasShortcutHostPermission()) return null
|
if (!launcherApps.hasShortcutHostPermission()) return null
|
||||||
else {
|
else {
|
||||||
@ -75,7 +75,6 @@ class LauncherShortcutDeserializer(
|
|||||||
return LauncherShortcut(
|
return LauncherShortcut(
|
||||||
context = context,
|
context = context,
|
||||||
launcherShortcut = shortcuts[0],
|
launcherShortcut = shortcuts[0],
|
||||||
appName = appName
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,7 +82,7 @@ class LauncherShortcutDeserializer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LegacyShortcutSerializer: SearchableSerializer {
|
class LegacyShortcutSerializer: SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String? {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as LegacyShortcut
|
searchable as LegacyShortcut
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"label" to searchable.label,
|
"label" to searchable.label,
|
||||||
@ -104,7 +103,7 @@ class LegacyShortcutSerializer: SearchableSerializer {
|
|||||||
class LegacyShortcutDeserializer(
|
class LegacyShortcutDeserializer(
|
||||||
val context: Context
|
val context: Context
|
||||||
): SearchableDeserializer {
|
): SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
val label = json.getString("label")
|
val label = json.getString("label")
|
||||||
val intent = Intent.parseUri(json.getString("intent"), 0)
|
val intent = Intent.parseUri(json.getString("intent"), 0)
|
||||||
|
|||||||
@ -7,10 +7,15 @@ import de.mm20.launcher2.appshortcuts.R
|
|||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TintedIconLayer
|
import de.mm20.launcher2.icons.TintedIconLayer
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
|
interface AppShortcut: PinnableSearchable {
|
||||||
|
|
||||||
abstract class AppShortcut(
|
|
||||||
val appName: String?
|
val appName: String?
|
||||||
) : Searchable() {
|
|
||||||
|
override val preferDetailsOverLaunch: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||||
return StaticLauncherIcon(
|
return StaticLauncherIcon(
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.ShortcutInfo
|
import android.content.pm.ShortcutInfo
|
||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -14,36 +15,55 @@ import de.mm20.launcher2.appshortcuts.R
|
|||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.ktx.getSerialNumber
|
import de.mm20.launcher2.ktx.getSerialNumber
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a modern (Android O+) launcher shortcut
|
* Represents a modern (Android O+) launcher shortcut
|
||||||
*/
|
*/
|
||||||
class LauncherShortcut(
|
data class LauncherShortcut(
|
||||||
context: Context,
|
|
||||||
val launcherShortcut: ShortcutInfo,
|
val launcherShortcut: ShortcutInfo,
|
||||||
appName: String
|
override val appName: String?,
|
||||||
) : AppShortcut(appName) {
|
internal val userSerialNumber: Long,
|
||||||
|
override val labelOverride: String? = null,
|
||||||
|
) : AppShortcut {
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
launcherShortcut: ShortcutInfo,
|
||||||
|
): this(
|
||||||
|
launcherShortcut = launcherShortcut,
|
||||||
|
appName = try {
|
||||||
|
context.packageManager.getApplicationInfo(launcherShortcut.`package`, 0)
|
||||||
|
.loadLabel(context.packageManager).toString()
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
userSerialNumber = launcherShortcut.userHandle.getSerialNumber(context)
|
||||||
|
)
|
||||||
|
|
||||||
override val label: String
|
override val label: String
|
||||||
get() = launcherShortcut.shortLabel?.toString() ?: ""
|
get() = launcherShortcut.shortLabel?.toString() ?: ""
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): LauncherShortcut {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val preferDetailsOverLaunch: Boolean = false
|
||||||
|
|
||||||
|
|
||||||
internal val userSerialNumber: Long = launcherShortcut.userHandle.getSerialNumber(context)
|
|
||||||
val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
|
val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
|
||||||
|
|
||||||
override val key: String
|
override val key: String
|
||||||
get() = if (isMainProfile) {
|
get() = if (isMainProfile) {
|
||||||
"shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}"
|
"$domain://${launcherShortcut.`package`}/${launcherShortcut.id}"
|
||||||
} else {
|
} else {
|
||||||
"shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}:${userSerialNumber}"
|
"$domain://${launcherShortcut.`package`}/${launcherShortcut.id}:${userSerialNumber}"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
|
||||||
return launcherShortcut.intent
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||||
try {
|
try {
|
||||||
@ -123,10 +143,10 @@ class LauncherShortcut(
|
|||||||
return LauncherShortcut(
|
return LauncherShortcut(
|
||||||
context,
|
context,
|
||||||
shortcutInfo,
|
shortcutInfo,
|
||||||
context.packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
|
|
||||||
.loadLabel(context.packageManager).toString()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val Domain = "shortcut"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4,22 +4,32 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.Intent.ShortcutIconResource
|
import android.content.Intent.ShortcutIconResource
|
||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.ktx.getDrawableOrNull
|
import de.mm20.launcher2.ktx.getDrawableOrNull
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
|
||||||
class LegacyShortcut(
|
data class LegacyShortcut(
|
||||||
val intent: Intent,
|
val intent: Intent,
|
||||||
override val label: String,
|
override val label: String,
|
||||||
appName: String?,
|
override val appName: String?,
|
||||||
val iconResource: ShortcutIconResource?,
|
val iconResource: ShortcutIconResource?,
|
||||||
) : AppShortcut(appName) {
|
override val labelOverride: String? = null,
|
||||||
override val key: String
|
) : AppShortcut {
|
||||||
get() = "legacyshortcut://${intent.toUri(0)}"
|
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent {
|
override val domain = Domain
|
||||||
return intent
|
override val key: String = "$domain://${intent.toUri(0)}"
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): LegacyShortcut {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(intent, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
val packageName: String?
|
val packageName: String?
|
||||||
@ -67,6 +77,9 @@ class LegacyShortcut(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val Domain = "legacyshortcut"
|
||||||
|
|
||||||
fun fromPinRequestIntent(context: Context, data: Intent): LegacyShortcut? {
|
fun fromPinRequestIntent(context: Context, data: Intent): LegacyShortcut? {
|
||||||
val intent: Intent? = data.extras?.getParcelable(Intent.EXTRA_SHORTCUT_INTENT)
|
val intent: Intent? = data.extras?.getParcelable(Intent.EXTRA_SHORTCUT_INTENT)
|
||||||
val name: String? = data.extras?.getString(Intent.EXTRA_SHORTCUT_NAME)
|
val name: String? = data.extras?.getString(Intent.EXTRA_SHORTCUT_NAME)
|
||||||
|
|||||||
@ -41,8 +41,8 @@ dependencies {
|
|||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":favorites"))
|
implementation(project(":favorites"))
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":widgets"))
|
implementation(project(":widgets"))
|
||||||
|
implementation(project(":search"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":customattrs"))
|
implementation(project(":customattrs"))
|
||||||
|
|||||||
@ -14,8 +14,6 @@ import kotlinx.coroutines.*
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|||||||
@ -49,6 +49,5 @@ dependencies {
|
|||||||
implementation(project(":notifications"))
|
implementation(project(":notifications"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":files"))
|
implementation(project(":files"))
|
||||||
}
|
}
|
||||||
@ -3,7 +3,7 @@ package de.mm20.launcher2.badges
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.mm20.launcher2.badges.providers.*
|
import de.mm20.launcher2.badges.providers.*
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import de.mm20.launcher2.badges.Badge
|
|||||||
import de.mm20.launcher2.graphics.BadgeDrawable
|
import de.mm20.launcher2.graphics.BadgeDrawable
|
||||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
import de.mm20.launcher2.search.data.LegacyShortcut
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package de.mm20.launcher2.badges.providers
|
package de.mm20.launcher2.badges.providers
|
||||||
|
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface BadgeProvider {
|
interface BadgeProvider {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package de.mm20.launcher2.badges.providers
|
|||||||
|
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
import de.mm20.launcher2.search.data.File
|
import de.mm20.launcher2.search.data.File
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,8 @@ package de.mm20.launcher2.badges.providers
|
|||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
import de.mm20.launcher2.notifications.NotificationRepository
|
import de.mm20.launcher2.notifications.NotificationRepository
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@ -17,7 +16,7 @@ class NotificationBadgeProvider : BadgeProvider, KoinComponent {
|
|||||||
private val notificationRepository: NotificationRepository by inject()
|
private val notificationRepository: NotificationRepository by inject()
|
||||||
|
|
||||||
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
|
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
|
||||||
if (searchable is Application) {
|
if (searchable is LauncherApp) {
|
||||||
val packageName = searchable.`package`
|
val packageName = searchable.`package`
|
||||||
notificationRepository.notifications.map {
|
notificationRepository.notifications.map {
|
||||||
it.filter { it.packageName == packageName }
|
it.filter { it.packageName == packageName }
|
||||||
|
|||||||
@ -3,8 +3,8 @@ package de.mm20.launcher2.badges.providers
|
|||||||
import de.mm20.launcher2.applications.AppRepository
|
import de.mm20.launcher2.applications.AppRepository
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
import de.mm20.launcher2.badges.R
|
import de.mm20.launcher2.badges.R
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@ -15,7 +15,7 @@ class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent {
|
|||||||
private val appRepository: AppRepository by inject()
|
private val appRepository: AppRepository by inject()
|
||||||
|
|
||||||
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
|
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
|
||||||
if (searchable is Application) {
|
if (searchable is LauncherApp) {
|
||||||
val packageName = searchable.`package`
|
val packageName = searchable.`package`
|
||||||
appRepository.getSuspendedPackages().collectLatest {
|
appRepository.getSuspendedPackages().collectLatest {
|
||||||
if (it.contains(packageName)) {
|
if (it.contains(packageName)) {
|
||||||
|
|||||||
@ -2,10 +2,9 @@ package de.mm20.launcher2.badges.providers
|
|||||||
|
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
import de.mm20.launcher2.badges.R
|
import de.mm20.launcher2.badges.R
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
|
import de.mm20.launcher2.ktx.romanize
|
||||||
|
import java.text.Collator
|
||||||
|
|
||||||
|
interface PinnableSearchable : Searchable, Comparable<PinnableSearchable> {
|
||||||
|
|
||||||
|
val label: String
|
||||||
|
val labelOverride: String?
|
||||||
|
get() = null
|
||||||
|
|
||||||
|
fun overrideLabel(label: String): PinnableSearchable
|
||||||
|
|
||||||
|
fun launch(context: Context, options: Bundle?): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this is true, tapping the item will open the details popup instead of launching it
|
||||||
|
*/
|
||||||
|
val preferDetailsOverLaunch: Boolean
|
||||||
|
|
||||||
|
fun getPlaceholderIcon(context: Context): StaticLauncherIcon
|
||||||
|
|
||||||
|
suspend fun loadIcon(
|
||||||
|
context: Context,
|
||||||
|
size: Int,
|
||||||
|
themed: Boolean
|
||||||
|
): LauncherIcon? = null
|
||||||
|
|
||||||
|
|
||||||
|
override fun compareTo(other: PinnableSearchable): Int {
|
||||||
|
val label1 = labelOverride ?: label
|
||||||
|
val label2 = other.labelOverride ?: other.label
|
||||||
|
return Collator.getInstance().apply { strength = Collator.SECONDARY }
|
||||||
|
.compare(label1.romanize(), label2.romanize())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
|
interface Searchable {
|
||||||
|
val domain: String
|
||||||
|
val key: String
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
|
interface SearchableDeserializer {
|
||||||
|
fun deserialize(serialized: String): PinnableSearchable?
|
||||||
|
}
|
||||||
|
|
||||||
|
class NullDeserializer: SearchableDeserializer {
|
||||||
|
override fun deserialize(serialized: String): PinnableSearchable? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface SearchableRepository<T: Searchable> {
|
||||||
|
fun search(query: String): Flow<ImmutableList<T>>
|
||||||
|
}
|
||||||
@ -1,14 +1,12 @@
|
|||||||
package de.mm20.launcher2.search
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
|
||||||
|
|
||||||
interface SearchableSerializer {
|
interface SearchableSerializer {
|
||||||
fun serialize(searchable: Searchable): String?
|
fun serialize(searchable: PinnableSearchable): String?
|
||||||
val typePrefix: String
|
val typePrefix: String
|
||||||
}
|
}
|
||||||
|
|
||||||
class NullSerializer : SearchableSerializer {
|
class NullSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String? {
|
override fun serialize(searchable: PinnableSearchable): String? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +46,6 @@ dependencies {
|
|||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":search"))
|
implementation(project(":base"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,14 +1,21 @@
|
|||||||
package de.mm20.launcher2.search.data
|
package de.mm20.launcher2.search.data
|
||||||
|
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class Calculator(
|
data class Calculator(
|
||||||
val term: String,
|
val term: String,
|
||||||
val solution: Double
|
val solution: Double
|
||||||
) {
|
): Searchable {
|
||||||
|
|
||||||
|
override val domain: String
|
||||||
|
get() = "calculator"
|
||||||
|
|
||||||
|
override val key: String
|
||||||
|
get() = "calculator://$term"
|
||||||
|
|
||||||
val formattedString: String
|
val formattedString: String
|
||||||
val formattedBinaryString: String
|
val formattedBinaryString: String
|
||||||
|
|||||||
@ -42,7 +42,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
api(project(":search"))
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
|
|||||||
@ -7,8 +7,12 @@ import androidx.core.database.getStringOrNull
|
|||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.search.data.UserCalendar
|
import de.mm20.launcher2.search.data.UserCalendar
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -16,9 +20,7 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
interface CalendarRepository {
|
interface CalendarRepository: SearchableRepository<CalendarEvent> {
|
||||||
fun search(query: String): Flow<List<CalendarEvent>>
|
|
||||||
|
|
||||||
fun getUpcomingEvents(): Flow<List<CalendarEvent>>
|
fun getUpcomingEvents(): Flow<List<CalendarEvent>>
|
||||||
|
|
||||||
suspend fun getCalendars(): List<UserCalendar>
|
suspend fun getCalendars(): List<UserCalendar>
|
||||||
@ -31,10 +33,10 @@ internal class CalendarRepositoryImpl(
|
|||||||
private val dataStore: LauncherDataStore by inject()
|
private val dataStore: LauncherDataStore by inject()
|
||||||
private val permissionsManager: PermissionsManager by inject()
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
|
|
||||||
override fun search(query: String): Flow<List<CalendarEvent>> {
|
override fun search(query: String): Flow<ImmutableList<CalendarEvent>> {
|
||||||
if (query.isBlank() || query.length < 3) {
|
if (query.isBlank() || query.length < 3) {
|
||||||
return flow {
|
return flow {
|
||||||
emit(emptyList())
|
emit(persistentListOf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,9 +51,9 @@ internal class CalendarRepositoryImpl(
|
|||||||
query,
|
query,
|
||||||
intervalStart = now,
|
intervalStart = now,
|
||||||
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
||||||
)
|
).toImmutableList()
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
persistentListOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,15 +7,16 @@ import android.content.pm.PackageManager
|
|||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class CalendarEventSerializer: SearchableSerializer {
|
class CalendarEventSerializer: SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as CalendarEvent
|
searchable as CalendarEvent
|
||||||
val json = JSONObject()
|
val json = JSONObject()
|
||||||
json.put("id", searchable.id)
|
json.put("id", searchable.id)
|
||||||
@ -27,7 +28,7 @@ class CalendarEventSerializer: SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable? {
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
val id = json.getLong("id")
|
val id = json.getLong("id")
|
||||||
|
|||||||
@ -3,13 +3,17 @@ package de.mm20.launcher2.search.data
|
|||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TextLayer
|
import de.mm20.launcher2.icons.TextLayer
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
class CalendarEvent(
|
data class CalendarEvent(
|
||||||
override val label: String,
|
override val label: String,
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val color: Int,
|
val color: Int,
|
||||||
@ -19,12 +23,20 @@ class CalendarEvent(
|
|||||||
val location: String,
|
val location: String,
|
||||||
val attendees: List<String>,
|
val attendees: List<String>,
|
||||||
val description: String,
|
val description: String,
|
||||||
val calendar: Long
|
val calendar: Long,
|
||||||
) : Searchable() {
|
override val labelOverride: String? = null,
|
||||||
|
) : PinnableSearchable {
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
override val key: String
|
override val key: String
|
||||||
get() = "calendar://$id"
|
get() = "$domain://$id"
|
||||||
|
|
||||||
|
override val preferDetailsOverLaunch: Boolean = true
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): CalendarEvent {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||||
val df = SimpleDateFormat("dd")
|
val df = SimpleDateFormat("dd")
|
||||||
@ -37,10 +49,18 @@ class CalendarEvent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent {
|
private fun getLaunchIntent(): Intent {
|
||||||
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
|
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
|
||||||
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(getLaunchIntent(), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val Domain = "calendar"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UserCalendar(
|
data class UserCalendar(
|
||||||
|
|||||||
@ -42,7 +42,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
|
|||||||
@ -5,16 +5,18 @@ import android.provider.ContactsContract
|
|||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
import de.mm20.launcher2.search.data.Contact
|
import de.mm20.launcher2.search.data.Contact
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
interface ContactRepository {
|
interface ContactRepository: SearchableRepository<Contact>
|
||||||
fun search(query: String): Flow<List<Contact>>
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ContactRepositoryImpl(
|
internal class ContactRepositoryImpl(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -23,13 +25,13 @@ internal class ContactRepositoryImpl(
|
|||||||
private val permissionsManager: PermissionsManager by inject()
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
private val dataStore: LauncherDataStore by inject()
|
private val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
override fun search(query: String): Flow<List<Contact>> {
|
override fun search(query: String): Flow<ImmutableList<Contact>> {
|
||||||
val searchContacts = dataStore.data.map { it.contactsSearch.enabled }
|
val searchContacts = dataStore.data.map { it.contactsSearch.enabled }
|
||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
||||||
|
|
||||||
if (query.length < 3) {
|
if (query.length < 3) {
|
||||||
return flow {
|
return flow {
|
||||||
emit(emptyList())
|
emit(persistentListOf())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,12 +41,12 @@ internal class ContactRepositoryImpl(
|
|||||||
if (it) {
|
if (it) {
|
||||||
queryContacts(query)
|
queryContacts(query)
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
persistentListOf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun queryContacts(query: String): List<Contact> {
|
private suspend fun queryContacts(query: String): ImmutableList<Contact> {
|
||||||
val results = withContext(Dispatchers.IO) {
|
val results = withContext(Dispatchers.IO) {
|
||||||
val proj = arrayOf(
|
val proj = arrayOf(
|
||||||
ContactsContract.RawContacts.CONTACT_ID,
|
ContactsContract.RawContacts.CONTACT_ID,
|
||||||
@ -67,6 +69,6 @@ internal class ContactRepositoryImpl(
|
|||||||
}
|
}
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
return results
|
return results.toImmutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,14 +6,15 @@ import android.content.pm.PackageManager
|
|||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.Contact
|
import de.mm20.launcher2.search.data.Contact
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
class ContactSerializer : SearchableSerializer {
|
class ContactSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as Contact
|
searchable as Contact
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"id" to searchable.id
|
"id" to searchable.id
|
||||||
@ -25,7 +26,7 @@ class ContactSerializer : SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ContactDeserializer(val context: Context) : SearchableDeserializer {
|
class ContactDeserializer(val context: Context) : SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable? {
|
||||||
if (ContextCompat.checkSelfPermission(
|
if (ContextCompat.checkSelfPermission(
|
||||||
context,
|
context,
|
||||||
Manifest.permission.READ_CONTACTS
|
Manifest.permission.READ_CONTACTS
|
||||||
|
|||||||
@ -3,20 +3,21 @@ package de.mm20.launcher2.search.data
|
|||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import de.mm20.launcher2.contacts.R
|
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.ktx.asBitmap
|
import de.mm20.launcher2.ktx.asBitmap
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class Contact(
|
data class Contact(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val firstName: String,
|
val firstName: String,
|
||||||
val lastName: String,
|
val lastName: String,
|
||||||
@ -27,12 +28,21 @@ class Contact(
|
|||||||
val telegram: Set<ContactInfo>,
|
val telegram: Set<ContactInfo>,
|
||||||
val whatsapp: Set<ContactInfo>,
|
val whatsapp: Set<ContactInfo>,
|
||||||
val postals: Set<ContactInfo>,
|
val postals: Set<ContactInfo>,
|
||||||
) : Searchable() {
|
override val labelOverride: String? = null
|
||||||
|
) : Searchable, PinnableSearchable {
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
override val key: String
|
override val key: String
|
||||||
get() = "contact://$id"
|
get() = "${Domain}://$id"
|
||||||
override val label: String
|
override val label: String
|
||||||
get() = "$firstName $lastName"
|
get() = "$firstName $lastName"
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): Contact {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val preferDetailsOverLaunch: Boolean = true
|
||||||
|
|
||||||
val summary: String
|
val summary: String
|
||||||
get() {
|
get() {
|
||||||
return phones.union(emails).joinToString(separator = ", ") { it.label }
|
return phones.union(emails).joinToString(separator = ", ") { it.label }
|
||||||
@ -69,7 +79,12 @@ class Contact(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent {
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
val intent = getLaunchIntent()
|
||||||
|
return context.tryStartActivity(intent, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLaunchIntent(): Intent {
|
||||||
val uri =
|
val uri =
|
||||||
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id)
|
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id)
|
||||||
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
@ -202,7 +217,10 @@ class Contact(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val Domain = "contact"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ContactInfo(
|
data class ContactInfo(
|
||||||
|
|||||||
@ -41,7 +41,7 @@ dependencies {
|
|||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":search"))
|
implementation(project(":base"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":crashreporter"))
|
implementation(project(":crashreporter"))
|
||||||
implementation(project(":favorites"))
|
implementation(project(":favorites"))
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import de.mm20.launcher2.database.AppDatabase
|
|||||||
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
||||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
@ -15,24 +15,24 @@ import org.json.JSONException
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface CustomAttributesRepository {
|
interface CustomAttributesRepository {
|
||||||
fun getCustomIcon(searchable: Searchable): Flow<CustomIcon?>
|
fun getCustomIcon(searchable: PinnableSearchable): Flow<CustomIcon?>
|
||||||
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?)
|
fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?)
|
||||||
|
|
||||||
fun getCustomLabels(items: List<Searchable>): Flow<List<CustomLabel>>
|
fun getCustomLabels(items: List<PinnableSearchable>): Flow<List<CustomLabel>>
|
||||||
fun setCustomLabel(searchable: Searchable, label: String)
|
fun setCustomLabel(searchable: PinnableSearchable, label: String)
|
||||||
fun clearCustomLabel(searchable: Searchable)
|
fun clearCustomLabel(searchable: PinnableSearchable)
|
||||||
|
|
||||||
fun setTags(searchable: Searchable, tags: List<String>)
|
fun setTags(searchable: PinnableSearchable, tags: List<String>)
|
||||||
fun getTags(searchable: Searchable): Flow<List<String>>
|
fun getTags(searchable: PinnableSearchable): Flow<List<String>>
|
||||||
|
|
||||||
suspend fun search(query: String): Flow<List<Searchable>>
|
suspend fun search(query: String): Flow<List<PinnableSearchable>>
|
||||||
|
|
||||||
suspend fun export(toDir: File)
|
suspend fun export(toDir: File)
|
||||||
suspend fun import(fromDir: File)
|
suspend fun import(fromDir: File)
|
||||||
|
|
||||||
suspend fun getAllTags(startsWith: String? = null): List<String>
|
suspend fun getAllTags(startsWith: String? = null): List<String>
|
||||||
fun getItemsForTag(tag: String): Flow<List<Searchable>>
|
fun getItemsForTag(tag: String): Flow<List<PinnableSearchable>>
|
||||||
fun addTag(item: Searchable, tag: String)
|
fun addTag(item: PinnableSearchable, tag: String)
|
||||||
suspend fun cleanupDatabase(): Int
|
suspend fun cleanupDatabase(): Int
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
) : CustomAttributesRepository {
|
) : CustomAttributesRepository {
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
|
|
||||||
override fun getCustomIcon(searchable: Searchable): Flow<CustomIcon?> {
|
override fun getCustomIcon(searchable: PinnableSearchable): Flow<CustomIcon?> {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
return dao.getCustomAttribute(searchable.key, CustomAttributeType.Icon.value)
|
return dao.getCustomAttribute(searchable.key, CustomAttributeType.Icon.value)
|
||||||
.map {
|
.map {
|
||||||
@ -50,7 +50,7 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) {
|
override fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?) {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Icon.value)
|
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Icon.value)
|
||||||
@ -60,7 +60,7 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCustomLabels(items: List<Searchable>): Flow<List<CustomLabel>> {
|
override fun getCustomLabels(items: List<PinnableSearchable>): Flow<List<CustomLabel>> {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
return dao.getCustomAttributes(items.map { it.key }, CustomAttributeType.Label.value)
|
return dao.getCustomAttributes(items.map { it.key }, CustomAttributeType.Label.value)
|
||||||
.map { list ->
|
.map { list ->
|
||||||
@ -68,7 +68,7 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setCustomLabel(searchable: Searchable, label: String) {
|
override fun setCustomLabel(searchable: PinnableSearchable, label: String) {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
favoritesRepository.save(searchable)
|
favoritesRepository.save(searchable)
|
||||||
@ -84,14 +84,14 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearCustomLabel(searchable: Searchable) {
|
override fun clearCustomLabel(searchable: PinnableSearchable) {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
|
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setTags(searchable: Searchable, tags: List<String>) {
|
override fun setTags(searchable: PinnableSearchable, tags: List<String>) {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
favoritesRepository.save(searchable)
|
favoritesRepository.save(searchable)
|
||||||
@ -101,7 +101,7 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTags(searchable: Searchable): Flow<List<String>> {
|
override fun getTags(searchable: PinnableSearchable): Flow<List<String>> {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
return dao.getCustomAttributes(listOf(searchable.key), CustomAttributeType.Tag.value).map {
|
return dao.getCustomAttributes(listOf(searchable.key), CustomAttributeType.Tag.value).map {
|
||||||
it.map { it.value }
|
it.map { it.value }
|
||||||
@ -117,21 +117,21 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemsForTag(tag: String): Flow<List<Searchable>> {
|
override fun getItemsForTag(tag: String): Flow<List<PinnableSearchable>> {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
return dao.getItemsWithTag(tag).map {
|
return dao.getItemsWithTag(tag).map {
|
||||||
favoritesRepository.getFromKeys(it)
|
favoritesRepository.getFromKeys(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addTag(item: Searchable, tag: String) {
|
override fun addTag(item: PinnableSearchable, tag: String) {
|
||||||
val dao = appDatabase.customAttrsDao()
|
val dao = appDatabase.customAttrsDao()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
dao.addTag(item.key, tag)
|
dao.addTag(item.key, tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): Flow<List<Searchable>> {
|
override suspend fun search(query: String): Flow<List<PinnableSearchable>> {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
return flow {
|
return flow {
|
||||||
emit(emptyList())
|
emit(emptyList())
|
||||||
|
|||||||
@ -43,7 +43,6 @@ dependencies {
|
|||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":calendar"))
|
implementation(project(":calendar"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
package de.mm20.launcher2.favorites
|
package de.mm20.launcher2.favorites
|
||||||
|
|
||||||
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
data class FavoritesItem(
|
data class FavoritesItem(
|
||||||
val key: String,
|
val key: String,
|
||||||
/**
|
/**
|
||||||
* null if searchable could not be deserialized (i.e. the app has been uninstalled)
|
* null if searchable could not be deserialized (i.e. the app has been uninstalled)
|
||||||
*/
|
*/
|
||||||
val searchable: Searchable?,
|
val searchable: PinnableSearchable?,
|
||||||
var launchCount: Int,
|
var launchCount: Int,
|
||||||
var pinPosition: Int,
|
var pinPosition: Int,
|
||||||
var hidden: Boolean
|
var hidden: Boolean
|
||||||
|
|||||||
@ -5,11 +5,10 @@ import android.util.Log
|
|||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
||||||
import de.mm20.launcher2.ktx.ceilToInt
|
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
@ -34,47 +33,47 @@ interface FavoritesRepository {
|
|||||||
automaticallySorted: Boolean = false,
|
automaticallySorted: Boolean = false,
|
||||||
frequentlyUsed: Boolean = false,
|
frequentlyUsed: Boolean = false,
|
||||||
limit: Int = 100
|
limit: Int = 100
|
||||||
): Flow<List<Searchable>>
|
): Flow<List<PinnableSearchable>>
|
||||||
|
|
||||||
|
|
||||||
fun getPinnedCalendarEvents(): Flow<List<Searchable>>
|
fun getPinnedCalendarEvents(): Flow<List<PinnableSearchable>>
|
||||||
fun getHiddenCalendarEventKeys(): Flow<List<String>>
|
fun getHiddenCalendarEventKeys(): Flow<List<String>>
|
||||||
fun isPinned(searchable: Searchable): Flow<Boolean>
|
fun isPinned(searchable: PinnableSearchable): Flow<Boolean>
|
||||||
fun pinItem(searchable: Searchable)
|
fun pinItem(searchable: PinnableSearchable)
|
||||||
fun unpinItem(searchable: Searchable)
|
fun unpinItem(searchable: PinnableSearchable)
|
||||||
fun isHidden(searchable: Searchable): Flow<Boolean>
|
fun isHidden(searchable: PinnableSearchable): Flow<Boolean>
|
||||||
fun hideItem(searchable: Searchable)
|
fun hideItem(searchable: PinnableSearchable)
|
||||||
fun unhideItem(searchable: Searchable)
|
fun unhideItem(searchable: PinnableSearchable)
|
||||||
fun incrementLaunchCounter(searchable: Searchable)
|
fun incrementLaunchCounter(searchable: PinnableSearchable)
|
||||||
fun updateFavorites(
|
fun updateFavorites(
|
||||||
manuallySorted: List<Searchable>,
|
manuallySorted: List<PinnableSearchable>,
|
||||||
automaticallySorted: List<Searchable>,
|
automaticallySorted: List<PinnableSearchable>,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getHiddenItems(): Flow<List<Searchable>>
|
fun getHiddenItems(): Flow<List<PinnableSearchable>>
|
||||||
fun getHiddenItemKeys(): Flow<List<String>>
|
fun getHiddenItemKeys(): Flow<List<String>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove this item from the Searchable database
|
* Remove this item from the Searchable database
|
||||||
*/
|
*/
|
||||||
fun remove(searchable: Searchable)
|
fun remove(searchable: PinnableSearchable)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove this item from favorites and reset launch counter
|
* Remove this item from favorites and reset launch counter
|
||||||
*/
|
*/
|
||||||
fun removeFromFavorites(searchable: Searchable)
|
fun removeFromFavorites(searchable: PinnableSearchable)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that this searchable exists in the Favorites table.
|
* Ensure that this searchable exists in the Favorites table.
|
||||||
* If it doesn't exist, insert it with 0 launch count, not pinned and not hidden
|
* If it doesn't exist, insert it with 0 launch count, not pinned and not hidden
|
||||||
*/
|
*/
|
||||||
fun save(searchable: Searchable)
|
fun save(searchable: PinnableSearchable)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get items with the given keys from the favorites database.
|
* Get items with the given keys from the favorites database.
|
||||||
* Items that don't exist in the database will not be returned.
|
* Items that don't exist in the database will not be returned.
|
||||||
*/
|
*/
|
||||||
suspend fun getFromKeys(keys: List<String>): List<Searchable>
|
suspend fun getFromKeys(keys: List<String>): List<PinnableSearchable>
|
||||||
|
|
||||||
suspend fun export(toDir: File)
|
suspend fun export(toDir: File)
|
||||||
suspend fun import(fromDir: File)
|
suspend fun import(fromDir: File)
|
||||||
@ -101,7 +100,7 @@ internal class FavoritesRepositoryImpl(
|
|||||||
automaticallySorted: Boolean,
|
automaticallySorted: Boolean,
|
||||||
frequentlyUsed: Boolean,
|
frequentlyUsed: Boolean,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Flow<List<Searchable>> {
|
): Flow<List<PinnableSearchable>> {
|
||||||
val dao = database.searchDao()
|
val dao = database.searchDao()
|
||||||
val entities = when {
|
val entities = when {
|
||||||
includeTypes == null && excludeTypes == null -> dao.getFavorites(
|
includeTypes == null && excludeTypes == null -> dao.getFavorites(
|
||||||
@ -150,11 +149,11 @@ internal class FavoritesRepositoryImpl(
|
|||||||
return database.searchDao().getHiddenCalendarEventKeys()
|
return database.searchDao().getHiddenCalendarEventKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isPinned(searchable: Searchable): Flow<Boolean> {
|
override fun isPinned(searchable: PinnableSearchable): Flow<Boolean> {
|
||||||
return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key)
|
return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pinItem(searchable: Searchable) {
|
override fun pinItem(searchable: PinnableSearchable) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val dao = AppDatabase.getInstance(context).searchDao()
|
val dao = AppDatabase.getInstance(context).searchDao()
|
||||||
@ -171,7 +170,7 @@ internal class FavoritesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unpinItem(searchable: Searchable) {
|
override fun unpinItem(searchable: PinnableSearchable) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key)
|
AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key)
|
||||||
@ -179,11 +178,11 @@ internal class FavoritesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isHidden(searchable: Searchable): Flow<Boolean> {
|
override fun isHidden(searchable: PinnableSearchable): Flow<Boolean> {
|
||||||
return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key)
|
return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hideItem(searchable: Searchable) {
|
override fun hideItem(searchable: PinnableSearchable) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val dao = AppDatabase.getInstance(context).searchDao()
|
val dao = AppDatabase.getInstance(context).searchDao()
|
||||||
@ -200,7 +199,7 @@ internal class FavoritesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unhideItem(searchable: Searchable) {
|
override fun unhideItem(searchable: PinnableSearchable) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key)
|
AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key)
|
||||||
@ -208,7 +207,7 @@ internal class FavoritesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun incrementLaunchCounter(searchable: Searchable) {
|
override fun incrementLaunchCounter(searchable: PinnableSearchable) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val item = FavoritesItem(searchable.key, searchable, 0, 0, false)
|
val item = FavoritesItem(searchable.key, searchable, 0, 0, false)
|
||||||
@ -220,7 +219,7 @@ internal class FavoritesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getHiddenItems(): Flow<List<Searchable>> {
|
override fun getHiddenItems(): Flow<List<PinnableSearchable>> {
|
||||||
return database.searchDao().getHiddenItems().map {
|
return database.searchDao().getHiddenItems().map {
|
||||||
it.mapNotNull { fromDatabaseEntity(it).searchable }
|
it.mapNotNull { fromDatabaseEntity(it).searchable }
|
||||||
}
|
}
|
||||||
@ -230,7 +229,7 @@ internal class FavoritesRepositoryImpl(
|
|||||||
return database.searchDao().getHiddenItemKeys()
|
return database.searchDao().getHiddenItemKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(searchable: Searchable) {
|
override fun remove(searchable: PinnableSearchable) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
database.searchDao().deleteByKey(searchable.key)
|
database.searchDao().deleteByKey(searchable.key)
|
||||||
@ -238,13 +237,13 @@ internal class FavoritesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeFromFavorites(searchable: Searchable) {
|
override fun removeFromFavorites(searchable: PinnableSearchable) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
database.searchDao().resetPinStatusAndLaunchCounter(searchable.key)
|
database.searchDao().resetPinStatusAndLaunchCounter(searchable.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun save(searchable: Searchable) {
|
override fun save(searchable: PinnableSearchable) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val entity = FavoritesItem(
|
val entity = FavoritesItem(
|
||||||
@ -260,8 +259,8 @@ internal class FavoritesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun updateFavorites(
|
override fun updateFavorites(
|
||||||
manuallySorted: List<Searchable>,
|
manuallySorted: List<PinnableSearchable>,
|
||||||
automaticallySorted: List<Searchable>
|
automaticallySorted: List<PinnableSearchable>
|
||||||
) {
|
) {
|
||||||
val dao = database.searchDao()
|
val dao = database.searchDao()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@ -321,7 +320,7 @@ internal class FavoritesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFromKeys(keys: List<String>): List<Searchable> {
|
override suspend fun getFromKeys(keys: List<String>): List<PinnableSearchable> {
|
||||||
val dao = database.searchDao()
|
val dao = database.searchDao()
|
||||||
return dao.getFromKeys(keys)
|
return dao.getFromKeys(keys)
|
||||||
.mapNotNull { fromDatabaseEntity(it).searchable }
|
.mapNotNull { fromDatabaseEntity(it).searchable }
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import de.mm20.launcher2.contacts.ContactSerializer
|
|||||||
import de.mm20.launcher2.files.*
|
import de.mm20.launcher2.files.*
|
||||||
import de.mm20.launcher2.search.NullDeserializer
|
import de.mm20.launcher2.search.NullDeserializer
|
||||||
import de.mm20.launcher2.search.NullSerializer
|
import de.mm20.launcher2.search.NullSerializer
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.*
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
package de.mm20.launcher2.favorites
|
package de.mm20.launcher2.favorites
|
||||||
|
|
||||||
import android.content.Context
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.Tag
|
import de.mm20.launcher2.search.data.Tag
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
class TagSerializer: SearchableSerializer {
|
class TagSerializer: SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as Tag
|
searchable as Tag
|
||||||
val json = JSONObject()
|
val json = JSONObject()
|
||||||
json.put("tag", searchable.tag)
|
json.put("tag", searchable.tag)
|
||||||
@ -20,7 +20,7 @@ class TagSerializer: SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TagDeserializer: SearchableDeserializer {
|
class TagDeserializer: SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable {
|
override fun deserialize(serialized: String): PinnableSearchable {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
|
|
||||||
return Tag(json.getString("tag"))
|
return Tag(json.getString("tag"))
|
||||||
|
|||||||
@ -1,20 +1,40 @@
|
|||||||
package de.mm20.launcher2.search.data
|
package de.mm20.launcher2.search.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TextLayer
|
import de.mm20.launcher2.icons.TextLayer
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
class Tag(
|
data class Tag(
|
||||||
val tag: String,
|
val tag: String,
|
||||||
): Searchable() {
|
override val labelOverride: String? = null
|
||||||
override val key: String = "tag://$tag"
|
): PinnableSearchable {
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val key: String = "$domain://$tag"
|
||||||
override val label: String = tag
|
override val label: String = tag
|
||||||
|
|
||||||
|
override val preferDetailsOverLaunch: Boolean = true
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
override fun overrideLabel(label: String): PinnableSearchable {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||||
return StaticLauncherIcon(
|
return StaticLauncherIcon(
|
||||||
foregroundLayer = TextLayer("#"),
|
foregroundLayer = TextLayer("#"),
|
||||||
backgroundLayer = ColorLayer()
|
backgroundLayer = ColorLayer()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val Domain = "tag"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -44,7 +44,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
package de.mm20.launcher2.files
|
package de.mm20.launcher2.files
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.*
|
||||||
@ -15,7 +16,7 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
|
||||||
class LocalFileSerializer : SearchableSerializer {
|
class LocalFileSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as LocalFile
|
searchable as LocalFile
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"id" to searchable.id
|
"id" to searchable.id
|
||||||
@ -29,7 +30,7 @@ class LocalFileSerializer : SearchableSerializer {
|
|||||||
class LocalFileDeserializer(
|
class LocalFileDeserializer(
|
||||||
val context: Context
|
val context: Context
|
||||||
) : SearchableDeserializer, KoinComponent {
|
) : SearchableDeserializer, KoinComponent {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable? {
|
||||||
val permissionsManager: PermissionsManager = get()
|
val permissionsManager: PermissionsManager = get()
|
||||||
if (!permissionsManager.checkPermissionOnce(
|
if (!permissionsManager.checkPermissionOnce(
|
||||||
PermissionGroup.ExternalStorage
|
PermissionGroup.ExternalStorage
|
||||||
@ -74,7 +75,7 @@ class LocalFileDeserializer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GDriveFileSerializer : SearchableSerializer {
|
class GDriveFileSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as GDriveFile
|
searchable as GDriveFile
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"id" to searchable.fileId,
|
"id" to searchable.fileId,
|
||||||
@ -103,7 +104,7 @@ class GDriveFileSerializer : SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GDriveFileDeserializer : SearchableDeserializer {
|
class GDriveFileDeserializer : SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
val id = json.getString("id")
|
val id = json.getString("id")
|
||||||
val label = json.getString("label")
|
val label = json.getString("label")
|
||||||
@ -134,7 +135,7 @@ class GDriveFileDeserializer : SearchableDeserializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class OneDriveFileSerializer : SearchableSerializer {
|
class OneDriveFileSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as OneDriveFile
|
searchable as OneDriveFile
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"id" to searchable.fileId,
|
"id" to searchable.fileId,
|
||||||
@ -161,7 +162,7 @@ class OneDriveFileSerializer : SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class OneDriveFileDeserializer : SearchableDeserializer {
|
class OneDriveFileDeserializer : SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
val fileId = json.getString("id")
|
val fileId = json.getString("id")
|
||||||
val label = json.getString("label")
|
val label = json.getString("label")
|
||||||
@ -189,10 +190,10 @@ class OneDriveFileDeserializer : SearchableDeserializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NextcloudFileSerializer : SearchableSerializer {
|
class NextcloudFileSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as NextcloudFile
|
searchable as NextcloudFile
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"id" to searchable.id,
|
"id" to searchable.fileId,
|
||||||
"label" to searchable.label,
|
"label" to searchable.label,
|
||||||
"path" to searchable.path,
|
"path" to searchable.path,
|
||||||
"mimeType" to searchable.mimeType,
|
"mimeType" to searchable.mimeType,
|
||||||
@ -216,7 +217,7 @@ class NextcloudFileSerializer : SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NextcloudFileDeserializer : SearchableDeserializer {
|
class NextcloudFileDeserializer : SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
val id = json.getLong("id")
|
val id = json.getLong("id")
|
||||||
val label = json.getString("label")
|
val label = json.getString("label")
|
||||||
@ -242,10 +243,10 @@ class NextcloudFileDeserializer : SearchableDeserializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class OwncloudFileSerializer : SearchableSerializer {
|
class OwncloudFileSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as OwncloudFile
|
searchable as OwncloudFile
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"id" to searchable.id,
|
"id" to searchable.fileId,
|
||||||
"label" to searchable.label,
|
"label" to searchable.label,
|
||||||
"path" to searchable.path,
|
"path" to searchable.path,
|
||||||
"mimeType" to searchable.mimeType,
|
"mimeType" to searchable.mimeType,
|
||||||
@ -269,7 +270,7 @@ class OwncloudFileSerializer : SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class OwncloudFileDeserializer : SearchableDeserializer {
|
class OwncloudFileDeserializer : SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
val id = json.getLong("id")
|
val id = json.getLong("id")
|
||||||
val label = json.getString("label")
|
val label = json.getString("label")
|
||||||
|
|||||||
@ -6,15 +6,18 @@ import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
|||||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
import de.mm20.launcher2.search.data.File
|
import de.mm20.launcher2.search.data.File
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
interface FileRepository {
|
interface FileRepository: SearchableRepository<File> {
|
||||||
fun search(query: String): Flow<List<File>>
|
|
||||||
fun deleteFile(file: File)
|
fun deleteFile(file: File)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,21 +63,21 @@ internal class FileRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Flow<List<File>> = channelFlow {
|
override fun search(query: String): Flow<ImmutableList<File>> = channelFlow {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
send(emptyList())
|
send(persistentListOf())
|
||||||
return@channelFlow
|
return@channelFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
providers.collectLatest { providers ->
|
providers.collectLatest { providers ->
|
||||||
if (providers.isEmpty()) {
|
if (providers.isEmpty()) {
|
||||||
send(emptyList())
|
send(persistentListOf())
|
||||||
return@collectLatest
|
return@collectLatest
|
||||||
}
|
}
|
||||||
val results = mutableListOf<File>()
|
val results = mutableListOf<File>()
|
||||||
for (provider in providers) {
|
for (provider in providers) {
|
||||||
results.addAll(provider.search(query))
|
results.addAll(provider.search(query))
|
||||||
send(results.toList())
|
send(results.toImmutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,29 @@
|
|||||||
package de.mm20.launcher2.search.data
|
package de.mm20.launcher2.search.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import de.mm20.launcher2.files.R
|
import de.mm20.launcher2.files.R
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TintedIconLayer
|
import de.mm20.launcher2.icons.TintedIconLayer
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class File(
|
interface File : PinnableSearchable {
|
||||||
val id: Long,
|
val path: String
|
||||||
val path: String,
|
val mimeType: String
|
||||||
val mimeType: String,
|
val size: Long
|
||||||
val size: Long,
|
val isDirectory: Boolean
|
||||||
val isDirectory: Boolean,
|
|
||||||
val metaData: List<Pair<Int, String>>
|
val metaData: List<Pair<Int, String>>
|
||||||
) : Searchable() {
|
|
||||||
abstract val isStoredInCloud: Boolean
|
|
||||||
|
|
||||||
open val providerIconRes: Int? = null
|
val isStoredInCloud: Boolean
|
||||||
|
|
||||||
|
override val preferDetailsOverLaunch: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
open val providerIconRes: Int?
|
||||||
|
get() = null
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||||
val (resId, bgColor) = when {
|
val (resId, bgColor) = when {
|
||||||
@ -124,7 +128,8 @@ abstract class File(
|
|||||||
return context.getString(resource)
|
return context.getString(resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
open val isDeletable: Boolean = false
|
val isDeletable: Boolean
|
||||||
open suspend fun delete(context: Context) {}
|
get() = false
|
||||||
|
suspend fun delete(context: Context) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -3,30 +3,47 @@ package de.mm20.launcher2.search.data
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import de.mm20.launcher2.files.R
|
import de.mm20.launcher2.files.R
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
|
||||||
class GDriveFile(
|
data class GDriveFile(
|
||||||
val fileId: String,
|
val fileId: String,
|
||||||
override val label: String,
|
override val label: String,
|
||||||
path: String,
|
override val path: String,
|
||||||
mimeType: String,
|
override val mimeType: String,
|
||||||
size: Long,
|
override val size: Long,
|
||||||
isDirectory: Boolean,
|
override val isDirectory: Boolean,
|
||||||
metaData: List<Pair<Int, String>>,
|
override val metaData: List<Pair<Int, String>>,
|
||||||
val directoryColor: String?,
|
val directoryColor: String?,
|
||||||
val viewUri: String
|
val viewUri: String,
|
||||||
) : File(0, path, mimeType, size, isDirectory, metaData) {
|
override val labelOverride: String? = null,
|
||||||
|
) : File {
|
||||||
|
|
||||||
override val key: String = "gdrive://$fileId"
|
override fun overrideLabel(label: String): GDriveFile {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val key: String = "$domain://$fileId"
|
||||||
|
|
||||||
override val isStoredInCloud = true
|
override val isStoredInCloud = true
|
||||||
|
|
||||||
override val providerIconRes = R.drawable.ic_badge_gdrive
|
override val providerIconRes = R.drawable.ic_badge_gdrive
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
private fun getLaunchIntent(): Intent {
|
||||||
return Intent(Intent.ACTION_VIEW).apply {
|
return Intent(Intent.ACTION_VIEW).apply {
|
||||||
data = Uri.parse(viewUri)
|
data = Uri.parse(viewUri)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(getLaunchIntent(), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val Domain = "gdrive"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -9,6 +9,7 @@ import android.location.Geocoder
|
|||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.media.ThumbnailUtils
|
import android.media.ThumbnailUtils
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
@ -17,25 +18,34 @@ import androidx.exifinterface.media.ExifInterface
|
|||||||
import de.mm20.launcher2.files.R
|
import de.mm20.launcher2.files.R
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.ktx.formatToString
|
import de.mm20.launcher2.ktx.formatToString
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.media.ThumbnailUtilsCompat
|
import de.mm20.launcher2.media.ThumbnailUtilsCompat
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.File as JavaIOFile
|
import java.io.File as JavaIOFile
|
||||||
|
|
||||||
open class LocalFile(
|
data class LocalFile(
|
||||||
id: Long,
|
val id: Long,
|
||||||
path: String,
|
override val path: String,
|
||||||
mimeType: String,
|
override val mimeType: String,
|
||||||
size: Long,
|
override val size: Long,
|
||||||
isDirectory: Boolean,
|
override val isDirectory: Boolean,
|
||||||
metaData: List<Pair<Int, String>>
|
override val metaData: List<Pair<Int, String>>,
|
||||||
) : File(id, path, mimeType, size, isDirectory, metaData) {
|
override val labelOverride: String? = null
|
||||||
|
) : File {
|
||||||
|
|
||||||
override val label = path.substringAfterLast('/')
|
override val label = path.substringAfterLast('/')
|
||||||
|
|
||||||
override val key = "file://$path"
|
override fun overrideLabel(label: String): LocalFile {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val key = "$domain://$path"
|
||||||
|
|
||||||
override val isStoredInCloud = false
|
override val isStoredInCloud = false
|
||||||
|
|
||||||
@ -148,7 +158,7 @@ open class LocalFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
private fun getLaunchIntent(context: Context): Intent {
|
||||||
val uri = if (isDirectory) {
|
val uri = if (isDirectory) {
|
||||||
Uri.parse(path)
|
Uri.parse(path)
|
||||||
} else {
|
} else {
|
||||||
@ -162,6 +172,10 @@ open class LocalFile(
|
|||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(getLaunchIntent(context), options)
|
||||||
|
}
|
||||||
|
|
||||||
override val isDeletable: Boolean
|
override val isDeletable: Boolean
|
||||||
get() {
|
get() {
|
||||||
val file = java.io.File(path)
|
val file = java.io.File(path)
|
||||||
@ -186,6 +200,9 @@ open class LocalFile(
|
|||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val Domain = "file"
|
||||||
|
|
||||||
internal fun getMimetypeByFileExtension(extension: String): String {
|
internal fun getMimetypeByFileExtension(extension: String): String {
|
||||||
return when (extension) {
|
return when (extension) {
|
||||||
"apk" -> "application/vnd.android.package-archive"
|
"apk" -> "application/vnd.android.package-archive"
|
||||||
|
|||||||
@ -4,34 +4,50 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import de.mm20.launcher2.files.R
|
import de.mm20.launcher2.files.R
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
|
||||||
class NextcloudFile(
|
data class NextcloudFile(
|
||||||
fileId: Long,
|
val fileId: Long,
|
||||||
override val label: String,
|
override val label: String,
|
||||||
path: String,
|
override val path: String,
|
||||||
mimeType: String,
|
override val mimeType: String,
|
||||||
size: Long,
|
override val size: Long,
|
||||||
isDirectory: Boolean,
|
override val isDirectory: Boolean,
|
||||||
val server: String,
|
val server: String,
|
||||||
metaData: List<Pair<Int, String>>
|
override val metaData: List<Pair<Int, String>>,
|
||||||
) : File(fileId, path, mimeType, size, isDirectory, metaData) {
|
override val labelOverride: String? = null,
|
||||||
override val key: String = "nextcloud://$server/$fileId"
|
) : File {
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): NextcloudFile {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val key: String = "$domain://$server/$fileId"
|
||||||
|
|
||||||
override val isStoredInCloud: Boolean
|
override val isStoredInCloud: Boolean
|
||||||
get() = true
|
get() = true
|
||||||
|
|
||||||
override val providerIconRes = R.drawable.ic_badge_nextcloud
|
override val providerIconRes = R.drawable.ic_badge_nextcloud
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
private fun getLaunchIntent(context: Context): Intent {
|
||||||
return Intent(Intent.ACTION_VIEW).apply {
|
return Intent(Intent.ACTION_VIEW).apply {
|
||||||
data = Uri.parse("$server/f/$id")
|
data = Uri.parse("$server/f/$fileId")
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
`package` = getNextcloudAppPackage(context)
|
`package` = getNextcloudAppPackage(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(getLaunchIntent(context), options)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val Domain = "nextcloud"
|
||||||
private fun getNextcloudAppPackage(context: Context): String? {
|
private fun getNextcloudAppPackage(context: Context): String? {
|
||||||
val candidates = listOf("com.nextcloud.client", "com.nextcloud.android.beta")
|
val candidates = listOf("com.nextcloud.client", "com.nextcloud.android.beta")
|
||||||
|
|
||||||
|
|||||||
@ -3,29 +3,46 @@ package de.mm20.launcher2.search.data
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import de.mm20.launcher2.files.R
|
import de.mm20.launcher2.files.R
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
|
||||||
class OneDriveFile(
|
data class OneDriveFile(
|
||||||
val fileId: String,
|
val fileId: String,
|
||||||
override val label: String,
|
override val label: String,
|
||||||
path: String,
|
override val path: String,
|
||||||
mimeType: String,
|
override val mimeType: String,
|
||||||
size: Long,
|
override val size: Long,
|
||||||
isDirectory: Boolean,
|
override val isDirectory: Boolean,
|
||||||
metaData: List<Pair<Int, String>>,
|
override val metaData: List<Pair<Int, String>>,
|
||||||
val webUrl: String
|
val webUrl: String,
|
||||||
) : File(0, path, mimeType, size, isDirectory, metaData) {
|
override val labelOverride: String? = null,
|
||||||
|
) : File {
|
||||||
|
|
||||||
override val key: String = "onedrive://$fileId"
|
override fun overrideLabel(label: String): OneDriveFile {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val key: String = "$domain://$fileId"
|
||||||
|
|
||||||
override val providerIconRes = R.drawable.ic_badge_onedrive
|
override val providerIconRes = R.drawable.ic_badge_onedrive
|
||||||
|
|
||||||
override val isStoredInCloud = true
|
override val isStoredInCloud = true
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
private fun getLaunchIntent(): Intent {
|
||||||
return Intent(Intent.ACTION_VIEW).apply {
|
return Intent(Intent.ACTION_VIEW).apply {
|
||||||
data = Uri.parse(webUrl)
|
data = Uri.parse(webUrl)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(getLaunchIntent(), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val Domain = "onedrive"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -3,29 +3,46 @@ package de.mm20.launcher2.search.data
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import de.mm20.launcher2.files.R
|
import de.mm20.launcher2.files.R
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
|
||||||
class OwncloudFile(
|
data class OwncloudFile(
|
||||||
fileId: Long,
|
val fileId: Long,
|
||||||
override val label: String,
|
override val label: String,
|
||||||
path: String,
|
override val path: String,
|
||||||
mimeType: String,
|
override val mimeType: String,
|
||||||
size: Long,
|
override val size: Long,
|
||||||
isDirectory: Boolean,
|
override val isDirectory: Boolean,
|
||||||
val server: String,
|
val server: String,
|
||||||
metaData: List<Pair<Int, String>>
|
override val metaData: List<Pair<Int, String>>,
|
||||||
) : File(fileId, path, mimeType, size, isDirectory, metaData) {
|
override val labelOverride: String? = null,
|
||||||
override val key: String = "owncloud://$server/$fileId"
|
) : File {
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): OwncloudFile {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val key: String = "$domain://$server/$fileId"
|
||||||
|
|
||||||
override val isStoredInCloud: Boolean
|
override val isStoredInCloud: Boolean
|
||||||
get() = true
|
get() = true
|
||||||
|
|
||||||
override val providerIconRes = R.drawable.ic_badge_owncloud
|
override val providerIconRes = R.drawable.ic_badge_owncloud
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
private fun getLaunchIntent(): Intent {
|
||||||
return Intent(Intent.ACTION_VIEW).apply {
|
return Intent(Intent.ACTION_VIEW).apply {
|
||||||
data = Uri.parse("$server/f/$id")
|
data = Uri.parse("$server/f/$fileId")
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(getLaunchIntent(), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val Domain = "owncloud"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -50,7 +50,6 @@ dependencies {
|
|||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":applications"))
|
implementation(project(":applications"))
|
||||||
implementation(project(":crashreporter"))
|
implementation(project(":crashreporter"))
|
||||||
api(project(":customattrs"))
|
api(project(":customattrs"))
|
||||||
|
|||||||
@ -13,8 +13,9 @@ import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
|
|||||||
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
|
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
|
||||||
import de.mm20.launcher2.icons.transformations.transform
|
import de.mm20.launcher2.icons.transformations.transform
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -98,7 +99,7 @@ class IconRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = channelFlow {
|
fun getIcon(searchable: PinnableSearchable, size: Int): Flow<LauncherIcon> = channelFlow {
|
||||||
iconProviders.collectLatest { providers ->
|
iconProviders.collectLatest { providers ->
|
||||||
transformations.collectLatest { transformations ->
|
transformations.collectLatest { transformations ->
|
||||||
customAttributesRepository.getCustomIcon(searchable).collectLatest { customIcon ->
|
customAttributesRepository.getCustomIcon(searchable).collectLatest { customIcon ->
|
||||||
@ -189,7 +190,7 @@ class IconRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCustomIconSuggestions(
|
suspend fun getCustomIconSuggestions(
|
||||||
searchable: Searchable,
|
searchable: PinnableSearchable,
|
||||||
size: Int
|
size: Int
|
||||||
): List<CustomIconWithPreview> {
|
): List<CustomIconWithPreview> {
|
||||||
val suggestions = mutableListOf<CustomIconWithPreview>()
|
val suggestions = mutableListOf<CustomIconWithPreview>()
|
||||||
@ -301,7 +302,7 @@ class IconRepository(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUncustomizedDefaultIcon(searchable: Searchable, size: Int): CustomIconWithPreview? {
|
suspend fun getUncustomizedDefaultIcon(searchable: PinnableSearchable, size: Int): CustomIconWithPreview? {
|
||||||
val icon = iconProviders.first().getFirstIcon(searchable, size)
|
val icon = iconProviders.first().getFirstIcon(searchable, size)
|
||||||
?.transform(transformations.first()) ?: return null
|
?.transform(transformations.first()) ?: return null
|
||||||
return CustomIconWithPreview(
|
return CustomIconWithPreview(
|
||||||
@ -338,7 +339,7 @@ class IconRepository(
|
|||||||
return iconPackIcons + themedIcons
|
return iconPackIcons + themedIcons
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) {
|
fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?) {
|
||||||
customAttributesRepository.setCustomIcon(searchable, icon)
|
customAttributesRepository.setCustomIcon(searchable, icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,16 +3,16 @@ package de.mm20.launcher2.icons.providers
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import de.mm20.launcher2.icons.DynamicCalendarIcon
|
import de.mm20.launcher2.icons.DynamicCalendarIcon
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
|
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
|
|
||||||
class CalendarIconProvider(val context: Context): IconProvider {
|
class CalendarIconProvider(val context: Context): IconProvider {
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
|
||||||
if(searchable !is Application) return null
|
if(searchable !is LauncherApp) return null
|
||||||
val component = ComponentName(searchable.`package`, searchable.activity)
|
val component = ComponentName(searchable.`package`, searchable.activity)
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
val ai = try {
|
val ai = try {
|
||||||
|
|||||||
@ -4,14 +4,14 @@ import android.content.ComponentName
|
|||||||
import de.mm20.launcher2.customattrs.CustomIconPackIcon
|
import de.mm20.launcher2.customattrs.CustomIconPackIcon
|
||||||
import de.mm20.launcher2.icons.IconPackManager
|
import de.mm20.launcher2.icons.IconPackManager
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
class CustomIconPackIconProvider(
|
class CustomIconPackIconProvider(
|
||||||
private val customIcon: CustomIconPackIcon,
|
private val customIcon: CustomIconPackIcon,
|
||||||
private val iconPackManager: IconPackManager,
|
private val iconPackManager: IconPackManager,
|
||||||
) : IconProvider {
|
) : IconProvider {
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
|
||||||
return iconPackManager.getIcon(
|
return iconPackManager.getIcon(
|
||||||
customIcon.iconPackPackage,
|
customIcon.iconPackPackage,
|
||||||
ComponentName.unflattenFromString(customIcon.iconComponentName) ?: return null
|
ComponentName.unflattenFromString(customIcon.iconComponentName) ?: return null
|
||||||
|
|||||||
@ -3,13 +3,14 @@ package de.mm20.launcher2.icons.providers
|
|||||||
import de.mm20.launcher2.customattrs.CustomThemedIcon
|
import de.mm20.launcher2.customattrs.CustomThemedIcon
|
||||||
import de.mm20.launcher2.icons.IconPackManager
|
import de.mm20.launcher2.icons.IconPackManager
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
class CustomThemedIconProvider(
|
class CustomThemedIconProvider(
|
||||||
private val customIcon: CustomThemedIcon,
|
private val customIcon: CustomThemedIcon,
|
||||||
private val iconPackManager: IconPackManager,
|
private val iconPackManager: IconPackManager,
|
||||||
): IconProvider {
|
): IconProvider {
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
|
||||||
return iconPackManager.getThemedIcon(customIcon.iconPackageName)
|
return iconPackManager.getThemedIcon(customIcon.iconPackageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,13 +8,13 @@ import android.graphics.drawable.LayerDrawable
|
|||||||
import android.graphics.drawable.RotateDrawable
|
import android.graphics.drawable.RotateDrawable
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlin.math.roundToInt
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
|
|
||||||
class GoogleClockIconProvider(val context: Context) : IconProvider {
|
class GoogleClockIconProvider(val context: Context) : IconProvider {
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
|
||||||
if (searchable !is Application) return null
|
if (searchable !is LauncherApp) return null
|
||||||
if (searchable.`package` != "com.google.android.deskclock") return null
|
if (searchable.`package` != "com.google.android.deskclock") return null
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
val appInfo = try {
|
val appInfo = try {
|
||||||
|
|||||||
@ -3,8 +3,9 @@ package de.mm20.launcher2.icons.providers
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ class IconPackIconProvider(
|
|||||||
private val iconPack: String,
|
private val iconPack: String,
|
||||||
private val iconPackManager: IconPackManager,
|
private val iconPackManager: IconPackManager,
|
||||||
): IconProvider {
|
): IconProvider {
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
|
||||||
if (searchable !is LauncherApp) return null
|
if (searchable !is LauncherApp) return null
|
||||||
|
|
||||||
val component = ComponentName(searchable.`package`, searchable.activity)
|
val component = ComponentName(searchable.`package`, searchable.activity)
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
package de.mm20.launcher2.icons.providers
|
package de.mm20.launcher2.icons.providers
|
||||||
|
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
interface IconProvider {
|
interface IconProvider {
|
||||||
suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon?
|
suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon?
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun Iterable<IconProvider>.getFirstIcon(
|
internal suspend fun Iterable<IconProvider>.getFirstIcon(
|
||||||
searchable: Searchable,
|
searchable: PinnableSearchable,
|
||||||
size: Int
|
size: Int
|
||||||
): LauncherIcon? {
|
): LauncherIcon? {
|
||||||
for (provider in this) {
|
for (provider in this) {
|
||||||
|
|||||||
@ -2,10 +2,11 @@ package de.mm20.launcher2.icons.providers
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
class PlaceholderIconProvider(val context: Context) : IconProvider {
|
class PlaceholderIconProvider(val context: Context) : IconProvider {
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon {
|
||||||
return searchable.getPlaceholderIcon(context)
|
return searchable.getPlaceholderIcon(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,13 +2,14 @@ package de.mm20.launcher2.icons.providers
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
class SystemIconProvider(
|
class SystemIconProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val themedIcons: Boolean,
|
private val themedIcons: Boolean,
|
||||||
) : IconProvider {
|
) : IconProvider {
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
|
||||||
return searchable.loadIcon(context, size, themedIcons)
|
return searchable.loadIcon(context, size, themedIcons)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,25 +1,16 @@
|
|||||||
package de.mm20.launcher2.icons.providers
|
package de.mm20.launcher2.icons.providers
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.drawable.LayerDrawable
|
|
||||||
import android.graphics.drawable.RotateDrawable
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
|
|
||||||
internal class ThemedIconProvider(
|
internal class ThemedIconProvider(
|
||||||
private val iconPackManager: IconPackManager,
|
private val iconPackManager: IconPackManager,
|
||||||
) : IconProvider {
|
) : IconProvider {
|
||||||
|
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
|
||||||
if (searchable !is Application) return null
|
if (searchable !is LauncherApp) return null
|
||||||
return iconPackManager.getThemedIcon(searchable.`package`)
|
return iconPackManager.getThemedIcon(searchable.`package`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,13 +2,14 @@ package de.mm20.launcher2.icons.providers
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
internal class ThemedPlaceholderIconProvider(
|
internal class ThemedPlaceholderIconProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
) : IconProvider {
|
) : IconProvider {
|
||||||
|
|
||||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon {
|
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon {
|
||||||
val icon = searchable.getPlaceholderIcon(context)
|
val icon = searchable.getPlaceholderIcon(context)
|
||||||
|
|
||||||
return StaticLauncherIcon(
|
return StaticLauncherIcon(
|
||||||
|
|||||||
@ -43,6 +43,5 @@ dependencies {
|
|||||||
|
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":crashreporter"))
|
implementation(project(":crashreporter"))
|
||||||
}
|
}
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package de.mm20.launcher2.search
|
|
||||||
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
|
||||||
|
|
||||||
interface SearchableDeserializer {
|
|
||||||
fun deserialize(serialized: String): Searchable?
|
|
||||||
}
|
|
||||||
|
|
||||||
class NullDeserializer: SearchableDeserializer {
|
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
package de.mm20.launcher2.search.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
|
||||||
import de.mm20.launcher2.ktx.romanize
|
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
|
||||||
import de.mm20.launcher2.search.R
|
|
||||||
import java.text.Collator
|
|
||||||
|
|
||||||
abstract class Searchable : Comparable<Searchable> {
|
|
||||||
|
|
||||||
abstract val key: String
|
|
||||||
abstract val label: String
|
|
||||||
|
|
||||||
var labelOverride: String? = null
|
|
||||||
|
|
||||||
open fun serialize(): String = ""
|
|
||||||
|
|
||||||
open fun getLaunchIntent(context: Context): Intent? = null
|
|
||||||
|
|
||||||
open fun launch(context: Context, options: Bundle?): Boolean {
|
|
||||||
val intent = getLaunchIntent(context) ?: return false
|
|
||||||
return if (context.tryStartActivity(intent, options)) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.error_activity_not_found, label),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open suspend fun loadIcon(
|
|
||||||
context: Context,
|
|
||||||
size: Int,
|
|
||||||
themed: Boolean,
|
|
||||||
): LauncherIcon? = null
|
|
||||||
|
|
||||||
abstract fun getPlaceholderIcon(context: Context): StaticLauncherIcon
|
|
||||||
|
|
||||||
override fun compareTo(other: Searchable): Int {
|
|
||||||
val label1 = labelOverride ?: label
|
|
||||||
val label2 = other.labelOverride ?: other.label
|
|
||||||
return Collator.getInstance().apply { strength = Collator.SECONDARY }
|
|
||||||
.compare(label1.romanize(), label2.romanize())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
return if (other is Searchable) key == other.key && label == other.label && labelOverride == other.labelOverride
|
|
||||||
else super.equals(other)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = key.hashCode()
|
|
||||||
result = 31 * result + label.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "$label ($key)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,7 +5,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import de.mm20.launcher2.customattrs.CustomAttributesRepository
|
import de.mm20.launcher2.customattrs.CustomAttributesRepository
|
||||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.Tag
|
import de.mm20.launcher2.search.data.Tag
|
||||||
import de.mm20.launcher2.ui.utils.withCustomLabels
|
import de.mm20.launcher2.ui.utils.withCustomLabels
|
||||||
import de.mm20.launcher2.widgets.WidgetRepository
|
import de.mm20.launcher2.widgets.WidgetRepository
|
||||||
@ -32,7 +33,7 @@ open class FavoritesVM : ViewModel(), KoinComponent {
|
|||||||
it.filterIsInstance<Tag>()
|
it.filterIsInstance<Tag>()
|
||||||
}
|
}
|
||||||
|
|
||||||
val favorites: Flow<List<Searchable>> = selectedTag.flatMapLatest { tag ->
|
val favorites: Flow<List<PinnableSearchable>> = selectedTag.flatMapLatest { tag ->
|
||||||
if (tag == null) {
|
if (tag == null) {
|
||||||
val columns = dataStore.data.map { it.grid.columnCount }
|
val columns = dataStore.data.map { it.grid.columnCount }
|
||||||
val excludeCalendar = widgetRepository.isCalendarWidgetEnabled()
|
val excludeCalendar = widgetRepository.isCalendarWidgetEnabled()
|
||||||
|
|||||||
@ -71,7 +71,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.drawOutline
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
@ -90,7 +89,8 @@ import androidx.compose.ui.unit.toSize
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||||
@ -739,7 +739,7 @@ fun ShortcutPicker(viewModel: EditFavoritesSheetVM) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface FavoritesSheetGridItem {
|
sealed interface FavoritesSheetGridItem {
|
||||||
class Favorite(val item: Searchable) : FavoritesSheetGridItem
|
class Favorite(val item: PinnableSearchable) : FavoritesSheetGridItem
|
||||||
class Divider(val section: FavoritesSheetSection) : FavoritesSheetGridItem
|
class Divider(val section: FavoritesSheetSection) : FavoritesSheetGridItem
|
||||||
class Spacer(val span: Int = 1) : FavoritesSheetGridItem
|
class Spacer(val span: Int = 1) : FavoritesSheetGridItem
|
||||||
object EmptySection : FavoritesSheetGridItem
|
object EmptySection : FavoritesSheetGridItem
|
||||||
|
|||||||
@ -17,12 +17,12 @@ import de.mm20.launcher2.favorites.FavoritesRepository
|
|||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.normalize
|
import de.mm20.launcher2.ktx.normalize
|
||||||
import de.mm20.launcher2.ktx.romanize
|
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.Tag
|
import de.mm20.launcher2.search.data.Tag
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@ -48,9 +48,9 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
val createShortcutTarget = MutableLiveData<FavoritesSheetSection?>(null)
|
val createShortcutTarget = MutableLiveData<FavoritesSheetSection?>(null)
|
||||||
|
|
||||||
private var manuallySorted: MutableList<Searchable> = mutableListOf()
|
private var manuallySorted: MutableList<PinnableSearchable> = mutableListOf()
|
||||||
private var automaticallySorted: MutableList<Searchable> = mutableListOf()
|
private var automaticallySorted: MutableList<PinnableSearchable> = mutableListOf()
|
||||||
private var frequentlyUsed: MutableList<Searchable> = mutableListOf()
|
private var frequentlyUsed: MutableList<PinnableSearchable> = mutableListOf()
|
||||||
|
|
||||||
val pinnedTags = MutableLiveData<List<Tag>>(emptyList())
|
val pinnedTags = MutableLiveData<List<Tag>>(emptyList())
|
||||||
val availableTags = MutableLiveData<List<Tag>>(emptyList())
|
val availableTags = MutableLiveData<List<Tag>>(emptyList())
|
||||||
@ -179,7 +179,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon?> {
|
fun getIcon(searchable: PinnableSearchable, size: Int): Flow<LauncherIcon?> {
|
||||||
return iconRepository.getIcon(searchable, size)
|
return iconRepository.getIcon(searchable, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,14 +16,15 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HiddenItemsSheet(
|
fun HiddenItemsSheet(
|
||||||
items: List<Searchable>,
|
items: List<PinnableSearchable>,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val viewModel: HiddenItemsSheetVM = viewModel()
|
val viewModel: HiddenItemsSheetVM = viewModel()
|
||||||
|
|||||||
@ -43,7 +43,8 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.LauncherCard
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||||
@ -381,7 +382,7 @@ fun SearchColumn(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun LazyListScope.GridResults(
|
fun LazyListScope.GridResults(
|
||||||
items: ImmutableList<Searchable>,
|
items: ImmutableList<PinnableSearchable>,
|
||||||
columns: Int,
|
columns: Int,
|
||||||
reverse: Boolean,
|
reverse: Boolean,
|
||||||
showLabels: Boolean,
|
showLabels: Boolean,
|
||||||
@ -446,7 +447,7 @@ fun LazyListScope.GridResults(
|
|||||||
@Composable
|
@Composable
|
||||||
fun GridRow(
|
fun GridRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
items: ImmutableList<Searchable>,
|
items: ImmutableList<PinnableSearchable>,
|
||||||
columns: Int,
|
columns: Int,
|
||||||
showLabels: Boolean,
|
showLabels: Boolean,
|
||||||
) {
|
) {
|
||||||
@ -470,7 +471,7 @@ fun GridRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun LazyListScope.ListResults(
|
fun LazyListScope.ListResults(
|
||||||
items: ImmutableList<Searchable>,
|
items: ImmutableList<PinnableSearchable>,
|
||||||
reverse: Boolean,
|
reverse: Boolean,
|
||||||
key: String,
|
key: String,
|
||||||
before: (@Composable () -> Unit)? = null,
|
before: (@Composable () -> Unit)? = null,
|
||||||
@ -523,7 +524,7 @@ fun LazyListScope.ListResults(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ListRow(
|
fun ListRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: Searchable,
|
item: PinnableSearchable,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.padding(
|
modifier = modifier.padding(
|
||||||
|
|||||||
@ -16,12 +16,13 @@ import de.mm20.launcher2.files.FileRepository
|
|||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.WebsearchRepository
|
import de.mm20.launcher2.search.WebsearchRepository
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.*
|
||||||
import de.mm20.launcher2.ui.utils.withCustomLabels
|
import de.mm20.launcher2.ui.utils.withCustomLabels
|
||||||
import de.mm20.launcher2.unitconverter.UnitConverterRepository
|
import de.mm20.launcher2.unitconverter.UnitConverterRepository
|
||||||
import de.mm20.launcher2.websites.WebsiteRepository
|
import de.mm20.launcher2.websites.WebsiteRepository
|
||||||
import de.mm20.launcher2.widgets.WidgetRepository
|
|
||||||
import de.mm20.launcher2.wikipedia.WikipediaRepository
|
import de.mm20.launcher2.wikipedia.WikipediaRepository
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@ -52,8 +53,8 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
val showLabels = dataStore.data.map { it.grid.showLabels }.asLiveData()
|
val showLabels = dataStore.data.map { it.grid.showLabels }.asLiveData()
|
||||||
|
|
||||||
val appResults = MutableLiveData<List<Application>>(emptyList())
|
val appResults = MutableLiveData<List<LauncherApp>>(emptyList())
|
||||||
val workAppResults = MutableLiveData<List<Application>>(emptyList())
|
val workAppResults = MutableLiveData<List<LauncherApp>>(emptyList())
|
||||||
val appShortcutResults = MutableLiveData<List<AppShortcut>>(emptyList())
|
val appShortcutResults = MutableLiveData<List<AppShortcut>>(emptyList())
|
||||||
val fileResults = MutableLiveData<List<File>>(emptyList())
|
val fileResults = MutableLiveData<List<File>>(emptyList())
|
||||||
val contactResults = MutableLiveData<List<Contact>>(emptyList())
|
val contactResults = MutableLiveData<List<Contact>>(emptyList())
|
||||||
@ -64,7 +65,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
val unitConverterResult = MutableLiveData<UnitConverter?>(null)
|
val unitConverterResult = MutableLiveData<UnitConverter?>(null)
|
||||||
val websearchResults = MutableLiveData<List<Websearch>>(emptyList())
|
val websearchResults = MutableLiveData<List<Websearch>>(emptyList())
|
||||||
|
|
||||||
val hiddenResults = MutableLiveData<List<Searchable>>(emptyList())
|
val hiddenResults = MutableLiveData<List<PinnableSearchable>>(emptyList())
|
||||||
|
|
||||||
val favoritesEnabled = dataStore.data.map { it.favorites.enabled }
|
val favoritesEnabled = dataStore.data.map { it.favorites.enabled }
|
||||||
val hideFavorites = MutableLiveData(false)
|
val hideFavorites = MutableLiveData(false)
|
||||||
@ -95,7 +96,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
val customAttrResults = customAttributesRepository.search(query)
|
val customAttrResults = customAttributesRepository.search(query)
|
||||||
.combine(dataStore.data) { items, settings ->
|
.combine(dataStore.data) { items, settings ->
|
||||||
items.filter {
|
items.filter {
|
||||||
it is Application
|
it is LauncherApp
|
||||||
|| it is Contact && settings.contactsSearch.enabled
|
|| it is Contact && settings.contactsSearch.enabled
|
||||||
|| it is CalendarEvent && settings.calendarSearch.enabled
|
|| it is CalendarEvent && settings.calendarSearch.enabled
|
||||||
|| it is AppShortcut && settings.appShortcutSearch.enabled
|
|| it is AppShortcut && settings.appShortcutSearch.enabled
|
||||||
@ -285,15 +286,15 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private inline fun <reified T : Searchable> Flow<List<T>>.withCustomAttributeResults(
|
private inline fun <reified T : PinnableSearchable> Flow<List<T>>.withCustomAttributeResults(
|
||||||
customAttributeResults: Flow<List<Searchable>>
|
customAttributeResults: Flow<List<PinnableSearchable>>
|
||||||
): Flow<List<T>> {
|
): Flow<List<T>> {
|
||||||
return this.combine(customAttributeResults) { items, items2 ->
|
return this.combine(customAttributeResults) { items, items2 ->
|
||||||
(items + items2.filterIsInstance<T>()).distinctBy { it.key }
|
(items + items2.filterIsInstance<T>()).distinctBy { it.key }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T : Searchable> Flow<List<T>>.collectWithHiddenItems(
|
private suspend fun <T : PinnableSearchable> Flow<List<T>>.collectWithHiddenItems(
|
||||||
hiddenItemKeys: Flow<List<String>>,
|
hiddenItemKeys: Flow<List<String>>,
|
||||||
action: (items: List<T>, hidden: List<T>) -> Unit
|
action: (items: List<T>, hidden: List<T>) -> Unit
|
||||||
) {
|
) {
|
||||||
@ -305,18 +306,18 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Searchable> Flow<List<T>>.sorted(): Flow<List<T>> = this.map { it.sorted() }
|
private fun <T : PinnableSearchable> Flow<List<T>>.sorted(): Flow<List<T>> = this.map { it.sorted() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class HiddenItemResults(
|
private data class HiddenItemResults(
|
||||||
val apps: List<Application> = emptyList(),
|
val apps: List<LauncherApp> = emptyList(),
|
||||||
val contacts: List<Contact> = emptyList(),
|
val contacts: List<Contact> = emptyList(),
|
||||||
val calendarEvents: List<CalendarEvent> = emptyList(),
|
val calendarEvents: List<CalendarEvent> = emptyList(),
|
||||||
val files: List<File> = emptyList(),
|
val files: List<File> = emptyList(),
|
||||||
val appShortcuts: List<AppShortcut> = emptyList(),
|
val appShortcuts: List<AppShortcut> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun joinToList(): List<Searchable> {
|
fun joinToList(): List<PinnableSearchable> {
|
||||||
return apps + contacts + calendarEvents + files + appShortcuts
|
return apps + contacts + calendarEvents + files + appShortcuts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,7 +23,7 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.*
|
import de.mm20.launcher2.ui.component.*
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
@ -40,7 +40,7 @@ import kotlin.math.roundToInt
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppItem(
|
fun AppItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
app: Application,
|
app: LauncherApp,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val viewModel = remember { AppItemVM(app) }
|
val viewModel = remember { AppItemVM(app) }
|
||||||
@ -336,7 +336,7 @@ fun AppItem(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppItemGridPopup(
|
fun AppItemGridPopup(
|
||||||
app: Application,
|
app: LauncherApp,
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
animationProgress: Float,
|
animationProgress: Float,
|
||||||
origin: Rect,
|
origin: Rect,
|
||||||
|
|||||||
@ -8,16 +8,13 @@ import android.content.pm.*
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.provider.Settings
|
|
||||||
import android.service.notification.StatusBarNotification
|
import android.service.notification.StatusBarNotification
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
|
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
|
||||||
import de.mm20.launcher2.notifications.NotificationRepository
|
import de.mm20.launcher2.notifications.NotificationRepository
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.Application
|
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -28,7 +25,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class AppItemVM(
|
class AppItemVM(
|
||||||
private val app: Application
|
private val app: LauncherApp
|
||||||
) : SearchableItemVM(app) {
|
) : SearchableItemVM(app) {
|
||||||
private val notificationRepository: NotificationRepository by inject()
|
private val notificationRepository: NotificationRepository by inject()
|
||||||
private val appShortcutRepository: AppShortcutRepository by inject()
|
private val appShortcutRepository: AppShortcutRepository by inject()
|
||||||
@ -44,21 +41,12 @@ class AppItemVM(
|
|||||||
fun openAppInfo(context: Context) {
|
fun openAppInfo(context: Context) {
|
||||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||||
|
|
||||||
if (app is LauncherApp) {
|
launcherApps.startAppDetailsActivity(
|
||||||
launcherApps.startAppDetailsActivity(
|
ComponentName(app.`package`, app.activity),
|
||||||
ComponentName(app.`package`, app.activity),
|
app.getUser(),
|
||||||
app.getUser(),
|
null,
|
||||||
null,
|
null
|
||||||
null
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
context.tryStartActivity(
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.parse("package:${app.`package`}")
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun shareApkFile(context: Context) {
|
suspend fun shareApkFile(context: Context) {
|
||||||
@ -128,9 +116,7 @@ class AppItemVM(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val shortcuts = flow {
|
val shortcuts = flow {
|
||||||
if (app is LauncherApp) {
|
emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5))
|
||||||
emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
|
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
|
||||||
|
|||||||
@ -10,15 +10,15 @@ import de.mm20.launcher2.favorites.FavoritesRepository
|
|||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
abstract class SearchableItemVM(
|
abstract class SearchableItemVM(
|
||||||
private val searchable: Searchable
|
private val searchable: PinnableSearchable
|
||||||
) : KoinComponent {
|
) : KoinComponent {
|
||||||
protected val favoritesRepository: FavoritesRepository by inject()
|
protected val favoritesRepository: FavoritesRepository by inject()
|
||||||
protected val badgeRepository: BadgeRepository by inject()
|
protected val badgeRepository: BadgeRepository by inject()
|
||||||
@ -74,7 +74,7 @@ abstract class SearchableItemVM(
|
|||||||
if (searchable.launch(context, bundle)) {
|
if (searchable.launch(context, bundle)) {
|
||||||
favoritesRepository.incrementLaunchCounter(searchable)
|
favoritesRepository.incrementLaunchCounter(searchable)
|
||||||
return true
|
return true
|
||||||
} else if (searchable is Application || searchable is AppShortcut) {
|
} else if (searchable is LauncherApp || searchable is AppShortcut) {
|
||||||
favoritesRepository.remove(searchable)
|
favoritesRepository.remove(searchable)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -23,7 +23,8 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
import de.mm20.launcher2.icons.CustomIconWithPreview
|
import de.mm20.launcher2.icons.CustomIconWithPreview
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
@ -35,7 +36,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomizeSearchableSheet(
|
fun CustomizeSearchableSheet(
|
||||||
searchable: Searchable,
|
searchable: PinnableSearchable,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val viewModel: CustomizeSearchableSheetVM =
|
val viewModel: CustomizeSearchableSheetVM =
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import de.mm20.launcher2.customattrs.CustomAttributesRepository
|
import de.mm20.launcher2.customattrs.CustomAttributesRepository
|
||||||
import de.mm20.launcher2.customattrs.CustomIcon
|
import de.mm20.launcher2.customattrs.CustomIcon
|
||||||
import de.mm20.launcher2.customattrs.customAttrsModule
|
|
||||||
import de.mm20.launcher2.icons.CustomIconWithPreview
|
import de.mm20.launcher2.icons.CustomIconWithPreview
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -16,7 +16,7 @@ import org.koin.core.component.inject
|
|||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
class CustomizeSearchableSheetVM(
|
class CustomizeSearchableSheetVM(
|
||||||
private val searchable: Searchable
|
private val searchable: PinnableSearchable
|
||||||
) : KoinComponent {
|
) : KoinComponent {
|
||||||
private val iconRepository: IconRepository by inject()
|
private val iconRepository: IconRepository by inject()
|
||||||
private val customAttributesRepository: CustomAttributesRepository by inject()
|
private val customAttributesRepository: CustomAttributesRepository by inject()
|
||||||
|
|||||||
@ -2,19 +2,15 @@ package de.mm20.launcher2.ui.launcher.search.common.grid
|
|||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
@ -26,6 +22,8 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.*
|
||||||
import de.mm20.launcher2.ui.component.LauncherCard
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
@ -47,7 +45,7 @@ import kotlinx.coroutines.delay
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GridItem(modifier: Modifier = Modifier, item: Searchable, showLabels: Boolean = true) {
|
fun GridItem(modifier: Modifier = Modifier, item: PinnableSearchable, showLabels: Boolean = true) {
|
||||||
val viewModel = remember(item.key) { GridItemVM(item) }
|
val viewModel = remember(item.key) { GridItemVM(item) }
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -59,13 +57,11 @@ fun GridItem(modifier: Modifier = Modifier, item: Searchable, showLabels: Boolea
|
|||||||
val iconSize = LocalGridIconSize.current.toPixels()
|
val iconSize = LocalGridIconSize.current.toPixels()
|
||||||
val icon by remember(item.key) { viewModel.getIcon(iconSize.toInt()) }.collectAsState(null)
|
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.preferDetailsOverLaunch
|
||||||
val launchOnPress =
|
|
||||||
item is File || item is Application || item is AppShortcut || item is Website || item is Wikipedia
|
|
||||||
|
|
||||||
val windowSize = LocalWindowSize.current
|
val windowSize = LocalWindowSize.current
|
||||||
|
|
||||||
if (item is Application) {
|
if (item is LauncherApp) {
|
||||||
HandleHomeTransition {
|
HandleHomeTransition {
|
||||||
val cn = ComponentName(item.`package`, item.activity)
|
val cn = ComponentName(item.`package`, item.activity)
|
||||||
if (
|
if (
|
||||||
@ -164,7 +160,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
|||||||
.wrapContentSize()
|
.wrapContentSize()
|
||||||
) {
|
) {
|
||||||
when (searchable) {
|
when (searchable) {
|
||||||
is Application -> {
|
is LauncherApp -> {
|
||||||
AppItemGridPopup(
|
AppItemGridPopup(
|
||||||
app = searchable,
|
app = searchable,
|
||||||
show = show,
|
show = show,
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search.common.grid
|
package de.mm20.launcher2.ui.launcher.search.common.grid
|
||||||
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
|
||||||
class GridItemVM(
|
class GridItemVM(
|
||||||
searchable: Searchable
|
searchable: PinnableSearchable
|
||||||
): SearchableItemVM(searchable)
|
): SearchableItemVM(searchable)
|
||||||
@ -5,14 +5,15 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridColumns
|
import de.mm20.launcher2.ui.locals.LocalGridColumns
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchResultGrid(
|
fun SearchResultGrid(
|
||||||
items: List<Searchable>,
|
items: List<PinnableSearchable>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showLabels: Boolean = true,
|
showLabels: Boolean = true,
|
||||||
columns: Int = LocalGridColumns.current,
|
columns: Int = LocalGridColumns.current,
|
||||||
|
|||||||
@ -2,13 +2,14 @@ package de.mm20.launcher2.ui.launcher.search.common.list
|
|||||||
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.*
|
||||||
import de.mm20.launcher2.ui.component.InnerCard
|
import de.mm20.launcher2.ui.component.InnerCard
|
||||||
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
|
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
|
||||||
@ -17,7 +18,7 @@ import de.mm20.launcher2.ui.launcher.search.files.FileItem
|
|||||||
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
|
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListItem(modifier: Modifier = Modifier, item: Searchable) {
|
fun ListItem(modifier: Modifier = Modifier, item: PinnableSearchable) {
|
||||||
var showDetails by remember { mutableStateOf(false) }
|
var showDetails by remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search.common.list
|
package de.mm20.launcher2.ui.launcher.search.common.list
|
||||||
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
|
||||||
|
|
||||||
class ListItemVM(
|
class ListItemVM(
|
||||||
searchable: Searchable
|
searchable: PinnableSearchable
|
||||||
): SearchableItemVM(searchable)
|
): SearchableItemVM(searchable)
|
||||||
@ -8,12 +8,13 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchResultList(
|
fun SearchResultList(
|
||||||
items: List<Searchable>,
|
items: List<PinnableSearchable>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
reverse: Boolean = false
|
reverse: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -57,7 +57,7 @@ fun WebsiteItem(
|
|||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = website.label,
|
text = website.labelOverride ?: website.label,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())
|
||||||
|
|||||||
@ -1,17 +1,5 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.widgets.favorites
|
package de.mm20.launcher2.ui.launcher.widgets.favorites
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
|
||||||
import de.mm20.launcher2.ui.common.FavoritesVM
|
import de.mm20.launcher2.ui.common.FavoritesVM
|
||||||
import de.mm20.launcher2.widgets.WidgetRepository
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
|
|
||||||
class FavoritesWidgetVM: FavoritesVM()
|
class FavoritesWidgetVM: FavoritesVM()
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package de.mm20.launcher2.ui.settings.hiddenitems
|
package de.mm20.launcher2.ui.settings.hiddenitems
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@ -21,7 +20,6 @@ import androidx.compose.ui.unit.DpOffset
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.data.Application
|
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||||
|
|||||||
@ -2,11 +2,8 @@ package de.mm20.launcher2.ui.settings.hiddenitems
|
|||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.LauncherApps
|
import android.content.pm.LauncherApps
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@ -17,10 +14,9 @@ import de.mm20.launcher2.favorites.FavoritesRepository
|
|||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.Application
|
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@ -37,18 +33,18 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
val allApps = appRepository.getAllInstalledApps().map {
|
val allApps = appRepository.getAllInstalledApps().map {
|
||||||
withContext(Dispatchers.Default) { it.sorted() }
|
withContext(Dispatchers.Default) { it.sorted() }
|
||||||
}.asLiveData()
|
}.asLiveData()
|
||||||
val hiddenItems: LiveData<List<Searchable>> = liveData {
|
val hiddenItems: LiveData<List<PinnableSearchable>> = liveData {
|
||||||
val hidden = withContext(Dispatchers.Default) {
|
val hidden = withContext(Dispatchers.Default) {
|
||||||
favoritesRepository.getHiddenItems().first().filter { it !is Application }.sorted()
|
favoritesRepository.getHiddenItems().first().filter { it !is LauncherApp }.sorted()
|
||||||
}
|
}
|
||||||
emit(hidden)
|
emit(hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isHidden(searchable: Searchable): Flow<Boolean> {
|
fun isHidden(searchable: PinnableSearchable): Flow<Boolean> {
|
||||||
return favoritesRepository.isHidden(searchable)
|
return favoritesRepository.isHidden(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setHidden(searchable: Searchable, hidden: Boolean) {
|
fun setHidden(searchable: PinnableSearchable, hidden: Boolean) {
|
||||||
if(hidden) {
|
if(hidden) {
|
||||||
favoritesRepository.hideItem(searchable)
|
favoritesRepository.hideItem(searchable)
|
||||||
} else {
|
} else {
|
||||||
@ -56,11 +52,11 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> {
|
fun getIcon(searchable: PinnableSearchable, size: Int): Flow<LauncherIcon> {
|
||||||
return iconRepository.getIcon(searchable, size)
|
return iconRepository.getIcon(searchable, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(context: Context, searchable: Searchable) {
|
fun launch(context: Context, searchable: PinnableSearchable) {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
if (isAtLeastApiLevel(31)) {
|
if (isAtLeastApiLevel(31)) {
|
||||||
bundle.putInt("android.activity.splashScreenStyle", 1)
|
bundle.putInt("android.activity.splashScreenStyle", 1)
|
||||||
@ -68,23 +64,14 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
searchable.launch(context, bundle)
|
searchable.launch(context, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openAppInfo(context: Context, app: Application) {
|
fun openAppInfo(context: Context, app: LauncherApp) {
|
||||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||||
|
|
||||||
if (app is LauncherApp) {
|
launcherApps.startAppDetailsActivity(
|
||||||
launcherApps.startAppDetailsActivity(
|
ComponentName(app.`package`, app.activity),
|
||||||
ComponentName(app.`package`, app.activity),
|
app.getUser(),
|
||||||
app.getUser(),
|
null,
|
||||||
null,
|
null
|
||||||
null
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
context.tryStartActivity(
|
|
||||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.parse("package:${app.`package`}")
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,23 +1,26 @@
|
|||||||
package de.mm20.launcher2.ui.utils
|
package de.mm20.launcher2.ui.utils
|
||||||
|
|
||||||
import de.mm20.launcher2.customattrs.CustomAttributesRepository
|
import de.mm20.launcher2.customattrs.CustomAttributesRepository
|
||||||
import de.mm20.launcher2.customattrs.CustomLabel
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
fun <T : Searchable> Flow<List<T>>.withCustomLabels(
|
fun <T : PinnableSearchable> Flow<List<T>>.withCustomLabels(
|
||||||
customAttributesRepository: CustomAttributesRepository,
|
customAttributesRepository: CustomAttributesRepository,
|
||||||
): Flow<List<T>> = channelFlow {
|
): Flow<List<T>> = channelFlow {
|
||||||
this@withCustomLabels.collectLatest { items ->
|
this@withCustomLabels.collectLatest { items ->
|
||||||
val customLabels = customAttributesRepository.getCustomLabels(items)
|
val customLabels = customAttributesRepository.getCustomLabels(items)
|
||||||
customLabels.collectLatest { labels ->
|
customLabels.collectLatest { labels ->
|
||||||
for (item in items) {
|
send(items.map { item ->
|
||||||
val customLabel = labels.find { it.key == item.key }
|
val customLabel = labels.find { it.key == item.key }
|
||||||
item.labelOverride = customLabel?.label
|
if (customLabel != null) {
|
||||||
}
|
item.overrideLabel(customLabel.label) as T
|
||||||
send(items)
|
} else {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,7 +45,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":currencies"))
|
implementation(project(":currencies"))
|
||||||
implementation(project(":search"))
|
implementation(project(":base"))
|
||||||
implementation(project(":i18n"))
|
implementation(project(":i18n"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -51,7 +51,6 @@ dependencies {
|
|||||||
implementation(libs.coil.core)
|
implementation(libs.coil.core)
|
||||||
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
|
||||||
|
|||||||
@ -2,26 +2,38 @@ package de.mm20.launcher2.search.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import de.mm20.launcher2.icons.*
|
import de.mm20.launcher2.icons.*
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.websites.R
|
import de.mm20.launcher2.websites.R
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
|
||||||
class Website(
|
data class Website(
|
||||||
override val label: String,
|
override val label: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
val image: String,
|
val image: String,
|
||||||
val favicon: String,
|
val favicon: String,
|
||||||
val color: Int
|
val color: Int,
|
||||||
) : Searchable() {
|
override val labelOverride: String? = null,
|
||||||
|
) : PinnableSearchable {
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val key = "$domain://$url"
|
||||||
|
|
||||||
|
override val preferDetailsOverLaunch: Boolean = false
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): Website {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
override val key = "web://$url"
|
|
||||||
override suspend fun loadIcon(
|
override suspend fun loadIcon(
|
||||||
context: Context,
|
context: Context,
|
||||||
size: Int,
|
size: Int,
|
||||||
@ -68,10 +80,18 @@ class Website(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
private fun getLaunchIntent(): Intent {
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
intent.data = Uri.parse(url)
|
intent.data = Uri.parse(url)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(getLaunchIntent(), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val Domain = "web"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,14 +1,15 @@
|
|||||||
package de.mm20.launcher2.websites
|
package de.mm20.launcher2.websites
|
||||||
|
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.Website
|
import de.mm20.launcher2.search.data.Website
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
class WebsiteSerializer : SearchableSerializer {
|
class WebsiteSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as Website
|
searchable as Website
|
||||||
return jsonObjectOf(
|
return jsonObjectOf(
|
||||||
"label" to searchable.label,
|
"label" to searchable.label,
|
||||||
@ -25,7 +26,7 @@ class WebsiteSerializer : SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WebsiteDeserializer: SearchableDeserializer {
|
class WebsiteDeserializer: SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable? {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
return Website(
|
return Website(
|
||||||
label = json.getString("label"),
|
label = json.getString("label"),
|
||||||
|
|||||||
@ -48,8 +48,8 @@ dependencies {
|
|||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":search"))
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
|
implementation(project(":ktx"))
|
||||||
implementation(project(":crashreporter"))
|
implementation(project(":crashreporter"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4,21 +4,35 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TintedIconLayer
|
import de.mm20.launcher2.icons.TintedIconLayer
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.wikipedia.R
|
import de.mm20.launcher2.wikipedia.R
|
||||||
|
|
||||||
class Wikipedia(
|
data class Wikipedia(
|
||||||
override val label: String,
|
override val label: String,
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val text: String,
|
val text: String,
|
||||||
val image: String?,
|
val image: String?,
|
||||||
val wikipediaUrl: String,
|
val wikipediaUrl: String,
|
||||||
) : Searchable() {
|
override val labelOverride: String? = null,
|
||||||
override val key = "wikipedia://$wikipediaUrl:$id"
|
) : PinnableSearchable {
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val preferDetailsOverLaunch: Boolean = false
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): Wikipedia {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val key = "$domain://$wikipediaUrl:$id"
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||||
return StaticLauncherIcon(
|
return StaticLauncherIcon(
|
||||||
@ -31,7 +45,7 @@ class Wikipedia(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
private fun getLaunchIntent(): Intent {
|
||||||
val intent = CustomTabsIntent
|
val intent = CustomTabsIntent
|
||||||
.Builder()
|
.Builder()
|
||||||
.setToolbarColor(Color.BLACK)
|
.setToolbarColor(Color.BLACK)
|
||||||
@ -42,4 +56,12 @@ class Wikipedia(
|
|||||||
intent.intent.data = Uri.parse(uri)
|
intent.intent.data = Uri.parse(uri)
|
||||||
return intent.intent
|
return intent.intent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
return context.tryStartActivity(getLaunchIntent(), options)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val Domain = "wikipedia"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,14 +1,15 @@
|
|||||||
package de.mm20.launcher2.wikipedia
|
package de.mm20.launcher2.wikipedia
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.search.PinnableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.Wikipedia
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
class WikipediaSerializer : SearchableSerializer {
|
class WikipediaSerializer : SearchableSerializer {
|
||||||
override fun serialize(searchable: Searchable): String {
|
override fun serialize(searchable: PinnableSearchable): String {
|
||||||
searchable as Wikipedia
|
searchable as Wikipedia
|
||||||
val json = JSONObject()
|
val json = JSONObject()
|
||||||
json.put("label", searchable.label)
|
json.put("label", searchable.label)
|
||||||
@ -24,7 +25,7 @@ class WikipediaSerializer : SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WikipediaDeserializer(val context: Context) : SearchableDeserializer {
|
class WikipediaDeserializer(val context: Context) : SearchableDeserializer {
|
||||||
override fun deserialize(serialized: String): Searchable? {
|
override fun deserialize(serialized: String): PinnableSearchable? {
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
return Wikipedia(
|
return Wikipedia(
|
||||||
label = json.getString("label"),
|
label = json.getString("label"),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user