Refactor search, favorites and calendar widget

This commit is contained in:
MM20
2021-12-31 20:16:08 +01:00
parent 0193d4e495
commit a2ccef64ce
81 changed files with 1335 additions and 1671 deletions
@@ -16,8 +16,8 @@ import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
@@ -26,11 +26,11 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.theme.divider
import org.koin.androidx.compose.getViewModel
import org.koin.androidx.compose.inject
import kotlin.math.roundToInt
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@@ -41,19 +41,19 @@ fun DefaultSwipeActions(
enabled: Boolean = true,
content: @Composable RowScope.() -> Unit
) {
val viewModel: FavoritesViewModel = getViewModel()
val repository: FavoritesRepository by inject()
val isPinned by viewModel.isPinned(item).observeAsState()
val isHidden by viewModel.isHidden(item).observeAsState()
val isPinned by repository.isPinned(item).collectAsState(false)
val isHidden by repository.isHidden(item).collectAsState(false)
val state = androidx.compose.material.rememberSwipeableState(
SwipeAction.Default,
confirmStateChange = {
if (it == SwipeAction.Favorites) {
if (isPinned == true) {
viewModel.unpinItem(item)
repository.unpinItem(item)
} else {
viewModel.pinItem(item)
repository.pinItem(item)
}
}
false
@@ -31,11 +31,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.locals.LocalWindowSize
import org.koin.androidx.compose.getViewModel
import org.koin.androidx.compose.viewModel
/**
* Search bar
@@ -54,7 +55,7 @@ fun SearchBar(
) {
var searchQuery by remember { mutableStateOf("") }
val viewModel: SearchViewModel = getViewModel()
val viewModel: SearchViewModel by viewModel()
LaunchedEffect(searchQuery) {
viewModel.search(searchQuery)
@@ -14,11 +14,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.integerResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import org.koin.androidx.compose.getViewModel
import org.koin.androidx.compose.inject
import kotlin.math.min
@Composable
@@ -165,8 +165,8 @@ data class ToggleToolbarAction(
@Composable
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel: FavoritesViewModel = getViewModel()
val isPinned by viewModel.isPinned(item).observeAsState(false)
val viewModel: FavoritesRepository by inject()
val isPinned by viewModel.isPinned(item).collectAsState(false)
return ToggleToolbarAction(
label = stringResource(
@@ -186,8 +186,8 @@ fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
@Composable
fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
val viewModel: FavoritesViewModel = getViewModel()
val isHidden by viewModel.isHidden(item).observeAsState(false)
val viewModel: FavoritesRepository by inject()
val isHidden by viewModel.isHidden(item).collectAsState(false)
return ToggleToolbarAction(
label = stringResource(
@@ -0,0 +1,23 @@
package de.mm20.launcher2.ui.launcher.modals
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import de.mm20.launcher2.favorites.FavoritesItem
import de.mm20.launcher2.favorites.FavoritesRepository
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class EditFavoritesVM : ViewModel(), KoinComponent {
private val repository: FavoritesRepository by inject()
suspend fun getFavorites(): List<FavoritesItem> {
return repository.getAllFavoriteItems()
}
fun saveFavorites(favorites: List<FavoritesItem>) {
repository.saveFavorites(favorites)
}
}
@@ -1,37 +1,28 @@
package de.mm20.launcher2.ui.legacy.component
package de.mm20.launcher2.ui.launcher.modals
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
import androidx.core.view.updateMargins
import androidx.core.widget.TextViewCompat
import com.google.android.material.textview.MaterialTextView
import de.mm20.launcher2.favorites.FavoritesItem
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ktx.setPadding
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.DialogEditFavoritesBinding
import de.mm20.launcher2.ui.databinding.EditFavoritesRowBinding
import de.mm20.launcher2.ui.databinding.EditFavoritesTitleBinding
import de.mm20.launcher2.ui.legacy.component.EditFavoritesRow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
class EditFavoritesView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
val viewModel : FavoritesViewModel by (context as AppCompatActivity).viewModel()
val viewModel: EditFavoritesVM by (context as AppCompatActivity).viewModels()
private val binding = DialogEditFavoritesBinding.inflate(LayoutInflater.from(context), this)
@@ -45,7 +36,7 @@ class EditFavoritesView @JvmOverloads constructor(
suspend fun initView() {
favorites = withContext(Dispatchers.IO) {
viewModel.getAllFavoriteItems().toMutableList()
viewModel.getFavorites().toMutableList()
}
binding.progressBar.visibility = View.GONE
binding.itemList.addView(getLabel(R.string.edit_favorites_dialog_stage0))
@@ -0,0 +1,25 @@
package de.mm20.launcher2.ui.launcher.modals
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class HiddenItemsVM: ViewModel(), KoinComponent {
private val repository: FavoritesRepository by inject()
val hiddenItems = MutableLiveData<List<Searchable>>(emptyList())
suspend fun onActive() {
withContext(Dispatchers.IO) {
repository.getHiddenItems().collectLatest {
hiddenItems.postValue(it)
}
}
}
}
@@ -0,0 +1,57 @@
package de.mm20.launcher2.ui.launcher.modals
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.setMargins
import androidx.core.widget.NestedScrollView
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.ui.legacy.search.SearchGridView
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class HiddenItemsView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : NestedScrollView(context, attrs) {
private val viewModel: HiddenItemsVM by (context as AppCompatActivity).viewModels()
init {
clipChildren = false
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val hiddenItemsGrid = SearchGridView(context)
hiddenItemsGrid.layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins((8 * dp).toInt())
}
val hiddenItems = viewModel.hiddenItems
hiddenItems.observe(context as AppCompatActivity) {
hiddenItemsGrid.submitItems(it)
}
addView(hiddenItemsGrid)
}
private var job: Job? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val job = Job()
this.job = job
lifecycleScope.launch(job) {
viewModel.onActive()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
job?.cancel()
}
}
@@ -1,17 +1,116 @@
package de.mm20.launcher2.ui.launcher.search
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.File
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.calculator.CalculatorRepository
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.contacts.ContactRepository
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.files.FileRepository
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.WebsearchRepository
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.unitconverter.UnitConverterRepository
import de.mm20.launcher2.websites.WebsiteRepository
import de.mm20.launcher2.wikipedia.WikipediaRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SearchViewModel: ViewModel(), KoinComponent {
class SearchViewModel : ViewModel(), KoinComponent {
fun search(query: String) {
private val favoritesRepository: FavoritesRepository by inject()
private val calendarRepository: CalendarRepository by inject()
private val contactRepository: ContactRepository by inject()
private val appRepository: AppRepository by inject()
private val wikipediaRepository: WikipediaRepository by inject()
private val unitConverterRepository: UnitConverterRepository by inject()
private val calculatorRepository: CalculatorRepository by inject()
private val websiteRepository: WebsiteRepository by inject()
private val fileRepository: FileRepository by inject()
private val websearchRepository: WebsearchRepository by inject()
val isSearching = MutableLiveData(false)
val favorites by lazy {
favoritesRepository.getFavorites().asLiveData()
}
val appResults = MutableLiveData<List<Application>>(emptyList())
val fileResults = MutableLiveData<List<File>>(emptyList())
val contactResults = MutableLiveData<List<Contact>>(emptyList())
val calendarResults = MutableLiveData<List<CalendarEvent>>(emptyList())
val wikipediaResult = MutableLiveData<Wikipedia?>(null)
val websiteResult = MutableLiveData<Website?>(null)
val calculatorResult = MutableLiveData<Calculator?>(null)
val unitConverterResult = MutableLiveData<UnitConverter?>(null)
val websearchResults = MutableLiveData<List<Websearch>>(emptyList())
val hideFavorites = MutableLiveData(false)
var searchJob: Job? = null
fun search(query: String) {
try {
searchJob?.cancel()
} catch (e: CancellationException) {
}
hideFavorites.postValue(query.isNotEmpty())
searchJob = viewModelScope.launch {
isSearching.postValue(true)
val jobs = mutableListOf<Deferred<Any>>()
jobs += async {
appRepository.search(query).collectLatest {
appResults.postValue(it)
}
}
jobs += async {
contactRepository.search(query).collectLatest {
contactResults.postValue(it)
}
}
jobs += async {
calendarRepository.search(query).collectLatest {
calendarResults.postValue(it)
}
}
jobs += async {
wikipediaRepository.search(query).collectLatest {
wikipediaResult.postValue(it)
}
}
jobs += async {
unitConverterRepository.search(query).collectLatest {
unitConverterResult.postValue(it)
}
}
jobs += async {
calculatorRepository.search(query).collectLatest {
calculatorResult.postValue(it)
}
}
jobs += async {
websiteRepository.search(query).collectLatest {
websiteResult.postValue(it)
}
}
jobs += async {
fileRepository.search(query).collectLatest {
fileResults.postValue(it)
}
}
jobs += async {
websearchRepository.search(query).collectLatest {
websearchResults.postValue(it)
}
}
jobs.map { it.await() }
isSearching.postValue(false)
}
}
private val _files = MutableLiveData<List<File>>()
val files: LiveData<List<File>> = _files
}
@@ -0,0 +1,138 @@
package de.mm20.launcher2.ui.launcher.widgets.calendar
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.provider.CalendarContract
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.CalendarEvent
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.lang.Integer.min
import java.time.*
import java.util.*
import kotlin.math.max
class CalendarWidgetVM : ViewModel(), KoinComponent {
private val calendarRepository: CalendarRepository by inject()
private val favoritesRepository: FavoritesRepository by inject()
val calendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
val pinnedCalendarEvents =
favoritesRepository.getPinnedCalendarEvents().asLiveData(viewModelScope.coroutineContext)
var availableDates = listOf(LocalDate.now())
val permissionMissing = MutableLiveData(false)
private var showRunningPastDayEvents = false
val hiddenPastEvents = MutableLiveData(0)
val selectedDate = MutableLiveData(LocalDate.now())
private var upcomingEvents: List<CalendarEvent> = emptyList()
set(value) {
field = value
val dates = value.flatMap {
val startDate =
Instant.ofEpochMilli(it.startTime).atZone(ZoneId.systemDefault()).toLocalDate()
val endDate =
Instant.ofEpochMilli(it.startTime).atZone(ZoneId.systemDefault()).toLocalDate()
return@flatMap listOf(
startDate,
endDate
)
}.union(listOf(LocalDate.now()))
.distinct()
.sorted()
availableDates = dates
val date = selectedDate.value?.takeIf { dates.contains(it) } ?: LocalDate.now()
selectDate(date)
}
fun nextDay() {
val dates = availableDates
val date = selectedDate.value ?: return
val currentIndex = dates.indexOf(date)
val index = min(currentIndex + 1, dates.lastIndex)
selectDate(dates[index])
}
fun previousDay() {
val dates = availableDates
val date = selectedDate.value ?: return
val currentIndex = dates.indexOf(date)
val index = max(currentIndex - 1, 0)
selectDate(dates[index])
}
fun selectDate(date: LocalDate) {
val dates = availableDates
showRunningPastDayEvents = false
if (dates.contains(date)) {
selectedDate.value = date
updateEvents()
}
}
fun showAllEvents() {
showRunningPastDayEvents = true
updateEvents()
}
fun createEvent(context: Context) {
val intent = Intent(Intent.ACTION_EDIT)
intent.data = CalendarContract.Events.CONTENT_URI
context.tryStartActivity(intent)
}
fun openCalendarApp(context: Context) {
val startMillis = System.currentTimeMillis()
val builder = CalendarContract.CONTENT_URI.buildUpon()
builder.appendPath("time")
ContentUris.appendId(builder, startMillis)
val intent = Intent(Intent.ACTION_VIEW)
.setData(builder.build())
context.tryStartActivity(intent)
}
private fun updateEvents() {
val date = selectedDate.value ?: return
val now = System.currentTimeMillis()
val offset = OffsetDateTime.now().offset
val dayStart = max(now, date.atStartOfDay().toEpochSecond(offset) * 1000)
val dayEnd = date.plusDays(1).atStartOfDay().toEpochSecond(offset) * 1000
var events = upcomingEvents.filter {
it.endTime >= dayStart && it.startTime < dayEnd
}
if (!showRunningPastDayEvents) {
val totalCount = events.size
events = events.filter {
it.startTime >= date.atStartOfDay().toEpochSecond(offset) * 1000
}
val hiddenCount = totalCount - events.size
hiddenPastEvents.postValue(hiddenCount)
} else {
hiddenPastEvents.postValue(0)
}
calendarEvents.postValue(events)
}
suspend fun onActive() {
calendarRepository.getUpcomingEvents().collectLatest {
upcomingEvents = it
}
}
}
@@ -14,7 +14,6 @@ import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.Point
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
@@ -23,7 +22,6 @@ import android.view.*
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.viewModels
@@ -44,7 +42,6 @@ import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.list.listItems
import com.jmedeisis.draglinearlayout.DragLinearLayout
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.icons.DynamicIconController
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.dp
@@ -53,15 +50,15 @@ import de.mm20.launcher2.ktx.isBrightColor
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.transition.OneShotLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
import de.mm20.launcher2.ui.legacy.component.EditFavoritesView
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
import de.mm20.launcher2.ui.launcher.modals.HiddenItemsView
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.component.WidgetView
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
import de.mm20.launcher2.ui.legacy.search.SearchGridView
import de.mm20.launcher2.weather.WeatherViewModel
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType
@@ -79,37 +76,37 @@ class LauncherActivity : AppCompatActivity() {
* True if the search result list is visible
*/
private var searchVisibility = false
set(value) {
field = value
windowBackgroundBlur = value
}
set(value) {
field = value
windowBackgroundBlur = value
}
private lateinit var widgetHost: AppWidgetHost
private val widgets = mutableListOf<Widget>()
private lateinit var overlayView: ViewGroupOverlay
private val searchViewModel: SearchViewModel by viewModel()
private val widgetViewModel: WidgetViewModel by viewModel()
private val favoritesViewModel: FavoritesViewModel by viewModel()
private val searchViewModel: SearchViewModel by viewModels()
private val preferences = LauncherPreferences.instance
private var windowBackgroundBlur: Boolean = false
set(value) {
if(field == value) return
field = value
if (!isAtLeastApiLevel(31)) return
window.attributes = window.attributes.also {
if (value) {
it.blurBehindRadius = (32 * dp).toInt()
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
} else {
it.blurBehindRadius = 0
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
set(value) {
if (field == value) return
field = value
if (!isAtLeastApiLevel(31)) return
window.attributes = window.attributes.also {
if (value) {
it.blurBehindRadius = (32 * dp).toInt()
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
} else {
it.blurBehindRadius = 0
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
}
}
}
}
private var widgetEditMode = false
set(value) {
@@ -318,28 +315,11 @@ class LauncherActivity : AppCompatActivity() {
)
}
R.id.menu_item_hidden -> {
val layout = NestedScrollView(this)
layout.clipChildren = false
layout.layoutParams = ViewGroup.LayoutParams(
MATCH_PARENT,
WRAP_CONTENT
)
val hiddenItemsGrid = SearchGridView(this)
hiddenItemsGrid.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT,
WRAP_CONTENT
).apply {
setMargins((8 * dp).toInt())
}
val hiddenItems = favoritesViewModel.hiddenItems
hiddenItems.observe(this) {
hiddenItemsGrid.submitItems(it)
}
layout.addView(hiddenItemsGrid)
val view = HiddenItemsView(this)
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
.show {
title(R.string.menu_hidden_items)
customView(view = layout)
customView(view = view)
negativeButton(R.string.close) { dismiss() }
}
//hiddenAppsActivated = true
@@ -584,8 +564,6 @@ class LauncherActivity : AppCompatActivity() {
ActivityStarter.create(binding.rootView)
binding.activityStartOverlay.visibility = View.INVISIBLE
val widgetViewModel by viewModels<WidgetViewModel>()
widgetViewModel.requestCalendarUpdate()
search(binding.searchBar.getSearchQuery())
updateSystemBarAppearance()
@@ -670,12 +648,8 @@ class LauncherActivity : AppCompatActivity() {
PermissionsManager.LOCATION -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
}
PermissionsManager.CALENDAR -> {
widgetViewModel.requestCalendarUpdate()
}
PermissionsManager.ALL -> {
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
widgetViewModel.requestCalendarUpdate()
search(binding.searchBar.getSearchQuery())
}
}
@@ -760,7 +734,8 @@ class LauncherActivity : AppCompatActivity() {
binding.container.translationY = binding.searchBar.height.toFloat()
}
windowBackgroundBlur = searchVisibility || newTransY > 0.6 * binding.searchBar.height
windowBackgroundBlur =
searchVisibility || newTransY > 0.6 * binding.searchBar.height
if (binding.container.translationY == 0f) return@onTouch false
}
@@ -6,12 +6,12 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.*
import de.mm20.launcher2.applications.AppViewModel
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.ui.databinding.ViewApplicationBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
class ApplicationView : FrameLayout {
@@ -27,8 +27,8 @@ class ApplicationView : FrameLayout {
layoutTransition = LayoutTransition()
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
binding.applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val viewModel: AppViewModel by (context as AppCompatActivity).viewModel()
applications = viewModel.applications
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
applications = viewModel.appResults
applications.observe(context as AppCompatActivity, Observer<List<Application>> {
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
binding.applicationGrid.submitItems(it)
@@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalContentColor
@@ -14,29 +15,29 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.calculator.CalculatorViewModel
import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.databinding.ViewCalculatorBinding
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.search.CalculatorItem
import de.mm20.launcher2.ui.search.UnitConverterItem
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.math.round
class CalculatorView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
private val calculator: LiveData<Calculator?>
private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true)
init {
val viewModel: CalculatorViewModel by (context as AppCompatActivity).viewModel()
calculator = viewModel.calculator
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
calculator = viewModel.calculatorResult
calculator.observe(context as AppCompatActivity, Observer {
if (it == null) visibility = View.GONE
else {
@@ -6,23 +6,26 @@ import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class CalendarView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
private val calendarEvents: LiveData<List<CalendarEvent>?>
@@ -33,21 +36,27 @@ class CalendarView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list)
val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel()
calendarEvents = viewModel.calendarEvents
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
calendarEvents = viewModel.calendarResults
calendarEvents.observe(context as AppCompatActivity, {
if (it == null) {
visibility = View.GONE
return@observe
}
if (it.isEmpty() && LauncherPreferences.instance.searchCalendars && !PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) {
if (it.isEmpty() && LauncherPreferences.instance.searchCalendars && !PermissionsManager.checkPermission(
context,
PermissionsManager.CALENDAR
)
) {
visibility = View.VISIBLE
list.submitItems(listOf(
list.submitItems(
listOf(
MissingPermission(
context.getString(R.string.permission_calendar_search),
PermissionsManager.CALENDAR
context.getString(R.string.permission_calendar_search),
PermissionsManager.CALENDAR
)
))
)
)
return@observe
}
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
@@ -6,24 +6,28 @@ import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.SearchView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.contacts.ContactViewModel
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class ContactView : FrameLayout {
private val contacts: LiveData<List<Contact>?>
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
init {
View.inflate(context, R.layout.view_search_category_list, this)
@@ -31,22 +35,28 @@ class ContactView : FrameLayout {
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val viewModel: ContactViewModel by (context as AppCompatActivity).viewModel()
contacts = viewModel.contacts
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
contacts = viewModel.contactResults
val list = findViewById<SearchListView>(R.id.list)
contacts.observe(context as AppCompatActivity, {
if (it == null) {
visibility = View.GONE
return@observe
}
if (it.isEmpty() && LauncherPreferences.instance.searchContacts && !PermissionsManager.checkPermission(context, PermissionsManager.CONTACTS)) {
if (it.isEmpty() && LauncherPreferences.instance.searchContacts && !PermissionsManager.checkPermission(
context,
PermissionsManager.CONTACTS
)
) {
visibility = View.VISIBLE
list.submitItems(listOf(
list.submitItems(
listOf(
MissingPermission(
context.getString(R.string.permission_contact_search),
PermissionsManager.CONTACTS
context.getString(R.string.permission_contact_search),
PermissionsManager.CONTACTS
)
))
)
)
return@observe
}
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
@@ -6,14 +6,10 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.*
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewFavoritesBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
class FavoritesView : FrameLayout {
@@ -21,18 +17,21 @@ class FavoritesView : FrameLayout {
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
private val favorites: LiveData<List<Searchable>>
private val binding = ViewFavoritesBinding.inflate(LayoutInflater.from(context), this, true)
init {
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
favorites = viewModel.getFavorites(LauncherPreferences.instance.gridColumnCount)
favorites.observe(context as AppCompatActivity, Observer {
visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
val favorites = viewModel.favorites
val hide = viewModel.hideFavorites
favorites.observe(context as AppCompatActivity) {
visibility = if (it?.isEmpty() == true || hide.value == true) View.GONE else View.VISIBLE
binding.favoritesGrid.submitItems(it)
})
}
hide.observe(context as AppCompatActivity) {
visibility = if(it == true) View.GONE else View.VISIBLE
}
layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
binding.favoritesCard.layoutTransition = LayoutTransition().apply { enableTransitionType(LayoutTransition.CHANGING) }
@@ -6,22 +6,26 @@ import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import de.mm20.launcher2.files.FilesViewModel
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.search.SearchListView
import org.koin.androidx.viewmodel.ext.android.viewModel
class FileView : FrameLayout {
private val files: LiveData<List<File>?>
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
init {
View.inflate(context, R.layout.view_search_category_list, this)
@@ -30,21 +34,27 @@ class FileView : FrameLayout {
val card = findViewById<ViewGroup>(R.id.card)
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
val list = findViewById<SearchListView>(R.id.list)
val viewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
files = viewModel.files
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
files = viewModel.fileResults
files.observe(context as AppCompatActivity, {
if (it == null) {
visibility = View.GONE
return@observe
}
if (it.isEmpty() && !PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) {
if (it.isEmpty() && !PermissionsManager.checkPermission(
context,
PermissionsManager.EXTERNAL_STORAGE
)
) {
visibility = View.VISIBLE
list.submitItems(listOf(
list.submitItems(
listOf(
MissingPermission(
context.getString(R.string.permission_files_search),
PermissionsManager.EXTERNAL_STORAGE
context.getString(R.string.permission_files_search),
PermissionsManager.EXTERNAL_STORAGE
)
))
)
)
return@observe
}
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
@@ -12,21 +12,17 @@ import android.view.LayoutInflater
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.postDelayed
import androidx.lifecycle.Observer
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.SearchStyles
import de.mm20.launcher2.search.SearchViewModel
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewSearchBarBinding
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
import org.koin.androidx.viewmodel.ext.android.viewModel
class SearchBar @JvmOverloads constructor(
context: Context,
@@ -72,12 +68,6 @@ class SearchBar @JvmOverloads constructor(
})
val viewModel = (context as AppCompatActivity).viewModel<SearchViewModel>().value
viewModel.isSearching.observe(context, Observer {
binding.searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE
})
binding.overflowMenu.setOnClickListener {
if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it)
else (setSearchQuery(""))
@@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalContentColor
@@ -17,9 +18,8 @@ import androidx.lifecycle.Observer
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.databinding.ViewUnitconverterBinding
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.search.UnitConverterItem
import de.mm20.launcher2.unitconverter.UnitConverterViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class UnitConverterView : FrameLayout {
@@ -36,8 +36,8 @@ class UnitConverterView : FrameLayout {
private val binding = ViewUnitconverterBinding.inflate(LayoutInflater.from(context), this, true)
init {
val unitConverterViewModel by (context as AppCompatActivity).viewModel<UnitConverterViewModel>()
unitConverter = unitConverterViewModel.unitConverter
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
unitConverter = viewModel.unitConverterResult
unitConverter.observe(context as AppCompatActivity, Observer {
if (it == null) visibility = View.GONE
else {
@@ -7,6 +7,7 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
@@ -17,24 +18,27 @@ import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.Chip
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.WebsearchViewModel
import de.mm20.launcher2.search.data.Websearch
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewWebsearchBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
class WebSearchView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
private val websearches: LiveData<List<Websearch>>
private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true)
init {
val viewModel: WebsearchViewModel by (context as AppCompatActivity).viewModel()
websearches = viewModel.websearches
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
websearches = viewModel.websearchResults
websearches.observe(context as AppCompatActivity, Observer {
updateWebsearches(it)
})
@@ -48,13 +52,16 @@ class WebSearchView : FrameLayout {
chip.text = search.label
if (search.icon != null) {
Glide.with(context)
.load(search.icon)
.into(object : SimpleTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
chip.chipIcon = resource
}
.load(search.icon)
.into(object : SimpleTarget<Drawable>() {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
chip.chipIcon = resource
}
})
})
chip.chipIconTint = null
} else {
chip.chipIcon = ContextCompat.getDrawable(context, R.drawable.ic_search)
@@ -63,7 +70,8 @@ class WebSearchView : FrameLayout {
}
chip.chipStrokeWidth = 1 * dp
chip.chipStrokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
chip.chipBackgroundColor = ContextCompat.getColorStateList(context, R.color.chip_background)
chip.chipBackgroundColor =
ContextCompat.getColorStateList(context, R.color.chip_background)
chip.setTextAppearanceResource(R.style.ChipTextAppearance)
chip.setOnClickListener {
ActivityStarter.start(context, chip, intent = search.getLaunchIntent())
@@ -5,34 +5,38 @@ import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.websites.WebsiteViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class WebsiteView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
private val website: LiveData<Website?>
init {
View.inflate(context, R.layout.view_search_category_single_item, this)
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST)
val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val params =
ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val card = findViewById<ViewGroup>(R.id.card)
websiteView.layoutParams = params
card.addView(websiteView)
val viewModel: WebsiteViewModel by (context as AppCompatActivity).viewModel()
website = viewModel.website
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
website = viewModel.websiteResult
website.observe(context as AppCompatActivity, Observer {
visibility = if (it == null) View.GONE else View.VISIBLE
card.setOnClickListener { _ ->
@@ -5,35 +5,38 @@ import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.wikipedia.WikipediaViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class WikipediaView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
val wikipedia: LiveData<Wikipedia?>
init {
View.inflate(context, R.layout.view_search_category_single_item, this)
val websiteView = SearchableView(context, SearchableView.REPRESENTATION_LIST)
val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val params =
ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val card = findViewById<ViewGroup>(R.id.card)
websiteView.layoutParams = params
card.addView(websiteView)
val viewModel: WikipediaViewModel by (context as AppCompatActivity).viewModel()
wikipedia = viewModel.wikipedia
wikipedia.observe(context as AppCompatActivity, Observer {
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
wikipedia = viewModel.wikipediaResult
wikipedia.observe(context as AppCompatActivity, {
visibility = if (it == null) View.GONE else View.VISIBLE
card.setOnClickListener { _ ->
ActivityStarter.start(context, websiteView, item = it)
@@ -22,7 +22,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.lang.ref.WeakReference
object ActivityStarter: KoinComponent {
object ActivityStarter : KoinComponent {
val favoritesRepository: FavoritesRepository by inject()
@@ -46,13 +46,20 @@ object ActivityStarter: KoinComponent {
initialized = true
}
fun start(context: Context, transitionView: View, item: Searchable? = null, intent: Intent? = null, pendingIntent: PendingIntent? = null): Boolean {
fun start(
context: Context,
transitionView: View,
item: Searchable? = null,
intent: Intent? = null,
pendingIntent: PendingIntent? = null
): Boolean {
if (!initialized) throw IllegalStateException("Item starter has not been initialized properly.")
if (!startActivity(context, item, intent, pendingIntent, transitionView)) return false
if (animationStyle == AppStartAnimation.SLIDE_BOTTOM || animationStyle == AppStartAnimation.FADE ||
animationStyle == AppStartAnimation.M) {
animationStyle == AppStartAnimation.M
) {
return true
}
@@ -80,28 +87,28 @@ object ActivityStarter: KoinComponent {
AnimatorSet().apply {
playTogether(
ViewPropertyObjectAnimator.animate(background)
.scaleX(1f)
.scaleY(1f)
.translationX(0f)
.translationY(0f)
.setDuration(200)
.setInterpolator(AccelerateInterpolator(0.8f))
.get(),
ViewPropertyObjectAnimator.animate(transitionView)
.scaleX(scale)
.scaleY(scale)
.alpha(0f)
.translationX(x)
.translationY(y)
.setDuration(200)
.setInterpolator(AccelerateInterpolator(0.8f))
.get(),
ViewPropertyObjectAnimator.animate(searchView)
.scaleX(0.8f)
.scaleY(0.8f)
.alpha(0f)
.get()
ViewPropertyObjectAnimator.animate(background)
.scaleX(1f)
.scaleY(1f)
.translationX(0f)
.translationY(0f)
.setDuration(200)
.setInterpolator(AccelerateInterpolator(0.8f))
.get(),
ViewPropertyObjectAnimator.animate(transitionView)
.scaleX(scale)
.scaleY(scale)
.alpha(0f)
.translationX(x)
.translationY(y)
.setDuration(200)
.setInterpolator(AccelerateInterpolator(0.8f))
.get(),
ViewPropertyObjectAnimator.animate(searchView)
.scaleX(0.8f)
.scaleY(0.8f)
.alpha(0f)
.get()
)
}.start()
onResumeCallback = {
@@ -127,10 +134,17 @@ object ActivityStarter: KoinComponent {
private var onResumeCallback: (() -> Unit)? = null
private fun startActivity(context: Context, item: Searchable? = null, intent: Intent? = null, pendingIntent: PendingIntent? = null, sourceView: View): Boolean {
private fun startActivity(
context: Context,
item: Searchable? = null,
intent: Intent? = null,
pendingIntent: PendingIntent? = null,
sourceView: View
): Boolean {
val pos = intArrayOf(0, 0)
sourceView.getLocationOnScreen(pos)
val sourceBounds = Rect(pos[0], pos[1], pos[0] + sourceView.width, pos[1] + sourceView.height)
val sourceBounds =
Rect(pos[0], pos[1], pos[0] + sourceView.width, pos[1] + sourceView.height)
val bundle = getActivityOptions(context, sourceView, sourceBounds)?.toBundle()
@@ -145,7 +159,7 @@ object ActivityStarter: KoinComponent {
if (item != null) {
if (item.launch(context, bundle)) {
favoritesRepository.incrementLaunchCount(item)
favoritesRepository.incrementLaunchCounter(item)
return true
}
return false
@@ -162,12 +176,36 @@ object ActivityStarter: KoinComponent {
}
}
private fun getActivityOptions(context: Context, sourceView: View, sourceBounds: Rect?): ActivityOptionsCompat? {
private fun getActivityOptions(
context: Context,
sourceView: View,
sourceBounds: Rect?
): ActivityOptionsCompat? {
return when (animationStyle) {
AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_fade_enter, R.anim.activity_start_fade_exit)
AppStartAnimation.SLIDE_BOTTOM -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_slide_bottom_enter, R.anim.activity_start_slide_bottom_exit)
AppStartAnimation.M -> sourceBounds?.let { ActivityOptionsCompat.makeClipRevealAnimation(sourceView, 0, 0, sourceView.width, sourceView.height) }
else -> ActivityOptionsCompat.makeCustomAnimation(context, R.anim.activity_start_splash2_enter, R.anim.activity_start_splash2_exit)
AppStartAnimation.FADE -> ActivityOptionsCompat.makeCustomAnimation(
context,
R.anim.activity_start_fade_enter,
R.anim.activity_start_fade_exit
)
AppStartAnimation.SLIDE_BOTTOM -> ActivityOptionsCompat.makeCustomAnimation(
context,
R.anim.activity_start_slide_bottom_enter,
R.anim.activity_start_slide_bottom_exit
)
AppStartAnimation.M -> sourceBounds?.let {
ActivityOptionsCompat.makeClipRevealAnimation(
sourceView,
0,
0,
sourceView.width,
sourceView.height
)
}
else -> ActivityOptionsCompat.makeCustomAnimation(
context,
R.anim.activity_start_splash2_enter,
R.anim.activity_start_splash2_exit
)
}
}
@@ -28,14 +28,13 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.core.graphics.alpha
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.*
import androidx.transition.Scene
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import de.mm20.launcher2.badges.BadgeProvider
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.ktx.castToOrNull
import de.mm20.launcher2.ktx.dp
@@ -51,10 +50,8 @@ import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
import de.mm20.launcher2.ui.legacy.view.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.concurrent.Executors
@@ -83,9 +80,10 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
shape = LauncherIconView.getDefaultShape(context)
icon = iconRepository.getIconIfCached(application)
lifecycleScope.launch {
iconRepository.getIcon(application, (84 * rootView.dp).toInt()).collectLatest {
icon = it
}
iconRepository.getIcon(application, (84 * rootView.dp).toInt())
.collectLatest {
icon = it
}
}
}
findViewById<SwipeCardView>(R.id.appCard).also {
@@ -275,7 +273,7 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
if (launcherApps.hasShortcutHostPermission()) {
val shortcuts = app.shortcuts
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
val repository: FavoritesRepository by inject()
var count = 0
for (si in shortcuts) {
@@ -308,7 +306,7 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
R.color.text_color_primary
)
)
val isPinned = viewModel.isPinned(si)
val isPinned = repository.isPinned(si).asLiveData()
isPinned.observe(context as LifecycleOwner, Observer {
view.isCloseIconVisible = isPinned.value == true
@@ -320,14 +318,14 @@ class ApplicationDetailRepresentation : Representation, KoinComponent {
}
view.setOnLongClickListener {
if (isPinned.value == true) {
viewModel.unpinItem(si)
repository.unpinItem(si)
} else {
viewModel.pinItem(si)
repository.pinItem(si)
}
true
}
view.setOnCloseIconClickListener {
viewModel.unpinItem(si)
repository.unpinItem(si)
view.isCloseIconVisible = false
}
appShortcuts.addView(view)
@@ -14,15 +14,17 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.google.android.material.card.MaterialCardView
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.ui.R
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.math.abs
class SwipeCardView @JvmOverloads constructor(
@@ -376,10 +378,12 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
R.drawable.ic_star_solid,
ContextCompat.getColor(context, R.color.amber),
{ false }
) {
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
), KoinComponent {
private val pinned = viewModel.isPinned(searchable)
private val repository: FavoritesRepository by inject()
private val pinned = repository.isPinned(searchable)
.asLiveData((context as AppCompatActivity).lifecycleScope.coroutineContext)
init {
@@ -392,7 +396,7 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
if (pinned) {
icon = R.drawable.ic_star_outline
action = {
viewModel.unpinItem(
repository.unpinItem(
searchable
)
false
@@ -400,7 +404,7 @@ class FavoriteSwipeAction(val context: Context, val searchable: Searchable) :
} else {
icon = R.drawable.ic_star_solid
action = {
viewModel.pinItem(
repository.pinItem(
searchable
)
false
@@ -413,9 +417,12 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
R.drawable.ic_visibility_off,
ContextCompat.getColor(context, R.color.blue),
{ false }
) {
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
private val hidden = viewModel.isHidden(searchable)
), KoinComponent {
private val repository: FavoritesRepository by inject()
private val hidden = repository.isPinned(searchable)
.asLiveData((context as AppCompatActivity).lifecycleScope.coroutineContext)
init {
hidden.observe(context as LifecycleOwner) {
@@ -427,7 +434,7 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
if (hidden) {
icon = R.drawable.ic_visibility
action = {
viewModel.unhideItem(
repository.unhideItem(
searchable
)
true
@@ -435,7 +442,7 @@ class HideSwipeAction(val context: Context, val searchable: Searchable) : SwipeC
} else {
icon = R.drawable.ic_visibility_off
action = {
viewModel.hideItem(
repository.hideItem(
searchable
)
true
@@ -6,16 +6,18 @@ import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.setPadding
import androidx.lifecycle.Observer
import de.mm20.launcher2.favorites.FavoritesViewModel
import androidx.lifecycle.*
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ToolbarView : LinearLayout {
constructor(context: Context) : super(context)
@@ -223,27 +225,37 @@ open class ToolbarSubaction(val title: String, var clickAction: (() -> Unit)) {
class FavoriteToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
R.drawable.ic_star_outline,
context.getString(R.string.favorites_menu_pin)
) {
), KoinComponent {
private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
private val isPinned = viewModel.isPinned(item)
init {
isPinned.observe(context as AppCompatActivity, Observer {
it ?: return@Observer
if (it) {
private val repository: FavoritesRepository by inject()
private var isPinned = false
set(value) {
field = value
if (value) {
title = context.getString(R.string.favorites_menu_unpin)
icon = R.drawable.ic_star_solid
} else {
title = context.getString(R.string.favorites_menu_pin)
icon = R.drawable.ic_star_outline
}
})
}
init {
clickAction = {
if (isPinned.value == true) {
viewModel.unpinItem(item)
if (isPinned) {
repository.unpinItem(item)
} else {
viewModel.pinItem(item)
repository.pinItem(item)
}
}
(context as LifecycleOwner).apply {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
repository.isPinned(item).collectLatest {
isPinned = it
}
}
}
}
}
@@ -252,27 +264,40 @@ class FavoriteToolbarAction(val context: Context, val item: Searchable) : Toolba
class VisibilityToolbarAction(val context: Context, val item: Searchable) : ToolbarAction(
R.drawable.ic_visibility,
context.getString(R.string.menu_hide)
) {
), KoinComponent {
private val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
private val isHidden = viewModel.isHidden(item)
init {
isHidden.observe(context as AppCompatActivity, Observer {
if (it) {
private val repository: FavoritesRepository by inject()
private var isHidden = false
set(value) {
field = value
if (value) {
title = context.getString(R.string.menu_unhide)
icon = R.drawable.ic_visibility
} else {
title = context.getString(R.string.menu_hide)
icon = R.drawable.ic_visibility_off
}
})
}
init {
clickAction = {
if (isHidden.value == true) {
viewModel.unhideItem(item)
if (isHidden) {
repository.unhideItem(item)
} else {
viewModel.hideItem(item)
repository.hideItem(item)
}
}
(context as LifecycleOwner).apply {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
repository.isHidden(item).collectLatest {
isHidden = it
}
}
}
}
}
}
@@ -1,145 +1,113 @@
package de.mm20.launcher2.ui.legacy.widget
import android.animation.LayoutTransition
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.provider.CalendarContract
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.permissions.PermissionsManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import de.mm20.launcher2.ktx.lifecycleOwner
import de.mm20.launcher2.ktx.lifecycleScope
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.ui.legacy.data.InformationText
import de.mm20.launcher2.search.data.MissingPermission
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewCalendarWidgetBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.util.*
import kotlin.math.max
import kotlin.math.min
import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidgetVM
import de.mm20.launcher2.ui.legacy.data.InformationText
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.ZoneId
class CalendarWidget : LauncherWidget {
override val canResize: Boolean
get() = false
private val calendarEvents: LiveData<List<CalendarEvent>>
private val pinnedCalendarEvents: LiveData<List<CalendarEvent>>
private val zoneOffset = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis())
private var selectedDay = 0L
set(value) {
field = value
binding.calendarDate.text = formatDay(value)
updateEventList()
}
private var availableDays: List<Long> = listOf(0L)
set(value) {
if (value.indexOf(selectedDay) == -1) selectedDay = 0L
field = value
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(
context,
attrs,
defStyleRes
)
private fun formatDay(day: Long): String {
return when (day) {
0L -> context.getString(R.string.date_today)
1L -> context.getString(R.string.date_tomorrow)
else -> DateUtils.formatDateTime(context, (getToday() + day) * (1000 * 60 * 60 * 24), DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY)
private fun formatDay(day: LocalDate): String {
val today = LocalDate.now()
return when {
today == day -> context.getString(R.string.date_today)
today.plusDays(1) == day -> context.getString(R.string.date_tomorrow)
else -> DateUtils.formatDateTime(
context,
day.atStartOfDay(ZoneId.systemDefault()).toEpochSecond() * 1000,
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY
)
}
}
private val binding = ViewCalendarWidgetBinding.inflate(LayoutInflater.from(context), this, true)
private val binding =
ViewCalendarWidgetBinding.inflate(LayoutInflater.from(context), this, true)
private val viewModel: CalendarWidgetVM by (context as AppCompatActivity).viewModels()
init {
clipToPadding = false
clipChildren = false
lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onActive()
}
}
binding.calendarNewEvent.setOnClickListener {
val intent = Intent(Intent.ACTION_EDIT)
intent.data = CalendarContract.Events.CONTENT_URI
ActivityStarter.start(context, this, intent = intent)
viewModel.createEvent(context)
}
binding.calendarDate.setOnClickListener {
val menu = PopupMenu(context, binding.calendarDate)
for (d in availableDays) {
val availableDates = viewModel.availableDates
for ((i, d) in availableDates.withIndex()) {
menu.menu.add(
Menu.NONE,
d.toInt(),
Menu.NONE,
formatDay(d)
Menu.NONE,
i,
Menu.NONE,
formatDay(d)
)
}
menu.setOnMenuItemClickListener {
selectedDay = it.itemId.toLong()
viewModel.selectDate(availableDates[it.itemId])
true
}
menu.show()
}
binding.calendarOpenApp.setOnClickListener {
val startMillis = System.currentTimeMillis()
val builder = CalendarContract.CONTENT_URI.buildUpon()
builder.appendPath("time")
ContentUris.appendId(builder, startMillis)
val intent = Intent(Intent.ACTION_VIEW)
.setData(builder.build())
ActivityStarter.start(context, binding.calendarWidgetRoot, intent = intent)
viewModel.openCalendarApp(context)
}
binding.calendarDateNext.setOnClickListener {
val i = min(availableDays.lastIndex - 1, availableDays.indexOf(selectedDay))
selectedDay = availableDays[i + 1]
viewModel.nextDay()
}
binding.calendarDatePrev.setOnClickListener {
val i = max(1, availableDays.indexOf(selectedDay))
selectedDay = availableDays[i - 1]
viewModel.previousDay()
}
val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel()
val favViewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
calendarEvents = viewModel.upcomingCalendarEvents
pinnedCalendarEvents = favViewModel.pinnedCalendarEvents
val calendarEvents = viewModel.calendarEvents
val pinnedCalendarEvents = viewModel.pinnedCalendarEvents
val hiddenPastEvents = viewModel.hiddenPastEvents
val selectedDate = viewModel.selectedDate
calendarEvents.observe(context as AppCompatActivity, {
if (!PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) {
binding.calendarWidgetList.submitItems(listOf(
MissingPermission(
context.getString(R.string.permission_calendar_widget),
PermissionsManager.CALENDAR
)
))
return@observe
}
val today = getToday()
availableDays = it
.map { ((it.startTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today }
.union(it.map { ((it.endTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today })
.union(listOf(0L))
.toSet().toList().sorted()
updateEventList()
updateEventList(it, hiddenPastEvents.value ?: 0)
})
pinnedCalendarEvents.observe(context as AppCompatActivity) {
val today = getToday()
binding.calendarWidgetPinnedList.submitItems(it.filter {
it.endTime > System.currentTimeMillis() &&
(it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) != today &&
(it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) != today
}.sortedBy { it.startTime })
binding.calendarWidgetPinnedList.submitItems(it)
if (it.isEmpty()) {
binding.calendarWidgetPinnedList.visibility = View.GONE
binding.calendarUpcomingEventsTitle.visibility = View.GONE
@@ -149,38 +117,41 @@ class CalendarWidget : LauncherWidget {
}
}
selectedDate.observe(context as AppCompatActivity) {
binding.calendarDate.text = formatDay(it)
}
binding.calendarWidgetRoot.layoutTransition = LayoutTransition().apply {
enableTransitionType(LayoutTransition.CHANGING)
}
}
private fun getToday(): Long {
return (System.currentTimeMillis() + zoneOffset) / (1000 * 60 * 60 * 24)
}
private fun updateEventList(includePastEvents: Boolean = false) {
val today = getToday()
val events: MutableList<Searchable> = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay || (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay }
?.toMutableList() ?: mutableListOf()
private fun updateEventList(
events: List<CalendarEvent>,
hiddenPastDayEvents: Int
) {
val items = events.toMutableList<Searchable>()
if (events.isEmpty()) {
events.add(
InformationText(context.getString(R.string.calendar_widget_no_events))
items.add(
InformationText(context.getString(R.string.calendar_widget_no_events))
)
}
val pastEvents = calendarEvents.value?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) < today + selectedDay && (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) > today + selectedDay }
if (pastEvents?.isNotEmpty() == true) {
if (includePastEvents) {
events.addAll(pastEvents)
} else {
events.add(InformationText(resources.getQuantityString(R.plurals.calendar_widget_running_events, pastEvents.size, pastEvents.size)) {
updateEventList(true)
if (hiddenPastDayEvents > 0) {
items.add(
InformationText(
resources.getQuantityString(
R.plurals.calendar_widget_running_events,
hiddenPastDayEvents,
hiddenPastDayEvents
)
) {
viewModel.showAllEvents()
})
}
}
binding.calendarWidgetList.submitItems(events)
binding.calendarWidgetList.submitItems(items)
}
@@ -6,13 +6,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.applications.AppViewModel
import org.koin.androidx.compose.getViewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
@Composable
fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit {
val viewModel: AppViewModel = getViewModel()
val apps by viewModel.applications.observeAsState(emptyList())
val viewModel: SearchViewModel by viewModel()
val apps by viewModel.appResults.observeAsState(emptyList())
return {
SearchableGrid(items = apps, listState = it)
}
@@ -1,30 +1,19 @@
package de.mm20.launcher2.ui.search
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.Card
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.calculator.CalculatorViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.component.SectionDivider
import org.koin.androidx.compose.getViewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
@Composable
fun calculatorItem(): LazyListScope.() -> Unit {
val viewModel: CalculatorViewModel = getViewModel()
val calculator by viewModel.calculator.observeAsState()
val viewModel: SearchViewModel by viewModel()
val calculator by viewModel.calculatorResult.observeAsState(null)
return {
calculator?.let {
item {
@@ -6,14 +6,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import org.koin.androidx.compose.getViewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
@Composable
fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit {
val viewModel: FavoritesViewModel = getViewModel()
val viewModel: SearchViewModel by viewModel()
val favorites by viewModel.getFavorites(5).observeAsState(emptyList())
val favorites by viewModel.favorites.observeAsState(emptyList())
return {
SearchableGrid(items = favorites, listState = it)
}
@@ -5,13 +5,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.files.FilesViewModel
import org.koin.androidx.compose.getViewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
@Composable
fun fileResults(): LazyListScope.() -> Unit {
val viewModel: FilesViewModel = getViewModel()
val files by viewModel.files.observeAsState(emptyList())
val viewModel: SearchViewModel by viewModel()
val files by viewModel.fileResults.observeAsState(emptyList())
return {
files?.let { SearchableList(items = it) }
}
@@ -7,13 +7,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.component.SectionDivider
import de.mm20.launcher2.wikipedia.WikipediaViewModel
import org.koin.androidx.compose.getViewModel
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
import org.koin.androidx.compose.viewModel
@Composable
fun wikipediaResult(): LazyListScope.() -> Unit {
val viewModel: WikipediaViewModel = getViewModel()
val wikipedia by viewModel.wikipedia.observeAsState()
val viewModel: SearchViewModel by viewModel()
val wikipedia by viewModel.wikipediaResult.observeAsState()
return {
wikipedia?.let {
item {
@@ -1,241 +0,0 @@
package de.mm20.launcher2.ui.searchable
import android.content.Intent
import android.net.Uri
import android.text.format.DateUtils
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Card
import androidx.compose.material3.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Star
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.DefaultSwipeActions
import de.mm20.launcher2.ui.search.Representation
import de.mm20.launcher2.ui.toPixels
import java.net.URLEncoder
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun CalendarEventItem(
event: CalendarEvent,
initialRepresentation: Representation,
modifier: Modifier
) {
var representation by remember { mutableStateOf(initialRepresentation) }
val favViewModel: FavoritesViewModel = viewModel()
val isPinned by favViewModel.isPinned(event).observeAsState()
val borderWidth = 8.dp.toPixels()
val context = LocalContext.current
DefaultSwipeActions(item = event, enabled = representation == Representation.List) {
Card(
elevation = animateDpAsState(if (representation == Representation.Full) 4.dp else 0.dp).value,
border = BorderStroke(
width = animateDpAsState(if (representation == Representation.List) 1.dp else 0.dp).value,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = animateFloatAsState(if (representation == Representation.List) 0.18f else 0f).value)
),
modifier = modifier
) {
Row(
modifier = (if (representation == Representation.List) Modifier.clickable(
onClick = {
representation = Representation.Full
}) else Modifier)
.fillMaxWidth()
.drawWithCache {
val color = Color(CalendarEvent.getDisplayColor(context, event.color))
onDrawWithContent {
drawContent()
drawRect(color, size = size.copy(width = borderWidth))
}
}
) {
Column {
Column(
modifier = Modifier.padding(vertical = 14.dp, horizontal = 16.dp)
) {
Text(
text = event.label,
style = MaterialTheme.typography.titleMedium
)
AnimatedVisibility(
representation == Representation.List
) {
Text(
text = formatEventTime(event = event),
style = MaterialTheme.typography.bodyMedium
)
}
}
AnimatedVisibility(
representation == Representation.Full
) {
Column(
modifier = Modifier.padding(start = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.ic_time),
contentDescription = null
)
Text(
text = if (event.allDay) {
stringResource(id = R.string.calendar_event_allday)
} else {
DateUtils.formatDateRange(
LocalContext.current,
event.startTime,
event.endTime,
DateUtils.FORMAT_SHOW_TIME
)
},
modifier = Modifier
.padding(start = 12.dp)
)
}
if (event.description.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.ic_description),
contentDescription = null
)
Text(
text = event.description,
modifier = Modifier.padding(start = 12.dp)
)
}
}
if (event.location.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(
"geo:0,0?q=${
URLEncoder.encode(
event.location,
"utf8"
)
}"
)
context.tryStartActivity(intent, null)
})
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.ic_location),
contentDescription = null
)
Text(
text = event.location,
modifier = Modifier.padding(start = 12.dp)
)
}
}
Row(
modifier = Modifier.padding(bottom = 4.dp)
) {
IconButton(onClick = { representation = Representation.List }) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = null
)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = {
if (isPinned == true) {
favViewModel.unpinItem(event)
} else {
favViewModel.pinItem(event)
}
}) {
Icon(
painter = if (isPinned == true) rememberVectorPainter(
Icons.Rounded.Star
) else painterResource(id = R.drawable.ic_star_outline),
contentDescription = null
)
}
}
}
}
}
}
}
}
}
@Composable
fun formatEventTime(event: CalendarEvent): String {
val isToday = DateUtils.isToday(event.startTime) && DateUtils.isToday(event.endTime)
return if (isToday) {
if (event.allDay) {
stringResource(R.string.calendar_event_allday)
} else {
DateUtils.formatDateRange(
LocalContext.current,
event.startTime,
event.endTime,
DateUtils.FORMAT_SHOW_TIME
)
}
} else {
if (event.allDay) {
DateUtils.formatDateRange(
LocalContext.current,
event.startTime,
event.endTime,
DateUtils.FORMAT_SHOW_DATE
)
} else {
DateUtils.formatDateRange(
LocalContext.current,
event.startTime,
event.endTime,
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
)
}
}
}
@@ -1,41 +0,0 @@
package de.mm20.launcher2.ui.searchable
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.search.Representation
@Deprecated("Use [SearchableList] instead")
@Composable
fun DeprecatedSearchableList(items: List<Searchable>, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.animateContentSize()
) {
for (item in items) {
Box(
modifier = Modifier.padding(bottom = 8.dp)
)
key(item.key) {
if (item is CalendarEvent) {
CalendarEventItem(
event = item,
initialRepresentation = Representation.List,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}
@@ -1,259 +1,8 @@
package de.mm20.launcher2.ui.widget
import android.content.Context
import android.text.format.DateUtils
import android.view.View
import android.widget.FrameLayout
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material3.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.favorites.FavoritesViewModel
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.InformationText
import de.mm20.launcher2.ui.LauncherTheme
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.pluralResource
import de.mm20.launcher2.ui.searchable.DeprecatedSearchableList
import org.koin.androidx.compose.getViewModel
import java.util.*
import kotlin.math.max
import kotlin.math.min
import androidx.compose.runtime.Composable
@Composable
fun CalendarWidget() {
val viewModel: CalendarViewModel = getViewModel()
val favViewModel: FavoritesViewModel = viewModel()
val events by viewModel.upcomingCalendarEvents.observeAsState()
val pinnedEvents by favViewModel.pinnedCalendarEvents.observeAsState(emptyList())
val today = getToday()
val availableDays: List<Long> = remember(events) {
events?.map { ((it.startTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today }
?.union(events?.map { ((it.endTime + zoneOffset) / (1000 * 60 * 60 * 24)) - today }
?: emptyList())
?.union(listOf(0L))
?.toSet()?.toList()?.sorted() ?: emptyList()
}
var selectedDay by remember { mutableStateOf(0L) }
var showAll by remember { mutableStateOf(false) }
val pastEvents =
events?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) < today + selectedDay && (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) > today + selectedDay }
var noEvents = true
val selectedEvents = remember(today, events, selectedDay, showAll) {
events
?.filter { (it.startTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay || (it.endTime + zoneOffset) / (1000 * 60 * 60 * 24) == today + selectedDay }
?.also { noEvents = it.isEmpty() }
?.union(if (showAll && pastEvents != null) pastEvents else emptyList())?.toList()
?: listOf<Searchable>()
}
Column(
modifier = Modifier.padding(bottom = 8.dp)
) {
Row(
modifier = Modifier.padding(8.dp)
) {
DaySelector(
availableDays = availableDays,
modifier = Modifier.weight(1f),
onSelectDay = {
selectedDay = it
showAll = false
}
)
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Rounded.OpenInNew,
contentDescription = stringResource(R.string.calendar_menu_open_externally),
)
}
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(R.string.calendar_widget_new_event)
)
}
}
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
if (noEvents) {
InformationText(
text = stringResource(id = R.string.calendar_widget_no_events)
)
}
DeprecatedSearchableList(
items = selectedEvents,
modifier = Modifier.padding(bottom = 8.dp)
)
if (!showAll && pastEvents?.isNotEmpty() == true) {
InformationText(
text = pluralResource(
R.plurals.calendar_widget_running_events,
pastEvents.size,
pastEvents.size
),
modifier = Modifier.padding(bottom = 8.dp),
onClick = {
showAll = true
}
)
}
if (pinnedEvents.isNotEmpty()) {
Text(
text = stringResource(id = R.string.calendar_widget_pinned_events),
style = MaterialTheme.typography.titleLarge
)
DeprecatedSearchableList(
items = pinnedEvents,
modifier = Modifier.padding(bottom = 8.dp)
)
}
}
}
}
@Composable
fun DaySelector(
modifier: Modifier = Modifier,
availableDays: List<Long>,
onSelectDay: (Long) -> Unit
) {
var menuExpanded by remember { mutableStateOf(false) }
var selectedDay by remember { mutableStateOf(0L) }
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {
val i = max(1, availableDays.indexOf(selectedDay))
selectedDay = availableDays[i - 1]
onSelectDay(selectedDay)
}) {
Icon(
imageVector = Icons.Rounded.ChevronLeft,
contentDescription = null
)
}
Row(
modifier = Modifier
.weight(1f),
horizontalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier
.clickable(onClick = {
menuExpanded = true
})
.padding(all = 12.dp)
.wrapContentWidth()
.animateContentSize()
) {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier
.wrapContentWidth(),
text = formatDay(LocalContext.current, selectedDay),
style = MaterialTheme.typography.titleLarge
)
Icon(
imageVector = Icons.Rounded.ArrowDropDown,
modifier = Modifier.size(24.dp),
contentDescription = null
)
}
DropdownMenu(expanded = menuExpanded, onDismissRequest = {
menuExpanded = false
}) {
for (day in availableDays) {
DropdownMenuItem(onClick = {
selectedDay = day
menuExpanded = false
onSelectDay(selectedDay)
}) {
Text(
text = formatDay(LocalContext.current, day),
style = MaterialTheme.typography.titleMedium
)
}
}
}
}
IconButton(onClick = {
val i = min(availableDays.lastIndex - 1, availableDays.indexOf(selectedDay))
selectedDay = availableDays[i + 1]
onSelectDay(selectedDay)
}) {
Icon(
imageVector = Icons.Rounded.ChevronRight,
contentDescription = null
)
}
}
}
private fun getToday(): Long {
return (System.currentTimeMillis() + zoneOffset) / (1000 * 60 * 60 * 24)
}
private val zoneOffset
get() = Calendar.getInstance().timeZone.getOffset(System.currentTimeMillis()).toLong()
private fun formatDay(context: Context, day: Long): String {
return when (day) {
0L -> context.getString(R.string.date_today)
1L -> context.getString(R.string.date_tomorrow)
else -> DateUtils.formatDateTime(
context,
(getToday() + day) * (1000 * 60 * 60 * 24),
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY
)
}
}
object CalendarWidgetShim {
fun getLegacyView(context: Context): View {
val composeView = ComposeView(context)
composeView.id = FrameLayout.generateViewId()
composeView.setContent {
LauncherTheme {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
Column {
CalendarWidget()
}
}
}
}
return composeView
}
}
@@ -9,9 +9,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import de.mm20.launcher2.calendar.CalendarViewModel
import de.mm20.launcher2.ui.component.TextClock
import org.koin.androidx.compose.getViewModel
@Composable
fun DatePart() {