Refactor search, favorites and calendar widget
This commit is contained in:
parent
0193d4e495
commit
a2ccef64ce
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
package de.mm20.launcher2.calculator
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class CalculatorViewModel(
|
||||
calculatorRepository: CalculatorRepository
|
||||
): ViewModel() {
|
||||
val calculator = calculatorRepository.calculator
|
||||
}
|
||||
@ -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() }
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -91,7 +91,5 @@ val favoritesModule = module {
|
||||
return@factory NullDeserializer()
|
||||
}
|
||||
|
||||
single { FavoritesRepository(androidContext()) }
|
||||
|
||||
viewModel { FavoritesViewModel(get()) }
|
||||
single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -35,7 +35,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -22,9 +22,4 @@ class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fun endSearch() {
|
||||
synchronized(runningSearches) {
|
||||
runningSearches--
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
}
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(""))
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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 { _ ->
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
package de.mm20.launcher2.unitconverter
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class UnitConverterViewModel(
|
||||
unitConverterRepository: UnitConverterRepository
|
||||
): ViewModel() {
|
||||
val unitConverter = unitConverterRepository.unitConverter
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -6,5 +6,5 @@ import org.koin.dsl.module
|
||||
|
||||
val widgetsModule = module {
|
||||
single { WidgetRepository(androidContext()) }
|
||||
viewModel { WidgetViewModel(get(), get()) }
|
||||
viewModel { WidgetViewModel(get()) }
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user