Migrate search settings

This commit is contained in:
MM20 2022-01-10 21:41:13 +01:00
parent d91fc525f8
commit 5834e7e8c4
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
22 changed files with 964 additions and 371 deletions

View File

@ -1,46 +1,62 @@
package de.mm20.launcher2.calculator package de.mm20.launcher2.calculator
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.Calculator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.mariuszgromada.math.mxparser.Expression import org.mariuszgromada.math.mxparser.Expression
interface CalculatorRepository { interface CalculatorRepository {
fun search(query: String): Flow<Calculator?> fun search(query: String): Flow<Calculator?>
} }
class CalculatorRepositoryImpl : CalculatorRepository { class CalculatorRepositoryImpl : CalculatorRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
override fun search(query: String): Flow<Calculator?> = channelFlow { override fun search(query: String): Flow<Calculator?> = channelFlow {
if (query.isBlank()) { if (query.isBlank()) {
send(null) send(null)
return@channelFlow return@channelFlow
} }
if (!LauncherPreferences.instance.searchCalculator) return@channelFlow val searchCalculator = dataStore.data.map { it.calculatorSearch.enabled }
val calc = when { searchCalculator.collectLatest {
if (it) {
send(queryCalculator(query))
} else {
send(null)
}
}
}
private suspend fun queryCalculator(query: String): Calculator? {
return when {
query.matches(Regex("0x[0-9a-fA-F]+")) -> { query.matches(Regex("0x[0-9a-fA-F]+")) -> {
val solution = query.substring(2).toIntOrNull(16) ?: run { val solution = query.substring(2).toIntOrNull(16) ?: run {
send(null) return null
return@channelFlow
} }
Calculator(term = query, solution = solution.toDouble()) Calculator(term = query, solution = solution.toDouble())
} }
query.matches(Regex("0b[01]+")) -> { query.matches(Regex("0b[01]+")) -> {
val solution = query.substring(2).toIntOrNull(2) ?: run { val solution = query.substring(2).toIntOrNull(2) ?: run {
send(null) return null
return@channelFlow
} }
Calculator(term = query, solution = solution.toDouble()) Calculator(term = query, solution = solution.toDouble())
} }
query.matches(Regex("0[0-7]+")) -> { query.matches(Regex("0[0-7]+")) -> {
val solution = query.substring(1).toIntOrNull(8) ?: run { val solution = query.substring(1).toIntOrNull(8) ?: run {
send(null) return null
return@channelFlow
} }
Calculator(term = query, solution = solution.toDouble()) Calculator(term = query, solution = solution.toDouble())
} }
else -> { else -> {
withContext(Dispatchers.IO) {
val exp = Expression(query) val exp = Expression(query)
if (exp.checkSyntax()) { if (exp.checkSyntax()) {
Calculator(term = query, solution = exp.calculate()) Calculator(term = query, solution = exp.calculate())
@ -52,6 +68,6 @@ class CalculatorRepositoryImpl : CalculatorRepository {
} }
} }
} }
send(calc) }
} }
} }

View File

@ -1,7 +1,13 @@
package de.mm20.launcher2.calendar package de.mm20.launcher2.calendar
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.provider.CalendarContract
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -9,6 +15,10 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import java.util.*
interface CalendarRepository { interface CalendarRepository {
fun search(query: String): Flow<List<CalendarEvent>> fun search(query: String): Flow<List<CalendarEvent>>
@ -19,32 +29,129 @@ interface CalendarRepository {
class CalendarRepositoryImpl( class CalendarRepositoryImpl(
private val context: Context, private val context: Context,
hiddenItemsRepository: HiddenItemsRepository hiddenItemsRepository: HiddenItemsRepository
) : CalendarRepository { ) : CalendarRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject()
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
override fun search(query: String): Flow<List<CalendarEvent>> = channelFlow { override fun search(query: String): Flow<List<CalendarEvent>> = channelFlow {
if (query.isBlank()) { if (query.isBlank() || query.length < 3) {
send(emptyList()) send(emptyList())
return@channelFlow return@channelFlow
} }
val events = withContext(Dispatchers.IO) {
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
val searchCalendar = dataStore.data.map { it.calendarSearch.enabled }
combine(hasPermission, searchCalendar) { permission, search ->
permission && search
}.map {
if (it) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
CalendarEvent.search( queryCalendarEvents(
context,
query, query,
intervalStart = now, intervalStart = now,
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L, intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
) )
} else {
emptyList()
}
}.flatMapLatest { events ->
hiddenItems.map { hidden ->
events.filter { !hidden.contains(it.key) }
}
}.collectLatest {
send(it)
} }
hiddenItems.collectLatest { hiddenItems ->
val calendarResults = withContext(Dispatchers.IO) {
events.filter { !hiddenItems.contains(it.key) }
}
send(calendarResults)
} }
private suspend fun queryCalendarEvents(
query: String,
intervalStart: Long,
intervalEnd: Long,
limit: Int = 10,
excludeAllDayEvents: Boolean = false,
excludeCalendars: List<Long> = emptyList(),
): List<CalendarEvent> {
val results = withContext(Dispatchers.IO) {
val results = mutableListOf<CalendarEvent>()
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, intervalStart)
ContentUris.appendId(builder, intervalEnd)
val uri = builder.build()
val projection = arrayOf(
CalendarContract.Instances.EVENT_ID,
CalendarContract.Instances.TITLE,
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.END,
CalendarContract.Instances.ALL_DAY,
CalendarContract.Instances.DISPLAY_COLOR,
CalendarContract.Instances.EVENT_LOCATION,
CalendarContract.Instances.CALENDAR_ID,
CalendarContract.Instances.DESCRIPTION
)
val selection = mutableListOf<String>()
if (query.isNotEmpty()) selection.add("${CalendarContract.Instances.TITLE} LIKE ?")
if (excludeCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${excludeCalendars.joinToString()})")
if (excludeAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0")
val selArgs = if (query.isBlank()) null else arrayOf("%$query%")
val sort =
"${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else ""
val cursor = context.contentResolver.query(
uri,
projection,
selection.joinToString(separator = " AND "),
selArgs,
sort
) ?: return@withContext mutableListOf()
val proj = arrayOf(
CalendarContract.Attendees.EVENT_ID,
CalendarContract.Attendees.ATTENDEE_NAME,
CalendarContract.Attendees.ATTENDEE_EMAIL
)
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
while (cursor.moveToNext()) {
val sel = "${CalendarContract.Attendees.EVENT_ID} = ${cursor.getLong(0)}"
val cur = context.contentResolver.query(
CalendarContract.Attendees.CONTENT_URI,
proj, sel, null, s
) ?: return@withContext mutableListOf()
val attendees = mutableListOf<String>()
while (cur.moveToNext()) {
attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() }
?: cur.getString(2))
}
cur.close()
val allday = cursor.getInt(4) > 0
val begin = cursor.getLong(2)
val tzOffset = if (allday) {
Calendar.getInstance().timeZone.getOffset(begin)
} else {
0
}
val event = CalendarEvent(
label = cursor.getString(1) ?: "",
id = cursor.getLong(0),
color = cursor.getInt(5),
startTime = begin - tzOffset,
endTime = cursor.getLong(3) - tzOffset - if (allday) 1 else 0,
allDay = allday,
location = cursor.getString(6) ?: "",
attendees = attendees,
description = cursor.getStringOrNull(8)
?: "",
calendar = cursor.getLong(7)
)
results.add(event)
}
cursor.close()
return@withContext results
}
return results
} }
override fun getUpcomingEvents(): Flow<List<CalendarEvent>> = channelFlow { override fun getUpcomingEvents(): Flow<List<CalendarEvent>> = channelFlow {
@ -70,18 +177,19 @@ class CalendarRepositoryImpl(
} }
} }
merge(unselectedCalendars, hideAlldayEvents, hiddenItems).collectLatest { hideAlldayEvents.collectLatest { hideAllday ->
unselectedCalendars.collectLatest { unselected ->
hiddenItems.collectLatest { hidden ->
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) { val events = withContext(Dispatchers.IO) {
CalendarEvent.search( queryCalendarEvents(
context = context,
query = "", query = "",
intervalStart = now, intervalStart = now,
intervalEnd = end, intervalEnd = end,
limit = 700, limit = 700,
hideAllDayEvents = LauncherPreferences.instance.calendarHideAllday, excludeAllDayEvents = hideAllday,
unselectedCalendars = LauncherPreferences.instance.unselectedCalendars excludeCalendars = unselected
).filter { ).filter {
!hiddenItems.value.contains(it.key) !hiddenItems.value.contains(it.key)
} }
@ -89,6 +197,8 @@ class CalendarRepositoryImpl(
send(events) send(events)
} }
} }
}
}
var unselectedCalendars: List<Long> var unselectedCalendars: List<Long>
get() = LauncherPreferences.instance.unselectedCalendars get() = LauncherPreferences.instance.unselectedCalendars

