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 pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() }
val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() }
val shortcut = AppShortcut(this.applicationContext, shortcutInfo,
packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
.loadLabel(packageManager).toString())
val shortcut = AppShortcut(
this.applicationContext, shortcutInfo,
packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
.loadLabel(packageManager).toString()
)
if (pinRequest.accept()) {
favoritesRepository.pinItem(shortcut)
}

View File

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

View File

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

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
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val calculatorModule = module {
single { CalculatorRepository() }
viewModel { CalculatorViewModel(get()) }
single<CalculatorRepository> { CalculatorRepositoryImpl() }
}

View File

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

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
val calendarModule = module {
single { CalendarRepository(androidContext(), get()) }
viewModel { CalendarViewModel(get()) }
single<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
}

View File

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

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
val contactsModule = module {
single { ContactRepository(androidContext(), get()) }
viewModel { ContactViewModel(get()) }
single<ContactRepository> { ContactRepositoryImpl(androidContext(), get()) }
}

View File

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

View File

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

View File

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

View File

@ -1,38 +1,183 @@
package de.mm20.launcher2.favorites
import android.content.Context
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.ktx.ceilToInt
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
import kotlin.math.max
import kotlin.math.min
class FavoritesRepository(private val context: Context) : BaseSearchableRepository() {
interface FavoritesRepository {
fun getFavorites(): Flow<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 favoriteItems: LiveData<List<FavoritesItemEntity>> = MutableLiveData()
private val scope = CoroutineScope(Dispatchers.Main + Job())
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 {
val deserializer: SearchableDeserializer = get { parametersOf(entity.serializedSearchable) }
@ -44,183 +189,4 @@ class FavoritesRepository(private val context: Context) : BaseSearchableReposito
hidden = entity.hidden
)
}
private val reloadFavorites: (String) -> Unit = {
scope.launch {
if(!LauncherPreferences.instance.searchShowFavorites) {
favorites.value = emptyList()
return@launch
}
val favs = mutableListOf<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()
}
single { FavoritesRepository(androidContext()) }
viewModel { FavoritesViewModel(get()) }
single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get()) }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,16 +5,34 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/**
* A low level repository for hidden items. This can only be used to retrieve keys and to check
* whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository.
*/
class HiddenItemsRepository(val context: Context) {
class HiddenItemsRepository(val context: Context, database: AppDatabase) {
val hiddenItemsKeys : LiveData<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)
}
}

View File

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

View File

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

