Refactor search

This commit is contained in:
MM20 2022-10-15 00:29:01 +02:00
parent 048c1e8cf9
commit 2157147caa
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
42 changed files with 540 additions and 517 deletions

View File

@ -46,7 +46,6 @@ dependencies {
implementation(libs.commons.text)
implementation(project(":base"))
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":compat"))

View File

@ -12,7 +12,6 @@ import android.os.Process
import android.os.UserHandle
import android.util.Log
import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.search.SearchableRepository
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -22,9 +21,10 @@ import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.*
interface AppRepository: SearchableRepository<LauncherApp> {
interface AppRepository {
fun getAllInstalledApps(): Flow<List<LauncherApp>>
fun getSuspendedPackages(): Flow<List<String>>
fun search(query: String): Flow<ImmutableList<LauncherApp>>
}
internal class AppRepositoryImpl(

View File

@ -46,7 +46,6 @@ dependencies {
implementation(project(":applications"))
implementation(project(":permissions"))
implementation(project(":base"))
implementation(project(":preferences"))
implementation(project(":ktx"))
}

View File

@ -3,7 +3,6 @@ package de.mm20.launcher2.appshortcuts
import android.content.Context
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
import android.os.Handler
import android.os.Looper
@ -14,8 +13,6 @@ import androidx.core.content.getSystemService
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
@ -26,12 +23,19 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.*
import java.util.Locale
interface AppShortcutRepository: SearchableRepository<AppShortcut> {
interface AppShortcutRepository {
fun search(query: String): Flow<ImmutableList<AppShortcut>>
suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo,
count: Int = 5
@ -45,7 +49,6 @@ interface AppShortcutRepository: SearchableRepository<AppShortcut> {
internal class AppShortcutRepositoryImpl(
private val context: Context,
private val permissionsManager: PermissionsManager,
private val dataStore: LauncherDataStore,
) : AppShortcutRepository {
private val scope = CoroutineScope(Dispatchers.Default + Job())
@ -89,49 +92,44 @@ internal class AppShortcutRepositoryImpl(
send(persistentListOf())
return@withContext
}
dataStore.data.map { it.appShortcutSearch.enabled }.collectLatest { enabled ->
if (!enabled) {
send(persistentListOf())
return@collectLatest
}
shortcutChangeEmitter.collectLatest {
val launcherApps =
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
persistentListOf()
shortcutChangeEmitter.collectLatest {
val launcherApps =
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
persistentListOf()
)
val shortcutQuery = LauncherApps.ShortcutQuery()
shortcutQuery.setQueryFlags(
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED or
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
)
val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle())
?.filter {
if (it.longLabel != null) {
return@filter matches(it.longLabel.toString(), query)
}
if (it.shortLabel != null) {
return@filter matches(it.shortLabel.toString(), query)
}
return@filter false
} ?: emptyList()
val pm = context.packageManager
send(
shortcuts.mapNotNull {
LauncherShortcut(
context,
it
)
val shortcutQuery = LauncherApps.ShortcutQuery()
shortcutQuery.setQueryFlags(
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED or
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
)
val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle())
?.filter {
if (it.longLabel != null) {
return@filter matches(it.longLabel.toString(), query)
}
if (it.shortLabel != null) {
return@filter matches(it.shortLabel.toString(), query)
}
return@filter false
} ?: emptyList()
val pm = context.packageManager
send(
shortcuts.mapNotNull {
LauncherShortcut(
context,
it
)
}.toImmutableList()
)
}
}.toImmutableList()
)
}
}
}

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val appShortcutsModule = module {
single<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get(), get()) }
single<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get()) }
}

View File

@ -9,6 +9,9 @@ import java.text.Collator
interface PinnableSearchable : Searchable, Comparable<PinnableSearchable> {
val domain: String
val key: String
val label: String
val labelOverride: String?
get() = null

View File

@ -1,6 +1,3 @@
package de.mm20.launcher2.search
interface Searchable {
val domain: String
val key: String
}
interface Searchable

View File

@ -1,8 +0,0 @@
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>>
}

View File

@ -45,7 +45,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":preferences"))
implementation(project(":base"))
}

View File