View File

@ -80,99 +80,6 @@ class CalendarEvent(
} }
companion object: KoinComponent { companion object: KoinComponent {
fun search(
context: Context,
query: String,
intervalStart: Long,
intervalEnd: Long,
limit: Int = 10,
hideAllDayEvents: Boolean = false,
unselectedCalendars: List<Long> = emptyList(),
hiddenEvents: List<Long> = emptyList()
): List<CalendarEvent> {
val permissionsManager: PermissionsManager = get()
val results = mutableListOf<CalendarEvent>()
if (!query.isEmpty() && query.length < 3) return results
if (!LauncherPreferences.instance.searchCalendars) return listOf()
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) {
return emptyList()
}
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, intervalStart)
ContentUris.appendId(builder, intervalEnd)
val uri = builder.build()
val projection = arrayOf(
CalendarContract.Instances.EVENT_ID,
CalendarContract.Instances.TITLE,
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.END,
CalendarContract.Instances.ALL_DAY,
CalendarContract.Instances.DISPLAY_COLOR,
CalendarContract.Instances.EVENT_LOCATION,
CalendarContract.Instances.CALENDAR_ID,
CalendarContract.Instances.DESCRIPTION
)
val selection = mutableListOf<String>()
if (query.isNotEmpty()) selection.add("${CalendarContract.Instances.TITLE} LIKE ?")
if (hiddenEvents.isNotEmpty()) selection.add("${CalendarContract.Instances.EVENT_ID} NOT IN (${hiddenEvents.joinToString()})")
if (unselectedCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${unselectedCalendars.joinToString()})")
if (hideAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0")
val selArgs = if (query.isBlank()) null else arrayOf("%$query%")
val sort =
"${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else ""
val cursor = context.contentResolver.query(
uri,
projection,
selection.joinToString(separator = " AND "),
selArgs,
sort
)
?: return mutableListOf()
val proj = arrayOf(
CalendarContract.Attendees.EVENT_ID,
CalendarContract.Attendees.ATTENDEE_NAME,
CalendarContract.Attendees.ATTENDEE_EMAIL
)
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
while (cursor.moveToNext()) {
val sel = "${CalendarContract.Attendees.EVENT_ID} = ${cursor.getLong(0)}"
val cur = context.contentResolver.query(
CalendarContract.Attendees.CONTENT_URI,
proj, sel, null, s
) ?: return mutableListOf()
val attendees = mutableListOf<String>()
while (cur.moveToNext()) {
attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() }
?: cur.getString(2))
}
cur.close()
val allday = cursor.getInt(4) > 0
val begin = cursor.getLong(2)
val tzOffset = if (allday) {
Calendar.getInstance().timeZone.getOffset(begin)
} else {
0
}
val event = CalendarEvent(
label = cursor.getString(1) ?: "",
id = cursor.getLong(0),
color = cursor.getInt(5),
startTime = begin - tzOffset,
endTime = cursor.getLong(3) - tzOffset - if (allday) 1 else 0,
allDay = allday,
location = cursor.getString(6) ?: "",
attendees = attendees,
description = cursor.getStringOrNull(8)
?: "",
calendar = cursor.getLong(7)
)
results.add(event)
}
cursor.close()
return results
}
fun getCalendars(context: Context): List<UserCalendar> { fun getCalendars(context: Context): List<UserCalendar> {
val calendars = mutableListOf<UserCalendar>() val calendars = mutableListOf<UserCalendar>()

View File

@ -1,14 +1,17 @@
package de.mm20.launcher2.contacts package de.mm20.launcher2.contacts
import android.content.Context import android.content.Context
import android.provider.ContactsContract
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Contact import de.mm20.launcher2.search.data.Contact
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface ContactRepository { interface ContactRepository {
fun search(query: String): Flow<List<Contact>> fun search(query: String): Flow<List<Contact>>
@ -17,19 +20,61 @@ interface ContactRepository {
class ContactRepositoryImpl( class ContactRepositoryImpl(
private val context: Context, private val context: Context,
hiddenItemsRepository: HiddenItemsRepository hiddenItemsRepository: HiddenItemsRepository
) : ContactRepository { ) : ContactRepository, KoinComponent {
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
override fun search(query: String): Flow<List<Contact>> = channelFlow { override fun search(query: String): Flow<List<Contact>> = channelFlow {
val contacts = withContext(Dispatchers.IO) { val searchContacts = dataStore.data.map { it.contactsSearch.enabled }
Contact.search(context, query) val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
if (query.length < 3) {
send(emptyList())
return@channelFlow
} }
hiddenItems.collectLatest { hiddenItems ->
val contactResults = withContext(Dispatchers.IO) { combine(searchContacts, hasPermission) { search, permission ->
contacts.filter { !hiddenItems.contains(it.key) } search && permission
}.map {
if (it) {
queryContacts(query)
} else {
emptyList()
} }
send(contactResults) }.flatMapLatest { contacts ->
hiddenItems.map { hidden ->
contacts.filter { !hidden.contains(it.key) }
}
}.collectLatest {
send(it)
} }
} }
private suspend fun queryContacts(query: String): List<Contact> {
val results = withContext(Dispatchers.IO) {
val proj = arrayOf(
ContactsContract.RawContacts.CONTACT_ID,
ContactsContract.RawContacts._ID
)
val sel = "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ?"
val selArgs = arrayOf("%$query%")
val cursor = context.contentResolver.query(
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
) ?: return@withContext mutableListOf()
//Maps raw contact ids to contact ids
val contactMap = mutableMapOf<Long, MutableSet<Long>>()
while (cursor.moveToNext()) {
contactMap.getOrPut(cursor.getLong(0)) { mutableSetOf() }.add(cursor.getLong(1))
}
cursor.close()
val results = mutableListOf<Contact>()
for ((id, rawIds) in contactMap) {
Contact.contactById(context, id, rawIds)?.let { results.add(it) }
}
results.sortedBy { it }
}
return results
}
} }

View File

@ -74,38 +74,7 @@ class Contact(
return null return null
} }
companion object: KoinComponent { companion object {
fun search(context: Context, query: String): List<Contact> {
if (query.length < 3) return mutableListOf()
if (!LauncherPreferences.instance.searchContacts) {
return mutableListOf()
}
val permissionsManager: PermissionsManager = get()
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Contacts)) {
return mutableListOf()
}
val proj = arrayOf(
ContactsContract.RawContacts.CONTACT_ID,
ContactsContract.RawContacts._ID
)
val sel = "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ?"
val selArgs = arrayOf("%$query%")
val cursor = context.contentResolver.query(
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
) ?: return mutableListOf()
//Maps raw contact ids to contact ids
val contactMap = mutableMapOf<Long, MutableSet<Long>>()
while (cursor.moveToNext()) {
contactMap.getOrPut(cursor.getLong(0)) { mutableSetOf() }.add(cursor.getLong(1))
}
cursor.close()
val results = mutableListOf<Contact>()
for ((id, rawIds) in contactMap) {
contactById(context, id, rawIds)?.let { results.add(it) }
}
return results.sortedBy { it }
}
internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? { internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
val s = "(" + rawIds.joinToString(separator = " OR ", val s = "(" + rawIds.joinToString(separator = " OR ",
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" + transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +

View File

@ -106,34 +106,23 @@
<string name="file_meta_app_pkgname">Paketname</string> <string name="file_meta_app_pkgname">Paketname</string>
<string name="file_meta_app_min_sdk">Min-SDK-Version</string> <string name="file_meta_app_min_sdk">Min-SDK-Version</string>
<string name="preference_screen_services">Dienste</string> <string name="preference_screen_services">Dienste</string>
<string name="preference_screen_search">Suche</string>
<string name="preference_screen_search_summary">Suchkategorien</string>
<string name="preference_category_search_favorites">Favoriten</string> <string name="preference_category_search_favorites">Favoriten</string>
<string name="preference_search_show_favorites">Favoriten anzeigen</string> <string name="preference_search_show_favorites">Favoriten anzeigen</string>
<string name="preference_search_show_favorites_summary">Favoritenliste über allen Apps anzeigen</string> <string name="preference_search_show_favorites_summary">Favoritenliste über allen Apps anzeigen</string>
<string name="preference_search_auto_add_favorites">Häufig genutze Elemente</string> <string name="preference_search_auto_add_favorites">Häufig genutze Elemente</string>
<string name="preference_search_auto_add_favorites_summary">Häufig genutzte Elemente automatisch in den Favoriten anzeigen</string> <string name="preference_search_auto_add_favorites_summary">Häufig genutzte Elemente automatisch in den Favoriten anzeigen</string>
<string name="preference_category_search_files">Dateien</string> <string name="preference_category_search_files">Dateien</string>
<string name="preference_search_files">Dateisuche</string>
<string name="preference_search_files_summary">Ordner, Dokumente, Fotos und andere Dateitypen auf diesem Gerät durchsuchen</string>
<string name="preference_category_search_wikipedia">Wikipedia</string> <string name="preference_category_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia">Wikipedia durchsuchen</string>
<string name="preference_search_mobile_data">Mobile Daten verwenden</string> <string name="preference_search_mobile_data">Mobile Daten verwenden</string>
<string name="preference_search_mobile_data_summary">Zusätzliche Kosten können anfallen</string> <string name="preference_search_mobile_data_summary">Zusätzliche Kosten können anfallen</string>
<string name="preference_category_search_websites">Webseiten</string> <string name="preference_category_search_websites">Webseiten</string>
<string name="preference_search_websites">Nach Webseiten suchen</string>
<string name="preference_search_websites_summary">Probieren Sie „wikipedia.org“</string>
<string name="preference_search_websites_protocol">Standard Web-Protokoll</string> <string name="preference_search_websites_protocol">Standard Web-Protokoll</string>
<string name="preference_search_websites_protocol_summary">Standard-Protokoll, um Webseiten zu suchen, wenn nicht angegeben. Stellen Sie https:// oder http:// vor Ihre Suchanfrage, um das Protokoll explizit festzulegen</string> <string name="preference_search_websites_protocol_summary">Standard-Protokoll, um Webseiten zu suchen, wenn nicht angegeben. Stellen Sie https:// oder http:// vor Ihre Suchanfrage, um das Protokoll explizit festzulegen</string>
<string name="websites_protocol_http">HTTP (unverschlüsselt)</string> <string name="websites_protocol_http">HTTP (unverschlüsselt)</string>
<string name="websites_protocol_https">HTTPS (verschlüsselt)</string> <string name="websites_protocol_https">HTTPS (verschlüsselt)</string>
<string name="preference_category_search_calculator">Taschenrechner</string> <string name="preference_category_search_calculator">Taschenrechner</string>
<string name="preference_search_calculator_summary">Probieren Sie „4*2+9“</string>
<string name="preference_search_calculator">Taschenrechner aktivieren</string>
<string name="preference_search_activities_summary">Probieren Sie „%1$s“</string> <string name="preference_search_activities_summary">Probieren Sie „%1$s“</string>
<string name="preference_category_search_websearch">Websuche</string> <string name="preference_category_search_websearch">Websuche</string>
<string name="preference_search_websearch_summary">Shortcuts zu verschiedenen Websuch-Engines anzeigen</string>
<string name="preference_search_websearch">Websuchen-Shortcuts</string>
<string name="preference_search_edit_websearch">Websuchen bearbeiten</string> <string name="preference_search_edit_websearch">Websuchen bearbeiten</string>
<string name="file_meta_location">Ort</string> <string name="file_meta_location">Ort</string>
<string name="websearch_google">Google</string> <string name="websearch_google">Google</string>
@ -192,9 +181,7 @@
<string name="menu_contact_postal">Ort</string> <string name="menu_contact_postal">Ort</string>
<string name="contact_multiple_postals">%1$d Postadressen</string> <string name="contact_multiple_postals">%1$d Postadressen</string>
<string name="preference_category_search_calendar">Kalender</string> <string name="preference_category_search_calendar">Kalender</string>
<string name="preference_search_calendar">Kalendersuche aktivieren</string>
<string name="preference_category_search_contacts">Kontakte</string> <string name="preference_category_search_contacts">Kontakte</string>
<string name="preference_search_contacts">Kontakte durchsuchen</string>
<string name="menu_app_info">App-Info</string> <string name="menu_app_info">App-Info</string>
<string name="preference_screen_services_summary">Verbundene Accounts und Dienste verwalten</string> <string name="preference_screen_services_summary">Verbundene Accounts und Dienste verwalten</string>
<string name="preference_category_services_google">Google</string> <string name="preference_category_services_google">Google</string>
@ -277,8 +264,6 @@
<string name="weather_unknown">Unbekannt</string> <string name="weather_unknown">Unbekannt</string>
<string name="preference_location_disabled_summary">Von diesem Wetterdienst nicht unterstützt</string> <string name="preference_location_disabled_summary">Von diesem Wetterdienst nicht unterstützt</string>
<string name="preference_category_search_unitconverter">Einheitenrechner</string> <string name="preference_category_search_unitconverter">Einheitenrechner</string>
<string name="preference_search_unitconverter_summary">Probieren Sie „23 kg“ oder„5 cm &gt;&gt; in“</string>
<string name="preference_search_unitconverter">Einheitenrechner aktivieren</string>
<string name="preference_legacy_icon_bg">Symbol-Hintergrund</string> <string name="preference_legacy_icon_bg">Symbol-Hintergrund</string>
<string name="preference_legacy_icon_bg_none">Keiner</string> <string name="preference_legacy_icon_bg_none">Keiner</string>
<string name="preference_legacy_icon_bg_color">Dynamisch</string> <string name="preference_legacy_icon_bg_color">Dynamisch</string>
@ -418,8 +403,10 @@
<string name="preference_grid_column_count">Spaltenanzahl</string> <string name="preference_grid_column_count">Spaltenanzahl</string>
<string name="grant_permission">Gewähren</string> <string name="grant_permission">Gewähren</string>
<string name="missing_permission_auto_location">Standortzugriff wird benötigt um den Standort automatisch zu ermitteln</string> <string name="missing_permission_auto_location">Standortzugriff wird benötigt, um den Standort automatisch zu ermitteln</string>
<string name="missing_permission_music_widget">Benachrichtigungszugriff wird benötigt um Medienwiedergabe zu steuern</string> <string name="missing_permission_music_widget">Benachrichtigungszugriff wird benötigt, um Medienwiedergabe zu steuern</string>
<string name="missing_permission_contact_search">Kontakteberechtigung ist erforderlich, um Kontakte durchsuchen zu können</string>
<string name="missing_permission_calendar_search">Kalenderberechtigung ist erforderlich, um Kalender durchsuchen zu können</string>
<string name="weather_widget_set_location">Standort festlegen</string> <string name="weather_widget_set_location">Standort festlegen</string>
<string name="preference_screen_debug">Debug</string> <string name="preference_screen_debug">Debug</string>
@ -445,6 +432,27 @@
<string name="preference_music_filter_sources">Auf Musik-Apps begrenzen</string> <string name="preference_music_filter_sources">Auf Musik-Apps begrenzen</string>
<string name="preference_music_filter_sources_summary">Mediensitzungen von Apps ignorieren, die keine Musik-Apps sind</string> <string name="preference_music_filter_sources_summary">Mediensitzungen von Apps ignorieren, die keine Musik-Apps sind</string>
<string name="preference_screen_search">Suche</string>
<string name="preference_screen_search_summary">Konfigurieren was durchsucht werden soll</string>
<string name="preference_search_favorites">Favoriten</string>
<string name="preference_search_favorites_summary">Angepinnte und häufig genutzte Elemente über dem App-Raster anzeigen</string>
<string name="preference_search_files">Dateien</string>
<string name="preference_search_files_summary">Lokale Dateien und Cloud-Dateien durchsuchen</string>
<string name="preference_search_contacts">Kontakte</string>
<string name="preference_search_contacts_summary">Kontakte auf diesem Gerät durchsuchen</string>
<string name="preference_search_calendar">Kalender</string>
<string name="preference_search_calendar_summary">Anstehende Termine durchsuchen</string>
<string name="preference_search_calculator">Taschenrechner</string>
<string name="preference_search_calculator_summary">Mathematische Terme auswerten</string>
<string name="preference_search_unitconverter">Einheitenrechner</string>
<string name="preference_search_unitconverter_summary">Benutzung: „1,5 kg“ oder „4 cm >> in“</string>
<string name="preference_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia_summary">Die freie Enzyklopädie durchsuchen</string>
<string name="preference_search_websites">Webseiten</string>
<string name="preference_search_websites_summary">Vorschau einer Webseite anzeigen wenn die Suchanfrage eine URL ist</string>
<string name="preference_search_websearch">Websuchen</string>
<string name="preference_search_websearch_summary">Shortcuts zu verschiedenen Websuch-Engines anzeigen</string>
<string name="music_widget_default_title">%1$s spielt Medien</string> <string name="music_widget_default_title">%1$s spielt Medien</string>
<string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string> <string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string>
</resources> </resources>

View File

@ -147,8 +147,6 @@
<string name="file_meta_app_pkgname">Package name</string> <string name="file_meta_app_pkgname">Package name</string>
<string name="file_meta_app_min_sdk">Min SDK version</string> <string name="file_meta_app_min_sdk">Min SDK version</string>
<string name="preference_screen_services">Services</string> <string name="preference_screen_services">Services</string>
<string name="preference_screen_search">Search</string>
<string name="preference_screen_search_summary">Search categories</string>
<string name="preference_theme_auto">Auto (by time of day)</string> <string name="preference_theme_auto">Auto (by time of day)</string>
<string name="preference_theme_system">Follow system</string> <string name="preference_theme_system">Follow system</string>
<string name="preference_category_search_favorites">Favorites</string> <string name="preference_category_search_favorites">Favorites</string>
@ -157,29 +155,18 @@
<string name="preference_search_auto_add_favorites">Frequently used items</string> <string name="preference_search_auto_add_favorites">Frequently used items</string>
<string name="preference_search_auto_add_favorites_summary">Automatically add frequently used items to favorites</string> <string name="preference_search_auto_add_favorites_summary">Automatically add frequently used items to favorites</string>
<string name="preference_category_search_files">Files</string> <string name="preference_category_search_files">Files</string>
<string name="preference_search_files">File search</string>
<string name="preference_search_files_summary">Search folders, documents, photos and other kinds of file on this device</string>
<string name="preference_category_search_wikipedia">Wikipedia</string> <string name="preference_category_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia">Search Wikipedia</string>
<string name="preference_search_mobile_data">Allow mobile data usage</string> <string name="preference_search_mobile_data">Allow mobile data usage</string>
<string name="preference_search_mobile_data_summary">Additional fees may apply</string> <string name="preference_search_mobile_data_summary">Additional fees may apply</string>
<string name="preference_category_search_websites">Websites</string> <string name="preference_category_search_websites">Websites</string>
<string name="preference_search_websites">Search for websites</string>
<string name="preference_search_websites_summary">Try \'wikipedia.org\'</string>
<string name="preference_search_websites_protocol">Default web protocol</string> <string name="preference_search_websites_protocol">Default web protocol</string>
<string name="preference_search_websites_protocol_summary">Default protocol for websites, if not given. You can explicate protocol by adding https:// or http:// in front of your search term</string> <string name="preference_search_websites_protocol_summary">Default protocol for websites, if not given. You can explicate protocol by adding https:// or http:// in front of your search term</string>
<string name="websites_protocol_http">HTTP (unencrypted)</string> <string name="websites_protocol_http">HTTP (unencrypted)</string>
<string name="websites_protocol_https">HTTPS (encrypted)</string> <string name="websites_protocol_https">HTTPS (encrypted)</string>
<string name="preference_search_calculator">Enable calculator</string>
<string name="preference_search_calculator_summary">Try \'4*2+9\'</string>
<string name="preference_category_search_unitconverter">Unit converter</string> <string name="preference_category_search_unitconverter">Unit converter</string>
<string name="preference_search_unitconverter_summary">Try \'23 kg\' or \'5 cm >> in\'</string>
<string name="preference_search_unitconverter">Enable unit converter</string>
<string name="preference_category_search_calculator">Calculator</string> <string name="preference_category_search_calculator">Calculator</string>
<string name="preference_search_activities_summary">Try \'%1$s\'</string> <string name="preference_search_activities_summary">Try \'%1$s\'</string>
<string name="preference_category_search_websearch">Web search</string> <string name="preference_category_search_websearch">Web search</string>
<string name="preference_search_websearch_summary">Show shortcuts to several web search engines</string>
<string name="preference_search_websearch">Web search shortcuts</string>
<string name="preference_search_edit_websearch">Edit web searches</string> <string name="preference_search_edit_websearch">Edit web searches</string>
<string name="file_meta_location">Location</string> <string name="file_meta_location">Location</string>
<string name="websearch_google">Google</string> <string name="websearch_google">Google</string>
@ -237,9 +224,7 @@
<string name="menu_contact_postal">Location</string> <string name="menu_contact_postal">Location</string>
<string name="contact_multiple_postals">%1$d postal addresses</string> <string name="contact_multiple_postals">%1$d postal addresses</string>
<string name="preference_category_search_calendar">Calendar</string> <string name="preference_category_search_calendar">Calendar</string>
<string name="preference_search_calendar">Search calendar events</string>
<string name="preference_category_search_contacts">Contacts</string> <string name="preference_category_search_contacts">Contacts</string>
<string name="preference_search_contacts">Search contacts</string>
<string name="menu_app_info">App info</string> <string name="menu_app_info">App info</string>
<string name="preference_screen_services_summary">Manage connected accounts and services</string> <string name="preference_screen_services_summary">Manage connected accounts and services</string>
<string name="preference_category_services_google">Google</string> <string name="preference_category_services_google">Google</string>
@ -458,6 +443,8 @@
<string name="grant_permission">Grant</string> <string name="grant_permission">Grant</string>
<string name="missing_permission_auto_location">Location access is required to determine the location automatically</string> <string name="missing_permission_auto_location">Location access is required to determine the location automatically</string>
<string name="missing_permission_music_widget">Notification access is required to control media playback</string> <string name="missing_permission_music_widget">Notification access is required to control media playback</string>
<string name="missing_permission_contact_search">Contact permission is required to search contacts</string>
<string name="missing_permission_calendar_search">Calendar permission is required to search contacts</string>
<string name="weather_widget_set_location">Set location</string> <string name="weather_widget_set_location">Set location</string>
<string name="preference_screen_debug">Debug</string> <string name="preference_screen_debug">Debug</string>
@ -480,6 +467,27 @@
<string name="preference_crash_reporter_summary">Error and crash reports</string> <string name="preference_crash_reporter_summary">Error and crash reports</string>
<string name="preference_export_log">Export log file</string> <string name="preference_export_log">Export log file</string>
<string name="preference_screen_search">Search</string>
<string name="preference_screen_search_summary">Configure what should be searched</string>
<string name="preference_search_favorites">Favorites</string>
<string name="preference_search_favorites_summary">Show pinned and frequently used items above app grid</string>
<string name="preference_search_files">Files</string>
<string name="preference_search_files_summary">Search local files and cloud files</string>
<string name="preference_search_contacts">Contacts</string>
<string name="preference_search_contacts_summary">Search contacts on this device</string>
<string name="preference_search_calendar">Calendar</string>
<string name="preference_search_calendar_summary">Search upcoming calendar events</string>
<string name="preference_search_calculator">Calculator</string>
<string name="preference_search_calculator_summary">Evaluate mathematical terms</string>
<string name="preference_search_unitconverter">Unit converter</string>
<string name="preference_search_unitconverter_summary">Usage: "1.5 kg" or "4 cm >> in"</string>
<string name="preference_search_wikipedia">Wikipedia</string>
<string name="preference_search_wikipedia_summary">Search the free encyclopedia</string>
<string name="preference_search_websites">Websites</string>
<string name="preference_search_websites_summary">Show a preview of a website if the search query is a URL</string>
<string name="preference_search_websearch">Web search</string>
<string name="preference_search_websearch_summary">Show shortcuts to different search engines</string>
<string name="preference_music_filter_sources">Restrict to music apps</string> <string name="preference_music_filter_sources">Restrict to music apps</string>
<string name="preference_music_filter_sources_summary">Ignore media sessions of apps that are not music apps</string> <string name="preference_music_filter_sources_summary">Ignore media sessions of apps that are not music apps</string>

View File

@ -27,5 +27,46 @@ fun createFactorySettings(context: Context): Settings {
.setClockStyle(Settings.ClockWidgetSettings.ClockStyle.DigitalClock1) .setClockStyle(Settings.ClockWidgetSettings.ClockStyle.DigitalClock1)
.build() .build()
) )
.setFavorites(Settings.FavoritesSettings
.newBuilder()
.setEnabled(true)
)
.setFileSearch(Settings.FilesSearchSettings
.newBuilder()
.setLocalFiles(true)
.setNextcloud(false)
.setGdrive(false)
.setOnedrive(false)
.setNextcloud(false)
)
.setContactsSearch(Settings.ContactsSearchSettings
.newBuilder()
.setEnabled(true)
)
.setCalendarSearch(Settings.CalendarSearchSettings
.newBuilder()
.setEnabled(true)
)
.setCalculatorSearch(Settings.CalculatorSearchSettings
.newBuilder()
.setEnabled(true)
)
.setUnitConverterSearch(Settings.UnitConverterSearchSettings
.newBuilder()
.setEnabled(true)
)
.setWikipediaSearch(Settings.WikipediaSearchSettings
.newBuilder()
.setEnabled(false)
.setCustomUrl(null)
)
.setWebsiteSearch(Settings.WebsiteSearchSettings
.newBuilder()
.setEnabled(false)
)
.setWebSearch(Settings.WebSearchSettings
.newBuilder()
.setEnabled(true)
)
.build() .build()
} }

View File

@ -52,4 +52,55 @@ message Settings {
} }
ClockWidgetSettings clock_widget = 7; ClockWidgetSettings clock_widget = 7;
message FavoritesSettings {
bool enabled = 1;
}
FavoritesSettings favorites = 8;
message FilesSearchSettings {
bool local_files = 1;
bool gdrive = 2;
bool onedrive = 3;
bool nextcloud = 4;
bool owncloud = 5;
}
FilesSearchSettings file_search = 9;
message ContactsSearchSettings {
bool enabled = 1;
}
ContactsSearchSettings contacts_search = 10;
message CalendarSearchSettings {
bool enabled = 1;
}
CalendarSearchSettings calendar_search = 11;
message CalculatorSearchSettings {
bool enabled = 1;
}
CalculatorSearchSettings calculator_search = 12;
message UnitConverterSearchSettings {
bool enabled = 1;
}
UnitConverterSearchSettings unit_converter_search = 13;
message WikipediaSearchSettings {
bool enabled = 1;
bool images = 2;
string custom_url = 3;
}
WikipediaSearchSettings wikipedia_search = 14;
message WebsiteSearchSettings {
bool enabled = 1;
}
WebsiteSearchSettings website_search = 15;
message WebSearchSettings {
bool enabled = 1;
}
WebSearchSettings web_search = 16;
} }

View File

@ -45,4 +45,5 @@ dependencies {
implementation(project(":base")) implementation(project(":base"))
implementation(project(":database")) implementation(project(":database"))
implementation(project(":preferences"))
} }

