Unclutter this mess we call LauncherActivity
also migrate search bar to Jetpack Compose
This commit is contained in:
parent
0447d3072d
commit
f701e90c47
@ -28,6 +28,7 @@ android {
|
|||||||
versionCode = versionCodeDate()
|
versionCode = versionCodeDate()
|
||||||
versionName = "1.3.0"
|
versionName = "1.3.0"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
android:label="@string/title_activity_settings"
|
android:label="@string/title_activity_settings"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:parentActivityName=".ui.legacy.activity.LauncherActivity"
|
android:parentActivityName=".ui.launcher.LauncherActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:taskAffinity="de.mm20.launcher2.settings"
|
android:taskAffinity="de.mm20.launcher2.settings"
|
||||||
android:theme="@style/SettingsTheme">
|
android:theme="@style/SettingsTheme">
|
||||||
@ -58,7 +58,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="de.mm20.launcher2.ui.legacy.activity.LauncherActivity" />
|
android:value="de.mm20.launcher2.ui.launcher.LauncherActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import de.mm20.launcher2.fragment.PreferencesMainFragment
|
import de.mm20.launcher2.fragment.PreferencesMainFragment
|
||||||
import de.mm20.launcher2.fragment.PreferencesServicesFragment
|
import de.mm20.launcher2.fragment.PreferencesServicesFragment
|
||||||
import de.mm20.launcher2.ui.legacy.activity.LauncherActivity
|
import de.mm20.launcher2.ui.launcher.LauncherActivity
|
||||||
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
|
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ android {
|
|||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<application>
|
<application>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".legacy.activity.LauncherActivity"
|
android:name=".launcher.LauncherActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@ -46,7 +46,7 @@
|
|||||||
android:label="@string/title_activity_settings"
|
android:label="@string/title_activity_settings"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:parentActivityName=".legacy.activity.LauncherActivity"
|
android:parentActivityName=".launcher.LauncherActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:taskAffinity="de.mm20.launcher2.settings"
|
android:taskAffinity="de.mm20.launcher2.settings"
|
||||||
android:theme="@style/SettingsTheme.NoActionBar">
|
android:theme="@style/SettingsTheme.NoActionBar">
|
||||||
@ -56,7 +56,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="de.mm20.launcher2.ui.legacy.activity.LauncherActivity" />
|
android:value="de.mm20.launcher2.ui.launcher.LauncherActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
package de.mm20.launcher2.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LauncherCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
elevation: Dp = 2.dp,
|
||||||
|
backgroundOpacity: Float = 1f,
|
||||||
|
content: @Composable () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
content = content,
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
|
||||||
|
shadowElevation = elevation,
|
||||||
|
tonalElevation = elevation
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -32,10 +32,9 @@ 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.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
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.viewModel
|
import org.koin.androidx.compose.viewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,7 +54,7 @@ fun SearchBar(
|
|||||||
) {
|
) {
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val viewModel: SearchViewModel by viewModel()
|
val viewModel: SearchVM by viewModel()
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
viewModel.search(searchQuery)
|
viewModel.search(searchQuery)
|
||||||
|
|||||||
@ -0,0 +1,162 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
|
import android.app.WallpaperManager
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.*
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.view.*
|
||||||
|
import com.afollestad.materialdialogs.LayoutMode
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
||||||
|
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||||
|
import com.afollestad.materialdialogs.customview.customView
|
||||||
|
import de.mm20.launcher2.icons.DynamicIconController
|
||||||
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
|
import de.mm20.launcher2.ktx.dp
|
||||||
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||||
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.base.BaseActivity
|
||||||
|
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
|
||||||
|
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
|
||||||
|
import de.mm20.launcher2.ui.launcher.modals.HiddenItemsView
|
||||||
|
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
|
||||||
|
import de.mm20.launcher2.widgets.Widget
|
||||||
|
import de.mm20.launcher2.widgets.WidgetViewModel
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class LauncherActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private val viewModel: LauncherActivityVM 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun updateSystemBarAppearance() {
|
||||||
|
val allowLightSystemBars = allowsLightSystemBars()
|
||||||
|
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||||
|
insetsController.isAppearanceLightNavigationBars =
|
||||||
|
allowLightSystemBars && preferences.lightNavBar
|
||||||
|
insetsController.isAppearanceLightStatusBars =
|
||||||
|
allowLightSystemBars && preferences.lightStatusBar
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityLauncherBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val iconRepository: IconRepository by inject()
|
||||||
|
iconRepository.recreate()
|
||||||
|
ThemeHelper.applyTheme(theme)
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
binding = ActivityLauncherBinding.inflate(LayoutInflater.from(this))
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
var editFavoritesDialog: MaterialDialog? = null
|
||||||
|
viewModel.isEditFavoritesShown.observe(this) {
|
||||||
|
if (it) {
|
||||||
|
val view = EditFavoritesView(this@LauncherActivity)
|
||||||
|
editFavoritesDialog =
|
||||||
|
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT)).show {
|
||||||
|
customView(view = view)
|
||||||
|
title(res = R.string.menu_item_edit_favs)
|
||||||
|
positiveButton(res = R.string.close) {
|
||||||
|
viewModel.hideEditFavorites()
|
||||||
|
it.dismiss()
|
||||||
|
}
|
||||||
|
onDismiss {
|
||||||
|
view.save()
|
||||||
|
viewModel.hideEditFavorites()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editFavoritesDialog?.dismiss()
|
||||||
|
editFavoritesDialog = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hiddenItemsDialog: MaterialDialog? = null
|
||||||
|
viewModel.isHiddenItemsShown.observe(this) {
|
||||||
|
if (it) {
|
||||||
|
val view = HiddenItemsView(this)
|
||||||
|
hiddenItemsDialog = MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
|
||||||
|
.show {
|
||||||
|
title(R.string.menu_hidden_items)
|
||||||
|
customView(view = view)
|
||||||
|
negativeButton(R.string.close) { dismiss() }
|
||||||
|
onDismiss {
|
||||||
|
viewModel.hideHiddenItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hiddenItemsDialog?.dismiss()
|
||||||
|
hiddenItemsDialog = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LauncherPreferences.instance.dimWallpaper) {
|
||||||
|
binding.dimWallpaper.setBackgroundColor(getColor(R.color.wallpaper_dim))
|
||||||
|
}
|
||||||
|
|
||||||
|
val dynamicIconController: DynamicIconController by inject()
|
||||||
|
|
||||||
|
lifecycle.addObserver(dynamicIconController)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
ActivityStarter.resume()
|
||||||
|
ActivityStarter.create(binding.rootView)
|
||||||
|
binding.activityStartOverlay.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
updateSystemBarAppearance()
|
||||||
|
|
||||||
|
binding.container.doOnNextLayout {
|
||||||
|
WallpaperManager.getInstance(this).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun allowsLightSystemBars(): Boolean {
|
||||||
|
val dimWallpaper = LauncherPreferences.instance.dimWallpaper
|
||||||
|
val isDarkTheme = resources.getBoolean(R.bool.is_dark_theme)
|
||||||
|
return !(isDarkTheme && dimWallpaper)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
ActivityStarter.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
ActivityStarter.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
|
class LauncherActivityVM : ViewModel() {
|
||||||
|
val isHiddenItemsShown = MutableLiveData(false)
|
||||||
|
val isEditFavoritesShown = MutableLiveData(false)
|
||||||
|
val dimBackground = MutableLiveData(false)
|
||||||
|
|
||||||
|
fun showEditFavorites() {
|
||||||
|
isEditFavoritesShown.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideEditFavorites() {
|
||||||
|
isEditFavoritesShown.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun showHiddenItems() {
|
||||||
|
isHiddenItemsShown.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideHiddenItems() {
|
||||||
|
isHiddenItemsShown.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import de.mm20.launcher2.ktx.isBrightColor
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||||
|
|
||||||
|
class LauncherScaffoldVM : ViewModel() {
|
||||||
|
val isSearchOpen = MutableLiveData(false)
|
||||||
|
val blurBackground = MutableLiveData(false)
|
||||||
|
|
||||||
|
val statusBarColor = MutableLiveData(0)
|
||||||
|
val darkStatusBarIcons = MutableLiveData(false)
|
||||||
|
|
||||||
|
val searchBarLevel = MutableLiveData(SearchBarLevel.Resting)
|
||||||
|
|
||||||
|
var scrollY = 0
|
||||||
|
set(value) {
|
||||||
|
if (value == 0 && field != 0) {
|
||||||
|
if (isSearchOpen.value == true) {
|
||||||
|
searchBarLevel.value = SearchBarLevel.Active
|
||||||
|
blurBackground.value = true
|
||||||
|
} else {
|
||||||
|
searchBarLevel.value = SearchBarLevel.Resting
|
||||||
|
blurBackground.value = false
|
||||||
|
}
|
||||||
|
} else if (value > 0 && field == 0) {
|
||||||
|
searchBarLevel.value = SearchBarLevel.Raised
|
||||||
|
blurBackground.value = true
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openSearch() {
|
||||||
|
if (isSearchOpen.value == true) return
|
||||||
|
isSearchOpen.value = true
|
||||||
|
if (scrollY == 0) {
|
||||||
|
searchBarLevel.value = SearchBarLevel.Active
|
||||||
|
blurBackground.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeSearch() {
|
||||||
|
if (isSearchOpen.value == false) return
|
||||||
|
isSearchOpen.value = false
|
||||||
|
if (scrollY == 0) {
|
||||||
|
searchBarLevel.value = SearchBarLevel.Resting
|
||||||
|
blurBackground.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSearch() {
|
||||||
|
if (isSearchOpen.value == true) closeSearch()
|
||||||
|
else openSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStatusBarColor(color: Int) {
|
||||||
|
statusBarColor.value = color
|
||||||
|
darkStatusBarIcons.value = color.isBrightColor()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,281 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.LayoutTransition
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.addCallback
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.animation.doOnEnd
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.core.view.setPadding
|
||||||
|
import de.mm20.launcher2.ktx.dp
|
||||||
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.ktx.isBrightColor
|
||||||
|
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||||
|
import de.mm20.launcher2.transition.OneShotLayoutTransition
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.databinding.ViewLauncherScaffoldBinding
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.widgets.WidgetsVM
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
class LauncherScaffoldView @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null
|
||||||
|
) : FrameLayout(context, attrs) {
|
||||||
|
|
||||||
|
private val binding = ViewLauncherScaffoldBinding.inflate(LayoutInflater.from(context), this)
|
||||||
|
|
||||||
|
private val viewModel: LauncherScaffoldVM by (context as AppCompatActivity).viewModels()
|
||||||
|
private val widgetsViewModel: WidgetsVM by (context as AppCompatActivity).viewModels()
|
||||||
|
private val searchViewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
|
|
||||||
|
private val scrollViewOnTouchListener = object : OnTouchListener {
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> return true
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
when {
|
||||||
|
binding.scrollView.scrollY == 0 -> {
|
||||||
|
if (event.historySize > 0) {
|
||||||
|
val dY = event.y - event.getHistoricalY(0)
|
||||||
|
val newTransY = 0.4f * dY + translationY
|
||||||
|
if (newTransY > 0 && newTransY < 48 * dp) {
|
||||||
|
translationY = newTransY
|
||||||
|
} else if (newTransY <= 0) {
|
||||||
|
translationY = 0f
|
||||||
|
} else {
|
||||||
|
translationY = 48 * dp
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (translationY == 0f) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.scrollView.scrollY == binding.scrollContainer.height - binding.scrollView.height && viewModel.isSearchOpen.value == true -> {
|
||||||
|
if (event.historySize > 0) {
|
||||||
|
val dY = event.y - event.getHistoricalY(0)
|
||||||
|
val newTransY = 0.4f * dY + translationY
|
||||||
|
|
||||||
|
if (newTransY <= 0 && newTransY > -48 * dp) {
|
||||||
|
translationY = newTransY
|
||||||
|
} else if (newTransY > 0) {
|
||||||
|
translationY = 0f
|
||||||
|
} else {
|
||||||
|
translationY = -48 * dp
|
||||||
|
}
|
||||||
|
|
||||||
|
if (translationY == 0f) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
if (translationY >= 48 * dp * 0.6) viewModel.toggleSearch()
|
||||||
|
if (translationY <= -48 * dp) viewModel.closeSearch()
|
||||||
|
animate().translationY(0f).setDuration(200).start()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
context as AppCompatActivity
|
||||||
|
|
||||||
|
context.onBackPressedDispatcher.addCallback {
|
||||||
|
viewModel.closeSearch()
|
||||||
|
widgetsViewModel.setEditMode(false)
|
||||||
|
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0).setDuration(200).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
|
|
||||||
|
binding.scrollView.scrollY = viewModel.scrollY
|
||||||
|
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
||||||
|
|
||||||
|
binding.scrollView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY: Int ->
|
||||||
|
viewModel.scrollY = scrollY
|
||||||
|
when {
|
||||||
|
/* Hide searchbar*/
|
||||||
|
scrollY > oldScrollY && ((scrollY > 48 * dp)) -> {
|
||||||
|
var newTransY = binding.searchBar.translationY - scrollY + oldScrollY
|
||||||
|
if (newTransY < -112 * dp) {
|
||||||
|
newTransY = -112 * dp
|
||||||
|
}
|
||||||
|
binding.searchBar.translationY = newTransY
|
||||||
|
}
|
||||||
|
/* Show searchbar*/
|
||||||
|
scrollY < oldScrollY -> {
|
||||||
|
var newTransY = binding.searchBar.translationY - scrollY + oldScrollY
|
||||||
|
if (newTransY > 0f) {
|
||||||
|
newTransY = 0f
|
||||||
|
}
|
||||||
|
binding.searchBar.translationY = newTransY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.isSearchOpen.observe(context) {
|
||||||
|
if (it) showSearch()
|
||||||
|
else hideSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.searchBarLevel.observe(context) {
|
||||||
|
binding.searchBar.level = it
|
||||||
|
}
|
||||||
|
|
||||||
|
searchViewModel.websearchResults.observe(context) {
|
||||||
|
binding.searchContainer.setPadding(
|
||||||
|
0,
|
||||||
|
(if (it.isEmpty()) 48 * dp else 96 * dp).toInt(),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetsViewModel.isEditMode.observe(context) {
|
||||||
|
if (it) {
|
||||||
|
binding.scrollView.setOnTouchListener(null)
|
||||||
|
OneShotLayoutTransition.run(binding.scrollContainer)
|
||||||
|
binding.searchBar.visibility = View.INVISIBLE
|
||||||
|
binding.editWidgetToolbar
|
||||||
|
.animate()
|
||||||
|
.translationY(0f)
|
||||||
|
.alpha(1f)
|
||||||
|
.withStartAction {
|
||||||
|
binding.editWidgetToolbar.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
.start()
|
||||||
|
binding.widgetContainer.setPadding(0, (56 * dp).toInt(), 0, 0)
|
||||||
|
val colorSurface = TypedValue()
|
||||||
|
context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
|
||||||
|
context.window.statusBarColor = colorSurface.data
|
||||||
|
viewModel.setStatusBarColor(colorSurface.data)
|
||||||
|
} else {
|
||||||
|
binding.widgetContainer.layoutTransition = ChangingLayoutTransition()
|
||||||
|
binding.scrollContainer.layoutTransition = ChangingLayoutTransition()
|
||||||
|
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
||||||
|
|
||||||
|
binding.searchBar.visibility = View.VISIBLE
|
||||||
|
binding.editWidgetToolbar
|
||||||
|
.animate()
|
||||||
|
.translationY(-binding.editWidgetToolbar.height.toFloat())
|
||||||
|
.alpha(0f)
|
||||||
|
.withEndAction {
|
||||||
|
binding.editWidgetToolbar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
.start()
|
||||||
|
binding.widgetContainer.setPadding(0)
|
||||||
|
viewModel.setStatusBarColor(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.editWidgetToolbar.apply {
|
||||||
|
navigationIcon =
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_done)?.apply {
|
||||||
|
setTint(ContextCompat.getColor(context, R.color.icon_color))
|
||||||
|
}
|
||||||
|
setNavigationOnClickListener {
|
||||||
|
widgetsViewModel.setEditMode(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.searchBar.onFocus = {
|
||||||
|
viewModel.openSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.blurBackground.observe(context) { blur ->
|
||||||
|
if (!isAtLeastApiLevel(31)) return@observe
|
||||||
|
context.window.attributes = context.window.attributes.also {
|
||||||
|
if (blur) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.statusBarColor.observe(context) {
|
||||||
|
context.window.statusBarColor = it
|
||||||
|
}
|
||||||
|
viewModel.darkStatusBarIcons.observe(context) {
|
||||||
|
WindowInsetsControllerCompat(context.window, this).isAppearanceLightStatusBars = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
if (changed) {
|
||||||
|
binding.widgetContainer.setClockWidgetHeight(bottom - top - paddingTop - paddingBottom)
|
||||||
|
}
|
||||||
|
super.onLayout(changed, left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideSearch() {
|
||||||
|
val set = AnimatorSet()
|
||||||
|
set.duration = 300
|
||||||
|
set.doOnEnd {
|
||||||
|
binding.searchContainer.visibility = View.GONE
|
||||||
|
binding.widgetContainer.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
set.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(binding.widgetContainer, "translationY", 0f),
|
||||||
|
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0),
|
||||||
|
ObjectAnimator.ofFloat(
|
||||||
|
binding.searchContainer, "translationY", 0f,
|
||||||
|
if (binding.scrollView.scrollY > binding.searchContainer.height / 2f) -binding.searchContainer.height.toFloat() else binding.scrollView.height.toFloat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set.doOnEnd {
|
||||||
|
searchViewModel.search("")
|
||||||
|
}
|
||||||
|
set.start()
|
||||||
|
binding.scrollView.scrollTo(0, 0)
|
||||||
|
context.getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(
|
||||||
|
binding.searchBar.windowToken,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSearch() {
|
||||||
|
binding.searchContainer.visibility = View.VISIBLE
|
||||||
|
binding.widgetContainer.visibility = View.GONE
|
||||||
|
val set = AnimatorSet()
|
||||||
|
set.duration = 300
|
||||||
|
set.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(
|
||||||
|
binding.widgetContainer,
|
||||||
|
"translationY",
|
||||||
|
binding.scrollView.height.toFloat()
|
||||||
|
),
|
||||||
|
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0),
|
||||||
|
ObjectAnimator.ofFloat(
|
||||||
|
binding.searchContainer,
|
||||||
|
"translationY",
|
||||||
|
binding.scrollView.height.toFloat(),
|
||||||
|
0f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
set.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,323 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.animateDp
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
|
||||||
|
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||||
|
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||||
|
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.material.DropdownMenu
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Search
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.focus.onFocusEvent
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.data.Websearch
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.launcher.LauncherActivityVM
|
||||||
|
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchBar(
|
||||||
|
level: SearchBarLevel,
|
||||||
|
onFocus: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val searchViewModel: SearchVM = viewModel()
|
||||||
|
val activityViewModel: LauncherActivityVM = viewModel()
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val query by searchViewModel.searchQuery.observeAsState("")
|
||||||
|
|
||||||
|
val websearches by searchViewModel.websearchResults.observeAsState(emptyList())
|
||||||
|
|
||||||
|
SearchBar(
|
||||||
|
level,
|
||||||
|
websearches,
|
||||||
|
value = query,
|
||||||
|
onValueChange = {
|
||||||
|
searchViewModel.search(it)
|
||||||
|
},
|
||||||
|
overflowMenu = { show, onDismissRequest ->
|
||||||
|
DropdownMenu(expanded = show, onDismissRequest = onDismissRequest) {
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
activityViewModel.showEditFavorites()
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.menu_item_edit_favs))
|
||||||
|
}
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
activityViewModel.showHiddenItems()
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.menu_hidden_items))
|
||||||
|
}
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
context.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_SET_WALLPAPER),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.wallpaper))
|
||||||
|
}
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
context.startActivity(Intent(context, SettingsActivity::class.java))
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.settings))
|
||||||
|
}
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
context.startActivity(Intent().also {
|
||||||
|
it.component = ComponentName(
|
||||||
|
context.packageName,
|
||||||
|
"de.mm20.launcher2.activity.SettingsActivity"
|
||||||
|
)
|
||||||
|
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
})
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text("Legacy Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFocus = onFocus,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationGraphicsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SearchBar(
|
||||||
|
level: SearchBarLevel,
|
||||||
|
websearches: List<Websearch>,
|
||||||
|
overflowMenu: @Composable (show: Boolean, onDismissRequest: () -> Unit) -> Unit = { _, _ -> },
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
onFocus: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val transition = updateTransition(level, label = "Searchbar")
|
||||||
|
|
||||||
|
|
||||||
|
val elevation by transition.animateDp(
|
||||||
|
label = "elevation",
|
||||||
|
transitionSpec = {
|
||||||
|
when {
|
||||||
|
initialState == SearchBarLevel.Resting -> tween(
|
||||||
|
durationMillis = 200,
|
||||||
|
delayMillis = 200
|
||||||
|
)
|
||||||
|
targetState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||||
|
else -> tween(durationMillis = 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
when (it) {
|
||||||
|
SearchBarLevel.Resting -> 0.dp
|
||||||
|
SearchBarLevel.Active -> 2.dp
|
||||||
|
SearchBarLevel.Raised -> 8.dp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backgroundOpacity by transition.animateFloat(label = "backgroundOpacity",
|
||||||
|
transitionSpec = {
|
||||||
|
when {
|
||||||
|
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||||
|
targetState == SearchBarLevel.Resting -> tween(
|
||||||
|
durationMillis = 200,
|
||||||
|
delayMillis = 200
|
||||||
|
)
|
||||||
|
else -> tween(durationMillis = 500)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if (it == SearchBarLevel.Resting) 0f else 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentColor by transition.animateColor(label = "textColor",
|
||||||
|
transitionSpec = {
|
||||||
|
when {
|
||||||
|
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||||
|
targetState == SearchBarLevel.Resting -> tween(
|
||||||
|
durationMillis = 200,
|
||||||
|
delayMillis = 200
|
||||||
|
)
|
||||||
|
else -> tween(durationMillis = 500)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
if (it == SearchBarLevel.Resting) Color.White else LocalContentColor.current
|
||||||
|
}
|
||||||
|
|
||||||
|
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
||||||
|
|
||||||
|
LauncherCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(8.dp),
|
||||||
|
backgroundOpacity = backgroundOpacity,
|
||||||
|
elevation = elevation
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.height(48.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
imageVector = Icons.Rounded.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = contentColor
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.edit_text_search_hint),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = contentColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
LaunchedEffect(level) {
|
||||||
|
if (level == SearchBarLevel.Resting) focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
BasicTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.onFocusChanged {
|
||||||
|
if (it.hasFocus) onFocus()
|
||||||
|
}
|
||||||
|
.fillMaxWidth(),
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
color = contentColor
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box {
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (value.isNotBlank()) onValueChange("")
|
||||||
|
else showOverflowMenu = true
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
painter = rememberAnimatedVectorPainter(
|
||||||
|
rightIcon,
|
||||||
|
atEnd = value.isNotBlank()
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = contentColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
overflowMenu(showOverflowMenu) { showOverflowMenu = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimatedVisibility(websearches.isNotEmpty()) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
items(websearches) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(32.dp)
|
||||||
|
.clickable {
|
||||||
|
it
|
||||||
|
.getLaunchIntent()
|
||||||
|
?.let {
|
||||||
|
context.tryStartActivity(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(horizontal = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val icon = it.icon
|
||||||
|
if (icon == null) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (it.color == 0) MaterialTheme.colorScheme.primary else Color(
|
||||||
|
it.color
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
painter = rememberImagePainter(File(icon)),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
it.label,
|
||||||
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SearchBarLevel {
|
||||||
|
/**
|
||||||
|
* The default, "hidden" state, when the launcher is in its initial state (scroll position is 0
|
||||||
|
* and search is closed)
|
||||||
|
*/
|
||||||
|
Resting,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the search is open but there is no content behind the search bar (scroll position is 0)
|
||||||
|
*/
|
||||||
|
Active,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When there is content below the search bar which requires the search bar to be raised above
|
||||||
|
* this content (scroll position is not 0)
|
||||||
|
*/
|
||||||
|
Raised
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class SearchViewModel : ViewModel(), KoinComponent {
|
class SearchVM : ViewModel(), KoinComponent {
|
||||||
|
|
||||||
private val favoritesRepository: FavoritesRepository by inject()
|
private val favoritesRepository: FavoritesRepository by inject()
|
||||||
|
|
||||||
@ -36,6 +36,7 @@ class SearchViewModel : ViewModel(), KoinComponent {
|
|||||||
private val websearchRepository: WebsearchRepository by inject()
|
private val websearchRepository: WebsearchRepository by inject()
|
||||||
|
|
||||||
val isSearching = MutableLiveData(false)
|
val isSearching = MutableLiveData(false)
|
||||||
|
val searchQuery = MutableLiveData("")
|
||||||
|
|
||||||
val favorites by lazy {
|
val favorites by lazy {
|
||||||
favoritesRepository.getFavorites().asLiveData()
|
favoritesRepository.getFavorites().asLiveData()
|
||||||
@ -53,8 +54,13 @@ class SearchViewModel : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
val hideFavorites = MutableLiveData(false)
|
val hideFavorites = MutableLiveData(false)
|
||||||
|
|
||||||
|
init {
|
||||||
|
search("")
|
||||||
|
}
|
||||||
|
|
||||||
var searchJob: Job? = null
|
var searchJob: Job? = null
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
|
searchQuery.value = query
|
||||||
try {
|
try {
|
||||||
searchJob?.cancel()
|
searchJob?.cancel()
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search
|
||||||
|
|
||||||
|
import android.animation.LayoutTransition
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||||
|
import de.mm20.launcher2.ui.databinding.ViewSearchBinding
|
||||||
|
|
||||||
|
class SearchView @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null
|
||||||
|
) : LinearLayout(context, attrs) {
|
||||||
|
val binding = ViewSearchBinding.inflate(LayoutInflater.from(context), this)
|
||||||
|
|
||||||
|
init {
|
||||||
|
orientation = VERTICAL
|
||||||
|
layoutTransition = ChangingLayoutTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.widgets
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.liveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.mm20.launcher2.widgets.Widget
|
||||||
|
import de.mm20.launcher2.widgets.WidgetRepository
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class WidgetsVM: ViewModel(), KoinComponent {
|
||||||
|
private val widgetRepository: WidgetRepository by inject()
|
||||||
|
|
||||||
|
val isEditMode = MutableLiveData(false)
|
||||||
|
|
||||||
|
val widgets = liveData<List<Widget>?> {
|
||||||
|
emit(widgetRepository.getWidgets())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEditMode(editMode: Boolean) {
|
||||||
|
isEditMode.value = editMode
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveWidgets(widgets: List<Widget>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
widgetRepository.saveWidgets(widgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInternalWidgets() : List<Widget> {
|
||||||
|
return widgetRepository.getInternalWidgets()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,291 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.widgets
|
||||||
|
|
||||||
|
import android.animation.LayoutTransition
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.appwidget.AppWidgetHost
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.iterator
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.list.listItems
|
||||||
|
import com.balsikandar.crashreporter.CrashReporter
|
||||||
|
import de.mm20.launcher2.ktx.dp
|
||||||
|
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||||
|
import de.mm20.launcher2.transition.OneShotLayoutTransition
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.databinding.ViewWidgetsBinding
|
||||||
|
import de.mm20.launcher2.ui.legacy.component.WidgetView
|
||||||
|
import de.mm20.launcher2.widgets.Widget
|
||||||
|
import de.mm20.launcher2.widgets.WidgetType
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class WidgetsView @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null
|
||||||
|
) : LinearLayout(context, attrs) {
|
||||||
|
|
||||||
|
private val binding = ViewWidgetsBinding.inflate(LayoutInflater.from(context), this)
|
||||||
|
|
||||||
|
private val widgetHost: AppWidgetHost = AppWidgetHost(context.applicationContext, 44203)
|
||||||
|
|
||||||
|
private val viewModel: WidgetsVM by (context as AppCompatActivity).viewModels()
|
||||||
|
|
||||||
|
private lateinit var widgets: MutableList<Widget>
|
||||||
|
|
||||||
|
private val pickWidgetLauncher: ActivityResultLauncher<Intent>
|
||||||
|
private val configureWidgetLauncher: ActivityResultLauncher<Intent>
|
||||||
|
|
||||||
|
init {
|
||||||
|
context as AppCompatActivity
|
||||||
|
|
||||||
|
layoutTransition = ChangingLayoutTransition()
|
||||||
|
binding.widgetList.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
|
|
||||||
|
configureWidgetLauncher = context.registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
val data = it.data ?: return@registerForActivityResult
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
bindAppWidget(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pickWidgetLauncher = context.registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
val data = it.data ?: return@registerForActivityResult
|
||||||
|
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||||
|
if (widgetId == -1) return@registerForActivityResult
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
val appWidget = AppWidgetManager.getInstance(context)
|
||||||
|
.getAppWidgetInfo(widgetId) ?: return@registerForActivityResult
|
||||||
|
if (appWidget.configure != null) {
|
||||||
|
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
|
||||||
|
intent.component = appWidget.configure
|
||||||
|
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
||||||
|
configureWidgetLauncher.launch(intent)
|
||||||
|
} else {
|
||||||
|
bindAppWidget(data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
widgetHost.deleteAppWidgetId(widgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.widgets.observe(context) {
|
||||||
|
if (it != null && !::widgets.isInitialized) {
|
||||||
|
widgets = it.toMutableList()
|
||||||
|
initWidgets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.isEditMode.observe(context) {
|
||||||
|
if (it) {
|
||||||
|
binding.clockWidget.visibility = View.GONE
|
||||||
|
|
||||||
|
for (v in binding.widgetList.iterator()) {
|
||||||
|
if (v is WidgetView) {
|
||||||
|
v.editMode = true
|
||||||
|
v.onResizeModeChange = {
|
||||||
|
OneShotLayoutTransition.run(binding.widgetList)
|
||||||
|
OneShotLayoutTransition.run(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OneShotLayoutTransition.run(binding.widgetList)
|
||||||
|
OneShotLayoutTransition.run(this)
|
||||||
|
binding.fabEditWidget.apply {
|
||||||
|
setIconResource(R.drawable.ic_add)
|
||||||
|
setText(R.string.widget_add_widget)
|
||||||
|
setOnClickListener {
|
||||||
|
addWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (::widgets.isInitialized) viewModel.saveWidgets(widgets)
|
||||||
|
binding.widgetList.layoutTransition = ChangingLayoutTransition()
|
||||||
|
binding.clockWidget.visibility = View.VISIBLE
|
||||||
|
for (v in binding.widgetList.iterator()) {
|
||||||
|
if (v is WidgetView) {
|
||||||
|
v.editMode = false
|
||||||
|
v.layoutTransition = ChangingLayoutTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.fabEditWidget.apply {
|
||||||
|
setIconResource(R.drawable.ic_edit)
|
||||||
|
setText(R.string.menu_edit_widgets)
|
||||||
|
setOnClickListener {
|
||||||
|
viewModel.setEditMode(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.lifecycleScope.launch {
|
||||||
|
context.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
widgetHost.startListening()
|
||||||
|
try {
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
// TODO: find out why there is a NPE thrown sometimes
|
||||||
|
try {
|
||||||
|
widgetHost.stopListening()
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
CrashReporter.logException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.fabEditWidget.setOnClickListener {
|
||||||
|
viewModel.setEditMode(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setClockWidgetHeight(height: Int) {
|
||||||
|
val params = binding.clockWidget.layoutParams
|
||||||
|
params.height = height
|
||||||
|
binding.clockWidget.layoutParams = params
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initWidgets() {
|
||||||
|
binding.widgetList.removeAllViews()
|
||||||
|
val params = LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
params.topMargin = (8 * dp).roundToInt()
|
||||||
|
for (w in widgets) {
|
||||||
|
val view = WidgetView(context)
|
||||||
|
view.layoutTransition = ChangingLayoutTransition()
|
||||||
|
view.layoutParams = params
|
||||||
|
if (view.setWidget(w, widgetHost)) {
|
||||||
|
binding.widgetList.addDragView(view, view.getDragHandle())
|
||||||
|
view.onRemove = {
|
||||||
|
OneShotLayoutTransition.run(binding.widgetList)
|
||||||
|
binding.widgetList.removeDragView(view)
|
||||||
|
removeWidget(view.widget)
|
||||||
|
}
|
||||||
|
view.onResizeModeChange = {
|
||||||
|
OneShotLayoutTransition.run(binding.widgetList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.widgetList.setOnViewSwapListener { _, firstPosition, _, secondPosition ->
|
||||||
|
Collections.swap(widgets, firstPosition, secondPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private fun addWidget() {
|
||||||
|
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
|
||||||
|
val internalWidgets =
|
||||||
|
viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
|
||||||
|
if (internalWidgets.isNotEmpty()) {
|
||||||
|
MaterialDialog(context).show {
|
||||||
|
title(R.string.widget_add_widget)
|
||||||
|
listItems(items = internalWidgets.map { it.label }) { dialog, index, _ ->
|
||||||
|
val widget = internalWidgets[index]
|
||||||
|
val view = WidgetView(context)
|
||||||
|
val params = LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
params.topMargin = (8 * dp).roundToInt()
|
||||||
|
view.layoutParams = params
|
||||||
|
if (view.setWidget(widget, widgetHost)) {
|
||||||
|
view.editMode = true
|
||||||
|
binding.widgetList.addDragView(view, view.getDragHandle())
|
||||||
|
view.onRemove = {
|
||||||
|
OneShotLayoutTransition.run(binding.widgetList)
|
||||||
|
OneShotLayoutTransition.run(this@WidgetsView)
|
||||||
|
binding.widgetList.removeDragView(view)
|
||||||
|
removeWidget(view.widget)
|
||||||
|
}
|
||||||
|
view.onResizeModeChange = {
|
||||||
|
OneShotLayoutTransition.run(binding.widgetList)
|
||||||
|
OneShotLayoutTransition.run(this@WidgetsView)
|
||||||
|
}
|
||||||
|
widgets.add(widget)
|
||||||
|
}
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
@Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged.
|
||||||
|
neutralButton(R.string.widget_add_external) {
|
||||||
|
val appWidgetId = widgetHost.allocateAppWidgetId()
|
||||||
|
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
|
||||||
|
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||||
|
pickWidgetLauncher.launch(pickIntent)
|
||||||
|
it.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val appWidgetId = widgetHost.allocateAppWidgetId()
|
||||||
|
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
|
||||||
|
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||||
|
pickWidgetLauncher.launch(pickIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeWidget(widget: Widget?) {
|
||||||
|
widget ?: return
|
||||||
|
widgets.remove(widget)
|
||||||
|
val id = widget.data.toIntOrNull() ?: return
|
||||||
|
widgetHost.deleteAppWidgetId(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAppWidget(data: Intent) {
|
||||||
|
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||||
|
if (widgetId == -1) return
|
||||||
|
val appWidget = AppWidgetManager.getInstance(context)
|
||||||
|
.getAppWidgetInfo(widgetId) ?: return
|
||||||
|
val widget = Widget(
|
||||||
|
type = WidgetType.THIRD_PARTY,
|
||||||
|
data = widgetId.toString(),
|
||||||
|
height = appWidget.minHeight
|
||||||
|
)
|
||||||
|
val params = LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
params.topMargin = (8 * dp).roundToInt()
|
||||||
|
val view = WidgetView(context)
|
||||||
|
view.layoutParams = params
|
||||||
|
if (view.setWidget(widget, widgetHost)) {
|
||||||
|
view.editMode = true
|
||||||
|
|
||||||
|
binding.widgetList.addDragView(view, view.getDragHandle())
|
||||||
|
view.onRemove = {
|
||||||
|
OneShotLayoutTransition.run(binding.widgetList)
|
||||||
|
OneShotLayoutTransition.run(this)
|
||||||
|
binding.widgetList.removeDragView(view)
|
||||||
|
removeWidget(view.widget)
|
||||||
|
}
|
||||||
|
view.onResizeModeChange = {
|
||||||
|
OneShotLayoutTransition.run(binding.widgetList)
|
||||||
|
OneShotLayoutTransition.run(this)
|
||||||
|
}
|
||||||
|
widgets.add(widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,743 +0,0 @@
|
|||||||
package de.mm20.launcher2.ui.legacy.activity
|
|
||||||
|
|
||||||
import android.animation.AnimatorSet
|
|
||||||
import android.animation.LayoutTransition
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.WallpaperManager
|
|
||||||
import android.appwidget.AppWidgetHost
|
|
||||||
import android.appwidget.AppWidgetManager
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Point
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.TypedValue
|
|
||||||
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.LinearLayout
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.core.animation.doOnEnd
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.*
|
|
||||||
import androidx.core.widget.NestedScrollView
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.afollestad.materialdialogs.LayoutMode
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
|
||||||
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.icons.DynamicIconController
|
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
|
||||||
import de.mm20.launcher2.ktx.dp
|
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
|
||||||
import de.mm20.launcher2.ktx.isBrightColor
|
|
||||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
|
||||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
|
||||||
import de.mm20.launcher2.transition.OneShotLayoutTransition
|
|
||||||
import de.mm20.launcher2.ui.R
|
|
||||||
import de.mm20.launcher2.ui.base.BaseActivity
|
|
||||||
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
|
|
||||||
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.settings.SettingsActivity
|
|
||||||
import de.mm20.launcher2.widgets.Widget
|
|
||||||
import de.mm20.launcher2.widgets.WidgetType
|
|
||||||
import de.mm20.launcher2.widgets.WidgetViewModel
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
|
|
||||||
class LauncherActivity : BaseActivity() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the search result list is visible
|
|
||||||
*/
|
|
||||||
private var searchVisibility = false
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
windowBackgroundBlur = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var widgetHost: AppWidgetHost
|
|
||||||
private val widgets = mutableListOf<Widget>()
|
|
||||||
|
|
||||||
private lateinit var overlayView: ViewGroupOverlay
|
|
||||||
|
|
||||||
private val widgetViewModel: WidgetViewModel 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var widgetEditMode = false
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
if (value) {
|
|
||||||
binding.clockWidget.visibility = View.GONE
|
|
||||||
binding.searchBar.setRightIcon(R.drawable.ic_done)
|
|
||||||
binding.scrollView.setOnTouchListener(null)
|
|
||||||
for (v in binding.widgetList.iterator()) {
|
|
||||||
if (v is WidgetView) {
|
|
||||||
v.editMode = true
|
|
||||||
v.onResizeModeChange = {
|
|
||||||
OneShotLayoutTransition.run(binding.widgetList)
|
|
||||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OneShotLayoutTransition.run(binding.widgetList)
|
|
||||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
|
||||||
OneShotLayoutTransition.run(binding.scrollContainer)
|
|
||||||
binding.fabEditWidget.apply {
|
|
||||||
setIconResource(R.drawable.ic_add)
|
|
||||||
setText(R.string.widget_add_widget)
|
|
||||||
setOnClickListener {
|
|
||||||
addWidget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val statusBarColor = TypedValue().also {
|
|
||||||
theme.resolveAttribute(
|
|
||||||
R.attr.colorSurface,
|
|
||||||
it,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}.data
|
|
||||||
window.statusBarColor = statusBarColor
|
|
||||||
if (statusBarColor.isBrightColor()) {
|
|
||||||
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
|
|
||||||
insetsController.isAppearanceLightStatusBars = true
|
|
||||||
}
|
|
||||||
binding.searchBar.visibility = View.INVISIBLE
|
|
||||||
binding.editWidgetToolbar
|
|
||||||
.animate()
|
|
||||||
.translationY(0f)
|
|
||||||
.alpha(1f)
|
|
||||||
.withStartAction {
|
|
||||||
binding.editWidgetToolbar.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
.start()
|
|
||||||
} else {
|
|
||||||
widgetViewModel.saveWidgets(widgets)
|
|
||||||
binding.widgetList.layoutTransition = ChangingLayoutTransition()
|
|
||||||
binding.widgetContainer.layoutTransition = ChangingLayoutTransition()
|
|
||||||
binding.scrollContainer.layoutTransition = ChangingLayoutTransition()
|
|
||||||
binding.searchBar.setRightIcon(R.drawable.ic_more_vert)
|
|
||||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
|
||||||
binding.clockWidget.visibility = View.VISIBLE
|
|
||||||
for (v in binding.widgetList.iterator()) {
|
|
||||||
if (v is WidgetView) {
|
|
||||||
v.editMode = false
|
|
||||||
v.layoutTransition = ChangingLayoutTransition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.fabEditWidget.apply {
|
|
||||||
setIconResource(R.drawable.ic_edit)
|
|
||||||
setText(R.string.menu_edit_widgets)
|
|
||||||
setOnClickListener {
|
|
||||||
widgetEditMode = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.statusBarColor = Color.TRANSPARENT
|
|
||||||
|
|
||||||
updateSystemBarAppearance()
|
|
||||||
|
|
||||||
binding.searchBar.visibility = View.VISIBLE
|
|
||||||
binding.editWidgetToolbar
|
|
||||||
.animate()
|
|
||||||
.translationY(-binding.editWidgetToolbar.height.toFloat())
|
|
||||||
.alpha(0f)
|
|
||||||
.withEndAction {
|
|
||||||
binding.editWidgetToolbar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSystemBarAppearance() {
|
|
||||||
val allowLightSystemBars = allowsLightSystemBars()
|
|
||||||
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
|
|
||||||
insetsController.isAppearanceLightNavigationBars =
|
|
||||||
allowLightSystemBars && preferences.lightNavBar
|
|
||||||
insetsController.isAppearanceLightStatusBars =
|
|
||||||
allowLightSystemBars && preferences.lightStatusBar
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var binding: ActivityLauncherBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val iconRepository: IconRepository by inject()
|
|
||||||
iconRepository.recreate()
|
|
||||||
ThemeHelper.applyTheme(theme)
|
|
||||||
|
|
||||||
if (LauncherPreferences.instance.firstRunVersion < 1) {
|
|
||||||
LauncherPreferences.instance.firstRunVersion = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
|
|
||||||
binding = ActivityLauncherBinding.inflate(LayoutInflater.from(this))
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
overlayView = binding.rootView.overlay
|
|
||||||
|
|
||||||
if (LauncherPreferences.instance.dimWallpaper) {
|
|
||||||
binding.dimWallpaper.setBackgroundColor(getColor(R.color.wallpaper_dim))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
|
||||||
binding.searchContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
|
||||||
binding.widgetContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
|
||||||
|
|
||||||
val params = binding.clockWidget.layoutParams
|
|
||||||
params.height = Point().also { windowManager.defaultDisplay.getSize(it) }.y
|
|
||||||
binding.clockWidget.layoutParams = params
|
|
||||||
binding.container.doOnLayout {
|
|
||||||
adjustWidgetSpace()
|
|
||||||
}
|
|
||||||
initWidgets()
|
|
||||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
|
||||||
binding.scrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, oldScrollY: Int ->
|
|
||||||
when {
|
|
||||||
/* Hide searchbar*/
|
|
||||||
scrollY > oldScrollY && ((scrollY > binding.searchBar.height) || widgetEditMode) -> {
|
|
||||||
var newTransY = binding.searchBar.translationY - scrollY + oldScrollY
|
|
||||||
if (newTransY < -binding.searchBar.height.toFloat() * 1.5f) {
|
|
||||||
newTransY = -binding.searchBar.height.toFloat() * 1.5f
|
|
||||||
}
|
|
||||||
binding.searchBar.translationY = newTransY
|
|
||||||
}
|
|
||||||
/* Show searchbar*/
|
|
||||||
scrollY < oldScrollY -> {
|
|
||||||
var newTransY = binding.searchBar.translationY - scrollY + oldScrollY
|
|
||||||
if (newTransY > 0f) {
|
|
||||||
newTransY = 0f
|
|
||||||
}
|
|
||||||
binding.searchBar.translationY = newTransY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (scrollY > 0 && (searchVisibility || widgetEditMode)
|
|
||||||
) {
|
|
||||||
binding.searchBar.raise()
|
|
||||||
} else binding.searchBar.drop()
|
|
||||||
if (scrollY == 0) {
|
|
||||||
if (!searchVisibility) {
|
|
||||||
binding.searchBar.hide()
|
|
||||||
windowBackgroundBlur = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.searchBar.show()
|
|
||||||
if (!searchVisibility) {
|
|
||||||
windowBackgroundBlur = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.searchBar.onRightIconClick = onRightIconClick@{
|
|
||||||
if (widgetEditMode) widgetEditMode = false
|
|
||||||
else {
|
|
||||||
val menu = PopupMenu(this, it)
|
|
||||||
menu.inflate(R.menu.menu_launcher)
|
|
||||||
menu.setOnMenuItemClickListener { item ->
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.menu_item_settings -> {
|
|
||||||
startActivity(Intent(this, SettingsActivity::class.java))
|
|
||||||
}
|
|
||||||
R.id.menu_item_wallpaper -> {
|
|
||||||
startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
Intent(Intent.ACTION_SET_WALLPAPER),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
R.id.menu_item_hidden -> {
|
|
||||||
val view = HiddenItemsView(this)
|
|
||||||
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
|
|
||||||
.show {
|
|
||||||
title(R.string.menu_hidden_items)
|
|
||||||
customView(view = view)
|
|
||||||
negativeButton(R.string.close) { dismiss() }
|
|
||||||
}
|
|
||||||
//hiddenAppsActivated = true
|
|
||||||
}
|
|
||||||
R.id.menu_item_edit_favs -> {
|
|
||||||
val view = EditFavoritesView(this@LauncherActivity)
|
|
||||||
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT)).show {
|
|
||||||
customView(view = view)
|
|
||||||
title(res = R.string.menu_item_edit_favs)
|
|
||||||
positiveButton(res = R.string.close) {
|
|
||||||
it.dismiss()
|
|
||||||
}
|
|
||||||
onDismiss {
|
|
||||||
view.save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.menu_item_settings_old -> {
|
|
||||||
finish()
|
|
||||||
startActivity(Intent().also {
|
|
||||||
it.component = ComponentName(
|
|
||||||
packageName,
|
|
||||||
"de.mm20.launcher2.activity.SettingsActivity"
|
|
||||||
)
|
|
||||||
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
menu.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.searchBar.setOnTouchListener { _, _ ->
|
|
||||||
if (!searchVisibility) showSearch()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.searchBar.onSearchQueryChanged = {
|
|
||||||
search(it)
|
|
||||||
}
|
|
||||||
binding.widgetList.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
|
||||||
|
|
||||||
binding.fabEditWidget.setOnClickListener {
|
|
||||||
widgetEditMode = true
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.editWidgetToolbar.apply {
|
|
||||||
navigationIcon =
|
|
||||||
ContextCompat.getDrawable(this@LauncherActivity, R.drawable.ic_done)?.apply {
|
|
||||||
setTint(ContextCompat.getColor(this@LauncherActivity, R.color.icon_color))
|
|
||||||
}
|
|
||||||
setNavigationOnClickListener {
|
|
||||||
widgetEditMode = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dynamicIconController: DynamicIconController by inject()
|
|
||||||
|
|
||||||
lifecycle.addObserver(dynamicIconController)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
widgets.addAll(widgetViewModel.getWidgets())
|
|
||||||
initWidgets()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun initWidgets() {
|
|
||||||
widgetHost = AppWidgetHost(applicationContext, 0xacab)
|
|
||||||
binding.widgetList.removeAllViews()
|
|
||||||
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
|
||||||
params.topMargin = (8 * dp).roundToInt()
|
|
||||||
for (w in widgets) {
|
|
||||||
val view = WidgetView(this)
|
|
||||||
view.layoutTransition = ChangingLayoutTransition()
|
|
||||||
view.layoutParams = params
|
|
||||||
if (view.setWidget(w, widgetHost)) {
|
|
||||||
binding.widgetList.addDragView(view, view.getDragHandle())
|
|
||||||
view.onRemove = {
|
|
||||||
OneShotLayoutTransition.run(binding.widgetList)
|
|
||||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
|
||||||
binding.widgetList.removeDragView(view)
|
|
||||||
removeWidget(view.widget)
|
|
||||||
}
|
|
||||||
view.onResizeModeChange = {
|
|
||||||
OneShotLayoutTransition.run(binding.widgetList)
|
|
||||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.widgetList.setOnViewSwapListener { _, firstPosition, _, secondPosition ->
|
|
||||||
Collections.swap(widgets, firstPosition, secondPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addWidget() {
|
|
||||||
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
|
|
||||||
val internalWidgets =
|
|
||||||
widgetViewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
|
|
||||||
if (internalWidgets.isNotEmpty()) {
|
|
||||||
MaterialDialog(this).show {
|
|
||||||
val widgetList =
|
|
||||||
this@LauncherActivity.findViewById<DragLinearLayout>(R.id.widgetList)
|
|
||||||
val widgetContainer =
|
|
||||||
this@LauncherActivity.findViewById<LinearLayout>(R.id.widgetContainer)
|
|
||||||
title(R.string.widget_add_widget)
|
|
||||||
listItems(items = internalWidgets.map { it.label }) { dialog, index, _ ->
|
|
||||||
val widget = internalWidgets[index]
|
|
||||||
val view = WidgetView(this@LauncherActivity)
|
|
||||||
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
|
||||||
params.topMargin = (8 * dp).roundToInt()
|
|
||||||
view.layoutParams = params
|
|
||||||
if (view.setWidget(widget, widgetHost)) {
|
|
||||||
view.editMode = true
|
|
||||||
widgetList.addDragView(view, view.getDragHandle())
|
|
||||||
view.onRemove = {
|
|
||||||
OneShotLayoutTransition.run(widgetList)
|
|
||||||
OneShotLayoutTransition.run(widgetContainer)
|
|
||||||
widgetList.removeDragView(view)
|
|
||||||
removeWidget(view.widget)
|
|
||||||
}
|
|
||||||
view.onResizeModeChange = {
|
|
||||||
OneShotLayoutTransition.run(widgetList)
|
|
||||||
OneShotLayoutTransition.run(widgetContainer)
|
|
||||||
}
|
|
||||||
widgets.add(widget)
|
|
||||||
}
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
@Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged.
|
|
||||||
neutralButton(R.string.widget_add_external) {
|
|
||||||
val appWidgetId = widgetHost.allocateAppWidgetId()
|
|
||||||
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
|
|
||||||
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
|
||||||
startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
|
|
||||||
it.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val appWidgetId = widgetHost.allocateAppWidgetId()
|
|
||||||
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
|
|
||||||
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
|
||||||
startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeWidget(widget: Widget?) {
|
|
||||||
widget ?: return
|
|
||||||
widgets.remove(widget)
|
|
||||||
val id = widget.data.toIntOrNull() ?: return
|
|
||||||
widgetHost.deleteAppWidgetId(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustWidgetSpace() {
|
|
||||||
val height = binding.scrollView.height - binding.searchBar.height - 8 * dp
|
|
||||||
binding.clockWidget.layoutParams = binding.clockWidget.layoutParams.also {
|
|
||||||
it.height = height.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun search(text: String) {
|
|
||||||
searchViewModel.search(text)
|
|
||||||
if (binding.webSearchViewSpacer.tag != "measured" || binding.webSearchViewSpacer.height == 0) {
|
|
||||||
val webSearchView = binding.searchBar.getWebSearchView()
|
|
||||||
webSearchView.doOnNextLayout {
|
|
||||||
binding.webSearchViewSpacer.layoutParams = binding.webSearchViewSpacer.layoutParams
|
|
||||||
.apply { height = webSearchView.height }
|
|
||||||
binding.webSearchViewSpacer.tag = "measured"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.webSearchViewSpacer.visibility = if (text.isBlank()) View.GONE else View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleSearch() {
|
|
||||||
if (searchVisibility) {
|
|
||||||
hideSearch()
|
|
||||||
} else {
|
|
||||||
showSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideSearch() {
|
|
||||||
|
|
||||||
searchVisibility = false
|
|
||||||
val set = AnimatorSet()
|
|
||||||
set.duration = 300
|
|
||||||
set.doOnEnd {
|
|
||||||
binding.searchContainer.visibility = View.GONE
|
|
||||||
binding.widgetContainer.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
set.playTogether(
|
|
||||||
ObjectAnimator.ofFloat(binding.widgetContainer, "translationY", 0f),
|
|
||||||
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0),
|
|
||||||
ObjectAnimator.ofFloat(
|
|
||||||
binding.searchContainer, "translationY", 0f,
|
|
||||||
if (binding.scrollView.scrollY > binding.searchContainer.height / 2f) -binding.searchContainer.height.toFloat() else binding.scrollView.height.toFloat()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
set.start()
|
|
||||||
binding.scrollView.scrollTo(0, 0)
|
|
||||||
binding.searchBar.hide()
|
|
||||||
if (!binding.searchBar.getSearchQuery().isEmpty()) binding.searchBar.setSearchQuery("")
|
|
||||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
|
||||||
.hideSoftInputFromWindow(binding.searchBar.windowToken, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSearch() {
|
|
||||||
|
|
||||||
searchVisibility = true
|
|
||||||
binding.searchBar.show()
|
|
||||||
binding.searchContainer.visibility = View.VISIBLE
|
|
||||||
binding.widgetContainer.visibility = View.GONE
|
|
||||||
val set = AnimatorSet()
|
|
||||||
set.duration = 300
|
|
||||||
set.doOnEnd {
|
|
||||||
search("")
|
|
||||||
}
|
|
||||||
set.playTogether(
|
|
||||||
ObjectAnimator.ofFloat(
|
|
||||||
binding.widgetContainer,
|
|
||||||
"translationY",
|
|
||||||
binding.scrollView.height.toFloat()
|
|
||||||
),
|
|
||||||
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0),
|
|
||||||
ObjectAnimator.ofFloat(
|
|
||||||
binding.searchContainer,
|
|
||||||
"translationY",
|
|
||||||
binding.scrollView.height.toFloat(),
|
|
||||||
0f
|
|
||||||
)
|
|
||||||
)
|
|
||||||
set.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (widgetEditMode) widgetEditMode = false
|
|
||||||
if (searchVisibility) hideSearch()
|
|
||||||
else ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0).setDuration(200).start()
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
ActivityStarter.resume()
|
|
||||||
ActivityStarter.create(binding.rootView)
|
|
||||||
binding.activityStartOverlay.visibility = View.INVISIBLE
|
|
||||||
|
|
||||||
search(binding.searchBar.getSearchQuery())
|
|
||||||
|
|
||||||
updateSystemBarAppearance()
|
|
||||||
|
|
||||||
binding.container.doOnNextLayout {
|
|
||||||
WallpaperManager.getInstance(this).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//getSystemService(Context.INPUT_METHOD_SERVICE)
|
|
||||||
// .castTo<InputMethodManager>()
|
|
||||||
// .hideSoftInputFromWindow(currentFocus?.windowToken, 0)
|
|
||||||
|
|
||||||
//overridePendingTransition(R.anim.app_to_launcher_in, R.anim.app_to_launcher_out)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun allowsLightSystemBars(): Boolean {
|
|
||||||
val dimWallpaper = LauncherPreferences.instance.dimWallpaper
|
|
||||||
val isDarkTheme = resources.getBoolean(R.bool.is_dark_theme)
|
|
||||||
return !(isDarkTheme && dimWallpaper)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasNotificationListenerPermission(): Boolean {
|
|
||||||
val listeners = NotificationManagerCompat.getEnabledListenerPackages(this)
|
|
||||||
for (listener in listeners) {
|
|
||||||
if (listener == packageName) return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private val themeListener = { key: String ->
|
|
||||||
recreate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
preferences.doOnPreferenceChange("is_light_wallpaper", action = themeListener)
|
|
||||||
widgetHost.startListening()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
ActivityStarter.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
try {
|
|
||||||
widgetHost.stopListening()
|
|
||||||
} catch (e: NullPointerException) {
|
|
||||||
Log.e("MM20", Log.getStackTraceString(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
onBackPressed()
|
|
||||||
adjustWidgetSpace()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
|
||||||
requestCode: Int,
|
|
||||||
permissions: Array<out String>,
|
|
||||||
grantResults: IntArray
|
|
||||||
) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
search(binding.searchBar.getSearchQuery())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
if (data == null) return
|
|
||||||
if (requestCode == REQUEST_PICK_APPWIDGET) {
|
|
||||||
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
|
||||||
if (widgetId == -1) return
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
val appWidget = AppWidgetManager.getInstance(applicationContext)
|
|
||||||
.getAppWidgetInfo(widgetId) ?: return
|
|
||||||
if (appWidget.configure != null) {
|
|
||||||
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
|
|
||||||
intent.component = appWidget.configure
|
|
||||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
|
||||||
startActivityForResult(intent, REQUEST_BIND_APPWIDGET)
|
|
||||||
} else {
|
|
||||||
onActivityResult(REQUEST_BIND_APPWIDGET, Activity.RESULT_OK, data)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
widgetHost.deleteAppWidgetId(widgetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (requestCode == REQUEST_CREATE_APPWIDGET) {
|
|
||||||
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
|
||||||
if (widgetId == -1) return
|
|
||||||
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
|
|
||||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
|
||||||
startActivityForResult(intent, REQUEST_BIND_APPWIDGET)
|
|
||||||
}
|
|
||||||
if (requestCode == REQUEST_BIND_APPWIDGET && resultCode == Activity.RESULT_OK) {
|
|
||||||
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
|
||||||
if (widgetId == -1) return
|
|
||||||
val appWidget = AppWidgetManager.getInstance(applicationContext)
|
|
||||||
.getAppWidgetInfo(widgetId) ?: return
|
|
||||||
val widget = Widget(
|
|
||||||
type = WidgetType.THIRD_PARTY,
|
|
||||||
data = widgetId.toString(),
|
|
||||||
height = appWidget.minHeight
|
|
||||||
)
|
|
||||||
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
|
||||||
params.topMargin = (8 * dp).roundToInt()
|
|
||||||
val view = WidgetView(this)
|
|
||||||
view.layoutParams = params
|
|
||||||
if (view.setWidget(widget, widgetHost)) {
|
|
||||||
view.editMode = true
|
|
||||||
|
|
||||||
binding.widgetList.addDragView(view, view.getDragHandle())
|
|
||||||
view.onRemove = {
|
|
||||||
OneShotLayoutTransition.run(binding.widgetList)
|
|
||||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
|
||||||
binding.widgetList.removeDragView(view)
|
|
||||||
removeWidget(view.widget)
|
|
||||||
}
|
|
||||||
view.onResizeModeChange = {
|
|
||||||
OneShotLayoutTransition.run(binding.widgetList)
|
|
||||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
|
||||||
}
|
|
||||||
widgets.add(widget)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scrollViewOnTouchListener: (View, MotionEvent) -> Boolean = onTouch@{ _, event ->
|
|
||||||
when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> true
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
|
||||||
when {
|
|
||||||
binding.scrollView.scrollY == 0 -> {
|
|
||||||
if (event.historySize > 0) {
|
|
||||||
val dY = event.y - event.getHistoricalY(0)
|
|
||||||
val newTransY = 0.4f * dY + binding.container.translationY
|
|
||||||
if (newTransY > 0 && newTransY < binding.searchBar.height) {
|
|
||||||
binding.container.translationY = newTransY
|
|
||||||
binding.searchBar.show()
|
|
||||||
} else if (newTransY <= 0) {
|
|
||||||
binding.container.translationY = 0f
|
|
||||||
} else {
|
|
||||||
binding.container.translationY = binding.searchBar.height.toFloat()
|
|
||||||
|
|
||||||
}
|
|
||||||
windowBackgroundBlur =
|
|
||||||
searchVisibility || newTransY > 0.6 * binding.searchBar.height
|
|
||||||
|
|
||||||
if (binding.container.translationY == 0f) return@onTouch false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.scrollView.scrollY == binding.scrollContainer.height - binding.scrollView.height && searchVisibility -> {
|
|
||||||
if (event.historySize > 0) {
|
|
||||||
val dY = event.y - event.getHistoricalY(0)
|
|
||||||
val newTransY = 0.4f * dY + binding.container.translationY
|
|
||||||
|
|
||||||
if (newTransY <= 0 && newTransY > -binding.searchBar.height) {
|
|
||||||
binding.container.translationY = newTransY
|
|
||||||
binding.searchBar.show()
|
|
||||||
} else if (newTransY > 0) {
|
|
||||||
binding.container.translationY = 0f
|
|
||||||
} else {
|
|
||||||
binding.container.translationY = -binding.searchBar.height.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.container.translationY == 0f) return@onTouch false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> return@onTouch false
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_UP -> {
|
|
||||||
if (binding.container.translationY >= binding.searchBar.height * 0.6) toggleSearch()
|
|
||||||
if (binding.container.translationY <= -binding.searchBar.height) hideSearch()
|
|
||||||
binding.container.animate().translationY(0f).setDuration(200).start()
|
|
||||||
if (!searchVisibility && binding.scrollView.scrollY == 0) binding.searchBar.hide()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
ActivityStarter.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val REQUEST_PICK_APPWIDGET = 4412
|
|
||||||
const val REQUEST_CREATE_APPWIDGET = 4460
|
|
||||||
const val REQUEST_BIND_APPWIDGET = 4124
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
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 de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
|
||||||
class ApplicationView : FrameLayout {
|
class ApplicationView : FrameLayout {
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ 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: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
applications = viewModel.appResults
|
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
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import androidx.lifecycle.Observer
|
|||||||
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.launcher.search.SearchVM
|
||||||
import de.mm20.launcher2.ui.search.CalculatorItem
|
import de.mm20.launcher2.ui.search.CalculatorItem
|
||||||
|
|
||||||
class CalculatorView : FrameLayout {
|
class CalculatorView : FrameLayout {
|
||||||
@ -36,7 +36,7 @@ class CalculatorView : FrameLayout {
|
|||||||
private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true)
|
private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
calculator = viewModel.calculatorResult
|
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
|
||||||
|
|||||||
@ -15,7 +15,7 @@ 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.R
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
@ -40,7 +40,7 @@ class CalendarView : FrameLayout, KoinComponent {
|
|||||||
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: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
calendarEvents = viewModel.calendarResults
|
calendarEvents = viewModel.calendarResults
|
||||||
calendarEvents.observe(context as AppCompatActivity, {
|
calendarEvents.observe(context as AppCompatActivity, {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ 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.launcher.search.SearchVM
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
@ -38,7 +38,7 @@ class ContactView : FrameLayout, KoinComponent {
|
|||||||
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: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
contacts = viewModel.contactResults
|
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, {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import android.widget.FrameLayout
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import de.mm20.launcher2.ui.databinding.ViewFavoritesBinding
|
import de.mm20.launcher2.ui.databinding.ViewFavoritesBinding
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
|
||||||
class FavoritesView : FrameLayout {
|
class FavoritesView : FrameLayout {
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ class FavoritesView : FrameLayout {
|
|||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
val favorites = viewModel.favorites
|
val favorites = viewModel.favorites
|
||||||
val hide = viewModel.hideFavorites
|
val hide = viewModel.hideFavorites
|
||||||
favorites.observe(context as AppCompatActivity) {
|
favorites.observe(context as AppCompatActivity) {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ 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.launcher.search.SearchVM
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
@ -38,7 +38,7 @@ class FileView : FrameLayout, KoinComponent {
|
|||||||
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: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
files = viewModel.fileResults
|
files = viewModel.fileResults
|
||||||
files.observe(context as AppCompatActivity, {
|
files.observe(context as AppCompatActivity, {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
|
|||||||
@ -1,256 +1,50 @@
|
|||||||
package de.mm20.launcher2.ui.legacy.component
|
package de.mm20.launcher2.ui.legacy.component
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorSet
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.MotionEvent
|
||||||
import android.view.animation.AccelerateInterpolator
|
import android.widget.FrameLayout
|
||||||
import android.view.animation.DecelerateInterpolator
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.core.view.postDelayed
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import com.airbnb.lottie.LottieCompositionFactory
|
import androidx.compose.ui.Alignment
|
||||||
import com.airbnb.lottie.LottieDrawable
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import de.mm20.launcher2.ktx.dp
|
import androidx.lifecycle.MutableLiveData
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.ui.LegacyLauncherTheme
|
||||||
import de.mm20.launcher2.preferences.SearchStyles
|
|
||||||
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.launcher.search.SearchBar
|
||||||
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
|
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||||
|
|
||||||
class SearchBar @JvmOverloads constructor(
|
class SearchBar @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = R.attr.materialCardViewStyle
|
defStyleAttr: Int = R.attr.materialCardViewStyle
|
||||||
) : LauncherCardView(context, attrs, defStyleAttr) {
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private var raised = false
|
var level: SearchBarLevel = SearchBarLevel.Resting
|
||||||
private var visible = true
|
set(value) {
|
||||||
private var currentAnimator: Animator? = null
|
levelState.value = value
|
||||||
|
field = value
|
||||||
private val rightDrawable = LottieDrawable().apply {
|
|
||||||
composition = LottieCompositionFactory.fromRawResSync(context, R.raw.ic_menu_to_clear).value
|
|
||||||
repeatMode = LottieDrawable.REVERSE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this)
|
private val levelState = MutableLiveData(level)
|
||||||
|
|
||||||
|
var onFocus: (() -> Unit)? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.overflowMenu.setImageDrawable(rightDrawable)
|
val view = ComposeView(context)
|
||||||
binding.searchEdit.addTextChangedListener(object : TextWatcher {
|
view.setContent {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
val level by levelState.observeAsState(SearchBarLevel.Resting)
|
||||||
val text = binding.searchEdit.text.toString()
|
LegacyLauncherTheme {
|
||||||
onSearchQueryChanged?.invoke(binding.searchEdit.text.toString())
|
Box(contentAlignment = Alignment.TopCenter) {
|
||||||
if (text.isEmpty()) {
|
SearchBar(
|
||||||
if (rightDrawable.frame > rightDrawable.minFrame.toInt()) {
|
level,
|
||||||
rightDrawable.speed = -1f
|
onFocus = { onFocus?.invoke() }
|
||||||
rightDrawable.resumeAnimation()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rightDrawable.frame < rightDrawable.maxFrame.toInt()) {
|
|
||||||
rightDrawable.speed = 1f
|
|
||||||
rightDrawable.resumeAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.overflowMenu.setOnClickListener {
|
|
||||||
if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it)
|
|
||||||
else (setSearchQuery(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
postDelayed(1) {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutTransition = ChangingLayoutTransition()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRightIcon(iconRes: Int) {
|
|
||||||
binding.overflowMenu.setImageResource(iconRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
var onSearchQueryChanged: ((String) -> Unit)? = null
|
|
||||||
var onRightIconClick: ((View) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun setOnTouchListener(l: OnTouchListener?) {
|
|
||||||
binding.searchEdit.setOnTouchListener(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchQuery(text: String) {
|
|
||||||
binding.searchEdit.setText(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSearchQuery(): String {
|
|
||||||
return binding.searchEdit.text.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Elevates the search bar to be higher than the other cards
|
|
||||||
*/
|
|
||||||
fun raise() {
|
|
||||||
if (raised) return
|
|
||||||
currentAnimator?.takeIf { it.isStarted }?.end()
|
|
||||||
currentAnimator = AnimatorSet().apply {
|
|
||||||
duration = 200
|
|
||||||
playTogether(
|
|
||||||
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", elevation, 7 * dp).apply {
|
|
||||||
interpolator = AccelerateInterpolator(3f)
|
|
||||||
},
|
|
||||||
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", 0xFF).apply {
|
|
||||||
interpolator = DecelerateInterpolator(3f)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
currentAnimator?.start()
|
|
||||||
raised = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drops the search bar back down to the other cards niveau
|
|
||||||
*/
|
|
||||||
|
|
||||||
fun drop() {
|
|
||||||
if (!raised) return
|
|
||||||
currentAnimator?.takeIf { it.isStarted }?.end()
|
|
||||||
currentAnimator = AnimatorSet().apply {
|
|
||||||
duration = 200
|
|
||||||
playTogether(
|
|
||||||
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", 0f).apply {
|
|
||||||
interpolator = DecelerateInterpolator(3f)
|
|
||||||
},
|
|
||||||
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", LauncherPreferences.instance.cardOpacity).apply {
|
|
||||||
interpolator = AccelerateInterpolator(3f)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
currentAnimator?.start()
|
|
||||||
raised = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show() {
|
|
||||||
if (visible) return
|
|
||||||
currentAnimator?.takeIf { it.isStarted }?.end()
|
|
||||||
currentAnimator = getShowAnimator()
|
|
||||||
currentAnimator?.start()
|
|
||||||
visible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hide() {
|
|
||||||
if (!visible) return
|
|
||||||
currentAnimator?.takeIf { it.isStarted }?.end()
|
|
||||||
currentAnimator = getHideAnimator()
|
|
||||||
currentAnimator?.start()
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getWebSearchView(): View {
|
|
||||||
return binding.webSearchView
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getHideAnimator(): AnimatorSet {
|
|
||||||
val searchStyle = LauncherPreferences.instance.searchStyle
|
|
||||||
return when (searchStyle) {
|
|
||||||
SearchStyles.NO_BG -> {
|
|
||||||
val iconColor = ContextCompat.getColor(context, R.color.icon_color)
|
|
||||||
val cardElevation = resources.getDimension(R.dimen.card_elevation)
|
|
||||||
val shadowY = resources.getDimension(R.dimen.elevation_shadow_1dp_y)
|
|
||||||
val shadowR = resources.getDimension(R.dimen.elevation_shadow_1dp_radius)
|
|
||||||
val shadowC = Color.argb(66, 0, 0, 0)
|
|
||||||
binding.searchEdit.setShadowLayer(shadowR, 0f, shadowY, shadowC)
|
|
||||||
AnimatorSet().apply {
|
|
||||||
duration = 200
|
|
||||||
playTogether(
|
|
||||||
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", 0).apply {
|
|
||||||
interpolator = AccelerateInterpolator(3f)
|
|
||||||
},
|
|
||||||
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", -elevation).apply {
|
|
||||||
interpolator = DecelerateInterpolator(3f)
|
|
||||||
duration = 150
|
|
||||||
},
|
|
||||||
ObjectAnimator.ofArgb(binding.searchEdit, "hintTextColor", binding.searchEdit.hintTextColors.defaultColor, Color.WHITE),
|
|
||||||
ObjectAnimator.ofArgb(binding.searchIcon, "colorFilter", iconColor, Color.WHITE),
|
|
||||||
ObjectAnimator.ofArgb(binding.overflowMenu, "colorFilter", iconColor, Color.WHITE),
|
|
||||||
ObjectAnimator.ofFloat(binding.searchIcon, "alpha", 1f),
|
|
||||||
ObjectAnimator.ofFloat(binding.overflowMenu, "alpha", 1f),
|
|
||||||
ObjectAnimator.ofFloat(binding.searchIcon, "elevation", cardElevation),
|
|
||||||
ObjectAnimator.ofFloat(binding.overflowMenu, "elevation", cardElevation)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Solid style
|
|
||||||
SearchStyles.SOLID -> {
|
|
||||||
AnimatorSet()
|
|
||||||
}
|
|
||||||
// Hidden style
|
|
||||||
else -> {
|
|
||||||
AnimatorSet().apply {
|
|
||||||
duration = 200
|
|
||||||
playTogether(
|
|
||||||
ObjectAnimator.ofFloat(this@SearchBar, "alpha", 0f)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getShowAnimator(): AnimatorSet? {
|
|
||||||
return when (LauncherPreferences.instance.searchStyle) {
|
|
||||||
// Transparent style
|
|
||||||
SearchStyles.NO_BG -> {
|
|
||||||
val hint = ContextCompat.getColor(context, R.color.text_color_primary_disabled)
|
|
||||||
val iconAttrs = context.obtainStyledAttributes(R.style.LauncherTheme_IconStyle, intArrayOf(android.R.attr.alpha))
|
|
||||||
val iconAlpha = iconAttrs.getFloat(0, 0f)
|
|
||||||
iconAttrs.recycle()
|
|
||||||
val iconColor = ContextCompat.getColor(context, R.color.icon_color)
|
|
||||||
binding.searchEdit.setShadowLayer(0f, 0f, 0f, 0)
|
|
||||||
AnimatorSet().apply {
|
|
||||||
duration = 200
|
|
||||||
playTogether(
|
|
||||||
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", 0f).apply {
|
|
||||||
interpolator = AccelerateInterpolator(3f)
|
|
||||||
},
|
|
||||||
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", LauncherPreferences.instance.cardOpacity).apply {
|
|
||||||
interpolator = DecelerateInterpolator(3f)
|
|
||||||
},
|
|
||||||
ObjectAnimator.ofArgb(binding.searchEdit, "hintTextColor", Color.WHITE, hint),
|
|
||||||
ObjectAnimator.ofArgb(binding.searchIcon, "colorFilter", Color.WHITE, iconColor),
|
|
||||||
ObjectAnimator.ofArgb(binding.overflowMenu, "colorFilter", Color.WHITE, iconColor),
|
|
||||||
ObjectAnimator.ofFloat(binding.searchIcon, "alpha", iconAlpha),
|
|
||||||
ObjectAnimator.ofFloat(binding.overflowMenu, "alpha", iconAlpha),
|
|
||||||
ObjectAnimator.ofFloat(binding.searchIcon, "elevation", 0f),
|
|
||||||
ObjectAnimator.ofFloat(binding.overflowMenu, "elevation", 0f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Solid style
|
|
||||||
SearchStyles.SOLID -> {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
// Hidden style
|
|
||||||
else -> {
|
|
||||||
AnimatorSet().apply {
|
|
||||||
duration = 200
|
|
||||||
playTogether(
|
|
||||||
ObjectAnimator.ofFloat(this@SearchBar, "alpha", 1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ 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.launcher.search.SearchVM
|
||||||
import de.mm20.launcher2.ui.search.UnitConverterItem
|
import de.mm20.launcher2.ui.search.UnitConverterItem
|
||||||
|
|
||||||
class UnitConverterView : FrameLayout {
|
class UnitConverterView : FrameLayout {
|
||||||
@ -36,7 +36,7 @@ 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 viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
unitConverter = viewModel.unitConverterResult
|
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
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.legacy.component
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -22,7 +21,7 @@ import de.mm20.launcher2.legacy.helper.ActivityStarter
|
|||||||
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 de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ class WebSearchView : FrameLayout {
|
|||||||
private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true)
|
private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
websearches = viewModel.websearchResults
|
websearches = viewModel.websearchResults
|
||||||
websearches.observe(context as AppCompatActivity, Observer {
|
websearches.observe(context as AppCompatActivity, Observer {
|
||||||
updateWebsearches(it)
|
updateWebsearches(it)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import androidx.lifecycle.Observer
|
|||||||
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.launcher.search.SearchVM
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
|
|
||||||
class WebsiteView : FrameLayout {
|
class WebsiteView : FrameLayout {
|
||||||
@ -35,7 +35,7 @@ class WebsiteView : FrameLayout {
|
|||||||
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: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
website = viewModel.websiteResult
|
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
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import androidx.lifecycle.LiveData
|
|||||||
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.launcher.search.SearchVM
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
|
|
||||||
class WikipediaView : FrameLayout {
|
class WikipediaView : FrameLayout {
|
||||||
@ -34,7 +34,7 @@ class WikipediaView : FrameLayout {
|
|||||||
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: SearchViewModel by (context as AppCompatActivity).viewModels()
|
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||||
wikipedia = viewModel.wikipediaResult
|
wikipedia = viewModel.wikipediaResult
|
||||||
wikipedia.observe(context as AppCompatActivity, {
|
wikipedia.observe(context as AppCompatActivity, {
|
||||||
visibility = if (it == null) View.GONE else View.VISIBLE
|
visibility = if (it == null) View.GONE else View.VISIBLE
|
||||||
|
|||||||
@ -6,11 +6,11 @@ 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.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
||||||
val viewModel: SearchViewModel by viewModel()
|
val viewModel: SearchVM by viewModel()
|
||||||
val apps by viewModel.appResults.observeAsState(emptyList())
|
val apps by viewModel.appResults.observeAsState(emptyList())
|
||||||
return {
|
return {
|
||||||
SearchableGrid(items = apps, listState = it)
|
SearchableGrid(items = apps, listState = it)
|
||||||
|
|||||||
@ -8,11 +8,11 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.ui.component.SectionDivider
|
import de.mm20.launcher2.ui.component.SectionDivider
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun calculatorItem(): LazyListScope.() -> Unit {
|
fun calculatorItem(): LazyListScope.() -> Unit {
|
||||||
val viewModel: SearchViewModel by viewModel()
|
val viewModel: SearchVM by viewModel()
|
||||||
val calculator by viewModel.calculatorResult.observeAsState(null)
|
val calculator by viewModel.calculatorResult.observeAsState(null)
|
||||||
return {
|
return {
|
||||||
calculator?.let {
|
calculator?.let {
|
||||||
|
|||||||
@ -6,11 +6,11 @@ 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.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
||||||
val viewModel: SearchViewModel by viewModel()
|
val viewModel: SearchVM by viewModel()
|
||||||
|
|
||||||
val favorites by viewModel.favorites.observeAsState(emptyList())
|
val favorites by viewModel.favorites.observeAsState(emptyList())
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -5,11 +5,11 @@ 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.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun fileResults(): LazyListScope.() -> Unit {
|
fun fileResults(): LazyListScope.() -> Unit {
|
||||||
val viewModel: SearchViewModel by viewModel()
|
val viewModel: SearchVM by viewModel()
|
||||||
val files by viewModel.fileResults.observeAsState(emptyList())
|
val files by viewModel.fileResults.observeAsState(emptyList())
|
||||||
return {
|
return {
|
||||||
files?.let { SearchableList(items = it) }
|
files?.let { SearchableList(items = it) }
|
||||||
|
|||||||
@ -7,12 +7,12 @@ 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.ui.launcher.search.SearchViewModel
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
import org.koin.androidx.compose.viewModel
|
import org.koin.androidx.compose.viewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun wikipediaResult(): LazyListScope.() -> Unit {
|
fun wikipediaResult(): LazyListScope.() -> Unit {
|
||||||
val viewModel: SearchViewModel by viewModel()
|
val viewModel: SearchVM by viewModel()
|
||||||
val wikipedia by viewModel.wikipediaResult.observeAsState()
|
val wikipedia by viewModel.wikipediaResult.observeAsState()
|
||||||
return {
|
return {
|
||||||
wikipedia?.let {
|
wikipedia?.let {
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/rootView"
|
android:id="@+id/rootView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -18,168 +16,11 @@
|
|||||||
android:layout_height="96dp"
|
android:layout_height="96dp"
|
||||||
android:layout_gravity="bottom|center_horizontal" />
|
android:layout_gravity="bottom|center_horizontal" />
|
||||||
|
|
||||||
<FrameLayout
|
<de.mm20.launcher2.ui.launcher.LauncherScaffoldView
|
||||||
android:id="@+id/container"
|
android:id="@+id/container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true" />
|
||||||
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
|
||||||
android:id="@+id/scrollView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:scrollbars="none"
|
|
||||||
tools:context=".activity.LauncherActivity">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/scrollContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingStart="8dp"
|
|
||||||
android:paddingTop="56dp"
|
|
||||||
android:paddingEnd="8dp"
|
|
||||||
android:paddingBottom="8dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/searchContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/webSearchViewSpacer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.FavoritesView
|
|
||||||
android:id="@+id/favoritesView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.ApplicationView
|
|
||||||
android:id="@+id/applicationView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.CalculatorView
|
|
||||||
android:id="@+id/calculatorView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.ContactView
|
|
||||||
android:id="@+id/contactView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.CalendarView
|
|
||||||
android:id="@+id/calendarView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.UnitConverterView
|
|
||||||
android:id="@+id/unitConverterView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.FileView
|
|
||||||
android:id="@+id/fileView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.WebsiteView
|
|
||||||
android:id="@+id/websiteView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.WikipediaView
|
|
||||||
android:id="@+id/wikipediaView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/widgetContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:paddingTop="8dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.widget.ClockWidget
|
|
||||||
android:id="@+id/clockWidget"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<com.jmedeisis.draglinearlayout.DragLinearLayout
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:id="@+id/widgetList"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<!-- Widgets will be added here -->
|
|
||||||
|
|
||||||
</com.jmedeisis.draglinearlayout.DragLinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
|
||||||
android:id="@+id/fabEditWidget"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:text="@string/menu_edit_widgets"
|
|
||||||
app:icon="@drawable/ic_edit" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.component.SearchBar
|
|
||||||
android:id="@+id/searchBar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:descendantFocusability="beforeDescendants"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
app:cardElevation="@dimen/card_elevation" />
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/editWidgetToolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:elevation="4dp"
|
|
||||||
android:translationY="-56dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
style="@style/Widget.Material3.Toolbar.Surface"
|
|
||||||
app:title="@string/menu_edit_widgets" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/activityStartOverlay"
|
android:id="@+id/activityStartOverlay"
|
||||||
|
|||||||
61
ui/src/main/res/layout/view_launcher_scaffold.xml
Normal file
61
ui/src/main/res/layout/view_launcher_scaffold.xml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:layout_height="match_parent"
|
||||||
|
tools:layout_width="match_parent"
|
||||||
|
tools:parentTag="android.widget.FrameLayout">
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/scrollView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:scrollbars="none"
|
||||||
|
tools:context=".activity.LauncherActivity">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/scrollContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.launcher.search.SearchView
|
||||||
|
android:id="@+id/searchContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.launcher.widgets.WidgetsView
|
||||||
|
android:id="@+id/widgetContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.SearchBar
|
||||||
|
android:id="@+id/searchBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="112dp"
|
||||||
|
android:descendantFocusability="beforeDescendants"
|
||||||
|
android:focusableInTouchMode="true" />
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/editWidgetToolbar"
|
||||||
|
style="@style/Widget.Material3.Toolbar.Surface"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:translationY="-56dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:title="@string/menu_edit_widgets" />
|
||||||
|
</merge>
|
||||||
68
ui/src/main/res/layout/view_search.xml
Normal file
68
ui/src/main/res/layout/view_search.xml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:parentTag="android.widget.LinearLayout"
|
||||||
|
tools:layout_width="match_parent"
|
||||||
|
tools:layout_height="wrap_content">
|
||||||
|
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/webSearchViewSpacer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.FavoritesView
|
||||||
|
android:id="@+id/favoritesView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.ApplicationView
|
||||||
|
android:id="@+id/applicationView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.CalculatorView
|
||||||
|
android:id="@+id/calculatorView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.ContactView
|
||||||
|
android:id="@+id/contactView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.CalendarView
|
||||||
|
android:id="@+id/calendarView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.UnitConverterView
|
||||||
|
android:id="@+id/unitConverterView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.FileView
|
||||||
|
android:id="@+id/fileView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.WebsiteView
|
||||||
|
android:id="@+id/websiteView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.component.WikipediaView
|
||||||
|
android:id="@+id/wikipediaView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</merge>
|
||||||
35
ui/src/main/res/layout/view_widgets.xml
Normal file
35
ui/src/main/res/layout/view_widgets.xml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
tools:layout_height="wrap_content"
|
||||||
|
tools:layout_width="match_parent">
|
||||||
|
|
||||||
|
<de.mm20.launcher2.ui.legacy.widget.ClockWidget
|
||||||
|
android:id="@+id/clockWidget"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<com.jmedeisis.draglinearlayout.DragLinearLayout
|
||||||
|
android:id="@+id/widgetList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Widgets will be added here -->
|
||||||
|
|
||||||
|
</com.jmedeisis.draglinearlayout.DragLinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/fabEditWidget"
|
||||||
|
app:icon="@drawable/ic_edit"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="@string/menu_edit_widgets" />
|
||||||
|
</merge>
|
||||||
Loading…
x
Reference in New Issue
Block a user