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 launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||||
val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() }
|
val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() }
|
||||||
val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() }
|
val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() }
|
||||||
val shortcut = AppShortcut(this.applicationContext, shortcutInfo,
|
val shortcut = AppShortcut(
|
||||||
packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
|
this.applicationContext, shortcutInfo,
|
||||||
.loadLabel(packageManager).toString())
|
packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
|
||||||
|
.loadLabel(packageManager).toString()
|
||||||
|
)
|
||||||
if (pinRequest.accept()) {
|
if (pinRequest.accept()) {
|
||||||
favoritesRepository.pinItem(shortcut)
|
favoritesRepository.pinItem(shortcut)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,70 +10,69 @@ import android.content.pm.ShortcutInfo
|
|||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.MediatorLiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.badges.Badge
|
import de.mm20.launcher2.badges.Badge
|
||||||
import de.mm20.launcher2.badges.BadgeProvider
|
import de.mm20.launcher2.badges.BadgeProvider
|
||||||
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.data.AppInstallation
|
import de.mm20.launcher2.search.data.AppInstallation
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.data.Application
|
||||||
import de.mm20.launcher2.search.data.LauncherApp
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class AppRepository(
|
interface AppRepository {
|
||||||
val context: Context,
|
fun search(query: String): Flow<List<Application>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppRepositoryImpl(
|
||||||
|
private val context: Context,
|
||||||
hiddenItemsRepository: HiddenItemsRepository,
|
hiddenItemsRepository: HiddenItemsRepository,
|
||||||
badgeProvider: BadgeProvider
|
private val badgeProvider: BadgeProvider
|
||||||
) : BaseSearchableRepository() {
|
) : AppRepository {
|
||||||
|
|
||||||
private val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
private val launcherApps =
|
||||||
|
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||||
|
|
||||||
val applications = MediatorLiveData<List<Application>>()
|
private val installedApps = MutableStateFlow<List<Application>>(emptyList())
|
||||||
|
private val installations = MutableStateFlow<MutableList<AppInstallation>>(mutableListOf())
|
||||||
|
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
|
|
||||||
private val installedApps = MutableLiveData<List<Application>>(emptyList())
|
private val profiles: List<UserHandle> =
|
||||||
private val installations = MutableLiveData<MutableList<AppInstallation>>(mutableListOf())
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||||
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
|
||||||
|
} else {
|
||||||
|
listOf(Process.myUserHandle())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private val installingPackages = mutableMapOf<Int, String>()
|
private val installingPackages = mutableMapOf<Int, String>()
|
||||||
|
|
||||||
private val profiles: List<UserHandle> = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
|
||||||
launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
|
|
||||||
} else {
|
|
||||||
listOf(Process.myUserHandle())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
applications.addSource(installedApps) {
|
|
||||||
launch { updateAppsForDisplay() }
|
|
||||||
}
|
|
||||||
applications.addSource(installations) {
|
|
||||||
launch { updateAppsForDisplay() }
|
|
||||||
}
|
|
||||||
|
|
||||||
applications.addSource(hiddenItemKeys) {
|
|
||||||
launch { updateAppsForDisplay() }
|
|
||||||
}
|
|
||||||
|
|
||||||
launcherApps.registerCallback(object : LauncherApps.Callback() {
|
launcherApps.registerCallback(object : LauncherApps.Callback() {
|
||||||
override fun onPackagesUnavailable(packageNames: Array<out String>, user: UserHandle, replacing: Boolean) {
|
override fun onPackagesUnavailable(
|
||||||
installedApps.value = installedApps.value?.filter { !packageNames.contains(it.`package`) }
|
packageNames: Array<out String>,
|
||||||
|
user: UserHandle,
|
||||||
|
replacing: Boolean
|
||||||
|
) {
|
||||||
|
installedApps.value =
|
||||||
|
installedApps.value.filter { !packageNames.contains(it.`package`) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackageChanged(packageName: String, user: UserHandle) {
|
override fun onPackageChanged(packageName: String, user: UserHandle) {
|
||||||
val apps = installedApps.value?.toMutableList() ?: return
|
val apps = installedApps.value.toMutableList()
|
||||||
apps.removeAll { packageName == it.`package` }
|
apps.removeAll { packageName == it.`package` }
|
||||||
apps.addAll(getApplications(packageName))
|
apps.addAll(getApplications(packageName))
|
||||||
installedApps.value = apps
|
installedApps.value = apps
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackagesAvailable(packageNames: Array<out String>, user: UserHandle, replacing: Boolean) {
|
override fun onPackagesAvailable(
|
||||||
val apps = installedApps.value?.toMutableList() ?: return
|
packageNames: Array<out String>,
|
||||||
|
user: UserHandle,
|
||||||
|
replacing: Boolean
|
||||||
|
) {
|
||||||
|
val apps = installedApps.value.toMutableList()
|
||||||
for (packageName in packageNames) {
|
for (packageName in packageNames) {
|
||||||
apps.addAll(getApplications(packageName))
|
apps.addAll(getApplications(packageName))
|
||||||
}
|
}
|
||||||
@ -82,16 +81,20 @@ class AppRepository(
|
|||||||
|
|
||||||
override fun onPackageAdded(packageName: String, user: UserHandle) {
|
override fun onPackageAdded(packageName: String, user: UserHandle) {
|
||||||
Log.d("MM20", "App installed: $packageName")
|
Log.d("MM20", "App installed: $packageName")
|
||||||
val apps = installedApps.value?.toMutableList() ?: return
|
val apps = installedApps.value.toMutableList()
|
||||||
apps.addAll(getApplications(packageName))
|
apps.addAll(getApplications(packageName))
|
||||||
installedApps.value = apps
|
installedApps.value = apps
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackageRemoved(packageName: String, user: UserHandle) {
|
override fun onPackageRemoved(packageName: String, user: UserHandle) {
|
||||||
installedApps.value = installedApps.value?.filter { packageName != (it.`package`) }
|
installedApps.value = installedApps.value.filter { packageName != (it.`package`) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShortcutsChanged(packageName: String, shortcuts: MutableList<ShortcutInfo>, user: UserHandle) {
|
override fun onShortcutsChanged(
|
||||||
|
packageName: String,
|
||||||
|
shortcuts: MutableList<ShortcutInfo>,
|
||||||
|
user: UserHandle
|
||||||
|
) {
|
||||||
super.onShortcutsChanged(packageName, shortcuts, user)
|
super.onShortcutsChanged(packageName, shortcuts, user)
|
||||||
onPackageChanged(packageName, user)
|
onPackageChanged(packageName, user)
|
||||||
}
|
}
|
||||||
@ -99,11 +102,17 @@ class AppRepository(
|
|||||||
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
||||||
super.onPackagesSuspended(packageNames, user)
|
super.onPackagesSuspended(packageNames, user)
|
||||||
packageNames?.forEach {
|
packageNames?.forEach {
|
||||||
badgeProvider.setBadge("app://$it", Badge(iconRes = R.drawable.ic_badge_suspended))
|
badgeProvider.setBadge(
|
||||||
|
"app://$it",
|
||||||
|
Badge(iconRes = R.drawable.ic_badge_suspended)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackagesUnsuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
override fun onPackagesUnsuspended(
|
||||||
|
packageNames: Array<out String>?,
|
||||||
|
user: UserHandle?
|
||||||
|
) {
|
||||||
super.onPackagesUnsuspended(packageNames, user)
|
super.onPackagesUnsuspended(packageNames, user)
|
||||||
packageNames?.forEach {
|
packageNames?.forEach {
|
||||||
badgeProvider.removeBadge("app://$it")
|
badgeProvider.removeBadge("app://$it")
|
||||||
@ -111,7 +120,10 @@ class AppRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
val apps = profiles.map { p ->
|
||||||
|
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) }
|
||||||
|
}.flatten()
|
||||||
|
installedApps.value = apps
|
||||||
|
|
||||||
val packageInstaller = context.packageManager.packageInstaller
|
val packageInstaller = context.packageManager.packageInstaller
|
||||||
|
|
||||||
@ -139,13 +151,13 @@ class AppRepository(
|
|||||||
installingPackages.remove(sessionId)
|
installingPackages.remove(sessionId)
|
||||||
val key = "app://$pkg"
|
val key = "app://$pkg"
|
||||||
val badge = badgeProvider.getBadge(key)?.apply { progress = null }
|
val badge = badgeProvider.getBadge(key)?.apply { progress = null }
|
||||||
?: Badge()
|
?: Badge()
|
||||||
badgeProvider.setBadge(key, badge)
|
badgeProvider.setBadge(key, badge)
|
||||||
val inst = installations.value ?: return
|
val inst = installations.value
|
||||||
inst.removeAll {
|
inst.removeAll {
|
||||||
it.session.sessionId == sessionId
|
it.session.sessionId == sessionId
|
||||||
}
|
}
|
||||||
installations.postValue(inst)
|
installations.value = inst
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,41 +177,66 @@ class AppRepository(
|
|||||||
val appInstallation = AppInstallation(session)
|
val appInstallation = AppInstallation(session)
|
||||||
val inst = installations.value ?: mutableListOf()
|
val inst = installations.value ?: mutableListOf()
|
||||||
inst.add(appInstallation)
|
inst.add(appInstallation)
|
||||||
installations.postValue(inst)
|
installations.value = inst
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
val apps = profiles.map { p -> launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) } }.flatten()
|
|
||||||
installedApps.value = apps
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
private fun getApplications(packageName: String): List<Application> {
|
||||||
updateAppsForDisplay()
|
if (packageName == context.packageName) return emptyList()
|
||||||
|
|
||||||
|
return profiles.map { p ->
|
||||||
|
launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) }
|
||||||
|
}.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateAppsForDisplay() {
|
|
||||||
val query = searchRepository.currentQuery.value ?: ""
|
|
||||||
|
|
||||||
val componentName = ComponentName.unflattenFromString(query)
|
private fun getApplication(
|
||||||
|
launcherActivityInfo: LauncherActivityInfo,
|
||||||
|
profile: UserHandle
|
||||||
|
): Application? {
|
||||||
|
if (launcherActivityInfo.applicationInfo.packageName == context.packageName && !context.packageName.endsWith(
|
||||||
|
".debug"
|
||||||
|
)
|
||||||
|
) return null
|
||||||
|
return LauncherApp(context, launcherActivityInfo)
|
||||||
|
}
|
||||||
|
|
||||||
val apps = withContext(Dispatchers.Default) {
|
override fun search(query: String): Flow<List<Application>> = channelFlow {
|
||||||
val hiddenItems = hiddenItemKeys.value ?: emptyList()
|
|
||||||
val installed = installedApps.value ?: emptyList()
|
combine(installedApps, hiddenItems, installations) {_, _, _ ->
|
||||||
val installing = installations.value ?: emptyList<AppInstallation>()
|
null
|
||||||
val results = mutableListOf<Application>()
|
}.collectLatest {
|
||||||
results.addAll(installed)
|
withContext(Dispatchers.IO) {
|
||||||
results.addAll(installing)
|
val appResults = mutableListOf<Application>()
|
||||||
if (query.isNotEmpty()) {
|
if (query.isEmpty()) {
|
||||||
results.removeAll { !it.label.contains(query, ignoreCase = true) }
|
appResults.addAll(installedApps.value)
|
||||||
getActivityByComponentName(componentName)?.let { results.add(it) }
|
appResults.addAll(installations.value)
|
||||||
|
} else {
|
||||||
|
appResults.addAll(installedApps.value.filter {
|
||||||
|
it.label.contains(
|
||||||
|
query,
|
||||||
|
ignoreCase = true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
appResults.addAll(installations.value.filter {
|
||||||
|
it.label.contains(
|
||||||
|
query,
|
||||||
|
ignoreCase = true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val componentName = ComponentName.unflattenFromString(query)
|
||||||
|
getActivityByComponentName(componentName)?.let { appResults.add(it) }
|
||||||
|
|
||||||
|
appResults.removeAll { hiddenItems.value.contains(it.key) }
|
||||||
|
|
||||||
|
appResults.sort()
|
||||||
|
|
||||||
|
send(appResults)
|
||||||
}
|
}
|
||||||
results.removeAll { hiddenItems.contains(it.key) }
|
|
||||||
results.sort()
|
|
||||||
results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applications.value = apps
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getActivityByComponentName(componentName: ComponentName?): Application? {
|
private fun getActivityByComponentName(componentName: ComponentName?): Application? {
|
||||||
@ -211,15 +248,4 @@ class AppRepository(
|
|||||||
LauncherApp(context, lai)
|
LauncherApp(context, lai)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getApplication(launcherActivityInfo: LauncherActivityInfo, profile: UserHandle): Application? {
|
|
||||||
if (launcherActivityInfo.applicationInfo.packageName == context.packageName && !context.packageName.endsWith(".debug")) return null
|
|
||||||
return LauncherApp(context, launcherActivityInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getApplications(packageName: String): List<Application> {
|
|
||||||
if (packageName == context.packageName) return emptyList()
|
|
||||||
|
|
||||||
return profiles.map { p -> launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } }.flatten()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val applicationsModule = module {
|
val applicationsModule = module {
|
||||||
single { AppRepository(androidContext(), get(), get()) }
|
single<AppRepository> { AppRepositoryImpl(androidContext(), get(), get()) }
|
||||||
viewModel { AppViewModel(get()) }
|
|
||||||
}
|
}
|
||||||
@ -1,40 +1,42 @@
|
|||||||
package de.mm20.launcher2.calculator
|
package de.mm20.launcher2.calculator
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.data.Calculator
|
import de.mm20.launcher2.search.data.Calculator
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import org.mariuszgromada.math.mxparser.Expression
|
import org.mariuszgromada.math.mxparser.Expression
|
||||||
|
|
||||||
class CalculatorRepository : BaseSearchableRepository() {
|
interface CalculatorRepository {
|
||||||
|
fun search(query: String): Flow<Calculator?>
|
||||||
|
}
|
||||||
|
|
||||||
val calculator = MutableLiveData<Calculator?>()
|
class CalculatorRepositoryImpl : CalculatorRepository {
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
override fun search(query: String): Flow<Calculator?> = channelFlow {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
calculator.value = null
|
send(null)
|
||||||
return
|
return@channelFlow
|
||||||
}
|
}
|
||||||
if (!LauncherPreferences.instance.searchCalculator) return
|
if (!LauncherPreferences.instance.searchCalculator) return@channelFlow
|
||||||
val calc = when {
|
val calc = when {
|
||||||
query.matches(Regex("0x[0-9a-fA-F]+")) -> {
|
query.matches(Regex("0x[0-9a-fA-F]+")) -> {
|
||||||
val solution = query.substring(2).toIntOrNull(16) ?: run {
|
val solution = query.substring(2).toIntOrNull(16) ?: run {
|
||||||
calculator.value = null
|
send(null)
|
||||||
return
|
return@channelFlow
|
||||||
}
|
}
|
||||||
Calculator(term = query, solution = solution.toDouble())
|
Calculator(term = query, solution = solution.toDouble())
|
||||||
}
|
}
|
||||||
query.matches(Regex("0b[01]+")) -> {
|
query.matches(Regex("0b[01]+")) -> {
|
||||||
val solution = query.substring(2).toIntOrNull(2) ?: run {
|
val solution = query.substring(2).toIntOrNull(2) ?: run {
|
||||||
calculator.value = null
|
send(null)
|
||||||
return
|
return@channelFlow
|
||||||
}
|
}
|
||||||
Calculator(term = query, solution = solution.toDouble())
|
Calculator(term = query, solution = solution.toDouble())
|
||||||
}
|
}
|
||||||
query.matches(Regex("0[0-7]+")) -> {
|
query.matches(Regex("0[0-7]+")) -> {
|
||||||
val solution = query.substring(1).toIntOrNull(8) ?: run {
|
val solution = query.substring(1).toIntOrNull(8) ?: run {
|
||||||
calculator.value = null
|
send(null)
|
||||||
return
|
return@channelFlow
|
||||||
}
|
}
|
||||||
Calculator(term = query, solution = solution.toDouble())
|
Calculator(term = query, solution = solution.toDouble())
|
||||||
}
|
}
|
||||||
@ -50,6 +52,6 @@ class CalculatorRepository : BaseSearchableRepository() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
calculator.value = calc
|
send(calc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
package de.mm20.launcher2.calculator
|
||||||
|
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val calculatorModule = module {
|
val calculatorModule = module {
|
||||||
single { CalculatorRepository() }
|
single<CalculatorRepository> { CalculatorRepositoryImpl() }
|
||||||
viewModel { CalculatorViewModel(get()) }
|
|
||||||
}
|
}
|
||||||
@ -1,44 +1,80 @@
|
|||||||
package de.mm20.launcher2.calendar
|
package de.mm20.launcher2.calendar
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import android.util.Log
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class CalendarRepository(
|
interface CalendarRepository {
|
||||||
val context: Context,
|
fun search(query: String): Flow<List<CalendarEvent>>
|
||||||
|
|
||||||
|
fun getUpcomingEvents(): Flow<List<CalendarEvent>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class CalendarRepositoryImpl(
|
||||||
|
private val context: Context,
|
||||||
hiddenItemsRepository: HiddenItemsRepository
|
hiddenItemsRepository: HiddenItemsRepository
|
||||||
) : BaseSearchableRepository() {
|
) : CalendarRepository {
|
||||||
|
|
||||||
val calendarEvents = MediatorLiveData<List<CalendarEvent>?>()
|
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
|
||||||
val upcomingCalendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
|
|
||||||
|
|
||||||
private val allEvents = MutableLiveData<List<CalendarEvent>?>(emptyList())
|
override fun search(query: String): Flow<List<CalendarEvent>> = channelFlow {
|
||||||
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
if (query.isBlank()) {
|
||||||
|
send(emptyList())
|
||||||
init {
|
return@channelFlow
|
||||||
calendarEvents.addSource(hiddenItemKeys) { keys ->
|
|
||||||
calendarEvents.value = allEvents.value?.filter { !keys.contains(it.key) }
|
|
||||||
}
|
}
|
||||||
calendarEvents.addSource(allEvents) { e ->
|
val events = withContext(Dispatchers.IO) {
|
||||||
calendarEvents.value = e?.filter { hiddenItemKeys.value?.contains(it.key) != true }
|
val now = System.currentTimeMillis()
|
||||||
|
CalendarEvent.search(
|
||||||
|
context,
|
||||||
|
query,
|
||||||
|
intervalStart = now,
|
||||||
|
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
hiddenItemKeys.observeForever {
|
|
||||||
requestCalendarUpdate()
|
hiddenItems.collectLatest { hiddenItems ->
|
||||||
|
val calendarResults = withContext(Dispatchers.IO) {
|
||||||
|
events.filter { !hiddenItems.contains(it.key) }
|
||||||
|
}
|
||||||
|
send(calendarResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestCalendarUpdate() {
|
override fun getUpcomingEvents(): Flow<List<CalendarEvent>> = channelFlow {
|
||||||
launch {
|
val unselectedCalendars = callbackFlow {
|
||||||
val unselectedCalendars = LauncherPreferences.instance.unselectedCalendars
|
val unregister =
|
||||||
val hideAlldayEvents = LauncherPreferences.instance.calendarHideAllday
|
LauncherPreferences.instance.doOnPreferenceChange("unselected_calendars") {
|
||||||
|
trySendBlocking(LauncherPreferences.instance.unselectedCalendars)
|
||||||
|
}
|
||||||
|
trySendBlocking(LauncherPreferences.instance.unselectedCalendars)
|
||||||
|
awaitClose {
|
||||||
|
unregister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hideAlldayEvents = callbackFlow {
|
||||||
|
val unregister =
|
||||||
|
LauncherPreferences.instance.doOnPreferenceChange("calendar_hide_allday") {
|
||||||
|
trySendBlocking(LauncherPreferences.instance.calendarHideAllday)
|
||||||
|
}
|
||||||
|
trySendBlocking(LauncherPreferences.instance.calendarHideAllday)
|
||||||
|
awaitClose {
|
||||||
|
unregister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
merge(unselectedCalendars, hideAlldayEvents, hiddenItems).collectLatest {
|
||||||
|
Log.d("MM20", "Calendar event flow has been created")
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val end = now + 14 * 24 * 60 * 60 * 1000L
|
val end = now + 14 * 24 * 60 * 60 * 1000L
|
||||||
val events = withContext(Dispatchers.IO) {
|
val events = withContext(Dispatchers.IO) {
|
||||||
@ -48,28 +84,20 @@ class CalendarRepository(
|
|||||||
intervalStart = now,
|
intervalStart = now,
|
||||||
intervalEnd = end,
|
intervalEnd = end,
|
||||||
limit = 700,
|
limit = 700,
|
||||||
hideAllDayEvents = hideAlldayEvents,
|
hideAllDayEvents = LauncherPreferences.instance.calendarHideAllday,
|
||||||
unselectedCalendars = unselectedCalendars,
|
unselectedCalendars = LauncherPreferences.instance.unselectedCalendars
|
||||||
hiddenEvents = hiddenItemKeys.value?.mapNotNull {
|
).filter {
|
||||||
if (it.startsWith("calendar")) it.substringAfterLast("/").toLong()
|
!hiddenItems.value.contains(it.key)
|
||||||
else null
|
}
|
||||||
} ?: emptyList()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
upcomingCalendarEvents.value = events
|
send(events)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
var unselectedCalendars: List<Long>
|
||||||
if (query.isBlank()) {
|
get() = LauncherPreferences.instance.unselectedCalendars
|
||||||
allEvents.value = null
|
set(value) {
|
||||||
return
|
LauncherPreferences.instance.unselectedCalendars = value
|
||||||
}
|
}
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
val endTime = System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000
|
|
||||||
val events = withContext(Dispatchers.IO) {
|
|
||||||
CalendarEvent.search(context, query, startTime, endTime)
|
|
||||||
}
|
|
||||||
allEvents.value = events
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val calendarModule = module {
|
val calendarModule = module {
|
||||||
single { CalendarRepository(androidContext(), get()) }
|
single<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
|
||||||
viewModel { CalendarViewModel(get()) }
|
|
||||||
}
|
}
|
||||||
@ -1,41 +1,35 @@
|
|||||||
package de.mm20.launcher2.contacts
|
package de.mm20.launcher2.contacts
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.MediatorLiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.data.Contact
|
import de.mm20.launcher2.search.data.Contact
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ContactRepository(
|
interface ContactRepository {
|
||||||
val context: Context,
|
fun search(query: String): Flow<List<Contact>>
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContactRepositoryImpl(
|
||||||
|
private val context: Context,
|
||||||
hiddenItemsRepository: HiddenItemsRepository
|
hiddenItemsRepository: HiddenItemsRepository
|
||||||
) : BaseSearchableRepository() {
|
) : ContactRepository {
|
||||||
|
|
||||||
val contacts = MediatorLiveData<List<Contact>?>()
|
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
private val allContacts = MutableLiveData<List<Contact>?>(emptyList())
|
override fun search(query: String): Flow<List<Contact>> = channelFlow {
|
||||||
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
val contacts = withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
init {
|
|
||||||
contacts.addSource(hiddenItemKeys) { keys ->
|
|
||||||
contacts.value = allContacts.value?.filter { !keys.contains(it.key) }
|
|
||||||
}
|
|
||||||
contacts.addSource(allContacts) { c ->
|
|
||||||
contacts.value = c?.filter { hiddenItemKeys.value?.contains(it.key) != true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
|
||||||
if (query.isBlank()) {
|
|
||||||
allContacts.value = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val results = withContext(Dispatchers.IO) {
|
|
||||||
Contact.search(context, query)
|
Contact.search(context, query)
|
||||||
}
|
}
|
||||||
allContacts.value = results
|
hiddenItems.collectLatest { hiddenItems ->
|
||||||
|
val contactResults = withContext(Dispatchers.IO) {
|
||||||
|
contacts.filter { !hiddenItems.contains(it.key) }
|
||||||
|
}
|
||||||
|
send(contactResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val contactsModule = module {
|
val contactsModule = module {
|
||||||
single { ContactRepository(androidContext(), get()) }
|
single<ContactRepository> { ContactRepositoryImpl(androidContext(), get()) }
|
||||||
viewModel { ContactViewModel(get()) }
|
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@ class CurrencyRepository(val context: Context) {
|
|||||||
init {
|
init {
|
||||||
val currencyWorker = PeriodicWorkRequest.Builder(ExchangeRateWorker::class.java, 60, TimeUnit.MINUTES)
|
val currencyWorker = PeriodicWorkRequest.Builder(ExchangeRateWorker::class.java, 60, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
WorkManager.getInstance().enqueueUniquePeriodicWork("ExchangeRates",
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork("ExchangeRates",
|
||||||
ExistingPeriodicWorkPolicy.REPLACE, currencyWorker)
|
ExistingPeriodicWorkPolicy.REPLACE, currencyWorker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
||||||
import de.mm20.launcher2.database.entities.WebsearchEntity
|
import de.mm20.launcher2.database.entities.WebsearchEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface SearchDao {
|
interface SearchDao {
|
||||||
@ -18,7 +19,10 @@ interface SearchDao {
|
|||||||
fun insertSkipExisting(items: FavoritesItemEntity)
|
fun insertSkipExisting(items: FavoritesItemEntity)
|
||||||
|
|
||||||
@Query("SELECT * FROM Searchable WHERE pinned > 0 ORDER BY pinned DESC, launchCount DESC")
|
@Query("SELECT * FROM Searchable WHERE pinned > 0 ORDER BY pinned DESC, launchCount DESC")
|
||||||
fun getFavorites() : LiveData<List<FavoritesItemEntity>>
|
fun getFavorites(): Flow<List<FavoritesItemEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Searchable WHERE pinned > 0 AND `key` LIKE 'calendar://%' ORDER BY pinned DESC, launchCount DESC")
|
||||||
|
fun getPinnedCalendarEvents(): Flow<List<FavoritesItemEntity>>
|
||||||
|
|
||||||
|
|
||||||
@Query("SELECT COUNT(key) as count FROM Searchable WHERE pinned = 1;")
|
@Query("SELECT COUNT(key) as count FROM Searchable WHERE pinned = 1;")
|
||||||
@ -44,14 +48,14 @@ interface SearchDao {
|
|||||||
fun unpinFavorite(key: String)
|
fun unpinFavorite(key: String)
|
||||||
|
|
||||||
@Query("DELETE FROM Searchable WHERE `key` = :key")
|
@Query("DELETE FROM Searchable WHERE `key` = :key")
|
||||||
fun deleteByKey(key: String)
|
suspend fun deleteByKey(key: String)
|
||||||
|
|
||||||
@Query("UPDATE Searchable SET pinned = 0 WHERE `key` = :key")
|
@Query("UPDATE Searchable SET pinned = 0 WHERE `key` = :key")
|
||||||
fun unpinApp(key: String)
|
fun unpinApp(key: String)
|
||||||
|
|
||||||
|
|
||||||
@Query("SELECT pinned FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinned ORDER BY pinned DESC LIMIT 1")
|
@Query("SELECT pinned FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinned ORDER BY pinned DESC LIMIT 1")
|
||||||
fun isPinned(key: String): LiveData<Boolean>
|
fun isPinned(key: String): Flow<Boolean>
|
||||||
|
|
||||||
|
|
||||||
@Query("UPDATE Searchable SET hidden = 1, pinned = 0 WHERE `key` = :key")
|
@Query("UPDATE Searchable SET hidden = 1, pinned = 0 WHERE `key` = :key")
|
||||||
@ -67,19 +71,16 @@ interface SearchDao {
|
|||||||
fun unhideItem(key: String)
|
fun unhideItem(key: String)
|
||||||
|
|
||||||
@Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1")
|
@Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1")
|
||||||
fun isHidden(key: String): LiveData<Boolean>
|
fun isHidden(key: String): Flow<Boolean>
|
||||||
|
|
||||||
@Query("SELECT `key` FROM SEARCHABLE WHERE hidden = 1")
|
@Query("SELECT `key` FROM SEARCHABLE WHERE hidden = 1")
|
||||||
fun getHiddenItemKeys(): LiveData<List<String>>
|
fun getHiddenItemKeys(): Flow<List<String>>
|
||||||
|
|
||||||
@Query("SELECT * FROM SEARCHABLE WHERE hidden = 1")
|
@Query("SELECT * FROM SEARCHABLE WHERE hidden = 1")
|
||||||
fun getHiddenItems(): LiveData<List<FavoritesItemEntity>>
|
fun getHiddenItems(): Flow<List<FavoritesItemEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM Websearch ORDER BY label ASC")
|
@Query("SELECT * FROM Websearch ORDER BY label ASC")
|
||||||
fun getWebSearches(): List<WebsearchEntity>
|
fun getWebSearches(): Flow<List<WebsearchEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM Websearch ORDER BY label ASC")
|
|
||||||
fun getWebSearchesLiveData(): LiveData<List<WebsearchEntity>>
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insertWebsearch(websearch: WebsearchEntity)
|
fun insertWebsearch(websearch: WebsearchEntity)
|
||||||
|
|||||||
@ -38,6 +38,7 @@ dependencies {
|
|||||||
implementation(libs.bundles.kotlin)
|
implementation(libs.bundles.kotlin)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +1,183 @@
|
|||||||
package de.mm20.launcher2.favorites
|
package de.mm20.launcher2.favorites
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MediatorLiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
||||||
import de.mm20.launcher2.ktx.ceilToInt
|
import de.mm20.launcher2.ktx.ceilToInt
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
class FavoritesRepository(private val context: Context) : BaseSearchableRepository() {
|
interface FavoritesRepository {
|
||||||
|
fun getFavorites(): Flow<List<Searchable>>
|
||||||
|
fun getPinnedCalendarEvents(): Flow<List<Searchable>>
|
||||||
|
fun isPinned(searchable: Searchable): Flow<Boolean>
|
||||||
|
fun pinItem(searchable: Searchable)
|
||||||
|
fun unpinItem(searchable: Searchable)
|
||||||
|
fun isHidden(searchable: Searchable): Flow<Boolean>
|
||||||
|
fun hideItem(searchable: Searchable)
|
||||||
|
fun unhideItem(searchable: Searchable)
|
||||||
|
fun incrementLaunchCounter(searchable: Searchable)
|
||||||
|
suspend fun getAllFavoriteItems(): List<FavoritesItem>
|
||||||
|
fun saveFavorites(favorites: List<FavoritesItem>)
|
||||||
|
fun getHiddenItems(): Flow<List<Searchable>>
|
||||||
|
}
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
class FavoritesRepositoryImpl(
|
||||||
|
private val context: Context,
|
||||||
|
private val database: AppDatabase
|
||||||
|
) : FavoritesRepository, KoinComponent {
|
||||||
|
|
||||||
private val favorites = MediatorLiveData<List<Searchable>>()
|
private val scope = CoroutineScope(Dispatchers.Main + Job())
|
||||||
private val favoriteItems: LiveData<List<FavoritesItemEntity>> = MutableLiveData()
|
|
||||||
|
|
||||||
val hiddenItems = MediatorLiveData<List<Searchable>>()
|
override fun getFavorites(): Flow<List<Searchable>> = channelFlow {
|
||||||
|
|
||||||
private val pinnedFavorites = AppDatabase.getInstance(context).searchDao().getFavorites()
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
val gridColumns = callbackFlow {
|
||||||
|
send(LauncherPreferences.instance.gridColumnCount)
|
||||||
|
val unregister =
|
||||||
|
LauncherPreferences.instance.doOnPreferenceChange("grid_column_count") {
|
||||||
|
trySendBlocking(LauncherPreferences.instance.gridColumnCount)
|
||||||
|
}
|
||||||
|
awaitClose {
|
||||||
|
unregister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val dao = database.searchDao()
|
||||||
|
|
||||||
|
val pinnedFavorites = dao.getFavorites().map {
|
||||||
|
it.mapNotNull {
|
||||||
|
val item = fromDatabaseEntity(it).searchable
|
||||||
|
if (item == null) {
|
||||||
|
dao.deleteByKey(it.key)
|
||||||
|
}
|
||||||
|
return@mapNotNull item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pinnedFavorites.collectLatest { pinned ->
|
||||||
|
gridColumns.collectLatest { columns ->
|
||||||
|
var favCount = (pinned.size.toDouble() / columns).ceilToInt() * columns
|
||||||
|
if (pinned.size < columns) favCount += columns
|
||||||
|
val autoFavs = dao.getAutoFavorites(favCount - pinned.size).mapNotNull {
|
||||||
|
val item = fromDatabaseEntity(it).searchable
|
||||||
|
if (item == null) {
|
||||||
|
dao.deleteByKey(it.key)
|
||||||
|
}
|
||||||
|
return@mapNotNull item
|
||||||
|
}
|
||||||
|
send(pinned + autoFavs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPinnedCalendarEvents(): Flow<List<CalendarEvent>> {
|
||||||
|
return database.searchDao().getPinnedCalendarEvents().map {
|
||||||
|
it.mapNotNull { fromDatabaseEntity(it).searchable as? CalendarEvent }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isPinned(searchable: Searchable): Flow<Boolean> {
|
||||||
|
return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pinItem(searchable: Searchable) {
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val dao = AppDatabase.getInstance(context).searchDao()
|
||||||
|
val databaseItem = dao.getFavorite(searchable.key)
|
||||||
|
val favoritesItem = FavoritesItem(
|
||||||
|
key = searchable.key,
|
||||||
|
searchable = searchable,
|
||||||
|
launchCount = databaseItem?.launchCount ?: 0,
|
||||||
|
pinPosition = 1,
|
||||||
|
hidden = false
|
||||||
|
)
|
||||||
|
dao.insertReplaceExisting(favoritesItem.toDatabaseEntity())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unpinItem(searchable: Searchable) {
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isHidden(searchable: Searchable): Flow<Boolean> {
|
||||||
|
return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hideItem(searchable: Searchable) {
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val dao = AppDatabase.getInstance(context).searchDao()
|
||||||
|
val databaseItem = dao.getFavorite(searchable.key)
|
||||||
|
val favoritesItem = FavoritesItem(
|
||||||
|
key = searchable.key,
|
||||||
|
searchable = searchable,
|
||||||
|
launchCount = databaseItem?.launchCount ?: 0,
|
||||||
|
pinPosition = 0,
|
||||||
|
hidden = true
|
||||||
|
)
|
||||||
|
dao.insertReplaceExisting(favoritesItem.toDatabaseEntity())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unhideItem(searchable: Searchable) {
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun incrementLaunchCounter(searchable: Searchable) {
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val item = FavoritesItem(searchable.key, searchable, 0, 0, false)
|
||||||
|
AppDatabase.getInstance(context).searchDao()
|
||||||
|
.incrementLaunchCount(item.toDatabaseEntity())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAllFavoriteItems(): List<FavoritesItem> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull {
|
||||||
|
fromDatabaseEntity(it).takeIf { it.searchable != null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveFavorites(favorites: List<FavoritesItem>) {
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
AppDatabase.getInstance(context).searchDao()
|
||||||
|
.saveFavorites(favorites.map { it.toDatabaseEntity() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHiddenItems(): Flow<List<Searchable>> {
|
||||||
|
return database.searchDao().getHiddenItems().map {
|
||||||
|
it.mapNotNull { fromDatabaseEntity(it).searchable }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val pinnedCalendarEvents = MediatorLiveData<List<CalendarEvent>>()
|
|
||||||
|
|
||||||
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
|
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
|
||||||
val deserializer: SearchableDeserializer = get { parametersOf(entity.serializedSearchable) }
|
val deserializer: SearchableDeserializer = get { parametersOf(entity.serializedSearchable) }
|
||||||
@ -44,183 +189,4 @@ class FavoritesRepository(private val context: Context) : BaseSearchableReposito
|
|||||||
hidden = entity.hidden
|
hidden = entity.hidden
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val reloadFavorites: (String) -> Unit = {
|
|
||||||
scope.launch {
|
|
||||||
if(!LauncherPreferences.instance.searchShowFavorites) {
|
|
||||||
favorites.value = emptyList()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val favs = mutableListOf<Searchable>()
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val dao = AppDatabase.getInstance(context).searchDao()
|
|
||||||
val favItems = pinnedFavorites.value ?: emptyList()
|
|
||||||
favs.addAll(favItems.mapNotNull {
|
|
||||||
val item = fromDatabaseEntity(it)
|
|
||||||
if (item.searchable == null) {
|
|
||||||
dao.deleteByKey(item.key)
|
|
||||||
}
|
|
||||||
if (item.searchable is CalendarEvent) return@mapNotNull null
|
|
||||||
item.searchable
|
|
||||||
})
|
|
||||||
var favCount = (favs.size.toDouble() / columns).ceilToInt() * columns
|
|
||||||
if(favItems.size < columns) favCount += columns
|
|
||||||
val autoFavs = dao.getAutoFavorites(favCount - favs.size)
|
|
||||||
favs.addAll(autoFavs.mapNotNull {
|
|
||||||
val item = fromDatabaseEntity(it)
|
|
||||||
if (item.searchable == null) {
|
|
||||||
dao.deleteByKey(item.key)
|
|
||||||
}
|
|
||||||
item.searchable
|
|
||||||
})
|
|
||||||
}
|
|
||||||
favorites.value = favs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var columns = 1
|
|
||||||
|
|
||||||
init {
|
|
||||||
val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems()
|
|
||||||
hiddenItems.addSource(hidden) { h ->
|
|
||||||
hiddenItems.value = h.mapNotNull { fromDatabaseEntity(it).searchable }
|
|
||||||
}
|
|
||||||
favorites.addSource(pinnedFavorites) {
|
|
||||||
reloadFavorites("")
|
|
||||||
}
|
|
||||||
pinnedCalendarEvents.addSource(pinnedFavorites) {
|
|
||||||
scope.launch {
|
|
||||||
val dao = AppDatabase.getInstance(context).searchDao()
|
|
||||||
pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull {
|
|
||||||
val item = fromDatabaseEntity(it)
|
|
||||||
if (item.searchable == null) {
|
|
||||||
withContext(Dispatchers.IO) { dao.deleteByKey(item.key) }
|
|
||||||
}
|
|
||||||
item.searchable as? CalendarEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LauncherPreferences.instance.doOnPreferenceChange(
|
|
||||||
"search_show_favorites",
|
|
||||||
"search_auto_add_favorites",
|
|
||||||
action = reloadFavorites
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun isHidden(searchable: Searchable): LiveData<Boolean> {
|
|
||||||
return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFavorites(columns: Int): LiveData<List<Searchable>> {
|
|
||||||
if (columns != this.columns) {
|
|
||||||
this.columns = columns
|
|
||||||
reloadFavorites("")
|
|
||||||
}
|
|
||||||
return favorites
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
|
||||||
if (query.isEmpty()) {
|
|
||||||
reloadFavorites("")
|
|
||||||
} else {
|
|
||||||
favorites.value = emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPinned(searchable: Searchable): LiveData<Boolean> {
|
|
||||||
return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pinItem(searchable: Searchable) {
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val dao = AppDatabase.getInstance(context).searchDao()
|
|
||||||
val databaseItem = dao.getFavorite(searchable.key)
|
|
||||||
val favoritesItem = FavoritesItem(
|
|
||||||
key = searchable.key,
|
|
||||||
searchable = searchable,
|
|
||||||
launchCount = databaseItem?.launchCount ?: 0,
|
|
||||||
pinPosition = 1,
|
|
||||||
hidden = false
|
|
||||||
)
|
|
||||||
dao.insertReplaceExisting(favoritesItem.toDatabaseEntity())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unpinItem(searchable: Searchable) {
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideItem(searchable: Searchable) {
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val dao = AppDatabase.getInstance(context).searchDao()
|
|
||||||
val databaseItem = dao.getFavorite(searchable.key)
|
|
||||||
val favoritesItem = FavoritesItem(
|
|
||||||
key = searchable.key,
|
|
||||||
searchable = searchable,
|
|
||||||
launchCount = databaseItem?.launchCount ?: 0,
|
|
||||||
pinPosition = 0,
|
|
||||||
hidden = true
|
|
||||||
)
|
|
||||||
dao.insertReplaceExisting(favoritesItem.toDatabaseEntity())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unhideItem(searchable: Searchable) {
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteItem(key: String) {
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
AppDatabase.getInstance(context).searchDao().deleteByKey(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun incrementLaunchCount(searchable: Searchable) {
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val item = FavoritesItem(searchable.key, searchable, 0, 0, false)
|
|
||||||
AppDatabase.getInstance(context).searchDao().incrementLaunchCount(item.toDatabaseEntity())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull {
|
|
||||||
fromDatabaseEntity(it).takeIf { it.searchable != null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveFavorites(favorites: MutableList<FavoritesItem>) {
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
AppDatabase.getInstance(context).searchDao().saveFavorites(favorites.map { it.toDatabaseEntity() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTopFavorites(count: Int): LiveData<List<Searchable>> {
|
|
||||||
val favs = MediatorLiveData<List<Searchable>>()
|
|
||||||
favs.addSource(favorites) {
|
|
||||||
favs.value = it.subList(0, min(count, it.size))
|
|
||||||
}
|
|
||||||
return favs
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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()
|
return@factory NullDeserializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
single { FavoritesRepository(androidContext()) }
|
single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get()) }
|
||||||
|
|
||||||
viewModel { FavoritesViewModel(get()) }
|
|
||||||
}
|
}
|
||||||
@ -1,26 +1,26 @@
|
|||||||
package de.mm20.launcher2.files
|
package de.mm20.launcher2.files
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.MediatorLiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
|
||||||
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
||||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
class FilesRepository(
|
interface FileRepository {
|
||||||
val context: Context,
|
fun search(query: String): Flow<List<File>>
|
||||||
|
suspend fun deleteFile(file: File)
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileRepositoryImpl(
|
||||||
|
private val context: Context,
|
||||||
hiddenItemsRepository: HiddenItemsRepository
|
hiddenItemsRepository: HiddenItemsRepository
|
||||||
) : BaseSearchableRepository() {
|
) : FileRepository {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
val files = MediatorLiveData<List<File>?>()
|
|
||||||
|
|
||||||
private val allFiles = MutableLiveData<List<File>?>(emptyList())
|
|
||||||
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
|
||||||
|
|
||||||
private val nextcloudClient by lazy {
|
private val nextcloudClient by lazy {
|
||||||
NextcloudApiHelper(context)
|
NextcloudApiHelper(context)
|
||||||
@ -29,44 +29,39 @@ class FilesRepository(
|
|||||||
OwncloudClient(context)
|
OwncloudClient(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
override fun search(query: String): Flow<List<File>> = channelFlow {
|
||||||
files.addSource(hiddenItemKeys) { keys ->
|
|
||||||
files.value = allFiles.value?.filter { !keys.contains(it.key) }
|
|
||||||
}
|
|
||||||
files.addSource(allFiles) { f ->
|
|
||||||
files.value = f?.filter { hiddenItemKeys.value?.contains(it.key) != true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
allFiles.value = null
|
send(emptyList())
|
||||||
return
|
return@channelFlow
|
||||||
}
|
}
|
||||||
val localFiles = withContext(Dispatchers.IO) {
|
|
||||||
LocalFile.search(context, query).sorted().toMutableList()
|
|
||||||
}
|
|
||||||
allFiles.value = localFiles
|
|
||||||
|
|
||||||
val cloudFiles = withContext(Dispatchers.IO) {
|
hiddenItems.collectLatest { hiddenItems ->
|
||||||
delay(300)
|
val files = mutableListOf<File>()
|
||||||
listOf(
|
|
||||||
async { OneDriveFile.search(context, query) },
|
val localFiles = withContext(Dispatchers.IO) {
|
||||||
async { GDriveFile.search(context, query) },
|
LocalFile.search(context, query).sorted().filter { !hiddenItems.contains(it.key) }
|
||||||
async { NextcloudFile.search(context, query, nextcloudClient) },
|
}
|
||||||
async { OwncloudFile.search(context, query, owncloudClient) }
|
files.addAll(localFiles)
|
||||||
).awaitAll().flatten()
|
send(localFiles)
|
||||||
|
|
||||||
|
val cloudFiles = withContext(Dispatchers.IO) {
|
||||||
|
delay(300)
|
||||||
|
listOf(
|
||||||
|
async { OneDriveFile.search(context, query) },
|
||||||
|
async { GDriveFile.search(context, query) },
|
||||||
|
async { NextcloudFile.search(context, query, nextcloudClient) },
|
||||||
|
async { OwncloudFile.search(context, query, owncloudClient) }
|
||||||
|
).awaitAll().flatten()
|
||||||
|
}
|
||||||
|
yield()
|
||||||
|
files.addAll(cloudFiles.filter { !hiddenItems.contains(it.key) })
|
||||||
|
send(files)
|
||||||
}
|
}
|
||||||
yield()
|
|
||||||
allFiles.value = localFiles + cloudFiles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteFile(file: File) {
|
override suspend fun deleteFile(file: File) {
|
||||||
if (file.isDeletable) {
|
if (file.isDeletable) {
|
||||||
scope.launch {
|
file.delete(context)
|
||||||
file.delete(context)
|
|
||||||
allFiles.value = allFiles.value?.filter { it != file }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,16 +1,16 @@
|
|||||||
package de.mm20.launcher2.files
|
package de.mm20.launcher2.files
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.search.data.File
|
import de.mm20.launcher2.search.data.File
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class FilesViewModel(
|
class FilesViewModel(
|
||||||
private val filesRepository: FilesRepository
|
private val filesRepository: FileRepository
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
|
||||||
|
|
||||||
val files = filesRepository.files
|
|
||||||
|
|
||||||
fun deleteFile(file: File) {
|
fun deleteFile(file: File) {
|
||||||
filesRepository.deleteFile(file)
|
viewModelScope.launch {
|
||||||
|
filesRepository.deleteFile(file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,6 +5,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val filesModule = module {
|
val filesModule = module {
|
||||||
single { FilesRepository(androidContext(), get()) }
|
single<FileRepository> { FileRepositoryImpl(androidContext(), get()) }
|
||||||
viewModel { FilesViewModel(get()) }
|
viewModel { FilesViewModel(get()) }
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.kotlin.stdlib)
|
implementation(libs.bundles.kotlin)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
||||||
|
|||||||
@ -5,16 +5,34 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A low level repository for hidden items. This can only be used to retrieve keys and to check
|
* A low level repository for hidden items. This can only be used to retrieve keys and to check
|
||||||
* whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository.
|
* whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository.
|
||||||
*/
|
*/
|
||||||
class HiddenItemsRepository(val context: Context) {
|
class HiddenItemsRepository(val context: Context, database: AppDatabase) {
|
||||||
|
|
||||||
val hiddenItemsKeys : LiveData<List<String>> = AppDatabase.getInstance(context).searchDao().getHiddenItemKeys()
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
val hiddenItemsKeys = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
|
||||||
fun isHidden(item: Searchable): LiveData<Boolean> {
|
init {
|
||||||
|
scope.launch {
|
||||||
|
AppDatabase.getInstance(context).searchDao().getHiddenItemKeys().collectLatest {
|
||||||
|
hiddenItemsKeys.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun isHidden(item: Searchable): Flow<Boolean> {
|
||||||
return AppDatabase.getInstance(context).searchDao().isHidden(item.key)
|
return AppDatabase.getInstance(context).searchDao().isHidden(item.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,6 +5,6 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val hiddenItemsModule = module {
|
val hiddenItemsModule = module {
|
||||||
single { HiddenItemsRepository(androidContext()) }
|
single { HiddenItemsRepository(androidContext(), get()) }
|
||||||
viewModel { HiddenItemsViewModel(get()) }
|
viewModel { HiddenItemsViewModel(get()) }
|
||||||
}
|
}
|
||||||
@ -19,6 +19,9 @@ fun View.asViewGroup(): ViewGroup? {
|
|||||||
val View.lifecycleScope
|
val View.lifecycleScope
|
||||||
get() = (context as LifecycleOwner).lifecycleScope
|
get() = (context as LifecycleOwner).lifecycleScope
|
||||||
|
|
||||||
|
val View.lifecycleOwner
|
||||||
|
get() = (context as LifecycleOwner)
|
||||||
|
|
||||||
fun View.setPadding(vertical: Int, horizontal: Int) {
|
fun View.setPadding(vertical: Int, horizontal: Int) {
|
||||||
setPadding(vertical, horizontal, vertical, horizontal)
|
setPadding(vertical, horizontal, vertical, horizontal)
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package de.mm20.launcher2.preferences
|
package de.mm20.launcher2.preferences
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
|
||||||
@ -109,10 +110,14 @@ class LauncherPreferences(val context: Application, version: Int = 3) {
|
|||||||
var gridColumnCount by IntPreference("grid_column_count", default = context.resources.getInteger(R.integer.config_columnCount))
|
var gridColumnCount by IntPreference("grid_column_count", default = context.resources.getInteger(R.integer.config_columnCount))
|
||||||
|
|
||||||
|
|
||||||
fun doOnPreferenceChange(vararg keys: String, action: (String) -> Unit) {
|
fun doOnPreferenceChange(vararg keys: String, action: (String) -> Unit): () -> Unit {
|
||||||
preferences.registerOnSharedPreferenceChangeListener { _, key ->
|
val listener = { _: SharedPreferences, key: String ->
|
||||||
if (keys.contains(key)) action(key)
|
if (keys.contains(key)) action(key)
|
||||||
}
|
}
|
||||||
|
preferences.registerOnSharedPreferenceChangeListener(listener)
|
||||||
|
return {
|
||||||
|
preferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -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
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val searchModule = module {
|
val searchModule = module {
|
||||||
single { SearchRepository() }
|
single<WebsearchRepository> { WebsearchRepositoryImpl(get()) }
|
||||||
viewModel { SearchViewModel(get()) }
|
|
||||||
single { WebsearchRepository(androidContext()) }
|
|
||||||
viewModel { WebsearchViewModel(get()) }
|
viewModel { WebsearchViewModel(get()) }
|
||||||
}
|
}
|
||||||
@ -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
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
|
||||||
import androidx.lifecycle.MediatorLiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
import de.mm20.launcher2.search.data.Websearch
|
import de.mm20.launcher2.search.data.Websearch
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class WebsearchRepository(val context: Context) : BaseSearchableRepository() {
|
interface WebsearchRepository {
|
||||||
|
fun search(query: String): Flow<List<Websearch>>
|
||||||
|
|
||||||
val websearches = MutableLiveData<List<Websearch>>(emptyList())
|
fun getWebsearches(): Flow<List<Websearch>>
|
||||||
|
|
||||||
val allWebsearches = MediatorLiveData<List<Websearch>>()
|
fun insertWebsearch(websearch: Websearch)
|
||||||
|
fun deleteWebsearch(websearch: Websearch)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
class WebsearchRepositoryImpl(
|
||||||
val databaseWebsearches = AppDatabase.getInstance(context).searchDao().getWebSearchesLiveData()
|
private val database: AppDatabase
|
||||||
allWebsearches.addSource(databaseWebsearches) {
|
) : WebsearchRepository {
|
||||||
allWebsearches.value = it.map { Websearch(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
|
override fun search(query: String): Flow<List<Websearch>> = channelFlow {
|
||||||
if (query.isEmpty()) {
|
if (query.isEmpty()) {
|
||||||
websearches.value = emptyList()
|
send(emptyList())
|
||||||
return
|
return@channelFlow
|
||||||
}
|
}
|
||||||
val searches = withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
AppDatabase.getInstance(context).searchDao().getWebSearches().map {
|
database.searchDao().getWebSearches().map {
|
||||||
Websearch(it, query)
|
it.map { Websearch(it, query) }
|
||||||
}
|
}
|
||||||
|
}.collectLatest {
|
||||||
|
send(it)
|
||||||
}
|
}
|
||||||
websearches.value = searches
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertWebsearch(websearch: Websearch) {
|
override fun getWebsearches(): Flow<List<Websearch>> =
|
||||||
launch {
|
database.searchDao().getWebSearches().map {
|
||||||
|
it.map { Websearch(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun insertWebsearch(websearch: Websearch) {
|
||||||
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
AppDatabase.getInstance(context).searchDao().insertWebsearch(websearch.toDatabaseEntity())
|
database.searchDao().insertWebsearch(websearch.toDatabaseEntity())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteWebsearch(websearch: Websearch) {
|
override fun deleteWebsearch(websearch: Websearch) {
|
||||||
launch {
|
scope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
AppDatabase.getInstance(context).searchDao().deleteWebsearch(websearch.toDatabaseEntity())
|
database.searchDao().deleteWebsearch(websearch.toDatabaseEntity())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.mm20.launcher2.search
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
import de.mm20.launcher2.search.data.Websearch
|
import de.mm20.launcher2.search.data.Websearch
|
||||||
|
|
||||||
class WebsearchViewModel(
|
class WebsearchViewModel(
|
||||||
@ -16,7 +17,6 @@ class WebsearchViewModel(
|
|||||||
websearchRepository.deleteWebsearch(websearch)
|
websearchRepository.deleteWebsearch(websearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
val websearches = websearchRepository.websearches
|
val allWebsearches = websearchRepository.getWebsearches().asLiveData()
|
||||||
val allWebsearches = websearchRepository.allWebsearches
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -62,6 +62,10 @@ dependencyResolutionManagement {
|
|||||||
listOf("kotlin.stdlib", "kotlinx.coroutines.core", "kotlinx.coroutines.android")
|
listOf("kotlin.stdlib", "kotlinx.coroutines.core", "kotlinx.coroutines.android")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
alias("desugar")
|
||||||
|
.to("com.android.tools", "desugar_jdk_libs")
|
||||||
|
.version("1.1.5")
|
||||||
|
|
||||||
version("androidx.compose", "1.1.0-rc01")
|
version("androidx.compose", "1.1.0-rc01")
|
||||||
alias("androidx.compose.runtime")
|
alias("androidx.compose.runtime")
|
||||||
.to("androidx.compose.runtime", "runtime")
|
.to("androidx.compose.runtime", "runtime")
|
||||||
|
|||||||
@ -24,6 +24,8 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@ -48,6 +50,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
coreLibraryDesugaring(libs.desugar)
|
||||||
|
|
||||||
implementation(libs.bundles.kotlin)
|
implementation(libs.bundles.kotlin)
|
||||||
|
|
||||||
implementation(libs.androidx.compose.runtime)
|
implementation(libs.androidx.compose.runtime)
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import androidx.compose.material.icons.rounded.VisibilityOff
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
@ -26,11 +26,11 @@ import androidx.compose.ui.res.colorResource
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.theme.divider
|
import de.mm20.launcher2.ui.theme.divider
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.inject
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
|
||||||
@ -41,19 +41,19 @@ fun DefaultSwipeActions(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
content: @Composable RowScope.() -> Unit
|
content: @Composable RowScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val viewModel: FavoritesViewModel = getViewModel()
|
val repository: FavoritesRepository by inject()
|
||||||
|
|
||||||
val isPinned by viewModel.isPinned(item).observeAsState()
|
val isPinned by repository.isPinned(item).collectAsState(false)
|
||||||
val isHidden by viewModel.isHidden(item).observeAsState()
|
val isHidden by repository.isHidden(item).collectAsState(false)
|
||||||
|
|
||||||
val state = androidx.compose.material.rememberSwipeableState(
|
val state = androidx.compose.material.rememberSwipeableState(
|
||||||
SwipeAction.Default,
|
SwipeAction.Default,
|
||||||
confirmStateChange = {
|
confirmStateChange = {
|
||||||
if (it == SwipeAction.Favorites) {
|
if (it == SwipeAction.Favorites) {
|
||||||
if (isPinned == true) {
|
if (isPinned == true) {
|
||||||
viewModel.unpinItem(item)
|
repository.unpinItem(item)
|
||||||
} else {
|
} else {
|
||||||
viewModel.pinItem(item)
|
repository.pinItem(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
|||||||
@ -31,11 +31,12 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import com.google.accompanist.pager.PagerState
|
import com.google.accompanist.pager.PagerState
|
||||||
import de.mm20.launcher2.search.SearchViewModel
|
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||||
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
import org.koin.androidx.compose.viewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search bar
|
* Search bar
|
||||||
@ -54,7 +55,7 @@ fun SearchBar(
|
|||||||
) {
|
) {
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val viewModel: SearchViewModel = getViewModel()
|
val viewModel: SearchViewModel by viewModel()
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
viewModel.search(searchQuery)
|
viewModel.search(searchQuery)
|
||||||
|
|||||||
@ -14,11 +14,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.res.integerResource
|
import androidx.compose.ui.res.integerResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
import org.koin.androidx.compose.inject
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -165,8 +165,8 @@ data class ToggleToolbarAction(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
|
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
|
||||||
val viewModel: FavoritesViewModel = getViewModel()
|
val viewModel: FavoritesRepository by inject()
|
||||||
val isPinned by viewModel.isPinned(item).observeAsState(false)
|
val isPinned by viewModel.isPinned(item).collectAsState(false)
|
||||||
|
|
||||||
return ToggleToolbarAction(
|
return ToggleToolbarAction(
|
||||||
label = stringResource(
|
label = stringResource(
|
||||||
@ -186,8 +186,8 @@ fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
|
fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
|
||||||
val viewModel: FavoritesViewModel = getViewModel()
|
val viewModel: FavoritesRepository by inject()
|
||||||
val isHidden by viewModel.isHidden(item).observeAsState(false)
|
val isHidden by viewModel.isHidden(item).collectAsState(false)
|
||||||
|
|
||||||
return ToggleToolbarAction(
|
return ToggleToolbarAction(
|
||||||
label = stringResource(
|
label = stringResource(
|
||||||
|
|||||||
@ -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.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.updateMargins
|
|
||||||
import androidx.core.widget.TextViewCompat
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
import de.mm20.launcher2.favorites.FavoritesItem
|
import de.mm20.launcher2.favorites.FavoritesItem
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
|
||||||
import de.mm20.launcher2.ktx.dp
|
|
||||||
import de.mm20.launcher2.ktx.lifecycleScope
|
import de.mm20.launcher2.ktx.lifecycleScope
|
||||||
import de.mm20.launcher2.ktx.setPadding
|
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.databinding.DialogEditFavoritesBinding
|
import de.mm20.launcher2.ui.databinding.DialogEditFavoritesBinding
|
||||||
import de.mm20.launcher2.ui.databinding.EditFavoritesRowBinding
|
|
||||||
import de.mm20.launcher2.ui.databinding.EditFavoritesTitleBinding
|
import de.mm20.launcher2.ui.databinding.EditFavoritesTitleBinding
|
||||||
|
import de.mm20.launcher2.ui.legacy.component.EditFavoritesRow
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class EditFavoritesView @JvmOverloads constructor(
|
class EditFavoritesView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null
|
context: Context, attrs: AttributeSet? = null
|
||||||
) : FrameLayout(context, attrs) {
|
) : FrameLayout(context, attrs) {
|
||||||
|
|
||||||
val viewModel : FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: EditFavoritesVM by (context as AppCompatActivity).viewModels()
|
||||||
|
|
||||||
private val binding = DialogEditFavoritesBinding.inflate(LayoutInflater.from(context), this)
|
private val binding = DialogEditFavoritesBinding.inflate(LayoutInflater.from(context), this)
|
||||||
|
|
||||||
@ -45,7 +36,7 @@ class EditFavoritesView @JvmOverloads constructor(
|
|||||||
|
|
||||||
suspend fun initView() {
|
suspend fun initView() {
|
||||||
favorites = withContext(Dispatchers.IO) {
|
favorites = withContext(Dispatchers.IO) {
|
||||||
viewModel.getAllFavoriteItems().toMutableList()
|
viewModel.getFavorites().toMutableList()
|
||||||
}
|
}
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
binding.itemList.addView(getLabel(R.string.edit_favorites_dialog_stage0))
|
binding.itemList.addView(getLabel(R.string.edit_favorites_dialog_stage0))
|
||||||
@ -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
|
package de.mm20.launcher2.ui.launcher.search
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import de.mm20.launcher2.search.data.File
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.mm20.launcher2.applications.AppRepository
|
||||||
|
import de.mm20.launcher2.calculator.CalculatorRepository
|
||||||
|
import de.mm20.launcher2.calendar.CalendarRepository
|
||||||
|
import de.mm20.launcher2.contacts.ContactRepository
|
||||||
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
|
import de.mm20.launcher2.files.FileRepository
|
||||||
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
|
import de.mm20.launcher2.search.WebsearchRepository
|
||||||
|
import de.mm20.launcher2.search.data.*
|
||||||
|
import de.mm20.launcher2.unitconverter.UnitConverterRepository
|
||||||
|
import de.mm20.launcher2.websites.WebsiteRepository
|
||||||
|
import de.mm20.launcher2.wikipedia.WikipediaRepository
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class SearchViewModel: ViewModel(), KoinComponent {
|
class SearchViewModel : ViewModel(), KoinComponent {
|
||||||
|
|
||||||
fun search(query: String) {
|
private val favoritesRepository: FavoritesRepository by inject()
|
||||||
|
|
||||||
|
private val calendarRepository: CalendarRepository by inject()
|
||||||
|
private val contactRepository: ContactRepository by inject()
|
||||||
|
private val appRepository: AppRepository by inject()
|
||||||
|
private val wikipediaRepository: WikipediaRepository by inject()
|
||||||
|
private val unitConverterRepository: UnitConverterRepository by inject()
|
||||||
|
private val calculatorRepository: CalculatorRepository by inject()
|
||||||
|
private val websiteRepository: WebsiteRepository by inject()
|
||||||
|
private val fileRepository: FileRepository by inject()
|
||||||
|
private val websearchRepository: WebsearchRepository by inject()
|
||||||
|
|
||||||
|
val isSearching = MutableLiveData(false)
|
||||||
|
|
||||||
|
val favorites by lazy {
|
||||||
|
favoritesRepository.getFavorites().asLiveData()
|
||||||
|
}
|
||||||
|
|
||||||
|
val appResults = MutableLiveData<List<Application>>(emptyList())
|
||||||
|
val fileResults = MutableLiveData<List<File>>(emptyList())
|
||||||
|
val contactResults = MutableLiveData<List<Contact>>(emptyList())
|
||||||
|
val calendarResults = MutableLiveData<List<CalendarEvent>>(emptyList())
|
||||||
|
val wikipediaResult = MutableLiveData<Wikipedia?>(null)
|
||||||
|
val websiteResult = MutableLiveData<Website?>(null)
|
||||||
|
val calculatorResult = MutableLiveData<Calculator?>(null)
|
||||||
|
val unitConverterResult = MutableLiveData<UnitConverter?>(null)
|
||||||
|
val websearchResults = MutableLiveData<List<Websearch>>(emptyList())
|
||||||
|
|
||||||
|
val hideFavorites = MutableLiveData(false)
|
||||||
|
|
||||||
|
var searchJob: Job? = null
|
||||||
|
fun search(query: String) {
|
||||||
|
try {
|
||||||
|
searchJob?.cancel()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
}
|
||||||
|
hideFavorites.postValue(query.isNotEmpty())
|
||||||
|
searchJob = viewModelScope.launch {
|
||||||
|
isSearching.postValue(true)
|
||||||
|
val jobs = mutableListOf<Deferred<Any>>()
|
||||||
|
jobs += async {
|
||||||
|
appRepository.search(query).collectLatest {
|
||||||
|
appResults.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs += async {
|
||||||
|
contactRepository.search(query).collectLatest {
|
||||||
|
contactResults.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs += async {
|
||||||
|
calendarRepository.search(query).collectLatest {
|
||||||
|
calendarResults.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs += async {
|
||||||
|
wikipediaRepository.search(query).collectLatest {
|
||||||
|
wikipediaResult.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs += async {
|
||||||
|
unitConverterRepository.search(query).collectLatest {
|
||||||
|
unitConverterResult.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs += async {
|
||||||
|
calculatorRepository.search(query).collectLatest {
|
||||||
|
calculatorResult.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs += async {
|
||||||
|
websiteRepository.search(query).collectLatest {
|
||||||
|
websiteResult.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs += async {
|
||||||
|
fileRepository.search(query).collectLatest {
|
||||||
|
fileResults.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs += async {
|
||||||
|
websearchRepository.search(query).collectLatest {
|
||||||
|
websearchResults.postValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs.map { it.await() }
|
||||||
|
isSearching.postValue(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _files = MutableLiveData<List<File>>()
|
|
||||||
val files: LiveData<List<File>> = _files
|
|
||||||
}
|
}
|
||||||
@ -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.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Point
|
import android.graphics.Point
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -23,7 +22,6 @@ import android.view.*
|
|||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@ -44,7 +42,6 @@ import com.afollestad.materialdialogs.callbacks.onDismiss
|
|||||||
import com.afollestad.materialdialogs.customview.customView
|
import com.afollestad.materialdialogs.customview.customView
|
||||||
import com.afollestad.materialdialogs.list.listItems
|
import com.afollestad.materialdialogs.list.listItems
|
||||||
import com.jmedeisis.draglinearlayout.DragLinearLayout
|
import com.jmedeisis.draglinearlayout.DragLinearLayout
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
|
||||||
import de.mm20.launcher2.icons.DynamicIconController
|
import de.mm20.launcher2.icons.DynamicIconController
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
@ -53,15 +50,15 @@ import de.mm20.launcher2.ktx.isBrightColor
|
|||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.SearchViewModel
|
|
||||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||||
import de.mm20.launcher2.transition.OneShotLayoutTransition
|
import de.mm20.launcher2.transition.OneShotLayoutTransition
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
|
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
|
||||||
import de.mm20.launcher2.ui.legacy.component.EditFavoritesView
|
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
|
||||||
|
import de.mm20.launcher2.ui.launcher.modals.HiddenItemsView
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.legacy.component.WidgetView
|
import de.mm20.launcher2.ui.legacy.component.WidgetView
|
||||||
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
|
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchGridView
|
|
||||||
import de.mm20.launcher2.weather.WeatherViewModel
|
import de.mm20.launcher2.weather.WeatherViewModel
|
||||||
import de.mm20.launcher2.widgets.Widget
|
import de.mm20.launcher2.widgets.Widget
|
||||||
import de.mm20.launcher2.widgets.WidgetType
|
import de.mm20.launcher2.widgets.WidgetType
|
||||||
@ -79,37 +76,37 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
* True if the search result list is visible
|
* True if the search result list is visible
|
||||||
*/
|
*/
|
||||||
private var searchVisibility = false
|
private var searchVisibility = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
windowBackgroundBlur = value
|
windowBackgroundBlur = value
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var widgetHost: AppWidgetHost
|
private lateinit var widgetHost: AppWidgetHost
|
||||||
private val widgets = mutableListOf<Widget>()
|
private val widgets = mutableListOf<Widget>()
|
||||||
|
|
||||||
private lateinit var overlayView: ViewGroupOverlay
|
private lateinit var overlayView: ViewGroupOverlay
|
||||||
|
|
||||||
private val searchViewModel: SearchViewModel by viewModel()
|
|
||||||
private val widgetViewModel: WidgetViewModel by viewModel()
|
private val widgetViewModel: WidgetViewModel by viewModel()
|
||||||
private val favoritesViewModel: FavoritesViewModel by viewModel()
|
|
||||||
|
private val searchViewModel: SearchViewModel by viewModels()
|
||||||
|
|
||||||
private val preferences = LauncherPreferences.instance
|
private val preferences = LauncherPreferences.instance
|
||||||
|
|
||||||
private var windowBackgroundBlur: Boolean = false
|
private var windowBackgroundBlur: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
if(field == value) return
|
if (field == value) return
|
||||||
field = value
|
field = value
|
||||||
if (!isAtLeastApiLevel(31)) return
|
if (!isAtLeastApiLevel(31)) return
|
||||||
window.attributes = window.attributes.also {
|
window.attributes = window.attributes.also {
|
||||||
if (value) {
|
if (value) {
|
||||||
it.blurBehindRadius = (32 * dp).toInt()
|
it.blurBehindRadius = (32 * dp).toInt()
|
||||||
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
|
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
|
||||||
} else {
|
} else {
|
||||||
it.blurBehindRadius = 0
|
it.blurBehindRadius = 0
|
||||||
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
|
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var widgetEditMode = false
|
private var widgetEditMode = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -318,28 +315,11 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.menu_item_hidden -> {
|
R.id.menu_item_hidden -> {
|
||||||
val layout = NestedScrollView(this)
|
val view = HiddenItemsView(this)
|
||||||
layout.clipChildren = false
|
|
||||||
layout.layoutParams = ViewGroup.LayoutParams(
|
|
||||||
MATCH_PARENT,
|
|
||||||
WRAP_CONTENT
|
|
||||||
)
|
|
||||||
val hiddenItemsGrid = SearchGridView(this)
|
|
||||||
hiddenItemsGrid.layoutParams = FrameLayout.LayoutParams(
|
|
||||||
MATCH_PARENT,
|
|
||||||
WRAP_CONTENT
|
|
||||||
).apply {
|
|
||||||
setMargins((8 * dp).toInt())
|
|
||||||
}
|
|
||||||
val hiddenItems = favoritesViewModel.hiddenItems
|
|
||||||
hiddenItems.observe(this) {
|
|
||||||
hiddenItemsGrid.submitItems(it)
|
|
||||||
}
|
|
||||||
layout.addView(hiddenItemsGrid)
|
|
||||||
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
|
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
|
||||||
.show {
|
.show {
|
||||||
title(R.string.menu_hidden_items)
|
title(R.string.menu_hidden_items)
|
||||||
customView(view = layout)
|
customView(view = view)
|
||||||
negativeButton(R.string.close) { dismiss() }
|
negativeButton(R.string.close) { dismiss() }
|
||||||
}
|
}
|
||||||
//hiddenAppsActivated = true
|
//hiddenAppsActivated = true
|
||||||
@ -584,8 +564,6 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
ActivityStarter.create(binding.rootView)
|
ActivityStarter.create(binding.rootView)
|
||||||
binding.activityStartOverlay.visibility = View.INVISIBLE
|
binding.activityStartOverlay.visibility = View.INVISIBLE
|
||||||
|
|
||||||
val widgetViewModel by viewModels<WidgetViewModel>()
|
|
||||||
widgetViewModel.requestCalendarUpdate()
|
|
||||||
search(binding.searchBar.getSearchQuery())
|
search(binding.searchBar.getSearchQuery())
|
||||||
|
|
||||||
updateSystemBarAppearance()
|
updateSystemBarAppearance()
|
||||||
@ -670,12 +648,8 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
PermissionsManager.LOCATION -> {
|
PermissionsManager.LOCATION -> {
|
||||||
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
|
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
|
||||||
}
|
}
|
||||||
PermissionsManager.CALENDAR -> {
|
|
||||||
widgetViewModel.requestCalendarUpdate()
|
|
||||||
}
|
|
||||||
PermissionsManager.ALL -> {
|
PermissionsManager.ALL -> {
|
||||||
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
|
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
|
||||||
widgetViewModel.requestCalendarUpdate()
|
|
||||||
search(binding.searchBar.getSearchQuery())
|
search(binding.searchBar.getSearchQuery())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -760,7 +734,8 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
binding.container.translationY = binding.searchBar.height.toFloat()
|
binding.container.translationY = binding.searchBar.height.toFloat()
|
||||||
|
|
||||||
}
|
}
|
||||||
windowBackgroundBlur = searchVisibility || newTransY > 0.6 * binding.searchBar.height
|
windowBackgroundBlur =
|
||||||
|
searchVisibility || newTransY > 0.6 * binding.searchBar.height
|
||||||
|
|
||||||
if (binding.container.translationY == 0f) return@onTouch false
|
if (binding.container.translationY == 0f) return@onTouch false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,12 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import de.mm20.launcher2.applications.AppViewModel
|
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.data.Application
|
||||||
import de.mm20.launcher2.ui.databinding.ViewApplicationBinding
|
import de.mm20.launcher2.ui.databinding.ViewApplicationBinding
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
|
|
||||||
class ApplicationView : FrameLayout {
|
class ApplicationView : FrameLayout {
|
||||||
|
|
||||||
@ -27,8 +27,8 @@ class ApplicationView : FrameLayout {
|
|||||||
layoutTransition = LayoutTransition()
|
layoutTransition = LayoutTransition()
|
||||||
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
binding.applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
binding.applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
val viewModel: AppViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
applications = viewModel.applications
|
applications = viewModel.appResults
|
||||||
applications.observe(context as AppCompatActivity, Observer<List<Application>> {
|
applications.observe(context as AppCompatActivity, Observer<List<Application>> {
|
||||||
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
||||||
binding.applicationGrid.submitItems(it)
|
binding.applicationGrid.submitItems(it)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
@ -14,29 +15,29 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import de.mm20.launcher2.ui.R
|
|
||||||
import de.mm20.launcher2.calculator.CalculatorViewModel
|
|
||||||
import de.mm20.launcher2.search.data.Calculator
|
import de.mm20.launcher2.search.data.Calculator
|
||||||
import de.mm20.launcher2.ui.LegacyLauncherTheme
|
import de.mm20.launcher2.ui.LegacyLauncherTheme
|
||||||
import de.mm20.launcher2.ui.databinding.ViewCalculatorBinding
|
import de.mm20.launcher2.ui.databinding.ViewCalculatorBinding
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.search.CalculatorItem
|
import de.mm20.launcher2.ui.search.CalculatorItem
|
||||||
import de.mm20.launcher2.ui.search.UnitConverterItem
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
import kotlin.math.round
|
|
||||||
|
|
||||||
class CalculatorView : FrameLayout {
|
class CalculatorView : FrameLayout {
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleRes
|
||||||
|
)
|
||||||
|
|
||||||
private val calculator: LiveData<Calculator?>
|
private val calculator: LiveData<Calculator?>
|
||||||
|
|
||||||
private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true)
|
private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val viewModel: CalculatorViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
calculator = viewModel.calculator
|
calculator = viewModel.calculatorResult
|
||||||
calculator.observe(context as AppCompatActivity, Observer {
|
calculator.observe(context as AppCompatActivity, Observer {
|
||||||
if (it == null) visibility = View.GONE
|
if (it == null) visibility = View.GONE
|
||||||
else {
|
else {
|
||||||
|
|||||||
@ -6,23 +6,26 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import de.mm20.launcher2.ui.R
|
|
||||||
import de.mm20.launcher2.calendar.CalendarViewModel
|
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.search.data.MissingPermission
|
import de.mm20.launcher2.search.data.MissingPermission
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class CalendarView : FrameLayout {
|
class CalendarView : FrameLayout {
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleRes
|
||||||
|
)
|
||||||
|
|
||||||
private val calendarEvents: LiveData<List<CalendarEvent>?>
|
private val calendarEvents: LiveData<List<CalendarEvent>?>
|
||||||
|
|
||||||
@ -33,21 +36,27 @@ class CalendarView : FrameLayout {
|
|||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
val list = findViewById<SearchListView>(R.id.list)
|
val list = findViewById<SearchListView>(R.id.list)
|
||||||
val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
calendarEvents = viewModel.calendarEvents
|
calendarEvents = viewModel.calendarResults
|
||||||
calendarEvents.observe(context as AppCompatActivity, {
|
calendarEvents.observe(context as AppCompatActivity, {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
if (it.isEmpty() && LauncherPreferences.instance.searchCalendars && !PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) {
|
if (it.isEmpty() && LauncherPreferences.instance.searchCalendars && !PermissionsManager.checkPermission(
|
||||||
|
context,
|
||||||
|
PermissionsManager.CALENDAR
|
||||||
|
)
|
||||||
|
) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
list.submitItems(listOf(
|
list.submitItems(
|
||||||
|
listOf(
|
||||||
MissingPermission(
|
MissingPermission(
|
||||||
context.getString(R.string.permission_calendar_search),
|
context.getString(R.string.permission_calendar_search),
|
||||||
PermissionsManager.CALENDAR
|
PermissionsManager.CALENDAR
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
|
)
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
|||||||
@ -6,24 +6,28 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.SearchView
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import de.mm20.launcher2.contacts.ContactViewModel
|
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.data.Contact
|
import de.mm20.launcher2.search.data.Contact
|
||||||
import de.mm20.launcher2.search.data.MissingPermission
|
import de.mm20.launcher2.search.data.MissingPermission
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class ContactView : FrameLayout {
|
class ContactView : FrameLayout {
|
||||||
private val contacts: LiveData<List<Contact>?>
|
private val contacts: LiveData<List<Contact>?>
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleRes
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_search_category_list, this)
|
View.inflate(context, R.layout.view_search_category_list, this)
|
||||||
@ -31,22 +35,28 @@ class ContactView : FrameLayout {
|
|||||||
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
val viewModel: ContactViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
contacts = viewModel.contacts
|
contacts = viewModel.contactResults
|
||||||
val list = findViewById<SearchListView>(R.id.list)
|
val list = findViewById<SearchListView>(R.id.list)
|
||||||
contacts.observe(context as AppCompatActivity, {
|
contacts.observe(context as AppCompatActivity, {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
if (it.isEmpty() && LauncherPreferences.instance.searchContacts && !PermissionsManager.checkPermission(context, PermissionsManager.CONTACTS)) {
|
if (it.isEmpty() && LauncherPreferences.instance.searchContacts && !PermissionsManager.checkPermission(
|
||||||
|
context,
|
||||||
|
PermissionsManager.CONTACTS
|
||||||
|
)
|
||||||
|
) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
list.submitItems(listOf(
|
list.submitItems(
|
||||||
|
listOf(
|
||||||
MissingPermission(
|
MissingPermission(
|
||||||
context.getString(R.string.permission_contact_search),
|
context.getString(R.string.permission_contact_search),
|
||||||
PermissionsManager.CONTACTS
|
PermissionsManager.CONTACTS
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
|
)
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
|||||||
@ -6,14 +6,10 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.*
|
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
|
||||||
import de.mm20.launcher2.ui.R
|
|
||||||
import de.mm20.launcher2.ui.databinding.ViewFavoritesBinding
|
import de.mm20.launcher2.ui.databinding.ViewFavoritesBinding
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
|
|
||||||
class FavoritesView : FrameLayout {
|
class FavoritesView : FrameLayout {
|
||||||
|
|
||||||
@ -21,18 +17,21 @@ class FavoritesView : FrameLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
||||||
|
|
||||||
private val favorites: LiveData<List<Searchable>>
|
|
||||||
|
|
||||||
private val binding = ViewFavoritesBinding.inflate(LayoutInflater.from(context), this, true)
|
private val binding = ViewFavoritesBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
favorites = viewModel.getFavorites(LauncherPreferences.instance.gridColumnCount)
|
val favorites = viewModel.favorites
|
||||||
favorites.observe(context as AppCompatActivity, Observer {
|
val hide = viewModel.hideFavorites
|
||||||
visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE
|
favorites.observe(context as AppCompatActivity) {
|
||||||
|
visibility = if (it?.isEmpty() == true || hide.value == true) View.GONE else View.VISIBLE
|
||||||
binding.favoritesGrid.submitItems(it)
|
binding.favoritesGrid.submitItems(it)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
hide.observe(context as AppCompatActivity) {
|
||||||
|
visibility = if(it == true) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
|
layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
|
||||||
binding.favoritesCard.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
|
binding.favoritesCard.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
|
||||||
|
|||||||
@ -6,22 +6,26 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import de.mm20.launcher2.files.FilesViewModel
|
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.search.data.File
|
import de.mm20.launcher2.search.data.File
|
||||||
import de.mm20.launcher2.search.data.MissingPermission
|
import de.mm20.launcher2.search.data.MissingPermission
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class FileView : FrameLayout {
|
class FileView : FrameLayout {
|
||||||
private val files: LiveData<List<File>?>
|
private val files: LiveData<List<File>?>
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleRes
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_search_category_list, this)
|
View.inflate(context, R.layout.view_search_category_list, this)
|
||||||
@ -30,21 +34,27 @@ class FileView : FrameLayout {
|
|||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
val list = findViewById<SearchListView>(R.id.list)
|
val list = findViewById<SearchListView>(R.id.list)
|
||||||
val viewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
files = viewModel.files
|
files = viewModel.fileResults
|
||||||
files.observe(context as AppCompatActivity, {
|
files.observe(context as AppCompatActivity, {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
if (it.isEmpty() && !PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) {
|
if (it.isEmpty() && !PermissionsManager.checkPermission(
|
||||||
|
context,
|
||||||
|
PermissionsManager.EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
|
) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
list.submitItems(listOf(
|
list.submitItems(
|
||||||
|
listOf(
|
||||||
MissingPermission(
|
MissingPermission(
|
||||||
context.getString(R.string.permission_files_search),
|
context.getString(R.string.permission_files_search),
|
||||||
PermissionsManager.EXTERNAL_STORAGE
|
PermissionsManager.EXTERNAL_STORAGE
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
|
)
|
||||||
return@observe
|
return@observe
|
||||||
}
|
}
|
||||||
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
|||||||
@ -12,21 +12,17 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AccelerateInterpolator
|
import android.view.animation.AccelerateInterpolator
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.postDelayed
|
import androidx.core.view.postDelayed
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import com.airbnb.lottie.LottieCompositionFactory
|
import com.airbnb.lottie.LottieCompositionFactory
|
||||||
import com.airbnb.lottie.LottieDrawable
|
import com.airbnb.lottie.LottieDrawable
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.preferences.SearchStyles
|
import de.mm20.launcher2.preferences.SearchStyles
|
||||||
import de.mm20.launcher2.search.SearchViewModel
|
|
||||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.databinding.ViewSearchBarBinding
|
import de.mm20.launcher2.ui.databinding.ViewSearchBarBinding
|
||||||
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
|
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class SearchBar @JvmOverloads constructor(
|
class SearchBar @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
@ -72,12 +68,6 @@ class SearchBar @JvmOverloads constructor(
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
val viewModel = (context as AppCompatActivity).viewModel<SearchViewModel>().value
|
|
||||||
|
|
||||||
viewModel.isSearching.observe(context, Observer {
|
|
||||||
binding.searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.overflowMenu.setOnClickListener {
|
binding.overflowMenu.setOnClickListener {
|
||||||
if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it)
|
if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it)
|
||||||
else (setSearchQuery(""))
|
else (setSearchQuery(""))
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
@ -17,9 +18,8 @@ import androidx.lifecycle.Observer
|
|||||||
import de.mm20.launcher2.search.data.UnitConverter
|
import de.mm20.launcher2.search.data.UnitConverter
|
||||||
import de.mm20.launcher2.ui.LegacyLauncherTheme
|
import de.mm20.launcher2.ui.LegacyLauncherTheme
|
||||||
import de.mm20.launcher2.ui.databinding.ViewUnitconverterBinding
|
import de.mm20.launcher2.ui.databinding.ViewUnitconverterBinding
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.search.UnitConverterItem
|
import de.mm20.launcher2.ui.search.UnitConverterItem
|
||||||
import de.mm20.launcher2.unitconverter.UnitConverterViewModel
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class UnitConverterView : FrameLayout {
|
class UnitConverterView : FrameLayout {
|
||||||
|
|
||||||
@ -36,8 +36,8 @@ class UnitConverterView : FrameLayout {
|
|||||||
private val binding = ViewUnitconverterBinding.inflate(LayoutInflater.from(context), this, true)
|
private val binding = ViewUnitconverterBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val unitConverterViewModel by (context as AppCompatActivity).viewModel<UnitConverterViewModel>()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
unitConverter = unitConverterViewModel.unitConverter
|
unitConverter = viewModel.unitConverterResult
|
||||||
unitConverter.observe(context as AppCompatActivity, Observer {
|
unitConverter.observe(context as AppCompatActivity, Observer {
|
||||||
if (it == null) visibility = View.GONE
|
if (it == null) visibility = View.GONE
|
||||||
else {
|
else {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
@ -17,24 +18,27 @@ import com.bumptech.glide.request.transition.Transition
|
|||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||||
import de.mm20.launcher2.search.WebsearchViewModel
|
|
||||||
import de.mm20.launcher2.search.data.Websearch
|
import de.mm20.launcher2.search.data.Websearch
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.databinding.ViewWebsearchBinding
|
import de.mm20.launcher2.ui.databinding.ViewWebsearchBinding
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
|
|
||||||
class WebSearchView : FrameLayout {
|
class WebSearchView : FrameLayout {
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleRes
|
||||||
|
)
|
||||||
|
|
||||||
private val websearches: LiveData<List<Websearch>>
|
private val websearches: LiveData<List<Websearch>>
|
||||||
|
|
||||||
private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true)
|
private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val viewModel: WebsearchViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
websearches = viewModel.websearches
|
websearches = viewModel.websearchResults
|
||||||
websearches.observe(context as AppCompatActivity, Observer {
|
websearches.observe(context as AppCompatActivity, Observer {
|
||||||
updateWebsearches(it)
|
updateWebsearches(it)
|
||||||
})
|
})
|
||||||
@ -48,13 +52,16 @@ class WebSearchView : FrameLayout {
|
|||||||
chip.text = search.label
|
chip.text = search.label
|
||||||
if (search.icon != null) {
|
if (search.icon != null) {
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.load(search.icon)
|
.load(search.icon)
|
||||||
.into(object : SimpleTarget<Drawable>() {
|
.into(object : SimpleTarget<Drawable>() {
|
||||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
override fun onResourceReady(
|
||||||
chip.chipIcon = resource
|
resource: Drawable,
|
||||||
}
|
transition: Transition<in Drawable>?
|
||||||
|
) {
|
||||||
|
chip.chipIcon = resource
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
chip.chipIconTint = null
|
chip.chipIconTint = null
|
||||||
} else {
|
} else {
|
||||||
chip.chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_search)
|
chip.chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_search)
|
||||||
@ -63,7 +70,8 @@ class WebSearchView : FrameLayout {
|
|||||||
}
|
}
|
||||||
chip.chipStrokeWidth = 1 * dp
|
chip.chipStrokeWidth = 1 * dp
|
||||||
chip.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
|
chip.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
|
||||||
chip.chipBackgroundColor = ContextCompat.getColorStateList(context, R.color.chip_background)
|
chip.chipBackgroundColor =
|
||||||
|
ContextCompat.getColorStateList(context, R.color.chip_background)
|
||||||
chip.setTextAppearanceResource(R.style.ChipTextAppearance)
|
chip.setTextAppearanceResource(R.style.ChipTextAppearance)
|
||||||
chip.setOnClickListener {
|
chip.setOnClickListener {
|
||||||
ActivityStarter.start(context, chip, intent = search.getLaunchIntent())
|
ActivityStarter.start(context, chip, intent = search.getLaunchIntent())
|
||||||
|
|||||||
@ -5,34 +5,38 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||||
import de.mm20.launcher2.search.data.Website
|
import de.mm20.launcher2.search.data.Website
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
import de.mm20.launcher2.websites.WebsiteViewModel
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class WebsiteView : FrameLayout {
|
class WebsiteView : FrameLayout {
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleRes
|
||||||
|
)
|
||||||
|
|
||||||
private val website: LiveData<Website?>
|
private val website: LiveData<Website?>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_search_category_single_item, this)
|
View.inflate(context, R.layout.view_search_category_single_item, this)
|
||||||
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST)
|
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST)
|
||||||
val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
val params =
|
||||||
|
ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
websiteView.layoutParams = params
|
websiteView.layoutParams = params
|
||||||
card.addView(websiteView)
|
card.addView(websiteView)
|
||||||
val viewModel: WebsiteViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
website = viewModel.website
|
website = viewModel.websiteResult
|
||||||
website.observe(context as AppCompatActivity, Observer {
|
website.observe(context as AppCompatActivity, Observer {
|
||||||
visibility = if (it == null) View.GONE else View.VISIBLE
|
visibility = if (it == null) View.GONE else View.VISIBLE
|
||||||
card.setOnClickListener { _ ->
|
card.setOnClickListener { _ ->
|
||||||
|
|||||||
@ -5,35 +5,38 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||||
import de.mm20.launcher2.search.data.Wikipedia
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
import de.mm20.launcher2.wikipedia.WikipediaViewModel
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class WikipediaView : FrameLayout {
|
class WikipediaView : FrameLayout {
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleRes
|
||||||
|
)
|
||||||
|
|
||||||
val wikipedia: LiveData<Wikipedia?>
|
val wikipedia: LiveData<Wikipedia?>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_search_category_single_item, this)
|
View.inflate(context, R.layout.view_search_category_single_item, this)
|
||||||
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST)
|
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST)
|
||||||
val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
val params =
|
||||||
|
ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
websiteView.layoutParams = params
|
websiteView.layoutParams = params
|
||||||
card.addView(websiteView)
|
card.addView(websiteView)
|
||||||
val viewModel: WikipediaViewModel by (context as AppCompatActivity).viewModel()
|
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||||
wikipedia = viewModel.wikipedia
|
wikipedia = viewModel.wikipediaResult
|
||||||
wikipedia.observe(context as AppCompatActivity, Observer {
|
wikipedia.observe(context as AppCompatActivity, {
|
||||||
visibility = if (it == null) View.GONE else View.VISIBLE
|
visibility = if (it == null) View.GONE else View.VISIBLE
|
||||||
card.setOnClickListener { _ ->
|
card.setOnClickListener { _ ->
|
||||||
ActivityStarter.start(context, websiteView, item = it)
|
ActivityStarter.start(context, websiteView, item = it)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
object ActivityStarter: KoinComponent {
|
object ActivityStarter : KoinComponent {
|
||||||
|
|
||||||
val favoritesRepository: FavoritesRepository by inject()
|
val favoritesRepository: FavoritesRepository by inject()
|
||||||
|
|
||||||
@ -46,13 +46,20 @@ object ActivityStarter: KoinComponent {
|
|||||||
initialized = true
|
initialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start(context: Context, transitionView: View, item: Searchable? = null, intent: Intent? = null, pendingIntent: PendingIntent? = null): Boolean {
|
fun start(
|
||||||
|
context: Context,
|
||||||
|
transitionView: View,
|
||||||
|
item: Searchable? = null,
|
||||||
|
intent: Intent? = null,
|
||||||
|
pendingIntent: PendingIntent? = null
|
||||||
|
): Boolean {
|
||||||
if (!initialized) throw IllegalStateException("Item starter has not been initialized properly.")
|
if (!initialized) throw IllegalStateException("Item starter has not been initialized properly.")
|
||||||
|
|
||||||
if (!startActivity(context, item, intent, pendingIntent, transitionView)) return false
|
if (!startActivity(context, item, intent, pendingIntent, transitionView)) return false
|
||||||
|
|
||||||
if (animationStyle == AppStartAnimation.SLIDE_BOTTOM || animationStyle == AppStartAnimation.FADE ||
|
if (animationStyle == AppStartAnimation.SLIDE_BOTTOM || animationStyle == AppStartAnimation.FADE ||
|
||||||
animationStyle == AppStartAnimation.M) {
|
animationStyle == AppStartAnimation.M
|
||||||
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,28 +87,28 @@ object ActivityStarter: KoinComponent {
|
|||||||
|
|
||||||
AnimatorSet().apply {
|
AnimatorSet().apply {
|
||||||
playTogether(
|
playTogether(
|
||||||
ViewPropertyObjectAnimator.animate(background)
|
ViewPropertyObjectAnimator.animate(background)
|
||||||
.scaleX(1f)
|
.scaleX(1f)
|
||||||
.scaleY(1f)
|
.scaleY(1f)
|
||||||
.translationX(0f)
|
.translationX(0f)
|
||||||
.translationY(0f)
|
.translationY(0f)
|
||||||
.setDuration(200)
|
.setDuration(200)
|
||||||
.setInterpolator(AccelerateInterpolator(0.8f))
|
.setInterpolator(AccelerateInterpolator(0.8f))
|
||||||
.get(),
|
.get(),
|
||||||
ViewPropertyObjectAnimator.animate(transitionView)
|
ViewPropertyObjectAnimator.animate(transitionView)
|
||||||
.scaleX(scale)
|
.scaleX(scale)
|
||||||
.scaleY(scale)
|
.scaleY(scale)
|
||||||
.alpha(0f)
|
.alpha(0f)
|
||||||
.translationX(x)
|
.translationX(x)
|
||||||
.translationY(y)
|
.translationY(y)
|
||||||
.setDuration(200)
|
.setDuration(200)
|
||||||
.setInterpolator(AccelerateInterpolator(0.8f))
|
.setInterpolator(AccelerateInterpolator(0.8f))
|
||||||
.get(),
|
.get(),
|
||||||
ViewPropertyObjectAnimator.animate(searchView)
|
ViewPropertyObjectAnimator.animate(searchView)
|
||||||
.scaleX(0.8f)
|
.scaleX(0.8f)
|
||||||
.scaleY(0.8f)
|
.scaleY(0.8f)
|
||||||
.alpha(0f)
|
.alpha(0f)
|
||||||
.get()
|
.get()
|
||||||
)
|
)
|
||||||
}.start()
|
}.start()
|
||||||
onResumeCallback = {
|
onResumeCallback = {
|
||||||
@ -127,10 +134,17 @@ object ActivityStarter: KoinComponent {
|
|||||||
|
|
||||||
private var onResumeCallback: (() -> Unit)? = null
|
private var onResumeCallback: (() -> Unit)? = null
|
||||||
|
|
||||||
private fun startActivity(context: Context, item: Searchable? = null, intent: Intent? = null, pendingIntent: PendingIntent? = null, sourceView: View): Boolean {
|
private fun startActivity(
|
||||||
|
context: Context,
|
||||||
|
item: Searchable? = null,
|
||||||
|
intent: Intent? = null,
|
||||||
|
pendingIntent: PendingIntent? = null,
|
||||||
|
sourceView: View
|
||||||
|
): Boolean {
|
||||||
val pos = intArrayOf(0, 0)
|
val pos = intArrayOf(0, 0)
|
||||||
sourceView.getLocationOnScreen(pos)
|
sourceView.getLocationOnScreen(pos)
|
||||||
val sourceBounds = Rect(pos[0], pos[1], pos[0] + sourceView.width, pos[1] + sourceView.height)
|
val sourceBounds =
|
||||||
|
Rect(pos[0], pos[1], pos[0] + sourceView.width, pos[1] + sourceView.height)
|
||||||
|
|
||||||
val bundle = getActivityOptions(context, sourceView, sourceBounds)?.toBundle()
|
val bundle = getActivityOptions(context, sourceView, sourceBounds)?.toBundle()
|
||||||
|
|
||||||
@ -145,7 +159,7 @@ object ActivityStarter: KoinComponent {
|
|||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
if (item.launch(context, bundle)) {
|
if (item.launch(context, bundle)) {
|
||||||
favoritesRepository.incrementLaunchCount(item)
|
favoritesRepository.incrementLaunchCounter(item)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -162,12 +176,36 @@ object ActivityStarter: KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getActivityOptions(context: Context, sourceView: View, sourceBounds: Rect?): ActivityOptionsCompat? {
|
private fun getActivityOptions(
|
||||||
|
context: Context,
|
||||||
|
sourceView: View,
|
||||||
|
sourceBounds: Rect?
|
||||||
|
): ActivityOptionsCompat? {
|
||||||
return when (animationStyle) {
|
return when (animationStyle) {
|
||||||
AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_fade_enter, R.anim.activity_start_fade_exit)
|
AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation(
|
||||||
AppStartAnimation.SLIDE_BOTTOM -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_slide_bottom_enter, R.anim.activity_start_slide_bottom_exit)
|
context,
|
||||||
AppStartAnimation.M -> sourceBounds?.let { ActivityOptionsCompat.makeClipRevealAnimation(sourceView, 0, 0, sourceView.width, sourceView.height) }
|
R.anim.activity_start_fade_enter,
|
||||||
else -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_splash2_enter, R.anim.activity_start_splash2_exit)
|
R.anim.activity_start_fade_exit
|
||||||
|
)
|
||||||
|
AppStartAnimation.SLIDE_BOTTOM -> ActivityOptionsCompat.makeCustomAnimation(
|
||||||
|
context,
|
||||||
|
R.anim.activity_start_slide_bottom_enter,
|
||||||
|
R.anim.activity_start_slide_bottom_exit
|
||||||
|
)
|
||||||
|
AppStartAnimation.M -> sourceBounds?.let {
|
||||||
|
ActivityOptionsCompat.makeClipRevealAnimation(
|
||||||
|
sourceView,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
sourceView.width,
|
||||||
|
sourceView.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> ActivityOptionsCompat.makeCustomAnimation(
|
||||||
|
context,
|
||||||
|
R.anim.activity_start_splash2_enter,
|
||||||
|
R.anim.activity_start_splash2_exit
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,14 +28,13 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.alpha
|
import androidx.core.graphics.alpha
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.*
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.transition.Scene
|
import androidx.transition.Scene
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.chip.ChipGroup
|
import com.google.android.material.chip.ChipGroup
|
||||||
import de.mm20.launcher2.badges.BadgeProvider
|
import de.mm20.launcher2.badges.BadgeProvider
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.ktx.castToOrNull
|
import de.mm20.launcher2.ktx.castToOrNull
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
@ -51,10 +50,8 @@ import de.mm20.launcher2.transition.ChangingLayoutTransition
|
|||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
import de.mm20.launcher2.ui.legacy.view.*
|
import de.mm20.launcher2.ui.legacy.view.*
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -83,9 +80,10 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
|
|||||||
shape = LauncherIconView.getDefaultShape(context)
|
shape = LauncherIconView.getDefaultShape(context)
|
||||||
icon = iconRepository.getIconIfCached(application)
|
icon = iconRepository.getIconIfCached(application)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
iconRepository.getIcon(application, (84 * rootView.dp).toInt()).collectLatest {
|
iconRepository.getIcon(application, (84 * rootView.dp).toInt())
|
||||||
icon = it
|
.collectLatest {
|
||||||
}
|
icon = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
findViewById<SwipeCardView>(R.id.appCard).also {
|
findViewById<SwipeCardView>(R.id.appCard).also {
|
||||||
@ -275,7 +273,7 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
|
|||||||
if (launcherApps.hasShortcutHostPermission()) {
|
if (launcherApps.hasShortcutHostPermission()) {
|
||||||
val shortcuts = app.shortcuts
|
val shortcuts = app.shortcuts
|
||||||
|
|
||||||
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
val repository: FavoritesRepository by inject()
|
||||||
|
|
||||||
var count = 0
|
var count = 0
|
||||||
for (si in shortcuts) {
|
for (si in shortcuts) {
|
||||||
@ -308,7 +306,7 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
|
|||||||
R.color.text_color_primary
|
R.color.text_color_primary
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val isPinned = viewModel.isPinned(si)
|
val isPinned = repository.isPinned(si).asLiveData()
|
||||||
|
|
||||||
isPinned.observe(context as LifecycleOwner, Observer {
|
isPinned.observe(context as LifecycleOwner, Observer {
|
||||||
view.isCloseIconVisible = isPinned.value == true
|
view.isCloseIconVisible = isPinned.value == true
|
||||||
@ -320,14 +318,14 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
|
|||||||
}
|
}
|
||||||
view.setOnLongClickListener {
|
view.setOnLongClickListener {
|
||||||
if (isPinned.value == true) {
|
if (isPinned.value == true) {
|
||||||
viewModel.unpinItem(si)
|
repository.unpinItem(si)
|
||||||
} else {
|
} else {
|
||||||
viewModel.pinItem(si)
|
repository.pinItem(si)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
view.setOnCloseIconClickListener {
|
view.setOnCloseIconClickListener {
|
||||||
viewModel.unpinItem(si)
|
repository.unpinItem(si)
|
||||||
view.isCloseIconVisible = false
|
view.isCloseIconVisible = false
|
||||||
}
|
}
|
||||||
appShortcuts.addView(view)
|
appShortcuts.addView(view)
|
||||||
|
|||||||
@ -14,15 +14,17 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
class SwipeCardView @JvmOverloads constructor(
|
class SwipeCardView @JvmOverloads constructor(
|
||||||
@ -376,10 +378,12 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
|
|||||||
R.drawable.ic_star_solid,
|
R.drawable.ic_star_solid,
|
||||||
ContextCompat.getColor(context, R.color.amber),
|
ContextCompat.getColor(context, R.color.amber),
|
||||||
{ false }
|
{ false }
|
||||||
) {
|
), KoinComponent {
|
||||||
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
|
||||||
|
|
||||||
private val pinned = viewModel.isPinned(searchable)
|
private val repository: FavoritesRepository by inject()
|
||||||
|
|
||||||
|
private val pinned = repository.isPinned(searchable)
|
||||||
|
.asLiveData((context as AppCompatActivity).lifecycleScope.coroutineContext)
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -392,7 +396,7 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
|
|||||||
if (pinned) {
|
if (pinned) {
|
||||||
icon = R.drawable.ic_star_outline
|
icon = R.drawable.ic_star_outline
|
||||||
action = {
|
action = {
|
||||||
viewModel.unpinItem(
|
repository.unpinItem(
|
||||||
searchable
|
searchable
|
||||||
)
|
)
|
||||||
false
|
false
|
||||||
@ -400,7 +404,7 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
|
|||||||
} else {
|
} else {
|
||||||
icon = R.drawable.ic_star_solid
|
icon = R.drawable.ic_star_solid
|
||||||
action = {
|
action = {
|
||||||
viewModel.pinItem(
|
repository.pinItem(
|
||||||
searchable
|
searchable
|
||||||
)
|
)
|
||||||
false
|
false
|
||||||
@ -413,9 +417,12 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
|
|||||||
R.drawable.ic_visibility_off,
|
R.drawable.ic_visibility_off,
|
||||||
ContextCompat.getColor(context, R.color.blue),
|
ContextCompat.getColor(context, R.color.blue),
|
||||||
{ false }
|
{ false }
|
||||||
) {
|
), KoinComponent {
|
||||||
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
|
||||||
private val hidden = viewModel.isHidden(searchable)
|
private val repository: FavoritesRepository by inject()
|
||||||
|
|
||||||
|
private val hidden = repository.isPinned(searchable)
|
||||||
|
.asLiveData((context as AppCompatActivity).lifecycleScope.coroutineContext)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
hidden.observe(context as LifecycleOwner) {
|
hidden.observe(context as LifecycleOwner) {
|
||||||
@ -427,7 +434,7 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
|
|||||||
if (hidden) {
|
if (hidden) {
|
||||||
icon = R.drawable.ic_visibility
|
icon = R.drawable.ic_visibility
|
||||||
action = {
|
action = {
|
||||||
viewModel.unhideItem(
|
repository.unhideItem(
|
||||||
searchable
|
searchable
|
||||||
)
|
)
|
||||||
true
|
true
|
||||||
@ -435,7 +442,7 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
|
|||||||
} else {
|
} else {
|
||||||
icon = R.drawable.ic_visibility_off
|
icon = R.drawable.ic_visibility_off
|
||||||
action = {
|
action = {
|
||||||
viewModel.hideItem(
|
repository.hideItem(
|
||||||
searchable
|
searchable
|
||||||
)
|
)
|
||||||
true
|
true
|
||||||
|
|||||||
@ -6,16 +6,18 @@ import android.view.View
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.appcompat.widget.TooltipCompat
|
import androidx.appcompat.widget.TooltipCompat
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.*
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class ToolbarView : LinearLayout {
|
class ToolbarView : LinearLayout {
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
@ -223,27 +225,37 @@ open class ToolbarSubaction(val title: String, var clickAction: (() -> Unit)) {
|
|||||||
class FavoriteToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
|
class FavoriteToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
|
||||||
R.drawable.ic_star_outline,
|
R.drawable.ic_star_outline,
|
||||||
context.getString(R.string.favorites_menu_pin)
|
context.getString(R.string.favorites_menu_pin)
|
||||||
) {
|
), KoinComponent {
|
||||||
|
|
||||||
private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
private val repository: FavoritesRepository by inject()
|
||||||
private val isPinned = viewModel.isPinned(item)
|
private var isPinned = false
|
||||||
|
set(value) {
|
||||||
init {
|
field = value
|
||||||
isPinned.observe(context as AppCompatActivity, Observer {
|
if (value) {
|
||||||
it ?: return@Observer
|
|
||||||
if (it) {
|
|
||||||
title = context.getString(R.string.favorites_menu_unpin)
|
title = context.getString(R.string.favorites_menu_unpin)
|
||||||
icon = R.drawable.ic_star_solid
|
icon = R.drawable.ic_star_solid
|
||||||
} else {
|
} else {
|
||||||
title = context.getString(R.string.favorites_menu_pin)
|
title = context.getString(R.string.favorites_menu_pin)
|
||||||
icon = R.drawable.ic_star_outline
|
icon = R.drawable.ic_star_outline
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
clickAction = {
|
clickAction = {
|
||||||
if (isPinned.value == true) {
|
if (isPinned) {
|
||||||
viewModel.unpinItem(item)
|
repository.unpinItem(item)
|
||||||
} else {
|
} else {
|
||||||
viewModel.pinItem(item)
|
repository.pinItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(context as LifecycleOwner).apply {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
repository.isPinned(item).collectLatest {
|
||||||
|
isPinned = it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,27 +264,40 @@ class FavoriteToolbarAction(val context: Context, val item: Searchable) : Toolba
|
|||||||
class VisibilityToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
|
class VisibilityToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
|
||||||
R.drawable.ic_visibility,
|
R.drawable.ic_visibility,
|
||||||
context.getString(R.string.menu_hide)
|
context.getString(R.string.menu_hide)
|
||||||
) {
|
), KoinComponent {
|
||||||
|
|
||||||
private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
private val repository: FavoritesRepository by inject()
|
||||||
private val isHidden = viewModel.isHidden(item)
|
private var isHidden = false
|
||||||
|
set(value) {
|
||||||
init {
|
field = value
|
||||||
isHidden.observe(context as AppCompatActivity, Observer {
|
if (value) {
|
||||||
if (it) {
|
|
||||||
title = context.getString(R.string.menu_unhide)
|
title = context.getString(R.string.menu_unhide)
|
||||||
icon = R.drawable.ic_visibility
|
icon = R.drawable.ic_visibility
|
||||||
} else {
|
} else {
|
||||||
title = context.getString(R.string.menu_hide)
|
title = context.getString(R.string.menu_hide)
|
||||||
icon = R.drawable.ic_visibility_off
|
icon = R.drawable.ic_visibility_off
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
clickAction = {
|
clickAction = {
|
||||||
if (isHidden.value == true) {
|
if (isHidden) {
|
||||||
viewModel.unhideItem(item)
|
repository.unhideItem(item)
|
||||||
} else {
|
} else {
|
||||||
viewModel.hideItem(item)
|
repository.hideItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(context as LifecycleOwner).apply {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
repository.isHidden(item).collectLatest {
|
||||||
|
isHidden = it
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,145 +1,113 @@
|
|||||||
package de.mm20.launcher2.ui.legacy.widget
|
package de.mm20.launcher2.ui.legacy.widget
|
||||||
|
|
||||||
import android.animation.LayoutTransition
|
import android.animation.LayoutTransition
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.provider.CalendarContract
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.Lifecycle
|
||||||
import de.mm20.launcher2.calendar.CalendarViewModel
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
import de.mm20.launcher2.ktx.lifecycleOwner
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
import de.mm20.launcher2.ktx.lifecycleScope
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.ui.legacy.data.InformationText
|
|
||||||
import de.mm20.launcher2.search.data.MissingPermission
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.databinding.ViewCalendarWidgetBinding
|
import de.mm20.launcher2.ui.databinding.ViewCalendarWidgetBinding
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidgetVM
|
||||||
import java.util.*
|
import de.mm20.launcher2.ui.legacy.data.InformationText
|
||||||
import kotlin.math.max
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.min
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
class CalendarWidget : LauncherWidget {
|
class CalendarWidget : LauncherWidget {
|
||||||
|
|
||||||
override val canResize: Boolean
|
override val canResize: Boolean
|
||||||
get() = false
|
get() = false
|
||||||
|
|
||||||
private val calendarEvents: LiveData<List<CalendarEvent>>
|
|
||||||
private val pinnedCalendarEvents: LiveData<List<CalendarEvent>>
|
|
||||||
|
|
||||||
private val zoneOffset = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis())
|
|
||||||
private var selectedDay = 0L
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
binding.calendarDate.text = formatDay(value)
|
|
||||||
updateEventList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var availableDays: List<Long> = listOf(0L)
|
|
||||||
set(value) {
|
|
||||||
if (value.indexOf(selectedDay) == -1) selectedDay = 0L
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
|
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
|
||||||
|
context,
|
||||||
|
attrs,
|
||||||
|
defStyleRes
|
||||||
|
)
|
||||||
|
|
||||||
private fun formatDay(day: Long): String {
|
private fun formatDay(day: LocalDate): String {
|
||||||
return when (day) {
|
val today = LocalDate.now()
|
||||||
0L -> context.getString(R.string.date_today)
|
return when {
|
||||||
1L -> context.getString(R.string.date_tomorrow)
|
today == day -> context.getString(R.string.date_today)
|
||||||
else -> DateUtils.formatDateTime(context, (getToday() + day) * (1000 * 60 * 60 * 24), DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY)
|
today.plusDays(1) == day -> context.getString(R.string.date_tomorrow)
|
||||||
|
else -> DateUtils.formatDateTime(
|
||||||
|
context,
|
||||||
|
day.atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000,
|
||||||
|
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val binding = ViewCalendarWidgetBinding.inflate(LayoutInflater.from(context), this, true)
|
private val binding =
|
||||||
|
ViewCalendarWidgetBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
|
private val viewModel: CalendarWidgetVM by (context as AppCompatActivity).viewModels()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
clipToPadding = false
|
clipToPadding = false
|
||||||
clipChildren = false
|
clipChildren = false
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
viewModel.onActive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.calendarNewEvent.setOnClickListener {
|
binding.calendarNewEvent.setOnClickListener {
|
||||||
val intent = Intent(Intent.ACTION_EDIT)
|
viewModel.createEvent(context)
|
||||||
intent.data = CalendarContract.Events.CONTENT_URI
|
|
||||||
ActivityStarter.start(context, this, intent = intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.calendarDate.setOnClickListener {
|
binding.calendarDate.setOnClickListener {
|
||||||
val menu = PopupMenu(context, binding.calendarDate)
|
val menu = PopupMenu(context, binding.calendarDate)
|
||||||
for (d in availableDays) {
|
val availableDates = viewModel.availableDates
|
||||||
|
for ((i, d) in availableDates.withIndex()) {
|
||||||
menu.menu.add(
|
menu.menu.add(
|
||||||
Menu.NONE,
|
Menu.NONE,
|
||||||
d.toInt(),
|
i,
|
||||||
Menu.NONE,
|
Menu.NONE,
|
||||||
formatDay(d)
|
formatDay(d)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
menu.setOnMenuItemClickListener {
|
menu.setOnMenuItemClickListener {
|
||||||
selectedDay = it.itemId.toLong()
|
viewModel.selectDate(availableDates[it.itemId])
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
menu.show()
|
menu.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.calendarOpenApp.setOnClickListener {
|
binding.calendarOpenApp.setOnClickListener {
|
||||||
val startMillis = System.currentTimeMillis()
|
viewModel.openCalendarApp(context)
|
||||||
val builder = CalendarContract.CONTENT_URI.buildUpon()
|
|
||||||
builder.appendPath("time")
|
|
||||||
ContentUris.appendId(builder, startMillis)
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
.setData(builder.build())
|
|
||||||
ActivityStarter.start(context, binding.calendarWidgetRoot, intent = intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.calendarDateNext.setOnClickListener {
|
binding.calendarDateNext.setOnClickListener {
|
||||||
val i = min(availableDays.lastIndex - 1, availableDays.indexOf(selectedDay))
|
viewModel.nextDay()
|
||||||
selectedDay = availableDays[i + 1]
|
|
||||||
}
|
}
|
||||||
binding.calendarDatePrev.setOnClickListener {
|
binding.calendarDatePrev.setOnClickListener {
|
||||||
val i = max(1, availableDays.indexOf(selectedDay))
|
viewModel.previousDay()
|
||||||
selectedDay = availableDays[i - 1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel()
|
val calendarEvents = viewModel.calendarEvents
|
||||||
val favViewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
val pinnedCalendarEvents = viewModel.pinnedCalendarEvents
|
||||||
calendarEvents = viewModel.upcomingCalendarEvents
|
val hiddenPastEvents = viewModel.hiddenPastEvents
|
||||||
pinnedCalendarEvents = favViewModel.pinnedCalendarEvents
|
val selectedDate = viewModel.selectedDate
|
||||||
|
|
||||||
calendarEvents.observe(context as AppCompatActivity, {
|
calendarEvents.observe(context as AppCompatActivity, {
|
||||||
if (!PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) {
|
updateEventList(it, hiddenPastEvents.value ?: 0)
|
||||||
binding.calendarWidgetList.submitItems(listOf(
|
|
||||||
MissingPermission(
|
|
||||||
context.getString(R.string.permission_calendar_widget),
|
|
||||||
PermissionsManager.CALENDAR
|
|
||||||
)
|
|
||||||
))
|
|
||||||
return@observe
|
|
||||||
}
|
|
||||||
val today = getToday()
|
|
||||||
availableDays = it
|
|
||||||
.map { ((it.startTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today }
|
|
||||||
.union(it.map { ((it.endTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today })
|
|
||||||
.union(listOf(0L))
|
|
||||||
.toSet().toList().sorted()
|
|
||||||
updateEventList()
|
|
||||||
})
|
})
|
||||||
pinnedCalendarEvents.observe(context as AppCompatActivity) {
|
pinnedCalendarEvents.observe(context as AppCompatActivity) {
|
||||||
val today = getToday()
|
binding.calendarWidgetPinnedList.submitItems(it)
|
||||||
binding.calendarWidgetPinnedList.submitItems(it.filter {
|
|
||||||
it.endTime > System.currentTimeMillis() &&
|
|
||||||
(it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) != today &&
|
|
||||||
(it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) != today
|
|
||||||
}.sortedBy { it.startTime })
|
|
||||||
if (it.isEmpty()) {
|
if (it.isEmpty()) {
|
||||||
binding.calendarWidgetPinnedList.visibility = View.GONE
|
binding.calendarWidgetPinnedList.visibility = View.GONE
|
||||||
binding.calendarUpcomingEventsTitle.visibility = View.GONE
|
binding.calendarUpcomingEventsTitle.visibility = View.GONE
|
||||||
@ -149,38 +117,41 @@ class CalendarWidget : LauncherWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedDate.observe(context as AppCompatActivity) {
|
||||||
|
binding.calendarDate.text = formatDay(it)
|
||||||
|
}
|
||||||
|
|
||||||
binding.calendarWidgetRoot.layoutTransition = LayoutTransition().apply {
|
binding.calendarWidgetRoot.layoutTransition = LayoutTransition().apply {
|
||||||
enableTransitionType(LayoutTransition.CHANGING)
|
enableTransitionType(LayoutTransition.CHANGING)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getToday(): Long {
|
private fun updateEventList(
|
||||||
return (System.currentTimeMillis() + zoneOffset) / (1000 * 60 * 60 * 24)
|
events: List<CalendarEvent>,
|
||||||
}
|
hiddenPastDayEvents: Int
|
||||||
|
) {
|
||||||
private fun updateEventList(includePastEvents: Boolean = false) {
|
val items = events.toMutableList<Searchable>()
|
||||||
val today = getToday()
|
|
||||||
val events: MutableList<Searchable> = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay || (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay }
|
|
||||||
?.toMutableList() ?: mutableListOf()
|
|
||||||
|
|
||||||
if (events.isEmpty()) {
|
if (events.isEmpty()) {
|
||||||
events.add(
|
items.add(
|
||||||
InformationText(context.getString(R.string.calendar_widget_no_events))
|
InformationText(context.getString(R.string.calendar_widget_no_events))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val pastEvents = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) < today + selectedDay && (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) > today + selectedDay }
|
|
||||||
|
|
||||||
if (pastEvents?.isNotEmpty() == true) {
|
if (hiddenPastDayEvents > 0) {
|
||||||
if (includePastEvents) {
|
items.add(
|
||||||
events.addAll(pastEvents)
|
InformationText(
|
||||||
} else {
|
resources.getQuantityString(
|
||||||
events.add(InformationText(resources.getQuantityString(R.plurals.calendar_widget_running_events, pastEvents.size, pastEvents.size)) {
|
R.plurals.calendar_widget_running_events,
|
||||||
updateEventList(true)
|
hiddenPastDayEvents,
|
||||||
|
hiddenPastDayEvents
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
viewModel.showAllEvents()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.calendarWidgetList.submitItems(events)
|
binding.calendarWidgetList.submitItems(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,12 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.applications.AppViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
||||||
val viewModel: AppViewModel = getViewModel()
|
val viewModel: SearchViewModel by viewModel()
|
||||||
val apps by viewModel.applications.observeAsState(emptyList())
|
val apps by viewModel.appResults.observeAsState(emptyList())
|
||||||
return {
|
return {
|
||||||
SearchableGrid(items = apps, listState = it)
|
SearchableGrid(items = apps, listState = it)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,19 @@
|
|||||||
package de.mm20.launcher2.ui.search
|
package de.mm20.launcher2.ui.search
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.ContentAlpha
|
|
||||||
import androidx.compose.material.LocalContentAlpha
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.calculator.CalculatorViewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.ui.component.SectionDivider
|
import de.mm20.launcher2.ui.component.SectionDivider
|
||||||
import org.koin.androidx.compose.getViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun calculatorItem(): LazyListScope.() -> Unit {
|
fun calculatorItem(): LazyListScope.() -> Unit {
|
||||||
val viewModel: CalculatorViewModel = getViewModel()
|
val viewModel: SearchViewModel by viewModel()
|
||||||
val calculator by viewModel.calculator.observeAsState()
|
val calculator by viewModel.calculatorResult.observeAsState(null)
|
||||||
return {
|
return {
|
||||||
calculator?.let {
|
calculator?.let {
|
||||||
item {
|
item {
|
||||||
|
|||||||
@ -6,14 +6,13 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
||||||
val viewModel: FavoritesViewModel = getViewModel()
|
val viewModel: SearchViewModel by viewModel()
|
||||||
|
|
||||||
val favorites by viewModel.getFavorites(5).observeAsState(emptyList())
|
val favorites by viewModel.favorites.observeAsState(emptyList())
|
||||||
return {
|
return {
|
||||||
SearchableGrid(items = favorites, listState = it)
|
SearchableGrid(items = favorites, listState = it)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,12 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.files.FilesViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun fileResults(): LazyListScope.() -> Unit {
|
fun fileResults(): LazyListScope.() -> Unit {
|
||||||
val viewModel: FilesViewModel = getViewModel()
|
val viewModel: SearchViewModel by viewModel()
|
||||||
val files by viewModel.files.observeAsState(emptyList())
|
val files by viewModel.fileResults.observeAsState(emptyList())
|
||||||
return {
|
return {
|
||||||
files?.let { SearchableList(items = it) }
|
files?.let { SearchableList(items = it) }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,13 +7,13 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.ui.component.SectionDivider
|
import de.mm20.launcher2.ui.component.SectionDivider
|
||||||
import de.mm20.launcher2.wikipedia.WikipediaViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.viewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun wikipediaResult(): LazyListScope.() -> Unit {
|
fun wikipediaResult(): LazyListScope.() -> Unit {
|
||||||
val viewModel: WikipediaViewModel = getViewModel()
|
val viewModel: SearchViewModel by viewModel()
|
||||||
val wikipedia by viewModel.wikipedia.observeAsState()
|
val wikipedia by viewModel.wikipediaResult.observeAsState()
|
||||||
return {
|
return {
|
||||||
wikipedia?.let {
|
wikipedia?.let {
|
||||||
item {
|
item {
|
||||||
|
|||||||
@ -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
|
package de.mm20.launcher2.ui.widget
|
||||||
|
|
||||||
import android.content.Context
|
import androidx.compose.runtime.Composable
|
||||||
import android.text.format.DateUtils
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.DropdownMenu
|
|
||||||
import androidx.compose.material.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import de.mm20.launcher2.calendar.CalendarViewModel
|
|
||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
|
||||||
import de.mm20.launcher2.ui.InformationText
|
|
||||||
import de.mm20.launcher2.ui.LauncherTheme
|
|
||||||
import de.mm20.launcher2.ui.R
|
|
||||||
import de.mm20.launcher2.ui.pluralResource
|
|
||||||
import de.mm20.launcher2.ui.searchable.DeprecatedSearchableList
|
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CalendarWidget() {
|
fun CalendarWidget() {
|
||||||
|
|
||||||
val viewModel: CalendarViewModel = getViewModel()
|
|
||||||
val favViewModel: FavoritesViewModel = viewModel()
|
|
||||||
|
|
||||||
val events by viewModel.upcomingCalendarEvents.observeAsState()
|
|
||||||
val pinnedEvents by favViewModel.pinnedCalendarEvents.observeAsState(emptyList())
|
|
||||||
val today = getToday()
|
|
||||||
val availableDays: List<Long> = remember(events) {
|
|
||||||
events?.map { ((it.startTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today }
|
|
||||||
?.union(events?.map { ((it.endTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today }
|
|
||||||
?: emptyList())
|
|
||||||
?.union(listOf(0L))
|
|
||||||
?.toSet()?.toList()?.sorted() ?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedDay by remember { mutableStateOf(0L) }
|
|
||||||
|
|
||||||
var showAll by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val pastEvents =
|
|
||||||
events?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) < today + selectedDay && (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) > today + selectedDay }
|
|
||||||
|
|
||||||
var noEvents = true
|
|
||||||
val selectedEvents = remember(today, events, selectedDay, showAll) {
|
|
||||||
events
|
|
||||||
?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay || (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay }
|
|
||||||
?.also { noEvents = it.isEmpty() }
|
|
||||||
?.union(if (showAll && pastEvents != null) pastEvents else emptyList())?.toList()
|
|
||||||
?: listOf<Searchable>()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(8.dp)
|
|
||||||
) {
|
|
||||||
DaySelector(
|
|
||||||
availableDays = availableDays,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
onSelectDay = {
|
|
||||||
selectedDay = it
|
|
||||||
showAll = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
IconButton(onClick = { /*TODO*/ }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.OpenInNew,
|
|
||||||
contentDescription = stringResource(R.string.calendar_menu_open_externally),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { /*TODO*/ }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Add,
|
|
||||||
contentDescription = stringResource(R.string.calendar_widget_new_event)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
if (noEvents) {
|
|
||||||
InformationText(
|
|
||||||
text = stringResource(id = R.string.calendar_widget_no_events)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DeprecatedSearchableList(
|
|
||||||
items = selectedEvents,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
if (!showAll && pastEvents?.isNotEmpty() == true) {
|
|
||||||
InformationText(
|
|
||||||
text = pluralResource(
|
|
||||||
R.plurals.calendar_widget_running_events,
|
|
||||||
pastEvents.size,
|
|
||||||
pastEvents.size
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
|
||||||
onClick = {
|
|
||||||
showAll = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (pinnedEvents.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.calendar_widget_pinned_events),
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
DeprecatedSearchableList(
|
|
||||||
items = pinnedEvents,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DaySelector(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
availableDays: List<Long>,
|
|
||||||
onSelectDay: (Long) -> Unit
|
|
||||||
) {
|
|
||||||
var menuExpanded by remember { mutableStateOf(false) }
|
|
||||||
var selectedDay by remember { mutableStateOf(0L) }
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = modifier,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
IconButton(onClick = {
|
|
||||||
val i = max(1, availableDays.indexOf(selectedDay))
|
|
||||||
selectedDay = availableDays[i - 1]
|
|
||||||
onSelectDay(selectedDay)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.ChevronLeft,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f),
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = {
|
|
||||||
menuExpanded = true
|
|
||||||
})
|
|
||||||
.padding(all = 12.dp)
|
|
||||||
.wrapContentWidth()
|
|
||||||
.animateContentSize()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.wrapContentWidth(),
|
|
||||||
text = formatDay(LocalContext.current, selectedDay),
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.ArrowDropDown,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DropdownMenu(expanded = menuExpanded, onDismissRequest = {
|
|
||||||
menuExpanded = false
|
|
||||||
}) {
|
|
||||||
|
|
||||||
for (day in availableDays) {
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
selectedDay = day
|
|
||||||
menuExpanded = false
|
|
||||||
onSelectDay(selectedDay)
|
|
||||||
}) {
|
|
||||||
Text(
|
|
||||||
text = formatDay(LocalContext.current, day),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
IconButton(onClick = {
|
|
||||||
val i = min(availableDays.lastIndex - 1, availableDays.indexOf(selectedDay))
|
|
||||||
selectedDay = availableDays[i + 1]
|
|
||||||
onSelectDay(selectedDay)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.ChevronRight,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getToday(): Long {
|
|
||||||
return (System.currentTimeMillis() + zoneOffset) / (1000 * 60 * 60 * 24)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val zoneOffset
|
|
||||||
get() = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis()).toLong()
|
|
||||||
|
|
||||||
private fun formatDay(context: Context, day: Long): String {
|
|
||||||
return when (day) {
|
|
||||||
0L -> context.getString(R.string.date_today)
|
|
||||||
1L -> context.getString(R.string.date_tomorrow)
|
|
||||||
else -> DateUtils.formatDateTime(
|
|
||||||
context,
|
|
||||||
(getToday() + day) * (1000 * 60 * 60 * 24),
|
|
||||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object CalendarWidgetShim {
|
|
||||||
fun getLegacyView(context: Context): View {
|
|
||||||
val composeView = ComposeView(context)
|
|
||||||
composeView.id = FrameLayout.generateViewId()
|
|
||||||
composeView.setContent {
|
|
||||||
LauncherTheme {
|
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
|
||||||
Column {
|
|
||||||
CalendarWidget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return composeView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,9 +9,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import de.mm20.launcher2.calendar.CalendarViewModel
|
|
||||||
import de.mm20.launcher2.ui.component.TextClock
|
import de.mm20.launcher2.ui.component.TextClock
|
||||||
import org.koin.androidx.compose.getViewModel
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DatePart() {
|
fun DatePart() {
|
||||||
|
|||||||
@ -7,6 +7,5 @@ import org.koin.dsl.module
|
|||||||
|
|
||||||
val unitConverterModule = module {
|
val unitConverterModule = module {
|
||||||
single { CurrencyRepository(androidContext()) }
|
single { CurrencyRepository(androidContext()) }
|
||||||
single { UnitConverterRepository(androidContext()) }
|
single<UnitConverterRepository> { UnitConverterRepositoryImpl(androidContext()) }
|
||||||
viewModel { UnitConverterViewModel(get()) }
|
|
||||||
}
|
}
|
||||||
@ -2,15 +2,19 @@ package de.mm20.launcher2.unitconverter
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import de.mm20.launcher2.currencies.CurrencyRepository
|
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.data.UnitConverter
|
import de.mm20.launcher2.search.data.UnitConverter
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
|
||||||
class UnitConverterRepository(val context: Context) : BaseSearchableRepository() {
|
interface UnitConverterRepository {
|
||||||
|
fun search(query:String): Flow<UnitConverter?>
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnitConverterRepositoryImpl(val context: Context) : UnitConverterRepository {
|
||||||
|
|
||||||
val unitConverter = MutableLiveData<UnitConverter?>()
|
val unitConverter = MutableLiveData<UnitConverter?>()
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
override fun search(query: String): Flow<UnitConverter?> = channelFlow {
|
||||||
unitConverter.value = UnitConverter.search(context, query)
|
send(UnitConverter.search(context, query))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val websitesModule = module {
|
val websitesModule = module {
|
||||||
single { WebsiteRepository(androidContext()) }
|
single<WebsiteRepository> { WebsiteRepositoryImpl(androidContext()) }
|
||||||
viewModel {
|
|
||||||
WebsiteViewModel(get())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,17 +1,19 @@
|
|||||||
package de.mm20.launcher2.websites
|
package de.mm20.launcher2.websites
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.data.Website
|
import de.mm20.launcher2.search.data.Website
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class WebsiteRepository(val context: Context) : BaseSearchableRepository() {
|
interface WebsiteRepository {
|
||||||
|
fun search(query: String): Flow<Website?>
|
||||||
|
}
|
||||||
|
|
||||||
val website = MutableLiveData<Website?>()
|
class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository {
|
||||||
|
|
||||||
private val httpClient = OkHttpClient
|
private val httpClient = OkHttpClient
|
||||||
.Builder()
|
.Builder()
|
||||||
@ -20,8 +22,8 @@ class WebsiteRepository(val context: Context) : BaseSearchableRepository() {
|
|||||||
.writeTimeout(1000, TimeUnit.MILLISECONDS)
|
.writeTimeout(1000, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun onCancel() {
|
override fun search(query: String): Flow<Website?> = channelFlow {
|
||||||
super.onCancel()
|
send(null)
|
||||||
httpClient.dispatcher.run {
|
httpClient.dispatcher.run {
|
||||||
runningCalls().forEach {
|
runningCalls().forEach {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
@ -30,14 +32,10 @@ class WebsiteRepository(val context: Context) : BaseSearchableRepository() {
|
|||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (query.isBlank()) return@channelFlow
|
||||||
|
val website = withContext(Dispatchers.IO) {
|
||||||
override suspend fun search(query: String) {
|
|
||||||
website.value = null
|
|
||||||
if (query.isBlank()) return
|
|
||||||
val wiki = withContext(Dispatchers.IO) {
|
|
||||||
Website.search(context, query, httpClient)
|
Website.search(context, query, httpClient)
|
||||||
}
|
}
|
||||||
website.value = wiki
|
send(website)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 {
|
val widgetsModule = module {
|
||||||
single { WidgetRepository(androidContext()) }
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.calendar.CalendarRepository
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class WidgetViewModel(
|
class WidgetViewModel(
|
||||||
private val widgetRepository: WidgetRepository,
|
private val widgetRepository: WidgetRepository
|
||||||
private val calendarRepository: CalendarRepository
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
||||||
@ -29,7 +27,4 @@ class WidgetViewModel(
|
|||||||
return widgetRepository.getInternalWidgets()
|
return widgetRepository.getInternalWidgets()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestCalendarUpdate() {
|
|
||||||
calendarRepository.requestCalendarUpdate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -5,6 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val wikipediaModule = module {
|
val wikipediaModule = module {
|
||||||
single { WikipediaRepository(androidContext()) }
|
single<WikipediaRepository> { WikipediaRepositoryImpl(androidContext()) }
|
||||||
viewModel { WikipediaViewModel(get()) }
|
|
||||||
}
|
}
|
||||||
@ -1,19 +1,23 @@
|
|||||||
package de.mm20.launcher2.wikipedia
|
package de.mm20.launcher2.wikipedia
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
|
||||||
import de.mm20.launcher2.search.data.Wikipedia
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
|
interface WikipediaRepository {
|
||||||
|
fun search(query: String): Flow<Wikipedia?>
|
||||||
|
}
|
||||||
|
|
||||||
val wikipedia = MutableLiveData<Wikipedia?>()
|
class WikipediaRepositoryImpl(
|
||||||
|
private val context: Context
|
||||||
|
): WikipediaRepository {
|
||||||
|
|
||||||
private val httpClient by lazy {
|
private val httpClient by lazy {
|
||||||
OkHttpClient
|
OkHttpClient
|
||||||
@ -24,7 +28,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val retrofit by lazy {
|
private val retrofit by lazy {
|
||||||
Retrofit.Builder()
|
Retrofit.Builder()
|
||||||
.client(httpClient)
|
.client(httpClient)
|
||||||
.baseUrl(context.getString(R.string.wikipedia_url))
|
.baseUrl(context.getString(R.string.wikipedia_url))
|
||||||
@ -36,9 +40,9 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
|
|||||||
retrofit.create(WikipediaApi::class.java)
|
retrofit.create(WikipediaApi::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel() {
|
|
||||||
super.onCancel()
|
|
||||||
|
|
||||||
|
override fun search(query: String): Flow<Wikipedia?> = channelFlow {
|
||||||
|
send(null)
|
||||||
httpClient.dispatcher.run {
|
httpClient.dispatcher.run {
|
||||||
runningCalls().forEach {
|
runningCalls().forEach {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
@ -47,20 +51,17 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
|
|||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String) {
|
if (query.isBlank()) return@channelFlow
|
||||||
wikipedia.value = null
|
|
||||||
if (query.isBlank()) return
|
|
||||||
|
|
||||||
val result = try {
|
val result = try {
|
||||||
wikipediaService.search(query)
|
wikipediaService.search(query)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
CrashReporter.logException(e)
|
CrashReporter.logException(e)
|
||||||
return
|
return@channelFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return
|
val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return@channelFlow
|
||||||
|
|
||||||
val image = if (LauncherPreferences.instance.searchWikipediaPictures) {
|
val image = if (LauncherPreferences.instance.searchWikipediaPictures) {
|
||||||
val width = context.resources.displayMetrics.widthPixels / 2
|
val width = context.resources.displayMetrics.widthPixels / 2
|
||||||
@ -68,7 +69,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
|
|||||||
wikipediaService.getPageImage(page.pageid, width)
|
wikipediaService.getPageImage(page.pageid, width)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
CrashReporter.logException(e)
|
CrashReporter.logException(e)
|
||||||
return
|
return@channelFlow
|
||||||
}
|
}
|
||||||
imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source
|
imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source
|
||||||
} else null
|
} else null
|
||||||
@ -79,7 +80,7 @@ class WikipediaRepository(val context: Context) : BaseSearchableRepository() {
|
|||||||
text = page.extract,
|
text = page.extract,
|
||||||
image = image
|
image = image
|
||||||
)
|
)
|
||||||
|
send(wiki)
|
||||||
wikipedia.value = wiki
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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