View File

@ -1,9 +1,12 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Websearch import de.mm20.launcher2.search.data.Websearch
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface WebsearchRepository { interface WebsearchRepository {
fun search(query: String): Flow<List<Websearch>> fun search(query: String): Flow<List<Websearch>>
@ -16,7 +19,9 @@ interface WebsearchRepository {
class WebsearchRepositoryImpl( class WebsearchRepositoryImpl(
private val database: AppDatabase private val database: AppDatabase
) : WebsearchRepository { ) : WebsearchRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
@ -25,6 +30,8 @@ class WebsearchRepositoryImpl(
send(emptyList()) send(emptyList())
return@channelFlow return@channelFlow
} }
dataStore.data.map { it.webSearch.enabled }.collectLatest {
if (it) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
database.searchDao().getWebSearches().map { database.searchDao().getWebSearches().map {
it.map { Websearch(it, query) } it.map { Websearch(it, query) }
@ -32,6 +39,10 @@ class WebsearchRepositoryImpl(
}.collectLatest { }.collectLatest {
send(it) send(it)
} }
} else {
send(emptyList())
}
}
} }
override fun getWebsearches(): Flow<List<Websearch>> = override fun getWebsearches(): Flow<List<Websearch>> =

View File

@ -0,0 +1,57 @@
package de.mm20.launcher2.ui.component.preferences
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun PreferenceWithSwitch(
title: String,
summary: String? = null,
icon: ImageVector? = null,
enabled: Boolean = true,
onClick: () -> Unit = {},
switchValue: Boolean,
onSwitchChanged: (Boolean) -> Unit
) {
Row(
verticalAlignment = (Alignment.CenterVertically)
) {
Box(
modifier = Modifier.weight(1f)
) {
Preference(
title = title,
summary = summary,
icon = icon,
enabled = enabled,
onClick = onClick
)
}
Box(
modifier = Modifier
.height(36.dp)
.width(1.dp)
.alpha(0.38f)
.background(LocalContentColor.current)
)
Switch(
modifier = Modifier.padding(horizontal = 16.dp),
checked = switchValue,
enabled = enabled,
onCheckedChange = onSwitchChanged,
colors = SwitchDefaults.colors(
uncheckedThumbColor = MaterialTheme.colorScheme.onSurface
)
)
}
}

View File

@ -764,3 +764,46 @@ val Icons.Rounded.Fdroid
close() close()
} }
} }
val Icons.Rounded.Wikipedia
get() = materialIcon("Icons.Rounded.Wikipedia") {
materialPath {
moveTo(14.97f, 18.95f)
lineTo(12.41f, 12.92f)
curveTo(11.39f, 14.91f, 10.27f, 17f, 9.31f, 18.95f)
curveTo(9.3f, 18.96f, 8.84f, 18.95f, 8.84f, 18.95f)
curveTo(7.37f, 15.5f, 5.85f, 12.1f, 4.37f, 8.68f)
curveTo(4.03f, 7.84f, 2.83f, 6.5f, 2f, 6.5f)
curveTo(2f, 6.4f, 2f, 6.18f, 2f, 6.05f)
horizontalLineTo(7.06f)
verticalLineTo(6.5f)
curveTo(6.46f, 6.5f, 5.44f, 6.9f, 5.7f, 7.55f)
curveTo(6.42f, 9.09f, 8.94f, 15.06f, 9.63f, 16.58f)
curveTo(10.1f, 15.64f, 11.43f, 13.16f, 12f, 12.11f)
curveTo(11.55f, 11.23f, 10.13f, 7.93f, 9.71f, 7.11f)
curveTo(9.39f, 6.57f, 8.58f, 6.5f, 7.96f, 6.5f)
curveTo(7.96f, 6.35f, 7.97f, 6.25f, 7.96f, 6.06f)
lineTo(12.42f, 6.07f)
verticalLineTo(6.47f)
curveTo(11.81f, 6.5f, 11.24f, 6.71f, 11.5f, 7.29f)
curveTo(12.1f, 8.53f, 12.45f, 9.42f, 13f, 10.57f)
curveTo(13.17f, 10.23f, 14.07f, 8.38f, 14.5f, 7.41f)
curveTo(14.76f, 6.76f, 14.37f, 6.5f, 13.29f, 6.5f)
curveTo(13.3f, 6.38f, 13.3f, 6.17f, 13.3f, 6.07f)
curveTo(14.69f, 6.06f, 16.78f, 6.06f, 17.15f, 6.05f)
verticalLineTo(6.47f)
curveTo(16.44f, 6.5f, 15.71f, 6.88f, 15.33f, 7.46f)
lineTo(13.5f, 11.3f)
curveTo(13.68f, 11.81f, 15.46f, 15.76f, 15.65f, 16.2f)
lineTo(19.5f, 7.37f)
curveTo(19.2f, 6.65f, 18.34f, 6.5f, 18f, 6.5f)
curveTo(18f, 6.37f, 18f, 6.2f, 18f, 6.05f)
lineTo(22f, 6.08f)
verticalLineTo(6.1f)
lineTo(22f, 6.5f)
curveTo(21.12f, 6.5f, 20.57f, 7f, 20.25f, 7.75f)
curveTo(19.45f, 9.54f, 17f, 15.24f, 15.4f, 18.95f)
curveTo(15.4f, 18.95f, 14.97f, 18.95f, 14.97f, 18.95f)
close()
}
}

