Refactor search, favorites and calendar widget

This commit is contained in:
MM20 2021-12-31 20:14:17 +01:00
parent 0193d4e495
commit a2ccef64ce
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
81 changed files with 1335 additions and 1671 deletions

View File

@ -19,9 +19,11 @@ class AddItemActivity : Activity() {
val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() } val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() }
val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() } val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() }
val shortcut = AppShortcut(this.applicationContext, shortcutInfo, val shortcut = AppShortcut(
packageManager.getApplicationInfo(shortcutInfo.`package`, 0) this.applicationContext, shortcutInfo,
.loadLabel(packageManager).toString()) packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
.loadLabel(packageManager).toString()
)
if (pinRequest.accept()) { if (pinRequest.accept()) {
favoritesRepository.pinItem(shortcut) favoritesRepository.pinItem(shortcut)
} }

View File

@ -10,70 +10,69 @@ import android.content.pm.ShortcutInfo
import android.os.Process import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeProvider import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.preferences.LauncherPreferences 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.AppInstallation
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class AppRepository( interface AppRepository {
val context: Context, fun search(query: String): Flow<List<Application>>
}
class AppRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository, hiddenItemsRepository: HiddenItemsRepository,
badgeProvider: BadgeProvider private val badgeProvider: BadgeProvider
) : BaseSearchableRepository() { ) : 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<List<Application>>() private val installedApps = MutableStateFlow<List<Application>>(emptyList())
private val installations = MutableStateFlow<MutableList<AppInstallation>>(mutableListOf())
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
private val installedApps = MutableLiveData<List<Application>>(emptyList()) private val profiles: List<UserHandle> =
private val installations = MutableLiveData<MutableList<AppInstallation>>(mutableListOf()) if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
} else {
listOf(Process.myUserHandle())
}
private val installingPackages = mutableMapOf<Int, String>() private val installingPackages = mutableMapOf<Int, String>()
private val profiles: List<UserHandle> = 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 { init {
applications.addSource(installedApps) {
launch { updateAppsForDisplay() }
}
applications.addSource(installations) {
launch { updateAppsForDisplay() }
}
applications.addSource(hiddenItemKeys) {
launch { updateAppsForDisplay() }
}
launcherApps.registerCallback(object : LauncherApps.Callback() { launcherApps.registerCallback(object : LauncherApps.Callback() {
override fun onPackagesUnavailable(packageNames: Array<out String>, user: UserHandle, replacing: Boolean) { override fun onPackagesUnavailable(
installedApps.value = installedApps.value?.filter { !packageNames.contains(it.`package`) } packageNames: Array<out String>,
user: UserHandle,
replacing: Boolean
) {
installedApps.value =
installedApps.value.filter { !packageNames.contains(it.`package`) }
} }
override fun onPackageChanged(packageName: String, user: UserHandle) { override fun onPackageChanged(packageName: String, user: UserHandle) {
val apps = installedApps.value?.toMutableList() ?: return val apps = installedApps.value.toMutableList()
apps.removeAll { packageName == it.`package` } apps.removeAll { packageName == it.`package` }
apps.addAll(getApplications(packageName)) apps.addAll(getApplications(packageName))
installedApps.value = apps installedApps.value = apps
} }
override fun onPackagesAvailable(packageNames: Array<out String>, user: UserHandle, replacing: Boolean) { override fun onPackagesAvailable(
val apps = installedApps.value?.toMutableList() ?: return packageNames: Array<out String>,
user: UserHandle,
replacing: Boolean
) {
val apps = installedApps.value.toMutableList()
for (packageName in packageNames) { for (packageName in packageNames) {
apps.addAll(getApplications(packageName)) apps.addAll(getApplications(packageName))
} }
@ -82,16 +81,20 @@ class AppRepository(
override fun onPackageAdded(packageName: String, user: UserHandle) { override fun onPackageAdded(packageName: String, user: UserHandle) {
Log.d("MM20", "App installed: $packageName") Log.d("MM20", "App installed: $packageName")
val apps = installedApps.value?.toMutableList() ?: return val apps = installedApps.value.toMutableList()
apps.addAll(getApplications(packageName)) apps.addAll(getApplications(packageName))
installedApps.value = apps installedApps.value = apps
} }
override fun onPackageRemoved(packageName: String, user: UserHandle) { 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<ShortcutInfo>, user: UserHandle) { override fun onShortcutsChanged(
packageName: String,
shortcuts: MutableList<ShortcutInfo>,
user: UserHandle
) {
super.onShortcutsChanged(packageName, shortcuts, user) super.onShortcutsChanged(packageName, shortcuts, user)
onPackageChanged(packageName, user) onPackageChanged(packageName, user)
} }
@ -99,11 +102,17 @@ class AppRepository(
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) { override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
super.onPackagesSuspended(packageNames, user) super.onPackagesSuspended(packageNames, user)
packageNames?.forEach { 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<out String>?, user: UserHandle?) { override fun onPackagesUnsuspended(
packageNames: Array<out String>?,
user: UserHandle?
) {
super.onPackagesUnsuspended(packageNames, user) super.onPackagesUnsuspended(packageNames, user)
packageNames?.forEach { packageNames?.forEach {
badgeProvider.removeBadge("app://$it") 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 val packageInstaller = context.packageManager.packageInstaller
@ -139,13 +151,13 @@ class AppRepository(
installingPackages.remove(sessionId) installingPackages.remove(sessionId)
val key = "app://$pkg" val key = "app://$pkg"
val badge = badgeProvider.getBadge(key)?.apply { progress = null } val badge = badgeProvider.getBadge(key)?.apply { progress = null }
?: Badge() ?: Badge()
badgeProvider.setBadge(key, badge) badgeProvider.setBadge(key, badge)
val inst = installations.value ?: return val inst = installations.value
inst.removeAll { inst.removeAll {
it.session.sessionId == sessionId it.session.sessionId == sessionId
} }
installations.postValue(inst) installations.value = inst
} }
@ -165,41 +177,66 @@ class AppRepository(
val appInstallation = AppInstallation(session) val appInstallation = AppInstallation(session)
val inst = installations.value ?: mutableListOf() val inst = installations.value ?: mutableListOf()
inst.add(appInstallation) 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) { private fun getApplications(packageName: String): List<Application> {
updateAppsForDisplay() 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) { override fun search(query: String): Flow<List<Application>> = channelFlow {
val hiddenItems = hiddenItemKeys.value ?: emptyList()
val installed = installedApps.value ?: emptyList() combine(installedApps, hiddenItems, installations) {_, _, _ ->
val installing = installations.value ?: emptyList<AppInstallation>() null
val results = mutableListOf<Application>() }.collectLatest {
results.addAll(installed) withContext(Dispatchers.IO) {
results.addAll(installing) val appResults = mutableListOf<Application>()
if (query.isNotEmpty()) { if (query.isEmpty()) {
results.removeAll { !it.label.contains(query, ignoreCase = true) } appResults.addAll(installedApps.value)
getActivityByComponentName(componentName)?.let { results.add(it) } 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? { private fun getActivityByComponentName(componentName: ComponentName?): Application? {
@ -211,15 +248,4 @@ class AppRepository(
LauncherApp(context, lai) 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<Application> {
if (packageName == context.packageName) return emptyList()
return profiles.map { p -> launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } }.flatten()
}
} }

View File

@ -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<List<Application>> = appRepository.applications
}

View File

@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val applicationsModule = module { val applicationsModule = module {
single { AppRepository(androidContext(), get(), get()) } single<AppRepository> { AppRepositoryImpl(androidContext(), get(), get()) }
viewModel { AppViewModel(get()) }
} }

View File

@ -1,40 +1,42 @@
package de.mm20.launcher2.calculator package de.mm20.launcher2.calculator
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.Calculator
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import org.mariuszgromada.math.mxparser.Expression import org.mariuszgromada.math.mxparser.Expression
class CalculatorRepository : BaseSearchableRepository() { interface CalculatorRepository {
fun search(query: String): Flow<Calculator?>
}
val calculator = MutableLiveData<Calculator?>() class CalculatorRepositoryImpl : CalculatorRepository {
override suspend fun search(query: String) { override fun search(query: String): Flow<Calculator?> = channelFlow {
if (query.isBlank()) { if (query.isBlank()) {
calculator.value = null send(null)
return return@channelFlow
} }
if (!LauncherPreferences.instance.searchCalculator) return if (!LauncherPreferences.instance.searchCalculator) return@channelFlow
val calc = when { val calc = when {
query.matches(Regex("0x[0-9a-fA-F]+")) -> { query.matches(Regex("0x[0-9a-fA-F]+")) -> {
val solution = query.substring(2).toIntOrNull(16) ?: run { val solution = query.substring(2).toIntOrNull(16) ?: run {
calculator.value = null send(null)
return return@channelFlow
} }
Calculator(term = query, solution = solution.toDouble()) Calculator(term = query, solution = solution.toDouble())
} }
query.matches(Regex("0b[01]+")) -> { query.matches(Regex("0b[01]+")) -> {
val solution = query.substring(2).toIntOrNull(2) ?: run { val solution = query.substring(2).toIntOrNull(2) ?: run {
calculator.value = null send(null)
return return@channelFlow
} }
Calculator(term = query, solution = solution.toDouble()) Calculator(term = query, solution = solution.toDouble())
} }
query.matches(Regex("0[0-7]+")) -> { query.matches(Regex("0[0-7]+")) -> {
val solution = query.substring(1).toIntOrNull(8) ?: run { val solution = query.substring(1).toIntOrNull(8) ?: run {
calculator.value = null send(null)
return return@channelFlow
} }
Calculator(term = query, solution = solution.toDouble()) Calculator(term = query, solution = solution.toDouble())
} }
@ -50,6 +52,6 @@ class CalculatorRepository : BaseSearchableRepository() {
} }
} }
} }
calculator.value = calc send(calc)
} }
} }

View File

@ -1,9 +0,0 @@
package de.mm20.launcher2.calculator
import androidx.lifecycle.ViewModel
class CalculatorViewModel(
calculatorRepository: CalculatorRepository
): ViewModel() {
val calculator = calculatorRepository.calculator
}

View File

@ -1,9 +1,7 @@
package de.mm20.launcher2.calculator package de.mm20.launcher2.calculator
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val calculatorModule = module { val calculatorModule = module {
single { CalculatorRepository() } single<CalculatorRepository> { CalculatorRepositoryImpl() }
viewModel { CalculatorViewModel(get()) }
} }

View File

@ -1,44 +1,80 @@
package de.mm20.launcher2.calendar package de.mm20.launcher2.calendar
import android.content.Context import android.content.Context
import androidx.lifecycle.MediatorLiveData import android.util.Log
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import kotlinx.coroutines.Dispatchers 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 import kotlinx.coroutines.withContext
class CalendarRepository( interface CalendarRepository {
val context: Context, fun search(query: String): Flow<List<CalendarEvent>>
fun getUpcomingEvents(): Flow<List<CalendarEvent>>
}
class CalendarRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() { ) : CalendarRepository {
val calendarEvents = MediatorLiveData<List<CalendarEvent>?>() private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
val upcomingCalendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
private val allEvents = MutableLiveData<List<CalendarEvent>?>(emptyList()) override fun search(query: String): Flow<List<CalendarEvent>> = channelFlow {
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys if (query.isBlank()) {
send(emptyList())
init { return@channelFlow
calendarEvents.addSource(hiddenItemKeys) { keys ->
calendarEvents.value = allEvents.value?.filter { !keys.contains(it.key) }
} }
calendarEvents.addSource(allEvents) { e -> val events = withContext(Dispatchers.IO) {
calendarEvents.value = e?.filter { hiddenItemKeys.value?.contains(it.key) != true } 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() { override fun getUpcomingEvents(): Flow<List<CalendarEvent>> = channelFlow {
launch { val unselectedCalendars = callbackFlow {
val unselectedCalendars = LauncherPreferences.instance.unselectedCalendars val unregister =
val hideAlldayEvents = LauncherPreferences.instance.calendarHideAllday 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 now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) { val events = withContext(Dispatchers.IO) {
@ -48,28 +84,20 @@ class CalendarRepository(
intervalStart = now, intervalStart = now,
intervalEnd = end, intervalEnd = end,
limit = 700, limit = 700,
hideAllDayEvents = hideAlldayEvents, hideAllDayEvents = LauncherPreferences.instance.calendarHideAllday,
unselectedCalendars = unselectedCalendars, unselectedCalendars = LauncherPreferences.instance.unselectedCalendars
hiddenEvents = hiddenItemKeys.value?.mapNotNull { ).filter {
if (it.startsWith("calendar")) it.substringAfterLast("/").toLong() !hiddenItems.value.contains(it.key)
else null }
} ?: emptyList()
)
} }
upcomingCalendarEvents.value = events send(events)
} }
} }
override suspend fun search(query: String) { var unselectedCalendars: List<Long>
if (query.isBlank()) { get() = LauncherPreferences.instance.unselectedCalendars
allEvents.value = null set(value) {
return 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
}
} }

View File

@ -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<List<CalendarEvent>?> = calendarRepository.calendarEvents
val upcomingCalendarEvents: LiveData<List<CalendarEvent>> = calendarRepository.upcomingCalendarEvents
}

View File

@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val calendarModule = module { val calendarModule = module {
single { CalendarRepository(androidContext(), get()) } single<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
viewModel { CalendarViewModel(get()) }
} }

View File

@ -1,41 +1,35 @@
package de.mm20.launcher2.contacts package de.mm20.launcher2.contacts
import android.content.Context import android.content.Context
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.Contact import de.mm20.launcher2.search.data.Contact
import kotlinx.coroutines.Dispatchers 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 import kotlinx.coroutines.withContext
class ContactRepository( interface ContactRepository {
val context: Context, fun search(query: String): Flow<List<Contact>>
}
class ContactRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() { ) : ContactRepository {
val contacts = MediatorLiveData<List<Contact>?>() private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
private val allContacts = MutableLiveData<List<Contact>?>(emptyList()) override fun search(query: String): Flow<List<Contact>> = channelFlow {
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys val contacts = withContext(Dispatchers.IO) {
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) {
Contact.search(context, query) Contact.search(context, query)
} }
allContacts.value = results hiddenItems.collectLatest { hiddenItems ->
val contactResults = withContext(Dispatchers.IO) {
contacts.filter { !hiddenItems.contains(it.key) }
}
send(contactResults)
}
} }
} }

View File

@ -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<List<Contact>?> = contactRepository.contacts
}

View File

@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val contactsModule = module { val contactsModule = module {
single { ContactRepository(androidContext(), get()) } single<ContactRepository> { ContactRepositoryImpl(androidContext(), get()) }
viewModel { ContactViewModel(get()) }
} }

View File

@ -13,7 +13,7 @@ class CurrencyRepository(val context: Context) {
init { init {
val currencyWorker = PeriodicWorkRequest.Builder(ExchangeRateWorker::class.java, 60, TimeUnit.MINUTES) val currencyWorker = PeriodicWorkRequest.Builder(ExchangeRateWorker::class.java, 60, TimeUnit.MINUTES)
.build() .build()
WorkManager.getInstance().enqueueUniquePeriodicWork("ExchangeRates", WorkManager.getInstance(context).enqueueUniquePeriodicWork("ExchangeRates",
ExistingPeriodicWorkPolicy.REPLACE, currencyWorker) ExistingPeriodicWorkPolicy.REPLACE, currencyWorker)
} }

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import de.mm20.launcher2.database.entities.FavoritesItemEntity import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.database.entities.WebsearchEntity import de.mm20.launcher2.database.entities.WebsearchEntity
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface SearchDao { interface SearchDao {
@ -18,7 +19,10 @@ interface SearchDao {
fun insertSkipExisting(items: FavoritesItemEntity) fun insertSkipExisting(items: FavoritesItemEntity)
@Query("SELECT * FROM Searchable WHERE pinned > 0 ORDER BY pinned DESC, launchCount DESC") @Query("SELECT * FROM Searchable WHERE pinned > 0 ORDER BY pinned DESC, launchCount DESC")
fun getFavorites() : LiveData<List<FavoritesItemEntity>> fun getFavorites(): Flow<List<FavoritesItemEntity>>
@Query("SELECT * FROM Searchable WHERE pinned > 0 AND `key` LIKE 'calendar://%' ORDER BY pinned DESC, launchCount DESC")
fun getPinnedCalendarEvents(): Flow<List<FavoritesItemEntity>>
@Query("SELECT COUNT(key) as count FROM Searchable WHERE pinned = 1;") @Query("SELECT COUNT(key) as count FROM Searchable WHERE pinned = 1;")
@ -44,14 +48,14 @@ interface SearchDao {
fun unpinFavorite(key: String) fun unpinFavorite(key: String)
@Query("DELETE FROM Searchable WHERE `key` = :key") @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") @Query("UPDATE Searchable SET pinned = 0 WHERE `key` = :key")
fun unpinApp(key: String) fun unpinApp(key: String)
@Query("SELECT pinned FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinned ORDER BY pinned DESC LIMIT 1") @Query("SELECT pinned FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinned ORDER BY pinned DESC LIMIT 1")
fun isPinned(key: String): LiveData<Boolean> fun isPinned(key: String): Flow<Boolean>
@Query("UPDATE Searchable SET hidden = 1, pinned = 0 WHERE `key` = :key") @Query("UPDATE Searchable SET hidden = 1, pinned = 0 WHERE `key` = :key")
@ -67,19 +71,16 @@ interface SearchDao {
fun unhideItem(key: String) fun unhideItem(key: String)
@Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1") @Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1")
fun isHidden(key: String): LiveData<Boolean> fun isHidden(key: String): Flow<Boolean>
@Query("SELECT `key` FROM SEARCHABLE WHERE hidden = 1") @Query("SELECT `key` FROM SEARCHABLE WHERE hidden = 1")
fun getHiddenItemKeys(): LiveData<List<String>> fun getHiddenItemKeys(): Flow<List<String>>
@Query("SELECT * FROM SEARCHABLE WHERE hidden = 1") @Query("SELECT * FROM SEARCHABLE WHERE hidden = 1")
fun getHiddenItems(): LiveData<List<FavoritesItemEntity>> fun getHiddenItems(): Flow<List<FavoritesItemEntity>>
@Query("SELECT * FROM Websearch ORDER BY label ASC") @Query("SELECT * FROM Websearch ORDER BY label ASC")
fun getWebSearches(): List<WebsearchEntity> fun getWebSearches(): Flow<List<WebsearchEntity>>
@Query("SELECT * FROM Websearch ORDER BY label ASC")
fun getWebSearchesLiveData(): LiveData<List<WebsearchEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertWebsearch(websearch: WebsearchEntity) fun insertWebsearch(websearch: WebsearchEntity)

View File

@ -38,6 +38,7 @@ dependencies {
implementation(libs.bundles.kotlin) implementation(libs.bundles.kotlin)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android) implementation(libs.koin.android)

View File

@ -1,38 +1,183 @@
package de.mm20.launcher2.favorites package de.mm20.launcher2.favorites
import android.content.Context 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.AppDatabase
import de.mm20.launcher2.database.entities.FavoritesItemEntity import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.ktx.ceilToInt import de.mm20.launcher2.ktx.ceilToInt
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.* 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.get
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf 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<List<Searchable>>
fun getPinnedCalendarEvents(): Flow<List<Searchable>>
fun isPinned(searchable: Searchable): Flow<Boolean>
fun pinItem(searchable: Searchable)
fun unpinItem(searchable: Searchable)
fun isHidden(searchable: Searchable): Flow<Boolean>
fun hideItem(searchable: Searchable)
fun unhideItem(searchable: Searchable)
fun incrementLaunchCounter(searchable: Searchable)
suspend fun getAllFavoriteItems(): List<FavoritesItem>
fun saveFavorites(favorites: List<FavoritesItem>)
fun getHiddenItems(): Flow<List<Searchable>>
}
private val scope = CoroutineScope(Job() + Dispatchers.Main) class FavoritesRepositoryImpl(
private val context: Context,
private val database: AppDatabase
) : FavoritesRepository, KoinComponent {
private val favorites = MediatorLiveData<List<Searchable>>() private val scope = CoroutineScope(Dispatchers.Main + Job())
private val favoriteItems: LiveData<List<FavoritesItemEntity>> = MutableLiveData()
val hiddenItems = MediatorLiveData<List<Searchable>>() override fun getFavorites(): Flow<List<Searchable>> = 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<List<CalendarEvent>> {
return database.searchDao().getPinnedCalendarEvents().map {
it.mapNotNull { fromDatabaseEntity(it).searchable as? CalendarEvent }
}
}
override fun isPinned(searchable: Searchable): Flow<Boolean> {
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<Boolean> {
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<FavoritesItem> {
return withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull {
fromDatabaseEntity(it).takeIf { it.searchable != null }
}
}
}
override fun saveFavorites(favorites: List<FavoritesItem>) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao()
.saveFavorites(favorites.map { it.toDatabaseEntity() })
}
}
}
override fun getHiddenItems(): Flow<List<Searchable>> {
return database.searchDao().getHiddenItems().map {
it.mapNotNull { fromDatabaseEntity(it).searchable }
}
}
val pinnedCalendarEvents = MediatorLiveData<List<CalendarEvent>>()
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem { private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
val deserializer: SearchableDeserializer = get { parametersOf(entity.serializedSearchable) } val deserializer: SearchableDeserializer = get { parametersOf(entity.serializedSearchable) }
@ -44,183 +189,4 @@ class FavoritesRepository(private val context: Context) : BaseSearchableReposito
hidden = entity.hidden hidden = entity.hidden
) )
} }
private val reloadFavorites: (String) -> Unit = {
scope.launch {
if(!LauncherPreferences.instance.searchShowFavorites) {
favorites.value = emptyList()
return@launch
}
val favs = mutableListOf<Searchable>()
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<Boolean> {
return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key)
}
fun getFavorites(columns: Int): LiveData<List<Searchable>> {
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<Boolean> {
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<FavoritesItem> {
return withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull {
fromDatabaseEntity(it).takeIf { it.searchable != null }
}
}
}
fun saveFavorites(favorites: MutableList<FavoritesItem>) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().saveFavorites(favorites.map { it.toDatabaseEntity() })
}
}
}
fun getTopFavorites(count: Int): LiveData<List<Searchable>> {
val favs = MediatorLiveData<List<Searchable>>()
favs.addSource(favorites) {
favs.value = it.subList(0, min(count, it.size))
}
return favs
}
} }

View File

@ -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<List<Searchable>> {
return favoritesRepository.getTopFavorites(count)
}
fun getFavorites(columns: Int): LiveData<List<Searchable>> {
return favoritesRepository.getFavorites(columns)
}
fun pinItem(searchable: Searchable) {
favoritesRepository.pinItem(searchable)
}
fun unpinItem(searchable: Searchable) {
favoritesRepository.unpinItem(searchable)
}
fun isPinned(searchable: Searchable): LiveData<Boolean> {
return favoritesRepository.isPinned(searchable)
}
fun isHidden(searchable: Searchable): LiveData<Boolean> {
return favoritesRepository.isHidden(searchable)
}
fun hideItem(searchable: Searchable) {
favoritesRepository.hideItem(searchable)
}
fun unhideItem(searchable: Searchable) {
favoritesRepository.unhideItem(searchable)
}
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
return favoritesRepository.getAllFavoriteItems()
}
fun saveFavorites(favorites: MutableList<FavoritesItem>) {
favoritesRepository.saveFavorites(favorites)
}
val hiddenItems: LiveData<List<Searchable>> = this.favoritesRepository.hiddenItems
val pinnedCalendarEvents: LiveData<List<CalendarEvent>> = this.favoritesRepository.pinnedCalendarEvents
}

View File

@ -91,7 +91,5 @@ val favoritesModule = module {
return@factory NullDeserializer() return@factory NullDeserializer()
} }
single { FavoritesRepository(androidContext()) } single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get()) }
viewModel { FavoritesViewModel(get()) }
} }

View File

@ -1,26 +1,26 @@
package de.mm20.launcher2.files package de.mm20.launcher2.files
import android.content.Context import android.content.Context
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.* import de.mm20.launcher2.search.data.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
class FilesRepository( interface FileRepository {
val context: Context, fun search(query: String): Flow<List<File>>
suspend fun deleteFile(file: File)
}
class FileRepositoryImpl(
private val context: Context,
hiddenItemsRepository: HiddenItemsRepository hiddenItemsRepository: HiddenItemsRepository
) : BaseSearchableRepository() { ) : FileRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
val files = MediatorLiveData<List<File>?>()
private val allFiles = MutableLiveData<List<File>?>(emptyList())
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
private val nextcloudClient by lazy { private val nextcloudClient by lazy {
NextcloudApiHelper(context) NextcloudApiHelper(context)
@ -29,44 +29,39 @@ class FilesRepository(
OwncloudClient(context) OwncloudClient(context)
} }
init { override fun search(query: String): Flow<List<File>> = channelFlow {
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) {
if (query.isBlank()) { if (query.isBlank()) {
allFiles.value = null send(emptyList())
return return@channelFlow
} }
val localFiles = withContext(Dispatchers.IO) {
LocalFile.search(context, query).sorted().toMutableList()
}
allFiles.value = localFiles
val cloudFiles = withContext(Dispatchers.IO) { hiddenItems.collectLatest { hiddenItems ->
delay(300) val files = mutableListOf<File>()
listOf(
async { OneDriveFile.search(context, query) }, val localFiles = withContext(Dispatchers.IO) {
async { GDriveFile.search(context, query) }, LocalFile.search(context, query).sorted().filter { !hiddenItems.contains(it.key) }
async { NextcloudFile.search(context, query, nextcloudClient) }, }
async { OwncloudFile.search(context, query, owncloudClient) } files.addAll(localFiles)
).awaitAll().flatten() 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) { if (file.isDeletable) {
scope.launch { file.delete(context)
file.delete(context)
allFiles.value = allFiles.value?.filter { it != file }
}
} }
} }
} }

View File

@ -1,16 +1,16 @@
package de.mm20.launcher2.files package de.mm20.launcher2.files
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.data.File
import kotlinx.coroutines.launch
class FilesViewModel( class FilesViewModel(
private val filesRepository: FilesRepository private val filesRepository: FileRepository
): ViewModel() { ): ViewModel() {
val files = filesRepository.files
fun deleteFile(file: File) { fun deleteFile(file: File) {
filesRepository.deleteFile(file) viewModelScope.launch {
filesRepository.deleteFile(file)
}
} }
} }

View File

@ -5,6 +5,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val filesModule = module { val filesModule = module {
single { FilesRepository(androidContext(), get()) } single<FileRepository> { FileRepositoryImpl(androidContext(), get()) }
viewModel { FilesViewModel(get()) } viewModel { FilesViewModel(get()) }
} }

View File

@ -35,7 +35,7 @@ android {
} }
dependencies { dependencies {
implementation(libs.kotlin.stdlib) implementation(libs.bundles.kotlin)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)

View File

@ -5,16 +5,34 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MediatorLiveData
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.search.data.Searchable 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 * 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. * 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<List<String>> = AppDatabase.getInstance(context).searchDao().getHiddenItemKeys() val scope = CoroutineScope(Job() + Dispatchers.Main)
val hiddenItemsKeys = MutableStateFlow<List<String>>(emptyList())
fun isHidden(item: Searchable): LiveData<Boolean> { init {
scope.launch {
AppDatabase.getInstance(context).searchDao().getHiddenItemKeys().collectLatest {
hiddenItemsKeys.value = it
}
}
}
fun isHidden(item: Searchable): Flow<Boolean> {
return AppDatabase.getInstance(context).searchDao().isHidden(item.key) return AppDatabase.getInstance(context).searchDao().isHidden(item.key)
} }
} }

View File

@ -5,6 +5,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val hiddenItemsModule = module { val hiddenItemsModule = module {
single { HiddenItemsRepository(androidContext()) } single { HiddenItemsRepository(androidContext(), get()) }
viewModel { HiddenItemsViewModel(get()) } viewModel { HiddenItemsViewModel(get()) }
} }

View File

@ -19,6 +19,9 @@ fun View.asViewGroup(): ViewGroup? {
val View.lifecycleScope val View.lifecycleScope
get() = (context as LifecycleOwner).lifecycleScope get() = (context as LifecycleOwner).lifecycleScope
val View.lifecycleOwner
get() = (context as LifecycleOwner)
fun View.setPadding(vertical: Int, horizontal: Int) { fun View.setPadding(vertical: Int, horizontal: Int) {
setPadding(vertical, horizontal, vertical, horizontal) setPadding(vertical, horizontal, vertical, horizontal)
} }

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.preferences package de.mm20.launcher2.preferences
import android.app.Application import android.app.Application
import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager 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)) var gridColumnCount by IntPreference("grid_column_count", default = context.resources.getInteger(R.integer.config_columnCount))
fun doOnPreferenceChange(vararg keys: String, action: (String) -> Unit) { fun doOnPreferenceChange(vararg keys: String, action: (String) -> Unit): () -> Unit {
preferences.registerOnSharedPreferenceChangeListener { _, key -> val listener = { _: SharedPreferences, key: String ->
if (keys.contains(key)) action(key) if (keys.contains(key)) action(key)
} }
preferences.registerOnSharedPreferenceChangeListener(listener)
return {
preferences.unregisterOnSharedPreferenceChangeListener(listener)
}
} }
companion object { companion object {

View File

@ -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)
}

View File

@ -1,12 +1,9 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val searchModule = module { val searchModule = module {
single { SearchRepository() } single<WebsearchRepository> { WebsearchRepositoryImpl(get()) }
viewModel { SearchViewModel(get()) }
single { WebsearchRepository(androidContext()) }
viewModel { WebsearchViewModel(get()) } viewModel { WebsearchViewModel(get()) }
} }

View File

@ -22,9 +22,4 @@ class SearchRepository {
} }
} }
fun endSearch() {
synchronized(runningSearches) {
runningSearches--
}
}
} }

