Migrate search settings
This commit is contained in:
parent
d91fc525f8
commit
5834e7e8c4
@ -1,57 +1,73 @@
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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
|
||||
|
||||
interface CalculatorRepository {
|
||||
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 {
|
||||
if (query.isBlank()) {
|
||||
send(null)
|
||||
return@channelFlow
|
||||
}
|
||||
if (!LauncherPreferences.instance.searchCalculator) return@channelFlow
|
||||
val calc = when {
|
||||
val searchCalculator = dataStore.data.map { it.calculatorSearch.enabled }
|
||||
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]+")) -> {
|
||||
val solution = query.substring(2).toIntOrNull(16) ?: run {
|
||||
send(null)
|
||||
return@channelFlow
|
||||
return null
|
||||
}
|
||||
Calculator(term = query, solution = solution.toDouble())
|
||||
}
|
||||
query.matches(Regex("0b[01]+")) -> {
|
||||
val solution = query.substring(2).toIntOrNull(2) ?: run {
|
||||
send(null)
|
||||
return@channelFlow
|
||||
return null
|
||||
}
|
||||
Calculator(term = query, solution = solution.toDouble())
|
||||
}
|
||||
query.matches(Regex("0[0-7]+")) -> {
|
||||
val solution = query.substring(1).toIntOrNull(8) ?: run {
|
||||
send(null)
|
||||
return@channelFlow
|
||||
return null
|
||||
}
|
||||
Calculator(term = query, solution = solution.toDouble())
|
||||
}
|
||||
else -> {
|
||||
val exp = Expression(query)
|
||||
if (exp.checkSyntax()) {
|
||||
Calculator(term = query, solution = exp.calculate())
|
||||
} else {
|
||||
val exp2 = Expression(query.replace(',', '.').replace(';', ','))
|
||||
if (exp2.checkSyntax()) {
|
||||
Calculator(term = query, solution = exp2.calculate())
|
||||
} else null
|
||||
withContext(Dispatchers.IO) {
|
||||
val exp = Expression(query)
|
||||
if (exp.checkSyntax()) {
|
||||
Calculator(term = query, solution = exp.calculate())
|
||||
} else {
|
||||
val exp2 = Expression(query.replace(',', '.').replace(';', ','))
|
||||
if (exp2.checkSyntax()) {
|
||||
Calculator(term = query, solution = exp2.calculate())
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
send(calc)
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,13 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
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.search.data.CalendarEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -9,6 +15,10 @@ import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.*
|
||||
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 {
|
||||
fun search(query: String): Flow<List<CalendarEvent>>
|
||||
@ -19,34 +29,131 @@ interface CalendarRepository {
|
||||
class CalendarRepositoryImpl(
|
||||
private val context: Context,
|
||||
hiddenItemsRepository: HiddenItemsRepository
|
||||
) : CalendarRepository {
|
||||
) : CalendarRepository, KoinComponent {
|
||||
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
|
||||
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
|
||||
|
||||
override fun search(query: String): Flow<List<CalendarEvent>> = channelFlow {
|
||||
if (query.isBlank()) {
|
||||
if (query.isBlank() || query.length < 3) {
|
||||
send(emptyList())
|
||||
return@channelFlow
|
||||
}
|
||||
val events = withContext(Dispatchers.IO) {
|
||||
val now = System.currentTimeMillis()
|
||||
CalendarEvent.search(
|
||||
context,
|
||||
query,
|
||||
intervalStart = now,
|
||||
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
||||
)
|
||||
}
|
||||
|
||||
hiddenItems.collectLatest { hiddenItems ->
|
||||
val calendarResults = withContext(Dispatchers.IO) {
|
||||
events.filter { !hiddenItems.contains(it.key) }
|
||||
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()
|
||||
queryCalendarEvents(
|
||||
query,
|
||||
intervalStart = now,
|
||||
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
send(calendarResults)
|
||||
}.flatMapLatest { events ->
|
||||
hiddenItems.map { hidden ->
|
||||
events.filter { !hidden.contains(it.key) }
|
||||
}
|
||||
}.collectLatest {
|
||||
send(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
val unselectedCalendars = callbackFlow {
|
||||
val unregister =
|
||||
@ -70,23 +177,26 @@ class CalendarRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
merge(unselectedCalendars, hideAlldayEvents, hiddenItems).collectLatest {
|
||||
val now = System.currentTimeMillis()
|
||||
val end = now + 14 * 24 * 60 * 60 * 1000L
|
||||
val events = withContext(Dispatchers.IO) {
|
||||
CalendarEvent.search(
|
||||
context = context,
|
||||
query = "",
|
||||
intervalStart = now,
|
||||
intervalEnd = end,
|
||||
limit = 700,
|
||||
hideAllDayEvents = LauncherPreferences.instance.calendarHideAllday,
|
||||
unselectedCalendars = LauncherPreferences.instance.unselectedCalendars
|
||||
).filter {
|
||||
!hiddenItems.value.contains(it.key)
|
||||
hideAlldayEvents.collectLatest { hideAllday ->
|
||||
unselectedCalendars.collectLatest { unselected ->
|
||||
hiddenItems.collectLatest { hidden ->
|
||||
val now = System.currentTimeMillis()
|
||||
val end = now + 14 * 24 * 60 * 60 * 1000L
|
||||
val events = withContext(Dispatchers.IO) {
|
||||
queryCalendarEvents(
|
||||
query = "",
|
||||
intervalStart = now,
|
||||
intervalEnd = end,
|
||||
limit = 700,
|
||||
excludeAllDayEvents = hideAllday,
|
||||
excludeCalendars = unselected
|
||||
).filter {
|
||||
!hiddenItems.value.contains(it.key)
|
||||
}
|
||||
}
|
||||
send(events)
|
||||
}
|
||||
}
|
||||
send(events)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -80,99 +80,6 @@ class CalendarEvent(
|
||||
}
|
||||
|
||||
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> {
|
||||
val calendars = mutableListOf<UserCalendar>()
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.ContactsContract
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
interface ContactRepository {
|
||||
fun search(query: String): Flow<List<Contact>>
|
||||
@ -17,19 +20,61 @@ interface ContactRepository {
|
||||
class ContactRepositoryImpl(
|
||||
private val context: Context,
|
||||
hiddenItemsRepository: HiddenItemsRepository
|
||||
) : ContactRepository {
|
||||
) : ContactRepository, KoinComponent {
|
||||
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
private val hiddenItems = hiddenItemsRepository.hiddenItemsKeys
|
||||
|
||||
override fun search(query: String): Flow<List<Contact>> = channelFlow {
|
||||
val contacts = withContext(Dispatchers.IO) {
|
||||
Contact.search(context, query)
|
||||
val searchContacts = dataStore.data.map { it.contactsSearch.enabled }
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
||||
|
||||
if (query.length < 3) {
|
||||
send(emptyList())
|
||||
return@channelFlow
|
||||
}
|
||||
hiddenItems.collectLatest { hiddenItems ->
|
||||
val contactResults = withContext(Dispatchers.IO) {
|
||||
contacts.filter { !hiddenItems.contains(it.key) }
|
||||
|
||||
combine(searchContacts, hasPermission) { search, permission ->
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -74,38 +74,7 @@ class Contact(
|
||||
return null
|
||||
}
|
||||
|
||||
companion object: KoinComponent {
|
||||
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 }
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
|
||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
|
||||
|
||||
@ -106,34 +106,23 @@
|
||||
<string name="file_meta_app_pkgname">Paketname</string>
|
||||
<string name="file_meta_app_min_sdk">Min-SDK-Version</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_search_show_favorites">Favoriten 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_summary">Häufig genutzte Elemente automatisch in den Favoriten anzeigen</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_search_wikipedia">Wikipedia durchsuchen</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_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_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_https">HTTPS (verschlüsselt)</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_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="file_meta_location">Ort</string>
|
||||
<string name="websearch_google">Google</string>
|
||||
@ -192,9 +181,7 @@
|
||||
<string name="menu_contact_postal">Ort</string>
|
||||
<string name="contact_multiple_postals">%1$d Postadressen</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_search_contacts">Kontakte durchsuchen</string>
|
||||
<string name="menu_app_info">App-Info</string>
|
||||
<string name="preference_screen_services_summary">Verbundene Accounts und Dienste verwalten</string>
|
||||
<string name="preference_category_services_google">Google</string>
|
||||
@ -277,8 +264,6 @@
|
||||
<string name="weather_unknown">Unbekannt</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_search_unitconverter_summary">Probieren Sie „23 kg“ oder„5 cm >> 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_none">Keiner</string>
|
||||
<string name="preference_legacy_icon_bg_color">Dynamisch</string>
|
||||
@ -418,8 +403,10 @@
|
||||
<string name="preference_grid_column_count">Spaltenanzahl</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_music_widget">Benachrichtigungszugriff wird benötigt um Medienwiedergabe zu steuern</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_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="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_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_no_data">Bisher wurden keine Medien abgespielt</string>
|
||||
</resources>
|
||||
@ -147,8 +147,6 @@
|
||||
<string name="file_meta_app_pkgname">Package name</string>
|
||||
<string name="file_meta_app_min_sdk">Min SDK version</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_system">Follow system</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_summary">Automatically add frequently used items to favorites</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_search_wikipedia">Search Wikipedia</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_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_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_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_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_search_activities_summary">Try \'%1$s\'</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="file_meta_location">Location</string>
|
||||
<string name="websearch_google">Google</string>
|
||||
@ -237,9 +224,7 @@
|
||||
<string name="menu_contact_postal">Location</string>
|
||||
<string name="contact_multiple_postals">%1$d postal addresses</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_search_contacts">Search contacts</string>
|
||||
<string name="menu_app_info">App info</string>
|
||||
<string name="preference_screen_services_summary">Manage connected accounts and services</string>
|
||||
<string name="preference_category_services_google">Google</string>
|
||||
@ -458,6 +443,8 @@
|
||||
<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_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="preference_screen_debug">Debug</string>
|
||||
@ -480,6 +467,27 @@
|
||||
<string name="preference_crash_reporter_summary">Error and crash reports</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_summary">Ignore media sessions of apps that are not music apps</string>
|
||||
|
||||
|
||||
@ -27,5 +27,46 @@ fun createFactorySettings(context: Context): Settings {
|
||||
.setClockStyle(Settings.ClockWidgetSettings.ClockStyle.DigitalClock1)
|
||||
.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()
|
||||
}
|
||||
@ -52,4 +52,55 @@ message Settings {
|
||||
}
|
||||
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;
|
||||
|
||||
}
|
||||
@ -45,4 +45,5 @@ dependencies {
|
||||
|
||||
implementation(project(":base"))
|
||||
implementation(project(":database"))
|
||||
implementation(project(":preferences"))
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.data.Websearch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
interface WebsearchRepository {
|
||||
fun search(query: String): Flow<List<Websearch>>
|
||||
@ -16,7 +19,9 @@ interface WebsearchRepository {
|
||||
|
||||
class WebsearchRepositoryImpl(
|
||||
private val database: AppDatabase
|
||||
) : WebsearchRepository {
|
||||
) : WebsearchRepository, KoinComponent {
|
||||
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
@ -25,12 +30,18 @@ class WebsearchRepositoryImpl(
|
||||
send(emptyList())
|
||||
return@channelFlow
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
database.searchDao().getWebSearches().map {
|
||||
it.map { Websearch(it, query) }
|
||||
dataStore.data.map { it.webSearch.enabled }.collectLatest {
|
||||
if (it) {
|
||||
withContext(Dispatchers.IO) {
|
||||
database.searchDao().getWebSearches().map {
|
||||
it.map { Websearch(it, query) }
|
||||
}
|
||||
}.collectLatest {
|
||||
send(it)
|
||||
}
|
||||
} else {
|
||||
send(emptyList())
|
||||
}
|
||||
}.collectLatest {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -763,4 +763,47 @@ val Icons.Rounded.Fdroid
|
||||
lineTo(6.3535156f, 10.242188f)
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -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.main.MainSettingsScreen
|
||||
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.widgets.WidgetsSettingsScreen
|
||||
|
||||
@ -85,6 +86,9 @@ class SettingsActivity : BaseActivity() {
|
||||
composable("settings/appearance") {
|
||||
AppearanceSettingsScreen()
|
||||
}
|
||||
composable("settings/search") {
|
||||
SearchSettingsScreen()
|
||||
}
|
||||
composable("settings/widgets") {
|
||||
WidgetsSettingsScreen()
|
||||
}
|
||||
|
||||
@ -33,7 +33,10 @@ fun MainSettingsScreen() {
|
||||
Preference(
|
||||
icon = Icons.Rounded.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(
|
||||
icon = Icons.Rounded.Widgets,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,51 +1,11 @@
|
||||
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.UnitValue
|
||||
import de.mm20.launcher2.unitconverter.converters.*
|
||||
|
||||
open class UnitConverter(
|
||||
val dimension: Dimension,
|
||||
val inputValue: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
val dimension: Dimension,
|
||||
val inputValue: UnitValue,
|
||||
val values: List<UnitValue>
|
||||
)
|
||||
|
||||
|
||||
@ -2,19 +2,69 @@ package de.mm20.launcher2.unitconverter
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.data.UnitConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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 {
|
||||
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?>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -89,78 +89,4 @@ class Website(
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
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) {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,36 @@
|
||||
package de.mm20.launcher2.websites
|
||||
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
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
|
||||
|
||||
interface WebsiteRepository {
|
||||
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
|
||||
.Builder()
|
||||
@ -24,18 +41,86 @@ class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository {
|
||||
|
||||
override fun search(query: String): Flow<Website?> = channelFlow {
|
||||
send(null)
|
||||
httpClient.dispatcher.run {
|
||||
runningCalls().forEach {
|
||||
it.cancel()
|
||||
}
|
||||
queuedCalls().forEach {
|
||||
it.cancel()
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
}
|
||||
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)
|
||||
} 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) {
|
||||
""
|
||||
}
|
||||
send(website)
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,17 @@ package de.mm20.launcher2.wikipedia
|
||||
|
||||
import android.content.Context
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -17,7 +23,9 @@ interface WikipediaRepository {
|
||||
|
||||
class WikipediaRepositoryImpl(
|
||||
private val context: Context
|
||||
): WikipediaRepository {
|
||||
) : WikipediaRepository, KoinComponent {
|
||||
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
private val httpClient by lazy {
|
||||
OkHttpClient
|
||||
@ -36,51 +44,56 @@ class WikipediaRepositoryImpl(
|
||||
.build()
|
||||
}
|
||||
|
||||
val wikipediaService by lazy {
|
||||
private val wikipediaService by lazy {
|
||||
retrofit.create(WikipediaApi::class.java)
|
||||
}
|
||||
|
||||
|
||||
override fun search(query: String): Flow<Wikipedia?> = channelFlow {
|
||||
send(null)
|
||||
httpClient.dispatcher.run {
|
||||
runningCalls().forEach {
|
||||
it.cancel()
|
||||
}
|
||||
queuedCalls().forEach {
|
||||
it.cancel()
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
}
|
||||
if (query.isBlank()) return@channelFlow
|
||||
|
||||
dataStore.data.map { it.wikipediaSearch }.collectLatest {
|
||||
if (it.enabled) {
|
||||
send(queryWikipedia(query, it.images))
|
||||
} else {
|
||||
send(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryWikipedia(query: String, loadImages: Boolean): Wikipedia? {
|
||||
|
||||
if (query.isBlank()) return@channelFlow
|
||||
|
||||
val result = try {
|
||||
wikipediaService.search(query)
|
||||
} catch (e: Exception) {
|
||||
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 imageResult = try {
|
||||
wikipediaService.getPageImage(page.pageid, width)
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
return@channelFlow
|
||||
return null
|
||||
}
|
||||
imageResult.query?.pages?.values?.toList()?.getOrNull(0)?.thumbnail?.source
|
||||
} else null
|
||||
|
||||
val wiki = Wikipedia(
|
||||
return Wikipedia(
|
||||
label = page.title,
|
||||
id = page.pageid,
|
||||
text = page.extract,
|
||||
image = image
|
||||
)
|
||||
send(wiki)
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user