View File

@ -28,6 +28,7 @@ import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.license.LicenseScreen 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.weatherwidget.WeatherWidgetSettingsScreen import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
@ -85,6 +86,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/appearance") { composable("settings/appearance") {
AppearanceSettingsScreen() AppearanceSettingsScreen()
} }
composable("settings/search") {
SearchSettingsScreen()
}
composable("settings/widgets") { composable("settings/widgets") {
WidgetsSettingsScreen() WidgetsSettingsScreen()
} }

View File

@ -33,7 +33,10 @@ fun MainSettingsScreen() {
Preference( Preference(
icon = Icons.Rounded.Search, icon = Icons.Rounded.Search,
title = stringResource(id = R.string.preference_screen_search), title = stringResource(id = R.string.preference_screen_search),
summary = stringResource(id = R.string.preference_screen_search_summary) summary = stringResource(id = R.string.preference_screen_search_summary),
onClick = {
navController?.navigate("settings/search")
}
) )
Preference( Preference(
icon = Icons.Rounded.Widgets, icon = Icons.Rounded.Widgets,

View File

@ -0,0 +1,146 @@
package de.mm20.launcher2.ui.settings.search
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.*
import de.mm20.launcher2.ui.icons.Wikipedia
@Composable
fun SearchSettingsScreen() {
val viewModel: SearchSettingsScreenVM = viewModel()
val context = LocalContext.current
PreferenceScreen(title = stringResource(R.string.preference_screen_search)) {
item {
PreferenceCategory {
val favorites by viewModel.favorites.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_favorites),
summary = stringResource(R.string.preference_search_favorites_summary),
icon = Icons.Rounded.Star,
value = favorites == true,
onValueChanged = {
viewModel.setFavorites(it)
}
)
Preference(
title = stringResource(R.string.preference_search_files),
summary = stringResource(R.string.preference_search_files_summary),
icon = Icons.Rounded.Description
)
val hasContactsPermission by viewModel.hasContactsPermission.observeAsState()
AnimatedVisibility(hasContactsPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_contact_search),
onClick = {
viewModel.requestContactsPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
val contacts by viewModel.contacts.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_contacts),
summary = stringResource(R.string.preference_search_contacts_summary),
icon = Icons.Rounded.Person,
value = contacts == true,
onValueChanged = {
viewModel.setContacts(it)
}
)
val hasCalendarPermission by viewModel.hasCalendarPermission.observeAsState()
AnimatedVisibility(hasCalendarPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search),
onClick = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
val calendar by viewModel.calendar.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
icon = Icons.Rounded.Today,
value = calendar == true,
onValueChanged = {
viewModel.setCalendar(it)
}
)
val calculator by viewModel.calculator.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_calculator),
summary = stringResource(R.string.preference_search_calculator_summary),
icon = Icons.Rounded.Calculate,
value = calculator == true,
onValueChanged = {
viewModel.setCalculator(it)
}
)
val unitConverter by viewModel.unitConverter.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_unitconverter),
summary = stringResource(R.string.preference_search_unitconverter_summary),
icon = Icons.Rounded.Loop,
value = unitConverter == true,
onValueChanged = {
viewModel.setUnitConverter(it)
}
)
val wikipedia by viewModel.wikipedia.observeAsState()
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_wikipedia),
summary = stringResource(R.string.preference_search_wikipedia_summary),
icon = Icons.Rounded.Wikipedia,
switchValue = wikipedia == true,
onSwitchChanged = {
viewModel.setWikipedia(it)
}
)
val websites by viewModel.websites.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_search_websites),
summary = stringResource(R.string.preference_search_websites_summary),
icon = Icons.Rounded.Language,
value = websites == true,
onValueChanged = {
viewModel.setWebsites(it)
}
)
val webSearch by viewModel.webSearch.observeAsState()
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_websearch),
summary = stringResource(R.string.preference_search_websearch_summary),
icon = Icons.Rounded.TravelExplore,
switchValue = webSearch == true,
onSwitchChanged = {
viewModel.setWebSearch(it)
}
)
}
}
}
}