View File

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

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
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val searchModule = module {
single { SearchRepository() }
viewModel { SearchViewModel(get()) }
single { WebsearchRepository(androidContext()) }
single<WebsearchRepository> { WebsearchRepositoryImpl(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
import android.content.Context
import android.content.Intent
import android.graphics.Color
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.search.data.Websearch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class WebsearchRepository(val context: Context) : BaseSearchableRepository() {
interface WebsearchRepository {
fun search(query: String): Flow<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 {
val databaseWebsearches = AppDatabase.getInstance(context).searchDao().getWebSearchesLiveData()
allWebsearches.addSource(databaseWebsearches) {
allWebsearches.value = it.map { Websearch(it) }
}
}
class WebsearchRepositoryImpl(
private val database: AppDatabase
) : WebsearchRepository {
override suspend fun search(query: String) {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
override fun search(query: String): Flow<List<Websearch>> = channelFlow {
if (query.isEmpty()) {
websearches.value = emptyList()
return
send(emptyList())
return@channelFlow
}
val searches = withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().getWebSearches().map {
Websearch(it, query)
withContext(Dispatchers.IO) {
database.searchDao().getWebSearches().map {
it.map { Websearch(it, query) }
}
}.collectLatest {
send(it)
}
websearches.value = searches
}
fun insertWebsearch(websearch: Websearch) {
launch {
override fun getWebsearches(): Flow<List<Websearch>> =
database.searchDao().getWebSearches().map {
it.map { Websearch(it) }
}
override fun insertWebsearch(websearch: Websearch) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().insertWebsearch(websearch.toDatabaseEntity())
database.searchDao().insertWebsearch(websearch.toDatabaseEntity())
}
}
}
fun deleteWebsearch(websearch: Websearch) {
launch {
override fun deleteWebsearch(websearch: Websearch) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().deleteWebsearch(websearch.toDatabaseEntity())
database.searchDao().deleteWebsearch(websearch.toDatabaseEntity())
}
}
}

View File

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

View File

@ -62,6 +62,10 @@ dependencyResolutionManagement {
listOf("kotlin.stdlib", "kotlinx.coroutines.core", "kotlinx.coroutines.android")
)
alias("desugar")
.to("com.android.tools", "desugar_jdk_libs")
.version("1.1.5")
version("androidx.compose", "1.1.0-rc01")
alias("androidx.compose.runtime")
.to("androidx.compose.runtime", "runtime")

View File

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

View File

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

View File

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

View File

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

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

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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.File
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.calculator.CalculatorRepository
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.contacts.ContactRepository
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.files.FileRepository
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.WebsearchRepository
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.unitconverter.UnitConverterRepository
import de.mm20.launcher2.websites.WebsiteRepository
import de.mm20.launcher2.wikipedia.WikipediaRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SearchViewModel: ViewModel(), KoinComponent {
class SearchViewModel : ViewModel(), KoinComponent {
fun search(query: String) {
private val favoritesRepository: FavoritesRepository by inject()
private val calendarRepository: CalendarRepository by inject()
private val contactRepository: ContactRepository by inject()
private val appRepository: AppRepository by inject()
private val wikipediaRepository: WikipediaRepository by inject()
private val unitConverterRepository: UnitConverterRepository by inject()
private val calculatorRepository: CalculatorRepository by inject()
private val websiteRepository: WebsiteRepository by inject()
private val fileRepository: FileRepository by inject()
private val websearchRepository: WebsearchRepository by inject()
val isSearching = MutableLiveData(false)
val favorites by lazy {
favoritesRepository.getFavorites().asLiveData()
}
val appResults = MutableLiveData<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.graphics.Color
import android.graphics.Point
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
@ -23,7 +22,6 @@ import android.view.*
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.viewModels
@ -44,7 +42,6 @@ import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.list.listItems
import com.jmedeisis.draglinearlayout.DragLinearLayout
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.icons.DynamicIconController
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
@ -53,15 +50,15 @@ import de.mm20.launcher2.ktx.isBrightColor
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.transition.OneShotLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
import de.mm20.launcher2.ui.legacy.component.EditFavoritesView
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
import de.mm20.launcher2.ui.launcher.modals.HiddenItemsView
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.component.WidgetView
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
import de.mm20.launcher2.ui.legacy.search.SearchGridView
import de.mm20.launcher2.weather.WeatherViewModel
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType
@ -79,37 +76,37 @@ class LauncherActivity : AppCompatActivity() {
* True if the search result list is visible
*/
private var searchVisibility = false
set(value) {
field = value
windowBackgroundBlur = value
}
set(value) {
field = value
windowBackgroundBlur = value
}
private lateinit var widgetHost: AppWidgetHost
private val widgets = mutableListOf<Widget>()
private lateinit var overlayView: ViewGroupOverlay
private val searchViewModel: SearchViewModel by viewModel()
private val widgetViewModel: WidgetViewModel by viewModel()
private val favoritesViewModel: FavoritesViewModel by viewModel()
private val searchViewModel: SearchViewModel by viewModels()
private val preferences = LauncherPreferences.instance
private var windowBackgroundBlur: Boolean = false
set(value) {
if(field == value) return
field = value
if (!isAtLeastApiLevel(31)) return
window.attributes = window.attributes.also {
if (value) {
it.blurBehindRadius = (32 * dp).toInt()
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
} else {
it.blurBehindRadius = 0
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
set(value) {
if (field == value) return
field = value
if (!isAtLeastApiLevel(31)) return
window.attributes = window.attributes.also {
if (value) {
it.blurBehindRadius = (32 * dp).toInt()
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
} else {
it.blurBehindRadius = 0
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
}
}
}
}
private var widgetEditMode = false
set(value) {
@ -318,28 +315,11 @@ class LauncherActivity : AppCompatActivity() {
)
}
R.id.menu_item_hidden -> {
val layout = NestedScrollView(this)
layout.clipChildren = false
layout.layoutParams = ViewGroup.LayoutParams(
MATCH_PARENT,
WRAP_CONTENT
)
val hiddenItemsGrid = SearchGridView(this)
hiddenItemsGrid.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT,
WRAP_CONTENT
).apply {
setMargins((8 * dp).toInt())
}
val hiddenItems = favoritesViewModel.hiddenItems
hiddenItems.observe(this) {
hiddenItemsGrid.submitItems(it)
}
layout.addView(hiddenItemsGrid)
val view = HiddenItemsView(this)
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
.show {
title(R.string.menu_hidden_items)
customView(view = layout)
customView(view = view)
negativeButton(R.string.close) { dismiss() }
}
//hiddenAppsActivated = true
@ -584,8 +564,6 @@ class LauncherActivity : AppCompatActivity() {
ActivityStarter.create(binding.rootView)
binding.activityStartOverlay.visibility = View.INVISIBLE
val widgetViewModel by viewModels<WidgetViewModel>()
widgetViewModel.requestCalendarUpdate()
search(binding.searchBar.getSearchQuery())
updateSystemBarAppearance()
@ -670,12 +648,8 @@ class LauncherActivity : AppCompatActivity() {
PermissionsManager.LOCATION -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
}
PermissionsManager.CALENDAR -> {
widgetViewModel.requestCalendarUpdate()
}
PermissionsManager.ALL -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
widgetViewModel.requestCalendarUpdate()
search(binding.searchBar.getSearchQuery())
}
}
@ -760,7 +734,8 @@ class LauncherActivity : AppCompatActivity() {
binding.container.translationY = binding.searchBar.height.toFloat()
}
windowBackgroundBlur = searchVisibility || newTransY > 0.6 * binding.searchBar.height
windowBackgroundBlur =
searchVisibility || newTransY > 0.6 * binding.searchBar.height
if (binding.container.translationY == 0f) return@onTouch false
}

View File

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

View File

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

View File

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

View File

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

View File

@ -6,14 +6,10 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.*
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewFavoritesBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
class FavoritesView : FrameLayout {
@ -21,18 +17,21 @@ class FavoritesView : FrameLayout {
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val favorites: LiveData<List<Searchable>>
private val binding = ViewFavoritesBinding.inflate(LayoutInflater.from(context), this, true)
init {
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
favorites = viewModel.getFavorites(LauncherPreferences.instance.gridColumnCount)
favorites.observe(context as AppCompatActivity, Observer {
visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
val favorites = viewModel.favorites
val hide = viewModel.hideFavorites
favorites.observe(context as AppCompatActivity) {
visibility = if (it?.isEmpty() == true || hide.value == true) View.GONE else View.VISIBLE
binding.favoritesGrid.submitItems(it)
})
}
hide.observe(context as AppCompatActivity) {
visibility = if(it == true) View.GONE else View.VISIBLE
}
layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
binding.favoritesCard.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,16 +6,18 @@ import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.setPadding
import androidx.lifecycle.Observer
import de.mm20.launcher2.favorites.FavoritesViewModel
import androidx.lifecycle.*
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ToolbarView : LinearLayout {
constructor(context: Context) : super(context)
@ -223,27 +225,37 @@ open class ToolbarSubaction(val title: String, var clickAction: (() -> Unit)) {
class FavoriteToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
R.drawable.ic_star_outline,
context.getString(R.string.favorites_menu_pin)
) {
), KoinComponent {
private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
private val isPinned = viewModel.isPinned(item)
init {
isPinned.observe(context as AppCompatActivity, Observer {
it ?: return@Observer
if (it) {
private val repository: FavoritesRepository by inject()
private var isPinned = false
set(value) {
field = value
if (value) {
title = context.getString(R.string.favorites_menu_unpin)
icon = R.drawable.ic_star_solid
} else {
title = context.getString(R.string.favorites_menu_pin)
icon = R.drawable.ic_star_outline
}
})
}
init {
clickAction = {
if (isPinned.value == true) {
viewModel.unpinItem(item)
if (isPinned) {
repository.unpinItem(item)
} else {
viewModel.pinItem(item)
repository.pinItem(item)
}
}
(context as LifecycleOwner).apply {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
repository.isPinned(item).collectLatest {
isPinned = it
}
}
}
}
}
@ -252,27 +264,40 @@ class FavoriteToolbarAction(val context: Context, val item: Searchable) : Toolba
class VisibilityToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
R.drawable.ic_visibility,
context.getString(R.string.menu_hide)
) {
), KoinComponent {
private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
private val isHidden = viewModel.isHidden(item)
init {
isHidden.observe(context as AppCompatActivity, Observer {
if (it) {
private val repository: FavoritesRepository by inject()
private var isHidden = false
set(value) {
field = value
if (value) {
title = context.getString(R.string.menu_unhide)
icon = R.drawable.ic_visibility
} else {
title = context.getString(R.string.menu_hide)
icon = R.drawable.ic_visibility_off
}
})
}
init {
clickAction = {
if (isHidden.value == true) {
viewModel.unhideItem(item)
if (isHidden) {
repository.unhideItem(item)
} else {
viewModel.hideItem(item)
repository.hideItem(item)
}
}
(context as LifecycleOwner).apply {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
repository.isHidden(item).collectLatest {
isHidden = it
}
}
}
}
}
}

View File

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

View File

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

View File

@ -1,30 +1,19 @@
package de.mm20.launcher2.ui.search
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.Card
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.calculator.CalculatorViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.component.SectionDivider
import org.koin.androidx.compose.getViewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
@Composable
fun calculatorItem(): LazyListScope.() -> Unit {
val viewModel: CalculatorViewModel = getViewModel()
val calculator by viewModel.calculator.observeAsState()
val viewModel: SearchViewModel by viewModel()
val calculator by viewModel.calculatorResult.observeAsState(null)
return {
calculator?.let {
item {

View File

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

View File

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

View File

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

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
import android.content.Context
import android.text.format.DateUtils
import android.view.View
import android.widget.FrameLayout
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material3.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.InformationText
import de.mm20.launcher2.ui.LauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.pluralResource
import de.mm20.launcher2.ui.searchable.DeprecatedSearchableList
import org.koin.androidx.compose.getViewModel
import java.util.*
import kotlin.math.max
import kotlin.math.min
import androidx.compose.runtime.Composable
@Composable
fun CalendarWidget() {
val viewModel: CalendarViewModel = getViewModel()
val favViewModel: FavoritesViewModel = viewModel()
val events by viewModel.upcomingCalendarEvents.observeAsState()
val pinnedEvents by favViewModel.pinnedCalendarEvents.observeAsState(emptyList())
val today = getToday()
val availableDays: List<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.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.ui.component.TextClock
import org.koin.androidx.compose.getViewModel
@Composable
fun DatePart() {

View File

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

View File

@ -2,15 +2,19 @@ package de.mm20.launcher2.unitconverter
import android.content.Context
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.currencies.CurrencyRepository
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.UnitConverter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
class UnitConverterRepository(val context: Context) : BaseSearchableRepository() {
interface UnitConverterRepository {
fun search(query:String): Flow<UnitConverter?>
}
class UnitConverterRepositoryImpl(val context: Context) : UnitConverterRepository {
val unitConverter = MutableLiveData<UnitConverter?>()
override suspend fun search(query: String) {
unitConverter.value = UnitConverter.search(context, query)
override fun search(query: String): Flow<UnitConverter?> = channelFlow {
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
val websitesModule = module {
single { WebsiteRepository(androidContext()) }
viewModel {
WebsiteViewModel(get())
}
single<WebsiteRepository> { WebsiteRepositoryImpl(androidContext()) }
}

View File

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

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 {
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.viewModelScope
import de.mm20.launcher2.calendar.CalendarRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class WidgetViewModel(
private val widgetRepository: WidgetRepository,
private val calendarRepository: CalendarRepository
private val widgetRepository: WidgetRepository
) : ViewModel() {
@ -29,7 +27,4 @@ class WidgetViewModel(
return widgetRepository.getInternalWidgets()
}
fun requestCalendarUpdate() {
calendarRepository.requestCalendarUpdate()
}
}

View File

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

View File

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

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
}