Add search quick actions
This commit is contained in:
parent
9a514dff31
commit
f862a578a1
@ -135,6 +135,7 @@ dependencies {
|
|||||||
implementation(project(":widgets"))
|
implementation(project(":widgets"))
|
||||||
implementation(project(":wikipedia"))
|
implementation(project(":wikipedia"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
|
implementation(project(":search-actions"))
|
||||||
|
|
||||||
// Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is
|
// Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is
|
||||||
//debugImplementation(libs.leakcanary)
|
//debugImplementation(libs.leakcanary)
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import de.mm20.launcher2.database.databaseModule
|
|||||||
import de.mm20.launcher2.notifications.notificationsModule
|
import de.mm20.launcher2.notifications.notificationsModule
|
||||||
import de.mm20.launcher2.permissions.permissionsModule
|
import de.mm20.launcher2.permissions.permissionsModule
|
||||||
import de.mm20.launcher2.preferences.preferencesModule
|
import de.mm20.launcher2.preferences.preferencesModule
|
||||||
|
import de.mm20.launcher2.searchactions.searchActionsModule
|
||||||
import de.mm20.launcher2.weather.weatherModule
|
import de.mm20.launcher2.weather.weatherModule
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
@ -68,6 +69,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
|
|||||||
permissionsModule,
|
permissionsModule,
|
||||||
preferencesModule,
|
preferencesModule,
|
||||||
searchModule,
|
searchModule,
|
||||||
|
searchActionsModule,
|
||||||
unitConverterModule,
|
unitConverterModule,
|
||||||
weatherModule,
|
weatherModule,
|
||||||
websitesModule,
|
websitesModule,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.3.1")
|
classpath("com.android.tools.build:gradle:7.3.1")
|
||||||
classpath(libs.kotlin.gradle)
|
classpath(libs.kotlin.gradle)
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
package de.mm20.launcher2.database.entities
|
||||||
|
|
||||||
|
data class SearchActionEntity(
|
||||||
|
val type: String,
|
||||||
|
val data: String? = null,
|
||||||
|
val options: String? = null,
|
||||||
|
val position: Int,
|
||||||
|
)
|
||||||
@ -676,4 +676,15 @@
|
|||||||
<string name="frequently_used_show_in_favorites">Show in favorites</string>
|
<string name="frequently_used_show_in_favorites">Show in favorites</string>
|
||||||
<string name="frequently_used_rows">Number of rows</string>
|
<string name="frequently_used_rows">Number of rows</string>
|
||||||
<string name="customize_tags_placeholder">Tags…</string>
|
<string name="customize_tags_placeholder">Tags…</string>
|
||||||
|
|
||||||
|
<string name="preference_screen_search_actions">Quick actions</string>
|
||||||
|
<string name="preference_search_search_actions_summary">Configure quick actions and search shortcuts</string>
|
||||||
|
<string name="search_action_call">Call</string>
|
||||||
|
<string name="search_action_message">Message</string>
|
||||||
|
<string name="search_action_email">Email</string>
|
||||||
|
<string name="search_action_alarm">Set alarm</string>
|
||||||
|
<string name="search_action_timer">Start timer</string>
|
||||||
|
<string name="search_action_contact">Add to contacts</string>
|
||||||
|
<string name="search_action_open_url">View website</string>
|
||||||
|
<string name="search_action_event">Schedule event</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -22,7 +22,7 @@ internal val Context.dataStore: LauncherDataStore by dataStore(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
internal const val SchemaVersion = 11
|
internal const val SchemaVersion = 12
|
||||||
|
|
||||||
internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
|
internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
|
||||||
return listOf(
|
return listOf(
|
||||||
@ -37,5 +37,6 @@ internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
|
|||||||
Migration_8_9(),
|
Migration_8_9(),
|
||||||
Migration_9_10(),
|
Migration_9_10(),
|
||||||
Migration_10_11(),
|
Migration_10_11(),
|
||||||
|
Migration_11_12(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -162,6 +162,17 @@ fun createFactorySettings(context: Context): Settings {
|
|||||||
Settings.WidgetSettings.newBuilder()
|
Settings.WidgetSettings.newBuilder()
|
||||||
.setEditButton(true)
|
.setEditButton(true)
|
||||||
)
|
)
|
||||||
|
.setSearchActions(
|
||||||
|
Settings.SearchActionSettings.newBuilder()
|
||||||
|
.setCall(true)
|
||||||
|
.setContact(true)
|
||||||
|
.setEmail(true)
|
||||||
|
.setMessage(true)
|
||||||
|
.setOpenUrl(true)
|
||||||
|
.setScheduleEvent(true)
|
||||||
|
.setSetAlarm(true)
|
||||||
|
.setStartTimer(true)
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.mm20.launcher2.preferences.migrations
|
||||||
|
|
||||||
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
|
||||||
|
class Migration_11_12: VersionedMigration(11, 12) {
|
||||||
|
override suspend fun applyMigrations(builder: Settings.Builder): Settings.Builder {
|
||||||
|
return builder
|
||||||
|
.setSearchActions(
|
||||||
|
Settings.SearchActionSettings.newBuilder()
|
||||||
|
.setCall(true)
|
||||||
|
.setContact(true)
|
||||||
|
.setEmail(true)
|
||||||
|
.setMessage(true)
|
||||||
|
.setOpenUrl(true)
|
||||||
|
.setScheduleEvent(true)
|
||||||
|
.setSetAlarm(true)
|
||||||
|
.setStartTimer(true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -288,4 +288,16 @@ message Settings {
|
|||||||
bool edit_button = 1;
|
bool edit_button = 1;
|
||||||
}
|
}
|
||||||
WidgetSettings widgets = 26;
|
WidgetSettings widgets = 26;
|
||||||
|
|
||||||
|
message SearchActionSettings {
|
||||||
|
bool call = 1;
|
||||||
|
bool message = 2;
|
||||||
|
bool email = 3;
|
||||||
|
bool contact = 4;
|
||||||
|
bool open_url = 5;
|
||||||
|
bool schedule_event = 6;
|
||||||
|
bool set_alarm = 7;
|
||||||
|
bool start_timer = 8;
|
||||||
|
}
|
||||||
|
SearchActionSettings search_actions = 27;
|
||||||
}
|
}
|
||||||
1
search-actions/.gitignore
vendored
Normal file
1
search-actions/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
48
search-actions/build.gradle.kts
Normal file
48
search-actions/build.gradle.kts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("kotlin-android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = sdk.versions.minSdk.get().toInt()
|
||||||
|
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
namespace = "de.mm20.launcher2.searchactions"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.bundles.kotlin)
|
||||||
|
implementation(libs.androidx.core)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
|
implementation(project(":base"))
|
||||||
|
implementation(project(":database"))
|
||||||
|
implementation(project(":ktx"))
|
||||||
|
implementation(project(":preferences"))
|
||||||
|
|
||||||
|
}
|
||||||
0
search-actions/consumer-rules.pro
Normal file
0
search-actions/consumer-rules.pro
Normal file
21
search-actions/proguard-rules.pro
vendored
Normal file
21
search-actions/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
4
search-actions/src/main/AndroidManifest.xml
Normal file
4
search-actions/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package de.mm20.launcher2.searchactions
|
||||||
|
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val searchActionsModule = module {
|
||||||
|
single<SearchActionRepository> { SearchActionRepositoryImpl() }
|
||||||
|
single<SearchActionService> { SearchActionServiceImpl(androidContext(), get(), TextClassifierImpl()) }
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package de.mm20.launcher2.searchactions
|
||||||
|
|
||||||
|
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface SearchActionRepository {
|
||||||
|
fun getSearchActionBuilders(filter: TextType?): Flow<List<SearchActionBuilder>>
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SearchActionRepositoryImpl: SearchActionRepository {
|
||||||
|
override fun getSearchActionBuilders(filter: TextType?): Flow<List<SearchActionBuilder>> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package de.mm20.launcher2.searchactions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.EmailActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.MessageActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.OpenUrlActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.ScheduleEventActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.SetAlarmActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
interface SearchActionService {
|
||||||
|
fun search(settings: SearchActionSettings, query: String): Flow<ImmutableList<SearchAction>>
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SearchActionServiceImpl(
|
||||||
|
private val context: Context,
|
||||||
|
private val repository: SearchActionRepository,
|
||||||
|
private val textClassifier: TextClassifier,
|
||||||
|
) : SearchActionService {
|
||||||
|
override fun search(settings: SearchActionSettings, query: String): Flow<ImmutableList<SearchAction>> = flow {
|
||||||
|
if (query.isBlank()) {
|
||||||
|
emit(persistentListOf())
|
||||||
|
return@flow
|
||||||
|
}
|
||||||
|
|
||||||
|
val builders = mutableListOf<SearchActionBuilder>()
|
||||||
|
|
||||||
|
if (settings.call) builders.add(CallActionBuilder)
|
||||||
|
if (settings.message) builders.add(MessageActionBuilder)
|
||||||
|
if (settings.contact) builders.add(CreateContactActionBuilder)
|
||||||
|
if (settings.email) builders.add(EmailActionBuilder)
|
||||||
|
if (settings.openUrl) builders.add(OpenUrlActionBuilder)
|
||||||
|
if (settings.scheduleEvent) builders.add(ScheduleEventActionBuilder)
|
||||||
|
if (settings.setAlarm) builders.add(SetAlarmActionBuilder)
|
||||||
|
if (settings.startTimer) builders.add(TimerActionBuilder)
|
||||||
|
|
||||||
|
val classificationResult = textClassifier.classify(context, query)
|
||||||
|
|
||||||
|
|
||||||
|
emit(builders.mapNotNull { it.build(context, classificationResult) }.toImmutableList())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
package de.mm20.launcher2.searchactions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.icu.text.SimpleDateFormat
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
internal interface TextClassifier {
|
||||||
|
suspend fun classify(context: Context, query: String): TextClassificationResult
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TextClassifierImpl : TextClassifier {
|
||||||
|
override suspend fun classify(context: Context, query: String): TextClassificationResult {
|
||||||
|
return when {
|
||||||
|
query.matches(Regex("^\\S+@\\S+$")) -> TextClassificationResult(
|
||||||
|
type = TextType.Email,
|
||||||
|
text = query,
|
||||||
|
email = query
|
||||||
|
)
|
||||||
|
|
||||||
|
query.matches(Regex("^\\+?[0-9- ]{4,18}$")) -> TextClassificationResult(
|
||||||
|
type = TextType.PhoneNumber,
|
||||||
|
text = query,
|
||||||
|
phoneNumber = query
|
||||||
|
)
|
||||||
|
|
||||||
|
query.matches(Regex("^(http(s)?://.)?(www\\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_+.~#?&/=]*)$")) -> TextClassificationResult(
|
||||||
|
type = TextType.Url,
|
||||||
|
text = query,
|
||||||
|
url = query
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
parseDate(context, query)?.let { return it }
|
||||||
|
TextClassificationResult(type = TextType.Text, text = query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDate(context: Context, query: String): TextClassificationResult? {
|
||||||
|
val dateTimeFormat = SimpleDateFormat(
|
||||||
|
DateFormat.getBestDateTimePattern(
|
||||||
|
Locale.getDefault(),
|
||||||
|
"yyyy-MM-dd, HH:mm"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
dateTimeFormat.parse(query)?.let {
|
||||||
|
val dateTime = LocalDateTime.ofInstant(it.toInstant(), ZoneId.systemDefault())
|
||||||
|
return TextClassificationResult(
|
||||||
|
type = TextType.DateTime,
|
||||||
|
text = query,
|
||||||
|
time = dateTime.toLocalTime(),
|
||||||
|
date = dateTime.toLocalDate(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
// Not a datetime
|
||||||
|
}
|
||||||
|
val dateFormat = DateFormat.getDateFormat(context)
|
||||||
|
try {
|
||||||
|
dateFormat.parse(query)?.let {
|
||||||
|
return TextClassificationResult(
|
||||||
|
type = TextType.Date,
|
||||||
|
text = query,
|
||||||
|
date = LocalDateTime.ofInstant(it.toInstant(), ZoneId.systemDefault())
|
||||||
|
.toLocalDate()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
// Not a date either
|
||||||
|
}
|
||||||
|
val timeFormat = DateFormat.getTimeFormat(context)
|
||||||
|
try {
|
||||||
|
timeFormat.parse(query)?.let {
|
||||||
|
return TextClassificationResult(
|
||||||
|
type = TextType.Time,
|
||||||
|
text = query,
|
||||||
|
time = LocalDateTime.ofInstant(it.toInstant(), ZoneId.systemDefault())
|
||||||
|
.toLocalTime(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
// Nope, not a time
|
||||||
|
}
|
||||||
|
|
||||||
|
val seconds = context.getString(R.string.unit_second_symbol)
|
||||||
|
if (query.matches(Regex("^[0-9]+ ${seconds}$"))) {
|
||||||
|
val value = query.substringBefore(" ").toLong()
|
||||||
|
return TextClassificationResult(
|
||||||
|
type = TextType.Timespan,
|
||||||
|
text = query,
|
||||||
|
timespan = Duration.ofSeconds(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val days = context.getString(R.string.unit_day_symbol)
|
||||||
|
if (query.matches(Regex("^[0-9]+ ${days}$"))) {
|
||||||
|
val value = query.substringBefore(" ").toLong()
|
||||||
|
return TextClassificationResult(
|
||||||
|
type = TextType.Timespan,
|
||||||
|
text = query,
|
||||||
|
timespan = Duration.ofDays(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val minutes = context.getString(R.string.unit_minute_symbol)
|
||||||
|
if (query.matches(Regex("^[0-9]+ ${minutes}$"))) {
|
||||||
|
val value = query.substringBefore(" ").toLong()
|
||||||
|
val then = LocalDateTime.now().plusMinutes(value)
|
||||||
|
return TextClassificationResult(
|
||||||
|
type = TextType.Timespan,
|
||||||
|
text = query,
|
||||||
|
timespan = Duration.ofMinutes(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val hours = context.getString(R.string.unit_hour_symbol)
|
||||||
|
if (query.matches(Regex("^[0-9]+ ${hours}$"))) {
|
||||||
|
val value = query.substringBefore(" ").toLong()
|
||||||
|
return TextClassificationResult(
|
||||||
|
type = TextType.Timespan,
|
||||||
|
text = query,
|
||||||
|
timespan = Duration.ofHours(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TextClassificationResult(
|
||||||
|
val type: TextType,
|
||||||
|
val text: String,
|
||||||
|
val email: String? = null,
|
||||||
|
val phoneNumber: String? = null,
|
||||||
|
val time: LocalTime? = null,
|
||||||
|
val date: LocalDate? = null,
|
||||||
|
val timespan: Duration? = null,
|
||||||
|
val url: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class TextType {
|
||||||
|
Text,
|
||||||
|
Email,
|
||||||
|
Url,
|
||||||
|
PhoneNumber,
|
||||||
|
DateTime,
|
||||||
|
Date,
|
||||||
|
Time,
|
||||||
|
Timespan,
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
|
||||||
|
data class CallAction(
|
||||||
|
override val label: String,
|
||||||
|
val number: String,
|
||||||
|
): SearchAction {
|
||||||
|
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Phone
|
||||||
|
override val iconColor: Int = 0
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_DIAL).apply {
|
||||||
|
data = Uri.parse("tel:$number")
|
||||||
|
}
|
||||||
|
context.tryStartActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import android.provider.ContactsContract.Intents.Insert
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
|
||||||
|
class CreateContactAction(
|
||||||
|
override val label: String,
|
||||||
|
val phone: String? = null,
|
||||||
|
val email: String? = null,
|
||||||
|
) : SearchAction {
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Contact
|
||||||
|
override val iconColor: Int = 0
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_INSERT).apply {
|
||||||
|
type = ContactsContract.Contacts.CONTENT_TYPE
|
||||||
|
if (email != null) putExtra(Insert.EMAIL, email)
|
||||||
|
if (phone != null) putExtra(Insert.PHONE, phone)
|
||||||
|
}
|
||||||
|
context.tryStartActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
|
||||||
|
data class EmailAction(
|
||||||
|
override val label: String,
|
||||||
|
val email: String,
|
||||||
|
) : SearchAction {
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Email
|
||||||
|
override val iconColor: Int = 0
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO).apply {
|
||||||
|
type = "*/*"
|
||||||
|
data = Uri.parse("mailto:$email")
|
||||||
|
}
|
||||||
|
context.tryStartActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
|
||||||
|
data class MessageAction(
|
||||||
|
override val label: String,
|
||||||
|
val number: String,
|
||||||
|
): SearchAction {
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Message
|
||||||
|
override val iconColor: Int = 0
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO).apply {
|
||||||
|
data = Uri.parse("sms:$number")
|
||||||
|
}
|
||||||
|
context.tryStartActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
|
||||||
|
data class OpenUrlAction(
|
||||||
|
override val label: String,
|
||||||
|
val url: String,
|
||||||
|
) : SearchAction {
|
||||||
|
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Website
|
||||||
|
override val iconColor: Int = 0
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
val url = if (url.startsWith("https://") || url.startsWith("http://")) url else "https://$url"
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
data = Uri.parse(url)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
context.tryStartActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.CalendarContract
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
data class ScheduleEventAction(
|
||||||
|
override val label: String,
|
||||||
|
val date: LocalDate,
|
||||||
|
val time: LocalTime?,
|
||||||
|
) : SearchAction {
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Calendar
|
||||||
|
override val iconColor: Int = 0
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
|
||||||
|
val startTime = date.let {
|
||||||
|
if (time != null) it.atTime(time)
|
||||||
|
else it.atTime(0, 0)
|
||||||
|
}.atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_INSERT).apply {
|
||||||
|
type = "vnd.android.cursor.dir/event"
|
||||||
|
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startTime)
|
||||||
|
if (time == null) putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, true)
|
||||||
|
}
|
||||||
|
context.tryStartActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
|
|
||||||
|
interface SearchAction : Searchable {
|
||||||
|
val label: String
|
||||||
|
val icon: SearchActionIcon
|
||||||
|
val iconColor: Int
|
||||||
|
fun start(context: Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SearchActionIcon {
|
||||||
|
Search,
|
||||||
|
Website,
|
||||||
|
Alarm,
|
||||||
|
Timer,
|
||||||
|
Contact,
|
||||||
|
Phone,
|
||||||
|
Email,
|
||||||
|
Message,
|
||||||
|
Calendar,
|
||||||
|
Translate,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.AlarmClock
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
data class SetAlarmAction(
|
||||||
|
override val label: String,
|
||||||
|
val time: LocalTime
|
||||||
|
) : SearchAction {
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Alarm
|
||||||
|
override val iconColor: Int = 0
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
val intent = Intent(AlarmClock.ACTION_SET_ALARM).apply {
|
||||||
|
putExtra(AlarmClock.EXTRA_HOUR, time.hour)
|
||||||
|
putExtra(AlarmClock.EXTRA_MINUTES, time.minute)
|
||||||
|
}
|
||||||
|
context.tryStartActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.AlarmClock
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
data class TimerAction(
|
||||||
|
override val label: String,
|
||||||
|
val length: Duration
|
||||||
|
): SearchAction {
|
||||||
|
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Timer
|
||||||
|
override val iconColor: Int = 0
|
||||||
|
|
||||||
|
override fun start(context: Context) {
|
||||||
|
val intent = Intent(AlarmClock.ACTION_SET_TIMER).apply {
|
||||||
|
putExtra(AlarmClock.EXTRA_LENGTH, length.seconds.toInt())
|
||||||
|
}
|
||||||
|
context.tryStartActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.actions.CallAction
|
||||||
|
|
||||||
|
object CallActionBuilder: SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (classifiedQuery.phoneNumber != null) {
|
||||||
|
return CallAction(
|
||||||
|
context.getString(R.string.search_action_call), classifiedQuery.phoneNumber
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.actions.CreateContactAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
|
||||||
|
object CreateContactActionBuilder : SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (classifiedQuery.phoneNumber != null || classifiedQuery.email != null) {
|
||||||
|
return CreateContactAction(
|
||||||
|
context.getString(R.string.search_action_contact),
|
||||||
|
phone = classifiedQuery.phoneNumber,
|
||||||
|
email = classifiedQuery.email,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.actions.EmailAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
|
||||||
|
object EmailActionBuilder: SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (classifiedQuery.email != null) {
|
||||||
|
return EmailAction(
|
||||||
|
context.getString(R.string.search_action_email), classifiedQuery.email
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.actions.MessageAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
|
||||||
|
object MessageActionBuilder: SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (classifiedQuery.phoneNumber != null) {
|
||||||
|
return MessageAction(
|
||||||
|
context.getString(R.string.search_action_message), classifiedQuery.phoneNumber
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.actions.MessageAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.OpenUrlAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
|
||||||
|
object OpenUrlActionBuilder : SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (classifiedQuery.url != null) {
|
||||||
|
return OpenUrlAction(
|
||||||
|
context.getString(R.string.search_action_open_url), classifiedQuery.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.actions.MessageAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.ScheduleEventAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
object ScheduleEventActionBuilder : SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (classifiedQuery.date != null) {
|
||||||
|
return ScheduleEventAction(
|
||||||
|
context.getString(R.string.search_action_event),
|
||||||
|
date = classifiedQuery.date,
|
||||||
|
time = classifiedQuery.time
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (classifiedQuery.timespan != null && classifiedQuery.timespan.seconds > 86400) {
|
||||||
|
val datetime = LocalDateTime.now().plus(classifiedQuery.timespan)
|
||||||
|
return ScheduleEventAction(
|
||||||
|
context.getString(R.string.search_action_event),
|
||||||
|
date = datetime.toLocalDate(),
|
||||||
|
time = datetime.toLocalTime(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
|
||||||
|
interface SearchActionBuilder {
|
||||||
|
fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction?
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SetAlarmAction
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
object SetAlarmActionBuilder : SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (classifiedQuery.time != null) {
|
||||||
|
return SetAlarmAction(
|
||||||
|
context.getString(R.string.search_action_alarm), classifiedQuery.time
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.mm20.launcher2.searchactions.R
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.actions.TimerAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
|
||||||
|
object TimerActionBuilder : SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (classifiedQuery.timespan != null && classifiedQuery.timespan.seconds <= 86400) {
|
||||||
|
return TimerAction(
|
||||||
|
context.getString(R.string.search_action_timer), classifiedQuery.timespan
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||||
|
import de.mm20.launcher2.searchactions.TextType
|
||||||
|
import de.mm20.launcher2.searchactions.actions.OpenUrlAction
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
class WebsearchActionBuilder(
|
||||||
|
val label: String,
|
||||||
|
val urlTemplate: String,
|
||||||
|
val filter: TextType? = null,
|
||||||
|
val encoding: QueryEncoding,
|
||||||
|
) : SearchActionBuilder {
|
||||||
|
|
||||||
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
||||||
|
if (filter == null || classifiedQuery.type == filter) {
|
||||||
|
val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding))
|
||||||
|
return OpenUrlAction(
|
||||||
|
label = label,
|
||||||
|
url = url,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun encodeQuery(query: String, encoding: QueryEncoding): String {
|
||||||
|
return when (encoding) {
|
||||||
|
QueryEncoding.UrlEncode -> Uri.encode(query)
|
||||||
|
QueryEncoding.FormData -> URLEncoder.encode(query, "UTF-8")
|
||||||
|
QueryEncoding.None -> query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class QueryEncoding {
|
||||||
|
UrlEncode,
|
||||||
|
FormData,
|
||||||
|
None;
|
||||||
|
|
||||||
|
fun toInt(): Int {
|
||||||
|
return when (this) {
|
||||||
|
UrlEncode -> 0
|
||||||
|
FormData -> 1
|
||||||
|
None -> 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInt(value: Int?): QueryEncoding {
|
||||||
|
return when (value) {
|
||||||
|
1 -> FormData
|
||||||
|
2 -> None
|
||||||
|
else -> UrlEncode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -57,6 +57,7 @@ dependencies {
|
|||||||
implementation(project(":websites"))
|
implementation(project(":websites"))
|
||||||
implementation(project(":wikipedia"))
|
implementation(project(":wikipedia"))
|
||||||
implementation(project(":customattrs"))
|
implementation(project(":customattrs"))
|
||||||
|
implementation(project(":search-actions"))
|
||||||
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
|
|||||||
@ -16,6 +16,7 @@ val searchModule = module {
|
|||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
|
get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single<WebsearchRepository> { WebsearchRepositoryImpl(androidContext(), get()) }
|
single<WebsearchRepository> { WebsearchRepositoryImpl(androidContext(), get()) }
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import de.mm20.launcher2.preferences.Settings.CalculatorSearchSettings
|
|||||||
import de.mm20.launcher2.preferences.Settings.CalendarSearchSettings
|
import de.mm20.launcher2.preferences.Settings.CalendarSearchSettings
|
||||||
import de.mm20.launcher2.preferences.Settings.ContactsSearchSettings
|
import de.mm20.launcher2.preferences.Settings.ContactsSearchSettings
|
||||||
import de.mm20.launcher2.preferences.Settings.FilesSearchSettings
|
import de.mm20.launcher2.preferences.Settings.FilesSearchSettings
|
||||||
|
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
|
||||||
import de.mm20.launcher2.preferences.Settings.UnitConverterSearchSettings
|
import de.mm20.launcher2.preferences.Settings.UnitConverterSearchSettings
|
||||||
import de.mm20.launcher2.preferences.Settings.WebsiteSearchSettings
|
import de.mm20.launcher2.preferences.Settings.WebsiteSearchSettings
|
||||||
import de.mm20.launcher2.preferences.Settings.WikipediaSearchSettings
|
import de.mm20.launcher2.preferences.Settings.WikipediaSearchSettings
|
||||||
@ -30,6 +31,8 @@ import de.mm20.launcher2.search.data.OwncloudFile
|
|||||||
import de.mm20.launcher2.search.data.UnitConverter
|
import de.mm20.launcher2.search.data.UnitConverter
|
||||||
import de.mm20.launcher2.search.data.Website
|
import de.mm20.launcher2.search.data.Website
|
||||||
import de.mm20.launcher2.search.data.Wikipedia
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.SearchActionService
|
||||||
import de.mm20.launcher2.unitconverter.UnitConverterRepository
|
import de.mm20.launcher2.unitconverter.UnitConverterRepository
|
||||||
import de.mm20.launcher2.websites.WebsiteRepository
|
import de.mm20.launcher2.websites.WebsiteRepository
|
||||||
import de.mm20.launcher2.wikipedia.WikipediaRepository
|
import de.mm20.launcher2.wikipedia.WikipediaRepository
|
||||||
@ -56,6 +59,7 @@ interface SearchService {
|
|||||||
unitConverter: UnitConverterSearchSettings,
|
unitConverter: UnitConverterSearchSettings,
|
||||||
websites: WebsiteSearchSettings,
|
websites: WebsiteSearchSettings,
|
||||||
wikipedia: WikipediaSearchSettings,
|
wikipedia: WikipediaSearchSettings,
|
||||||
|
searchActions: SearchActionSettings,
|
||||||
): Flow<ImmutableList<Searchable>>
|
): Flow<ImmutableList<Searchable>>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +73,7 @@ internal class SearchServiceImpl(
|
|||||||
private val unitConverterRepository: UnitConverterRepository,
|
private val unitConverterRepository: UnitConverterRepository,
|
||||||
private val calculatorRepository: CalculatorRepository,
|
private val calculatorRepository: CalculatorRepository,
|
||||||
private val websiteRepository: WebsiteRepository,
|
private val websiteRepository: WebsiteRepository,
|
||||||
|
private val searchActionService: SearchActionService,
|
||||||
private val customAttributesRepository: CustomAttributesRepository,
|
private val customAttributesRepository: CustomAttributesRepository,
|
||||||
) : SearchService {
|
) : SearchService {
|
||||||
|
|
||||||
@ -82,6 +87,7 @@ internal class SearchServiceImpl(
|
|||||||
unitConverter: UnitConverterSearchSettings,
|
unitConverter: UnitConverterSearchSettings,
|
||||||
websites: WebsiteSearchSettings,
|
websites: WebsiteSearchSettings,
|
||||||
wikipedia: WikipediaSearchSettings,
|
wikipedia: WikipediaSearchSettings,
|
||||||
|
searchActions: SearchActionSettings,
|
||||||
): Flow<ImmutableList<Searchable>> = channelFlow {
|
): Flow<ImmutableList<Searchable>> = channelFlow {
|
||||||
supervisorScope {
|
supervisorScope {
|
||||||
val results = MutableStateFlow(SearchResults())
|
val results = MutableStateFlow(SearchResults())
|
||||||
@ -213,6 +219,14 @@ internal class SearchServiceImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
searchActionService.search(searchActions, query)
|
||||||
|
.collectLatest { r ->
|
||||||
|
results.update {
|
||||||
|
it.copy(searchActions = r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
launch {
|
launch {
|
||||||
results
|
results
|
||||||
.map { it.toList().sortedBy { it as? SavableSearchable }.toImmutableList() }
|
.map { it.toList().sortedBy { it as? SavableSearchable }.toImmutableList() }
|
||||||
@ -234,9 +248,10 @@ internal data class SearchResults(
|
|||||||
val unitConverters: List<UnitConverter> = emptyList(),
|
val unitConverters: List<UnitConverter> = emptyList(),
|
||||||
val websites: List<Website> = emptyList(),
|
val websites: List<Website> = emptyList(),
|
||||||
val wikipedia: List<Wikipedia> = emptyList(),
|
val wikipedia: List<Wikipedia> = emptyList(),
|
||||||
|
val searchActions: List<SearchAction> = emptyList(),
|
||||||
val other: List<SavableSearchable> = emptyList(),
|
val other: List<SavableSearchable> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun toList(): List<Searchable> {
|
fun toList(): List<Searchable> {
|
||||||
return (apps + shortcuts + contacts + calendars + files + websites + wikipedia + other).distinctBy { it.key } + calculators + unitConverters
|
return searchActions + (apps + shortcuts + contacts + calendars + files + websites + wikipedia + other).distinctBy { it.key } + calculators + unitConverters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -287,3 +287,4 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
include(":search-actions")
|
||||||
|
|||||||
@ -141,4 +141,5 @@ dependencies {
|
|||||||
implementation(project(":owncloud"))
|
implementation(project(":owncloud"))
|
||||||
implementation(project(":accounts"))
|
implementation(project(":accounts"))
|
||||||
implementation(project(":backup"))
|
implementation(project(":backup"))
|
||||||
|
implementation(project(":search-actions"))
|
||||||
}
|
}
|
||||||
@ -8,7 +8,6 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusManager
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
@ -22,12 +21,13 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
|
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
|
||||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -162,9 +162,9 @@ fun AssistantScaffold(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val searchVM: SearchVM = viewModel()
|
val searchVM: SearchVM = viewModel()
|
||||||
val websearches by searchVM.websearchResults.observeAsState(emptyList())
|
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||||
val webSearchPadding by animateDpAsState(
|
val webSearchPadding by animateDpAsState(
|
||||||
if (websearches.isEmpty()) 0.dp else 48.dp
|
if (actions.isEmpty()) 0.dp else 48.dp
|
||||||
)
|
)
|
||||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||||
Box(
|
Box(
|
||||||
@ -182,8 +182,12 @@ fun AssistantScaffold(
|
|||||||
state = searchState
|
state = searchState
|
||||||
)
|
)
|
||||||
|
|
||||||
SearchBar(
|
val value by searchVM.searchQuery.observeAsState("")
|
||||||
level = { searchBarLevel },
|
|
||||||
|
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
|
||||||
|
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
|
||||||
|
|
||||||
|
LauncherSearchBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
@ -197,10 +201,17 @@ fun AssistantScaffold(
|
|||||||
searchBarOffset.toInt() * if (bottomSearchBar == true) -1 else 1
|
searchBarOffset.toInt() * if (bottomSearchBar == true) -1 else 1
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
level = { searchBarLevel },
|
||||||
focused = searchBarFocused,
|
focused = searchBarFocused,
|
||||||
onFocusChange = {
|
onFocusChange = {
|
||||||
|
if (it) viewModel.openSearch()
|
||||||
viewModel.setSearchbarFocus(it)
|
viewModel.setSearchbarFocus(it)
|
||||||
},
|
},
|
||||||
|
actions = actions,
|
||||||
|
value = { value },
|
||||||
|
onValueChange = { searchVM.search(it) },
|
||||||
|
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
|
||||||
|
style = searchBarStyle,
|
||||||
reverse = bottomSearchBar == true
|
reverse = bottomSearchBar == true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,7 @@ fun LauncherCard(
|
|||||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
|
color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
|
||||||
shadowElevation = if (backgroundOpacity == 1f) elevation else 0.dp,
|
shadowElevation = if (backgroundOpacity == 1f) elevation else 0.dp,
|
||||||
tonalElevation = elevation
|
tonalElevation = elevation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
205
ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt
Normal file
205
ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package de.mm20.launcher2.ui.component
|
||||||
|
|
||||||
|
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.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.icons.rounded.Search
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: Settings.SearchBarSettings.SearchBarStyle,
|
||||||
|
level: SearchBarLevel,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||||
|
onFocus: () -> Unit = {},
|
||||||
|
onUnfocus: () -> Unit = {},
|
||||||
|
reverse: Boolean = false,
|
||||||
|
darkColors: Boolean = false,
|
||||||
|
menu: @Composable RowScope.() -> Unit = {},
|
||||||
|
actions: @Composable () -> Unit = {},
|
||||||
|
) {
|
||||||
|
|
||||||
|
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 && style != Settings.SearchBarSettings.SearchBarStyle.Solid -> 0.dp
|
||||||
|
it == SearchBarLevel.Raised -> 8.dp
|
||||||
|
else -> 2.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 = 200)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
when {
|
||||||
|
it == SearchBarLevel.Active -> LocalCardStyle.current.opacity
|
||||||
|
style != Settings.SearchBarSettings.SearchBarStyle.Transparent -> 1f
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
when {
|
||||||
|
style != Settings.SearchBarSettings.SearchBarStyle.Transparent -> MaterialTheme.colorScheme.onSurface
|
||||||
|
it == SearchBarLevel.Resting -> if (darkColors) Color(0, 0, 0, 180) else Color.White
|
||||||
|
else -> MaterialTheme.colorScheme.onSurface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val opacity by transition.animateFloat(label = "opacity") {
|
||||||
|
if (style == Settings.SearchBarSettings.SearchBarStyle.Hidden && it == SearchBarLevel.Resting) 0f
|
||||||
|
else 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
LauncherCard(
|
||||||
|
modifier = modifier
|
||||||
|
.alpha(opacity),
|
||||||
|
backgroundOpacity = backgroundOpacity,
|
||||||
|
elevation = elevation
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalContentColor provides contentColor
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.height(48.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
imageVector = androidx.compose.material.icons.Icons.Rounded.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = contentColor
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.search_bar_placeholder),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = contentColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LaunchedEffect(level) {
|
||||||
|
if (level == SearchBarLevel.Resting) onUnfocus()
|
||||||
|
}
|
||||||
|
BasicTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.onFocusChanged {
|
||||||
|
if (it.hasFocus) onFocus()
|
||||||
|
}
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
textStyle = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
color = contentColor
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
menu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -52,4 +52,6 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
val wallpaperBlur = dataStore.data.map { it.appearance.blurWallpaper }.asLiveData()
|
val wallpaperBlur = dataStore.data.map { it.appearance.blurWallpaper }.asLiveData()
|
||||||
|
|
||||||
val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }.asLiveData()
|
val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }.asLiveData()
|
||||||
|
val searchBarColor = dataStore.data.map { it.searchBar.color }.asLiveData()
|
||||||
|
val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }.asLiveData()
|
||||||
}
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package de.mm20.launcher2.ui.launcher
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@ -8,7 +7,24 @@ import androidx.compose.animation.slideIn
|
|||||||
import androidx.compose.animation.slideOut
|
import androidx.compose.animation.slideOut
|
||||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@ -17,9 +33,19 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.rounded.Done
|
import androidx.compose.material.icons.rounded.Done
|
||||||
import androidx.compose.material.rememberSwipeableState
|
import androidx.compose.material.rememberSwipeableState
|
||||||
import androidx.compose.material.swipeable
|
import androidx.compose.material.swipeable
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@ -37,15 +63,18 @@ import androidx.compose.ui.unit.Velocity
|
|||||||
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 com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
||||||
|
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
|
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||||
import de.mm20.launcher2.ui.utils.rememberNotificationShadeController
|
import de.mm20.launcher2.ui.utils.rememberNotificationShadeController
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
@ -64,6 +93,8 @@ fun PagerScaffold(
|
|||||||
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
||||||
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
||||||
|
|
||||||
|
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||||
|
|
||||||
val widgetsScrollState = rememberScrollState()
|
val widgetsScrollState = rememberScrollState()
|
||||||
val searchState = rememberLazyListState()
|
val searchState = rememberLazyListState()
|
||||||
val swipeableState = rememberSwipeableState(if (isSearchOpen) Page.Search else Page.Widgets)
|
val swipeableState = rememberSwipeableState(if (isSearchOpen) Page.Search else Page.Widgets)
|
||||||
@ -76,7 +107,8 @@ fun PagerScaffold(
|
|||||||
|
|
||||||
val isSearchAtEnd by remember {
|
val isSearchAtEnd by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val lastItem = searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
|
val lastItem =
|
||||||
|
searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
|
||||||
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
|
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,9 +211,11 @@ fun PagerScaffold(
|
|||||||
viewModel.closeSearch()
|
viewModel.closeSearch()
|
||||||
searchVM.search("")
|
searchVM.search("")
|
||||||
}
|
}
|
||||||
|
|
||||||
isWidgetEditMode -> {
|
isWidgetEditMode -> {
|
||||||
viewModel.setWidgetEditMode(false)
|
viewModel.setWidgetEditMode(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
widgetsScrollState.value != 0 -> {
|
widgetsScrollState.value != 0 -> {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
widgetsScrollState.animateScrollTo(0)
|
widgetsScrollState.animateScrollTo(0)
|
||||||
@ -226,14 +260,16 @@ fun PagerScaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchNestedScrollConnection = remember { object: NestedScrollConnection {
|
val searchNestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
if (source == NestedScrollSource.Drag && available.y.absoluteValue > available.x.absoluteValue * 2) {
|
if (source == NestedScrollSource.Drag && available.y.absoluteValue > available.x.absoluteValue * 2) {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
}
|
}
|
||||||
return super.onPreScroll(available, source)
|
return super.onPreScroll(available, source)
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val insets = WindowInsets.safeDrawing.asPaddingValues()
|
val insets = WindowInsets.safeDrawing.asPaddingValues()
|
||||||
|
|
||||||
@ -296,7 +332,7 @@ fun PagerScaffold(
|
|||||||
|
|
||||||
val clockHeight by remember {
|
val clockHeight by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (fillClockHeight){
|
if (fillClockHeight) {
|
||||||
height - (64.dp + insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding)
|
height - (64.dp + insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@ -338,10 +374,8 @@ fun PagerScaffold(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val websearches by searchVM.websearchResults.observeAsState(emptyList())
|
|
||||||
val webSearchPadding by animateDpAsState(
|
val webSearchPadding by animateDpAsState(
|
||||||
if (websearches.isEmpty()) 0.dp else 48.dp
|
if (actions.isEmpty()) 0.dp else 48.dp
|
||||||
)
|
)
|
||||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||||
SearchColumn(
|
SearchColumn(
|
||||||
@ -398,17 +432,29 @@ fun PagerScaffold(
|
|||||||
if (isWidgetEditMode) 128.dp else 0.dp
|
if (isWidgetEditMode) 128.dp else 0.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
SearchBar(
|
val value by searchVM.searchQuery.observeAsState("")
|
||||||
|
|
||||||
|
val searchBarColor by viewModel.searchBarColor.observeAsState(SearchBarColors.Auto)
|
||||||
|
val searchBarStyle by viewModel.searchBarStyle.observeAsState(SearchBarStyle.Transparent)
|
||||||
|
|
||||||
|
LauncherSearchBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.offset(y = widgetEditModeOffset),
|
.offset(y = widgetEditModeOffset),
|
||||||
level = { searchBarLevel }, focused = focusSearchBar, onFocusChange = {
|
level = { searchBarLevel },
|
||||||
|
focused = focusSearchBar,
|
||||||
|
onFocusChange = {
|
||||||
if (it) viewModel.openSearch()
|
if (it) viewModel.openSearch()
|
||||||
viewModel.setSearchbarFocus(it)
|
viewModel.setSearchbarFocus(it)
|
||||||
},
|
},
|
||||||
|
actions = actions,
|
||||||
|
value = { value },
|
||||||
|
onValueChange = { searchVM.search(it) },
|
||||||
|
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
|
||||||
|
style = searchBarStyle,
|
||||||
reverse = true
|
reverse = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,15 +34,17 @@ import androidx.compose.ui.unit.Velocity
|
|||||||
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 com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import de.mm20.launcher2.preferences.Settings
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
import de.mm20.launcher2.ui.ktx.animateTo
|
import de.mm20.launcher2.ui.ktx.animateTo
|
||||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
|
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@ -58,6 +60,8 @@ fun PullDownScaffold(
|
|||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||||
|
|
||||||
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
||||||
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
||||||
|
|
||||||
@ -277,9 +281,8 @@ fun PullDownScaffold(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val websearches by searchVM.websearchResults.observeAsState(emptyList())
|
|
||||||
val webSearchPadding by animateDpAsState(
|
val webSearchPadding by animateDpAsState(
|
||||||
if (websearches.isEmpty()) 0.dp else 48.dp
|
if (actions.isEmpty()) 0.dp else 48.dp
|
||||||
)
|
)
|
||||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||||
SearchColumn(
|
SearchColumn(
|
||||||
@ -392,8 +395,12 @@ fun PullDownScaffold(
|
|||||||
if (isWidgetEditMode) -128.dp else 0.dp
|
if (isWidgetEditMode) -128.dp else 0.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
SearchBar(
|
val value by searchVM.searchQuery.observeAsState("")
|
||||||
level = { searchBarLevel },
|
|
||||||
|
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
|
||||||
|
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
|
||||||
|
|
||||||
|
LauncherSearchBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
@ -410,11 +417,17 @@ fun PullDownScaffold(
|
|||||||
.roundToInt()
|
.roundToInt()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
level = { searchBarLevel },
|
||||||
focused = searchBarFocused,
|
focused = searchBarFocused,
|
||||||
onFocusChange = {
|
onFocusChange = {
|
||||||
if (it) viewModel.openSearch()
|
if (it) viewModel.openSearch()
|
||||||
viewModel.setSearchbarFocus(it)
|
viewModel.setSearchbarFocus(it)
|
||||||
}
|
},
|
||||||
|
actions = actions,
|
||||||
|
value = { value },
|
||||||
|
onValueChange = { searchVM.search(it) },
|
||||||
|
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
|
||||||
|
style = searchBarStyle,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,403 +0,0 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
|
||||||
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.res.animatedVectorResource
|
|
||||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
|
||||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.HelpOutline
|
|
||||||
import androidx.compose.material.icons.rounded.Search
|
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
|
||||||
import androidx.compose.material.icons.rounded.Wallpaper
|
|
||||||
import androidx.compose.material3.AssistChip
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ElevatedAssistChip
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
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.AsyncImage
|
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
|
||||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings
|
|
||||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
|
||||||
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.layout.BottomReversed
|
|
||||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
|
||||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
|
||||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import org.koin.androidx.compose.inject
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SearchBar(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
level: () -> SearchBarLevel,
|
|
||||||
focused: Boolean,
|
|
||||||
onFocusChange: (Boolean) -> Unit,
|
|
||||||
reverse: Boolean = false,
|
|
||||||
) {
|
|
||||||
val searchViewModel: SearchVM = viewModel()
|
|
||||||
val activityViewModel: LauncherActivityVM = viewModel()
|
|
||||||
|
|
||||||
val dataStore: LauncherDataStore by inject()
|
|
||||||
|
|
||||||
val style by remember { dataStore.data.map { it.searchBar.searchBarStyle } }
|
|
||||||
.collectAsState(SearchBarSettings.SearchBarStyle.Hidden)
|
|
||||||
|
|
||||||
val color by remember { dataStore.data.map { it.searchBar.color } }
|
|
||||||
.collectAsState(SearchBarSettings.SearchBarColors.Auto)
|
|
||||||
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
LaunchedEffect(focused) {
|
|
||||||
if (focused) focusRequester.requestFocus()
|
|
||||||
else focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
val query by searchViewModel.searchQuery.observeAsState("")
|
|
||||||
|
|
||||||
val websearches by searchViewModel.websearchResults.observeAsState(emptyList())
|
|
||||||
|
|
||||||
SearchBar(
|
|
||||||
modifier,
|
|
||||||
level(),
|
|
||||||
websearches,
|
|
||||||
value = query,
|
|
||||||
onValueChange = {
|
|
||||||
searchViewModel.search(it)
|
|
||||||
},
|
|
||||||
style = style,
|
|
||||||
overflowMenu = { show, onDismissRequest ->
|
|
||||||
DropdownMenu(expanded = show, onDismissRequest = onDismissRequest) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
Intent(Intent.ACTION_SET_WALLPAPER),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.wallpaper))
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(imageVector = Icons.Rounded.Wallpaper, contentDescription = null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
context.startActivity(Intent(context, SettingsActivity::class.java))
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.settings))
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(imageVector = Icons.Rounded.Settings, contentDescription = null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
CustomTabsIntent.Builder()
|
|
||||||
.setDefaultColorSchemeParams(
|
|
||||||
CustomTabColorSchemeParams.Builder()
|
|
||||||
.setToolbarColor(colorScheme.primaryContainer.toArgb())
|
|
||||||
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build().launchUrl(context, Uri.parse("https://kvaesitso.mm20.de/docs/user-guide"))
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(stringResource(R.string.help))
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(imageVector = Icons.Rounded.HelpOutline, contentDescription = null)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
onFocus = {
|
|
||||||
onFocusChange(true)
|
|
||||||
},
|
|
||||||
onUnfocus = {
|
|
||||||
onFocusChange(false)
|
|
||||||
},
|
|
||||||
reverse = reverse,
|
|
||||||
darkColors = color == SearchBarColors.Dark || color == SearchBarColors.Auto && LocalPreferDarkContentOverWallpaper.current
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SearchBar(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
level: SearchBarLevel,
|
|
||||||
websearches: List<Websearch>,
|
|
||||||
overflowMenu: @Composable (show: Boolean, onDismissRequest: () -> Unit) -> Unit = { _, _ -> },
|
|
||||||
value: String,
|
|
||||||
style: SearchBarSettings.SearchBarStyle,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
onFocus: () -> Unit = {},
|
|
||||||
onUnfocus: () -> Unit = {},
|
|
||||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
|
||||||
reverse: Boolean = false,
|
|
||||||
darkColors: Boolean = false,
|
|
||||||
) {
|
|
||||||
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 && style != SearchBarSettings.SearchBarStyle.Solid -> 0.dp
|
|
||||||
it == SearchBarLevel.Raised -> 8.dp
|
|
||||||
else -> 2.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 = 200)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
when {
|
|
||||||
it == SearchBarLevel.Active -> LocalCardStyle.current.opacity
|
|
||||||
style != SearchBarSettings.SearchBarStyle.Transparent -> 1f
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
when {
|
|
||||||
style != SearchBarSettings.SearchBarStyle.Transparent -> MaterialTheme.colorScheme.onSurface
|
|
||||||
it == SearchBarLevel.Resting -> if (darkColors) Color(0, 0, 0, 180) else Color.White
|
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val opacity by transition.animateFloat(label = "opacity") {
|
|
||||||
if (style == SearchBarSettings.SearchBarStyle.Hidden && it == SearchBarLevel.Resting) 0f
|
|
||||||
else 1f
|
|
||||||
}
|
|
||||||
|
|
||||||
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
|
||||||
|
|
||||||
LauncherCard(
|
|
||||||
modifier = modifier
|
|
||||||
.alpha(opacity),
|
|
||||||
backgroundOpacity = backgroundOpacity,
|
|
||||||
elevation = elevation
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
|
|
||||||
) {
|
|
||||||
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.search_bar_placeholder),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = contentColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LaunchedEffect(level) {
|
|
||||||
if (level == SearchBarLevel.Resting) onUnfocus()
|
|
||||||
}
|
|
||||||
BasicTextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.onFocusChanged {
|
|
||||||
if (it.hasFocus) onFocus()
|
|
||||||
}
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.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)
|
|
||||||
.padding(bottom = 12.dp, top = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
contentPadding = PaddingValues(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
items(websearches) {
|
|
||||||
AssistChip(
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
|
||||||
onClick = {
|
|
||||||
it
|
|
||||||
.getLaunchIntent()
|
|
||||||
?.let {
|
|
||||||
context.tryStartActivity(it)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text(it.label) },
|
|
||||||
leadingIcon = {
|
|
||||||
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 {
|
|
||||||
AsyncImage(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
model = File(icon),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@ -3,7 +3,6 @@ package de.mm20.launcher2.ui.launcher.search
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
@ -12,9 +11,25 @@ import de.mm20.launcher2.preferences.LauncherDataStore
|
|||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.search.SearchService
|
import de.mm20.launcher2.search.SearchService
|
||||||
import de.mm20.launcher2.search.WebsearchRepository
|
import de.mm20.launcher2.search.WebsearchRepository
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
import kotlinx.coroutines.*
|
import de.mm20.launcher2.search.data.Calculator
|
||||||
import kotlinx.coroutines.flow.*
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
|
import de.mm20.launcher2.search.data.Contact
|
||||||
|
import de.mm20.launcher2.search.data.File
|
||||||
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
|
import de.mm20.launcher2.search.data.UnitConverter
|
||||||
|
import de.mm20.launcher2.search.data.Website
|
||||||
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.shareIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -41,7 +56,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
val websiteResults = MutableLiveData<List<Website>>(emptyList())
|
val websiteResults = MutableLiveData<List<Website>>(emptyList())
|
||||||
val calculatorResults = MutableLiveData<List<Calculator>>(emptyList())
|
val calculatorResults = MutableLiveData<List<Calculator>>(emptyList())
|
||||||
val unitConverterResults = MutableLiveData<List<UnitConverter>>(emptyList())
|
val unitConverterResults = MutableLiveData<List<UnitConverter>>(emptyList())
|
||||||
val websearchResults = MutableLiveData<List<Websearch>>(emptyList())
|
val searchActionResults = MutableLiveData<List<SearchAction>>(emptyList())
|
||||||
|
|
||||||
val hiddenResults = MutableLiveData<List<SavableSearchable>>(emptyList())
|
val hiddenResults = MutableLiveData<List<SavableSearchable>>(emptyList())
|
||||||
|
|
||||||
@ -71,9 +86,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
searchJob = viewModelScope.launch {
|
searchJob = viewModelScope.launch {
|
||||||
isSearching.postValue(true)
|
isSearching.postValue(true)
|
||||||
|
|
||||||
websearchResults.value = websearchRepository.search(query).first()
|
|
||||||
|
|
||||||
|
|
||||||
dataStore.data.collectLatest {
|
dataStore.data.collectLatest {
|
||||||
searchService.search(
|
searchService.search(
|
||||||
query,
|
query,
|
||||||
@ -85,6 +97,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
shortcuts = it.appShortcutSearch,
|
shortcuts = it.appShortcutSearch,
|
||||||
websites = it.websiteSearch,
|
websites = it.websiteSearch,
|
||||||
wikipedia = it.wikipediaSearch,
|
wikipedia = it.wikipediaSearch,
|
||||||
|
searchActions = it.searchActions,
|
||||||
).collectLatest { results ->
|
).collectLatest { results ->
|
||||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
hiddenItemKeys.collectLatest { hiddenKeys ->
|
||||||
val hidden = mutableListOf<SavableSearchable>()
|
val hidden = mutableListOf<SavableSearchable>()
|
||||||
@ -98,11 +111,13 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
val calc = mutableListOf<Calculator>()
|
val calc = mutableListOf<Calculator>()
|
||||||
val wikipedia = mutableListOf<Wikipedia>()
|
val wikipedia = mutableListOf<Wikipedia>()
|
||||||
val website = mutableListOf<Website>()
|
val website = mutableListOf<Website>()
|
||||||
|
val actions = mutableListOf<SearchAction>()
|
||||||
for (r in results) {
|
for (r in results) {
|
||||||
when {
|
when {
|
||||||
r is SavableSearchable && hiddenKeys.contains(r.key) -> {
|
r is SavableSearchable && hiddenKeys.contains(r.key) -> {
|
||||||
hidden.add(r)
|
hidden.add(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
r is LauncherApp && !r.isMainProfile -> workApps.add(r)
|
r is LauncherApp && !r.isMainProfile -> workApps.add(r)
|
||||||
r is LauncherApp -> apps.add(r)
|
r is LauncherApp -> apps.add(r)
|
||||||
r is AppShortcut -> shortcuts.add(r)
|
r is AppShortcut -> shortcuts.add(r)
|
||||||
@ -113,8 +128,10 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
r is Calculator -> calc.add(r)
|
r is Calculator -> calc.add(r)
|
||||||
r is Website -> website.add(r)
|
r is Website -> website.add(r)
|
||||||
r is Wikipedia -> wikipedia.add(r)
|
r is Wikipedia -> wikipedia.add(r)
|
||||||
|
r is SearchAction -> actions.add(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
searchActionResults.value = actions
|
||||||
appResults.value = apps
|
appResults.value = apps
|
||||||
workAppResults.value = workApps
|
workAppResults.value = workApps
|
||||||
appShortcutResults.value = shortcuts
|
appShortcutResults.value = shortcuts
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.searchbar
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.ui.component.SearchBar
|
||||||
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LauncherSearchBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: Settings.SearchBarSettings.SearchBarStyle,
|
||||||
|
level: () -> SearchBarLevel,
|
||||||
|
value: () -> String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
focused: Boolean,
|
||||||
|
onFocusChange: (Boolean) -> Unit,
|
||||||
|
actions: List<SearchAction>,
|
||||||
|
reverse: Boolean = false,
|
||||||
|
darkColors: Boolean = false,
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(focused) {
|
||||||
|
if (focused) focusRequester.requestFocus()
|
||||||
|
else focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
val _value = value()
|
||||||
|
|
||||||
|
SearchBar(
|
||||||
|
modifier = modifier,
|
||||||
|
style = style, level = level(), value = _value, onValueChange = onValueChange,
|
||||||
|
reverse = reverse,
|
||||||
|
darkColors = darkColors,
|
||||||
|
menu = {
|
||||||
|
SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
SearchBarActions(actions = actions, reverse = reverse)
|
||||||
|
},
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
onFocus = { onFocusChange(true) },
|
||||||
|
onUnfocus = { onFocusChange(false) },
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.searchbar
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Alarm
|
||||||
|
import androidx.compose.material.icons.rounded.Call
|
||||||
|
import androidx.compose.material.icons.rounded.Email
|
||||||
|
import androidx.compose.material.icons.rounded.Event
|
||||||
|
import androidx.compose.material.icons.rounded.Language
|
||||||
|
import androidx.compose.material.icons.rounded.Person
|
||||||
|
import androidx.compose.material.icons.rounded.Search
|
||||||
|
import androidx.compose.material.icons.rounded.Sms
|
||||||
|
import androidx.compose.material.icons.rounded.Timer
|
||||||
|
import androidx.compose.material.icons.rounded.Translate
|
||||||
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchBarActions(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actions: List<SearchAction>,
|
||||||
|
reverse: Boolean = false,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
AnimatedVisibility(actions.isNotEmpty()) {
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(bottom = if (reverse) 4.dp else 12.dp, top = if (reverse) 12.dp else 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
items(actions) {
|
||||||
|
AssistChip(
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
onClick = {
|
||||||
|
it.start(context)
|
||||||
|
},
|
||||||
|
label = { Text(it.label) },
|
||||||
|
leadingIcon = {
|
||||||
|
val icon = it.icon
|
||||||
|
if (it.icon != SearchActionIcon.Custom) {
|
||||||
|
Icon(
|
||||||
|
imageVector = when (it.icon) {
|
||||||
|
SearchActionIcon.Phone -> Icons.Rounded.Call
|
||||||
|
SearchActionIcon.Website -> Icons.Rounded.Language
|
||||||
|
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
|
||||||
|
SearchActionIcon.Timer -> Icons.Rounded.Timer
|
||||||
|
SearchActionIcon.Contact -> Icons.Rounded.Person
|
||||||
|
SearchActionIcon.Email -> Icons.Rounded.Email
|
||||||
|
SearchActionIcon.Message -> Icons.Rounded.Sms
|
||||||
|
SearchActionIcon.Calendar -> Icons.Rounded.Event
|
||||||
|
SearchActionIcon.Translate -> Icons.Rounded.Translate
|
||||||
|
else -> Icons.Rounded.Search
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (it.iconColor == 0) MaterialTheme.colorScheme.primary else Color(
|
||||||
|
it.iconColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*leadingIcon = {
|
||||||
|
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 {
|
||||||
|
AsyncImage(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
model = File(icon),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.searchbar
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
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.layout.RowScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.HelpOutline
|
||||||
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import androidx.compose.material.icons.rounded.Wallpaper
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RowScope.SearchBarMenu(
|
||||||
|
searchBarValue: String,
|
||||||
|
onSearchBarValueChange: (newValue: String) -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||||
|
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
||||||
|
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (searchBarValue.isNotBlank()) onSearchBarValueChange("")
|
||||||
|
else showOverflowMenu = true
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
painter = rememberAnimatedVectorPainter(
|
||||||
|
rightIcon,
|
||||||
|
atEnd = searchBarValue.isNotEmpty()
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LocalContentColor.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded = showOverflowMenu, onDismissRequest = { showOverflowMenu = false }) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
context.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
Intent(Intent.ACTION_SET_WALLPAPER),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
showOverflowMenu = false
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.wallpaper))
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(imageVector = Icons.Rounded.Wallpaper, contentDescription = null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
context.startActivity(Intent(context, SettingsActivity::class.java))
|
||||||
|
showOverflowMenu = false
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.settings))
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(imageVector = Icons.Rounded.Settings, contentDescription = null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
CustomTabsIntent.Builder()
|
||||||
|
.setDefaultColorSchemeParams(
|
||||||
|
CustomTabColorSchemeParams.Builder()
|
||||||
|
.setToolbarColor(colorScheme.primaryContainer.toArgb())
|
||||||
|
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.launchUrl(context, Uri.parse("https://kvaesitso.mm20.de/docs/user-guide"))
|
||||||
|
showOverflowMenu = false
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(stringResource(R.string.help))
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(imageVector = Icons.Rounded.HelpOutline, contentDescription = null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,15 @@
|
|||||||
package de.mm20.launcher2.ui.settings
|
package de.mm20.launcher2.ui.settings
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||||
import com.google.accompanist.navigation.animation.composable
|
import com.google.accompanist.navigation.animation.composable
|
||||||
@ -16,7 +18,6 @@ import de.mm20.launcher2.licenses.AppLicense
|
|||||||
import de.mm20.launcher2.licenses.OpenSourceLicenses
|
import de.mm20.launcher2.licenses.OpenSourceLicenses
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
|
||||||
import de.mm20.launcher2.ui.base.BaseActivity
|
import de.mm20.launcher2.ui.base.BaseActivity
|
||||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||||
@ -43,11 +44,13 @@ import de.mm20.launcher2.ui.settings.license.LicenseScreen
|
|||||||
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
|
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
|
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
|
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
|
||||||
|
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
|
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
|
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.websearch.WebSearchSettingsScreen
|
import de.mm20.launcher2.ui.settings.websearch.WebSearchSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
|
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
|
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
|
||||||
|
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||||
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -119,6 +122,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/search/websearch") {
|
composable("settings/search/websearch") {
|
||||||
WebSearchSettingsScreen()
|
WebSearchSettingsScreen()
|
||||||
}
|
}
|
||||||
|
composable("settings/search/searchactions") {
|
||||||
|
SearchActionsSettingsScreen()
|
||||||
|
}
|
||||||
composable("settings/search/hiddenitems") {
|
composable("settings/search/hiddenitems") {
|
||||||
HiddenItemsSettingsScreen()
|
HiddenItemsSettingsScreen()
|
||||||
}
|
}
|
||||||
@ -164,7 +170,8 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/debug/crashreporter") {
|
composable("settings/debug/crashreporter") {
|
||||||
CrashReporterScreen()
|
CrashReporterScreen()
|
||||||
}
|
}
|
||||||
composable("settings/debug/crashreporter/report?fileName={fileName}",
|
composable(
|
||||||
|
"settings/debug/crashreporter/report?fileName={fileName}",
|
||||||
arguments = listOf(navArgument("fileName") {
|
arguments = listOf(navArgument("fileName") {
|
||||||
nullable = false
|
nullable = false
|
||||||
})
|
})
|
||||||
|
|||||||
@ -38,11 +38,11 @@ import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
|||||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
|
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
|
||||||
import de.mm20.launcher2.preferences.Settings.SystemBarsSettings.SystemBarColors
|
import de.mm20.launcher2.preferences.Settings.SystemBarsSettings.SystemBarColors
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.SearchBar
|
||||||
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
import de.mm20.launcher2.ui.component.getShape
|
import de.mm20.launcher2.ui.component.getShape
|
||||||
import de.mm20.launcher2.ui.component.preferences.*
|
import de.mm20.launcher2.ui.component.preferences.*
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
|
||||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||||
import de.mm20.launcher2.ui.theme.getTypography
|
import de.mm20.launcher2.ui.theme.getTypography
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -389,7 +389,6 @@ fun SearchBarStylePreference(
|
|||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(8.dp),
|
||||||
level = level,
|
level = level,
|
||||||
style = styles[it],
|
style = styles[it],
|
||||||
websearches = emptyList(),
|
|
||||||
value = previewSearchValue,
|
value = previewSearchValue,
|
||||||
onValueChange = {})
|
onValueChange = {})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,17 +169,12 @@ fun SearchSettingsScreen() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val webSearch by viewModel.webSearch.observeAsState()
|
Preference(
|
||||||
PreferenceWithSwitch(
|
title = stringResource(R.string.preference_screen_search_actions),
|
||||||
title = stringResource(R.string.preference_search_websearch),
|
summary = stringResource(R.string.preference_search_search_actions_summary),
|
||||||
summary = stringResource(R.string.preference_search_websearch_summary),
|
icon = Icons.Rounded.ArrowOutward,
|
||||||
icon = Icons.Rounded.TravelExplore,
|
|
||||||
switchValue = webSearch == true,
|
|
||||||
onSwitchChanged = {
|
|
||||||
viewModel.setWebSearch(it)
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
navController?.navigate("settings/search/websearch")
|
navController?.navigate("settings/search/searchactions")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,117 @@
|
|||||||
|
package de.mm20.launcher2.ui.settings.searchactions
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Alarm
|
||||||
|
import androidx.compose.material.icons.rounded.CalendarToday
|
||||||
|
import androidx.compose.material.icons.rounded.Call
|
||||||
|
import androidx.compose.material.icons.rounded.Email
|
||||||
|
import androidx.compose.material.icons.rounded.Event
|
||||||
|
import androidx.compose.material.icons.rounded.Language
|
||||||
|
import androidx.compose.material.icons.rounded.Person
|
||||||
|
import androidx.compose.material.icons.rounded.Sms
|
||||||
|
import androidx.compose.material.icons.rounded.Timer
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||||
|
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||||
|
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchActionsSettingsScreen() {
|
||||||
|
val viewModel: SearchActionsSettingsScreenVM = viewModel()
|
||||||
|
val settings by viewModel.searchActionSettings.observeAsState(
|
||||||
|
Settings.SearchActionSettings.getDefaultInstance()
|
||||||
|
)
|
||||||
|
|
||||||
|
PreferenceScreen(stringResource(id = R.string.preference_screen_search_actions)) {
|
||||||
|
item {
|
||||||
|
PreferenceCategory {
|
||||||
|
SwitchPreference(
|
||||||
|
icon = Icons.Rounded.Call,
|
||||||
|
title = stringResource(R.string.search_action_call),
|
||||||
|
value = settings.call,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.updateSettings {
|
||||||
|
setCall(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
icon = Icons.Rounded.Sms,
|
||||||
|
title = stringResource(R.string.search_action_message),
|
||||||
|
value = settings.message,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.updateSettings {
|
||||||
|
setMessage(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
icon = Icons.Rounded.Email,
|
||||||
|
title = stringResource(R.string.search_action_email),
|
||||||
|
value = settings.email,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.updateSettings {
|
||||||
|
setEmail(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
icon = Icons.Rounded.Person,
|
||||||
|
title = stringResource(R.string.search_action_contact),
|
||||||
|
value = settings.contact,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.updateSettings {
|
||||||
|
setContact(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
icon = Icons.Rounded.Alarm,
|
||||||
|
title = stringResource(R.string.search_action_alarm),
|
||||||
|
value = settings.setAlarm,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.updateSettings {
|
||||||
|
setSetAlarm(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
icon = Icons.Rounded.Timer,
|
||||||
|
title = stringResource(R.string.search_action_timer),
|
||||||
|
value = settings.startTimer,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.updateSettings {
|
||||||
|
setStartTimer(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
icon = Icons.Rounded.Event,
|
||||||
|
title = stringResource(R.string.search_action_event),
|
||||||
|
value = settings.scheduleEvent,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.updateSettings {
|
||||||
|
setScheduleEvent(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
icon = Icons.Rounded.Language,
|
||||||
|
title = stringResource(R.string.search_action_open_url),
|
||||||
|
value = settings.openUrl,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.updateSettings {
|
||||||
|
setOpenUrl(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package de.mm20.launcher2.ui.settings.searchactions
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
|
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
|
||||||
|
private val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
|
val searchActionSettings = dataStore.data.map { it.searchActions }.asLiveData()
|
||||||
|
|
||||||
|
fun updateSettings(block: SearchActionSettings.Builder.() -> SearchActionSettings.Builder) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
dataStore.updateData {
|
||||||
|
it.toBuilder()
|
||||||
|
.setSearchActions(
|
||||||
|
it.searchActions.toBuilder().block()
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user