View File

@ -0,0 +1,139 @@
package de.mm20.launcher2.ui.settings.search
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SearchSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject()
val favorites = dataStore.data.map { it.favorites.enabled }.asLiveData()
fun setFavorites(favorites: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setFavorites(
it.favorites.toBuilder()
.setEnabled(favorites)
)
.build()
}
}
}
val hasContactsPermission = permissionsManager.hasPermission(PermissionGroup.Contacts).asLiveData()
val contacts = dataStore.data.map { it.contactsSearch.enabled }.asLiveData()
fun setContacts(contacts: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setContactsSearch(
it.contactsSearch.toBuilder()
.setEnabled(contacts)
)
.build()
}
}
}
fun requestContactsPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
}
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar).asLiveData()
val calendar = dataStore.data.map { it.calendarSearch.enabled }.asLiveData()
fun setCalendar(calendar: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalendarSearch(
it.calendarSearch.toBuilder()
.setEnabled(calendar)
)
.build()
}
}
}
fun requestCalendarPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
}
val calculator = dataStore.data.map { it.calculatorSearch.enabled }.asLiveData()
fun setCalculator(calculator: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalculatorSearch(
it.calculatorSearch.toBuilder()
.setEnabled(calculator)
)
.build()
}
}
}
val unitConverter = dataStore.data.map { it.unitConverterSearch.enabled }.asLiveData()
fun setUnitConverter(unitConverter: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setUnitConverterSearch(
it.unitConverterSearch.toBuilder()
.setEnabled(unitConverter)
)
.build()
}
}
}
val wikipedia = dataStore.data.map { it.wikipediaSearch.enabled }.asLiveData()
fun setWikipedia(wikipedia: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWikipediaSearch(
it.wikipediaSearch.toBuilder()
.setEnabled(wikipedia)
)
.build()
}
}
}
val websites = dataStore.data.map { it.websiteSearch.enabled }.asLiveData()
fun setWebsites(websites: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWebsiteSearch(
it.websiteSearch.toBuilder()
.setEnabled(websites)
)
.build()
}
}
}
val webSearch = dataStore.data.map { it.webSearch.enabled }.asLiveData()
fun setWebSearch(webSearch: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setWebSearch(
it.webSearch.toBuilder()
.setEnabled(webSearch)
)
.build()
}
}
}
}

