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