View File

@ -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<Boolean> = searchRepository.isSearching
fun search(query: String) {
searchRepository.currentQuery.value = query
}
}

View File

@ -1,54 +1,56 @@
package de.mm20.launcher2.search 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.database.AppDatabase
import de.mm20.launcher2.search.data.Websearch import de.mm20.launcher2.search.data.Websearch
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.delay import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
class WebsearchRepository(val context: Context) : BaseSearchableRepository() { interface WebsearchRepository {
fun search(query: String): Flow<List<Websearch>>
val websearches = MutableLiveData<List<Websearch>>(emptyList()) fun getWebsearches(): Flow<List<Websearch>>
val allWebsearches = MediatorLiveData<List<Websearch>>() fun insertWebsearch(websearch: Websearch)
fun deleteWebsearch(websearch: Websearch)
}
init { class WebsearchRepositoryImpl(
val databaseWebsearches = AppDatabase.getInstance(context).searchDao().getWebSearchesLiveData() private val database: AppDatabase
allWebsearches.addSource(databaseWebsearches) { ) : WebsearchRepository {
allWebsearches.value = it.map { Websearch(it) }
}
}
override suspend fun search(query: String) { private val scope = CoroutineScope(Job() + Dispatchers.Main)
override fun search(query: String): Flow<List<Websearch>> = channelFlow {
if (query.isEmpty()) { if (query.isEmpty()) {
websearches.value = emptyList() send(emptyList())
return return@channelFlow
} }
val searches = withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().getWebSearches().map { database.searchDao().getWebSearches().map {
Websearch(it, query) it.map { Websearch(it, query) }
} }
}.collectLatest {
send(it)
} }
websearches.value = searches
} }
fun insertWebsearch(websearch: Websearch) { override fun getWebsearches(): Flow<List<Websearch>> =
launch { database.searchDao().getWebSearches().map {
it.map { Websearch(it) }
}
override fun insertWebsearch(websearch: Websearch) {
scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().insertWebsearch(websearch.toDatabaseEntity()) database.searchDao().insertWebsearch(websearch.toDatabaseEntity())
} }
} }
} }
fun deleteWebsearch(websearch: Websearch) { override fun deleteWebsearch(websearch: Websearch) {
launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().deleteWebsearch(websearch.toDatabaseEntity()) database.searchDao().deleteWebsearch(websearch.toDatabaseEntity())
} }
} }
} }

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import de.mm20.launcher2.search.data.Websearch import de.mm20.launcher2.search.data.Websearch
class WebsearchViewModel( class WebsearchViewModel(
@ -16,7 +17,6 @@ class WebsearchViewModel(
websearchRepository.deleteWebsearch(websearch) websearchRepository.deleteWebsearch(websearch)
} }
val websearches = websearchRepository.websearches val allWebsearches = websearchRepository.getWebsearches().asLiveData()
val allWebsearches = websearchRepository.allWebsearches
} }