View File

@ -1,51 +1,11 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.search.data
import android.content.Context
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.unitconverter.Dimension import de.mm20.launcher2.unitconverter.Dimension
import de.mm20.launcher2.unitconverter.UnitValue import de.mm20.launcher2.unitconverter.UnitValue
import de.mm20.launcher2.unitconverter.converters.*
open class UnitConverter( open class UnitConverter(
val dimension: Dimension, val dimension: Dimension,
val inputValue: UnitValue, val inputValue: UnitValue,
val values: List<UnitValue> val values: List<UnitValue>
) {
companion object {
suspend fun search(context: Context, query: String): UnitConverter? {
if (!LauncherPreferences.instance.searchUnitConverter) return null
if (!query.matches(Regex("[0-9,.:]+ [A-Za-z/²³°.]+")) && !query.matches(Regex("[0-9,.:]+ [A-Za-z/²³°.]+ >> [A-Za-z/²³°]+"))) return null
val valueStr: String
val unitStr: String
val targetUnitStr: String?
query.split(" ").also {
valueStr = it.get(0)
unitStr = it.get(1)
targetUnitStr = it.getOrNull(3)
}
val value = valueStr.toDoubleOrNull() ?: valueStr.replace(',', '.').toDoubleOrNull()
?: return null
val converters = listOf(
lazy { MassConverter(context) },
lazy { LengthConverter(context) },
lazy { CurrencyConverter() },
lazy { DataConverter(context) },
lazy { TimeConverter(context) },
lazy { VelocityConverter(context) },
lazy { AreaConverter(context) },
lazy { TemperatureConverter(context) }
) )
for (conv in converters) {
val converter = conv.value
if (!converter.isValidUnit(unitStr)) continue
if (targetUnitStr != null && !converter.isValidUnit(targetUnitStr)) continue
return converter.convert(context, unitStr, value, targetUnitStr)
}
return null
}
}
}

