Add tasks.org integration
This commit is contained in:
parent
da924013b6
commit
7132e69ec8
@ -30,6 +30,7 @@
|
|||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="org.tasks.permission.READ_TASKS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".LauncherApplication"
|
android:name=".LauncherApplication"
|
||||||
|
|||||||
@ -12,7 +12,16 @@ import de.mm20.launcher2.searchable.PinnedLevel
|
|||||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
import de.mm20.launcher2.services.favorites.FavoritesService
|
||||||
import de.mm20.launcher2.widgets.CalendarWidget
|
import de.mm20.launcher2.widgets.CalendarWidget
|
||||||
import de.mm20.launcher2.widgets.WidgetRepository
|
import de.mm20.launcher2.widgets.WidgetRepository
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.shareIn
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
@ -25,7 +34,8 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
val selectedTag = MutableStateFlow<String?>(null)
|
val selectedTag = MutableStateFlow<String?>(null)
|
||||||
|
|
||||||
val showEditButton = settings.showEditButton.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
val showEditButton =
|
||||||
|
settings.showEditButton.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||||
abstract val tagsExpanded: Flow<Boolean>
|
abstract val tagsExpanded: Flow<Boolean>
|
||||||
abstract val compactTags: Flow<Boolean>
|
abstract val compactTags: Flow<Boolean>
|
||||||
|
|
||||||
@ -52,14 +62,24 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
|
|||||||
val frequentlyUsedRows = it.second.frequentlyUsedRows
|
val frequentlyUsedRows = it.second.frequentlyUsedRows
|
||||||
|
|
||||||
val pinned = favoritesService.getFavorites(
|
val pinned = favoritesService.getFavorites(
|
||||||
excludeTypes = if (excludeCalendar) listOf("calendar", "tag", "plugin.calendar") else listOf("tag"),
|
excludeTypes = if (excludeCalendar) listOf(
|
||||||
|
"calendar",
|
||||||
|
"tasks.org",
|
||||||
|
"tag",
|
||||||
|
"plugin.calendar"
|
||||||
|
) else listOf("tag"),
|
||||||
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
||||||
limit = 10 * columns,
|
limit = 10 * columns,
|
||||||
)
|
)
|
||||||
if (includeFrequentlyUsed) {
|
if (includeFrequentlyUsed) {
|
||||||
emitAll(pinned.flatMapLatest { pinned ->
|
emitAll(pinned.flatMapLatest { pinned ->
|
||||||
favoritesService.getFavorites(
|
favoritesService.getFavorites(
|
||||||
excludeTypes = if (excludeCalendar) listOf("calendar", "tag", "plugin.calendar") else listOf("tag"),
|
excludeTypes = if (excludeCalendar) listOf(
|
||||||
|
"calendar",
|
||||||
|
"tasks.org",
|
||||||
|
"tag",
|
||||||
|
"plugin.calendar"
|
||||||
|
) else listOf("tag"),
|
||||||
maxPinnedLevel = PinnedLevel.FrequentlyUsed,
|
maxPinnedLevel = PinnedLevel.FrequentlyUsed,
|
||||||
minPinnedLevel = PinnedLevel.FrequentlyUsed,
|
minPinnedLevel = PinnedLevel.FrequentlyUsed,
|
||||||
limit = frequentlyUsedRows * columns - pinned.size % columns,
|
limit = frequentlyUsedRows * columns - pinned.size % columns,
|
||||||
|
|||||||
@ -554,6 +554,7 @@ fun ColumnScope.ConfigureCalendarWidget(
|
|||||||
for (group in groups) {
|
for (group in groups) {
|
||||||
val pluginName = remember(plugins, group.key) {
|
val pluginName = remember(plugins, group.key) {
|
||||||
if (group.key == "local") context.getString(R.string.preference_calendar_calendars)
|
if (group.key == "local") context.getString(R.string.preference_calendar_calendars)
|
||||||
|
else if (group.key == "tasks.org") context.getString(R.string.preference_calendar_tasks)
|
||||||
else plugins.find { it.authority == group.key }?.label
|
else plugins.find { it.authority == group.key }?.label
|
||||||
}
|
}
|
||||||
if (pluginName != null) {
|
if (pluginName != null) {
|
||||||
|
|||||||
@ -43,7 +43,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
|||||||
val calendarEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
val calendarEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
||||||
val pinnedCalendarEvents =
|
val pinnedCalendarEvents =
|
||||||
favoritesService.getFavorites(
|
favoritesService.getFavorites(
|
||||||
includeTypes = listOf("calendar", "plugin.calendar"),
|
includeTypes = listOf("calendar", "tasks.org", "plugin.calendar"),
|
||||||
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
||||||
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||||
val nextEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
val nextEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
||||||
@ -172,7 +172,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
|||||||
excludeCalendars = config.excludedCalendarIds ?: config.legacyExcludedCalendarIds?.map { "local:$it" } ?: emptyList(),
|
excludeCalendars = config.excludedCalendarIds ?: config.legacyExcludedCalendarIds?.map { "local:$it" } ?: emptyList(),
|
||||||
).collectLatest { events ->
|
).collectLatest { events ->
|
||||||
searchableRepository.getKeys(
|
searchableRepository.getKeys(
|
||||||
includeTypes = listOf("calendar", "plugin.calendar"),
|
includeTypes = listOf("calendar", "tasks.org", "plugin.calendar"),
|
||||||
maxVisibility = VisibilityLevel.SearchOnly,
|
maxVisibility = VisibilityLevel.SearchOnly,
|
||||||
limit = 9999,
|
limit = 9999,
|
||||||
).collectLatest { hidden ->
|
).collectLatest { hidden ->
|
||||||
|
|||||||
@ -46,7 +46,7 @@ fun CalendarProviderSettingsScreen(providerId: String) {
|
|||||||
|
|
||||||
val pluginState by viewModel.pluginState.collectAsStateWithLifecycle(null)
|
val pluginState by viewModel.pluginState.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
val providerAvailable = providerId == "local" || pluginState != null
|
val providerAvailable = providerId == "local" || providerId == "tasks.org" || pluginState != null
|
||||||
|
|
||||||
PreferenceScreen(
|
PreferenceScreen(
|
||||||
title = pluginState?.plugin?.label ?: stringResource(R.string.preference_search_calendar)
|
title = pluginState?.plugin?.label ?: stringResource(R.string.preference_search_calendar)
|
||||||
@ -59,9 +59,11 @@ fun CalendarProviderSettingsScreen(providerId: String) {
|
|||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title =
|
title =
|
||||||
if (providerId == "local") stringResource(R.string.preference_search_calendar)
|
if (providerId == "local") stringResource(R.string.preference_search_calendar)
|
||||||
|
else if (providerId == "tasks.org") stringResource(R.string.preference_search_tasks)
|
||||||
else pluginState?.plugin?.label ?: "",
|
else pluginState?.plugin?.label ?: "",
|
||||||
summary =
|
summary =
|
||||||
if (providerId == "local") stringResource(R.string.preference_search_local_calendar_summary)
|
if (providerId == "local") stringResource(R.string.preference_search_local_calendar_summary)
|
||||||
|
else if (providerId == "tasks.org") stringResource(R.string.preference_search_tasks_summary)
|
||||||
else (pluginState?.state as? PluginState.Ready)?.text
|
else (pluginState?.state as? PluginState.Ready)?.text
|
||||||
?: pluginState?.plugin?.description,
|
?: pluginState?.plugin?.description,
|
||||||
value = enabled && (pluginState == null || pluginState?.state is PluginState.Ready),
|
value = enabled && (pluginState == null || pluginState?.state is PluginState.Ready),
|
||||||
|
|||||||
@ -11,9 +11,6 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -39,7 +36,12 @@ fun CalendarSearchSettingsScreen() {
|
|||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsState(null)
|
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsState(null)
|
||||||
val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(emptyList(), minActiveState = Lifecycle.State.RESUMED)
|
val hasTasksPermission by viewModel.hasTasksPermission.collectAsState(null)
|
||||||
|
val isTasksAppInstalled by viewModel.isTasksAppInstalled.collectAsStateWithLifecycle(false)
|
||||||
|
val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(
|
||||||
|
emptyList(),
|
||||||
|
minActiveState = Lifecycle.State.RESUMED
|
||||||
|
)
|
||||||
val enabledProviders by viewModel.enabledProviders.collectAsState(emptySet())
|
val enabledProviders by viewModel.enabledProviders.collectAsState(emptySet())
|
||||||
|
|
||||||
PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) {
|
PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) {
|
||||||
@ -66,6 +68,29 @@ fun CalendarSearchSettingsScreen() {
|
|||||||
navController?.navigate("settings/search/calendar/local")
|
navController?.navigate("settings/search/calendar/local")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (isTasksAppInstalled) {
|
||||||
|
AnimatedVisibility(hasTasksPermission == false) {
|
||||||
|
MissingPermissionBanner(
|
||||||
|
text = stringResource(R.string.missing_permission_tasks_search_settings),
|
||||||
|
onClick = {
|
||||||
|
viewModel.requestTasksPermission(context as AppCompatActivity)
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PreferenceWithSwitch(
|
||||||
|
title = stringResource(R.string.preference_search_tasks),
|
||||||
|
summary = stringResource(R.string.preference_search_tasks_summary),
|
||||||
|
switchValue = enabledProviders.contains("tasks.org") && hasTasksPermission == true,
|
||||||
|
onSwitchChanged = {
|
||||||
|
viewModel.setProviderEnabled("tasks.org", it)
|
||||||
|
},
|
||||||
|
enabled = hasTasksPermission == true,
|
||||||
|
onClick = {
|
||||||
|
navController?.navigate("settings/search/calendar/tasks.org")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
for (plugin in plugins) {
|
for (plugin in plugins) {
|
||||||
val state = plugin.state
|
val state = plugin.state
|
||||||
if (state is PluginState.SetupRequired) {
|
if (state is PluginState.SetupRequired) {
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
package de.mm20.launcher2.ui.settings.calendarsearch
|
package de.mm20.launcher2.ui.settings.calendarsearch
|
||||||
|
|
||||||
|
import android.os.Process
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import de.mm20.launcher2.applications.AppRepository
|
||||||
import de.mm20.launcher2.calendar.CalendarRepository
|
import de.mm20.launcher2.calendar.CalendarRepository
|
||||||
import de.mm20.launcher2.calendar.providers.CalendarList
|
import de.mm20.launcher2.calendar.providers.CalendarList
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
@ -11,16 +14,25 @@ import de.mm20.launcher2.plugin.PluginType
|
|||||||
import de.mm20.launcher2.plugins.PluginService
|
import de.mm20.launcher2.plugins.PluginService
|
||||||
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class CalendarSearchSettingsScreenVM : ViewModel(), KoinComponent {
|
class CalendarSearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||||
private val settings: CalendarSearchSettings by inject()
|
private val settings: CalendarSearchSettings by inject()
|
||||||
private val calendarRepository: CalendarRepository by inject()
|
private val calendarRepository: CalendarRepository by inject()
|
||||||
|
private val appRepository: AppRepository by inject()
|
||||||
private val pluginService: PluginService by inject()
|
private val pluginService: PluginService by inject()
|
||||||
private val permissionsManager: PermissionsManager by inject()
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
|
|
||||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
|
val hasTasksPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
|
||||||
|
|
||||||
|
val isTasksAppInstalled = appRepository.findOne("org.tasks", Process.myUserHandle())
|
||||||
|
.map { it != null }
|
||||||
|
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
||||||
|
|
||||||
val availablePlugins = pluginService.getPluginsWithState(
|
val availablePlugins = pluginService.getPluginsWithState(
|
||||||
type = PluginType.Calendar,
|
type = PluginType.Calendar,
|
||||||
@ -37,10 +49,8 @@ class CalendarSearchSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
|
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
|
||||||
}
|
}
|
||||||
|
|
||||||
val calendarLists = calendarRepository.getCalendars()
|
fun requestTasksPermission(activity: AppCompatActivity) {
|
||||||
|
permissionsManager.requestPermission(activity, PermissionGroup.Tasks)
|
||||||
|
}
|
||||||
|
|
||||||
val excludedCalendars = settings.excludedCalendars
|
|
||||||
fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
|
|
||||||
settings.setCalendarExcluded(calendarId, excluded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -111,7 +111,7 @@ fun SearchSettingsScreen() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasLocationPlugins != false) {
|
if (hasContactPlugins != false) {
|
||||||
Preference(
|
Preference(
|
||||||
title = stringResource(R.string.preference_search_contacts),
|
title = stringResource(R.string.preference_search_contacts),
|
||||||
summary = stringResource(R.string.preference_search_contacts_summary),
|
summary = stringResource(R.string.preference_search_contacts_summary),
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
package de.mm20.launcher2.search
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.net.toUri
|
||||||
import de.mm20.launcher2.icons.ColorLayer
|
import de.mm20.launcher2.icons.ColorLayer
|
||||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||||
import de.mm20.launcher2.icons.TextLayer
|
import de.mm20.launcher2.icons.TextLayer
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
interface CalendarEvent : SavableSearchable {
|
interface CalendarEvent : SavableSearchable {
|
||||||
@ -34,5 +38,19 @@ interface CalendarEvent : SavableSearchable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openLocation(context: Context) {}
|
fun openLocation(context: Context) {
|
||||||
|
if (location == null) return
|
||||||
|
context.tryStartActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW)
|
||||||
|
.setData(
|
||||||
|
"geo:0,0?q=${
|
||||||
|
URLEncoder.encode(
|
||||||
|
location,
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
}".toUri()
|
||||||
|
)
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -388,6 +388,7 @@
|
|||||||
<string name="missing_permission_call_contacts_settings">Call permission is required to start calls</string>
|
<string name="missing_permission_call_contacts_settings">Call permission is required to start calls</string>
|
||||||
<!-- Missing calendar permission in search settings screen -->
|
<!-- Missing calendar permission in search settings screen -->
|
||||||
<string name="missing_permission_calendar_search_settings">Calendar permission is required to search calendar</string>
|
<string name="missing_permission_calendar_search_settings">Calendar permission is required to search calendar</string>
|
||||||
|
<string name="missing_permission_tasks_search_settings">Tasks permission is required to search tasks</string>
|
||||||
<!-- Missing calendar permission in calendar widget settings screen -->
|
<!-- Missing calendar permission in calendar widget settings screen -->
|
||||||
<string name="missing_permission_calendar_widget_settings">This widget requires calendar permission</string>
|
<string name="missing_permission_calendar_widget_settings">This widget requires calendar permission</string>
|
||||||
<string name="widget_config_calendar_missing_calendars_hint">Can\'t find your calendars?</string>
|
<string name="widget_config_calendar_missing_calendars_hint">Can\'t find your calendars?</string>
|
||||||
@ -608,6 +609,8 @@
|
|||||||
<string name="preference_search_calendar">Calendar</string>
|
<string name="preference_search_calendar">Calendar</string>
|
||||||
<string name="preference_search_calendar_summary">Search upcoming appointments and events</string>
|
<string name="preference_search_calendar_summary">Search upcoming appointments and events</string>
|
||||||
<string name="preference_search_local_calendar_summary">Search calendars on this device</string>
|
<string name="preference_search_local_calendar_summary">Search calendars on this device</string>
|
||||||
|
<string name="preference_search_tasks">Tasks</string>
|
||||||
|
<string name="preference_search_tasks_summary">Search tasks in the Tasks.org app</string>
|
||||||
<string name="preference_search_appshortcuts">App shortcuts</string>
|
<string name="preference_search_appshortcuts">App shortcuts</string>
|
||||||
<string name="preference_search_appshortcuts_summary">Search app shortcuts</string>
|
<string name="preference_search_appshortcuts_summary">Search app shortcuts</string>
|
||||||
<string name="preference_search_calculator">Calculator</string>
|
<string name="preference_search_calculator">Calculator</string>
|
||||||
@ -630,6 +633,7 @@
|
|||||||
<string name="preference_search_cloud_summary">Search %1$s\'s files</string>
|
<string name="preference_search_cloud_summary">Search %1$s\'s files</string>
|
||||||
<string name="preference_search_owncloud">Owncloud</string>
|
<string name="preference_search_owncloud">Owncloud</string>
|
||||||
<string name="preference_calendar_calendars">Calendars</string>
|
<string name="preference_calendar_calendars">Calendars</string>
|
||||||
|
<string name="preference_calendar_tasks">Tasks</string>
|
||||||
<string name="preference_calendar_hide_completed">Hide completed tasks</string>
|
<string name="preference_calendar_hide_completed">Hide completed tasks</string>
|
||||||
<string name="preference_screen_buildinfo">Build information</string>
|
<string name="preference_screen_buildinfo">Build information</string>
|
||||||
<string name="preference_screen_buildinfo_summary">More information about this build of this app</string>
|
<string name="preference_screen_buildinfo_summary">More information about this build of this app</string>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package de.mm20.launcher2.calendar
|
package de.mm20.launcher2.calendar
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
||||||
import de.mm20.launcher2.calendar.providers.CalendarList
|
import de.mm20.launcher2.calendar.providers.CalendarList
|
||||||
import de.mm20.launcher2.calendar.providers.CalendarProvider
|
import de.mm20.launcher2.calendar.providers.CalendarProvider
|
||||||
import de.mm20.launcher2.calendar.providers.PluginCalendarProvider
|
import de.mm20.launcher2.calendar.providers.PluginCalendarProvider
|
||||||
|
import de.mm20.launcher2.calendar.providers.TasksCalendarProvider
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
import de.mm20.launcher2.permissions.PermissionsManager
|
||||||
import de.mm20.launcher2.plugin.PluginRepository
|
import de.mm20.launcher2.plugin.PluginRepository
|
||||||
@ -21,10 +21,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.combineTransform
|
import kotlinx.coroutines.flow.combineTransform
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.transform
|
import kotlinx.coroutines.flow.transform
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@ -57,14 +54,21 @@ internal class CalendarRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
|
val hasTasksPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
|
||||||
val providerIds = settings.providers
|
val providerIds = settings.providers
|
||||||
val excludedCalendars = settings.excludedCalendars
|
val excludedCalendars = settings.excludedCalendars
|
||||||
|
|
||||||
return combineTransform(hasPermission, providerIds, excludedCalendars) { perm, providerIds, excludedCalendars ->
|
return combineTransform(
|
||||||
|
hasCalendarPermission,
|
||||||
|
hasTasksPermission,
|
||||||
|
providerIds,
|
||||||
|
excludedCalendars
|
||||||
|
) { calPerm, taskPerm, providerIds, excludedCalendars ->
|
||||||
val providers = providerIds.mapNotNull {
|
val providers = providerIds.mapNotNull {
|
||||||
when (it) {
|
when (it) {
|
||||||
"local" -> if (perm) AndroidCalendarProvider(context) else null
|
"local" -> if (calPerm) AndroidCalendarProvider(context) else null
|
||||||
|
"tasks.org" -> if (taskPerm) TasksCalendarProvider(context) else null
|
||||||
else -> PluginCalendarProvider(context, it)
|
else -> PluginCalendarProvider(context, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,14 +94,16 @@ internal class CalendarRepositoryImpl(
|
|||||||
excludeCalendars: List<String>,
|
excludeCalendars: List<String>,
|
||||||
excludeAllDayEvents: Boolean,
|
excludeAllDayEvents: Boolean,
|
||||||
): Flow<ImmutableList<CalendarEvent>> {
|
): Flow<ImmutableList<CalendarEvent>> {
|
||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
|
val hasTasksPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
|
||||||
val plugins = pluginRepository.findMany(
|
val plugins = pluginRepository.findMany(
|
||||||
type = PluginType.Calendar,
|
type = PluginType.Calendar,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
)
|
)
|
||||||
return combineTransform(hasPermission, plugins) { perm, plugins ->
|
return combineTransform(hasCalendarPermission, hasTasksPermission, plugins) { calPerm, taskPerm, plugins ->
|
||||||
val providers = buildList {
|
val providers = buildList {
|
||||||
if (perm) add(AndroidCalendarProvider(context)) else null
|
if (calPerm) add(AndroidCalendarProvider(context)) else null
|
||||||
|
if (taskPerm) add(TasksCalendarProvider(context)) else null
|
||||||
addAll(
|
addAll(
|
||||||
plugins.map {
|
plugins.map {
|
||||||
PluginCalendarProvider(context, it.authority)
|
PluginCalendarProvider(context, it.authority)
|
||||||
@ -119,7 +125,7 @@ internal class CalendarRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun queryCalendarEvents(
|
private fun queryCalendarEvents(
|
||||||
query: String?,
|
query: String?,
|
||||||
intervalStart: Long,
|
intervalStart: Long,
|
||||||
intervalEnd: Long,
|
intervalEnd: Long,
|
||||||
@ -154,21 +160,42 @@ internal class CalendarRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getCalendars(providerId: String?): Flow<List<CalendarList>> {
|
override fun getCalendars(providerId: String?): Flow<List<CalendarList>> {
|
||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
|
val hasTaskPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
|
||||||
|
|
||||||
val providers: Flow<List<CalendarProvider>> = if (providerId != null) {
|
val providers: Flow<List<CalendarProvider>> = if (providerId != null) {
|
||||||
when (providerId) {
|
when (providerId) {
|
||||||
"local" -> hasPermission.map { if (it) listOf(AndroidCalendarProvider(context)) else emptyList() }
|
"local" -> hasCalendarPermission.map {
|
||||||
else -> pluginRepository.get(providerId).map { if (it?.enabled == true) listOf(PluginCalendarProvider(context, providerId)) else emptyList() }
|
if (it) listOf(
|
||||||
|
AndroidCalendarProvider(
|
||||||
|
context
|
||||||
|
)
|
||||||
|
) else emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
"tasks.org" -> hasTaskPermission.map { if (it) listOf(TasksCalendarProvider(context)) else emptyList() }
|
||||||
|
else -> pluginRepository.get(providerId).map {
|
||||||
|
if (it?.enabled == true) listOf(
|
||||||
|
PluginCalendarProvider(
|
||||||
|
context,
|
||||||
|
providerId
|
||||||
|
)
|
||||||
|
) else emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val plugins = pluginRepository.findMany(
|
val plugins = pluginRepository.findMany(
|
||||||
type = PluginType.Calendar,
|
type = PluginType.Calendar,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
)
|
)
|
||||||
combine(hasPermission, plugins) { perm, plugins ->
|
combine(
|
||||||
|
hasCalendarPermission,
|
||||||
|
hasTaskPermission,
|
||||||
|
plugins
|
||||||
|
) { calPerm, tasksPerm, plugins ->
|
||||||
buildList {
|
buildList {
|
||||||
if (perm) add(AndroidCalendarProvider(context))
|
if (calPerm) add(AndroidCalendarProvider(context))
|
||||||
|
if (tasksPerm) add(TasksCalendarProvider(context))
|
||||||
addAll(plugins.map { PluginCalendarProvider(context, it.authority) })
|
addAll(plugins.map { PluginCalendarProvider(context, it.authority) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import de.mm20.launcher2.calendar.providers.AndroidCalendarEvent
|
|||||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
||||||
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
||||||
import de.mm20.launcher2.calendar.providers.PluginCalendarProvider
|
import de.mm20.launcher2.calendar.providers.PluginCalendarProvider
|
||||||
|
import de.mm20.launcher2.calendar.providers.TasksCalendarEvent
|
||||||
|
import de.mm20.launcher2.calendar.providers.TasksCalendarProvider
|
||||||
import de.mm20.launcher2.plugin.PluginRepository
|
import de.mm20.launcher2.plugin.PluginRepository
|
||||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
@ -44,9 +46,31 @@ class AndroidCalendarEventDeserializer(val context: Context): SearchableDeserial
|
|||||||
val id = json.getLong("id")
|
val id = json.getLong("id")
|
||||||
return AndroidCalendarProvider(context).get(id)
|
return AndroidCalendarProvider(context).get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TasksCalendarEventSerializer: SearchableSerializer {
|
||||||
|
override fun serialize(searchable: SavableSearchable): String {
|
||||||
|
searchable as TasksCalendarEvent
|
||||||
|
val json = JSONObject()
|
||||||
|
json.put("id", searchable.id)
|
||||||
|
return json.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val typePrefix: String
|
||||||
|
get() = TasksCalendarEvent.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
class TasksCalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||||
|
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, "org.tasks.permission.READ_TASKS") != PackageManager.PERMISSION_GRANTED) return null
|
||||||
|
val json = JSONObject(serialized)
|
||||||
|
val id = json.getLong("id")
|
||||||
|
return TasksCalendarProvider(context).get(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
internal data class SerializedCalendarEvent(
|
internal data class SerializedCalendarEvent(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package de.mm20.launcher2.calendar
|
|||||||
|
|
||||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarEvent
|
import de.mm20.launcher2.calendar.providers.AndroidCalendarEvent
|
||||||
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
||||||
|
import de.mm20.launcher2.calendar.providers.TasksCalendarEvent
|
||||||
import de.mm20.launcher2.search.CalendarEvent
|
import de.mm20.launcher2.search.CalendarEvent
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableRepository
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
@ -13,5 +14,6 @@ val calendarModule = module {
|
|||||||
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { get<CalendarRepository>() }
|
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { get<CalendarRepository>() }
|
||||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get(), get()) }
|
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get(), get()) }
|
||||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { AndroidCalendarEventDeserializer(androidContext()) }
|
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { AndroidCalendarEventDeserializer(androidContext()) }
|
||||||
|
factory<SearchableDeserializer>(named(TasksCalendarEvent.Domain)) { TasksCalendarEventDeserializer(androidContext()) }
|
||||||
factory<SearchableDeserializer>(named(PluginCalendarEvent.Domain)) { PluginCalendarEventDeserializer(androidContext(), get()) }
|
factory<SearchableDeserializer>(named(PluginCalendarEvent.Domain)) { PluginCalendarEventDeserializer(androidContext(), get()) }
|
||||||
}
|
}
|
||||||
@ -11,6 +11,7 @@ import de.mm20.launcher2.ktx.tryStartActivity
|
|||||||
import de.mm20.launcher2.search.CalendarEvent
|
import de.mm20.launcher2.search.CalendarEvent
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
internal data class AndroidCalendarEvent(
|
internal data class AndroidCalendarEvent(
|
||||||
override val label: String,
|
override val label: String,
|
||||||
@ -44,24 +45,6 @@ internal data class AndroidCalendarEvent(
|
|||||||
return context.tryStartActivity(getLaunchIntent(), options)
|
return context.tryStartActivity(getLaunchIntent(), options)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openLocation(context: Context) {
|
|
||||||
if (location == null) return
|
|
||||||
context.tryStartActivity(
|
|
||||||
Intent(Intent.ACTION_VIEW)
|
|
||||||
.setData(
|
|
||||||
Uri.parse(
|
|
||||||
"geo:0,0?q=${
|
|
||||||
URLEncoder.encode(
|
|
||||||
location,
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSerializer(): SearchableSerializer {
|
override fun getSerializer(): SearchableSerializer {
|
||||||
return AndroidCalendarEventSerializer()
|
return AndroidCalendarEventSerializer()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
package de.mm20.launcher2.calendar.providers
|
||||||
|
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.CalendarContract
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import de.mm20.launcher2.calendar.TasksCalendarEventSerializer
|
||||||
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
|
import de.mm20.launcher2.search.CalendarEvent
|
||||||
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
|
|
||||||
|
data class TasksCalendarEvent(
|
||||||
|
override val label: String,
|
||||||
|
val id: Long,
|
||||||
|
override val color: Int?,
|
||||||
|
override val startTime: Long?,
|
||||||
|
override val endTime: Long,
|
||||||
|
override val allDay: Boolean,
|
||||||
|
override val isCompleted: Boolean?,
|
||||||
|
override val description: String?,
|
||||||
|
override val calendarName: String?,
|
||||||
|
override val labelOverride: String? = null,
|
||||||
|
): CalendarEvent {
|
||||||
|
|
||||||
|
override val domain: String = Domain
|
||||||
|
|
||||||
|
override val key: String = "$domain://$id"
|
||||||
|
|
||||||
|
override val location: String? = null
|
||||||
|
override val attendees: List<String> = emptyList()
|
||||||
|
|
||||||
|
|
||||||
|
override fun overrideLabel(label: String): TasksCalendarEvent {
|
||||||
|
return this.copy(labelOverride = label)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||||
|
val uri = ContentUris.withAppendedId("content://org.tasks/tasks".toUri(), id)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
return context.tryStartActivity(intent, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSerializer(): SearchableSerializer {
|
||||||
|
return TasksCalendarEventSerializer()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val Domain = "tasks.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
package de.mm20.launcher2.calendar.providers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.database.getIntOrNull
|
||||||
|
import androidx.core.database.getLongOrNull
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import de.mm20.launcher2.search.CalendarEvent
|
||||||
|
import de.mm20.launcher2.search.calendar.CalendarListType
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
internal class TasksCalendarProvider(
|
||||||
|
private val context: Context,
|
||||||
|
) : CalendarProvider {
|
||||||
|
override suspend fun search(
|
||||||
|
query: String?,
|
||||||
|
from: Long,
|
||||||
|
to: Long,
|
||||||
|
excludedCalendars: List<String>,
|
||||||
|
excludeAllDayEvents: Boolean,
|
||||||
|
allowNetwork: Boolean
|
||||||
|
): List<CalendarEvent> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
queryTasks(
|
||||||
|
selection = buildList {
|
||||||
|
add("dueDate >= $from")
|
||||||
|
add("dueDate <= $to")
|
||||||
|
if (excludedCalendars.isNotEmpty()) {
|
||||||
|
add("cdl_id NOT IN (${excludedCalendars.joinToString()})")
|
||||||
|
}
|
||||||
|
if (query != null) {
|
||||||
|
add(
|
||||||
|
"title LIKE '%${
|
||||||
|
query.replace("'", "").replace("%", "")
|
||||||
|
}%'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.joinToString(" AND ").takeIf { it.isNotEmpty() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getCalendarLists(): List<CalendarList> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val uri = "content://org.tasks/lists".toUri()
|
||||||
|
val cursor = context.contentResolver.query(uri, arrayOf(), null, arrayOf(), null)
|
||||||
|
?: return@withContext emptyList()
|
||||||
|
val results = mutableListOf<CalendarList>()
|
||||||
|
|
||||||
|
val idIndex = cursor.getColumnIndex("cdl_id")
|
||||||
|
val nameIndex = cursor.getColumnIndex("cdl_name")
|
||||||
|
val colorIndex = cursor.getColumnIndex("cdl_color")
|
||||||
|
val accountIndex = cursor.getColumnIndex("cdl_account")
|
||||||
|
|
||||||
|
cursor.use {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getLongOrNull(idIndex)?.toString() ?: continue
|
||||||
|
results += CalendarList(
|
||||||
|
id = "$namespace:$id",
|
||||||
|
name = cursor.getStringOrNull(nameIndex) ?: continue,
|
||||||
|
color = cursor.getIntOrNull(colorIndex) ?: 0,
|
||||||
|
types = listOf(CalendarListType.Tasks),
|
||||||
|
providerId = "tasks.org",
|
||||||
|
owner = cursor.getStringOrNull(accountIndex)?.substringAfter(":", "")?.takeIf { it.isNotBlank() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryTasks(
|
||||||
|
selection: String? = null,
|
||||||
|
selectionArgs: Array<String>? = arrayOf(),
|
||||||
|
): List<CalendarEvent> {
|
||||||
|
val uri = "content://org.tasks/todoagenda".toUri()
|
||||||
|
val cursor = context.contentResolver.query(uri, arrayOf(), selection, selectionArgs, null)
|
||||||
|
|
||||||
|
val results = mutableListOf<CalendarEvent>()
|
||||||
|
|
||||||
|
cursor?.use {
|
||||||
|
val idIndex = cursor.getColumnIndex("_id")
|
||||||
|
val titleIndex = cursor.getColumnIndex("title")
|
||||||
|
val dueIndex = cursor.getColumnIndex("dueDate")
|
||||||
|
val completedIndex = cursor.getColumnIndex("completed")
|
||||||
|
val notesIndex = cursor.getColumnIndex("notes")
|
||||||
|
val colorIndex = cursor.getColumnIndex("cdl_color")
|
||||||
|
val calendarNameIndex = cursor.getColumnIndex("cdl_name")
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
|
||||||
|
val id = cursor.getLongOrNull(idIndex) ?: continue
|
||||||
|
val dueDate = cursor.getLongOrNull(dueIndex)?.takeIf { it > 0L } ?: continue
|
||||||
|
|
||||||
|
results += TasksCalendarEvent(
|
||||||
|
id = id,
|
||||||
|
label = cursor.getStringOrNull(titleIndex) ?: continue,
|
||||||
|
description = cursor.getStringOrNull(notesIndex),
|
||||||
|
color = cursor.getIntOrNull(colorIndex),
|
||||||
|
calendarName = cursor.getStringOrNull(calendarNameIndex),
|
||||||
|
startTime = null,
|
||||||
|
endTime = dueDate,
|
||||||
|
allDay = dueDate % 60000 <= 0, // https://github.com/tasks/tasks/blob/13d4c029e855fd32ec91e4d4ec5f740ec506136e/data/src/commonMain/kotlin/org/tasks/data/entity/Task.kt#L345
|
||||||
|
isCompleted = (cursor.getLongOrNull(completedIndex) ?: 0L) != 0L,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(id: Long): CalendarEvent? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
queryTasks(
|
||||||
|
selection = "_id = $id",
|
||||||
|
).firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val namespace: String = "tasks.org"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user