View File

@ -62,6 +62,10 @@ dependencyResolutionManagement {
listOf("kotlin.stdlib", "kotlinx.coroutines.core", "kotlinx.coroutines.android") 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") version("androidx.compose", "1.1.0-rc01")
alias("androidx.compose.runtime") alias("androidx.compose.runtime")
.to("androidx.compose.runtime", "runtime") .to("androidx.compose.runtime", "runtime")

View File

@ -24,6 +24,8 @@ android {
} }
} }
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
@ -48,6 +50,8 @@ android {
} }
dependencies { dependencies {
coreLibraryDesugaring(libs.desugar)
implementation(libs.bundles.kotlin) implementation(libs.bundles.kotlin)
implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.runtime)

View File

@ -16,8 +16,8 @@ import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale 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.IntOffset
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp 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.search.data.Searchable
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.theme.divider import de.mm20.launcher2.ui.theme.divider
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class) @OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@ -41,19 +41,19 @@ fun DefaultSwipeActions(
enabled: Boolean = true, enabled: Boolean = true,
content: @Composable RowScope.() -> Unit content: @Composable RowScope.() -> Unit
) { ) {
val viewModel: FavoritesViewModel = getViewModel() val repository: FavoritesRepository by inject()
val isPinned by viewModel.isPinned(item).observeAsState() val isPinned by repository.isPinned(item).collectAsState(false)
val isHidden by viewModel.isHidden(item).observeAsState() val isHidden by repository.isHidden(item).collectAsState(false)
val state = androidx.compose.material.rememberSwipeableState( val state = androidx.compose.material.rememberSwipeableState(
SwipeAction.Default, SwipeAction.Default,
confirmStateChange = { confirmStateChange = {
if (it == SwipeAction.Favorites) { if (it == SwipeAction.Favorites) {
if (isPinned == true) { if (isPinned == true) {
viewModel.unpinItem(item) repository.unpinItem(item)
} else { } else {
viewModel.pinItem(item) repository.pinItem(item)
} }
} }
false false

View File

@ -31,11 +31,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.ui.R 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.LocalNavController
import de.mm20.launcher2.ui.locals.LocalWindowSize import de.mm20.launcher2.ui.locals.LocalWindowSize
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
import org.koin.androidx.compose.viewModel
/** /**
* Search bar * Search bar
@ -54,7 +55,7 @@ fun SearchBar(
) { ) {
var searchQuery by remember { mutableStateOf("") } var searchQuery by remember { mutableStateOf("") }
val viewModel: SearchViewModel = getViewModel() val viewModel: SearchViewModel by viewModel()
LaunchedEffect(searchQuery) { LaunchedEffect(searchQuery) {
viewModel.search(searchQuery) viewModel.search(searchQuery)

View File

@ -14,11 +14,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.integerResource import androidx.compose.ui.res.integerResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
import org.koin.androidx.compose.inject
import kotlin.math.min import kotlin.math.min
@Composable @Composable
@ -165,8 +165,8 @@ data class ToggleToolbarAction(
@Composable @Composable
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction { fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel: FavoritesViewModel = getViewModel() val viewModel: FavoritesRepository by inject()
val isPinned by viewModel.isPinned(item).observeAsState(false) val isPinned by viewModel.isPinned(item).collectAsState(false)
return ToggleToolbarAction( return ToggleToolbarAction(
label = stringResource( label = stringResource(
@ -186,8 +186,8 @@ fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
@Composable @Composable
fun hideToolbarAction(item: Searchable): ToggleToolbarAction { fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel: FavoritesViewModel = getViewModel() val viewModel: FavoritesRepository by inject()
val isHidden by viewModel.isHidden(item).observeAsState(false) val isHidden by viewModel.isHidden(item).collectAsState(false)
return ToggleToolbarAction( return ToggleToolbarAction(
label = stringResource( label = stringResource(

View File

@ -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<FavoritesItem> {
return repository.getAllFavoriteItems()
}
fun saveFavorites(favorites: List<FavoritesItem>) {
repository.saveFavorites(favorites)
}
}

View File

@ -1,37 +1,28 @@
package de.mm20.launcher2.ui.legacy.component package de.mm20.launcher2.ui.launcher.modals
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.TextView import androidx.activity.viewModels
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity 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.FavoritesItem
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleScope import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.setPadding
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.DialogEditFavoritesBinding 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.databinding.EditFavoritesTitleBinding
import de.mm20.launcher2.ui.legacy.component.EditFavoritesRow
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
class EditFavoritesView @JvmOverloads constructor( class EditFavoritesView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) { ) : 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) private val binding = DialogEditFavoritesBinding.inflate(LayoutInflater.from(context), this)
@ -45,7 +36,7 @@ class EditFavoritesView @JvmOverloads constructor(
suspend fun initView() { suspend fun initView() {
favorites = withContext(Dispatchers.IO) { favorites = withContext(Dispatchers.IO) {
viewModel.getAllFavoriteItems().toMutableList() viewModel.getFavorites().toMutableList()
} }
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
binding.itemList.addView(getLabel(R.string.edit_favorites_dialog_stage0)) binding.itemList.addView(getLabel(R.string.edit_favorites_dialog_stage0))

View File

@ -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<List<Searchable>>(emptyList())
suspend fun onActive() {
withContext(Dispatchers.IO) {
repository.getHiddenItems().collectLatest {
hiddenItems.postValue(it)
}
}
}
}

View File

@ -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()
}
}

View File

@ -1,17 +1,116 @@
package de.mm20.launcher2.ui.launcher.search package de.mm20.launcher2.ui.launcher.search
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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.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<List<Application>>(emptyList())
val fileResults = MutableLiveData<List<File>>(emptyList())
val contactResults = MutableLiveData<List<Contact>>(emptyList())
val calendarResults = MutableLiveData<List<CalendarEvent>>(emptyList())
val wikipediaResult = MutableLiveData<Wikipedia?>(null)
val websiteResult = MutableLiveData<Website?>(null)
val calculatorResult = MutableLiveData<Calculator?>(null)
val unitConverterResult = MutableLiveData<UnitConverter?>(null)
val websearchResults = MutableLiveData<List<Websearch>>(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<Deferred<Any>>()
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<List<File>>()
val files: LiveData<List<File>> = _files
} }

View File

@ -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<List<CalendarEvent>>(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<CalendarEvent> = 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
}
}
}

View File

@ -14,7 +14,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Point import android.graphics.Point
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
@ -23,7 +22,6 @@ import android.view.*
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
@ -44,7 +42,6 @@ import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.jmedeisis.draglinearlayout.DragLinearLayout import com.jmedeisis.draglinearlayout.DragLinearLayout
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.icons.DynamicIconController import de.mm20.launcher2.icons.DynamicIconController
import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp 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.legacy.helper.ActivityStarter
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.transition.ChangingLayoutTransition import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.transition.OneShotLayoutTransition import de.mm20.launcher2.transition.OneShotLayoutTransition
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding 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.component.WidgetView
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper 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.weather.WeatherViewModel
import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType import de.mm20.launcher2.widgets.WidgetType
@ -79,37 +76,37 @@ class LauncherActivity : AppCompatActivity() {
* True if the search result list is visible * True if the search result list is visible
*/ */
private var searchVisibility = false private var searchVisibility = false
set(value) { set(value) {
field = value field = value
windowBackgroundBlur = value windowBackgroundBlur = value
} }
private lateinit var widgetHost: AppWidgetHost private lateinit var widgetHost: AppWidgetHost
private val widgets = mutableListOf<Widget>() private val widgets = mutableListOf<Widget>()
private lateinit var overlayView: ViewGroupOverlay private lateinit var overlayView: ViewGroupOverlay
private val searchViewModel: SearchViewModel by viewModel()
private val widgetViewModel: WidgetViewModel 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 val preferences = LauncherPreferences.instance
private var windowBackgroundBlur: Boolean = false private var windowBackgroundBlur: Boolean = false
set(value) { set(value) {
if(field == value) return if (field == value) return
field = value field = value
if (!isAtLeastApiLevel(31)) return if (!isAtLeastApiLevel(31)) return
window.attributes = window.attributes.also { window.attributes = window.attributes.also {
if (value) { if (value) {
it.blurBehindRadius = (32 * dp).toInt() it.blurBehindRadius = (32 * dp).toInt()
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
} else { } else {
it.blurBehindRadius = 0 it.blurBehindRadius = 0
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv() it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
}
} }
} }
}
private var widgetEditMode = false private var widgetEditMode = false
set(value) { set(value) {
@ -318,28 +315,11 @@ class LauncherActivity : AppCompatActivity() {
) )
} }
R.id.menu_item_hidden -> { R.id.menu_item_hidden -> {
val layout = NestedScrollView(this) val view = HiddenItemsView(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)
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT)) MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
.show { .show {
title(R.string.menu_hidden_items) title(R.string.menu_hidden_items)
customView(view = layout) customView(view = view)
negativeButton(R.string.close) { dismiss() } negativeButton(R.string.close) { dismiss() }
} }
//hiddenAppsActivated = true //hiddenAppsActivated = true
@ -584,8 +564,6 @@ class LauncherActivity : AppCompatActivity() {
ActivityStarter.create(binding.rootView) ActivityStarter.create(binding.rootView)
binding.activityStartOverlay.visibility = View.INVISIBLE binding.activityStartOverlay.visibility = View.INVISIBLE
val widgetViewModel by viewModels<WidgetViewModel>()
widgetViewModel.requestCalendarUpdate()
search(binding.searchBar.getSearchQuery()) search(binding.searchBar.getSearchQuery())
updateSystemBarAppearance() updateSystemBarAppearance()
@ -670,12 +648,8 @@ class LauncherActivity : AppCompatActivity() {
PermissionsManager.LOCATION -> { PermissionsManager.LOCATION -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this) ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
} }
PermissionsManager.CALENDAR -> {
widgetViewModel.requestCalendarUpdate()
}
PermissionsManager.ALL -> { PermissionsManager.ALL -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this) ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
widgetViewModel.requestCalendarUpdate()
search(binding.searchBar.getSearchQuery()) search(binding.searchBar.getSearchQuery())
} }
} }
@ -760,7 +734,8 @@ class LauncherActivity : AppCompatActivity() {
binding.container.translationY = binding.searchBar.height.toFloat() 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 if (binding.container.translationY == 0f) return@onTouch false
} }