View File

@ -2,19 +2,69 @@ package de.mm20.launcher2.unitconverter
import android.content.Context import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.UnitConverter import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.unitconverter.converters.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface UnitConverterRepository { interface UnitConverterRepository {
fun search(query:String): Flow<UnitConverter?> fun search(query:String): Flow<UnitConverter?>
} }
class UnitConverterRepositoryImpl(val context: Context) : UnitConverterRepository { class UnitConverterRepositoryImpl(val context: Context) : UnitConverterRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
val unitConverter = MutableLiveData<UnitConverter?>() val unitConverter = MutableLiveData<UnitConverter?>()
override fun search(query: String): Flow<UnitConverter?> = channelFlow { override fun search(query: String): Flow<UnitConverter?> = channelFlow {
send(UnitConverter.search(context, query)) if (query.isBlank()) {
send(null)
return@channelFlow
}
dataStore.data.map { it.unitConverterSearch.enabled }.collectLatest {
if (it) {
send(queryUnitConverter(query))
} else {
send(null)
}
}
}
private suspend fun queryUnitConverter(query: String): UnitConverter? {
if (!query.matches(Regex("[0-9,.:]+ [A-Za-z/²³°.]+")) && !query.matches(Regex("[0-9,.:]+ [A-Za-z/²³°.]+ >> [A-Za-z/²³°]+"))) return null
val valueStr: String
val unitStr: String
val targetUnitStr: String?
query.split(" ").also {
valueStr = it.get(0)
unitStr = it.get(1)
targetUnitStr = it.getOrNull(3)
}
val value = valueStr.toDoubleOrNull() ?: valueStr.replace(',', '.').toDoubleOrNull()
?: return null
val converters = listOf(
lazy { MassConverter(context) },
lazy { LengthConverter(context) },
lazy { CurrencyConverter() },
lazy { DataConverter(context) },
lazy { TimeConverter(context) },
lazy { VelocityConverter(context) },
lazy { AreaConverter(context) },
lazy { TemperatureConverter(context) }
)
for (conv in converters) {
val converter = conv.value
if (!converter.isValidUnit(unitStr)) continue
if (targetUnitStr != null && !converter.isValidUnit(targetUnitStr)) continue
return converter.convert(context, unitStr, value, targetUnitStr)
}
return null
} }
} }

View File