@ -1,15 +1,11 @@
package de.mm20.launcher2.calculator
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Calculator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.mariuszgromada.math.mxparser.Expression
interface CalculatorRepository {
@ -18,21 +14,14 @@ interface CalculatorRepository {
class CalculatorRepositoryImpl : CalculatorRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
override fun search(query: String): Flow<Calculator?> = channelFlow {
if (query.isBlank()) {
send(null)
return@channelFlow
}
val searchCalculator = dataStore.data.map { it.calculatorSearch.enabled }
searchCalculator.collectLatest {
if (it) {
send(queryCalculator(query))
} else {
send(null)
}
}
send(queryCalculator(query))
}
private suspend fun queryCalculator(query: String): Calculator? {
@ -43,18 +32,21 @@ class CalculatorRepositoryImpl : CalculatorRepository, KoinComponent {
}
Calculator(term = query, solution = solution.toDouble())
}
query.matches(Regex("0b[01]+")) -> {
val solution = query.substring(2).toIntOrNull(2) ?: run {
return null
}
Calculator(term = query, solution = solution.toDouble())
}
query.matches(Regex("0[0-7]+")) -> {
val solution = query.substring(1).toIntOrNull(8) ?: run {
return null
}
Calculator(term = query, solution = solution.toDouble())
}
else -> {
withContext(Dispatchers.IO) {
val exp = Expression(query)

View File

@ -11,12 +11,6 @@ data class Calculator(
val solution: Double
): Searchable {
override val domain: String
get() = "calculator"
override val key: String
get() = "calculator://$term"
val formattedString: String
val formattedBinaryString: String
val formattedHexString: String

View File

@ -42,7 +42,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":base"))
implementation(project(":permissions"))

View File

@ -6,32 +6,36 @@ import android.provider.CalendarContract
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.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.*
import java.util.Calendar
interface CalendarRepository: SearchableRepository<CalendarEvent> {
fun getUpcomingEvents(): Flow<List<CalendarEvent>>
interface CalendarRepository {
fun search(query: String): Flow<ImmutableList<CalendarEvent>>
fun getUpcomingEvents(
excludeCalendars: List<Long>,
excludeAllDayEvents: Boolean
): Flow<List<CalendarEvent>>
suspend fun getCalendars(): List<UserCalendar>
}
internal class CalendarRepositoryImpl(
private val context: Context,
) : CalendarRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject()
private val permissionsManager: PermissionsManager,
) : CalendarRepository {
override fun search(query: String): Flow<ImmutableList<CalendarEvent>> {
if (query.isBlank() || query.length < 3) {
@ -41,10 +45,7 @@ internal class CalendarRepositoryImpl(
}
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
val searchCalendar = dataStore.data.map { it.calendarSearch.enabled }
return combine(hasPermission, searchCalendar) { permission, search ->
permission && search
}.map {
return hasPermission.map {
if (it) {
val now = System.currentTimeMillis()
queryCalendarEvents(
@ -149,25 +150,26 @@ internal class CalendarRepositoryImpl(
return results
}
override fun getUpcomingEvents(): Flow<List<CalendarEvent>> = channelFlow {
override fun getUpcomingEvents(
excludeCalendars: List<Long>,
excludeAllDayEvents: Boolean,
): Flow<List<CalendarEvent>> = channelFlow {
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
hasPermission.collectLatest {
if (it) {
dataStore.data.map { it.calendarWidget }.collectLatest { settings ->
val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) {
queryCalendarEvents(
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
excludeAllDayEvents = settings.hideAlldayEvents,
excludeCalendars = settings.excludeCalendarsList
)
}
send(events)
val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) {
queryCalendarEvents(
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
excludeAllDayEvents = excludeAllDayEvents,
excludeCalendars = excludeCalendars
)
}
send(events)
} else {
send(emptyList())
}

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val calendarModule = module {
single<CalendarRepository> { CalendarRepositoryImpl(androidContext()) }
single<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
}

View File

@ -42,7 +42,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":base"))
implementation(project(":permissions"))

View File

@ -4,8 +4,6 @@ import android.content.Context
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
@ -13,20 +11,17 @@ 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: SearchableRepository<Contact>
interface ContactRepository {
fun search(query: String): Flow<ImmutableList<Contact>>
}
internal class ContactRepositoryImpl(
private val context: Context,
) : ContactRepository, KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager
) : ContactRepository {
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) {
@ -35,9 +30,7 @@ internal class ContactRepositoryImpl(
}
}
return combine(searchContacts, hasPermission) { search, permission ->
search && permission
}.map {
return hasPermission.map {
if (it) {
queryContacts(query)
} else {

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val contactsModule = module {
single<ContactRepository> { ContactRepositoryImpl(androidContext()) }
single<ContactRepository> { ContactRepositoryImpl(androidContext(), get()) }
}

View File

@ -6,6 +6,9 @@ import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.PinnableSearchable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@ -15,6 +18,9 @@ import org.json.JSONException
import java.io.File
interface CustomAttributesRepository {
fun search(query: String): Flow<ImmutableList<PinnableSearchable>>
fun getCustomIcon(searchable: PinnableSearchable): Flow<CustomIcon?>
fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?)
@ -25,8 +31,6 @@ interface CustomAttributesRepository {
fun setTags(searchable: PinnableSearchable, tags: List<String>)
fun getTags(searchable: PinnableSearchable): Flow<List<String>>
suspend fun search(query: String): Flow<List<PinnableSearchable>>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
@ -131,15 +135,15 @@ internal class CustomAttributesRepositoryImpl(
}
}
override suspend fun search(query: String): Flow<List<PinnableSearchable>> {
override fun search(query: String): Flow<ImmutableList<PinnableSearchable>> {
if (query.isBlank()) {
return flow {
emit(emptyList())
emit(persistentListOf())
}
}
val dao = appDatabase.customAttrsDao()
return dao.search("%$query%").map {
favoritesRepository.getFromKeys(it)
favoritesRepository.getFromKeys(it).toImmutableList()
}
}

View File

@ -1,25 +1,24 @@
package de.mm20.launcher2.ui.utils
package de.mm20.launcher2.customattrs.utils
import de.mm20.launcher2.customattrs.CustomAttributesRepository
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 : PinnableSearchable> 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 ->
send(items.map { item ->
val customLabel = labels.find { it.key == item.key }
if (customLabel != null) {
item.overrideLabel(customLabel.label) as T
} else {
item
}
val customLabel = labels.find { it.key == item.key }
if (customLabel != null) {
item.overrideLabel(customLabel.label) as T
} else {
item
}
})
}
}

View File

@ -16,13 +16,13 @@ The source code consists of a number of Gradle submodules which all depend on ea
- `:calculator`: Implements the calculator
- `:calendar`: query calendar events for the calendar widget and calendar search
- `:compat`: Compatibility helpers for old Android versions
- `:contacts`: Contact search
- `:contacts`: Query contacts on the device
- `:crashreporter`: Crash reporter; based on https://github.com/MindorksOpenSource/CrashReporter
- `:currencies`: APIs to fetch currency conversion rates, used by `:unitconverter`
- `:customattrs`: common (low-level) APIs to store per-app customizations (custom labels, custom icons, tags)
- `:database`: the launcher database, uses AndroidX Room
- `:favorites`: Handles pinned, frequently used and hidden items and serialization / deserialization of items. Depends on most of the search modules (`:apps`, `:calendar`, `:contacts`, etc.)
- `:files`: File search (local and cloud)
- `:files`: Manage and find files (local and cloud)
- `:g-services`: Google APIs and Google sign-in; used by `:accounts` and `:files`
- `:i18n`: All resources that require localization. Mainly strings but can also be used for icon resources if they need localization.
- `:icons`: Used to retrieve icons for items. Handles icon packs, themed icons and also custom icons (on a higher level)
@ -35,7 +35,7 @@ The source code consists of a number of Gradle submodules which all depend on ea
- `:owncloud`: Owncloud APIs and Owncloud sign-in; used by `:accounts` and `:files`
- `:permissions`: Request and observe permission status for this app
- `:preferences`: Store user preferences; uses AndroidX Datastore
- `:search`: Base classes for search items and search item serialization. Also websearches. Does not contain the actual search which is split across several modules (`:applications`, `:calendar`, `:contacts`, `:files` and so on)
- `:search`: The search. Also websearches.
- `:ui`: Contains almost the entire user interface (except for account sign-in UIs). Uses Jetpack Compose.
- `:unitconverter`: Unit and currency converter
- `:weather`: APIs to fetch weather data

View File

@ -88,14 +88,12 @@ digraph {
":app" -> ":database" [style=dotted]
":applications" -> ":applications"
":applications" -> ":base" [style=dotted]
":applications" -> ":preferences" [style=dotted]
":applications" -> ":ktx" [style=dotted]
":applications" -> ":compat" [style=dotted]
":appshortcuts" -> ":appshortcuts"
":appshortcuts" -> ":applications" [style=dotted]
":appshortcuts" -> ":permissions" [style=dotted]
":appshortcuts" -> ":base" [style=dotted]
":appshortcuts" -> ":preferences" [style=dotted]
":appshortcuts" -> ":ktx" [style=dotted]
":backup" -> ":backup"
":backup" -> ":favorites" [style=dotted]
@ -116,17 +114,14 @@ digraph {
":base" -> ":ktx" [style=dotted]
":base" -> ":i18n" [style=dotted]
":calculator" -> ":calculator"
":calculator" -> ":preferences" [style=dotted]
":calculator" -> ":base" [style=dotted]
":calendar" -> ":calendar"
":calendar" -> ":preferences" [style=dotted]
":calendar" -> ":ktx" [style=dotted]
":calendar" -> ":base" [style=dotted]
":calendar" -> ":permissions" [style=dotted]
":calendar" -> ":material-color-utilities" [style=dotted]
":compat" -> ":compat"
":contacts" -> ":contacts"
":contacts" -> ":preferences" [style=dotted]
":contacts" -> ":ktx" [style=dotted]
":contacts" -> ":base" [style=dotted]
":contacts" -> ":permissions" [style=dotted]
@ -161,7 +156,6 @@ digraph {
":favorites" -> ":badges" [style=dotted]
":favorites" -> ":crashreporter" [style=dotted]
":files" -> ":files"
":files" -> ":preferences" [style=dotted]
":files" -> ":base" [style=dotted]
":files" -> ":ktx" [style=dotted]
":files" -> ":ms-services" [style=dotted]
@ -216,6 +210,16 @@ digraph {
":preferences" -> ":crashreporter" [style=dotted]
":preferences" -> ":material-color-utilities" [style=dotted]
":search" -> ":search"
":search" -> ":applications" [style=dotted]
":search" -> ":appshortcuts" [style=dotted]
":search" -> ":calculator" [style=dotted]
":search" -> ":calendar" [style=dotted]
":search" -> ":contacts" [style=dotted]
":search" -> ":files" [style=dotted]
":search" -> ":unitconverter" [style=dotted]
":search" -> ":websites" [style=dotted]
":search" -> ":wikipedia" [style=dotted]
":search" -> ":customattrs" [style=dotted]
":search" -> ":base" [style=dotted]
":search" -> ":database" [style=dotted]
":search" -> ":preferences" [style=dotted]
@ -269,7 +273,6 @@ digraph {
":webdav" -> ":crashreporter" [style=dotted]
":webdav" -> ":ktx" [style=dotted]
":websites" -> ":websites"
":websites" -> ":preferences" [style=dotted]
":websites" -> ":base" [style=dotted]
":websites" -> ":ktx" [style=dotted]
":widgets" -> ":widgets"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -44,7 +44,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":preferences"))
implementation(project(":base"))
implementation(project(":ktx"))
implementation(project(":ms-services"))

View File

@ -1,12 +1,15 @@
package de.mm20.launcher2.files
import android.content.Context
import de.mm20.launcher2.files.providers.*
import de.mm20.launcher2.files.providers.FileProvider
import de.mm20.launcher2.files.providers.GDriveFileProvider
import de.mm20.launcher2.files.providers.LocalFileProvider
import de.mm20.launcher2.files.providers.NextcloudFileProvider
import de.mm20.launcher2.files.providers.OneDriveFileProvider
import de.mm20.launcher2.files.providers.OwncloudFileProvider
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
@ -14,24 +17,30 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
interface FileRepository: SearchableRepository<File> {
interface FileRepository {
fun search(
query: String,
local: Boolean = true,
gdrive: Boolean = true,
onedrive: Boolean = true,
nextcloud: Boolean = true,
owncloud: Boolean = true,
): Flow<ImmutableList<File>>
fun deleteFile(file: File)
}
internal class FileRepositoryImpl(
private val context: Context,
private val dataStore: LauncherDataStore,
private val permissionsManager: PermissionsManager,
) : FileRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val providers = MutableStateFlow<List<FileProvider>>(emptyList())
private val nextcloudClient by lazy {
NextcloudApiHelper(context)
}
@ -39,46 +48,35 @@ internal class FileRepositoryImpl(
OwncloudClient(context)
}
init {
scope.launch {
dataStore.data.map { it.fileSearch }.distinctUntilChanged().collectLatest {
val provs = mutableListOf<FileProvider>()
if (it.localFiles) {
provs += LocalFileProvider(context, permissionsManager)
}
if (it.nextcloud) {
provs += NextcloudFileProvider(nextcloudClient)
}
if (it.owncloud) {
provs += OwncloudFileProvider(owncloudClient)
}
if (it.gdrive) {
provs += GDriveFileProvider(context)
}
if (it.onedrive) {
provs += OneDriveFileProvider(context)
}
providers.value = provs
}
}
}
override fun search(query: String): Flow<ImmutableList<File>> = channelFlow {
override fun search(
query: String,
local: Boolean,
gdrive: Boolean,
onedrive: Boolean,
nextcloud: Boolean,
owncloud: Boolean
) = channelFlow {
if (query.isBlank()) {
send(persistentListOf())
return@channelFlow
}
providers.collectLatest { providers ->
if (providers.isEmpty()) {
send(persistentListOf())
return@collectLatest
}
val results = mutableListOf<File>()
for (provider in providers) {
results.addAll(provider.search(query))
send(results.toImmutableList())
}
val providers = mutableListOf<FileProvider>()
if (local) providers.add(LocalFileProvider(context, permissionsManager))
if (gdrive) providers.add(GDriveFileProvider(context))
if (onedrive) providers.add(OneDriveFileProvider(context))
if (nextcloud) providers.add(NextcloudFileProvider(nextcloudClient))
if (owncloud) providers.add(OwncloudFileProvider(owncloudClient))
if (providers.isEmpty()) {
send(persistentListOf())
return@channelFlow
}
val results = mutableListOf<File>()
for (provider in providers) {
results.addAll(provider.search(query))
send(results.toImmutableList())
}
}

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val filesModule = module {
single<FileRepository> { FileRepositoryImpl(androidContext(), get(), get()) }
single<FileRepository> { FileRepositoryImpl(androidContext(), get()) }
}

View File

@ -47,6 +47,17 @@ dependencies {
implementation(libs.okhttp)
implementation(libs.coil.core)
implementation(project(":applications"))
implementation(project(":appshortcuts"))
implementation(project(":calculator"))
implementation(project(":calendar"))
implementation(project(":contacts"))
implementation(project(":files"))
implementation(project(":unitconverter"))
implementation(project(":websites"))
implementation(project(":wikipedia"))
implementation(project(":customattrs"))
implementation(project(":base"))
implementation(project(":database"))
implementation(project(":preferences"))

View File

@ -4,5 +4,19 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val searchModule = module {
single<SearchService> {
SearchServiceImpl(
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
get(),
)
}
single<WebsearchRepository> { WebsearchRepositoryImpl(androidContext(), get()) }
}

View File

@ -0,0 +1,243 @@
package de.mm20.launcher2.search
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.calculator.CalculatorRepository
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.contacts.ContactRepository
import de.mm20.launcher2.customattrs.CustomAttributesRepository
import de.mm20.launcher2.customattrs.utils.withCustomLabels
import de.mm20.launcher2.files.FileRepository
import de.mm20.launcher2.preferences.Settings.AppShortcutSearchSettings
import de.mm20.launcher2.preferences.Settings.CalculatorSearchSettings
import de.mm20.launcher2.preferences.Settings.CalendarSearchSettings
import de.mm20.launcher2.preferences.Settings.ContactsSearchSettings
import de.mm20.launcher2.preferences.Settings.FilesSearchSettings
import de.mm20.launcher2.preferences.Settings.UnitConverterSearchSettings
import de.mm20.launcher2.preferences.Settings.WebsiteSearchSettings
import de.mm20.launcher2.preferences.Settings.WikipediaSearchSettings
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.GDriveFile
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LocalFile
import de.mm20.launcher2.search.data.NextcloudFile
import de.mm20.launcher2.search.data.OneDriveFile
import de.mm20.launcher2.search.data.OwncloudFile
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.unitconverter.UnitConverterRepository
import de.mm20.launcher2.websites.WebsiteRepository
import de.mm20.launcher2.wikipedia.WikipediaRepository
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
interface SearchService {
fun search(
query: String,
shortcuts: AppShortcutSearchSettings,
contacts: ContactsSearchSettings,
calendars: CalendarSearchSettings,
files: FilesSearchSettings,
calculator: CalculatorSearchSettings,
unitConverter: UnitConverterSearchSettings,
websites: WebsiteSearchSettings,
wikipedia: WikipediaSearchSettings,
): Flow<ImmutableList<Searchable>>
}
internal class SearchServiceImpl(
private val appRepository: AppRepository,
private val appShortcutRepository: AppShortcutRepository,
private val calendarRepository: CalendarRepository,
private val contactRepository: ContactRepository,
private val fileRepository: FileRepository,
private val wikipediaRepository: WikipediaRepository,
private val unitConverterRepository: UnitConverterRepository,
private val calculatorRepository: CalculatorRepository,
private val websiteRepository: WebsiteRepository,
private val customAttributesRepository: CustomAttributesRepository,
) : SearchService {
override fun search(
query: String,
shortcuts: AppShortcutSearchSettings,
contacts: ContactsSearchSettings,
calendars: CalendarSearchSettings,
files: FilesSearchSettings,
calculator: CalculatorSearchSettings,
unitConverter: UnitConverterSearchSettings,
websites: WebsiteSearchSettings,
wikipedia: WikipediaSearchSettings,
): Flow<ImmutableList<Searchable>> = channelFlow {
supervisorScope {
val results = MutableStateFlow(SearchResults())
launch {
appRepository.search(query)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(apps = r)
}
}
}
if (shortcuts.enabled) {
launch {
appShortcutRepository.search(query)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(shortcuts = r)
}
}
}
}
if (contacts.enabled) {
launch {
contactRepository.search(query)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(contacts = r)
}
}
}
}
if (calendars.enabled) {
launch {
calendarRepository.search(query)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(calendars = r)
}
}
}
}
if (calculator.enabled) {
launch {
calculatorRepository.search(query).collectLatest { r ->
results.update {
it.copy(calculators = r?.let { persistentListOf(it) }
?: persistentListOf())
}
}
}
}
if (unitConverter.enabled) {
launch {
unitConverterRepository.search(query, unitConverter.currencies).collectLatest { r ->
results.update {
it.copy(unitConverters = r?.let { persistentListOf(it) }
?: persistentListOf())
}
}
}
}
if (websites.enabled) {
launch {
websiteRepository.search(query)
.map { it?.let { listOf(it) } ?: listOf() }
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(websites = r)
}
}
}
}
if (wikipedia.enabled) {
launch {
wikipediaRepository.search(query, loadImages = wikipedia.images)
.map { it?.let { listOf(it) } ?: listOf() }
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(wikipedia = r)
}
}
}
}
if (files.localFiles || files.owncloud || files.onedrive || files.gdrive || files.nextcloud) {
launch {
fileRepository.search(
query,
local = files.localFiles,
nextcloud = files.nextcloud,
owncloud = files.owncloud,
onedrive = files.onedrive,
gdrive = files.gdrive,
)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(files = r)
}
}
}
}
launch {
customAttributesRepository.search(query)
.withCustomLabels(customAttributesRepository)
.collectLatest { r ->
results.update {
it.copy(
other = r
.filter {
it is LauncherApp ||
shortcuts.enabled && it is AppShortcut ||
files.localFiles && it is LocalFile ||
files.nextcloud && it is NextcloudFile ||
files.owncloud && it is OwncloudFile ||
files.onedrive && it is OneDriveFile ||
files.gdrive && it is GDriveFile ||
wikipedia.enabled && it is Wikipedia ||
websites.enabled && it is Website ||
calendars.enabled && it is CalendarEvent ||
contacts.enabled && it is Contact
}.toImmutableList()
)
}
}
}
launch {
results
.map { it.toList().sortedBy { it as? PinnableSearchable }.toImmutableList() }
.collectLatest {
send(it)
}
}
}
}
}
internal data class SearchResults(
val apps: List<LauncherApp> = emptyList(),
val shortcuts: List<AppShortcut> = emptyList(),
val contacts: List<Contact> = emptyList(),
val calendars: List<CalendarEvent> = emptyList(),
val files: List<File> = emptyList(),
val calculators: List<Calculator> = emptyList(),
val unitConverters: List<UnitConverter> = emptyList(),
val websites: List<Website> = emptyList(),
val wikipedia: List<Wikipedia> = emptyList(),
val other: List<PinnableSearchable> = emptyList(),
) {
fun toList(): List<Searchable> {
return (apps + shortcuts + contacts + calendars + websites + wikipedia + other).distinctBy { it.key } + calculators+ unitConverters
}
}

View File

@ -3,12 +3,11 @@ package de.mm20.launcher2.ui.common
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.customattrs.CustomAttributesRepository
import de.mm20.launcher2.customattrs.utils.withCustomLabels
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore
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
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
@ -81,7 +80,7 @@ open class FavoritesVM : ViewModel(), KoinComponent {
customAttributesRepository
.getItemsForTag(tag)
.withCustomLabels(customAttributesRepository)
.map { it.sorted() }
.map { it.sortedBy { it } }
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)

View File

@ -91,10 +91,10 @@ fun SearchColumn(
val contacts by viewModel.contactResults.observeAsState(emptyList())
val files by viewModel.fileResults.observeAsState(emptyList())
val events by viewModel.calendarResults.observeAsState(emptyList())
val unitConverter by viewModel.unitConverterResult.observeAsState(null)
val calculator by viewModel.calculatorResult.observeAsState(null)
val wikipedia by viewModel.wikipediaResult.observeAsState(null)
val website by viewModel.websiteResult.observeAsState(null)
val unitConverter by viewModel.unitConverterResults.observeAsState(emptyList())
val calculator by viewModel.calculatorResults.observeAsState(emptyList())
val wikipedia by viewModel.wikipediaResults.observeAsState(emptyList())
val website by viewModel.websiteResults.observeAsState(emptyList())
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
@ -276,14 +276,12 @@ fun SearchColumn(
reverse = reverse,
key = "shortcuts"
)
val uc = unitConverter
if (uc != null) {
for (conv in unitConverter) {
SingleResult {
UnitConverterItem(unitConverter = uc)
UnitConverterItem(unitConverter = conv)
}
}
val calc = calculator
if (calc != null) {
for (calc in calculator) {
SingleResult {
CalculatorItem(calculator = calc)
}
@ -334,14 +332,12 @@ fun SearchColumn(
reverse = reverse,
key = "contacts"
)
val wiki = wikipedia
if (wiki != null) {
for (wiki in wikipedia) {
SingleResult {
WikipediaItem(wikipedia = wiki)
}
}
val ws = website
if (ws != null) {
for (ws in website) {
SingleResult {
WebsiteItem(website = ws)
}

View File

@ -1,29 +1,19 @@
package de.mm20.launcher2.ui.launcher.search
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.calculator.CalculatorRepository
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.contacts.ContactRepository
import de.mm20.launcher2.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository
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.SearchService
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.wikipedia.WikipediaRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
@ -33,19 +23,9 @@ class SearchVM : ViewModel(), KoinComponent {
private val favoritesRepository: FavoritesRepository by inject()
private val permissionsManager: PermissionsManager by inject()
private val customAttributesRepository: CustomAttributesRepository by inject()
private val dataStore: LauncherDataStore by inject()
private val calendarRepository: CalendarRepository by inject()
private val contactRepository: ContactRepository by inject()
private val appRepository: AppRepository by inject()
private val appShortcutRepository: AppShortcutRepository by inject()
private val wikipediaRepository: WikipediaRepository by inject()
private val unitConverterRepository: UnitConverterRepository by inject()
private val calculatorRepository: CalculatorRepository by inject()
private val websiteRepository: WebsiteRepository by inject()
private val fileRepository: FileRepository by inject()
private val websearchRepository: WebsearchRepository by inject()
private val searchService: SearchService by inject()
val isSearching = MutableLiveData(false)
val searchQuery = MutableLiveData("")
@ -59,10 +39,10 @@ class SearchVM : ViewModel(), KoinComponent {
val fileResults = MutableLiveData<List<File>>(emptyList())
val contactResults = MutableLiveData<List<Contact>>(emptyList())
val calendarResults = MutableLiveData<List<CalendarEvent>>(emptyList())
val wikipediaResult = MutableLiveData<Wikipedia?>(null)
val websiteResult = MutableLiveData<Website?>(null)
val calculatorResult = MutableLiveData<Calculator?>(null)
val unitConverterResult = MutableLiveData<UnitConverter?>(null)
val wikipediaResults = MutableLiveData<List<Wikipedia>>(emptyList())
val websiteResults = MutableLiveData<List<Website>>(emptyList())
val calculatorResults = MutableLiveData<List<Calculator>>(emptyList())
val unitConverterResults = MutableLiveData<List<UnitConverter>>(emptyList())
val websearchResults = MutableLiveData<List<Websearch>>(emptyList())
val hiddenResults = MutableLiveData<List<PinnableSearchable>>(emptyList())
@ -84,8 +64,6 @@ class SearchVM : ViewModel(), KoinComponent {
isSearchEmpty.value = query.isEmpty()
hiddenResults.value = emptyList()
val hiddenItems = MutableStateFlow(HiddenItemResults())
try {
searchJob?.cancel()
} catch (_: CancellationException) {
@ -93,117 +71,64 @@ class SearchVM : ViewModel(), KoinComponent {
hideFavorites.postValue(query.isNotEmpty())
searchJob = viewModelScope.launch {
isSearching.postValue(true)
val customAttrResults = customAttributesRepository.search(query)
.combine(dataStore.data) { items, settings ->
items.filter {
it is LauncherApp
|| it is Contact && settings.contactsSearch.enabled
|| it is CalendarEvent && settings.calendarSearch.enabled
|| it is AppShortcut && settings.appShortcutSearch.enabled
|| it is LocalFile && settings.fileSearch.localFiles
|| it is GDriveFile && settings.fileSearch.gdrive
|| it is OneDriveFile && settings.fileSearch.onedrive
}
}
val jobs = mutableListOf<Deferred<Any>>()
jobs += async(Dispatchers.Default) {
appRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels(customAttributesRepository)
.sorted()
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
val (work, personal) = results.partition { it is LauncherApp && !it.isMainProfile }
appResults.postValue(personal)
workAppResults.postValue(work)
hiddenItems.update {
it.copy(apps = hidden)
dataStore.data.collectLatest {
searchService.search(
query,
calculator = it.calculatorSearch,
unitConverter = it.unitConverterSearch,
calendars = it.calendarSearch,
contacts = it.contactsSearch,
files = it.fileSearch,
shortcuts = it.appShortcutSearch,
websites = it.websiteSearch,
wikipedia = it.wikipediaSearch,
).collectLatest { results ->
hiddenItemKeys.collectLatest { hiddenKeys ->
val hidden = mutableListOf<PinnableSearchable>()
val apps = mutableListOf<LauncherApp>()
val workApps = mutableListOf<LauncherApp>()
val shortcuts = mutableListOf<AppShortcut>()
val files = mutableListOf<File>()
val contacts = mutableListOf<Contact>()
val events = mutableListOf<CalendarEvent>()
val unitConv = mutableListOf<UnitConverter>()
val calc = mutableListOf<Calculator>()
val wikipedia = mutableListOf<Wikipedia>()
val website = mutableListOf<Website>()
for (r in results) {
when {
r is PinnableSearchable && hiddenKeys.contains(r.key) -> {
hidden.add(r)
}
r is LauncherApp && !r.isMainProfile -> workApps.add(r)
r is LauncherApp -> apps.add(r)
r is AppShortcut -> shortcuts.add(r)
r is File -> files.add(r)
r is Contact -> contacts.add(r)
r is CalendarEvent -> events.add(r)
r is UnitConverter -> unitConv.add(r)
r is Calculator -> calc.add(r)
r is Website -> website.add(r)
r is Wikipedia -> wikipedia.add(r)
}
}
appResults.value = apps
workAppResults.value = workApps
appShortcutResults.value = shortcuts
fileResults.value = files
contactResults.value = contacts
calendarResults.value = events
wikipediaResults.value = wikipedia
websiteResults.value = website
calculatorResults.value = calc
unitConverterResults.value = unitConv
hiddenResults.value = hidden
}
}
jobs += async(Dispatchers.Default) {
contactRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels(customAttributesRepository)
.sorted()
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
contactResults.postValue(results)
hiddenItems.update {
it.copy(contacts = hidden)
}
}
}
jobs += async(Dispatchers.Default) {
calendarRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels(customAttributesRepository)
.sorted()
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
calendarResults.postValue(results)
hiddenItems.update {
it.copy(calendarEvents = hidden)
}
}
}
jobs += async(Dispatchers.Default) {
wikipediaRepository.search(query).collectLatest {
wikipediaResult.postValue(it)
}
}
jobs += async(Dispatchers.Default) {
unitConverterRepository.search(query).collectLatest {
unitConverterResult.postValue(it)
}
}
jobs += async(Dispatchers.Default) {
calculatorRepository.search(query).collectLatest {
calculatorResult.postValue(it)
}
}
jobs += async(Dispatchers.Default) {
websiteRepository.search(query).collectLatest {
websiteResult.postValue(it)
}
}
jobs += async(Dispatchers.Default) {
fileRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels(customAttributesRepository)
.sorted()
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
fileResults.postValue(results)
hiddenItems.update {
it.copy(files = hidden)
}
}
}
jobs += async(Dispatchers.Default) {
websearchRepository.search(query).collectLatest {
websearchResults.postValue(it)
}
}
jobs += async(Dispatchers.Default) {
appShortcutRepository
.search(query)
.withCustomAttributeResults(customAttrResults)
.withCustomLabels(customAttributesRepository)
.sorted()
.collectWithHiddenItems(hiddenItemKeys) { results, hidden ->
appShortcutResults.postValue(results)
hiddenItems.update {
it.copy(appShortcuts = hidden)
}
}
}
launch(Dispatchers.Default) {
hiddenItems.collectLatest {
hiddenResults.postValue(it.joinToList())
}
}
jobs.map { it.await() }
isSearching.postValue(false)
}
}

View File

@ -1,29 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.calculator
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.launcher.search.SearchVM
@Composable
fun ColumnScope.CalculatorResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val calculator by viewModel.calculatorResult.observeAsState(null)
AnimatedVisibility(calculator != null) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
) {
calculator?.let { CalculatorItem(calculator = it) }
}
}
}

View File

@ -1,29 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.unitconverter
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.launcher.search.SearchVM
@Composable
fun ColumnScope.UnitConverterResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val unitConverter by viewModel.unitConverterResult.observeAsState(null)
AnimatedVisibility(unitConverter != null) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
) {
unitConverter?.let { UnitConverterItem(unitConverter = it) }
}
}
}

View File

@ -1,29 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.website
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.launcher.search.SearchVM
@Composable
fun ColumnScope.WebsiteResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val website by viewModel.websiteResult.observeAsState(null)
AnimatedVisibility(website != null) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
) {
website?.let { WebsiteItem(website = it) }
}
}
}

View File

@ -1,29 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.wikipedia
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.launcher.search.SearchVM
@Composable
fun ColumnScope.WikipediaResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val wikipedia by viewModel.wikipediaResult.observeAsState(null)
AnimatedVisibility(wikipedia != null) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
) {
wikipedia?.let { WikipediaItem(wikipedia = it) }
}
}
}

View File

@ -14,17 +14,22 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.CalendarEvent
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.lang.Integer.min
import java.time.*
import java.util.*
import java.time.Instant
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneId
import kotlin.math.max
class CalendarWidgetVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val calendarRepository: CalendarRepository by inject()
private val favoritesRepository: FavoritesRepository by inject()
@ -144,10 +149,16 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
suspend fun onActive() {
selectDate(LocalDate.now())
calendarRepository.getUpcomingEvents().collectLatest { events ->
favoritesRepository.getHiddenCalendarEventKeys().collectLatest { hidden ->
upcomingEvents = events.filter { !hidden.contains(it.key) }
dataStore.data.map { it.calendarWidget }.collectLatest { settings ->
calendarRepository.getUpcomingEvents(
excludeAllDayEvents = settings.hideAlldayEvents,
excludeCalendars = settings.excludeCalendarsList
).collectLatest { events ->
favoritesRepository.getHiddenCalendarEventKeys().collectLatest { hidden ->
upcomingEvents = events.filter { !hidden.contains(it.key) }
}
}
}
}

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.search.data
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.UnitValue
@ -7,5 +8,4 @@ open class UnitConverter(
val dimension: Dimension,
val inputValue: UnitValue,
val values: List<UnitValue>
)
): Searchable

View File

@ -1,7 +1,6 @@
package de.mm20.launcher2.unitconverter
import android.content.Context
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.currencies.CurrencyRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.UnitConverter
@ -15,7 +14,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface UnitConverterRepository {
fun search(query: String): Flow<UnitConverter?>
fun search(query: String, includeCurrencies: Boolean): Flow<UnitConverter?>
}
internal class UnitConverterRepositoryImpl(
@ -26,8 +25,6 @@ internal class UnitConverterRepositoryImpl(
private val scope = CoroutineScope(Job() + Dispatchers.Default)
val unitConverter = MutableLiveData<UnitConverter?>()
init {
scope.launch {
dataStore.data.map { it.unitConverterSearch }.distinctUntilChanged().collectLatest {
@ -37,18 +34,12 @@ internal class UnitConverterRepositoryImpl(
}
}
override fun search(query: String): Flow<UnitConverter?> = channelFlow {
override fun search(query: String, includeCurrencies: Boolean): Flow<UnitConverter?> = channelFlow {
if (query.isBlank()) {
send(null)
return@channelFlow
}
dataStore.data.map { it.unitConverterSearch }.collectLatest {
if (it.enabled) {
send(queryUnitConverter(query, it.currencies))
} else {
send(null)
}
}
send(queryUnitConverter(query, includeCurrencies))
}
private suspend fun queryUnitConverter(

View File

@ -50,7 +50,6 @@ dependencies {
implementation(libs.coil.core)
implementation(project(":preferences"))
implementation(project(":base"))
implementation(project(":ktx"))

View File

@ -3,13 +3,10 @@ package de.mm20.launcher2.websites
import android.content.Context
import android.webkit.URLUtil
import androidx.core.graphics.toColorInt
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Website
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
@ -17,7 +14,6 @@ import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.UncheckedIOException
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.IOException
import java.net.MalformedURLException
import java.net.URISyntaxException
@ -30,8 +26,6 @@ interface WebsiteRepository {
internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val httpClient = OkHttpClient
.Builder()
.connectTimeout(200, TimeUnit.MILLISECONDS)
@ -46,14 +40,8 @@ internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository,
}
if (query.isBlank()) return@channelFlow
dataStore.data.map { it.websiteSearch.enabled }.collectLatest {
if(it) {
val website = queryWebsite(query)
send(website)
} else {
send(null)
}
}
val website = queryWebsite(query)
send(website)
}

View File

@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val wikipediaModule = module {
single<WikipediaRepository> { WikipediaRepositoryImpl(androidContext()) }
single<WikipediaRepository> { WikipediaRepositoryImpl(androidContext(), get()) }
}

View File

@ -15,17 +15,16 @@ import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
interface WikipediaRepository {
fun search(query: String): Flow<Wikipedia?>
fun search(query: String, loadImages: Boolean = false): Flow<Wikipedia?>
}
internal class WikipediaRepositoryImpl(
private val context: Context
private val context: Context,
private val dataStore: LauncherDataStore
) : WikipediaRepository, KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val dataStore: LauncherDataStore by inject()
private val httpClient = OkHttpClient
.Builder()
.connectTimeout(200, TimeUnit.MILLISECONDS)
@ -58,7 +57,7 @@ internal class WikipediaRepositoryImpl(
private lateinit var wikipediaService: WikipediaApi
override fun search(query: String): Flow<Wikipedia?> = channelFlow {
override fun search(query: String, loadImages: Boolean): Flow<Wikipedia?> = channelFlow {
send(null)
withContext(Dispatchers.IO) {
httpClient.dispatcher.cancelAll()
@ -67,13 +66,7 @@ internal class WikipediaRepositoryImpl(
if (!::wikipediaService.isInitialized) return@channelFlow
if (query.isBlank()) return@channelFlow
dataStore.data.map { it.wikipediaSearch }.collectLatest {
if (it.enabled) {
send(queryWikipedia(query, it.images))
} else {
send(null)
}
}
send(queryWikipedia(query, loadImages))
}
private suspend fun queryWikipedia(query: String, loadImages: Boolean): Wikipedia? {