View File

@ -6,12 +6,12 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.* import androidx.lifecycle.*
import de.mm20.launcher2.applications.AppViewModel
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.ui.databinding.ViewApplicationBinding 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 { class ApplicationView : FrameLayout {
@ -27,8 +27,8 @@ class ApplicationView : FrameLayout {
layoutTransition = LayoutTransition() layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING) layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
binding.applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) binding.applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val viewModel: AppViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
applications = viewModel.applications applications = viewModel.appResults
applications.observe(context as AppCompatActivity, Observer<List<Application>> { applications.observe(context as AppCompatActivity, Observer<List<Application>> {
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
binding.applicationGrid.submitItems(it) binding.applicationGrid.submitItems(it)

View File

@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -14,29 +15,29 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer 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.search.data.Calculator
import de.mm20.launcher2.ui.LegacyLauncherTheme import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.databinding.ViewCalculatorBinding 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.CalculatorItem
import de.mm20.launcher2.ui.search.UnitConverterItem
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.math.round
class CalculatorView : FrameLayout { class CalculatorView : FrameLayout {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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<Calculator?> private val calculator: LiveData<Calculator?>
private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true) private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true)
init { init {
val viewModel: CalculatorViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
calculator = viewModel.calculator calculator = viewModel.calculatorResult
calculator.observe(context as AppCompatActivity, Observer { calculator.observe(context as AppCompatActivity, Observer {
if (it == null) visibility = View.GONE if (it == null) visibility = View.GONE
else { else {

View File

@ -6,23 +6,26 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData 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.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.MissingPermission 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 de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class CalendarView : FrameLayout { class CalendarView : FrameLayout {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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<List<CalendarEvent>?> private val calendarEvents: LiveData<List<CalendarEvent>?>
@ -33,21 +36,27 @@ class CalendarView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list) val list = findViewById<SearchListView>(R.id.list)
val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
calendarEvents = viewModel.calendarEvents calendarEvents = viewModel.calendarResults
calendarEvents.observe(context as AppCompatActivity, { calendarEvents.observe(context as AppCompatActivity, {
if (it == null) { if (it == null) {
visibility = View.GONE visibility = View.GONE
return@observe 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 visibility = View.VISIBLE
list.submitItems(listOf( list.submitItems(
listOf(
MissingPermission( MissingPermission(
context.getString(R.string.permission_calendar_search), context.getString(R.string.permission_calendar_search),
PermissionsManager.CALENDAR PermissionsManager.CALENDAR
) )
)) )
)
return@observe return@observe
} }
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE visibility = if (it.isEmpty()) View.GONE else View.VISIBLE

View File

@ -6,24 +6,28 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.SearchView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.contacts.ContactViewModel
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Contact import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.search.SearchListView import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class ContactView : FrameLayout { class ContactView : FrameLayout {
private val contacts: LiveData<List<Contact>?> private val contacts: LiveData<List<Contact>?>
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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 { init {
View.inflate(context, R.layout.view_search_category_list, this) View.inflate(context, R.layout.view_search_category_list, this)
@ -31,22 +35,28 @@ class ContactView : FrameLayout {
layoutTransition.enableTransitionType(LayoutTransition.CHANGING) layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val viewModel: ContactViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
contacts = viewModel.contacts contacts = viewModel.contactResults
val list = findViewById<SearchListView>(R.id.list) val list = findViewById<SearchListView>(R.id.list)
contacts.observe(context as AppCompatActivity, { contacts.observe(context as AppCompatActivity, {
if (it == null) { if (it == null) {
visibility = View.GONE visibility = View.GONE
return@observe 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 visibility = View.VISIBLE
list.submitItems(listOf( list.submitItems(
listOf(
MissingPermission( MissingPermission(
context.getString(R.string.permission_contact_search), context.getString(R.string.permission_contact_search),
PermissionsManager.CONTACTS PermissionsManager.CONTACTS
) )
)) )
)
return@observe return@observe
} }
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE visibility = if (it.isEmpty()) View.GONE else View.VISIBLE

View File

@ -6,14 +6,10 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity 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 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 { class FavoritesView : FrameLayout {
@ -21,18 +17,21 @@ class FavoritesView : FrameLayout {
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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 favorites: LiveData<List<Searchable>>
private val binding = ViewFavoritesBinding.inflate(LayoutInflater.from(context), this, true) private val binding = ViewFavoritesBinding.inflate(LayoutInflater.from(context), this, true)
init { init {
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
favorites = viewModel.getFavorites(LauncherPreferences.instance.gridColumnCount) val favorites = viewModel.favorites
favorites.observe(context as AppCompatActivity, Observer { val hide = viewModel.hideFavorites
visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE favorites.observe(context as AppCompatActivity) {
visibility = if (it?.isEmpty() == true || hide.value == true) View.GONE else View.VISIBLE
binding.favoritesGrid.submitItems(it) binding.favoritesGrid.submitItems(it)
}) }
hide.observe(context as AppCompatActivity) {
visibility = if(it == true) View.GONE else View.VISIBLE
}
layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) } layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
binding.favoritesCard.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) } binding.favoritesCard.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }

View File

@ -6,22 +6,26 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import de.mm20.launcher2.files.FilesViewModel
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.MissingPermission import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.search.SearchListView import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class FileView : FrameLayout { class FileView : FrameLayout {
private val files: LiveData<List<File>?> private val files: LiveData<List<File>?>
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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 { init {
View.inflate(context, R.layout.view_search_category_list, this) View.inflate(context, R.layout.view_search_category_list, this)
@ -30,21 +34,27 @@ class FileView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list) val list = findViewById<SearchListView>(R.id.list)
val viewModel: FilesViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
files = viewModel.files files = viewModel.fileResults
files.observe(context as AppCompatActivity, { files.observe(context as AppCompatActivity, {
if (it == null) { if (it == null) {
visibility = View.GONE visibility = View.GONE
return@observe return@observe
} }
if (it.isEmpty() && !PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) { if (it.isEmpty() && !PermissionsManager.checkPermission(
context,
PermissionsManager.EXTERNAL_STORAGE
)
) {
visibility = View.VISIBLE visibility = View.VISIBLE
list.submitItems(listOf( list.submitItems(
listOf(
MissingPermission( MissingPermission(
context.getString(R.string.permission_files_search), context.getString(R.string.permission_files_search),
PermissionsManager.EXTERNAL_STORAGE PermissionsManager.EXTERNAL_STORAGE
) )
)) )
)
return@observe return@observe
} }
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE visibility = if (it.isEmpty()) View.GONE else View.VISIBLE

View File

@ -12,21 +12,17 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.animation.AccelerateInterpolator import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import androidx.lifecycle.Observer
import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable import com.airbnb.lottie.LottieDrawable
import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.SearchStyles import de.mm20.launcher2.preferences.SearchStyles
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.transition.ChangingLayoutTransition import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewSearchBarBinding import de.mm20.launcher2.ui.databinding.ViewSearchBarBinding
import de.mm20.launcher2.ui.legacy.view.LauncherCardView import de.mm20.launcher2.ui.legacy.view.LauncherCardView
import org.koin.androidx.viewmodel.ext.android.viewModel
class SearchBar @JvmOverloads constructor( class SearchBar @JvmOverloads constructor(
context: Context, context: Context,
@ -72,12 +68,6 @@ class SearchBar @JvmOverloads constructor(
}) })
val viewModel = (context as AppCompatActivity).viewModel<SearchViewModel>().value
viewModel.isSearching.observe(context, Observer {
binding.searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE
})
binding.overflowMenu.setOnClickListener { binding.overflowMenu.setOnClickListener {
if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it) if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it)
else (setSearchQuery("")) else (setSearchQuery(""))

View File

@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -17,9 +18,8 @@ import androidx.lifecycle.Observer
import de.mm20.launcher2.search.data.UnitConverter import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.ui.LegacyLauncherTheme import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.databinding.ViewUnitconverterBinding 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.ui.search.UnitConverterItem
import de.mm20.launcher2.unitconverter.UnitConverterViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class UnitConverterView : FrameLayout { class UnitConverterView : FrameLayout {
@ -36,8 +36,8 @@ class UnitConverterView : FrameLayout {
private val binding = ViewUnitconverterBinding.inflate(LayoutInflater.from(context), this, true) private val binding = ViewUnitconverterBinding.inflate(LayoutInflater.from(context), this, true)
init { init {
val unitConverterViewModel by (context as AppCompatActivity).viewModel<UnitConverterViewModel>() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
unitConverter = unitConverterViewModel.unitConverter unitConverter = viewModel.unitConverterResult
unitConverter.observe(context as AppCompatActivity, Observer { unitConverter.observe(context as AppCompatActivity, Observer {
if (it == null) visibility = View.GONE if (it == null) visibility = View.GONE
else { else {

View File

@ -7,6 +7,7 @@ import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -17,24 +18,27 @@ import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.legacy.helper.ActivityStarter import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.WebsearchViewModel
import de.mm20.launcher2.search.data.Websearch import de.mm20.launcher2.search.data.Websearch
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewWebsearchBinding 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 { class WebSearchView : FrameLayout {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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<List<Websearch>> private val websearches: LiveData<List<Websearch>>
private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true) private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true)
init { init {
val viewModel: WebsearchViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
websearches = viewModel.websearches websearches = viewModel.websearchResults
websearches.observe(context as AppCompatActivity, Observer { websearches.observe(context as AppCompatActivity, Observer {
updateWebsearches(it) updateWebsearches(it)
}) })
@ -48,13 +52,16 @@ class WebSearchView : FrameLayout {
chip.text = search.label chip.text = search.label
if (search.icon != null) { if (search.icon != null) {
Glide.with(context) Glide.with(context)
.load(search.icon) .load(search.icon)
.into(object : SimpleTarget<Drawable>() { .into(object : SimpleTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { override fun onResourceReady(
chip.chipIcon = resource resource: Drawable,
} transition: Transition<in Drawable>?
) {
chip.chipIcon = resource
}
}) })
chip.chipIconTint = null chip.chipIconTint = null
} else { } else {
chip.chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_search) chip.chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_search)
@ -63,7 +70,8 @@ class WebSearchView : FrameLayout {
} }
chip.chipStrokeWidth = 1 * dp chip.chipStrokeWidth = 1 * dp
chip.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke) 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.setTextAppearanceResource(R.style.ChipTextAppearance)
chip.setOnClickListener { chip.setOnClickListener {
ActivityStarter.start(context, chip, intent = search.getLaunchIntent()) ActivityStarter.start(context, chip, intent = search.getLaunchIntent())

View File

@ -5,34 +5,38 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.legacy.helper.ActivityStarter import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Website import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.ui.R 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.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.websites.WebsiteViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class WebsiteView : FrameLayout { class WebsiteView : FrameLayout {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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<Website?> private val website: LiveData<Website?>
init { init {
View.inflate(context, R.layout.view_search_category_single_item, this) View.inflate(context, R.layout.view_search_category_single_item, this)
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST) 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<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
websiteView.layoutParams = params websiteView.layoutParams = params
card.addView(websiteView) card.addView(websiteView)
val viewModel: WebsiteViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
website = viewModel.website website = viewModel.websiteResult
website.observe(context as AppCompatActivity, Observer { website.observe(context as AppCompatActivity, Observer {
visibility = if (it == null) View.GONE else View.VISIBLE visibility = if (it == null) View.GONE else View.VISIBLE
card.setOnClickListener { _ -> card.setOnClickListener { _ ->

View File

@ -5,35 +5,38 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.legacy.helper.ActivityStarter import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Wikipedia import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.R 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.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.wikipedia.WikipediaViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class WikipediaView : FrameLayout { class WikipediaView : FrameLayout {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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<Wikipedia?> val wikipedia: LiveData<Wikipedia?>
init { init {
View.inflate(context, R.layout.view_search_category_single_item, this) View.inflate(context, R.layout.view_search_category_single_item, this)
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST) 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<ViewGroup>(R.id.card) val card = findViewById<ViewGroup>(R.id.card)
websiteView.layoutParams = params websiteView.layoutParams = params
card.addView(websiteView) card.addView(websiteView)
val viewModel: WikipediaViewModel by (context as AppCompatActivity).viewModel() val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
wikipedia = viewModel.wikipedia wikipedia = viewModel.wikipediaResult
wikipedia.observe(context as AppCompatActivity, Observer { wikipedia.observe(context as AppCompatActivity, {
visibility = if (it == null) View.GONE else View.VISIBLE visibility = if (it == null) View.GONE else View.VISIBLE
card.setOnClickListener { _ -> card.setOnClickListener { _ ->
ActivityStarter.start(context, websiteView, item = it) ActivityStarter.start(context, websiteView, item = it)

View File

@ -22,7 +22,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
object ActivityStarter: KoinComponent { object ActivityStarter : KoinComponent {
val favoritesRepository: FavoritesRepository by inject() val favoritesRepository: FavoritesRepository by inject()
@ -46,13 +46,20 @@ object ActivityStarter: KoinComponent {
initialized = true 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 (!initialized) throw IllegalStateException("Item starter has not been initialized properly.")
if (!startActivity(context, item, intent, pendingIntent, transitionView)) return false if (!startActivity(context, item, intent, pendingIntent, transitionView)) return false
if (animationStyle == AppStartAnimation.SLIDE_BOTTOM || animationStyle == AppStartAnimation.FADE || if (animationStyle == AppStartAnimation.SLIDE_BOTTOM || animationStyle == AppStartAnimation.FADE ||
animationStyle == AppStartAnimation.M) { animationStyle == AppStartAnimation.M
) {
return true return true
} }
@ -80,28 +87,28 @@ object ActivityStarter: KoinComponent {
AnimatorSet().apply { AnimatorSet().apply {
playTogether( playTogether(
ViewPropertyObjectAnimator.animate(background) ViewPropertyObjectAnimator.animate(background)
.scaleX(1f) .scaleX(1f)
.scaleY(1f) .scaleY(1f)
.translationX(0f) .translationX(0f)
.translationY(0f) .translationY(0f)
.setDuration(200) .setDuration(200)
.setInterpolator(AccelerateInterpolator(0.8f)) .setInterpolator(AccelerateInterpolator(0.8f))
.get(), .get(),
ViewPropertyObjectAnimator.animate(transitionView) ViewPropertyObjectAnimator.animate(transitionView)
.scaleX(scale) .scaleX(scale)
.scaleY(scale) .scaleY(scale)
.alpha(0f) .alpha(0f)
.translationX(x) .translationX(x)
.translationY(y) .translationY(y)
.setDuration(200) .setDuration(200)
.setInterpolator(AccelerateInterpolator(0.8f)) .setInterpolator(AccelerateInterpolator(0.8f))
.get(), .get(),
ViewPropertyObjectAnimator.animate(searchView) ViewPropertyObjectAnimator.animate(searchView)
.scaleX(0.8f) .scaleX(0.8f)
.scaleY(0.8f) .scaleY(0.8f)
.alpha(0f) .alpha(0f)
.get() .get()
) )
}.start() }.start()
onResumeCallback = { onResumeCallback = {
@ -127,10 +134,17 @@ object ActivityStarter: KoinComponent {
private var onResumeCallback: (() -> Unit)? = null 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) val pos = intArrayOf(0, 0)
sourceView.getLocationOnScreen(pos) 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() val bundle = getActivityOptions(context, sourceView, sourceBounds)?.toBundle()
@ -145,7 +159,7 @@ object ActivityStarter: KoinComponent {
if (item != null) { if (item != null) {
if (item.launch(context, bundle)) { if (item.launch(context, bundle)) {
favoritesRepository.incrementLaunchCount(item) favoritesRepository.incrementLaunchCounter(item)
return true return true
} }
return false 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) { return when (animationStyle) {
AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_fade_enter, R.anim.activity_start_fade_exit) AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation(
AppStartAnimation.SLIDE_BOTTOM -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_slide_bottom_enter, R.anim.activity_start_slide_bottom_exit) context,
AppStartAnimation.M -> sourceBounds?.let { ActivityOptionsCompat.makeClipRevealAnimation(sourceView, 0, 0, sourceView.width, sourceView.height) } R.anim.activity_start_fade_enter,
else -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_splash2_enter, R.anim.activity_start_splash2_exit) 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
)
} }
} }

View File

@ -28,14 +28,13 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.alpha import androidx.core.graphics.alpha
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.*
import androidx.lifecycle.Observer
import androidx.transition.Scene import androidx.transition.Scene
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import de.mm20.launcher2.badges.BadgeProvider import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.crashreporter.CrashReporter 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.icons.IconRepository
import de.mm20.launcher2.ktx.castToOrNull import de.mm20.launcher2.ktx.castToOrNull
import de.mm20.launcher2.ktx.dp 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.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.* import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -83,9 +80,10 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
shape = LauncherIconView.getDefaultShape(context) shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(application) icon = iconRepository.getIconIfCached(application)
lifecycleScope.launch { lifecycleScope.launch {
iconRepository.getIcon(application, (84 * rootView.dp).toInt()).collectLatest { iconRepository.getIcon(application, (84 * rootView.dp).toInt())
icon = it .collectLatest {
} icon = it
}
} }
} }
findViewById<SwipeCardView>(R.id.appCard).also { findViewById<SwipeCardView>(R.id.appCard).also {
@ -275,7 +273,7 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
if (launcherApps.hasShortcutHostPermission()) { if (launcherApps.hasShortcutHostPermission()) {
val shortcuts = app.shortcuts val shortcuts = app.shortcuts
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() val repository: FavoritesRepository by inject()
var count = 0 var count = 0
for (si in shortcuts) { for (si in shortcuts) {
@ -308,7 +306,7 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
R.color.text_color_primary R.color.text_color_primary
) )
) )
val isPinned = viewModel.isPinned(si) val isPinned = repository.isPinned(si).asLiveData()
isPinned.observe(context as LifecycleOwner, Observer { isPinned.observe(context as LifecycleOwner, Observer {
view.isCloseIconVisible = isPinned.value == true view.isCloseIconVisible = isPinned.value == true
@ -320,14 +318,14 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
} }
view.setOnLongClickListener { view.setOnLongClickListener {
if (isPinned.value == true) { if (isPinned.value == true) {
viewModel.unpinItem(si) repository.unpinItem(si)
} else { } else {
viewModel.pinItem(si) repository.pinItem(si)
} }
true true
} }
view.setOnCloseIconClickListener { view.setOnCloseIconClickListener {
viewModel.unpinItem(si) repository.unpinItem(si)
view.isCloseIconVisible = false view.isCloseIconVisible = false
} }
appShortcuts.addView(view) appShortcuts.addView(view)

View File

@ -14,15 +14,17 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.google.android.material.card.MaterialCardView 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.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.transition.ChangingLayoutTransition import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R 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 import kotlin.math.abs
class SwipeCardView @JvmOverloads constructor( class SwipeCardView @JvmOverloads constructor(
@ -376,10 +378,12 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
R.drawable.ic_star_solid, R.drawable.ic_star_solid,
ContextCompat.getColor(context, R.color.amber), ContextCompat.getColor(context, R.color.amber),
{ false } { false }
) { ), KoinComponent {
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
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 { init {
@ -392,7 +396,7 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
if (pinned) { if (pinned) {
icon = R.drawable.ic_star_outline icon = R.drawable.ic_star_outline
action = { action = {
viewModel.unpinItem( repository.unpinItem(
searchable searchable
) )
false false
@ -400,7 +404,7 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
} else { } else {
icon = R.drawable.ic_star_solid icon = R.drawable.ic_star_solid
action = { action = {
viewModel.pinItem( repository.pinItem(
searchable searchable
) )
false false
@ -413,9 +417,12 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
R.drawable.ic_visibility_off, R.drawable.ic_visibility_off,
ContextCompat.getColor(context, R.color.blue), ContextCompat.getColor(context, R.color.blue),
{ false } { false }
) { ), KoinComponent {
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
private val hidden = viewModel.isHidden(searchable) private val repository: FavoritesRepository by inject()
private val hidden = repository.isPinned(searchable)
.asLiveData((context as AppCompatActivity).lifecycleScope.coroutineContext)
init { init {
hidden.observe(context as LifecycleOwner) { hidden.observe(context as LifecycleOwner) {
@ -427,7 +434,7 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
if (hidden) { if (hidden) {
icon = R.drawable.ic_visibility icon = R.drawable.ic_visibility
action = { action = {
viewModel.unhideItem( repository.unhideItem(
searchable searchable
) )
true true
@ -435,7 +442,7 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
} else { } else {
icon = R.drawable.ic_visibility_off icon = R.drawable.ic_visibility_off
action = { action = {
viewModel.hideItem( repository.hideItem(
searchable searchable
) )
true true

View File

@ -6,16 +6,18 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.setPadding import androidx.core.view.setPadding
import androidx.lifecycle.Observer import androidx.lifecycle.*
import de.mm20.launcher2.favorites.FavoritesViewModel import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R 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 { class ToolbarView : LinearLayout {
constructor(context: Context) : super(context) 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( class FavoriteToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
R.drawable.ic_star_outline, R.drawable.ic_star_outline,
context.getString(R.string.favorites_menu_pin) context.getString(R.string.favorites_menu_pin)
) { ), KoinComponent {
private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() private val repository: FavoritesRepository by inject()
private val isPinned = viewModel.isPinned(item) private var isPinned = false
set(value) {
init { field = value
isPinned.observe(context as AppCompatActivity, Observer { if (value) {
it ?: return@Observer
if (it) {
title = context.getString(R.string.favorites_menu_unpin) title = context.getString(R.string.favorites_menu_unpin)
icon = R.drawable.ic_star_solid icon = R.drawable.ic_star_solid
} else { } else {
title = context.getString(R.string.favorites_menu_pin) title = context.getString(R.string.favorites_menu_pin)
icon = R.drawable.ic_star_outline icon = R.drawable.ic_star_outline
} }
}) }
init {
clickAction = { clickAction = {
if (isPinned.value == true) { if (isPinned) {
viewModel.unpinItem(item) repository.unpinItem(item)
} else { } 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( class VisibilityToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
R.drawable.ic_visibility, R.drawable.ic_visibility,
context.getString(R.string.menu_hide) context.getString(R.string.menu_hide)
) { ), KoinComponent {
private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() private val repository: FavoritesRepository by inject()
private val isHidden = viewModel.isHidden(item) private var isHidden = false
set(value) {
init { field = value
isHidden.observe(context as AppCompatActivity, Observer { if (value) {
if (it) {
title = context.getString(R.string.menu_unhide) title = context.getString(R.string.menu_unhide)
icon = R.drawable.ic_visibility icon = R.drawable.ic_visibility
} else { } else {
title = context.getString(R.string.menu_hide) title = context.getString(R.string.menu_hide)
icon = R.drawable.ic_visibility_off icon = R.drawable.ic_visibility_off
} }
}) }
init {
clickAction = { clickAction = {
if (isHidden.value == true) { if (isHidden) {
viewModel.unhideItem(item) repository.unhideItem(item)
} else { } else {
viewModel.hideItem(item) repository.hideItem(item)
}
}
(context as LifecycleOwner).apply {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
repository.isHidden(item).collectLatest {
isHidden = it
}
}
} }
} }
} }
} }

View File

@ -1,145 +1,113 @@
package de.mm20.launcher2.ui.legacy.widget package de.mm20.launcher2.ui.legacy.widget
import android.animation.LayoutTransition import android.animation.LayoutTransition
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.Intent
import android.provider.CalendarContract
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.View import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData import androidx.lifecycle.Lifecycle
import de.mm20.launcher2.calendar.CalendarViewModel import androidx.lifecycle.repeatOnLifecycle
import de.mm20.launcher2.favorites.FavoritesViewModel import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.legacy.helper.ActivityStarter import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.CalendarEvent 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.search.data.Searchable
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewCalendarWidgetBinding import de.mm20.launcher2.ui.databinding.ViewCalendarWidgetBinding
import org.koin.androidx.viewmodel.ext.android.viewModel import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidgetVM
import java.util.* import de.mm20.launcher2.ui.legacy.data.InformationText
import kotlin.math.max import kotlinx.coroutines.launch
import kotlin.math.min import java.time.LocalDate
import java.time.ZoneId
class CalendarWidget : LauncherWidget { class CalendarWidget : LauncherWidget {
override val canResize: Boolean override val canResize: Boolean
get() = false get() = false
private val calendarEvents: LiveData<List<CalendarEvent>>
private val pinnedCalendarEvents: LiveData<List<CalendarEvent>>
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<Long> = listOf(0L)
set(value) {
if (value.indexOf(selectedDay) == -1) selectedDay = 0L
field = value
}
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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 { private fun formatDay(day: LocalDate): String {
return when (day) { val today = LocalDate.now()
0L -> context.getString(R.string.date_today) return when {
1L -> context.getString(R.string.date_tomorrow) today == day -> context.getString(R.string.date_today)
else -> DateUtils.formatDateTime(context, (getToday() + day) * (1000 * 60 * 60 * 24), DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY) 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 { init {
clipToPadding = false clipToPadding = false
clipChildren = false clipChildren = false
lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onActive()
}
}
binding.calendarNewEvent.setOnClickListener { binding.calendarNewEvent.setOnClickListener {
val intent = Intent(Intent.ACTION_EDIT) viewModel.createEvent(context)
intent.data = CalendarContract.Events.CONTENT_URI
ActivityStarter.start(context, this, intent = intent)
} }
binding.calendarDate.setOnClickListener { binding.calendarDate.setOnClickListener {
val menu = PopupMenu(context, binding.calendarDate) val menu = PopupMenu(context, binding.calendarDate)
for (d in availableDays) { val availableDates = viewModel.availableDates
for ((i, d) in availableDates.withIndex()) {
menu.menu.add( menu.menu.add(
Menu.NONE, Menu.NONE,
d.toInt(), i,
Menu.NONE, Menu.NONE,
formatDay(d) formatDay(d)
) )
} }
menu.setOnMenuItemClickListener { menu.setOnMenuItemClickListener {
selectedDay = it.itemId.toLong() viewModel.selectDate(availableDates[it.itemId])
true true
} }
menu.show() menu.show()
} }
binding.calendarOpenApp.setOnClickListener { binding.calendarOpenApp.setOnClickListener {
val startMillis = System.currentTimeMillis() viewModel.openCalendarApp(context)
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)
} }
binding.calendarDateNext.setOnClickListener { binding.calendarDateNext.setOnClickListener {
val i = min(availableDays.lastIndex - 1, availableDays.indexOf(selectedDay)) viewModel.nextDay()
selectedDay = availableDays[i + 1]
} }
binding.calendarDatePrev.setOnClickListener { binding.calendarDatePrev.setOnClickListener {
val i = max(1, availableDays.indexOf(selectedDay)) viewModel.previousDay()
selectedDay = availableDays[i - 1]
} }
val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel() val calendarEvents = viewModel.calendarEvents
val favViewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel() val pinnedCalendarEvents = viewModel.pinnedCalendarEvents
calendarEvents = viewModel.upcomingCalendarEvents val hiddenPastEvents = viewModel.hiddenPastEvents
pinnedCalendarEvents = favViewModel.pinnedCalendarEvents val selectedDate = viewModel.selectedDate
calendarEvents.observe(context as AppCompatActivity, { calendarEvents.observe(context as AppCompatActivity, {
if (!PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) { updateEventList(it, hiddenPastEvents.value ?: 0)
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()
}) })
pinnedCalendarEvents.observe(context as AppCompatActivity) { pinnedCalendarEvents.observe(context as AppCompatActivity) {
val today = getToday() binding.calendarWidgetPinnedList.submitItems(it)
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 })
if (it.isEmpty()) { if (it.isEmpty()) {
binding.calendarWidgetPinnedList.visibility = View.GONE binding.calendarWidgetPinnedList.visibility = View.GONE
binding.calendarUpcomingEventsTitle.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 { binding.calendarWidgetRoot.layoutTransition = LayoutTransition().apply {
enableTransitionType(LayoutTransition.CHANGING) enableTransitionType(LayoutTransition.CHANGING)
} }
} }
private fun getToday(): Long { private fun updateEventList(
return (System.currentTimeMillis() + zoneOffset) / (1000 * 60 * 60 * 24) events: List<CalendarEvent>,
} hiddenPastDayEvents: Int
) {
private fun updateEventList(includePastEvents: Boolean = false) { val items = events.toMutableList<Searchable>()
val today = getToday()
val events: MutableList<Searchable> = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay || (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay }
?.toMutableList() ?: mutableListOf()
if (events.isEmpty()) { if (events.isEmpty()) {
events.add( items.add(
InformationText(context.getString(R.string.calendar_widget_no_events)) 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 (hiddenPastDayEvents > 0) {
if (includePastEvents) { items.add(
events.addAll(pastEvents) InformationText(
} else { resources.getQuantityString(
events.add(InformationText(resources.getQuantityString(R.plurals.calendar_widget_running_events, pastEvents.size, pastEvents.size)) { R.plurals.calendar_widget_running_events,
updateEventList(true) hiddenPastDayEvents,
hiddenPastDayEvents
)
) {
viewModel.showAllEvents()
}) })
}
} }
binding.calendarWidgetList.submitItems(events) binding.calendarWidgetList.submitItems(items)
} }

View File

@ -6,13 +6,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.applications.AppViewModel import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import org.koin.androidx.compose.getViewModel
@Composable @Composable
fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit { fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit {
val viewModel: AppViewModel = getViewModel() val viewModel: SearchViewModel by viewModel()
val apps by viewModel.applications.observeAsState(emptyList()) val apps by viewModel.appResults.observeAsState(emptyList())
return { return {
SearchableGrid(items = apps, listState = it) SearchableGrid(items = apps, listState = it)
} }

View File

@ -1,30 +1,19 @@
package de.mm20.launcher2.ui.search 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.foundation.lazy.LazyListScope
import androidx.compose.material.Card 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.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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 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 de.mm20.launcher2.ui.component.SectionDivider
import org.koin.androidx.compose.getViewModel import de.mm20.launcher2.ui.launcher.search.SearchViewModel
@Composable @Composable
fun calculatorItem(): LazyListScope.() -> Unit { fun calculatorItem(): LazyListScope.() -> Unit {
val viewModel: CalculatorViewModel = getViewModel() val viewModel: SearchViewModel by viewModel()
val calculator by viewModel.calculator.observeAsState() val calculator by viewModel.calculatorResult.observeAsState(null)
return { return {
calculator?.let { calculator?.let {
item { item {

View File

@ -6,14 +6,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.favorites.FavoritesViewModel import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import org.koin.androidx.compose.getViewModel
@Composable @Composable
fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit { 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 { return {
SearchableGrid(items = favorites, listState = it) SearchableGrid(items = favorites, listState = it)
} }

View File

@ -5,13 +5,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.files.FilesViewModel import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import org.koin.androidx.compose.getViewModel
@Composable @Composable
fun fileResults(): LazyListScope.() -> Unit { fun fileResults(): LazyListScope.() -> Unit {
val viewModel: FilesViewModel = getViewModel() val viewModel: SearchViewModel by viewModel()
val files by viewModel.files.observeAsState(emptyList()) val files by viewModel.fileResults.observeAsState(emptyList())
return { return {
files?.let { SearchableList(items = it) } files?.let { SearchableList(items = it) }
} }

View File

@ -7,13 +7,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.component.SectionDivider import de.mm20.launcher2.ui.component.SectionDivider
import de.mm20.launcher2.wikipedia.WikipediaViewModel import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.viewModel
@Composable @Composable
fun wikipediaResult(): LazyListScope.() -> Unit { fun wikipediaResult(): LazyListScope.() -> Unit {
val viewModel: WikipediaViewModel = getViewModel() val viewModel: SearchViewModel by viewModel()
val wikipedia by viewModel.wikipedia.observeAsState() val wikipedia by viewModel.wikipediaResult.observeAsState()
return { return {
wikipedia?.let { wikipedia?.let {
item { item {

View File

@ -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
)
}
}
}

View File

@ -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<Searchable>, 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()
)
}
}
}
}
}

View File

@ -1,259 +1,8 @@
package de.mm20.launcher2.ui.widget package de.mm20.launcher2.ui.widget
import android.content.Context import androidx.compose.runtime.Composable
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
@Composable @Composable
fun CalendarWidget() { 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<Long> = 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<Searchable>()
}
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<Long>,
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
}
} }

View File

@ -9,9 +9,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.ui.component.TextClock import de.mm20.launcher2.ui.component.TextClock
import org.koin.androidx.compose.getViewModel
@Composable @Composable
fun DatePart() { fun DatePart() {

View File

@ -7,6 +7,5 @@ import org.koin.dsl.module
val unitConverterModule = module { val unitConverterModule = module {
single { CurrencyRepository(androidContext()) } single { CurrencyRepository(androidContext()) }
single { UnitConverterRepository(androidContext()) } single<UnitConverterRepository> { UnitConverterRepositoryImpl(androidContext()) }
viewModel { UnitConverterViewModel(get()) }
} }

View File

@ -2,15 +2,19 @@ package de.mm20.launcher2.unitconverter
import android.content.Context import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.currencies.CurrencyRepository
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.UnitConverter 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<UnitConverter?>
}
class UnitConverterRepositoryImpl(val context: Context) : UnitConverterRepository {
val unitConverter = MutableLiveData<UnitConverter?>() val unitConverter = MutableLiveData<UnitConverter?>()
override suspend fun search(query: String) { override fun search(query: String): Flow<UnitConverter?> = channelFlow {
unitConverter.value = UnitConverter.search(context, query) send(UnitConverter.search(context, query))
} }
} }

View File

@ -1,9 +0,0 @@
package de.mm20.launcher2.unitconverter
import androidx.lifecycle.ViewModel
class UnitConverterViewModel(
unitConverterRepository: UnitConverterRepository
): ViewModel() {
val unitConverter = unitConverterRepository.unitConverter
}

View File

@ -5,8 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val websitesModule = module { val websitesModule = module {
single { WebsiteRepository(androidContext()) } single<WebsiteRepository> { WebsiteRepositoryImpl(androidContext()) }
viewModel {
WebsiteViewModel(get())
}
} }

View File

@ -1,17 +1,19 @@
package de.mm20.launcher2.websites package de.mm20.launcher2.websites
import android.content.Context import android.content.Context
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.Website import de.mm20.launcher2.search.data.Website
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class WebsiteRepository(val context: Context) : BaseSearchableRepository() { interface WebsiteRepository {
fun search(query: String): Flow<Website?>
}
val website = MutableLiveData<Website?>() class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository {
private val httpClient = OkHttpClient private val httpClient = OkHttpClient
.Builder() .Builder()
@ -20,8 +22,8 @@ class WebsiteRepository(val context: Context) : BaseSearchableRepository() {
.writeTimeout(1000, TimeUnit.MILLISECONDS) .writeTimeout(1000, TimeUnit.MILLISECONDS)
.build() .build()
override fun onCancel() { override fun search(query: String): Flow<Website?> = channelFlow {
super.onCancel() send(null)
httpClient.dispatcher.run { httpClient.dispatcher.run {
runningCalls().forEach { runningCalls().forEach {
it.cancel() it.cancel()
@ -30,14 +32,10 @@ class WebsiteRepository(val context: Context) : BaseSearchableRepository() {
it.cancel() it.cancel()
} }
} }
} if (query.isBlank()) return@channelFlow
val website = withContext(Dispatchers.IO) {
override suspend fun search(query: String) {
website.value = null
if (query.isBlank()) return
val wiki = withContext(Dispatchers.IO) {
Website.search(context, query, httpClient) Website.search(context, query, httpClient)
} }
website.value = wiki send(website)
} }
} }

View File

@ -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<Website?> = websiteRepository.website
}

View File

@ -6,5 +6,5 @@ import org.koin.dsl.module
val widgetsModule = module { val widgetsModule = module {
single { WidgetRepository(androidContext()) } single { WidgetRepository(androidContext()) }
viewModel { WidgetViewModel(get(), get()) } viewModel { WidgetViewModel(get()) }
} }

View File

@ -2,14 +2,12 @@ package de.mm20.launcher2.widgets
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.calendar.CalendarRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class WidgetViewModel( class WidgetViewModel(
private val widgetRepository: WidgetRepository, private val widgetRepository: WidgetRepository
private val calendarRepository: CalendarRepository
) : ViewModel() { ) : ViewModel() {
@ -29,7 +27,4 @@ class WidgetViewModel(
return widgetRepository.getInternalWidgets() return widgetRepository.getInternalWidgets()
} }
fun requestCalendarUpdate() {
calendarRepository.requestCalendarUpdate()
}
} }

View File

@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val wikipediaModule = module { val wikipediaModule = module {
single { WikipediaRepository(androidContext()) } single<WikipediaRepository> { WikipediaRepositoryImpl(androidContext()) }
viewModel { WikipediaViewModel(get()) }
} }

View File

@ -1,19 +1,23 @@
package de.mm20.launcher2.wikipedia package de.mm20.launcher2.wikipedia
import android.content.Context import android.content.Context
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.Wikipedia import de.mm20.launcher2.search.data.Wikipedia
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class WikipediaRepository(val context: Context) : BaseSearchableRepository() { interface WikipediaRepository {
fun search(query: String): Flow<Wikipedia?>
}
val wikipedia = MutableLiveData<Wikipedia?>() class WikipediaRepositoryImpl(
private val context: Context
): WikipediaRepository {
private val httpClient by lazy { private val httpClient by lazy {
OkHttpClient OkHttpClient
@ -24,7 +28,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
.build() .build()
} }
val retrofit by lazy { private val retrofit by lazy {
Retrofit.Builder() Retrofit.Builder()
.client(httpClient) .client(httpClient)
.baseUrl(context.getString(R.string.wikipedia_url)) .baseUrl(context.getString(R.string.wikipedia_url))
@ -36,9 +40,9 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
retrofit.create(WikipediaApi::class.java) retrofit.create(WikipediaApi::class.java)
} }
override fun onCancel() {
super.onCancel()
override fun search(query: String): Flow<Wikipedia?> = channelFlow {
send(null)
httpClient.dispatcher.run { httpClient.dispatcher.run {
runningCalls().forEach { runningCalls().forEach {
it.cancel() it.cancel()
@ -47,20 +51,17 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
it.cancel() it.cancel()
} }
} }
}
override suspend fun search(query: String) { if (query.isBlank()) return@channelFlow
wikipedia.value = null
if (query.isBlank()) return
val result = try { val result = try {
wikipediaService.search(query) wikipediaService.search(query)
} catch (e: Exception) { } catch (e: Exception) {
CrashReporter.logException(e) 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 image = if (LauncherPreferences.instance.searchWikipediaPictures) {
val width = context.resources.displayMetrics.widthPixels / 2 val width = context.resources.displayMetrics.widthPixels / 2
@ -68,7 +69,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
wikipediaService.getPageImage(page.pageid, width) wikipediaService.getPageImage(page.pageid, width)
} catch (e: Exception) { } catch (e: Exception) {
CrashReporter.logException(e) CrashReporter.logException(e)
return return@channelFlow
} }
imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source
} else null } else null
@ -79,7 +80,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
text = page.extract, text = page.extract,
image = image image = image
) )
send(wiki)
wikipedia.value = wiki
} }
} }

View File

@ -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<Wikipedia?> = wikipediaRepository.wikipedia
}