@ -89,78 +89,4 @@ class Website(
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return intent return intent
} }
companion object {
fun search(context: Context, query: String, client: OkHttpClient): Website? {
var url = query
val prefs = LauncherPreferences.instance
if (!prefs.searchWebsite ||
NetworkUtils.isOffline(context, prefs.searchWebsitesMobileData)
) return null
val protocol =
if (LauncherPreferences.instance.searchWebsitesProtocol == WebsiteProtocols.HTTPS) "https://" else "http://"
if (!query.startsWith("https://") && !query.startsWith("http://")) url =
"$protocol$query"
if (!URLUtil.isValidUrl(url)) return null
try {
val request = Request.Builder()
.url(URL(url))
.get()
.tag("onlinesearch")
.build()
val response = client.newCall(request).execute()
url = response.request.url.toString()
val body = response.body?.string() ?: return null
val doc = Jsoup.parse(body)
var title = doc.select("meta[property=og:title]").attr("content")
if (title.isBlank()) title = doc.title()
if (title.isBlank()) title = url
var description = doc.select("meta[property=og:description]").attr("content")
if (description.isBlank()) description =
doc.select("meta[name=description]").attr("content")
val color = try {
val colorString = doc.select("meta[name=theme-color]").attr("content")
if (colorString.isNotEmpty()) colorString.toColorInt()
else 0
} catch (e: IllegalArgumentException) {
0
}
var image = doc.select("meta[property=og:image]").attr("content")
var favicon = doc.select("link[rel=apple-touch-icon]").attr("href")
if (favicon.isBlank()) favicon =
doc.head().select("meta[itemprop=image]").attr("content")
if (favicon.isBlank()) favicon = doc.select("link[rel=icon]").attr("href")
if (favicon.isBlank()) favicon =
doc.head().select("link[href~=.*\\.(ico|png)]").attr("href")
if (!favicon.isBlank()) favicon = resolve(response.request.url, favicon)
if (!image.isBlank()) image = resolve(response.request.url, image)
return Website(
label = title,
url = url,
description = description,
image = image,
favicon = favicon,
color = color
)
} catch (e: IOException) {
//Ignore. Not a HTML page or no connection. No result for this query
} catch (e: UncheckedIOException) {
} catch (e: URISyntaxException) {
} catch (e: RuntimeException) {
} catch (e: IllegalArgumentException) {
}
return null
}
private fun resolve(url: HttpUrl, link: String): String {
/*if(link.startsWith("http://") || link.startsWith("https://")) return link
if(link.startsWith("//")) return "${urlTemplate.scheme()}:$link"
if(link.startsWith("/")) return "${urlTemplate.scheme()}:$link"*/
return try {
URL(url.toUrl(), link).toString()
} catch (e: MalformedURLException) {
""
}
}
}
} }

View File

@ -1,19 +1,36 @@
package de.mm20.launcher2.websites package de.mm20.launcher2.websites
import android.content.Context import android.content.Context
import android.webkit.URLUtil
import androidx.core.graphics.toColorInt
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Website import de.mm20.launcher2.search.data.Website
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.UncheckedIOException
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.IOException
import java.net.MalformedURLException
import java.net.URISyntaxException
import java.net.URL
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
interface WebsiteRepository { interface WebsiteRepository {
fun search(query: String): Flow<Website?> fun search(query: String): Flow<Website?>
} }
class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository { class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val httpClient = OkHttpClient private val httpClient = OkHttpClient
.Builder() .Builder()
@ -24,18 +41,86 @@ class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository {
override fun search(query: String): Flow<Website?> = channelFlow { override fun search(query: String): Flow<Website?> = channelFlow {
send(null) send(null)
httpClient.dispatcher.run { withContext(Dispatchers.IO) {
runningCalls().forEach { httpClient.dispatcher.cancelAll()
it.cancel()
}
queuedCalls().forEach {
it.cancel()
}
} }
if (query.isBlank()) return@channelFlow if (query.isBlank()) return@channelFlow
val website = withContext(Dispatchers.IO) {
Website.search(context, query, httpClient) dataStore.data.map { it.websiteSearch.enabled }.collectLatest {
} if(it) {
val website = queryWebsite(query)
send(website) send(website)
} else {
send(null)
}
}
}
private suspend fun queryWebsite(query: String): Website? {
val result = withContext(Dispatchers.IO) {
var url = query
val protocol = "https://"
if (!query.startsWith("https://") && !query.startsWith("http://")) url =
"$protocol$query"
if (!URLUtil.isValidUrl(url)) return@withContext null
try {
val request = Request.Builder()
.url(URL(url))
.get()
.tag("onlinesearch")
.build()
val response = httpClient.newCall(request).execute()
url = response.request.url.toString()
val body = response.body?.string() ?: return@withContext null
val doc = Jsoup.parse(body)
var title = doc.select("meta[property=og:title]").attr("content")
if (title.isBlank()) title = doc.title()
if (title.isBlank()) title = url
var description = doc.select("meta[property=og:description]").attr("content")
if (description.isBlank()) description =
doc.select("meta[name=description]").attr("content")
val color = try {
val colorString = doc.select("meta[name=theme-color]").attr("content")
if (colorString.isNotEmpty()) colorString.toColorInt()
else 0
} catch (e: IllegalArgumentException) {
0
}
var image = doc.select("meta[property=og:image]").attr("content")
var favicon = doc.select("link[rel=apple-touch-icon]").attr("href")
if (favicon.isBlank()) favicon =
doc.head().select("meta[itemprop=image]").attr("content")
if (favicon.isBlank()) favicon = doc.select("link[rel=icon]").attr("href")
if (favicon.isBlank()) favicon =
doc.head().select("link[href~=.*\\.(ico|png)]").attr("href")
if (!favicon.isBlank()) favicon = resolveUrl(response.request.url, favicon)
if (!image.isBlank()) image = resolveUrl(response.request.url, image)
return@withContext Website(
label = title,
url = url,
description = description,
image = image,
favicon = favicon,
color = color
)
} catch (e: IOException) {
//Ignore. Not a HTML page or no connection. No result for this query
} catch (e: UncheckedIOException) {
} catch (e: URISyntaxException) {
} catch (e: RuntimeException) {
} catch (e: IllegalArgumentException) {
}
return@withContext null
}
return result
}
private fun resolveUrl(url: HttpUrl, link: String): String {
return try {
URL(url.toUrl(), link).toString()
} catch (e: MalformedURLException) {
""
}
} }
} }

View File

@ -2,11 +2,17 @@ package de.mm20.launcher2.wikipedia
import android.content.Context import android.content.Context
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Wikipedia import de.mm20.launcher2.search.data.Wikipedia
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -17,7 +23,9 @@ interface WikipediaRepository {
class WikipediaRepositoryImpl( class WikipediaRepositoryImpl(
private val context: Context private val context: Context
): WikipediaRepository { ) : WikipediaRepository, KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val httpClient by lazy { private val httpClient by lazy {
OkHttpClient OkHttpClient
@ -36,51 +44,56 @@ class WikipediaRepositoryImpl(
.build() .build()
} }
val wikipediaService by lazy { private val wikipediaService by lazy {
retrofit.create(WikipediaApi::class.java) retrofit.create(WikipediaApi::class.java)
} }
override fun search(query: String): Flow<Wikipedia?> = channelFlow { override fun search(query: String): Flow<Wikipedia?> = channelFlow {
send(null) send(null)
httpClient.dispatcher.run { withContext(Dispatchers.IO) {
runningCalls().forEach { httpClient.dispatcher.cancelAll()
it.cancel() }
if (query.isBlank()) return@channelFlow
dataStore.data.map { it.wikipediaSearch }.collectLatest {
if (it.enabled) {
send(queryWikipedia(query, it.images))
} else {
send(null)
} }
queuedCalls().forEach {
it.cancel()
} }
} }
if (query.isBlank()) return@channelFlow private suspend fun queryWikipedia(query: String, loadImages: Boolean): Wikipedia? {
val result = try { val result = try {
wikipediaService.search(query) wikipediaService.search(query)
} catch (e: Exception) { } catch (e: Exception) {
CrashReporter.logException(e) CrashReporter.logException(e)
return@channelFlow return null
} }
val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return@channelFlow val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return null
val image = if (LauncherPreferences.instance.searchWikipediaPictures) { val image = if (loadImages) {
val width = context.resources.displayMetrics.widthPixels / 2 val width = context.resources.displayMetrics.widthPixels / 2
val imageResult = try { val imageResult = try {
wikipediaService.getPageImage(page.pageid, width) wikipediaService.getPageImage(page.pageid, width)
} catch (e: Exception) { } catch (e: Exception) {
CrashReporter.logException(e) CrashReporter.logException(e)
return@channelFlow return null
} }
imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source
} else null } else null
val wiki = Wikipedia( return Wikipedia(
label = page.title, label = page.title,
id = page.pageid, id = page.pageid,
text = page.extract, text = page.extract,
image = image image = image
) )
send(wiki)
} }
} }