Refactor preferences module
This commit is contained in:
@@ -47,5 +47,6 @@ dependencies {
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:preferences"))
|
||||
|
||||
}
|
||||
+44
-39
@@ -12,6 +12,7 @@ import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.ktx.normalize
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.search.ShortcutSearchSettings
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -24,9 +25,10 @@ import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.text.similarity.FuzzyScore
|
||||
@@ -50,6 +52,7 @@ interface AppShortcutRepository : SearchableRepository<AppShortcut> {
|
||||
internal class AppShortcutRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
private val settings: ShortcutSearchSettings,
|
||||
) : AppShortcutRepository {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||
@@ -108,55 +111,57 @@ internal class AppShortcutRepositoryImpl(
|
||||
return flags
|
||||
}
|
||||
|
||||
override fun search(query: String) = channelFlow<ImmutableList<AppShortcut>> {
|
||||
override fun search(query: String): Flow<ImmutableList<AppShortcut>> {
|
||||
if (query.length < 3) {
|
||||
send(persistentListOf())
|
||||
return@channelFlow
|
||||
return flowOf(persistentListOf())
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.AppShortcuts)) {
|
||||
send(persistentListOf())
|
||||
return@withContext
|
||||
}
|
||||
|
||||
shortcutChangeEmitter.collectLatest {
|
||||
val launcherApps =
|
||||
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
|
||||
persistentListOf()
|
||||
return combine(
|
||||
listOf(
|
||||
settings.enabled,
|
||||
permissionsManager.hasPermission(PermissionGroup.AppShortcuts),
|
||||
shortcutChangeEmitter
|
||||
)
|
||||
) { it }
|
||||
.map { (enabled, perm, _) ->
|
||||
enabled as Boolean
|
||||
perm as Boolean
|
||||
|
||||
if (enabled && perm) {
|
||||
val launcherApps =
|
||||
context.getSystemService<LauncherApps>() ?: return@map persistentListOf()
|
||||
|
||||
|
||||
val shortcutQuery = LauncherApps.ShortcutQuery()
|
||||
shortcutQuery.setQueryFlags(
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED or
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
|
||||
)
|
||||
val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle())
|
||||
?.filter {
|
||||
if (it.longLabel != null) {
|
||||
return@filter matches(it.longLabel.toString(), query)
|
||||
}
|
||||
if (it.shortLabel != null) {
|
||||
return@filter matches(it.shortLabel.toString(), query)
|
||||
}
|
||||
return@filter false
|
||||
} ?: emptyList()
|
||||
|
||||
val shortcutQuery = LauncherApps.ShortcutQuery()
|
||||
shortcutQuery.setQueryFlags(
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED or
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED or
|
||||
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
|
||||
)
|
||||
val shortcuts = launcherApps.getShortcuts(shortcutQuery, Process.myUserHandle())
|
||||
?.filter {
|
||||
if (it.longLabel != null) {
|
||||
return@filter matches(it.longLabel.toString(), query)
|
||||
}
|
||||
if (it.shortLabel != null) {
|
||||
return@filter matches(it.shortLabel.toString(), query)
|
||||
}
|
||||
return@filter false
|
||||
} ?: emptyList()
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
|
||||
send(
|
||||
shortcuts.mapNotNull {
|
||||
LauncherShortcut(
|
||||
context,
|
||||
it
|
||||
)
|
||||
}.toImmutableList()
|
||||
)
|
||||
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val shortcutChangeEmitter = callbackFlow {
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appShortcutsModule = module {
|
||||
factory<SearchableRepository<AppShortcut>>(named<AppShortcut>()) { AppShortcutRepositoryImpl(androidContext(), get()) }
|
||||
factory<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get()) }
|
||||
factory<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get(), get()) }
|
||||
factory<SearchableRepository<AppShortcut>>(named<AppShortcut>()) { get<AppShortcutRepository>() }
|
||||
factory<SearchableDeserializer>(named(LauncherShortcut.Domain)) { LauncherShortcutDeserializer(androidContext()) }
|
||||
factory<SearchableDeserializer>(named(LegacyShortcut.Domain)) { LegacyShortcutDeserializer(androidContext()) }
|
||||
}
|
||||
@@ -45,5 +45,6 @@ dependencies {
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":core:preferences"))
|
||||
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package de.mm20.launcher2.calculator
|
||||
|
||||
import de.mm20.launcher2.preferences.search.CalculatorSearchSettings
|
||||
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.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.mariuszgromada.math.mxparser.Expression
|
||||
@@ -12,16 +14,19 @@ interface CalculatorRepository {
|
||||
fun search(query: String): Flow<Calculator?>
|
||||
}
|
||||
|
||||
class CalculatorRepositoryImpl : CalculatorRepository, KoinComponent {
|
||||
class CalculatorRepositoryImpl(
|
||||
private val settings: CalculatorSearchSettings
|
||||
) : CalculatorRepository, KoinComponent {
|
||||
|
||||
|
||||
override fun search(query: String): Flow<Calculator?> = channelFlow {
|
||||
if (query.isBlank()) {
|
||||
send(null)
|
||||
return@channelFlow
|
||||
override fun search(query: String): Flow<Calculator?> {
|
||||
return settings.enabled.map {
|
||||
if (it && query.isNotBlank()) {
|
||||
queryCalculator(query)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
send(queryCalculator(query))
|
||||
}
|
||||
|
||||
private suspend fun queryCalculator(query: String): Calculator? {
|
||||
|
||||
@@ -3,5 +3,5 @@ package de.mm20.launcher2.calculator
|
||||
import org.koin.dsl.module
|
||||
|
||||
val calculatorModule = module {
|
||||
single<CalculatorRepository> { CalculatorRepositoryImpl() }
|
||||
single<CalculatorRepository> { CalculatorRepositoryImpl(get()) }
|
||||
}
|
||||
@@ -44,6 +44,7 @@ dependencies {
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":core:permissions"))
|
||||
implementation(project(":core:preferences"))
|
||||
implementation(project(":libs:material-color-utilities"))
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.provider.CalendarContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -15,12 +16,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Calendar
|
||||
|
||||
interface CalendarRepository: SearchableRepository<CalendarEvent> {
|
||||
interface CalendarRepository : SearchableRepository<CalendarEvent> {
|
||||
fun findMany(
|
||||
from: Long = System.currentTimeMillis(),
|
||||
to: Long = from + 14 * 24 * 60 * 60 * 1000L,
|
||||
@@ -35,6 +37,7 @@ interface CalendarRepository: SearchableRepository<CalendarEvent> {
|
||||
internal class CalendarRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
private val settings: CalendarSearchSettings,
|
||||
) : CalendarRepository {
|
||||
|
||||
override fun search(query: String): Flow<ImmutableList<CalendarEvent>> {
|
||||
@@ -45,18 +48,21 @@ internal class CalendarRepositoryImpl(
|
||||
}
|
||||
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
return hasPermission.map {
|
||||
if (it) {
|
||||
val now = System.currentTimeMillis()
|
||||
queryCalendarEvents(
|
||||
query,
|
||||
intervalStart = now,
|
||||
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
||||
).toImmutableList()
|
||||
} else {
|
||||
persistentListOf()
|
||||
val enabled = settings.enabled
|
||||
|
||||
return hasPermission.combine(enabled) { a, b -> a && b }
|
||||
.map {
|
||||
if (it) {
|
||||
val now = System.currentTimeMillis()
|
||||
queryCalendarEvents(
|
||||
query,
|
||||
intervalStart = now,
|
||||
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
||||
).toImmutableList()
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val calendarModule = module {
|
||||
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { CalendarRepositoryImpl(androidContext(), get()) }
|
||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
|
||||
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { get<CalendarRepository>() }
|
||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { CalendarEventDeserializer(androidContext()) }
|
||||
}
|
||||
@@ -44,5 +44,6 @@ dependencies {
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":core:permissions"))
|
||||
implementation(project(":core:preferences"))
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.provider.ContactsContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.search.ContactSearchSettings
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.ContactInfo
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
@@ -12,12 +13,16 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class ContactRepository(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager
|
||||
private val permissionsManager: PermissionsManager,
|
||||
private val settings: ContactSearchSettings,
|
||||
) : SearchableRepository<Contact> {
|
||||
|
||||
fun get(id: Long): Flow<Contact?> = flow {
|
||||
@@ -44,114 +49,120 @@ internal class ContactRepository(
|
||||
emit(getWithRawIds(id, rawContacts))
|
||||
}
|
||||
|
||||
private suspend fun getWithRawIds(id: Long, rawIds: Set<Long>): Contact? = withContext(Dispatchers.IO) {
|
||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
|
||||
" AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${TelegramContactInfo.ItemType}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${WhatsAppContactInfo.ItemType}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${SignalContactInfo.ItemType}\"" +
|
||||
")"
|
||||
val dataCursor = context.contentResolver.query(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
null, s, null, null
|
||||
) ?: return@withContext null
|
||||
val contactInfos = mutableSetOf<ContactInfo>()
|
||||
var firstName = ""
|
||||
var lastName = ""
|
||||
var displayName = ""
|
||||
val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE)
|
||||
val emailAddressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
|
||||
val numberColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
val addressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)
|
||||
val displayNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)
|
||||
val givenNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
||||
val familyNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
|
||||
val data1Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA1)
|
||||
val data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3)
|
||||
val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID)
|
||||
loop@ while (dataCursor.moveToNext()) {
|
||||
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
||||
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(emailAddressColumn)?.let {
|
||||
contactInfos.add(MailContactInfo(it))
|
||||
private suspend fun getWithRawIds(id: Long, rawIds: Set<Long>): Contact? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
|
||||
" AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${TelegramContactInfo.ItemType}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${WhatsAppContactInfo.ItemType}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${SignalContactInfo.ItemType}\"" +
|
||||
")"
|
||||
val dataCursor = context.contentResolver.query(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
null, s, null, null
|
||||
) ?: return@withContext null
|
||||
val contactInfos = mutableSetOf<ContactInfo>()
|
||||
var firstName = ""
|
||||
var lastName = ""
|
||||
var displayName = ""
|
||||
val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE)
|
||||
val emailAddressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
|
||||
val numberColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
val addressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)
|
||||
val displayNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)
|
||||
val givenNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
||||
val familyNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
|
||||
val data1Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA1)
|
||||
val data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3)
|
||||
val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID)
|
||||
loop@ while (dataCursor.moveToNext()) {
|
||||
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
||||
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(emailAddressColumn)?.let {
|
||||
contactInfos.add(MailContactInfo(it))
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(numberColumn)?.let {
|
||||
val phone = it.replace(Regex("[^+0-9]"), "")
|
||||
contactInfos.add(PhoneContactInfo(phone))
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(addressColumn)?.let {
|
||||
contactInfos.add(PostalContactInfo(it))
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||
firstName = dataCursor.getStringOrNull(givenNameColumn) ?: ""
|
||||
lastName = dataCursor.getStringOrNull(familyNameColumn) ?: ""
|
||||
displayName = dataCursor.getStringOrNull(displayNameColumn) ?: ""
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(numberColumn)?.let {
|
||||
val phone = it.replace(Regex("[^+0-9]"), "")
|
||||
contactInfos.add(PhoneContactInfo(phone))
|
||||
TelegramContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val data3 = dataCursor.getStringOrNull(data3Column)
|
||||
?: continue@loop
|
||||
contactInfos.add(
|
||||
TelegramContactInfo(data3.substringAfterLast(" "), data1)
|
||||
)
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(addressColumn)?.let {
|
||||
contactInfos.add(PostalContactInfo(it))
|
||||
WhatsAppContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
contactInfos.add(
|
||||
WhatsAppContactInfo(
|
||||
"+${data1.substringBefore('@')}",
|
||||
dataId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||
firstName = dataCursor.getStringOrNull(givenNameColumn) ?: ""
|
||||
lastName = dataCursor.getStringOrNull(familyNameColumn) ?: ""
|
||||
displayName = dataCursor.getStringOrNull(displayNameColumn) ?: ""
|
||||
}
|
||||
|
||||
TelegramContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val data3 = dataCursor.getStringOrNull(data3Column)
|
||||
?: continue@loop
|
||||
contactInfos.add(
|
||||
TelegramContactInfo(data3.substringAfterLast(" "), data1)
|
||||
)
|
||||
}
|
||||
|
||||
WhatsAppContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
contactInfos.add(WhatsAppContactInfo("+${data1.substringBefore('@')}", dataId))
|
||||
}
|
||||
|
||||
SignalContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
contactInfos.add(SignalContactInfo(data1, dataId))
|
||||
SignalContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
contactInfos.add(SignalContactInfo(data1, dataId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataCursor.close()
|
||||
dataCursor.close()
|
||||
|
||||
val lookupKeyCursor = context.contentResolver.query(
|
||||
ContactsContract.Contacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.Contacts.LOOKUP_KEY),
|
||||
"${ContactsContract.Contacts._ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
) ?: return@withContext null
|
||||
var lookUpKey = ""
|
||||
if (lookupKeyCursor.moveToNext()) {
|
||||
lookUpKey = lookupKeyCursor.getString(0)
|
||||
}
|
||||
lookupKeyCursor.close()
|
||||
val lookupKeyCursor = context.contentResolver.query(
|
||||
ContactsContract.Contacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.Contacts.LOOKUP_KEY),
|
||||
"${ContactsContract.Contacts._ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
) ?: return@withContext null
|
||||
var lookUpKey = ""
|
||||
if (lookupKeyCursor.moveToNext()) {
|
||||
lookUpKey = lookupKeyCursor.getString(0)
|
||||
}
|
||||
lookupKeyCursor.close()
|
||||
|
||||
return@withContext AndroidContact(
|
||||
id = id,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
displayName = displayName,
|
||||
contactInfos = contactInfos,
|
||||
lookupKey = lookUpKey
|
||||
)
|
||||
}
|
||||
return@withContext AndroidContact(
|
||||
id = id,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
displayName = displayName,
|
||||
contactInfos = contactInfos,
|
||||
lookupKey = lookUpKey
|
||||
)
|
||||
}
|
||||
|
||||
override fun search(query: String): Flow<ImmutableList<Contact>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
||||
@@ -162,7 +173,7 @@ internal class ContactRepository(
|
||||
}
|
||||
}
|
||||
|
||||
return hasPermission.map {
|
||||
return hasPermission.combine(settings.enabled) { perm, en -> perm && en }.map {
|
||||
if (it) {
|
||||
queryContacts(query)
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val contactsModule = module {
|
||||
factory { ContactRepository(androidContext(), get()) }
|
||||
factory<SearchableRepository<Contact>>(named<Contact>()) { ContactRepository(androidContext(), get()) }
|
||||
factory { ContactRepository(androidContext(), get(), get()) }
|
||||
factory<SearchableRepository<Contact>>(named<Contact>()) { get<ContactRepository>() }
|
||||
factory<SearchableDeserializer>(named(AndroidContact.Domain)) { ContactDeserializer(get(), get()) }
|
||||
}
|
||||
@@ -152,7 +152,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
Migration_21_22(),
|
||||
Migration_22_23(),
|
||||
Migration_23_24(),
|
||||
Migration_24_25(context),
|
||||
Migration_24_25(),
|
||||
Migration_25_26(),
|
||||
).build()
|
||||
if (_instance == null) _instance = instance
|
||||
|
||||
+2
-207
@@ -1,217 +1,12 @@
|
||||
package de.mm20.launcher2.database.migrations
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import de.mm20.launcher2.database.R
|
||||
import de.mm20.launcher2.ktx.toBytes
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.util.UUID
|
||||
|
||||
class Migration_24_25(
|
||||
private val context: Context,
|
||||
) : Migration(24, 25), KoinComponent {
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
class Migration_24_25 : Migration(24, 25), KoinComponent {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `Theme` (
|
||||
`id` BLOB NOT NULL,
|
||||
`name` TEXT NOT NULL,
|
||||
|
||||
`corePaletteA1` INTEGER,
|
||||
`corePaletteA2` INTEGER,
|
||||
`corePaletteA3` INTEGER,
|
||||
`corePaletteN1` INTEGER,
|
||||
`corePaletteN2` INTEGER,
|
||||
`corePaletteE` INTEGER,
|
||||
`lightPrimary` TEXT,
|
||||
`lightOnPrimary` TEXT,
|
||||
`lightPrimaryContainer` TEXT,
|
||||
`lightOnPrimaryContainer` TEXT,
|
||||
`lightSecondary` TEXT,
|
||||
`lightOnSecondary` TEXT,
|
||||
`lightSecondaryContainer` TEXT,
|
||||
`lightOnSecondaryContainer` TEXT,
|
||||
`lightTertiary` TEXT,
|
||||
`lightOnTertiary` TEXT,
|
||||
`lightTertiaryContainer` TEXT,
|
||||
`lightOnTertiaryContainer` TEXT,
|
||||
`lightError` TEXT,
|
||||
`lightOnError` TEXT,
|
||||
`lightErrorContainer` TEXT,
|
||||
`lightOnErrorContainer` TEXT,
|
||||
`lightSurface` TEXT,
|
||||
`lightOnSurface` TEXT,
|
||||
`lightOnSurfaceVariant` TEXT,
|
||||
`lightOutline` TEXT,
|
||||
`lightOutlineVariant` TEXT,
|
||||
`lightInverseSurface` TEXT,
|
||||
`lightInverseOnSurface` TEXT,
|
||||
`lightInversePrimary` TEXT,
|
||||
`lightSurfaceDim` TEXT,
|
||||
`lightSurfaceBright` TEXT,
|
||||
`lightSurfaceContainerLowest` TEXT,
|
||||
`lightSurfaceContainerLow` TEXT,
|
||||
`lightSurfaceContainer` TEXT,
|
||||
`lightSurfaceContainerHigh` TEXT,
|
||||
`lightSurfaceContainerHighest` TEXT,
|
||||
`lightBackground` TEXT,
|
||||
`lightOnBackground` TEXT,
|
||||
`lightSurfaceTint` TEXT,
|
||||
`lightScrim` TEXT,
|
||||
`lightSurfaceVariant` TEXT,
|
||||
|
||||
`darkPrimary` TEXT,
|
||||
`darkOnPrimary` TEXT,
|
||||
`darkPrimaryContainer` TEXT,
|
||||
`darkOnPrimaryContainer` TEXT,
|
||||
`darkSecondary` TEXT,
|
||||
`darkOnSecondary` TEXT,
|
||||
`darkSecondaryContainer` TEXT,
|
||||
`darkOnSecondaryContainer` TEXT,
|
||||
`darkTertiary` TEXT,
|
||||
`darkOnTertiary` TEXT,
|
||||
`darkTertiaryContainer` TEXT,
|
||||
`darkOnTertiaryContainer` TEXT,
|
||||
`darkError` TEXT,
|
||||
`darkOnError` TEXT,
|
||||
`darkErrorContainer` TEXT,
|
||||
`darkOnErrorContainer` TEXT,
|
||||
`darkSurface` TEXT,
|
||||
`darkOnSurface` TEXT,
|
||||
`darkOnSurfaceVariant` TEXT,
|
||||
`darkOutline` TEXT,
|
||||
`darkOutlineVariant` TEXT,
|
||||
`darkInverseSurface` TEXT,
|
||||
`darkInverseOnSurface` TEXT,
|
||||
`darkInversePrimary` TEXT,
|
||||
`darkSurfaceDim` TEXT,
|
||||
`darkSurfaceBright` TEXT,
|
||||
`darkSurfaceContainerLowest` TEXT,
|
||||
`darkSurfaceContainerLow` TEXT,
|
||||
`darkSurfaceContainer` TEXT,
|
||||
`darkSurfaceContainerHigh` TEXT,
|
||||
`darkSurfaceContainerHighest` TEXT,
|
||||
`darkBackground` TEXT,
|
||||
`darkOnBackground` TEXT,
|
||||
`darkSurfaceTint` TEXT,
|
||||
`darkScrim` TEXT,
|
||||
`darkSurfaceVariant` TEXT,
|
||||
PRIMARY KEY(`id`)
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
// Special UUID for migrated custom color scheme. Same UUID is used in data store migration 16..17
|
||||
val uuid = UUID(1L, 1L)
|
||||
val customColors = runBlocking {
|
||||
dataStore.data.map { it.appearance.customColors }.first()
|
||||
}
|
||||
|
||||
database.execSQL("""INSERT INTO `Theme` VALUES (
|
||||
?,?,?,?,?,?,?,?,?,?,
|
||||
?,?,?,?,?,?,?,?,?,?,
|
||||
?,?,?,?,?,?,?,?,?,?,
|
||||
?,?,?,?,?,?,?,?,?,?,
|
||||
?,?,?,?,?,?,?,?,?,?,
|
||||
?,?,?,?,?,?,?,?,?,?,
|
||||
?,?,?,?,?,?,?,?,?,?,
|
||||
?,?,?,?,?,?,?,?,?,?
|
||||
)
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
uuid.toBytes(),
|
||||
context.getString(R.string.preference_colors_custom),
|
||||
customColors.baseColors.accent1.toHexColor(),
|
||||
customColors.baseColors.accent2.toHexColor(),
|
||||
customColors.baseColors.accent3.toHexColor(),
|
||||
customColors.baseColors.neutral1.toHexColor(),
|
||||
customColors.baseColors.neutral2.toHexColor(),
|
||||
customColors.baseColors.error.toHexColor(),
|
||||
customColors.lightScheme.primary.toHexColor(),
|
||||
customColors.lightScheme.onPrimary.toHexColor(),
|
||||
customColors.lightScheme.primaryContainer.toHexColor(),
|
||||
customColors.lightScheme.onPrimaryContainer.toHexColor(),
|
||||
customColors.lightScheme.secondary.toHexColor(),
|
||||
customColors.lightScheme.onSecondary.toHexColor(),
|
||||
customColors.lightScheme.secondaryContainer.toHexColor(),
|
||||
customColors.lightScheme.onSecondaryContainer.toHexColor(),
|
||||
customColors.lightScheme.tertiary.toHexColor(),
|
||||
customColors.lightScheme.onTertiary.toHexColor(),
|
||||
customColors.lightScheme.tertiaryContainer.toHexColor(),
|
||||
customColors.lightScheme.onTertiaryContainer.toHexColor(),
|
||||
customColors.lightScheme.error.toHexColor(),
|
||||
customColors.lightScheme.onError.toHexColor(),
|
||||
customColors.lightScheme.errorContainer.toHexColor(),
|
||||
customColors.lightScheme.onErrorContainer.toHexColor(),
|
||||
customColors.lightScheme.surface.toHexColor(),
|
||||
customColors.lightScheme.onSurface.toHexColor(),
|
||||
customColors.lightScheme.onSurfaceVariant.toHexColor(),
|
||||
customColors.lightScheme.outline.toHexColor(),
|
||||
customColors.lightScheme.outlineVariant.toHexColor(),
|
||||
customColors.lightScheme.inverseSurface.toHexColor(),
|
||||
customColors.lightScheme.inverseOnSurface.toHexColor(),
|
||||
customColors.lightScheme.inversePrimary.toHexColor(),
|
||||
customColors.lightScheme.surfaceDim.toHexColor(),
|
||||
customColors.lightScheme.surfaceBright.toHexColor(),
|
||||
customColors.lightScheme.surfaceContainerLowest.toHexColor(),
|
||||
customColors.lightScheme.surfaceContainerLow.toHexColor(),
|
||||
customColors.lightScheme.surfaceContainer.toHexColor(),
|
||||
customColors.lightScheme.surfaceContainerHigh.toHexColor(),
|
||||
customColors.lightScheme.surfaceContainerHighest.toHexColor(),
|
||||
customColors.lightScheme.background.toHexColor(),
|
||||
customColors.lightScheme.onBackground.toHexColor(),
|
||||
customColors.lightScheme.surfaceTint.toHexColor(),
|
||||
customColors.lightScheme.scrim.toHexColor(),
|
||||
customColors.lightScheme.surfaceVariant.toHexColor(),
|
||||
|
||||
customColors.darkScheme.primary.toHexColor(),
|
||||
customColors.darkScheme.onPrimary.toHexColor(),
|
||||
customColors.darkScheme.primaryContainer.toHexColor(),
|
||||
customColors.darkScheme.onPrimaryContainer.toHexColor(),
|
||||
customColors.darkScheme.secondary.toHexColor(),
|
||||
customColors.darkScheme.onSecondary.toHexColor(),
|
||||
customColors.darkScheme.secondaryContainer.toHexColor(),
|
||||
customColors.darkScheme.onSecondaryContainer.toHexColor(),
|
||||
customColors.darkScheme.tertiary.toHexColor(),
|
||||
customColors.darkScheme.onTertiary.toHexColor(),
|
||||
customColors.darkScheme.tertiaryContainer.toHexColor(),
|
||||
customColors.darkScheme.onTertiaryContainer.toHexColor(),
|
||||
customColors.darkScheme.error.toHexColor(),
|
||||
customColors.darkScheme.onError.toHexColor(),
|
||||
customColors.darkScheme.errorContainer.toHexColor(),
|
||||
customColors.darkScheme.onErrorContainer.toHexColor(),
|
||||
customColors.darkScheme.surface.toHexColor(),
|
||||
customColors.darkScheme.onSurface.toHexColor(),
|
||||
customColors.darkScheme.onSurfaceVariant.toHexColor(),
|
||||
customColors.darkScheme.outline.toHexColor(),
|
||||
customColors.darkScheme.outlineVariant.toHexColor(),
|
||||
customColors.darkScheme.inverseSurface.toHexColor(),
|
||||
customColors.darkScheme.inverseOnSurface.toHexColor(),
|
||||
customColors.darkScheme.inversePrimary.toHexColor(),
|
||||
customColors.darkScheme.surfaceDim.toHexColor(),
|
||||
customColors.darkScheme.surfaceBright.toHexColor(),
|
||||
customColors.darkScheme.surfaceContainerLowest.toHexColor(),
|
||||
customColors.darkScheme.surfaceContainerLow.toHexColor(),
|
||||
customColors.darkScheme.surfaceContainer.toHexColor(),
|
||||
customColors.darkScheme.surfaceContainerHigh.toHexColor(),
|
||||
customColors.darkScheme.surfaceContainerHighest.toHexColor(),
|
||||
customColors.darkScheme.background.toHexColor(),
|
||||
customColors.darkScheme.onBackground.toHexColor(),
|
||||
customColors.darkScheme.surfaceTint.toHexColor(),
|
||||
customColors.darkScheme.scrim.toHexColor(),
|
||||
customColors.darkScheme.surfaceVariant.toHexColor(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun Int.toHexColor(): String {
|
||||
return "#${toUInt().toString(16).padStart(6, '0')}"
|
||||
// removed
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,26 @@
|
||||
package de.mm20.launcher2.files
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.files.providers.FileProvider
|
||||
import de.mm20.launcher2.files.providers.GDriveFileProvider
|
||||
import de.mm20.launcher2.files.providers.LocalFileProvider
|
||||
import de.mm20.launcher2.files.providers.NextcloudFileProvider
|
||||
import de.mm20.launcher2.files.providers.OwncloudFileProvider
|
||||
import de.mm20.launcher2.files.providers.PluginFileProvider
|
||||
import de.mm20.launcher2.files.settings.FileSearchSettings
|
||||
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.search.FileSearchSettings
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class FileRepository(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
private val settings: FileSearchSettings,
|
||||
private val pluginRepository: PluginRepository,
|
||||
) : SearchableRepository<File> {
|
||||
|
||||
private val nextcloudClient by lazy {
|
||||
@@ -48,26 +38,15 @@ internal class FileRepository(
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
val filePlugins = pluginRepository.findMany(
|
||||
type = PluginType.FileSearch,
|
||||
enabled = true,
|
||||
)
|
||||
|
||||
settings.data.collectLatest { settings ->
|
||||
val providers = mutableListOf<FileProvider>()
|
||||
|
||||
if (settings.localFiles) providers.add(
|
||||
LocalFileProvider(
|
||||
context,
|
||||
permissionsManager
|
||||
)
|
||||
)
|
||||
if (settings.gdriveFiles) providers.add(GDriveFileProvider(context))
|
||||
if (settings.nextcloudFiles) providers.add(NextcloudFileProvider(nextcloudClient))
|
||||
if (settings.owncloudFiles) providers.add(OwncloudFileProvider(owncloudClient))
|
||||
|
||||
for (plugin in settings.plugins) {
|
||||
providers.add(PluginFileProvider(context, plugin))
|
||||
settings.enabledProviders.collectLatest { providerIds ->
|
||||
val providers = providerIds.map {
|
||||
when (it) {
|
||||
"local" -> LocalFileProvider(context, permissionsManager)
|
||||
"gdrive" -> GDriveFileProvider(context)
|
||||
"nextcloud" -> NextcloudFileProvider(nextcloudClient)
|
||||
"owncloud" -> OwncloudFileProvider(owncloudClient)
|
||||
else -> PluginFileProvider(context, it)
|
||||
}
|
||||
}
|
||||
|
||||
if (providers.isEmpty()) {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package de.mm20.launcher2.files
|
||||
|
||||
import de.mm20.launcher2.backup.Backupable
|
||||
import de.mm20.launcher2.files.providers.GDriveFile
|
||||
import de.mm20.launcher2.files.providers.LocalFile
|
||||
import de.mm20.launcher2.files.providers.NextcloudFile
|
||||
import de.mm20.launcher2.files.providers.OneDriveFile
|
||||
import de.mm20.launcher2.files.providers.OwncloudFile
|
||||
import de.mm20.launcher2.files.providers.PluginFile
|
||||
import de.mm20.launcher2.files.settings.FileSearchSettings
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
@@ -20,7 +18,6 @@ val filesModule = module {
|
||||
FileRepository(
|
||||
androidContext(),
|
||||
get(),
|
||||
get(),
|
||||
get()
|
||||
)
|
||||
}
|
||||
@@ -35,6 +32,4 @@ val filesModule = module {
|
||||
get()
|
||||
)
|
||||
}
|
||||
single<FileSearchSettings> { FileSearchSettings(androidContext(), get()) }
|
||||
factory<Backupable>(named<FileSearchSettings>()) { get<FileSearchSettings>() }
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package de.mm20.launcher2.files.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.dataStore
|
||||
import de.mm20.launcher2.backup.Backupable
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.files.settings.migrations.Migration1
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.settings.BaseSettings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
class FileSearchSettings(
|
||||
private val context: Context,
|
||||
dataStore: LauncherDataStore,
|
||||
) : BaseSettings<FileSearchSettingsData>(
|
||||
context = context,
|
||||
fileName = "file_search.json",
|
||||
serializer = FileSearchSettingsDataSerializer,
|
||||
migrations = listOf(
|
||||
Migration1(dataStore),
|
||||
)
|
||||
) {
|
||||
|
||||
internal val data
|
||||
get() = context.dataStore.data
|
||||
|
||||
val localFiles
|
||||
get(): Flow<Boolean> {
|
||||
return context.dataStore.data.map { it.localFiles }
|
||||
}
|
||||
|
||||
fun setLocalFiles(localFiles: Boolean) {
|
||||
updateData {
|
||||
it.copy(localFiles = localFiles)
|
||||
}
|
||||
}
|
||||
|
||||
val gdriveFiles
|
||||
get(): Flow<Boolean> {
|
||||
return context.dataStore.data.map { it.gdriveFiles }
|
||||
}
|
||||
|
||||
fun setGdriveFiles(gdriveFiles: Boolean) {
|
||||
updateData {
|
||||
it.copy(gdriveFiles = gdriveFiles)
|
||||
}
|
||||
}
|
||||
|
||||
val nextcloudFiles
|
||||
get(): Flow<Boolean> {
|
||||
return context.dataStore.data.map { it.nextcloudFiles }
|
||||
}
|
||||
|
||||
fun setNextcloudFiles(nextcloudFiles: Boolean) {
|
||||
updateData {
|
||||
it.copy(nextcloudFiles = nextcloudFiles)
|
||||
}
|
||||
}
|
||||
|
||||
val owncloudFiles
|
||||
get(): Flow<Boolean> {
|
||||
return context.dataStore.data.map { it.owncloudFiles }
|
||||
}
|
||||
|
||||
fun setOwncloudFiles(owncloudFiles: Boolean) {
|
||||
updateData {
|
||||
it.copy(owncloudFiles = owncloudFiles)
|
||||
}
|
||||
}
|
||||
|
||||
val enabledPlugins: Flow<Set<String>>
|
||||
get(): Flow<Set<String>> {
|
||||
return context.dataStore.data.map { it.plugins }
|
||||
}
|
||||
|
||||
fun setEnabledPlugins(enabledPlugins: Set<String>) {
|
||||
updateData {
|
||||
it.copy(plugins = enabledPlugins)
|
||||
}
|
||||
}
|
||||
|
||||
fun setPluginEnabled(authority: String, enabled: Boolean) {
|
||||
updateData {
|
||||
if (enabled) {
|
||||
it.copy(plugins = it.plugins + authority)
|
||||
} else {
|
||||
it.copy(plugins = it.plugins - authority)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package de.mm20.launcher2.files.settings
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
@Serializable
|
||||
data class FileSearchSettingsData(
|
||||
val localFiles: Boolean = true,
|
||||
val gdriveFiles: Boolean = false,
|
||||
val nextcloudFiles: Boolean = false,
|
||||
val owncloudFiles: Boolean = false,
|
||||
val plugins: Set<String> = emptySet(),
|
||||
val schemaVersion: Int = 1,
|
||||
)
|
||||
|
||||
internal object FileSearchSettingsDataSerializer : Serializer<FileSearchSettingsData> {
|
||||
|
||||
internal val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
override val defaultValue: FileSearchSettingsData
|
||||
get() = FileSearchSettingsData(schemaVersion = 0)
|
||||
|
||||
override suspend fun readFrom(input: InputStream): FileSearchSettingsData {
|
||||
try {
|
||||
return json.decodeFromStream(input)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw (CorruptionException("Cannot read json.", e))
|
||||
} catch (e: SerializationException) {
|
||||
throw (CorruptionException("Cannot read json.", e))
|
||||
} catch (e: IOException) {
|
||||
throw (CorruptionException("Cannot read json.", e))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: FileSearchSettingsData, output: OutputStream) {
|
||||
json.encodeToStream(t, output)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package de.mm20.launcher2.files.settings.migrations
|
||||
|
||||
import androidx.datastore.core.DataMigration
|
||||
import de.mm20.launcher2.files.settings.FileSearchSettingsData
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* This migration is used to migrate the data from the old proto data store.
|
||||
* TODO: remove after a few releases
|
||||
*/
|
||||
internal class Migration1(
|
||||
private val dataStore: LauncherDataStore,
|
||||
): DataMigration<FileSearchSettingsData> {
|
||||
override suspend fun cleanUp() {
|
||||
|
||||
}
|
||||
|
||||
override suspend fun shouldMigrate(currentData: FileSearchSettingsData): Boolean {
|
||||
return currentData.schemaVersion < 1
|
||||
}
|
||||
|
||||
override suspend fun migrate(currentData: FileSearchSettingsData): FileSearchSettingsData {
|
||||
val data = dataStore.data.first().fileSearch
|
||||
return currentData.copy(
|
||||
localFiles = data.localFiles,
|
||||
gdriveFiles = data.gdrive,
|
||||
nextcloudFiles = data.nextcloud,
|
||||
owncloudFiles = data.owncloud,
|
||||
schemaVersion = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ package de.mm20.launcher2.searchable
|
||||
import de.mm20.launcher2.backup.Backupable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.data.Tag
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val searchableModule = module {
|
||||
factory <Backupable>(named<SavableSearchableRepository>()) { SavableSearchableRepositoryImpl(androidContext(), get(), get()) }
|
||||
factory <SavableSearchableRepository> { SavableSearchableRepositoryImpl(androidContext(), get(), get()) }
|
||||
factory <Backupable>(named<SavableSearchableRepository>()) { SavableSearchableRepositoryImpl(
|
||||
get(),
|
||||
get()
|
||||
) }
|
||||
factory <SavableSearchableRepository> { SavableSearchableRepositoryImpl(get(), get()) }
|
||||
factory<SearchableDeserializer>(named(Tag.Domain)) { TagDeserializer() }
|
||||
}
|
||||
+4
-6
@@ -1,6 +1,5 @@
|
||||
package de.mm20.launcher2.searchable
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.room.withTransaction
|
||||
import de.mm20.launcher2.backup.Backupable
|
||||
@@ -9,8 +8,8 @@ import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.database.entities.SavedSearchableEntity
|
||||
import de.mm20.launcher2.database.entities.SavedSearchableUpdatePinEntity
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
|
||||
import de.mm20.launcher2.preferences.WeightFactor
|
||||
import de.mm20.launcher2.preferences.search.RankingSettings
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -117,9 +116,8 @@ interface SavableSearchableRepository: Backupable {
|
||||
}
|
||||
|
||||
internal class SavableSearchableRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val database: AppDatabase,
|
||||
private val dataStore: LauncherDataStore
|
||||
private val settings: RankingSettings,
|
||||
) : SavableSearchableRepository, KoinComponent {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
@@ -193,7 +191,7 @@ internal class SavableSearchableRepositoryImpl(
|
||||
override fun touch(searchable: SavableSearchable) {
|
||||
scope.launch {
|
||||
val weightFactor =
|
||||
when (dataStore.data.map { it.resultOrdering.weightFactor }.firstOrNull()) {
|
||||
when (settings.weightFactor.firstOrNull()) {
|
||||
WeightFactor.Low -> WEIGHT_FACTOR_LOW
|
||||
WeightFactor.High -> WEIGHT_FACTOR_HIGH
|
||||
else -> WEIGHT_FACTOR_MEDIUM
|
||||
|
||||
@@ -46,6 +46,7 @@ dependencies {
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":data:database"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:preferences"))
|
||||
implementation(project(":libs:material-color-utilities"))
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import de.mm20.launcher2.backup.Backupable
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.preferences.ThemeDescriptor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -49,9 +50,15 @@ class ThemeRepository(
|
||||
}
|
||||
}
|
||||
|
||||
fun getThemeOrDefault(id: UUID?): Flow<Theme> {
|
||||
if (id == null) return flowOf(getDefaultTheme())
|
||||
return getTheme(id).map { it ?: getDefaultTheme() }
|
||||
fun getThemeOrDefault(theme: ThemeDescriptor?): Flow<Theme> {
|
||||
return when(theme) {
|
||||
is ThemeDescriptor.BlackAndWhite -> flowOf(getBlackAndWhiteTheme())
|
||||
is ThemeDescriptor.Custom -> {
|
||||
val id = UUID.fromString(theme.id)
|
||||
getTheme(id).map { it ?: getDefaultTheme() }
|
||||
}
|
||||
else -> flowOf(getDefaultTheme())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBuiltInThemes(): List<Theme> {
|
||||
|
||||
@@ -6,5 +6,5 @@ import org.koin.dsl.module
|
||||
|
||||
val unitConverterModule = module {
|
||||
single { CurrencyRepository(androidContext()) }
|
||||
single<UnitConverterRepository> { UnitConverterRepositoryImpl(androidContext(), get()) }
|
||||
single<UnitConverterRepository> { UnitConverterRepositoryImpl(androidContext(), get(), get()) }
|
||||
}
|
||||
+26
-15
@@ -2,44 +2,55 @@ package de.mm20.launcher2.unitconverter
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.currencies.CurrencyRepository
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.search.UnitConverterSettings
|
||||
import de.mm20.launcher2.search.data.UnitConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.*
|
||||
import de.mm20.launcher2.unitconverter.converters.AreaConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.CurrencyConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.DataConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.LengthConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.MassConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.TemperatureConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.TimeConverter
|
||||
import de.mm20.launcher2.unitconverter.converters.VelocityConverter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
interface UnitConverterRepository {
|
||||
fun search(query: String, includeCurrencies: Boolean): Flow<UnitConverter?>
|
||||
fun search(query: String): Flow<UnitConverter?>
|
||||
}
|
||||
|
||||
internal class UnitConverterRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val currencyRepository: CurrencyRepository,
|
||||
private val settings: UnitConverterSettings,
|
||||
) : UnitConverterRepository, KoinComponent {
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
dataStore.data.map { it.unitConverterSearch }.distinctUntilChanged().collectLatest {
|
||||
if (it.enabled && it.currencies) currencyRepository.enableCurrencyUpdateWorker()
|
||||
else currencyRepository.disableCurrencyUpdateWorker()
|
||||
}
|
||||
settings.map { it.enabled && it.currencies }
|
||||
.distinctUntilChanged().collectLatest {
|
||||
if (it) currencyRepository.enableCurrencyUpdateWorker()
|
||||
else currencyRepository.disableCurrencyUpdateWorker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String, includeCurrencies: Boolean): Flow<UnitConverter?> = channelFlow {
|
||||
if (query.isBlank()) {
|
||||
send(null)
|
||||
return@channelFlow
|
||||
override fun search(query: String): Flow<UnitConverter?> {
|
||||
if (query.isBlank()) return flowOf(null)
|
||||
return settings.distinctUntilChanged().map {
|
||||
if (!it.enabled) null
|
||||
else queryUnitConverter(query, it.currencies)
|
||||
}
|
||||
send(queryUnitConverter(query, includeCurrencies))
|
||||
}
|
||||
|
||||
private suspend fun queryUnitConverter(
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.location.Geocoder
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.formatToString
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
import de.mm20.launcher2.backup.Backupable
|
||||
import de.mm20.launcher2.weather.brightsky.BrightSkyProvider
|
||||
import de.mm20.launcher2.weather.here.HereProvider
|
||||
import de.mm20.launcher2.weather.metno.MetNoProvider
|
||||
import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider
|
||||
import de.mm20.launcher2.weather.plugin.PluginWeatherProvider
|
||||
import de.mm20.launcher2.weather.settings.WeatherSettings
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val weatherModule = module {
|
||||
single<WeatherRepository> { WeatherRepositoryImpl(androidContext(), get(), get(), get()) }
|
||||
single<WeatherSettings> { WeatherSettings(androidContext()) }
|
||||
factory<Backupable>(named<WeatherSettings>()) { get<WeatherSettings>() }
|
||||
factory<WeatherProvider> { (providerId: String) ->
|
||||
when (providerId) {
|
||||
OpenWeatherMapProvider.Id -> OpenWeatherMapProvider(androidContext())
|
||||
|
||||
@@ -1,16 +1,2 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
sealed interface WeatherLocation {
|
||||
val name: String
|
||||
|
||||
data class LatLon(
|
||||
override val name: String,
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
) : WeatherLocation
|
||||
|
||||
data class Id(
|
||||
override val name: String,
|
||||
val locationId: String,
|
||||
) : WeatherLocation
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@@ -13,13 +13,13 @@ import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.preferences.LatLon
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import de.mm20.launcher2.preferences.weather.WeatherSettings
|
||||
import de.mm20.launcher2.weather.brightsky.BrightSkyProvider
|
||||
import de.mm20.launcher2.weather.here.HereProvider
|
||||
import de.mm20.launcher2.weather.metno.MetNoProvider
|
||||
import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider
|
||||
import de.mm20.launcher2.weather.settings.LatLon
|
||||
import de.mm20.launcher2.weather.settings.ProviderSettings
|
||||
import de.mm20.launcher2.weather.settings.WeatherSettings
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
@@ -65,7 +65,7 @@ internal class WeatherRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun searchLocations(query: String): Flow<List<WeatherLocation>> {
|
||||
return settings.data.map {
|
||||
return settings.map {
|
||||
val provider = WeatherProvider.getInstance(it.provider)
|
||||
provider.findLocation(query)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ internal class WeatherRepositoryImpl(
|
||||
}
|
||||
}
|
||||
scope.launch {
|
||||
settings.data.collectLatest {
|
||||
settings.collectLatest {
|
||||
requestUpdate()
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,7 @@ class WeatherUpdateWorker(val context: Context, params: WorkerParameters) :
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Log.d("WeatherUpdateWorker", "Requesting weather data")
|
||||
val settingsData = settings.data.first()
|
||||
val settingsData = settings.first()
|
||||
val provider = WeatherProvider.getInstance(settingsData.provider)
|
||||
|
||||
val updateInterval = provider.getUpdateInterval()
|
||||
|
||||
@@ -5,10 +5,10 @@ import android.icu.text.SimpleDateFormat
|
||||
import android.icu.util.Calendar
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.GeocoderWeatherProvider
|
||||
import de.mm20.launcher2.weather.R
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.create
|
||||
|
||||
@@ -3,9 +3,9 @@ package de.mm20.launcher2.weather.here
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.R
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
|
||||
@@ -7,13 +7,11 @@ import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import de.mm20.launcher2.preferences.weather.WeatherSettings
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.GeocoderWeatherProvider
|
||||
import de.mm20.launcher2.weather.R
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import de.mm20.launcher2.weather.settings.ProviderSettings
|
||||
import de.mm20.launcher2.weather.settings.WeatherSettings
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
+1
-1
@@ -3,9 +3,9 @@ package de.mm20.launcher2.weather.openweathermap
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.R
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
|
||||
+1
-1
@@ -13,8 +13,8 @@ import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.plugin.config.WeatherPluginConfig
|
||||
import de.mm20.launcher2.plugin.contracts.PluginContract
|
||||
import de.mm20.launcher2.plugin.contracts.WeatherPluginContract
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package de.mm20.launcher2.weather.settings
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.settings.BaseSettings
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.WeatherProviderInfo
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class WeatherSettings(
|
||||
private val context: Context,
|
||||
) : BaseSettings<WeatherSettingsData>(
|
||||
context,
|
||||
"weather_settings.json",
|
||||
WeatherSettingsSerializer,
|
||||
emptyList(),
|
||||
) {
|
||||
internal val data
|
||||
get() = context.dataStore.data
|
||||
|
||||
val location = data.map {
|
||||
val providerSettings = it.providerSettings[it.provider]
|
||||
val id = providerSettings?.locationId
|
||||
val name = providerSettings?.locationName
|
||||
|
||||
if (id != null && name != null) {
|
||||
WeatherLocation.Id(name, id)
|
||||
} else if (it.location != null && it.locationName != null) {
|
||||
WeatherLocation.LatLon(it.locationName, it.location.lat, it.location.lon)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val autoLocation = data.map { it.autoLocation }
|
||||
|
||||
fun setLocation(location: WeatherLocation) {
|
||||
updateData {
|
||||
val providerSettings =
|
||||
it.providerSettings.getOrDefault(it.provider, ProviderSettings())
|
||||
when (location) {
|
||||
is WeatherLocation.LatLon -> {
|
||||
it.copy(
|
||||
location = LatLon(lat = location.lat, lon = location.lon),
|
||||
locationName = location.name,
|
||||
lastUpdate = 0L,
|
||||
autoLocation = false,
|
||||
providerSettings = it.providerSettings.toMutableMap().apply {
|
||||
put(
|
||||
it.provider,
|
||||
providerSettings.copy(
|
||||
locationId = null,
|
||||
locationName = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is WeatherLocation.Id -> {
|
||||
it.copy(
|
||||
location = null,
|
||||
locationName = null,
|
||||
autoLocation = false,
|
||||
lastUpdate = 0L,
|
||||
providerSettings = it.providerSettings.toMutableMap().apply {
|
||||
put(
|
||||
it.provider,
|
||||
providerSettings.copy(
|
||||
locationId = location.locationId,
|
||||
locationName = location.name
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setLastLocation(location: LatLon) {
|
||||
updateData {
|
||||
it.copy(
|
||||
lastLocation = location,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val lastUpdate = data.map { it.lastUpdate }
|
||||
|
||||
fun setLastUpdate(lastUpdate: Long) {
|
||||
updateData {
|
||||
it.copy(lastUpdate = lastUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
val providerId = data.map { it.provider }
|
||||
|
||||
fun setProvider(provider: WeatherProviderInfo) {
|
||||
setProviderId(provider.id)
|
||||
}
|
||||
|
||||
fun setAutoLocation(autoLocation: Boolean) {
|
||||
updateData {
|
||||
it.copy(
|
||||
autoLocation = autoLocation,
|
||||
lastUpdate = 0L,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setProviderId(providerId: String) {
|
||||
updateData {
|
||||
it.copy(
|
||||
provider = providerId,
|
||||
lastUpdate = 0L,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package de.mm20.launcher2.weather.settings
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.encodeToStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
|
||||
@Serializable
|
||||
data class LatLon(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProviderSettings(
|
||||
val locationId: String? = null,
|
||||
val locationName: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WeatherSettingsData(
|
||||
val schemaVersion: Int = 1,
|
||||
val provider: String = "metno",
|
||||
val autoLocation: Boolean = true,
|
||||
val location: LatLon? = null,
|
||||
val locationName: String? = null,
|
||||
val lastLocation: LatLon? = null,
|
||||
val lastUpdate: Long = 0L,
|
||||
val providerSettings: Map<String, ProviderSettings> = emptyMap(),
|
||||
)
|
||||
|
||||
internal object WeatherSettingsSerializer : Serializer<WeatherSettingsData>{
|
||||
internal val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
override val defaultValue: WeatherSettingsData
|
||||
get() = WeatherSettingsData()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): WeatherSettingsData {
|
||||
try {
|
||||
return json.decodeFromStream(input)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw (CorruptionException("Cannot read json.", e))
|
||||
} catch (e: SerializationException) {
|
||||
throw (CorruptionException("Cannot read json.", e))
|
||||
} catch (e: IOException) {
|
||||
throw (CorruptionException("Cannot read json.", e))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: WeatherSettingsData, output: OutputStream) {
|
||||
json.encodeToStream(t, output)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ dependencies {
|
||||
implementation(libs.coil.core)
|
||||
|
||||
implementation(project(":core:base"))
|
||||
implementation(project(":core:preferences"))
|
||||
implementation(project(":core:ktx"))
|
||||
|
||||
}
|
||||
@@ -8,6 +8,6 @@ import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val websitesModule = module {
|
||||
single<SearchableRepository<Website>>(named<Website>()) { WebsiteRepository(androidContext()) }
|
||||
single<SearchableRepository<Website>>(named<Website>()) { WebsiteRepository(androidContext(), get()) }
|
||||
factory<SearchableDeserializer>(named(WebsiteImpl.Domain)) { WebsiteDeserializer() }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.webkit.URLUtil
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.core.graphics.toColorInt
|
||||
import de.mm20.launcher2.preferences.search.WebsiteSearchSettings
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import de.mm20.launcher2.search.Website
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -11,6 +12,8 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -25,7 +28,10 @@ import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
internal class WebsiteRepository(val context: Context) : SearchableRepository<Website> {
|
||||
internal class WebsiteRepository(
|
||||
val context: Context,
|
||||
val settings: WebsiteSearchSettings,
|
||||
) : SearchableRepository<Website> {
|
||||
|
||||
private val httpClient = OkHttpClient
|
||||
.Builder()
|
||||
@@ -34,16 +40,18 @@ internal class WebsiteRepository(val context: Context) : SearchableRepository<We
|
||||
.writeTimeout(1000, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
override fun search(query: String): Flow<ImmutableList<Website>> = channelFlow {
|
||||
send(persistentListOf())
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
}
|
||||
if (query.isBlank()) return@channelFlow
|
||||
override fun search(query: String): Flow<ImmutableList<Website>> {
|
||||
return settings.enabled.transformLatest {enabled ->
|
||||
emit(persistentListOf())
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
}
|
||||
if (!enabled || query.isBlank()) return@transformLatest
|
||||
|
||||
val website = queryWebsite(query)
|
||||
website?.let {
|
||||
send(persistentListOf(it))
|
||||
val website = queryWebsite(query)
|
||||
website?.let {
|
||||
emit(persistentListOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.UUID
|
||||
@Serializable
|
||||
data class FavoritesWidgetConfig(
|
||||
val editButton: Boolean = true,
|
||||
val tagsMultiline: Boolean = false,
|
||||
)
|
||||
|
||||
data class FavoritesWidget(
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.mm20.launcher2.wikipedia
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.search.WikipediaSearchSettings
|
||||
import de.mm20.launcher2.search.Article
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -10,7 +10,6 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.core.component.KoinComponent
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -18,7 +17,7 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
internal class WikipediaRepository(
|
||||
private val context: Context,
|
||||
private val dataStore: LauncherDataStore
|
||||
private val settings: WikipediaSearchSettings,
|
||||
) : SearchableRepository<Article> {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
@@ -34,8 +33,7 @@ internal class WikipediaRepository(
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
dataStore.data
|
||||
.map { it.wikipediaSearch.customUrl }
|
||||
settings.customUrl
|
||||
.distinctUntilChanged()
|
||||
.collectLatest {
|
||||
try { retrofit = Retrofit.Builder()
|
||||
@@ -55,24 +53,27 @@ internal class WikipediaRepository(
|
||||
private lateinit var wikipediaService: WikipediaApi
|
||||
|
||||
|
||||
override fun search(query: String): Flow<ImmutableList<Wikipedia>> = channelFlow {
|
||||
send(persistentListOf())
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
override fun search(query: String): Flow<ImmutableList<Wikipedia>> {
|
||||
if (query.length < 4) return flowOf(persistentListOf())
|
||||
|
||||
return settings.enabled.transformLatest {
|
||||
emit(persistentListOf())
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
}
|
||||
|
||||
if (!it || !::wikipediaService.isInitialized) return@transformLatest
|
||||
if (query.isBlank()) return@transformLatest
|
||||
|
||||
val results = queryWikipedia(query)
|
||||
if (results != null) {
|
||||
emit(persistentListOf(results))
|
||||
}
|
||||
}
|
||||
|
||||
if (query.length < 4) return@channelFlow
|
||||
|
||||
if (!::wikipediaService.isInitialized) return@channelFlow
|
||||
if (query.isBlank()) return@channelFlow
|
||||
|
||||
dataStore.data.map { it.wikipediaSearch.images }.collectLatest {
|
||||
val wikipedia = queryWikipedia(query, false)
|
||||
send(wikipedia?.let { persistentListOf(it) } ?: persistentListOf())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryWikipedia(query: String, loadImages: Boolean): Wikipedia? {
|
||||
private suspend fun queryWikipedia(query: String): Wikipedia? {
|
||||
|
||||
val wikipediaService = wikipediaService
|
||||
val wikipediaUrl = retrofit.baseUrl().toString()
|
||||
@@ -87,9 +88,7 @@ internal class WikipediaRepository(
|
||||
|
||||
val page = result.query?.pages?.values?.toList()?.getOrNull(0) ?: return null
|
||||
|
||||
val image = if (loadImages) {
|
||||
result.query.pages.values.toList().getOrNull(0)?.thumbnail?.source
|
||||
} else null
|
||||
val image = result.query.pages.values.toList().getOrNull(0)?.thumbnail?.source
|
||||
|
||||
return Wikipedia(
|
||||
label = page.title,
|
||||
|
||||
Reference in New Issue
Block a user