From a2ccef64ce915b4166c35581bd61907a41c38a25 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Fri, 31 Dec 2021 20:14:17 +0100 Subject: [PATCH] Refactor search, favorites and calendar widget --- .../launcher2/activity/AddItemActivity.kt | 8 +- .../launcher2/applications/AppRepository.kt | 188 ++++++---- .../launcher2/applications/AppViewModel.kt | 12 - .../de/mm20/launcher2/applications/Module.kt | 3 +- .../calculator/CalculatorRepository.kt | 32 +- .../calculator/CalculatorViewModel.kt | 9 - .../de/mm20/launcher2/calculator/Module.kt | 4 +- .../launcher2/calendar/CalendarRepository.kt | 110 ++++-- .../launcher2/calendar/CalendarViewModel.kt | 12 - .../java/de/mm20/launcher2/calendar/Module.kt | 3 +- .../launcher2/contacts/ContactRepository.kt | 46 +-- .../launcher2/contacts/ContactViewModel.kt | 11 - .../java/de/mm20/launcher2/contacts/Module.kt | 3 +- .../currencies/CurrencyRepository.kt | 2 +- .../de/mm20/launcher2/database/SearchDao.kt | 21 +- favorites/build.gradle.kts | 1 + .../favorites/FavoritesRepository.kt | 354 ++++++++---------- .../launcher2/favorites/FavoritesViewModel.kt | 54 --- .../de/mm20/launcher2/favorites/Module.kt | 4 +- .../mm20/launcher2/files/FilesRepository.kt | 81 ++-- .../de/mm20/launcher2/files/FilesViewModel.kt | 12 +- .../java/de/mm20/launcher2/files/Module.kt | 2 +- hiddenitems/build.gradle.kts | 2 +- .../hiddenitems/HiddenItemsRepository.kt | 24 +- .../de/mm20/launcher2/hiddenitems/Module.kt | 2 +- .../main/java/de/mm20/launcher2/ktx/View.kt | 3 + .../preferences/LauncherPreferences.kt | 9 +- .../search/BaseSearchableRepository.kt | 53 --- .../java/de/mm20/launcher2/search/Module.kt | 5 +- .../mm20/launcher2/search/SearchRepository.kt | 5 - .../mm20/launcher2/search/SearchViewModel.kt | 16 - .../launcher2/search/WebsearchRepository.kt | 62 +-- .../launcher2/search/WebsearchViewModel.kt | 4 +- settings.gradle.kts | 4 + ui/build.gradle.kts | 4 + .../ui/component/DefaultSwipeActions.kt | 16 +- .../mm20/launcher2/ui/component/SearchBar.kt | 5 +- .../de/mm20/launcher2/ui/component/Toolbar.kt | 12 +- .../ui/launcher/modals/EditFavoritesVM.kt | 23 ++ .../modals}/EditFavoritesView.kt | 21 +- .../ui/launcher/modals/HiddenItemsVM.kt | 25 ++ .../ui/launcher/modals/HiddenItemsView.kt | 57 +++ .../ui/launcher/search/SearchViewModel.kt | 111 +++++- .../widgets/calendar/CalendarWidgetVM.kt | 138 +++++++ .../ui/legacy/activity/LauncherActivity.kt | 75 ++-- .../ui/legacy/component/ApplicationView.kt | 8 +- .../ui/legacy/component/CalculatorView.kt | 17 +- .../ui/legacy/component/CalendarView.kt | 33 +- .../ui/legacy/component/ContactView.kt | 32 +- .../ui/legacy/component/FavoritesView.kt | 25 +- .../launcher2/ui/legacy/component/FileView.kt | 30 +- .../ui/legacy/component/SearchBar.kt | 10 - .../ui/legacy/component/UnitConverterView.kt | 8 +- .../ui/legacy/component/WebSearchView.kt | 32 +- .../ui/legacy/component/WebsiteView.kt | 18 +- .../ui/legacy/component/WikipediaView.kt | 21 +- .../ui/legacy/helper/ActivityStarter.kt | 104 +++-- .../search/ApplicationDetailRepresentation.kt | 24 +- .../launcher2/ui/legacy/view/SwipeCardView.kt | 33 +- .../launcher2/ui/legacy/view/ToolbarView.kt | 79 ++-- .../ui/legacy/widget/CalendarWidget.kt | 177 ++++----- .../launcher2/ui/search/ApplicationResults.kt | 7 +- .../launcher2/ui/search/CalculatorResults.kt | 19 +- .../launcher2/ui/search/FavoriteResults.kt | 7 +- .../mm20/launcher2/ui/search/FileResults.kt | 7 +- .../launcher2/ui/search/WikipediaResult.kt | 8 +- .../ui/searchable/CalendarEventItem.kt | 241 ------------ .../ui/searchable/DeprecatedSearchableList.kt | 41 -- .../launcher2/ui/widget/CalendarWidget.kt | 253 +------------ .../launcher2/ui/widget/parts/DatePart.kt | 2 - .../de/mm20/launcher2/unitconverter/Module.kt | 3 +- .../unitconverter/UnitConverterRepository.kt | 14 +- .../unitconverter/UnitConverterViewModel.kt | 9 - .../java/de/mm20/launcher2/websites/Module.kt | 5 +- .../launcher2/websites/WebsiteRepository.kt | 24 +- .../launcher2/websites/WebsiteViewModel.kt | 11 - .../java/de/mm20/launcher2/widgets/Module.kt | 2 +- .../mm20/launcher2/widgets/WidgetViewModel.kt | 7 +- .../de/mm20/launcher2/wikipedia/Module.kt | 3 +- .../wikipedia/WikipediaRepository.kt | 33 +- .../launcher2/wikipedia/WikipediaViewModel.kt | 11 - 81 files changed, 1335 insertions(+), 1671 deletions(-) delete mode 100644 applications/src/main/java/de/mm20/launcher2/applications/AppViewModel.kt delete mode 100644 calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorViewModel.kt delete mode 100644 calendar/src/main/java/de/mm20/launcher2/calendar/CalendarViewModel.kt delete mode 100644 contacts/src/main/java/de/mm20/launcher2/contacts/ContactViewModel.kt delete mode 100644 favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesViewModel.kt delete mode 100644 search/src/main/java/de/mm20/launcher2/search/BaseSearchableRepository.kt delete mode 100644 search/src/main/java/de/mm20/launcher2/search/SearchViewModel.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesVM.kt rename ui/src/main/java/de/mm20/launcher2/ui/{legacy/component => launcher/modals}/EditFavoritesView.kt (85%) create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/HiddenItemsVM.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/HiddenItemsView.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/searchable/CalendarEventItem.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/searchable/DeprecatedSearchableList.kt delete mode 100644 unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterViewModel.kt delete mode 100644 websites/src/main/java/de/mm20/launcher2/websites/WebsiteViewModel.kt delete mode 100644 wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaViewModel.kt diff --git a/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt b/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt index ee5538cb..fe71a43d 100644 --- a/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt +++ b/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt @@ -19,9 +19,11 @@ class AddItemActivity : Activity() { val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() } val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() } - val shortcut = AppShortcut(this.applicationContext, shortcutInfo, - packageManager.getApplicationInfo(shortcutInfo.`package`, 0) - .loadLabel(packageManager).toString()) + val shortcut = AppShortcut( + this.applicationContext, shortcutInfo, + packageManager.getApplicationInfo(shortcutInfo.`package`, 0) + .loadLabel(packageManager).toString() + ) if (pinRequest.accept()) { favoritesRepository.pinItem(shortcut) } 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 3781bef5..4252ced6 100644 --- a/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt +++ b/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt @@ -10,70 +10,69 @@ import android.content.pm.ShortcutInfo import android.os.Process import android.os.UserHandle import android.util.Log -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.BadgeProvider import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.preferences.LauncherPreferences -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.data.AppInstallation import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.LauncherApp import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext -class AppRepository( - val context: Context, +interface AppRepository { + fun search(query: String): Flow> +} + +class AppRepositoryImpl( + private val context: Context, hiddenItemsRepository: HiddenItemsRepository, - badgeProvider: BadgeProvider - ) : BaseSearchableRepository() { + private val badgeProvider: BadgeProvider +) : AppRepository { - private val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + private val launcherApps = + context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val applications = MediatorLiveData>() + private val installedApps = MutableStateFlow>(emptyList()) + private val installations = MutableStateFlow>(mutableListOf()) + private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys - private val installedApps = MutableLiveData>(emptyList()) - private val installations = MutableLiveData>(mutableListOf()) - private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys + private val profiles: List = + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle()) + } else { + listOf(Process.myUserHandle()) + } + private val installingPackages = mutableMapOf() - private val profiles: List = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle()) - } else { - listOf(Process.myUserHandle()) - } - - init { - - applications.addSource(installedApps) { - launch { updateAppsForDisplay() } - } - applications.addSource(installations) { - launch { updateAppsForDisplay() } - } - - applications.addSource(hiddenItemKeys) { - launch { updateAppsForDisplay() } - } - launcherApps.registerCallback(object : LauncherApps.Callback() { - override fun onPackagesUnavailable(packageNames: Array, user: UserHandle, replacing: Boolean) { - installedApps.value = installedApps.value?.filter { !packageNames.contains(it.`package`) } + override fun onPackagesUnavailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + installedApps.value = + installedApps.value.filter { !packageNames.contains(it.`package`) } } override fun onPackageChanged(packageName: String, user: UserHandle) { - val apps = installedApps.value?.toMutableList() ?: return + val apps = installedApps.value.toMutableList() apps.removeAll { packageName == it.`package` } apps.addAll(getApplications(packageName)) installedApps.value = apps } - override fun onPackagesAvailable(packageNames: Array, user: UserHandle, replacing: Boolean) { - val apps = installedApps.value?.toMutableList() ?: return + override fun onPackagesAvailable( + packageNames: Array, + user: UserHandle, + replacing: Boolean + ) { + val apps = installedApps.value.toMutableList() for (packageName in packageNames) { apps.addAll(getApplications(packageName)) } @@ -82,16 +81,20 @@ class AppRepository( override fun onPackageAdded(packageName: String, user: UserHandle) { Log.d("MM20", "App installed: $packageName") - val apps = installedApps.value?.toMutableList() ?: return + val apps = installedApps.value.toMutableList() apps.addAll(getApplications(packageName)) installedApps.value = apps } override fun onPackageRemoved(packageName: String, user: UserHandle) { - installedApps.value = installedApps.value?.filter { packageName != (it.`package`) } + installedApps.value = installedApps.value.filter { packageName != (it.`package`) } } - override fun onShortcutsChanged(packageName: String, shortcuts: MutableList, user: UserHandle) { + override fun onShortcutsChanged( + packageName: String, + shortcuts: MutableList, + user: UserHandle + ) { super.onShortcutsChanged(packageName, shortcuts, user) onPackageChanged(packageName, user) } @@ -99,11 +102,17 @@ class AppRepository( override fun onPackagesSuspended(packageNames: Array?, user: UserHandle?) { super.onPackagesSuspended(packageNames, user) packageNames?.forEach { - badgeProvider.setBadge("app://$it", Badge(iconRes = R.drawable.ic_badge_suspended)) + badgeProvider.setBadge( + "app://$it", + Badge(iconRes = R.drawable.ic_badge_suspended) + ) } } - override fun onPackagesUnsuspended(packageNames: Array?, user: UserHandle?) { + override fun onPackagesUnsuspended( + packageNames: Array?, + user: UserHandle? + ) { super.onPackagesUnsuspended(packageNames, user) packageNames?.forEach { badgeProvider.removeBadge("app://$it") @@ -111,7 +120,10 @@ class AppRepository( } }) - + val apps = profiles.map { p -> + launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) } + }.flatten() + installedApps.value = apps val packageInstaller = context.packageManager.packageInstaller @@ -139,13 +151,13 @@ class AppRepository( installingPackages.remove(sessionId) val key = "app://$pkg" val badge = badgeProvider.getBadge(key)?.apply { progress = null } - ?: Badge() + ?: Badge() badgeProvider.setBadge(key, badge) - val inst = installations.value ?: return + val inst = installations.value inst.removeAll { it.session.sessionId == sessionId } - installations.postValue(inst) + installations.value = inst } @@ -165,41 +177,66 @@ class AppRepository( val appInstallation = AppInstallation(session) val inst = installations.value ?: mutableListOf() inst.add(appInstallation) - installations.postValue(inst) + installations.value = inst } }) - - - val apps = profiles.map { p -> launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) } }.flatten() - installedApps.value = apps } - override suspend fun search(query: String) { - updateAppsForDisplay() + private fun getApplications(packageName: String): List { + if (packageName == context.packageName) return emptyList() + + return profiles.map { p -> + launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } + }.flatten() } - private suspend fun updateAppsForDisplay() { - val query = searchRepository.currentQuery.value ?: "" - val componentName = ComponentName.unflattenFromString(query) + private fun getApplication( + launcherActivityInfo: LauncherActivityInfo, + profile: UserHandle + ): Application? { + if (launcherActivityInfo.applicationInfo.packageName == context.packageName && !context.packageName.endsWith( + ".debug" + ) + ) return null + return LauncherApp(context, launcherActivityInfo) + } - val apps = withContext(Dispatchers.Default) { - val hiddenItems = hiddenItemKeys.value ?: emptyList() - val installed = installedApps.value ?: emptyList() - val installing = installations.value ?: emptyList() - val results = mutableListOf() - results.addAll(installed) - results.addAll(installing) - if (query.isNotEmpty()) { - results.removeAll { !it.label.contains(query, ignoreCase = true) } - getActivityByComponentName(componentName)?.let { results.add(it) } + override fun search(query: String): Flow> = channelFlow { + + combine(installedApps, hiddenItems, installations) {_, _, _ -> + null + }.collectLatest { + withContext(Dispatchers.IO) { + val appResults = mutableListOf() + if (query.isEmpty()) { + appResults.addAll(installedApps.value) + appResults.addAll(installations.value) + } else { + appResults.addAll(installedApps.value.filter { + it.label.contains( + query, + ignoreCase = true + ) + }) + appResults.addAll(installations.value.filter { + it.label.contains( + query, + ignoreCase = true + ) + }) + } + + val componentName = ComponentName.unflattenFromString(query) + getActivityByComponentName(componentName)?.let { appResults.add(it) } + + appResults.removeAll { hiddenItems.value.contains(it.key) } + + appResults.sort() + + send(appResults) } - results.removeAll { hiddenItems.contains(it.key) } - results.sort() - results } - - applications.value = apps } private fun getActivityByComponentName(componentName: ComponentName?): Application? { @@ -211,15 +248,4 @@ class AppRepository( LauncherApp(context, lai) } } - - private fun getApplication(launcherActivityInfo: LauncherActivityInfo, profile: UserHandle): Application? { - if (launcherActivityInfo.applicationInfo.packageName == context.packageName && !context.packageName.endsWith(".debug")) return null - return LauncherApp(context, launcherActivityInfo) - } - - private fun getApplications(packageName: String): List { - if (packageName == context.packageName) return emptyList() - - return profiles.map { p -> launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } }.flatten() - } } \ No newline at end of file diff --git a/applications/src/main/java/de/mm20/launcher2/applications/AppViewModel.kt b/applications/src/main/java/de/mm20/launcher2/applications/AppViewModel.kt deleted file mode 100644 index 98691f0c..00000000 --- a/applications/src/main/java/de/mm20/launcher2/applications/AppViewModel.kt +++ /dev/null @@ -1,12 +0,0 @@ -package de.mm20.launcher2.applications - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import de.mm20.launcher2.search.data.Application - -class AppViewModel( - appRepository: AppRepository -): ViewModel() { - val applications: LiveData> = appRepository.applications -} - diff --git a/applications/src/main/java/de/mm20/launcher2/applications/Module.kt b/applications/src/main/java/de/mm20/launcher2/applications/Module.kt index 26e7b4be..fe879c42 100644 --- a/applications/src/main/java/de/mm20/launcher2/applications/Module.kt +++ b/applications/src/main/java/de/mm20/launcher2/applications/Module.kt @@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val applicationsModule = module { - single { AppRepository(androidContext(), get(), get()) } - viewModel { AppViewModel(get()) } + single { AppRepositoryImpl(androidContext(), get(), get()) } } \ 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 5cdd727d..b204296d 100644 --- a/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt +++ b/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt @@ -1,40 +1,42 @@ package de.mm20.launcher2.calculator -import androidx.lifecycle.MutableLiveData import de.mm20.launcher2.preferences.LauncherPreferences -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.data.Calculator +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import org.mariuszgromada.math.mxparser.Expression -class CalculatorRepository : BaseSearchableRepository() { +interface CalculatorRepository { + fun search(query: String): Flow +} - val calculator = MutableLiveData() +class CalculatorRepositoryImpl : CalculatorRepository { - override suspend fun search(query: String) { + override fun search(query: String): Flow = channelFlow { if (query.isBlank()) { - calculator.value = null - return + send(null) + return@channelFlow } - if (!LauncherPreferences.instance.searchCalculator) return + if (!LauncherPreferences.instance.searchCalculator) return@channelFlow val calc = when { query.matches(Regex("0x[0-9a-fA-F]+")) -> { val solution = query.substring(2).toIntOrNull(16) ?: run { - calculator.value = null - return + send(null) + return@channelFlow } Calculator(term = query, solution = solution.toDouble()) } query.matches(Regex("0b[01]+")) -> { val solution = query.substring(2).toIntOrNull(2) ?: run { - calculator.value = null - return + send(null) + return@channelFlow } Calculator(term = query, solution = solution.toDouble()) } query.matches(Regex("0[0-7]+")) -> { val solution = query.substring(1).toIntOrNull(8) ?: run { - calculator.value = null - return + send(null) + return@channelFlow } Calculator(term = query, solution = solution.toDouble()) } @@ -50,6 +52,6 @@ class CalculatorRepository : BaseSearchableRepository() { } } } - calculator.value = calc + send(calc) } } \ No newline at end of file diff --git a/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorViewModel.kt b/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorViewModel.kt deleted file mode 100644 index ccd93e19..00000000 --- a/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorViewModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package de.mm20.launcher2.calculator - -import androidx.lifecycle.ViewModel - -class CalculatorViewModel( - calculatorRepository: CalculatorRepository -): ViewModel() { - val calculator = calculatorRepository.calculator -} \ No newline at end of file diff --git a/calculator/src/main/java/de/mm20/launcher2/calculator/Module.kt b/calculator/src/main/java/de/mm20/launcher2/calculator/Module.kt index 6afdd62e..2d7399ec 100644 --- a/calculator/src/main/java/de/mm20/launcher2/calculator/Module.kt +++ b/calculator/src/main/java/de/mm20/launcher2/calculator/Module.kt @@ -1,9 +1,7 @@ package de.mm20.launcher2.calculator -import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val calculatorModule = module { - single { CalculatorRepository() } - viewModel { CalculatorViewModel(get()) } + single { CalculatorRepositoryImpl() } } \ No newline at end of file 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 e3fe78f7..b4150607 100644 --- a/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt +++ b/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt @@ -1,44 +1,80 @@ package de.mm20.launcher2.calendar import android.content.Context -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData +import android.util.Log import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.preferences.LauncherPreferences -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.data.CalendarEvent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext -class CalendarRepository( - val context: Context, +interface CalendarRepository { + fun search(query: String): Flow> + + fun getUpcomingEvents(): Flow> +} + +class CalendarRepositoryImpl( + private val context: Context, hiddenItemsRepository: HiddenItemsRepository -) : BaseSearchableRepository() { +) : CalendarRepository { - val calendarEvents = MediatorLiveData?>() - val upcomingCalendarEvents = MutableLiveData>(emptyList()) + private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys - private val allEvents = MutableLiveData?>(emptyList()) - private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys - - init { - calendarEvents.addSource(hiddenItemKeys) { keys -> - calendarEvents.value = allEvents.value?.filter { !keys.contains(it.key) } + override fun search(query: String): Flow> = channelFlow { + if (query.isBlank()) { + send(emptyList()) + return@channelFlow } - calendarEvents.addSource(allEvents) { e -> - calendarEvents.value = e?.filter { hiddenItemKeys.value?.contains(it.key) != true } + val events = withContext(Dispatchers.IO) { + val now = System.currentTimeMillis() + CalendarEvent.search( + context, + query, + intervalStart = now, + intervalEnd = now + 14 * 24 * 60 * 60 * 1000L, + ) } - hiddenItemKeys.observeForever { - requestCalendarUpdate() + + hiddenItems.collectLatest { hiddenItems -> + val calendarResults = withContext(Dispatchers.IO) { + events.filter { !hiddenItems.contains(it.key) } + } + send(calendarResults) } } - fun requestCalendarUpdate() { - launch { - val unselectedCalendars = LauncherPreferences.instance.unselectedCalendars - val hideAlldayEvents = LauncherPreferences.instance.calendarHideAllday + override fun getUpcomingEvents(): Flow> = channelFlow { + val unselectedCalendars = callbackFlow { + val unregister = + LauncherPreferences.instance.doOnPreferenceChange("unselected_calendars") { + trySendBlocking(LauncherPreferences.instance.unselectedCalendars) + } + trySendBlocking(LauncherPreferences.instance.unselectedCalendars) + awaitClose { + unregister() + } + } + val hideAlldayEvents = callbackFlow { + val unregister = + LauncherPreferences.instance.doOnPreferenceChange("calendar_hide_allday") { + trySendBlocking(LauncherPreferences.instance.calendarHideAllday) + } + trySendBlocking(LauncherPreferences.instance.calendarHideAllday) + awaitClose { + unregister() + } + } + + merge(unselectedCalendars, hideAlldayEvents, hiddenItems).collectLatest { + Log.d("MM20", "Calendar event flow has been created") val now = System.currentTimeMillis() val end = now + 14 * 24 * 60 * 60 * 1000L val events = withContext(Dispatchers.IO) { @@ -48,28 +84,20 @@ class CalendarRepository( intervalStart = now, intervalEnd = end, limit = 700, - hideAllDayEvents = hideAlldayEvents, - unselectedCalendars = unselectedCalendars, - hiddenEvents = hiddenItemKeys.value?.mapNotNull { - if (it.startsWith("calendar")) it.substringAfterLast("/").toLong() - else null - } ?: emptyList() - ) + hideAllDayEvents = LauncherPreferences.instance.calendarHideAllday, + unselectedCalendars = LauncherPreferences.instance.unselectedCalendars + ).filter { + !hiddenItems.value.contains(it.key) + } } - upcomingCalendarEvents.value = events + send(events) } } - override suspend fun search(query: String) { - if (query.isBlank()) { - allEvents.value = null - return + var unselectedCalendars: List + get() = LauncherPreferences.instance.unselectedCalendars + set(value) { + LauncherPreferences.instance.unselectedCalendars = value } - val startTime = System.currentTimeMillis() - val endTime = System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000 - val events = withContext(Dispatchers.IO) { - CalendarEvent.search(context, query, startTime, endTime) - } - allEvents.value = events - } + } \ No newline at end of file diff --git a/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarViewModel.kt b/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarViewModel.kt deleted file mode 100644 index d156e7b7..00000000 --- a/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarViewModel.kt +++ /dev/null @@ -1,12 +0,0 @@ -package de.mm20.launcher2.calendar - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import de.mm20.launcher2.search.data.CalendarEvent - -class CalendarViewModel( - calendarRepository: CalendarRepository -): ViewModel() { - val calendarEvents: LiveData?> = calendarRepository.calendarEvents - val upcomingCalendarEvents: LiveData> = calendarRepository.upcomingCalendarEvents -} \ No newline at end of file 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 8458f5de..f03c78b6 100644 --- a/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt +++ b/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt @@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val calendarModule = module { - single { CalendarRepository(androidContext(), get()) } - viewModel { CalendarViewModel(get()) } + single { CalendarRepositoryImpl(androidContext(), get()) } } \ No newline at end of file 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 1421060f..0a350c1a 100644 --- a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt +++ b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt @@ -1,41 +1,35 @@ package de.mm20.launcher2.contacts import android.content.Context -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData import de.mm20.launcher2.hiddenitems.HiddenItemsRepository -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.data.Contact import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext -class ContactRepository( - val context: Context, +interface ContactRepository { + fun search(query: String): Flow> +} + +class ContactRepositoryImpl( + private val context: Context, hiddenItemsRepository: HiddenItemsRepository -) : BaseSearchableRepository() { +) : ContactRepository { - val contacts = MediatorLiveData?>() + private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys - private val allContacts = MutableLiveData?>(emptyList()) - private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys - - init { - contacts.addSource(hiddenItemKeys) { keys -> - contacts.value = allContacts.value?.filter { !keys.contains(it.key) } - } - contacts.addSource(allContacts) { c -> - contacts.value = c?.filter { hiddenItemKeys.value?.contains(it.key) != true } - } - } - - override suspend fun search(query: String) { - if (query.isBlank()) { - allContacts.value = null - return - } - val results = withContext(Dispatchers.IO) { + override fun search(query: String): Flow> = channelFlow { + val contacts = withContext(Dispatchers.IO) { Contact.search(context, query) } - allContacts.value = results + hiddenItems.collectLatest { hiddenItems -> + val contactResults = withContext(Dispatchers.IO) { + contacts.filter { !hiddenItems.contains(it.key) } + } + send(contactResults) + } } } \ No newline at end of file diff --git a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactViewModel.kt b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactViewModel.kt deleted file mode 100644 index 7154d28e..00000000 --- a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactViewModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.mm20.launcher2.contacts - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import de.mm20.launcher2.search.data.Contact - -class ContactViewModel( - contactRepository: ContactRepository -) : ViewModel() { - val contacts: LiveData?> = contactRepository.contacts -} \ No newline at end of file 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 20661c31..a94ea095 100644 --- a/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt +++ b/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt @@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val contactsModule = module { - single { ContactRepository(androidContext(), get()) } - viewModel { ContactViewModel(get()) } + single { ContactRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/currencies/src/main/java/de/mm20/launcher2/currencies/CurrencyRepository.kt b/currencies/src/main/java/de/mm20/launcher2/currencies/CurrencyRepository.kt index c27b1d8a..2a4028ac 100644 --- a/currencies/src/main/java/de/mm20/launcher2/currencies/CurrencyRepository.kt +++ b/currencies/src/main/java/de/mm20/launcher2/currencies/CurrencyRepository.kt @@ -13,7 +13,7 @@ class CurrencyRepository(val context: Context) { init { val currencyWorker = PeriodicWorkRequest.Builder(ExchangeRateWorker::class.java, 60, TimeUnit.MINUTES) .build() - WorkManager.getInstance().enqueueUniquePeriodicWork("ExchangeRates", + WorkManager.getInstance(context).enqueueUniquePeriodicWork("ExchangeRates", ExistingPeriodicWorkPolicy.REPLACE, currencyWorker) } diff --git a/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt b/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt index d66fa64f..1d3974f3 100644 --- a/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import androidx.room.* import de.mm20.launcher2.database.entities.FavoritesItemEntity import de.mm20.launcher2.database.entities.WebsearchEntity +import kotlinx.coroutines.flow.Flow @Dao interface SearchDao { @@ -18,7 +19,10 @@ interface SearchDao { fun insertSkipExisting(items: FavoritesItemEntity) @Query("SELECT * FROM Searchable WHERE pinned > 0 ORDER BY pinned DESC, launchCount DESC") - fun getFavorites() : LiveData> + fun getFavorites(): Flow> + + @Query("SELECT * FROM Searchable WHERE pinned > 0 AND `key` LIKE 'calendar://%' ORDER BY pinned DESC, launchCount DESC") + fun getPinnedCalendarEvents(): Flow> @Query("SELECT COUNT(key) as count FROM Searchable WHERE pinned = 1;") @@ -44,14 +48,14 @@ interface SearchDao { fun unpinFavorite(key: String) @Query("DELETE FROM Searchable WHERE `key` = :key") - fun deleteByKey(key: String) + suspend fun deleteByKey(key: String) @Query("UPDATE Searchable SET pinned = 0 WHERE `key` = :key") fun unpinApp(key: String) @Query("SELECT pinned FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinned ORDER BY pinned DESC LIMIT 1") - fun isPinned(key: String): LiveData + fun isPinned(key: String): Flow @Query("UPDATE Searchable SET hidden = 1, pinned = 0 WHERE `key` = :key") @@ -67,19 +71,16 @@ interface SearchDao { fun unhideItem(key: String) @Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1") - fun isHidden(key: String): LiveData + fun isHidden(key: String): Flow @Query("SELECT `key` FROM SEARCHABLE WHERE hidden = 1") - fun getHiddenItemKeys(): LiveData> + fun getHiddenItemKeys(): Flow> @Query("SELECT * FROM SEARCHABLE WHERE hidden = 1") - fun getHiddenItems(): LiveData> + fun getHiddenItems(): Flow> @Query("SELECT * FROM Websearch ORDER BY label ASC") - fun getWebSearches(): List - - @Query("SELECT * FROM Websearch ORDER BY label ASC") - fun getWebSearchesLiveData(): LiveData> + fun getWebSearches(): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertWebsearch(websearch: WebsearchEntity) diff --git a/favorites/build.gradle.kts b/favorites/build.gradle.kts index ced9b9b0..60fd1893 100644 --- a/favorites/build.gradle.kts +++ b/favorites/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(libs.bundles.kotlin) implementation(libs.androidx.core) implementation(libs.androidx.appcompat) + implementation(libs.bundles.androidx.lifecycle) implementation(libs.koin.android) diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt index 1783cd38..fc50aba3 100644 --- a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt @@ -1,38 +1,183 @@ package de.mm20.launcher2.favorites import android.content.Context -import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.entities.FavoritesItemEntity import de.mm20.launcher2.ktx.ceilToInt import de.mm20.launcher2.preferences.LauncherPreferences -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.Searchable import kotlinx.coroutines.* +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.* +import org.koin.core.component.KoinComponent import org.koin.core.component.get -import org.koin.core.component.inject import org.koin.core.parameter.parametersOf -import kotlin.math.max -import kotlin.math.min -class FavoritesRepository(private val context: Context) : BaseSearchableRepository() { +interface FavoritesRepository { + fun getFavorites(): Flow> + fun getPinnedCalendarEvents(): Flow> + fun isPinned(searchable: Searchable): Flow + fun pinItem(searchable: Searchable) + fun unpinItem(searchable: Searchable) + fun isHidden(searchable: Searchable): Flow + fun hideItem(searchable: Searchable) + fun unhideItem(searchable: Searchable) + fun incrementLaunchCounter(searchable: Searchable) + suspend fun getAllFavoriteItems(): List + fun saveFavorites(favorites: List) + fun getHiddenItems(): Flow> +} - private val scope = CoroutineScope(Job() + Dispatchers.Main) +class FavoritesRepositoryImpl( + private val context: Context, + private val database: AppDatabase +) : FavoritesRepository, KoinComponent { - private val favorites = MediatorLiveData>() - private val favoriteItems: LiveData> = MutableLiveData() + private val scope = CoroutineScope(Dispatchers.Main + Job()) - val hiddenItems = MediatorLiveData>() + override fun getFavorites(): Flow> = channelFlow { - private val pinnedFavorites = AppDatabase.getInstance(context).searchDao().getFavorites() + withContext(Dispatchers.IO) { + val gridColumns = callbackFlow { + send(LauncherPreferences.instance.gridColumnCount) + val unregister = + LauncherPreferences.instance.doOnPreferenceChange("grid_column_count") { + trySendBlocking(LauncherPreferences.instance.gridColumnCount) + } + awaitClose { + unregister() + } + } + val dao = database.searchDao() + + val pinnedFavorites = dao.getFavorites().map { + it.mapNotNull { + val item = fromDatabaseEntity(it).searchable + if (item == null) { + dao.deleteByKey(it.key) + } + return@mapNotNull item + } + } + + pinnedFavorites.collectLatest { pinned -> + gridColumns.collectLatest { columns -> + var favCount = (pinned.size.toDouble() / columns).ceilToInt() * columns + if (pinned.size < columns) favCount += columns + val autoFavs = dao.getAutoFavorites(favCount - pinned.size).mapNotNull { + val item = fromDatabaseEntity(it).searchable + if (item == null) { + dao.deleteByKey(it.key) + } + return@mapNotNull item + } + send(pinned + autoFavs) + } + } + } + } + + override fun getPinnedCalendarEvents(): Flow> { + return database.searchDao().getPinnedCalendarEvents().map { + it.mapNotNull { fromDatabaseEntity(it).searchable as? CalendarEvent } + } + } + + override fun isPinned(searchable: Searchable): Flow { + return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key) + } + + override fun pinItem(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + val dao = AppDatabase.getInstance(context).searchDao() + val databaseItem = dao.getFavorite(searchable.key) + val favoritesItem = FavoritesItem( + key = searchable.key, + searchable = searchable, + launchCount = databaseItem?.launchCount ?: 0, + pinPosition = 1, + hidden = false + ) + dao.insertReplaceExisting(favoritesItem.toDatabaseEntity()) + } + } + } + + override fun unpinItem(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key) + } + } + } + + override fun isHidden(searchable: Searchable): Flow { + return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key) + } + + override fun hideItem(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + val dao = AppDatabase.getInstance(context).searchDao() + val databaseItem = dao.getFavorite(searchable.key) + val favoritesItem = FavoritesItem( + key = searchable.key, + searchable = searchable, + launchCount = databaseItem?.launchCount ?: 0, + pinPosition = 0, + hidden = true + ) + dao.insertReplaceExisting(favoritesItem.toDatabaseEntity()) + } + } + } + + override fun unhideItem(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key) + } + } + } + + override fun incrementLaunchCounter(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + val item = FavoritesItem(searchable.key, searchable, 0, 0, false) + AppDatabase.getInstance(context).searchDao() + .incrementLaunchCount(item.toDatabaseEntity()) + } + } + } + + override suspend fun getAllFavoriteItems(): List { + return withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull { + fromDatabaseEntity(it).takeIf { it.searchable != null } + } + } + } + + override fun saveFavorites(favorites: List) { + scope.launch { + withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao() + .saveFavorites(favorites.map { it.toDatabaseEntity() }) + } + } + } + + override fun getHiddenItems(): Flow> { + return database.searchDao().getHiddenItems().map { + it.mapNotNull { fromDatabaseEntity(it).searchable } + } + } - val pinnedCalendarEvents = MediatorLiveData>() private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem { val deserializer: SearchableDeserializer = get { parametersOf(entity.serializedSearchable) } @@ -44,183 +189,4 @@ class FavoritesRepository(private val context: Context) : BaseSearchableReposito hidden = entity.hidden ) } - - private val reloadFavorites: (String) -> Unit = { - scope.launch { - if(!LauncherPreferences.instance.searchShowFavorites) { - favorites.value = emptyList() - return@launch - } - val favs = mutableListOf() - withContext(Dispatchers.IO) { - val dao = AppDatabase.getInstance(context).searchDao() - val favItems = pinnedFavorites.value ?: emptyList() - favs.addAll(favItems.mapNotNull { - val item = fromDatabaseEntity(it) - if (item.searchable == null) { - dao.deleteByKey(item.key) - } - if (item.searchable is CalendarEvent) return@mapNotNull null - item.searchable - }) - var favCount = (favs.size.toDouble() / columns).ceilToInt() * columns - if(favItems.size < columns) favCount += columns - val autoFavs = dao.getAutoFavorites(favCount - favs.size) - favs.addAll(autoFavs.mapNotNull { - val item = fromDatabaseEntity(it) - if (item.searchable == null) { - dao.deleteByKey(item.key) - } - item.searchable - }) - } - favorites.value = favs - } - } - - private var columns = 1 - - init { - val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems() - hiddenItems.addSource(hidden) { h -> - hiddenItems.value = h.mapNotNull { fromDatabaseEntity(it).searchable } - } - favorites.addSource(pinnedFavorites) { - reloadFavorites("") - } - pinnedCalendarEvents.addSource(pinnedFavorites) { - scope.launch { - val dao = AppDatabase.getInstance(context).searchDao() - pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull { - val item = fromDatabaseEntity(it) - if (item.searchable == null) { - withContext(Dispatchers.IO) { dao.deleteByKey(item.key) } - } - item.searchable as? CalendarEvent - } - } - } - LauncherPreferences.instance.doOnPreferenceChange( - "search_show_favorites", - "search_auto_add_favorites", - action = reloadFavorites - ) - } - - - fun isHidden(searchable: Searchable): LiveData { - return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key) - } - - fun getFavorites(columns: Int): LiveData> { - if (columns != this.columns) { - this.columns = columns - reloadFavorites("") - } - return favorites - } - - override suspend fun search(query: String) { - if (query.isEmpty()) { - reloadFavorites("") - } else { - favorites.value = emptyList() - } - } - - fun isPinned(searchable: Searchable): LiveData { - return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key) - } - - fun pinItem(searchable: Searchable) { - scope.launch { - withContext(Dispatchers.IO) { - val dao = AppDatabase.getInstance(context).searchDao() - val databaseItem = dao.getFavorite(searchable.key) - val favoritesItem = FavoritesItem( - key = searchable.key, - searchable = searchable, - launchCount = databaseItem?.launchCount ?: 0, - pinPosition = 1, - hidden = false - ) - dao.insertReplaceExisting(favoritesItem.toDatabaseEntity()) - } - } - } - - fun unpinItem(searchable: Searchable) { - scope.launch { - withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key) - } - } - } - - fun hideItem(searchable: Searchable) { - scope.launch { - withContext(Dispatchers.IO) { - val dao = AppDatabase.getInstance(context).searchDao() - val databaseItem = dao.getFavorite(searchable.key) - val favoritesItem = FavoritesItem( - key = searchable.key, - searchable = searchable, - launchCount = databaseItem?.launchCount ?: 0, - pinPosition = 0, - hidden = true - ) - dao.insertReplaceExisting(favoritesItem.toDatabaseEntity()) - } - } - } - - fun unhideItem(searchable: Searchable) { - scope.launch { - withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key) - } - } - } - - fun deleteItem(key: String) { - scope.launch { - withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).searchDao().deleteByKey(key) - } - } - } - - fun incrementLaunchCount(searchable: Searchable) { - scope.launch { - withContext(Dispatchers.IO) { - val item = FavoritesItem(searchable.key, searchable, 0, 0, false) - AppDatabase.getInstance(context).searchDao().incrementLaunchCount(item.toDatabaseEntity()) - } - } - } - - suspend fun getAllFavoriteItems(): List { - return withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull { - fromDatabaseEntity(it).takeIf { it.searchable != null } - } - } - } - - fun saveFavorites(favorites: MutableList) { - scope.launch { - withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).searchDao().saveFavorites(favorites.map { it.toDatabaseEntity() }) - } - } - } - - fun getTopFavorites(count: Int): LiveData> { - val favs = MediatorLiveData>() - favs.addSource(favorites) { - favs.value = it.subList(0, min(count, it.size)) - } - return favs - } - } \ No newline at end of file diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesViewModel.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesViewModel.kt deleted file mode 100644 index 7a3b471e..00000000 --- a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesViewModel.kt +++ /dev/null @@ -1,54 +0,0 @@ -package de.mm20.launcher2.favorites - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import de.mm20.launcher2.search.data.CalendarEvent -import de.mm20.launcher2.search.data.Searchable - -class FavoritesViewModel( - private val favoritesRepository: FavoritesRepository -) : ViewModel() { - - fun getTopFavorites(count: Int): LiveData> { - return favoritesRepository.getTopFavorites(count) - } - - fun getFavorites(columns: Int): LiveData> { - return favoritesRepository.getFavorites(columns) - } - - fun pinItem(searchable: Searchable) { - favoritesRepository.pinItem(searchable) - } - - fun unpinItem(searchable: Searchable) { - favoritesRepository.unpinItem(searchable) - } - - fun isPinned(searchable: Searchable): LiveData { - return favoritesRepository.isPinned(searchable) - } - - fun isHidden(searchable: Searchable): LiveData { - return favoritesRepository.isHidden(searchable) - } - - fun hideItem(searchable: Searchable) { - favoritesRepository.hideItem(searchable) - } - - fun unhideItem(searchable: Searchable) { - favoritesRepository.unhideItem(searchable) - } - - suspend fun getAllFavoriteItems(): List { - return favoritesRepository.getAllFavoriteItems() - } - - fun saveFavorites(favorites: MutableList) { - favoritesRepository.saveFavorites(favorites) - } - - val hiddenItems: LiveData> = this.favoritesRepository.hiddenItems - val pinnedCalendarEvents: LiveData> = this.favoritesRepository.pinnedCalendarEvents -} \ No newline at end of file diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt index 4d85e7a5..e4d8f50d 100644 --- a/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt @@ -91,7 +91,5 @@ val favoritesModule = module { return@factory NullDeserializer() } - single { FavoritesRepository(androidContext()) } - - viewModel { FavoritesViewModel(get()) } + single { FavoritesRepositoryImpl(androidContext(), get()) } } \ No newline at end of file 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 9f8a9c1f..722fc3e5 100644 --- a/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt +++ b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt @@ -1,26 +1,26 @@ package de.mm20.launcher2.files import android.content.Context -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.owncloud.OwncloudClient -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.data.* import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest -class FilesRepository( - val context: Context, +interface FileRepository { + fun search(query: String): Flow> + suspend fun deleteFile(file: File) +} + +class FileRepositoryImpl( + private val context: Context, hiddenItemsRepository: HiddenItemsRepository -) : BaseSearchableRepository() { +) : FileRepository { - private val scope = CoroutineScope(Job() + Dispatchers.Main) - - val files = MediatorLiveData?>() - - private val allFiles = MutableLiveData?>(emptyList()) - private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys + private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys private val nextcloudClient by lazy { NextcloudApiHelper(context) @@ -29,44 +29,39 @@ class FilesRepository( OwncloudClient(context) } - init { - files.addSource(hiddenItemKeys) { keys -> - files.value = allFiles.value?.filter { !keys.contains(it.key) } - } - files.addSource(allFiles) { f -> - files.value = f?.filter { hiddenItemKeys.value?.contains(it.key) != true } - } - } - - override suspend fun search(query: String) { + override fun search(query: String): Flow> = channelFlow { if (query.isBlank()) { - allFiles.value = null - return + send(emptyList()) + return@channelFlow } - val localFiles = withContext(Dispatchers.IO) { - LocalFile.search(context, query).sorted().toMutableList() - } - allFiles.value = localFiles - val cloudFiles = withContext(Dispatchers.IO) { - delay(300) - listOf( - async { OneDriveFile.search(context, query) }, - async { GDriveFile.search(context, query) }, - async { NextcloudFile.search(context, query, nextcloudClient) }, - async { OwncloudFile.search(context, query, owncloudClient) } - ).awaitAll().flatten() + hiddenItems.collectLatest { hiddenItems -> + val files = mutableListOf() + + val localFiles = withContext(Dispatchers.IO) { + LocalFile.search(context, query).sorted().filter { !hiddenItems.contains(it.key) } + } + files.addAll(localFiles) + send(localFiles) + + val cloudFiles = withContext(Dispatchers.IO) { + delay(300) + listOf( + async { OneDriveFile.search(context, query) }, + async { GDriveFile.search(context, query) }, + async { NextcloudFile.search(context, query, nextcloudClient) }, + async { OwncloudFile.search(context, query, owncloudClient) } + ).awaitAll().flatten() + } + yield() + files.addAll(cloudFiles.filter { !hiddenItems.contains(it.key) }) + send(files) } - yield() - allFiles.value = localFiles + cloudFiles } - fun deleteFile(file: File) { + override suspend fun deleteFile(file: File) { if (file.isDeletable) { - scope.launch { - file.delete(context) - allFiles.value = allFiles.value?.filter { it != file } - } + file.delete(context) } } } \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt b/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt index e9585585..c038d48c 100644 --- a/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt +++ b/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt @@ -1,16 +1,16 @@ package de.mm20.launcher2.files import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import de.mm20.launcher2.search.data.File +import kotlinx.coroutines.launch class FilesViewModel( - private val filesRepository: FilesRepository + private val filesRepository: FileRepository ): ViewModel() { - - - val files = filesRepository.files - fun deleteFile(file: File) { - filesRepository.deleteFile(file) + viewModelScope.launch { + filesRepository.deleteFile(file) + } } } \ No newline at end of file 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 1d0b446c..458f43a9 100644 --- a/files/src/main/java/de/mm20/launcher2/files/Module.kt +++ b/files/src/main/java/de/mm20/launcher2/files/Module.kt @@ -5,6 +5,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val filesModule = module { - single { FilesRepository(androidContext(), get()) } + single { FileRepositoryImpl(androidContext(), get()) } viewModel { FilesViewModel(get()) } } \ No newline at end of file diff --git a/hiddenitems/build.gradle.kts b/hiddenitems/build.gradle.kts index 30922688..a555d243 100644 --- a/hiddenitems/build.gradle.kts +++ b/hiddenitems/build.gradle.kts @@ -35,7 +35,7 @@ android { } dependencies { - implementation(libs.kotlin.stdlib) + implementation(libs.bundles.kotlin) implementation(libs.androidx.core) implementation(libs.androidx.appcompat) diff --git a/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsRepository.kt b/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsRepository.kt index 995afad1..3a86323b 100644 --- a/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsRepository.kt +++ b/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsRepository.kt @@ -5,16 +5,34 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.search.data.Searchable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * A low level repository for hidden items. This can only be used to retrieve keys and to check * whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository. */ -class HiddenItemsRepository(val context: Context) { +class HiddenItemsRepository(val context: Context, database: AppDatabase) { - val hiddenItemsKeys : LiveData> = AppDatabase.getInstance(context).searchDao().getHiddenItemKeys() + val scope = CoroutineScope(Job() + Dispatchers.Main) + val hiddenItemsKeys = MutableStateFlow>(emptyList()) - fun isHidden(item: Searchable): LiveData { + init { + scope.launch { + AppDatabase.getInstance(context).searchDao().getHiddenItemKeys().collectLatest { + hiddenItemsKeys.value = it + } + } + } + + + fun isHidden(item: Searchable): Flow { return AppDatabase.getInstance(context).searchDao().isHidden(item.key) } } \ No newline at end of file diff --git a/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/Module.kt b/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/Module.kt index d1c23441..aa1776f3 100644 --- a/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/Module.kt +++ b/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/Module.kt @@ -5,6 +5,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val hiddenItemsModule = module { - single { HiddenItemsRepository(androidContext()) } + single { HiddenItemsRepository(androidContext(), get()) } viewModel { HiddenItemsViewModel(get()) } } \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/View.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/View.kt index a1336b0d..aa6b79ec 100644 --- a/ktx/src/main/java/de/mm20/launcher2/ktx/View.kt +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/View.kt @@ -19,6 +19,9 @@ fun View.asViewGroup(): ViewGroup? { val View.lifecycleScope get() = (context as LifecycleOwner).lifecycleScope +val View.lifecycleOwner + get() = (context as LifecycleOwner) + fun View.setPadding(vertical: Int, horizontal: Int) { setPadding(vertical, horizontal, vertical, horizontal) } \ No newline at end of file diff --git a/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherPreferences.kt b/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherPreferences.kt index f41625f0..ecc75f6e 100644 --- a/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherPreferences.kt +++ b/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherPreferences.kt @@ -1,6 +1,7 @@ package de.mm20.launcher2.preferences import android.app.Application +import android.content.SharedPreferences import androidx.core.content.edit import androidx.preference.PreferenceManager @@ -109,10 +110,14 @@ class LauncherPreferences(val context: Application, version: Int = 3) { var gridColumnCount by IntPreference("grid_column_count", default = context.resources.getInteger(R.integer.config_columnCount)) - fun doOnPreferenceChange(vararg keys: String, action: (String) -> Unit) { - preferences.registerOnSharedPreferenceChangeListener { _, key -> + fun doOnPreferenceChange(vararg keys: String, action: (String) -> Unit): () -> Unit { + val listener = { _: SharedPreferences, key: String -> if (keys.contains(key)) action(key) } + preferences.registerOnSharedPreferenceChangeListener(listener) + return { + preferences.unregisterOnSharedPreferenceChangeListener(listener) + } } companion object { diff --git a/search/src/main/java/de/mm20/launcher2/search/BaseSearchableRepository.kt b/search/src/main/java/de/mm20/launcher2/search/BaseSearchableRepository.kt deleted file mode 100644 index cce2cba3..00000000 --- a/search/src/main/java/de/mm20/launcher2/search/BaseSearchableRepository.kt +++ /dev/null @@ -1,53 +0,0 @@ -package de.mm20.launcher2.search - -import kotlinx.coroutines.* -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext - -abstract class BaseSearchableRepository: KoinComponent { - - private val scope = CoroutineScope(Job() + Dispatchers.Main) - val searchRepository: SearchRepository by inject() - private val searchQuery = searchRepository.currentQuery - - init { - searchQuery.observeForever { - onQueryChange(it) - } - } - - protected open fun onCancel() { - - } - - protected fun launch( - context: CoroutineContext = EmptyCoroutineContext, - start: CoroutineStart = CoroutineStart.DEFAULT, - block: suspend CoroutineScope.() -> Unit - ) { - scope.launch(context, start, block) - } - - private var searchJob: Job? = null - private fun onQueryChange(query: String) { - scope.launch { - onCancel() - searchJob?.takeIf { !it.isCompleted || !it.isCancelled }?.cancelAndJoin() - searchJob = scope.launch { - searchRepository.startSearch() - search(query) - }.also { - it.invokeOnCompletion { searchRepository.endSearch() } - } - } - } - - /** - * Called when the query string changes. This method should change the current data presented - * by this SearchableRepository. - */ - protected abstract suspend fun search(query: String) - -} \ No newline at end of file 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 77a0c4f0..67173f60 100644 --- a/search/src/main/java/de/mm20/launcher2/search/Module.kt +++ b/search/src/main/java/de/mm20/launcher2/search/Module.kt @@ -1,12 +1,9 @@ package de.mm20.launcher2.search -import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val searchModule = module { - single { SearchRepository() } - viewModel { SearchViewModel(get()) } - single { WebsearchRepository(androidContext()) } + single { WebsearchRepositoryImpl(get()) } viewModel { WebsearchViewModel(get()) } } \ No newline at end of file diff --git a/search/src/main/java/de/mm20/launcher2/search/SearchRepository.kt b/search/src/main/java/de/mm20/launcher2/search/SearchRepository.kt index 5aa38405..76a430c7 100644 --- a/search/src/main/java/de/mm20/launcher2/search/SearchRepository.kt +++ b/search/src/main/java/de/mm20/launcher2/search/SearchRepository.kt @@ -22,9 +22,4 @@ class SearchRepository { } } - fun endSearch() { - synchronized(runningSearches) { - runningSearches-- - } - } } \ No newline at end of file diff --git a/search/src/main/java/de/mm20/launcher2/search/SearchViewModel.kt b/search/src/main/java/de/mm20/launcher2/search/SearchViewModel.kt deleted file mode 100644 index c9cf4973..00000000 --- a/search/src/main/java/de/mm20/launcher2/search/SearchViewModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.mm20.launcher2.search - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel - -class SearchViewModel( - private val searchRepository: SearchRepository -) : ViewModel() { - - val isSearching: LiveData = searchRepository.isSearching - - fun search(query: String) { - searchRepository.currentQuery.value = query - } - -} \ No newline at end of file diff --git a/search/src/main/java/de/mm20/launcher2/search/WebsearchRepository.kt b/search/src/main/java/de/mm20/launcher2/search/WebsearchRepository.kt index c9b7e059..f8edc5da 100644 --- a/search/src/main/java/de/mm20/launcher2/search/WebsearchRepository.kt +++ b/search/src/main/java/de/mm20/launcher2/search/WebsearchRepository.kt @@ -1,54 +1,56 @@ package de.mm20.launcher2.search -import android.content.Context -import android.content.Intent -import android.graphics.Color -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.search.data.Websearch -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* -class WebsearchRepository(val context: Context) : BaseSearchableRepository() { +interface WebsearchRepository { + fun search(query: String): Flow> - val websearches = MutableLiveData>(emptyList()) + fun getWebsearches(): Flow> - val allWebsearches = MediatorLiveData>() + fun insertWebsearch(websearch: Websearch) + fun deleteWebsearch(websearch: Websearch) +} - init { - val databaseWebsearches = AppDatabase.getInstance(context).searchDao().getWebSearchesLiveData() - allWebsearches.addSource(databaseWebsearches) { - allWebsearches.value = it.map { Websearch(it) } - } - } +class WebsearchRepositoryImpl( + private val database: AppDatabase +) : WebsearchRepository { - override suspend fun search(query: String) { + private val scope = CoroutineScope(Job() + Dispatchers.Main) + + override fun search(query: String): Flow> = channelFlow { if (query.isEmpty()) { - websearches.value = emptyList() - return + send(emptyList()) + return@channelFlow } - val searches = withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).searchDao().getWebSearches().map { - Websearch(it, query) + withContext(Dispatchers.IO) { + database.searchDao().getWebSearches().map { + it.map { Websearch(it, query) } } + }.collectLatest { + send(it) } - websearches.value = searches } - fun insertWebsearch(websearch: Websearch) { - launch { + override fun getWebsearches(): Flow> = + database.searchDao().getWebSearches().map { + it.map { Websearch(it) } + } + + override fun insertWebsearch(websearch: Websearch) { + scope.launch { withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).searchDao().insertWebsearch(websearch.toDatabaseEntity()) + database.searchDao().insertWebsearch(websearch.toDatabaseEntity()) } } } - fun deleteWebsearch(websearch: Websearch) { - launch { + override fun deleteWebsearch(websearch: Websearch) { + scope.launch { withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).searchDao().deleteWebsearch(websearch.toDatabaseEntity()) + database.searchDao().deleteWebsearch(websearch.toDatabaseEntity()) } } } diff --git a/search/src/main/java/de/mm20/launcher2/search/WebsearchViewModel.kt b/search/src/main/java/de/mm20/launcher2/search/WebsearchViewModel.kt index 6c7229d5..e91be8ca 100644 --- a/search/src/main/java/de/mm20/launcher2/search/WebsearchViewModel.kt +++ b/search/src/main/java/de/mm20/launcher2/search/WebsearchViewModel.kt @@ -1,6 +1,7 @@ package de.mm20.launcher2.search import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData import de.mm20.launcher2.search.data.Websearch class WebsearchViewModel( @@ -16,7 +17,6 @@ class WebsearchViewModel( websearchRepository.deleteWebsearch(websearch) } - val websearches = websearchRepository.websearches - val allWebsearches = websearchRepository.allWebsearches + val allWebsearches = websearchRepository.getWebsearches().asLiveData() } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 065f1a95..99087197 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -62,6 +62,10 @@ dependencyResolutionManagement { listOf("kotlin.stdlib", "kotlinx.coroutines.core", "kotlinx.coroutines.android") ) + alias("desugar") + .to("com.android.tools", "desugar_jdk_libs") + .version("1.1.5") + version("androidx.compose", "1.1.0-rc01") alias("androidx.compose.runtime") .to("androidx.compose.runtime", "runtime") diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 0a3606e4..38cc9d1b 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -24,6 +24,8 @@ android { } } compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } @@ -48,6 +50,8 @@ android { } dependencies { + coreLibraryDesugaring(libs.desugar) + implementation(libs.bundles.kotlin) implementation(libs.androidx.compose.runtime) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/DefaultSwipeActions.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/DefaultSwipeActions.kt index 15f5c780..7f6ddb8f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/component/DefaultSwipeActions.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/DefaultSwipeActions.kt @@ -16,8 +16,8 @@ import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale @@ -26,11 +26,11 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import de.mm20.launcher2.favorites.FavoritesViewModel +import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.theme.divider -import org.koin.androidx.compose.getViewModel +import org.koin.androidx.compose.inject import kotlin.math.roundToInt @OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class) @@ -41,19 +41,19 @@ fun DefaultSwipeActions( enabled: Boolean = true, content: @Composable RowScope.() -> Unit ) { - val viewModel: FavoritesViewModel = getViewModel() + val repository: FavoritesRepository by inject() - val isPinned by viewModel.isPinned(item).observeAsState() - val isHidden by viewModel.isHidden(item).observeAsState() + val isPinned by repository.isPinned(item).collectAsState(false) + val isHidden by repository.isHidden(item).collectAsState(false) val state = androidx.compose.material.rememberSwipeableState( SwipeAction.Default, confirmStateChange = { if (it == SwipeAction.Favorites) { if (isPinned == true) { - viewModel.unpinItem(item) + repository.unpinItem(item) } else { - viewModel.pinItem(item) + repository.pinItem(item) } } false diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt index f49eb3c3..982f5b35 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt @@ -31,11 +31,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState -import de.mm20.launcher2.search.SearchViewModel import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.locals.LocalNavController import de.mm20.launcher2.ui.locals.LocalWindowSize import org.koin.androidx.compose.getViewModel +import org.koin.androidx.compose.viewModel /** * Search bar @@ -54,7 +55,7 @@ fun SearchBar( ) { var searchQuery by remember { mutableStateOf("") } - val viewModel: SearchViewModel = getViewModel() + val viewModel: SearchViewModel by viewModel() LaunchedEffect(searchQuery) { viewModel.search(searchQuery) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt index dc9003ba..58e189bb 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.integerResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import de.mm20.launcher2.favorites.FavoritesViewModel +import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.ui.R import org.koin.androidx.compose.getViewModel +import org.koin.androidx.compose.inject import kotlin.math.min @Composable @@ -165,8 +165,8 @@ data class ToggleToolbarAction( @Composable fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction { - val viewModel: FavoritesViewModel = getViewModel() - val isPinned by viewModel.isPinned(item).observeAsState(false) + val viewModel: FavoritesRepository by inject() + val isPinned by viewModel.isPinned(item).collectAsState(false) return ToggleToolbarAction( label = stringResource( @@ -186,8 +186,8 @@ fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction { @Composable fun hideToolbarAction(item: Searchable): ToggleToolbarAction { - val viewModel: FavoritesViewModel = getViewModel() - val isHidden by viewModel.isHidden(item).observeAsState(false) + val viewModel: FavoritesRepository by inject() + val isHidden by viewModel.isHidden(item).collectAsState(false) return ToggleToolbarAction( label = stringResource( diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesVM.kt new file mode 100644 index 00000000..1d15a43d --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesVM.kt @@ -0,0 +1,23 @@ +package de.mm20.launcher2.ui.launcher.modals + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.liveData +import de.mm20.launcher2.favorites.FavoritesItem +import de.mm20.launcher2.favorites.FavoritesRepository +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class EditFavoritesVM : ViewModel(), KoinComponent { + + private val repository: FavoritesRepository by inject() + + suspend fun getFavorites(): List { + return repository.getAllFavoriteItems() + } + + fun saveFavorites(favorites: List) { + repository.saveFavorites(favorites) + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/EditFavoritesView.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesView.kt similarity index 85% rename from ui/src/main/java/de/mm20/launcher2/ui/legacy/component/EditFavoritesView.kt rename to ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesView.kt index 88d1a03f..a0790ba0 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/EditFavoritesView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/EditFavoritesView.kt @@ -1,37 +1,28 @@ -package de.mm20.launcher2.ui.legacy.component +package de.mm20.launcher2.ui.launcher.modals import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout -import android.widget.TextView +import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.AppCompatTextView -import androidx.core.content.ContextCompat -import androidx.core.view.updateMargins -import androidx.core.widget.TextViewCompat -import com.google.android.material.textview.MaterialTextView import de.mm20.launcher2.favorites.FavoritesItem -import de.mm20.launcher2.favorites.FavoritesViewModel -import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.ktx.lifecycleScope -import de.mm20.launcher2.ktx.setPadding import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.databinding.DialogEditFavoritesBinding -import de.mm20.launcher2.ui.databinding.EditFavoritesRowBinding import de.mm20.launcher2.ui.databinding.EditFavoritesTitleBinding +import de.mm20.launcher2.ui.legacy.component.EditFavoritesRow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.koin.androidx.viewmodel.ext.android.viewModel class EditFavoritesView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null + context: Context, attrs: AttributeSet? = null ) : FrameLayout(context, attrs) { - val viewModel : FavoritesViewModel by (context as AppCompatActivity).viewModel() + val viewModel: EditFavoritesVM by (context as AppCompatActivity).viewModels() private val binding = DialogEditFavoritesBinding.inflate(LayoutInflater.from(context), this) @@ -45,7 +36,7 @@ class EditFavoritesView @JvmOverloads constructor( suspend fun initView() { favorites = withContext(Dispatchers.IO) { - viewModel.getAllFavoriteItems().toMutableList() + viewModel.getFavorites().toMutableList() } binding.progressBar.visibility = View.GONE binding.itemList.addView(getLabel(R.string.edit_favorites_dialog_stage0)) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/HiddenItemsVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/HiddenItemsVM.kt new file mode 100644 index 00000000..e0e05742 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/HiddenItemsVM.kt @@ -0,0 +1,25 @@ +package de.mm20.launcher2.ui.launcher.modals + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import de.mm20.launcher2.favorites.FavoritesRepository +import de.mm20.launcher2.search.data.Searchable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class HiddenItemsVM: ViewModel(), KoinComponent { + private val repository: FavoritesRepository by inject() + + val hiddenItems = MutableLiveData>(emptyList()) + + suspend fun onActive() { + withContext(Dispatchers.IO) { + repository.getHiddenItems().collectLatest { + hiddenItems.postValue(it) + } + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/HiddenItemsView.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/HiddenItemsView.kt new file mode 100644 index 00000000..d08057e3 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/modals/HiddenItemsView.kt @@ -0,0 +1,57 @@ +package de.mm20.launcher2.ui.launcher.modals + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.setMargins +import androidx.core.widget.NestedScrollView +import de.mm20.launcher2.ktx.dp +import de.mm20.launcher2.ktx.lifecycleScope +import de.mm20.launcher2.ui.legacy.search.SearchGridView +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class HiddenItemsView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : NestedScrollView(context, attrs) { + private val viewModel: HiddenItemsVM by (context as AppCompatActivity).viewModels() + + init { + clipChildren = false + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + val hiddenItemsGrid = SearchGridView(context) + hiddenItemsGrid.layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins((8 * dp).toInt()) + } + val hiddenItems = viewModel.hiddenItems + hiddenItems.observe(context as AppCompatActivity) { + hiddenItemsGrid.submitItems(it) + } + addView(hiddenItemsGrid) + } + + private var job: Job? = null + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + val job = Job() + this.job = job + lifecycleScope.launch(job) { + viewModel.onActive() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + job?.cancel() + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchViewModel.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchViewModel.kt index 20db0b7d..af735da5 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchViewModel.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchViewModel.kt @@ -1,17 +1,116 @@ package de.mm20.launcher2.ui.launcher.search -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import de.mm20.launcher2.search.data.File +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.applications.AppRepository +import de.mm20.launcher2.calculator.CalculatorRepository +import de.mm20.launcher2.calendar.CalendarRepository +import de.mm20.launcher2.contacts.ContactRepository +import de.mm20.launcher2.favorites.FavoritesRepository +import de.mm20.launcher2.files.FileRepository +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.search.WebsearchRepository +import de.mm20.launcher2.search.data.* +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.collectLatest import org.koin.core.component.KoinComponent +import org.koin.core.component.inject -class SearchViewModel: ViewModel(), KoinComponent { +class SearchViewModel : ViewModel(), KoinComponent { - fun search(query: String) { + private val favoritesRepository: FavoritesRepository by inject() + private val calendarRepository: CalendarRepository by inject() + private val contactRepository: ContactRepository by inject() + private val appRepository: AppRepository 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() + + val isSearching = MutableLiveData(false) + + val favorites by lazy { + favoritesRepository.getFavorites().asLiveData() + } + + val appResults = MutableLiveData>(emptyList()) + 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 websearchResults = MutableLiveData>(emptyList()) + + val hideFavorites = MutableLiveData(false) + + var searchJob: Job? = null + fun search(query: String) { + try { + searchJob?.cancel() + } catch (e: CancellationException) { + } + hideFavorites.postValue(query.isNotEmpty()) + searchJob = viewModelScope.launch { + isSearching.postValue(true) + val jobs = mutableListOf>() + jobs += async { + appRepository.search(query).collectLatest { + appResults.postValue(it) + } + } + jobs += async { + contactRepository.search(query).collectLatest { + contactResults.postValue(it) + } + } + jobs += async { + calendarRepository.search(query).collectLatest { + calendarResults.postValue(it) + } + } + jobs += async { + wikipediaRepository.search(query).collectLatest { + wikipediaResult.postValue(it) + } + } + jobs += async { + unitConverterRepository.search(query).collectLatest { + unitConverterResult.postValue(it) + } + } + jobs += async { + calculatorRepository.search(query).collectLatest { + calculatorResult.postValue(it) + } + } + jobs += async { + websiteRepository.search(query).collectLatest { + websiteResult.postValue(it) + } + } + jobs += async { + fileRepository.search(query).collectLatest { + fileResults.postValue(it) + } + } + jobs += async { + websearchRepository.search(query).collectLatest { + websearchResults.postValue(it) + } + } + jobs.map { it.await() } + isSearching.postValue(false) + } } - private val _files = MutableLiveData>() - val files: LiveData> = _files } \ 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 new file mode 100644 index 00000000..eb75825a --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt @@ -0,0 +1,138 @@ +package de.mm20.launcher2.ui.launcher.widgets.calendar + +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.provider.CalendarContract +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import de.mm20.launcher2.calendar.CalendarRepository +import de.mm20.launcher2.favorites.FavoritesRepository +import de.mm20.launcher2.ktx.tryStartActivity +import de.mm20.launcher2.search.data.CalendarEvent +import kotlinx.coroutines.flow.collectLatest +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.lang.Integer.min +import java.time.* +import java.util.* +import kotlin.math.max + +class CalendarWidgetVM : ViewModel(), KoinComponent { + + private val calendarRepository: CalendarRepository by inject() + private val favoritesRepository: FavoritesRepository by inject() + + val calendarEvents = MutableLiveData>(emptyList()) + val pinnedCalendarEvents = + favoritesRepository.getPinnedCalendarEvents().asLiveData(viewModelScope.coroutineContext) + var availableDates = listOf(LocalDate.now()) + + val permissionMissing = MutableLiveData(false) + + private var showRunningPastDayEvents = false + val hiddenPastEvents = MutableLiveData(0) + + val selectedDate = MutableLiveData(LocalDate.now()) + + private var upcomingEvents: List = emptyList() + set(value) { + field = value + val dates = value.flatMap { + val startDate = + Instant.ofEpochMilli(it.startTime).atZone(ZoneId.systemDefault()).toLocalDate() + val endDate = + Instant.ofEpochMilli(it.startTime).atZone(ZoneId.systemDefault()).toLocalDate() + return@flatMap listOf( + startDate, + endDate + ) + }.union(listOf(LocalDate.now())) + .distinct() + .sorted() + availableDates = dates + val date = selectedDate.value?.takeIf { dates.contains(it) } ?: LocalDate.now() + selectDate(date) + } + + + fun nextDay() { + val dates = availableDates + val date = selectedDate.value ?: return + val currentIndex = dates.indexOf(date) + val index = min(currentIndex + 1, dates.lastIndex) + selectDate(dates[index]) + } + + fun previousDay() { + val dates = availableDates + val date = selectedDate.value ?: return + val currentIndex = dates.indexOf(date) + val index = max(currentIndex - 1, 0) + selectDate(dates[index]) + } + + fun selectDate(date: LocalDate) { + val dates = availableDates + showRunningPastDayEvents = false + if (dates.contains(date)) { + selectedDate.value = date + updateEvents() + } + } + + fun showAllEvents() { + showRunningPastDayEvents = true + updateEvents() + } + + fun createEvent(context: Context) { + val intent = Intent(Intent.ACTION_EDIT) + intent.data = CalendarContract.Events.CONTENT_URI + context.tryStartActivity(intent) + } + + fun openCalendarApp(context: Context) { + val startMillis = System.currentTimeMillis() + val builder = CalendarContract.CONTENT_URI.buildUpon() + builder.appendPath("time") + ContentUris.appendId(builder, startMillis) + val intent = Intent(Intent.ACTION_VIEW) + .setData(builder.build()) + context.tryStartActivity(intent) + } + + private fun updateEvents() { + val date = selectedDate.value ?: return + val now = System.currentTimeMillis() + val offset = OffsetDateTime.now().offset + val dayStart = max(now, date.atStartOfDay().toEpochSecond(offset) * 1000) + val dayEnd = date.plusDays(1).atStartOfDay().toEpochSecond(offset) * 1000 + var events = upcomingEvents.filter { + it.endTime >= dayStart && it.startTime < dayEnd + } + + if (!showRunningPastDayEvents) { + val totalCount = events.size + + events = events.filter { + it.startTime >= date.atStartOfDay().toEpochSecond(offset) * 1000 + } + + val hiddenCount = totalCount - events.size + hiddenPastEvents.postValue(hiddenCount) + } else { + hiddenPastEvents.postValue(0) + } + + calendarEvents.postValue(events) + } + + suspend fun onActive() { + calendarRepository.getUpcomingEvents().collectLatest { + upcomingEvents = it + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/activity/LauncherActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/activity/LauncherActivity.kt index da1b0aea..a9d588d5 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/activity/LauncherActivity.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/activity/LauncherActivity.kt @@ -14,7 +14,6 @@ import android.content.Context import android.content.Intent import android.graphics.Color import android.graphics.Point -import android.os.Build import android.os.Bundle import android.provider.Settings import android.util.Log @@ -23,7 +22,6 @@ import android.view.* import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.inputmethod.InputMethodManager -import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.Toast import androidx.activity.viewModels @@ -44,7 +42,6 @@ import com.afollestad.materialdialogs.callbacks.onDismiss import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.list.listItems import com.jmedeisis.draglinearlayout.DragLinearLayout -import de.mm20.launcher2.favorites.FavoritesViewModel import de.mm20.launcher2.icons.DynamicIconController import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.ktx.dp @@ -53,15 +50,15 @@ import de.mm20.launcher2.ktx.isBrightColor import de.mm20.launcher2.legacy.helper.ActivityStarter import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherPreferences -import de.mm20.launcher2.search.SearchViewModel import de.mm20.launcher2.transition.ChangingLayoutTransition import de.mm20.launcher2.transition.OneShotLayoutTransition import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding -import de.mm20.launcher2.ui.legacy.component.EditFavoritesView +import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView +import de.mm20.launcher2.ui.launcher.modals.HiddenItemsView +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.legacy.component.WidgetView import de.mm20.launcher2.ui.legacy.helper.ThemeHelper -import de.mm20.launcher2.ui.legacy.search.SearchGridView import de.mm20.launcher2.weather.WeatherViewModel import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.WidgetType @@ -79,37 +76,37 @@ class LauncherActivity : AppCompatActivity() { * True if the search result list is visible */ private var searchVisibility = false - set(value) { - field = value - windowBackgroundBlur = value - } + set(value) { + field = value + windowBackgroundBlur = value + } private lateinit var widgetHost: AppWidgetHost private val widgets = mutableListOf() private lateinit var overlayView: ViewGroupOverlay - private val searchViewModel: SearchViewModel by viewModel() private val widgetViewModel: WidgetViewModel by viewModel() - private val favoritesViewModel: FavoritesViewModel by viewModel() + + private val searchViewModel: SearchViewModel by viewModels() private val preferences = LauncherPreferences.instance private var windowBackgroundBlur: Boolean = false - set(value) { - if(field == value) return - field = value - if (!isAtLeastApiLevel(31)) return - window.attributes = window.attributes.also { - if (value) { - it.blurBehindRadius = (32 * dp).toInt() - it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND - } else { - it.blurBehindRadius = 0 - it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv() + set(value) { + if (field == value) return + field = value + if (!isAtLeastApiLevel(31)) return + window.attributes = window.attributes.also { + if (value) { + it.blurBehindRadius = (32 * dp).toInt() + it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND + } else { + it.blurBehindRadius = 0 + it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv() + } } } - } private var widgetEditMode = false set(value) { @@ -318,28 +315,11 @@ class LauncherActivity : AppCompatActivity() { ) } R.id.menu_item_hidden -> { - val layout = NestedScrollView(this) - layout.clipChildren = false - layout.layoutParams = ViewGroup.LayoutParams( - MATCH_PARENT, - WRAP_CONTENT - ) - val hiddenItemsGrid = SearchGridView(this) - hiddenItemsGrid.layoutParams = FrameLayout.LayoutParams( - MATCH_PARENT, - WRAP_CONTENT - ).apply { - setMargins((8 * dp).toInt()) - } - val hiddenItems = favoritesViewModel.hiddenItems - hiddenItems.observe(this) { - hiddenItemsGrid.submitItems(it) - } - layout.addView(hiddenItemsGrid) + val view = HiddenItemsView(this) MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT)) .show { title(R.string.menu_hidden_items) - customView(view = layout) + customView(view = view) negativeButton(R.string.close) { dismiss() } } //hiddenAppsActivated = true @@ -584,8 +564,6 @@ class LauncherActivity : AppCompatActivity() { ActivityStarter.create(binding.rootView) binding.activityStartOverlay.visibility = View.INVISIBLE - val widgetViewModel by viewModels() - widgetViewModel.requestCalendarUpdate() search(binding.searchBar.getSearchQuery()) updateSystemBarAppearance() @@ -670,12 +648,8 @@ class LauncherActivity : AppCompatActivity() { PermissionsManager.LOCATION -> { ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this) } - PermissionsManager.CALENDAR -> { - widgetViewModel.requestCalendarUpdate() - } PermissionsManager.ALL -> { ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this) - widgetViewModel.requestCalendarUpdate() search(binding.searchBar.getSearchQuery()) } } @@ -760,7 +734,8 @@ class LauncherActivity : AppCompatActivity() { binding.container.translationY = binding.searchBar.height.toFloat() } - windowBackgroundBlur = searchVisibility || newTransY > 0.6 * binding.searchBar.height + windowBackgroundBlur = + searchVisibility || newTransY > 0.6 * binding.searchBar.height if (binding.container.translationY == 0f) return@onTouch false } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/ApplicationView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/ApplicationView.kt index 9fa0a6c3..8a5cc070 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/ApplicationView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/ApplicationView.kt @@ -6,12 +6,12 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.* -import de.mm20.launcher2.applications.AppViewModel import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.ui.databinding.ViewApplicationBinding -import org.koin.androidx.viewmodel.ext.android.viewModel +import de.mm20.launcher2.ui.launcher.search.SearchViewModel class ApplicationView : FrameLayout { @@ -27,8 +27,8 @@ class ApplicationView : FrameLayout { layoutTransition = LayoutTransition() layoutTransition.enableTransitionType(LayoutTransition.CHANGING) binding.applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) - val viewModel: AppViewModel by (context as AppCompatActivity).viewModel() - applications = viewModel.applications + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + applications = viewModel.appResults applications.observe(context as AppCompatActivity, Observer> { visibility = if (it.isEmpty()) View.GONE else View.VISIBLE binding.applicationGrid.submitItems(it) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/CalculatorView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/CalculatorView.kt index f49b3ebd..dd070364 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/CalculatorView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/CalculatorView.kt @@ -5,6 +5,7 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column import androidx.compose.material3.LocalContentColor @@ -14,29 +15,29 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.calculator.CalculatorViewModel import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.ui.LegacyLauncherTheme import de.mm20.launcher2.ui.databinding.ViewCalculatorBinding +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.search.CalculatorItem -import de.mm20.launcher2.ui.search.UnitConverterItem -import org.koin.androidx.viewmodel.ext.android.viewModel -import kotlin.math.round class CalculatorView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( + context, + attrs, + defStyleRes + ) private val calculator: LiveData private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true) init { - val viewModel: CalculatorViewModel by (context as AppCompatActivity).viewModel() - calculator = viewModel.calculator + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + calculator = viewModel.calculatorResult calculator.observe(context as AppCompatActivity, Observer { if (it == null) visibility = View.GONE else { diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/CalendarView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/CalendarView.kt index a8a089d1..f352885f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/CalendarView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/CalendarView.kt @@ -6,23 +6,26 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModelProvider -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.calendar.CalendarViewModel import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.MissingPermission +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.legacy.search.SearchListView -import org.koin.androidx.viewmodel.ext.android.viewModel class CalendarView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( + context, + attrs, + defStyleRes + ) private val calendarEvents: LiveData?> @@ -33,21 +36,27 @@ class CalendarView : FrameLayout { val card = findViewById(R.id.card) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) val list = findViewById(R.id.list) - val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel() - calendarEvents = viewModel.calendarEvents + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + calendarEvents = viewModel.calendarResults calendarEvents.observe(context as AppCompatActivity, { if (it == null) { visibility = View.GONE return@observe } - if (it.isEmpty() && LauncherPreferences.instance.searchCalendars && !PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) { + if (it.isEmpty() && LauncherPreferences.instance.searchCalendars && !PermissionsManager.checkPermission( + context, + PermissionsManager.CALENDAR + ) + ) { visibility = View.VISIBLE - list.submitItems(listOf( + list.submitItems( + listOf( MissingPermission( - context.getString(R.string.permission_calendar_search), - PermissionsManager.CALENDAR + context.getString(R.string.permission_calendar_search), + PermissionsManager.CALENDAR ) - )) + ) + ) return@observe } visibility = if (it.isEmpty()) View.GONE else View.VISIBLE diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/ContactView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/ContactView.kt index c4f2b91d..0eeb91f7 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/ContactView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/ContactView.kt @@ -6,24 +6,28 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import android.widget.SearchView +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModelProvider -import de.mm20.launcher2.contacts.ContactViewModel import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.search.data.Contact import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.legacy.search.SearchListView -import org.koin.androidx.viewmodel.ext.android.viewModel class ContactView : FrameLayout { private val contacts: LiveData?> constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( + context, + attrs, + defStyleRes + ) init { View.inflate(context, R.layout.view_search_category_list, this) @@ -31,22 +35,28 @@ class ContactView : FrameLayout { layoutTransition.enableTransitionType(LayoutTransition.CHANGING) val card = findViewById(R.id.card) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) - val viewModel: ContactViewModel by (context as AppCompatActivity).viewModel() - contacts = viewModel.contacts + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + contacts = viewModel.contactResults val list = findViewById(R.id.list) contacts.observe(context as AppCompatActivity, { if (it == null) { visibility = View.GONE return@observe } - if (it.isEmpty() && LauncherPreferences.instance.searchContacts && !PermissionsManager.checkPermission(context, PermissionsManager.CONTACTS)) { + if (it.isEmpty() && LauncherPreferences.instance.searchContacts && !PermissionsManager.checkPermission( + context, + PermissionsManager.CONTACTS + ) + ) { visibility = View.VISIBLE - list.submitItems(listOf( + list.submitItems( + listOf( MissingPermission( - context.getString(R.string.permission_contact_search), - PermissionsManager.CONTACTS + context.getString(R.string.permission_contact_search), + PermissionsManager.CONTACTS ) - )) + ) + ) return@observe } visibility = if (it.isEmpty()) View.GONE else View.VISIBLE diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FavoritesView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FavoritesView.kt index a0da3040..6aa5fa39 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FavoritesView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FavoritesView.kt @@ -6,14 +6,10 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.* -import de.mm20.launcher2.favorites.FavoritesViewModel -import de.mm20.launcher2.preferences.LauncherPreferences -import de.mm20.launcher2.search.data.Searchable -import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.databinding.ViewFavoritesBinding -import org.koin.androidx.viewmodel.ext.android.viewModel +import de.mm20.launcher2.ui.launcher.search.SearchViewModel class FavoritesView : FrameLayout { @@ -21,18 +17,21 @@ class FavoritesView : FrameLayout { constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) - private val favorites: LiveData> - private val binding = ViewFavoritesBinding.inflate(LayoutInflater.from(context), this, true) init { - val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() - favorites = viewModel.getFavorites(LauncherPreferences.instance.gridColumnCount) - favorites.observe(context as AppCompatActivity, Observer { - visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + val favorites = viewModel.favorites + val hide = viewModel.hideFavorites + favorites.observe(context as AppCompatActivity) { + visibility = if (it?.isEmpty() == true || hide.value == true) View.GONE else View.VISIBLE binding.favoritesGrid.submitItems(it) - }) + } + + hide.observe(context as AppCompatActivity) { + visibility = if(it == true) View.GONE else View.VISIBLE + } layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) } binding.favoritesCard.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FileView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FileView.kt index 72b70182..da8a76e2 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FileView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/FileView.kt @@ -6,22 +6,26 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData -import de.mm20.launcher2.files.FilesViewModel import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.legacy.search.SearchListView -import org.koin.androidx.viewmodel.ext.android.viewModel class FileView : FrameLayout { private val files: LiveData?> constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( + context, + attrs, + defStyleRes + ) init { View.inflate(context, R.layout.view_search_category_list, this) @@ -30,21 +34,27 @@ class FileView : FrameLayout { val card = findViewById(R.id.card) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) val list = findViewById(R.id.list) - val viewModel: FilesViewModel by (context as AppCompatActivity).viewModel() - files = viewModel.files + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + files = viewModel.fileResults files.observe(context as AppCompatActivity, { if (it == null) { visibility = View.GONE return@observe } - if (it.isEmpty() && !PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) { + if (it.isEmpty() && !PermissionsManager.checkPermission( + context, + PermissionsManager.EXTERNAL_STORAGE + ) + ) { visibility = View.VISIBLE - list.submitItems(listOf( + list.submitItems( + listOf( MissingPermission( - context.getString(R.string.permission_files_search), - PermissionsManager.EXTERNAL_STORAGE + context.getString(R.string.permission_files_search), + PermissionsManager.EXTERNAL_STORAGE ) - )) + ) + ) return@observe } visibility = if (it.isEmpty()) View.GONE else View.VISIBLE diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/SearchBar.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/SearchBar.kt index 839fca63..bfba700a 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/SearchBar.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/SearchBar.kt @@ -12,21 +12,17 @@ import android.view.LayoutInflater import android.view.View import android.view.animation.AccelerateInterpolator import android.view.animation.DecelerateInterpolator -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.postDelayed -import androidx.lifecycle.Observer import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieDrawable import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.SearchStyles -import de.mm20.launcher2.search.SearchViewModel import de.mm20.launcher2.transition.ChangingLayoutTransition import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.databinding.ViewSearchBarBinding import de.mm20.launcher2.ui.legacy.view.LauncherCardView -import org.koin.androidx.viewmodel.ext.android.viewModel class SearchBar @JvmOverloads constructor( context: Context, @@ -72,12 +68,6 @@ class SearchBar @JvmOverloads constructor( }) - val viewModel = (context as AppCompatActivity).viewModel().value - - viewModel.isSearching.observe(context, Observer { - binding.searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE - }) - binding.overflowMenu.setOnClickListener { if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it) else (setSearchQuery("")) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/UnitConverterView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/UnitConverterView.kt index 21b169ae..b677043f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/UnitConverterView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/UnitConverterView.kt @@ -5,6 +5,7 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column import androidx.compose.material3.LocalContentColor @@ -17,9 +18,8 @@ import androidx.lifecycle.Observer import de.mm20.launcher2.search.data.UnitConverter import de.mm20.launcher2.ui.LegacyLauncherTheme import de.mm20.launcher2.ui.databinding.ViewUnitconverterBinding +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.search.UnitConverterItem -import de.mm20.launcher2.unitconverter.UnitConverterViewModel -import org.koin.androidx.viewmodel.ext.android.viewModel class UnitConverterView : FrameLayout { @@ -36,8 +36,8 @@ class UnitConverterView : FrameLayout { private val binding = ViewUnitconverterBinding.inflate(LayoutInflater.from(context), this, true) init { - val unitConverterViewModel by (context as AppCompatActivity).viewModel() - unitConverter = unitConverterViewModel.unitConverter + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + unitConverter = viewModel.unitConverterResult unitConverter.observe(context as AppCompatActivity, Observer { if (it == null) visibility = View.GONE else { diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WebSearchView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WebSearchView.kt index de7ea3f4..9a6c7546 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WebSearchView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WebSearchView.kt @@ -7,6 +7,7 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData @@ -17,24 +18,27 @@ import com.bumptech.glide.request.transition.Transition import com.google.android.material.chip.Chip import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.legacy.helper.ActivityStarter -import de.mm20.launcher2.search.WebsearchViewModel import de.mm20.launcher2.search.data.Websearch import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.databinding.ViewWebsearchBinding -import org.koin.androidx.viewmodel.ext.android.viewModel +import de.mm20.launcher2.ui.launcher.search.SearchViewModel class WebSearchView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( + context, + attrs, + defStyleRes + ) private val websearches: LiveData> private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true) init { - val viewModel: WebsearchViewModel by (context as AppCompatActivity).viewModel() - websearches = viewModel.websearches + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + websearches = viewModel.websearchResults websearches.observe(context as AppCompatActivity, Observer { updateWebsearches(it) }) @@ -48,13 +52,16 @@ class WebSearchView : FrameLayout { chip.text = search.label if (search.icon != null) { Glide.with(context) - .load(search.icon) - .into(object : SimpleTarget() { - override fun onResourceReady(resource: Drawable, transition: Transition?) { - chip.chipIcon = resource - } + .load(search.icon) + .into(object : SimpleTarget() { + override fun onResourceReady( + resource: Drawable, + transition: Transition? + ) { + chip.chipIcon = resource + } - }) + }) chip.chipIconTint = null } else { chip.chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_search) @@ -63,7 +70,8 @@ class WebSearchView : FrameLayout { } chip.chipStrokeWidth = 1 * dp chip.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke) - chip.chipBackgroundColor = ContextCompat.getColorStateList(context, R.color.chip_background) + chip.chipBackgroundColor = + ContextCompat.getColorStateList(context, R.color.chip_background) chip.setTextAppearanceResource(R.style.ChipTextAppearance) chip.setOnClickListener { ActivityStarter.start(context, chip, intent = search.getLaunchIntent()) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WebsiteView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WebsiteView.kt index 218b8b1f..c52fa541 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WebsiteView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WebsiteView.kt @@ -5,34 +5,38 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider import de.mm20.launcher2.legacy.helper.ActivityStarter import de.mm20.launcher2.search.data.Website import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.legacy.searchable.SearchableView -import de.mm20.launcher2.websites.WebsiteViewModel -import org.koin.androidx.viewmodel.ext.android.viewModel class WebsiteView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( + context, + attrs, + defStyleRes + ) private val website: LiveData init { View.inflate(context, R.layout.view_search_category_single_item, this) val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST) - val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + val params = + ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) val card = findViewById(R.id.card) websiteView.layoutParams = params card.addView(websiteView) - val viewModel: WebsiteViewModel by (context as AppCompatActivity).viewModel() - website = viewModel.website + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + website = viewModel.websiteResult website.observe(context as AppCompatActivity, Observer { visibility = if (it == null) View.GONE else View.VISIBLE card.setOnClickListener { _ -> diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WikipediaView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WikipediaView.kt index c99bd1fd..519111ce 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WikipediaView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WikipediaView.kt @@ -5,35 +5,38 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider import de.mm20.launcher2.legacy.helper.ActivityStarter import de.mm20.launcher2.search.data.Wikipedia import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.launcher.search.SearchViewModel import de.mm20.launcher2.ui.legacy.searchable.SearchableView -import de.mm20.launcher2.wikipedia.WikipediaViewModel -import org.koin.androidx.viewmodel.ext.android.viewModel class WikipediaView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( + context, + attrs, + defStyleRes + ) val wikipedia: LiveData init { View.inflate(context, R.layout.view_search_category_single_item, this) val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST) - val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + val params = + ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) val card = findViewById(R.id.card) websiteView.layoutParams = params card.addView(websiteView) - val viewModel: WikipediaViewModel by (context as AppCompatActivity).viewModel() - wikipedia = viewModel.wikipedia - wikipedia.observe(context as AppCompatActivity, Observer { + val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels() + wikipedia = viewModel.wikipediaResult + wikipedia.observe(context as AppCompatActivity, { visibility = if (it == null) View.GONE else View.VISIBLE card.setOnClickListener { _ -> ActivityStarter.start(context, websiteView, item = it) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/helper/ActivityStarter.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/helper/ActivityStarter.kt index 4da59941..527730e6 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/helper/ActivityStarter.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/helper/ActivityStarter.kt @@ -22,7 +22,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.lang.ref.WeakReference -object ActivityStarter: KoinComponent { +object ActivityStarter : KoinComponent { val favoritesRepository: FavoritesRepository by inject() @@ -46,13 +46,20 @@ object ActivityStarter: KoinComponent { initialized = true } - fun start(context: Context, transitionView: View, item: Searchable? = null, intent: Intent? = null, pendingIntent: PendingIntent? = null): Boolean { + fun start( + context: Context, + transitionView: View, + item: Searchable? = null, + intent: Intent? = null, + pendingIntent: PendingIntent? = null + ): Boolean { if (!initialized) throw IllegalStateException("Item starter has not been initialized properly.") if (!startActivity(context, item, intent, pendingIntent, transitionView)) return false if (animationStyle == AppStartAnimation.SLIDE_BOTTOM || animationStyle == AppStartAnimation.FADE || - animationStyle == AppStartAnimation.M) { + animationStyle == AppStartAnimation.M + ) { return true } @@ -80,28 +87,28 @@ object ActivityStarter: KoinComponent { AnimatorSet().apply { playTogether( - ViewPropertyObjectAnimator.animate(background) - .scaleX(1f) - .scaleY(1f) - .translationX(0f) - .translationY(0f) - .setDuration(200) - .setInterpolator(AccelerateInterpolator(0.8f)) - .get(), - ViewPropertyObjectAnimator.animate(transitionView) - .scaleX(scale) - .scaleY(scale) - .alpha(0f) - .translationX(x) - .translationY(y) - .setDuration(200) - .setInterpolator(AccelerateInterpolator(0.8f)) - .get(), - ViewPropertyObjectAnimator.animate(searchView) - .scaleX(0.8f) - .scaleY(0.8f) - .alpha(0f) - .get() + ViewPropertyObjectAnimator.animate(background) + .scaleX(1f) + .scaleY(1f) + .translationX(0f) + .translationY(0f) + .setDuration(200) + .setInterpolator(AccelerateInterpolator(0.8f)) + .get(), + ViewPropertyObjectAnimator.animate(transitionView) + .scaleX(scale) + .scaleY(scale) + .alpha(0f) + .translationX(x) + .translationY(y) + .setDuration(200) + .setInterpolator(AccelerateInterpolator(0.8f)) + .get(), + ViewPropertyObjectAnimator.animate(searchView) + .scaleX(0.8f) + .scaleY(0.8f) + .alpha(0f) + .get() ) }.start() onResumeCallback = { @@ -127,10 +134,17 @@ object ActivityStarter: KoinComponent { private var onResumeCallback: (() -> Unit)? = null - private fun startActivity(context: Context, item: Searchable? = null, intent: Intent? = null, pendingIntent: PendingIntent? = null, sourceView: View): Boolean { + private fun startActivity( + context: Context, + item: Searchable? = null, + intent: Intent? = null, + pendingIntent: PendingIntent? = null, + sourceView: View + ): Boolean { val pos = intArrayOf(0, 0) sourceView.getLocationOnScreen(pos) - val sourceBounds = Rect(pos[0], pos[1], pos[0] + sourceView.width, pos[1] + sourceView.height) + val sourceBounds = + Rect(pos[0], pos[1], pos[0] + sourceView.width, pos[1] + sourceView.height) val bundle = getActivityOptions(context, sourceView, sourceBounds)?.toBundle() @@ -145,7 +159,7 @@ object ActivityStarter: KoinComponent { if (item != null) { if (item.launch(context, bundle)) { - favoritesRepository.incrementLaunchCount(item) + favoritesRepository.incrementLaunchCounter(item) return true } return false @@ -162,12 +176,36 @@ object ActivityStarter: KoinComponent { } } - private fun getActivityOptions(context: Context, sourceView: View, sourceBounds: Rect?): ActivityOptionsCompat? { + private fun getActivityOptions( + context: Context, + sourceView: View, + sourceBounds: Rect? + ): ActivityOptionsCompat? { return when (animationStyle) { - AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_fade_enter, R.anim.activity_start_fade_exit) - AppStartAnimation.SLIDE_BOTTOM -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_slide_bottom_enter, R.anim.activity_start_slide_bottom_exit) - AppStartAnimation.M -> sourceBounds?.let { ActivityOptionsCompat.makeClipRevealAnimation(sourceView, 0, 0, sourceView.width, sourceView.height) } - else -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_splash2_enter, R.anim.activity_start_splash2_exit) + AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation( + context, + R.anim.activity_start_fade_enter, + R.anim.activity_start_fade_exit + ) + AppStartAnimation.SLIDE_BOTTOM -> ActivityOptionsCompat.makeCustomAnimation( + context, + R.anim.activity_start_slide_bottom_enter, + R.anim.activity_start_slide_bottom_exit + ) + AppStartAnimation.M -> sourceBounds?.let { + ActivityOptionsCompat.makeClipRevealAnimation( + sourceView, + 0, + 0, + sourceView.width, + sourceView.height + ) + } + else -> ActivityOptionsCompat.makeCustomAnimation( + context, + R.anim.activity_start_splash2_enter, + R.anim.activity_start_splash2_exit + ) } } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/ApplicationDetailRepresentation.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/ApplicationDetailRepresentation.kt index b7d89e71..dc8c87ea 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/ApplicationDetailRepresentation.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/search/ApplicationDetailRepresentation.kt @@ -28,14 +28,13 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.getSystemService import androidx.core.graphics.alpha -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.Observer +import androidx.lifecycle.* import androidx.transition.Scene import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import de.mm20.launcher2.badges.BadgeProvider import de.mm20.launcher2.crashreporter.CrashReporter -import de.mm20.launcher2.favorites.FavoritesViewModel +import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.ktx.castToOrNull import de.mm20.launcher2.ktx.dp @@ -51,10 +50,8 @@ import de.mm20.launcher2.transition.ChangingLayoutTransition import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.legacy.searchable.SearchableView import de.mm20.launcher2.ui.legacy.view.* -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.util.concurrent.Executors @@ -83,9 +80,10 @@ class ApplicationDetailRepresentation : Representation, KoinComponent { shape = LauncherIconView.getDefaultShape(context) icon = iconRepository.getIconIfCached(application) lifecycleScope.launch { - iconRepository.getIcon(application, (84 * rootView.dp).toInt()).collectLatest { - icon = it - } + iconRepository.getIcon(application, (84 * rootView.dp).toInt()) + .collectLatest { + icon = it + } } } findViewById(R.id.appCard).also { @@ -275,7 +273,7 @@ class ApplicationDetailRepresentation : Representation, KoinComponent { if (launcherApps.hasShortcutHostPermission()) { val shortcuts = app.shortcuts - val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() + val repository: FavoritesRepository by inject() var count = 0 for (si in shortcuts) { @@ -308,7 +306,7 @@ class ApplicationDetailRepresentation : Representation, KoinComponent { R.color.text_color_primary ) ) - val isPinned = viewModel.isPinned(si) + val isPinned = repository.isPinned(si).asLiveData() isPinned.observe(context as LifecycleOwner, Observer { view.isCloseIconVisible = isPinned.value == true @@ -320,14 +318,14 @@ class ApplicationDetailRepresentation : Representation, KoinComponent { } view.setOnLongClickListener { if (isPinned.value == true) { - viewModel.unpinItem(si) + repository.unpinItem(si) } else { - viewModel.pinItem(si) + repository.pinItem(si) } true } view.setOnCloseIconClickListener { - viewModel.unpinItem(si) + repository.unpinItem(si) view.isCloseIconVisible = false } appShortcuts.addView(view) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/SwipeCardView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/SwipeCardView.kt index d518de61..4873a216 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/SwipeCardView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/SwipeCardView.kt @@ -14,15 +14,17 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.animation.doOnEnd import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asLiveData +import androidx.lifecycle.lifecycleScope import com.google.android.material.card.MaterialCardView -import de.mm20.launcher2.favorites.FavoritesViewModel +import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.transition.ChangingLayoutTransition import de.mm20.launcher2.ui.R -import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import kotlin.math.abs class SwipeCardView @JvmOverloads constructor( @@ -376,10 +378,12 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) : R.drawable.ic_star_solid, ContextCompat.getColor(context, R.color.amber), { false } - ) { - val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() + ), KoinComponent { - private val pinned = viewModel.isPinned(searchable) + private val repository: FavoritesRepository by inject() + + private val pinned = repository.isPinned(searchable) + .asLiveData((context as AppCompatActivity).lifecycleScope.coroutineContext) init { @@ -392,7 +396,7 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) : if (pinned) { icon = R.drawable.ic_star_outline action = { - viewModel.unpinItem( + repository.unpinItem( searchable ) false @@ -400,7 +404,7 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) : } else { icon = R.drawable.ic_star_solid action = { - viewModel.pinItem( + repository.pinItem( searchable ) false @@ -413,9 +417,12 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC R.drawable.ic_visibility_off, ContextCompat.getColor(context, R.color.blue), { false } -) { - val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() - private val hidden = viewModel.isHidden(searchable) +), KoinComponent { + + private val repository: FavoritesRepository by inject() + + private val hidden = repository.isPinned(searchable) + .asLiveData((context as AppCompatActivity).lifecycleScope.coroutineContext) init { hidden.observe(context as LifecycleOwner) { @@ -427,7 +434,7 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC if (hidden) { icon = R.drawable.ic_visibility action = { - viewModel.unhideItem( + repository.unhideItem( searchable ) true @@ -435,7 +442,7 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC } else { icon = R.drawable.ic_visibility_off action = { - viewModel.hideItem( + repository.hideItem( searchable ) true diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/ToolbarView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/ToolbarView.kt index 75358fea..f006c7ea 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/ToolbarView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/ToolbarView.kt @@ -6,16 +6,18 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import androidx.annotation.DrawableRes -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.TooltipCompat import androidx.core.view.setPadding -import androidx.lifecycle.Observer -import de.mm20.launcher2.favorites.FavoritesViewModel +import androidx.lifecycle.* +import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.ui.R -import org.koin.androidx.viewmodel.ext.android.viewModel +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collectLatest +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject class ToolbarView : LinearLayout { constructor(context: Context) : super(context) @@ -223,27 +225,37 @@ open class ToolbarSubaction(val title: String, var clickAction: (() -> Unit)) { class FavoriteToolbarAction(val context: Context, val item: Searchable) : ToolbarAction( R.drawable.ic_star_outline, context.getString(R.string.favorites_menu_pin) -) { +), KoinComponent { - private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() - private val isPinned = viewModel.isPinned(item) - - init { - isPinned.observe(context as AppCompatActivity, Observer { - it ?: return@Observer - if (it) { + private val repository: FavoritesRepository by inject() + private var isPinned = false + set(value) { + field = value + if (value) { title = context.getString(R.string.favorites_menu_unpin) icon = R.drawable.ic_star_solid } else { title = context.getString(R.string.favorites_menu_pin) icon = R.drawable.ic_star_outline } - }) + } + + init { clickAction = { - if (isPinned.value == true) { - viewModel.unpinItem(item) + if (isPinned) { + repository.unpinItem(item) } else { - viewModel.pinItem(item) + repository.pinItem(item) + } + } + + (context as LifecycleOwner).apply { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + repository.isPinned(item).collectLatest { + isPinned = it + } + } } } } @@ -252,27 +264,40 @@ class FavoriteToolbarAction(val context: Context, val item: Searchable) : Toolba class VisibilityToolbarAction(val context: Context, val item: Searchable) : ToolbarAction( R.drawable.ic_visibility, context.getString(R.string.menu_hide) -) { +), KoinComponent { - private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() - private val isHidden = viewModel.isHidden(item) - - init { - isHidden.observe(context as AppCompatActivity, Observer { - if (it) { + private val repository: FavoritesRepository by inject() + private var isHidden = false + set(value) { + field = value + if (value) { title = context.getString(R.string.menu_unhide) icon = R.drawable.ic_visibility } else { title = context.getString(R.string.menu_hide) icon = R.drawable.ic_visibility_off } - }) + } + + init { clickAction = { - if (isHidden.value == true) { - viewModel.unhideItem(item) + if (isHidden) { + repository.unhideItem(item) } else { - viewModel.hideItem(item) + repository.hideItem(item) + } + } + + (context as LifecycleOwner).apply { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + repository.isHidden(item).collectLatest { + isHidden = it + } + } } } } + + } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt index 1e1df372..6a3cb236 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt @@ -1,145 +1,113 @@ package de.mm20.launcher2.ui.legacy.widget import android.animation.LayoutTransition -import android.content.ContentUris import android.content.Context -import android.content.Intent -import android.provider.CalendarContract import android.text.format.DateUtils import android.util.AttributeSet import android.view.LayoutInflater import android.view.Menu import android.view.View +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu -import androidx.lifecycle.LiveData -import de.mm20.launcher2.calendar.CalendarViewModel -import de.mm20.launcher2.favorites.FavoritesViewModel -import de.mm20.launcher2.legacy.helper.ActivityStarter -import de.mm20.launcher2.permissions.PermissionsManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import de.mm20.launcher2.ktx.lifecycleOwner +import de.mm20.launcher2.ktx.lifecycleScope import de.mm20.launcher2.search.data.CalendarEvent -import de.mm20.launcher2.ui.legacy.data.InformationText -import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.databinding.ViewCalendarWidgetBinding -import org.koin.androidx.viewmodel.ext.android.viewModel -import java.util.* -import kotlin.math.max -import kotlin.math.min +import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidgetVM +import de.mm20.launcher2.ui.legacy.data.InformationText +import kotlinx.coroutines.launch +import java.time.LocalDate +import java.time.ZoneId class CalendarWidget : LauncherWidget { override val canResize: Boolean get() = false - private val calendarEvents: LiveData> - private val pinnedCalendarEvents: LiveData> - - private val zoneOffset = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis()) - private var selectedDay = 0L - set(value) { - field = value - binding.calendarDate.text = formatDay(value) - updateEventList() - } - - private var availableDays: List = listOf(0L) - set(value) { - if (value.indexOf(selectedDay) == -1) selectedDay = 0L - field = value - } - constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( + context, + attrs, + defStyleRes + ) - private fun formatDay(day: Long): String { - return when (day) { - 0L -> context.getString(R.string.date_today) - 1L -> context.getString(R.string.date_tomorrow) - else -> DateUtils.formatDateTime(context, (getToday() + day) * (1000 * 60 * 60 * 24), DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY) + private fun formatDay(day: LocalDate): String { + val today = LocalDate.now() + return when { + today == day -> context.getString(R.string.date_today) + today.plusDays(1) == day -> context.getString(R.string.date_tomorrow) + else -> DateUtils.formatDateTime( + context, + day.atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY + ) } } - private val binding = ViewCalendarWidgetBinding.inflate(LayoutInflater.from(context), this, true) + private val binding = + ViewCalendarWidgetBinding.inflate(LayoutInflater.from(context), this, true) + private val viewModel: CalendarWidgetVM by (context as AppCompatActivity).viewModels() init { clipToPadding = false clipChildren = false + + lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.onActive() + } + } + binding.calendarNewEvent.setOnClickListener { - val intent = Intent(Intent.ACTION_EDIT) - intent.data = CalendarContract.Events.CONTENT_URI - ActivityStarter.start(context, this, intent = intent) + viewModel.createEvent(context) } binding.calendarDate.setOnClickListener { val menu = PopupMenu(context, binding.calendarDate) - for (d in availableDays) { + val availableDates = viewModel.availableDates + for ((i, d) in availableDates.withIndex()) { menu.menu.add( - Menu.NONE, - d.toInt(), - Menu.NONE, - formatDay(d) + Menu.NONE, + i, + Menu.NONE, + formatDay(d) ) } menu.setOnMenuItemClickListener { - selectedDay = it.itemId.toLong() + viewModel.selectDate(availableDates[it.itemId]) true } menu.show() } binding.calendarOpenApp.setOnClickListener { - val startMillis = System.currentTimeMillis() - val builder = CalendarContract.CONTENT_URI.buildUpon() - builder.appendPath("time") - ContentUris.appendId(builder, startMillis) - val intent = Intent(Intent.ACTION_VIEW) - .setData(builder.build()) - ActivityStarter.start(context, binding.calendarWidgetRoot, intent = intent) + viewModel.openCalendarApp(context) } binding.calendarDateNext.setOnClickListener { - val i = min(availableDays.lastIndex - 1, availableDays.indexOf(selectedDay)) - selectedDay = availableDays[i + 1] + viewModel.nextDay() } binding.calendarDatePrev.setOnClickListener { - val i = max(1, availableDays.indexOf(selectedDay)) - selectedDay = availableDays[i - 1] + viewModel.previousDay() } - val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel() - val favViewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() - calendarEvents = viewModel.upcomingCalendarEvents - pinnedCalendarEvents = favViewModel.pinnedCalendarEvents + val calendarEvents = viewModel.calendarEvents + val pinnedCalendarEvents = viewModel.pinnedCalendarEvents + val hiddenPastEvents = viewModel.hiddenPastEvents + val selectedDate = viewModel.selectedDate calendarEvents.observe(context as AppCompatActivity, { - if (!PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) { - binding.calendarWidgetList.submitItems(listOf( - MissingPermission( - context.getString(R.string.permission_calendar_widget), - PermissionsManager.CALENDAR - ) - )) - return@observe - } - val today = getToday() - availableDays = it - .map { ((it.startTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today } - .union(it.map { ((it.endTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today }) - .union(listOf(0L)) - .toSet().toList().sorted() - updateEventList() + updateEventList(it, hiddenPastEvents.value ?: 0) }) pinnedCalendarEvents.observe(context as AppCompatActivity) { - val today = getToday() - binding.calendarWidgetPinnedList.submitItems(it.filter { - it.endTime > System.currentTimeMillis() && - (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) != today && - (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) != today - }.sortedBy { it.startTime }) + binding.calendarWidgetPinnedList.submitItems(it) if (it.isEmpty()) { binding.calendarWidgetPinnedList.visibility = View.GONE binding.calendarUpcomingEventsTitle.visibility = View.GONE @@ -149,38 +117,41 @@ class CalendarWidget : LauncherWidget { } } + selectedDate.observe(context as AppCompatActivity) { + binding.calendarDate.text = formatDay(it) + } + binding.calendarWidgetRoot.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) } } - private fun getToday(): Long { - return (System.currentTimeMillis() + zoneOffset) / (1000 * 60 * 60 * 24) - } - - private fun updateEventList(includePastEvents: Boolean = false) { - val today = getToday() - val events: MutableList = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay || (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay } - ?.toMutableList() ?: mutableListOf() + private fun updateEventList( + events: List, + hiddenPastDayEvents: Int + ) { + val items = events.toMutableList() if (events.isEmpty()) { - events.add( - InformationText(context.getString(R.string.calendar_widget_no_events)) + items.add( + InformationText(context.getString(R.string.calendar_widget_no_events)) ) } - val pastEvents = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) < today + selectedDay && (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) > today + selectedDay } - if (pastEvents?.isNotEmpty() == true) { - if (includePastEvents) { - events.addAll(pastEvents) - } else { - events.add(InformationText(resources.getQuantityString(R.plurals.calendar_widget_running_events, pastEvents.size, pastEvents.size)) { - updateEventList(true) + if (hiddenPastDayEvents > 0) { + items.add( + InformationText( + resources.getQuantityString( + R.plurals.calendar_widget_running_events, + hiddenPastDayEvents, + hiddenPastDayEvents + ) + ) { + viewModel.showAllEvents() }) - } } - binding.calendarWidgetList.submitItems(events) + binding.calendarWidgetList.submitItems(items) } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/search/ApplicationResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/search/ApplicationResults.kt index 03d1cab0..7a2f48ab 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/search/ApplicationResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/search/ApplicationResults.kt @@ -6,13 +6,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.lifecycle.viewmodel.compose.viewModel -import de.mm20.launcher2.applications.AppViewModel -import org.koin.androidx.compose.getViewModel +import de.mm20.launcher2.ui.launcher.search.SearchViewModel @Composable fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit { - val viewModel: AppViewModel = getViewModel() - val apps by viewModel.applications.observeAsState(emptyList()) + val viewModel: SearchViewModel by viewModel() + val apps by viewModel.appResults.observeAsState(emptyList()) return { SearchableGrid(items = apps, listState = it) } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/search/CalculatorResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/search/CalculatorResults.kt index 564c412b..059e6544 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/search/CalculatorResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/search/CalculatorResults.kt @@ -1,30 +1,19 @@ package de.mm20.launcher2.ui.search -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material.Card -import androidx.compose.material.ContentAlpha -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp -import de.mm20.launcher2.calculator.CalculatorViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.ui.component.SectionDivider -import org.koin.androidx.compose.getViewModel +import de.mm20.launcher2.ui.launcher.search.SearchViewModel @Composable fun calculatorItem(): LazyListScope.() -> Unit { - val viewModel: CalculatorViewModel = getViewModel() - val calculator by viewModel.calculator.observeAsState() + val viewModel: SearchViewModel by viewModel() + val calculator by viewModel.calculatorResult.observeAsState(null) return { calculator?.let { item { diff --git a/ui/src/main/java/de/mm20/launcher2/ui/search/FavoriteResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/search/FavoriteResults.kt index f6b37fbf..6e9a5517 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/search/FavoriteResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/search/FavoriteResults.kt @@ -6,14 +6,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.lifecycle.viewmodel.compose.viewModel -import de.mm20.launcher2.favorites.FavoritesViewModel -import org.koin.androidx.compose.getViewModel +import de.mm20.launcher2.ui.launcher.search.SearchViewModel @Composable fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit { - val viewModel: FavoritesViewModel = getViewModel() + val viewModel: SearchViewModel by viewModel() - val favorites by viewModel.getFavorites(5).observeAsState(emptyList()) + val favorites by viewModel.favorites.observeAsState(emptyList()) return { SearchableGrid(items = favorites, listState = it) } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/search/FileResults.kt b/ui/src/main/java/de/mm20/launcher2/ui/search/FileResults.kt index b58d3428..f1d1763f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/search/FileResults.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/search/FileResults.kt @@ -5,13 +5,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.lifecycle.viewmodel.compose.viewModel -import de.mm20.launcher2.files.FilesViewModel -import org.koin.androidx.compose.getViewModel +import de.mm20.launcher2.ui.launcher.search.SearchViewModel @Composable fun fileResults(): LazyListScope.() -> Unit { - val viewModel: FilesViewModel = getViewModel() - val files by viewModel.files.observeAsState(emptyList()) + val viewModel: SearchViewModel by viewModel() + val files by viewModel.fileResults.observeAsState(emptyList()) return { files?.let { SearchableList(items = it) } } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/search/WikipediaResult.kt b/ui/src/main/java/de/mm20/launcher2/ui/search/WikipediaResult.kt index 6462368b..fb297aa2 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/search/WikipediaResult.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/search/WikipediaResult.kt @@ -7,13 +7,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.unit.dp import de.mm20.launcher2.ui.component.SectionDivider -import de.mm20.launcher2.wikipedia.WikipediaViewModel -import org.koin.androidx.compose.getViewModel +import de.mm20.launcher2.ui.launcher.search.SearchViewModel +import org.koin.androidx.compose.viewModel @Composable fun wikipediaResult(): LazyListScope.() -> Unit { - val viewModel: WikipediaViewModel = getViewModel() - val wikipedia by viewModel.wikipedia.observeAsState() + val viewModel: SearchViewModel by viewModel() + val wikipedia by viewModel.wikipediaResult.observeAsState() return { wikipedia?.let { item { diff --git a/ui/src/main/java/de/mm20/launcher2/ui/searchable/CalendarEventItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/searchable/CalendarEventItem.kt deleted file mode 100644 index da78cc90..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/searchable/CalendarEventItem.kt +++ /dev/null @@ -1,241 +0,0 @@ -package de.mm20.launcher2.ui.searchable - -import android.content.Intent -import android.net.Uri -import android.text.format.DateUtils -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Card -import androidx.compose.material3.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Star -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import de.mm20.launcher2.favorites.FavoritesViewModel -import de.mm20.launcher2.ktx.tryStartActivity -import de.mm20.launcher2.search.data.CalendarEvent -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.component.DefaultSwipeActions -import de.mm20.launcher2.ui.search.Representation -import de.mm20.launcher2.ui.toPixels -import java.net.URLEncoder - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun CalendarEventItem( - event: CalendarEvent, - initialRepresentation: Representation, - modifier: Modifier -) { - - var representation by remember { mutableStateOf(initialRepresentation) } - - val favViewModel: FavoritesViewModel = viewModel() - val isPinned by favViewModel.isPinned(event).observeAsState() - - val borderWidth = 8.dp.toPixels() - - val context = LocalContext.current - DefaultSwipeActions(item = event, enabled = representation == Representation.List) { - Card( - elevation = animateDpAsState(if (representation == Representation.Full) 4.dp else 0.dp).value, - border = BorderStroke( - width = animateDpAsState(if (representation == Representation.List) 1.dp else 0.dp).value, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = animateFloatAsState(if (representation == Representation.List) 0.18f else 0f).value) - ), - modifier = modifier - ) { - Row( - modifier = (if (representation == Representation.List) Modifier.clickable( - onClick = { - representation = Representation.Full - }) else Modifier) - .fillMaxWidth() - .drawWithCache { - val color = Color(CalendarEvent.getDisplayColor(context, event.color)) - onDrawWithContent { - drawContent() - drawRect(color, size = size.copy(width = borderWidth)) - } - } - ) { - Column { - Column( - modifier = Modifier.padding(vertical = 14.dp, horizontal = 16.dp) - ) { - Text( - text = event.label, - style = MaterialTheme.typography.titleMedium - ) - AnimatedVisibility( - representation == Representation.List - ) { - Text( - text = formatEventTime(event = event), - style = MaterialTheme.typography.bodyMedium - ) - } - } - AnimatedVisibility( - representation == Representation.Full - ) { - Column( - modifier = Modifier.padding(start = 4.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.ic_time), - contentDescription = null - ) - Text( - text = if (event.allDay) { - stringResource(id = R.string.calendar_event_allday) - } else { - DateUtils.formatDateRange( - LocalContext.current, - event.startTime, - event.endTime, - DateUtils.FORMAT_SHOW_TIME - ) - }, - modifier = Modifier - .padding(start = 12.dp) - ) - } - - if (event.description.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.ic_description), - contentDescription = null - ) - Text( - text = event.description, - modifier = Modifier.padding(start = 12.dp) - ) - } - } - - if (event.location.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse( - "geo:0,0?q=${ - URLEncoder.encode( - event.location, - "utf8" - ) - }" - ) - context.tryStartActivity(intent, null) - }) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.ic_location), - contentDescription = null - ) - Text( - text = event.location, - modifier = Modifier.padding(start = 12.dp) - ) - } - } - - Row( - modifier = Modifier.padding(bottom = 4.dp) - ) { - IconButton(onClick = { representation = Representation.List }) { - Icon( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = null - ) - } - Spacer(modifier = Modifier.weight(1f)) - IconButton(onClick = { - if (isPinned == true) { - favViewModel.unpinItem(event) - } else { - favViewModel.pinItem(event) - } - }) { - Icon( - painter = if (isPinned == true) rememberVectorPainter( - Icons.Rounded.Star - ) else painterResource(id = R.drawable.ic_star_outline), - contentDescription = null - ) - } - } - } - } - } - } - } - } - - -} - -@Composable -fun formatEventTime(event: CalendarEvent): String { - val isToday = DateUtils.isToday(event.startTime) && DateUtils.isToday(event.endTime) - return if (isToday) { - if (event.allDay) { - stringResource(R.string.calendar_event_allday) - } else { - DateUtils.formatDateRange( - LocalContext.current, - event.startTime, - event.endTime, - DateUtils.FORMAT_SHOW_TIME - ) - } - } else { - if (event.allDay) { - DateUtils.formatDateRange( - LocalContext.current, - event.startTime, - event.endTime, - DateUtils.FORMAT_SHOW_DATE - ) - } else { - DateUtils.formatDateRange( - LocalContext.current, - event.startTime, - event.endTime, - DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE - ) - } - } -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/searchable/DeprecatedSearchableList.kt b/ui/src/main/java/de/mm20/launcher2/ui/searchable/DeprecatedSearchableList.kt deleted file mode 100644 index 377b51fe..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/searchable/DeprecatedSearchableList.kt +++ /dev/null @@ -1,41 +0,0 @@ -package de.mm20.launcher2.ui.searchable - -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import de.mm20.launcher2.search.data.CalendarEvent -import de.mm20.launcher2.search.data.Searchable -import de.mm20.launcher2.ui.search.Representation - -@Deprecated("Use [SearchableList] instead") -@Composable -fun DeprecatedSearchableList(items: List, modifier: Modifier = Modifier) { - - Column( - modifier = modifier - .fillMaxWidth() - .animateContentSize() - ) { - - for (item in items) { - Box( - modifier = Modifier.padding(bottom = 8.dp) - ) - key(item.key) { - if (item is CalendarEvent) { - CalendarEventItem( - event = item, - initialRepresentation = Representation.List, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - } -} diff --git a/ui/src/main/java/de/mm20/launcher2/ui/widget/CalendarWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/widget/CalendarWidget.kt index 2dc30937..90670a1d 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/widget/CalendarWidget.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/widget/CalendarWidget.kt @@ -1,259 +1,8 @@ package de.mm20.launcher2.ui.widget -import android.content.Context -import android.text.format.DateUtils -import android.view.View -import android.widget.FrameLayout -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material3.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.* -import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import de.mm20.launcher2.calendar.CalendarViewModel -import de.mm20.launcher2.favorites.FavoritesViewModel -import de.mm20.launcher2.search.data.Searchable -import de.mm20.launcher2.ui.InformationText -import de.mm20.launcher2.ui.LauncherTheme -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.pluralResource -import de.mm20.launcher2.ui.searchable.DeprecatedSearchableList -import org.koin.androidx.compose.getViewModel -import java.util.* -import kotlin.math.max -import kotlin.math.min +import androidx.compose.runtime.Composable @Composable fun CalendarWidget() { - val viewModel: CalendarViewModel = getViewModel() - val favViewModel: FavoritesViewModel = viewModel() - - val events by viewModel.upcomingCalendarEvents.observeAsState() - val pinnedEvents by favViewModel.pinnedCalendarEvents.observeAsState(emptyList()) - val today = getToday() - val availableDays: List = remember(events) { - events?.map { ((it.startTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today } - ?.union(events?.map { ((it.endTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today } - ?: emptyList()) - ?.union(listOf(0L)) - ?.toSet()?.toList()?.sorted() ?: emptyList() - } - - var selectedDay by remember { mutableStateOf(0L) } - - var showAll by remember { mutableStateOf(false) } - - val pastEvents = - events?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) < today + selectedDay && (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) > today + selectedDay } - - var noEvents = true - val selectedEvents = remember(today, events, selectedDay, showAll) { - events - ?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay || (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay } - ?.also { noEvents = it.isEmpty() } - ?.union(if (showAll && pastEvents != null) pastEvents else emptyList())?.toList() - ?: listOf() - } - - - - Column( - modifier = Modifier.padding(bottom = 8.dp) - ) { - Row( - modifier = Modifier.padding(8.dp) - ) { - DaySelector( - availableDays = availableDays, - modifier = Modifier.weight(1f), - onSelectDay = { - selectedDay = it - showAll = false - } - ) - IconButton(onClick = { /*TODO*/ }) { - Icon( - imageVector = Icons.Rounded.OpenInNew, - contentDescription = stringResource(R.string.calendar_menu_open_externally), - ) - } - IconButton(onClick = { /*TODO*/ }) { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = stringResource(R.string.calendar_widget_new_event) - ) - } - } - Column( - modifier = Modifier.padding(horizontal = 16.dp) - ) { - if (noEvents) { - InformationText( - text = stringResource(id = R.string.calendar_widget_no_events) - ) - } - DeprecatedSearchableList( - items = selectedEvents, - modifier = Modifier.padding(bottom = 8.dp) - ) - if (!showAll && pastEvents?.isNotEmpty() == true) { - InformationText( - text = pluralResource( - R.plurals.calendar_widget_running_events, - pastEvents.size, - pastEvents.size - ), - modifier = Modifier.padding(bottom = 8.dp), - onClick = { - showAll = true - } - ) - } - if (pinnedEvents.isNotEmpty()) { - Text( - text = stringResource(id = R.string.calendar_widget_pinned_events), - style = MaterialTheme.typography.titleLarge - ) - DeprecatedSearchableList( - items = pinnedEvents, - modifier = Modifier.padding(bottom = 8.dp) - ) - } - } - - - } } - -@Composable -fun DaySelector( - modifier: Modifier = Modifier, - availableDays: List, - onSelectDay: (Long) -> Unit -) { - var menuExpanded by remember { mutableStateOf(false) } - var selectedDay by remember { mutableStateOf(0L) } - - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { - val i = max(1, availableDays.indexOf(selectedDay)) - selectedDay = availableDays[i - 1] - onSelectDay(selectedDay) - }) { - Icon( - imageVector = Icons.Rounded.ChevronLeft, - contentDescription = null - ) - } - Row( - modifier = Modifier - .weight(1f), - horizontalArrangement = Arrangement.Center - ) { - Row( - modifier = Modifier - .clickable(onClick = { - menuExpanded = true - }) - .padding(all = 12.dp) - .wrapContentWidth() - .animateContentSize() - ) { - Text( - overflow = TextOverflow.Ellipsis, - maxLines = 1, - modifier = Modifier - .wrapContentWidth(), - text = formatDay(LocalContext.current, selectedDay), - style = MaterialTheme.typography.titleLarge - ) - Icon( - imageVector = Icons.Rounded.ArrowDropDown, - modifier = Modifier.size(24.dp), - contentDescription = null - ) - } - DropdownMenu(expanded = menuExpanded, onDismissRequest = { - menuExpanded = false - }) { - - for (day in availableDays) { - DropdownMenuItem(onClick = { - selectedDay = day - menuExpanded = false - onSelectDay(selectedDay) - }) { - Text( - text = formatDay(LocalContext.current, day), - style = MaterialTheme.typography.titleMedium - ) - } - } - } - } - - - IconButton(onClick = { - val i = min(availableDays.lastIndex - 1, availableDays.indexOf(selectedDay)) - selectedDay = availableDays[i + 1] - onSelectDay(selectedDay) - }) { - Icon( - imageVector = Icons.Rounded.ChevronRight, - contentDescription = null - ) - } - } -} - -private fun getToday(): Long { - return (System.currentTimeMillis() + zoneOffset) / (1000 * 60 * 60 * 24) -} - -private val zoneOffset - get() = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis()).toLong() - -private fun formatDay(context: Context, day: Long): String { - return when (day) { - 0L -> context.getString(R.string.date_today) - 1L -> context.getString(R.string.date_tomorrow) - else -> DateUtils.formatDateTime( - context, - (getToday() + day) * (1000 * 60 * 60 * 24), - DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY - ) - } -} - -object CalendarWidgetShim { - fun getLegacyView(context: Context): View { - val composeView = ComposeView(context) - composeView.id = FrameLayout.generateViewId() - composeView.setContent { - LauncherTheme { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { - Column { - CalendarWidget() - } - } - } - } - return composeView - } -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/widget/parts/DatePart.kt b/ui/src/main/java/de/mm20/launcher2/ui/widget/parts/DatePart.kt index df58ae76..86cd04cf 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/widget/parts/DatePart.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/widget/parts/DatePart.kt @@ -9,9 +9,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import de.mm20.launcher2.calendar.CalendarViewModel import de.mm20.launcher2.ui.component.TextClock -import org.koin.androidx.compose.getViewModel @Composable fun DatePart() { diff --git a/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/Module.kt b/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/Module.kt index 19e17408..4a207456 100644 --- a/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/Module.kt +++ b/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/Module.kt @@ -7,6 +7,5 @@ import org.koin.dsl.module val unitConverterModule = module { single { CurrencyRepository(androidContext()) } - single { UnitConverterRepository(androidContext()) } - viewModel { UnitConverterViewModel(get()) } + single { UnitConverterRepositoryImpl(androidContext()) } } \ No newline at end of file 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 20dacb04..ab201bc9 100644 --- a/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterRepository.kt +++ b/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterRepository.kt @@ -2,15 +2,19 @@ package de.mm20.launcher2.unitconverter import android.content.Context import androidx.lifecycle.MutableLiveData -import de.mm20.launcher2.currencies.CurrencyRepository -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.data.UnitConverter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow -class UnitConverterRepository(val context: Context) : BaseSearchableRepository() { +interface UnitConverterRepository { + fun search(query:String): Flow +} + +class UnitConverterRepositoryImpl(val context: Context) : UnitConverterRepository { val unitConverter = MutableLiveData() - override suspend fun search(query: String) { - unitConverter.value = UnitConverter.search(context, query) + override fun search(query: String): Flow = channelFlow { + send(UnitConverter.search(context, query)) } } \ No newline at end of file diff --git a/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterViewModel.kt b/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterViewModel.kt deleted file mode 100644 index 669f5c5a..00000000 --- a/unitconverter/src/main/java/de/mm20/launcher2/unitconverter/UnitConverterViewModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package de.mm20.launcher2.unitconverter - -import androidx.lifecycle.ViewModel - -class UnitConverterViewModel( - unitConverterRepository: UnitConverterRepository -): ViewModel() { - val unitConverter = unitConverterRepository.unitConverter -} \ No newline at end of file diff --git a/websites/src/main/java/de/mm20/launcher2/websites/Module.kt b/websites/src/main/java/de/mm20/launcher2/websites/Module.kt index 3617c6ce..9765f84d 100644 --- a/websites/src/main/java/de/mm20/launcher2/websites/Module.kt +++ b/websites/src/main/java/de/mm20/launcher2/websites/Module.kt @@ -5,8 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val websitesModule = module { - single { WebsiteRepository(androidContext()) } - viewModel { - WebsiteViewModel(get()) - } + single { WebsiteRepositoryImpl(androidContext()) } } \ No newline at end of file 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 6b2b42c9..14f53a3d 100644 --- a/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt +++ b/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt @@ -1,17 +1,19 @@ package de.mm20.launcher2.websites import android.content.Context -import androidx.lifecycle.MutableLiveData -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.data.Website import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit -class WebsiteRepository(val context: Context) : BaseSearchableRepository() { +interface WebsiteRepository { + fun search(query: String): Flow +} - val website = MutableLiveData() +class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository { private val httpClient = OkHttpClient .Builder() @@ -20,8 +22,8 @@ class WebsiteRepository(val context: Context) : BaseSearchableRepository() { .writeTimeout(1000, TimeUnit.MILLISECONDS) .build() - override fun onCancel() { - super.onCancel() + override fun search(query: String): Flow = channelFlow { + send(null) httpClient.dispatcher.run { runningCalls().forEach { it.cancel() @@ -30,14 +32,10 @@ class WebsiteRepository(val context: Context) : BaseSearchableRepository() { it.cancel() } } - } - - override suspend fun search(query: String) { - website.value = null - if (query.isBlank()) return - val wiki = withContext(Dispatchers.IO) { + if (query.isBlank()) return@channelFlow + val website = withContext(Dispatchers.IO) { Website.search(context, query, httpClient) } - website.value = wiki + send(website) } } \ No newline at end of file diff --git a/websites/src/main/java/de/mm20/launcher2/websites/WebsiteViewModel.kt b/websites/src/main/java/de/mm20/launcher2/websites/WebsiteViewModel.kt deleted file mode 100644 index 6e67dde2..00000000 --- a/websites/src/main/java/de/mm20/launcher2/websites/WebsiteViewModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.mm20.launcher2.websites - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import de.mm20.launcher2.search.data.Website - -class WebsiteViewModel( - websiteRepository: WebsiteRepository -): ViewModel() { - val website: LiveData = websiteRepository.website -} \ No newline at end of file diff --git a/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt b/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt index 1c4276f4..8a6e4d1b 100644 --- a/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt +++ b/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt @@ -6,5 +6,5 @@ import org.koin.dsl.module val widgetsModule = module { single { WidgetRepository(androidContext()) } - viewModel { WidgetViewModel(get(), get()) } + viewModel { WidgetViewModel(get()) } } \ No newline at end of file diff --git a/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetViewModel.kt b/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetViewModel.kt index 74f65a58..6a690334 100644 --- a/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetViewModel.kt +++ b/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetViewModel.kt @@ -2,14 +2,12 @@ package de.mm20.launcher2.widgets import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.mm20.launcher2.calendar.CalendarRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class WidgetViewModel( - private val widgetRepository: WidgetRepository, - private val calendarRepository: CalendarRepository + private val widgetRepository: WidgetRepository ) : ViewModel() { @@ -29,7 +27,4 @@ class WidgetViewModel( return widgetRepository.getInternalWidgets() } - fun requestCalendarUpdate() { - calendarRepository.requestCalendarUpdate() - } } \ No newline at end of file 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 711d4f6b..e3d73ab0 100644 --- a/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt +++ b/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt @@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val wikipediaModule = module { - single { WikipediaRepository(androidContext()) } - viewModel { WikipediaViewModel(get()) } + single { WikipediaRepositoryImpl(androidContext()) } } \ 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 a956a7db..4d3a6970 100644 --- a/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt +++ b/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt @@ -1,19 +1,23 @@ package de.mm20.launcher2.wikipedia import android.content.Context -import androidx.lifecycle.MutableLiveData import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.preferences.LauncherPreferences -import de.mm20.launcher2.search.BaseSearchableRepository import de.mm20.launcher2.search.data.Wikipedia +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit -class WikipediaRepository(val context: Context) : BaseSearchableRepository() { +interface WikipediaRepository { + fun search(query: String): Flow +} - val wikipedia = MutableLiveData() +class WikipediaRepositoryImpl( + private val context: Context +): WikipediaRepository { private val httpClient by lazy { OkHttpClient @@ -24,7 +28,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() { .build() } - val retrofit by lazy { + private val retrofit by lazy { Retrofit.Builder() .client(httpClient) .baseUrl(context.getString(R.string.wikipedia_url)) @@ -36,9 +40,9 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() { retrofit.create(WikipediaApi::class.java) } - override fun onCancel() { - super.onCancel() + override fun search(query: String): Flow = channelFlow { + send(null) httpClient.dispatcher.run { runningCalls().forEach { it.cancel() @@ -47,20 +51,17 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() { it.cancel() } } - } - override suspend fun search(query: String) { - wikipedia.value = null - if (query.isBlank()) return + if (query.isBlank()) return@channelFlow val result = try { wikipediaService.search(query) } catch (e: Exception) { CrashReporter.logException(e) - return + return@channelFlow } - val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return + val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return@channelFlow val image = if (LauncherPreferences.instance.searchWikipediaPictures) { val width = context.resources.displayMetrics.widthPixels / 2 @@ -68,7 +69,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() { wikipediaService.getPageImage(page.pageid, width) } catch (e: Exception) { CrashReporter.logException(e) - return + return@channelFlow } imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source } else null @@ -79,7 +80,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() { text = page.extract, image = image ) - - wikipedia.value = wiki + send(wiki) } + } \ No newline at end of file diff --git a/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaViewModel.kt b/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaViewModel.kt deleted file mode 100644 index 522f9a86..00000000 --- a/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaViewModel.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.mm20.launcher2.wikipedia - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import de.mm20.launcher2.search.data.Wikipedia - -class WikipediaViewModel( - wikipediaRepository: WikipediaRepository -): ViewModel() { - val wikipedia: LiveData = wikipediaRepository.wikipedia -} \ No newline at end of file