diff --git a/applications/build.gradle.kts b/applications/build.gradle.kts index 6808c20c..9de0c0df 100644 --- a/applications/build.gradle.kts +++ b/applications/build.gradle.kts @@ -46,7 +46,6 @@ dependencies { implementation(libs.commons.text) implementation(project(":base")) - implementation(project(":preferences")) implementation(project(":ktx")) implementation(project(":compat")) diff --git a/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt b/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt index b7feb328..0ad40e59 100644 --- a/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt +++ b/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt @@ -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 { +interface AppRepository { fun getAllInstalledApps(): Flow> fun getSuspendedPackages(): Flow> + fun search(query: String): Flow> } internal class AppRepositoryImpl( diff --git a/appshortcuts/build.gradle.kts b/appshortcuts/build.gradle.kts index 3c208a99..b19fa790 100644 --- a/appshortcuts/build.gradle.kts +++ b/appshortcuts/build.gradle.kts @@ -46,7 +46,6 @@ dependencies { implementation(project(":applications")) implementation(project(":permissions")) implementation(project(":base")) - implementation(project(":preferences")) implementation(project(":ktx")) } \ No newline at end of file diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt index cee3bb87..6be648f9 100644 --- a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -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 { +interface AppShortcutRepository { + + fun search(query: String): Flow> suspend fun getShortcutsForActivity( launcherActivityInfo: LauncherActivityInfo, count: Int = 5 @@ -45,7 +49,6 @@ interface AppShortcutRepository: SearchableRepository { 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() ?: return@collectLatest send( - persistentListOf() + + shortcutChangeEmitter.collectLatest { + val launcherApps = + context.getSystemService() ?: 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() + ) } } } diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt index e3b727dd..3524c53a 100644 --- a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt @@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val appShortcutsModule = module { - single { AppShortcutRepositoryImpl(androidContext(), get(), get()) } + single { AppShortcutRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/base/src/main/java/de/mm20/launcher2/search/PinnableSearchable.kt b/base/src/main/java/de/mm20/launcher2/search/PinnableSearchable.kt index 94a704be..9faba0f4 100644 --- a/base/src/main/java/de/mm20/launcher2/search/PinnableSearchable.kt +++ b/base/src/main/java/de/mm20/launcher2/search/PinnableSearchable.kt @@ -9,6 +9,9 @@ import java.text.Collator interface PinnableSearchable : Searchable, Comparable { + val domain: String + val key: String + val label: String val labelOverride: String? get() = null diff --git a/base/src/main/java/de/mm20/launcher2/search/Searchable.kt b/base/src/main/java/de/mm20/launcher2/search/Searchable.kt index 4945955b..8f07941a 100644 --- a/base/src/main/java/de/mm20/launcher2/search/Searchable.kt +++ b/base/src/main/java/de/mm20/launcher2/search/Searchable.kt @@ -1,6 +1,3 @@ package de.mm20.launcher2.search -interface Searchable { - val domain: String - val key: String -} \ No newline at end of file +interface Searchable \ No newline at end of file diff --git a/base/src/main/java/de/mm20/launcher2/search/SearchableRepository.kt b/base/src/main/java/de/mm20/launcher2/search/SearchableRepository.kt deleted file mode 100644 index 38b075c4..00000000 --- a/base/src/main/java/de/mm20/launcher2/search/SearchableRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.mm20.launcher2.search - -import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.flow.Flow - -interface SearchableRepository { - fun search(query: String): Flow> -} \ No newline at end of file diff --git a/calculator/build.gradle.kts b/calculator/build.gradle.kts index d71940d9..dd521dfe 100644 --- a/calculator/build.gradle.kts +++ b/calculator/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(libs.koin.android) - implementation(project(":preferences")) implementation(project(":base")) } \ No newline at end of file diff --git a/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt b/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt index c50663dd..3f8a775a 100644 --- a/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt +++ b/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt @@ -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 = 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) diff --git a/calculator/src/main/java/de/mm20/launcher2/search/data/Calculator.kt b/calculator/src/main/java/de/mm20/launcher2/search/data/Calculator.kt index d8daf2ab..8f51ec29 100644 --- a/calculator/src/main/java/de/mm20/launcher2/search/data/Calculator.kt +++ b/calculator/src/main/java/de/mm20/launcher2/search/data/Calculator.kt @@ -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 diff --git a/calendar/build.gradle.kts b/calendar/build.gradle.kts index 3ff6dbe4..7a8e2184 100644 --- a/calendar/build.gradle.kts +++ b/calendar/build.gradle.kts @@ -42,7 +42,6 @@ dependencies { implementation(libs.koin.android) - implementation(project(":preferences")) implementation(project(":ktx")) implementation(project(":base")) implementation(project(":permissions")) diff --git a/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt b/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt index 8c8cd042..f4f165b8 100644 --- a/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt +++ b/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt @@ -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 { - fun getUpcomingEvents(): Flow> +interface CalendarRepository { + + fun search(query: String): Flow> + fun getUpcomingEvents( + excludeCalendars: List, + excludeAllDayEvents: Boolean + ): Flow> suspend fun getCalendars(): List } 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> { 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> = channelFlow { + override fun getUpcomingEvents( + excludeCalendars: List, + excludeAllDayEvents: Boolean, + ): Flow> = 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()) } diff --git a/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt b/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt index 2944249c..5b5e42e7 100644 --- a/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt +++ b/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt @@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val calendarModule = module { - single { CalendarRepositoryImpl(androidContext()) } + single { CalendarRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/contacts/build.gradle.kts b/contacts/build.gradle.kts index 66db85ae..38e7bbbe 100644 --- a/contacts/build.gradle.kts +++ b/contacts/build.gradle.kts @@ -42,7 +42,6 @@ dependencies { implementation(libs.koin.android) - implementation(project(":preferences")) implementation(project(":ktx")) implementation(project(":base")) implementation(project(":permissions")) diff --git a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt index 75747382..c7166c77 100644 --- a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt +++ b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt @@ -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 +interface ContactRepository { + fun search(query: String): Flow> +} 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> { - 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 { diff --git a/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt b/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt index 640a2d1f..b19013e6 100644 --- a/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt +++ b/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt @@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val contactsModule = module { - single { ContactRepositoryImpl(androidContext()) } + single { ContactRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt index b8f9ae4f..e14d692b 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt @@ -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> + fun getCustomIcon(searchable: PinnableSearchable): Flow fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?) @@ -25,8 +31,6 @@ interface CustomAttributesRepository { fun setTags(searchable: PinnableSearchable, tags: List) fun getTags(searchable: PinnableSearchable): Flow> - suspend fun search(query: String): Flow> - suspend fun export(toDir: File) suspend fun import(fromDir: File) @@ -131,15 +135,15 @@ internal class CustomAttributesRepositoryImpl( } } - override suspend fun search(query: String): Flow> { + override fun search(query: String): Flow> { 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() } } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/utils/CustomLabels.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/utils/Utils.kt similarity index 60% rename from ui/src/main/java/de/mm20/launcher2/ui/utils/CustomLabels.kt rename to customattrs/src/main/java/de/mm20/launcher2/customattrs/utils/Utils.kt index 89585f60..14ef4cdb 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/utils/CustomLabels.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/utils/Utils.kt @@ -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 Flow>.withCustomLabels( +fun Flow>.withCustomLabels( customAttributesRepository: CustomAttributesRepository, ): Flow> = 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 + } }) } } diff --git a/docs/docs/developer-guide/modules.md b/docs/docs/developer-guide/modules.md index 9e1aa875..85520f80 100644 --- a/docs/docs/developer-guide/modules.md +++ b/docs/docs/developer-guide/modules.md @@ -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 diff --git a/docs/static/img/dependency-graph.dot b/docs/static/img/dependency-graph.dot index 0c75023f..03c15503 100644 --- a/docs/static/img/dependency-graph.dot +++ b/docs/static/img/dependency-graph.dot @@ -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" diff --git a/docs/static/img/dependency-graph.dot.png b/docs/static/img/dependency-graph.dot.png index 612ea8a3..1e99a9e3 100644 Binary files a/docs/static/img/dependency-graph.dot.png and b/docs/static/img/dependency-graph.dot.png differ diff --git a/files/build.gradle.kts b/files/build.gradle.kts index 8dc0323d..4410f73c 100644 --- a/files/build.gradle.kts +++ b/files/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { implementation(libs.koin.android) - implementation(project(":preferences")) implementation(project(":base")) implementation(project(":ktx")) implementation(project(":ms-services")) diff --git a/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt index 2db7a14d..a1ba695f 100644 --- a/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt +++ b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt @@ -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 { +interface FileRepository { + fun search( + query: String, + local: Boolean = true, + gdrive: Boolean = true, + onedrive: Boolean = true, + nextcloud: Boolean = true, + owncloud: Boolean = true, + ): Flow> + 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>(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() - 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> = 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() - for (provider in providers) { - results.addAll(provider.search(query)) - send(results.toImmutableList()) - } + val providers = mutableListOf() + + 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() + for (provider in providers) { + results.addAll(provider.search(query)) + send(results.toImmutableList()) } } diff --git a/files/src/main/java/de/mm20/launcher2/files/Module.kt b/files/src/main/java/de/mm20/launcher2/files/Module.kt index f2f17e85..cb93ecde 100644 --- a/files/src/main/java/de/mm20/launcher2/files/Module.kt +++ b/files/src/main/java/de/mm20/launcher2/files/Module.kt @@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val filesModule = module { - single { FileRepositoryImpl(androidContext(), get(), get()) } + single { FileRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/search/build.gradle.kts b/search/build.gradle.kts index 410fe7b8..03b49b23 100644 --- a/search/build.gradle.kts +++ b/search/build.gradle.kts @@ -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")) diff --git a/search/src/main/java/de/mm20/launcher2/search/Module.kt b/search/src/main/java/de/mm20/launcher2/search/Module.kt index d90d769c..35cf5334 100644 --- a/search/src/main/java/de/mm20/launcher2/search/Module.kt +++ b/search/src/main/java/de/mm20/launcher2/search/Module.kt @@ -4,5 +4,19 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val searchModule = module { + single { + SearchServiceImpl( + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + ) + } single { WebsearchRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/search/src/main/java/de/mm20/launcher2/search/SearchService.kt b/search/src/main/java/de/mm20/launcher2/search/SearchService.kt new file mode 100644 index 00000000..5fe001b5 --- /dev/null +++ b/search/src/main/java/de/mm20/launcher2/search/SearchService.kt @@ -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> +} + +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> = 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 = emptyList(), + val shortcuts: List = emptyList(), + val contacts: List = emptyList(), + val calendars: List = emptyList(), + val files: List = emptyList(), + val calculators: List = emptyList(), + val unitConverters: List = emptyList(), + val websites: List = emptyList(), + val wikipedia: List = emptyList(), + val other: List = emptyList(), +) { + fun toList(): List { + return (apps + shortcuts + contacts + calendars + websites + wikipedia + other).distinctBy { it.key } + calculators+ unitConverters + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt index 348ee6ae..5312f3b8 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt @@ -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) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt index f9f95089..b4b40f19 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt @@ -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) } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index 7f767130..06c673a3 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -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>(emptyList()) val contactResults = MutableLiveData>(emptyList()) val calendarResults = MutableLiveData>(emptyList()) - val wikipediaResult = MutableLiveData(null) - val websiteResult = MutableLiveData(null) - val calculatorResult = MutableLiveData(null) - val unitConverterResult = MutableLiveData(null) + val wikipediaResults = MutableLiveData>(emptyList()) + val websiteResults = MutableLiveData>(emptyList()) + val calculatorResults = MutableLiveData>(emptyList()) + val unitConverterResults = MutableLiveData>(emptyList()) val websearchResults = MutableLiveData>(emptyList()) val hiddenResults = MutableLiveData>(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>() - 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() + val apps = mutableListOf() + val workApps = mutableListOf() + val shortcuts = mutableListOf() + val files = mutableListOf() + val contacts = mutableListOf() + val events = mutableListOf() + val unitConv = mutableListOf() + val calc = mutableListOf() + val wikipedia = mutableListOf() + val website = mutableListOf() + 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) } } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calculator/CalculatorResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calculator/CalculatorResults.kt deleted file mode 100644 index 310221db..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calculator/CalculatorResults.kt +++ /dev/null @@ -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) } - } - } - -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterResults.kt deleted file mode 100644 index 0ca5bd15..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterResults.kt +++ /dev/null @@ -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) } - } - } - -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteResults.kt deleted file mode 100644 index d5099b12..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteResults.kt +++ /dev/null @@ -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) } - } - } - -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaResults.kt deleted file mode 100644 index 3bf18d05..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaResults.kt +++ /dev/null @@ -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) } - } - } - -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt index 1c8d6b6f..f89075a4 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt @@ -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) } + } } + } } diff --git a/unitconverter/src/main/java/de/mm20/launcher2/search/data/UnitConverter.kt b/unitconverter/src/main/java/de/mm20/launcher2/search/data/UnitConverter.kt index a03fc36c..851f3cb5 100644 --- a/unitconverter/src/main/java/de/mm20/launcher2/search/data/UnitConverter.kt +++ b/unitconverter/src/main/java/de/mm20/launcher2/search/data/UnitConverter.kt @@ -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 -) - +): Searchable diff --git a/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterRepository.kt b/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterRepository.kt index 185899a8..1d041dc3 100644 --- a/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterRepository.kt +++ b/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterRepository.kt @@ -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 + fun search(query: String, includeCurrencies: Boolean): Flow } internal class UnitConverterRepositoryImpl( @@ -26,8 +25,6 @@ internal class UnitConverterRepositoryImpl( private val scope = CoroutineScope(Job() + Dispatchers.Default) - val unitConverter = MutableLiveData() - init { scope.launch { dataStore.data.map { it.unitConverterSearch }.distinctUntilChanged().collectLatest { @@ -37,18 +34,12 @@ internal class UnitConverterRepositoryImpl( } } - override fun search(query: String): Flow = channelFlow { + override fun search(query: String, includeCurrencies: Boolean): Flow = 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( diff --git a/websites/build.gradle.kts b/websites/build.gradle.kts index e940b2c6..c7e2f17b 100644 --- a/websites/build.gradle.kts +++ b/websites/build.gradle.kts @@ -50,7 +50,6 @@ dependencies { implementation(libs.coil.core) - implementation(project(":preferences")) implementation(project(":base")) implementation(project(":ktx")) diff --git a/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt b/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt index 68ce94d5..e2fab7e2 100644 --- a/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt +++ b/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt @@ -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) } diff --git a/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt b/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt index dff6cb1d..fa8e2414 100644 --- a/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt +++ b/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt @@ -4,5 +4,5 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val wikipediaModule = module { - single { WikipediaRepositoryImpl(androidContext()) } + single { WikipediaRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt b/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt index 9cdd1a19..5769f17f 100644 --- a/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt +++ b/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt @@ -15,17 +15,16 @@ import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit interface WikipediaRepository { - fun search(query: String): Flow + fun search(query: String, loadImages: Boolean = false): Flow } 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 = channelFlow { + override fun search(query: String, loadImages: Boolean): Flow = 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? {