mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-06 19:34:23 +00:00
Rewrite CreateCalendarActivity to Compose (#645)
* Added color picker dialog Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> * Removed title Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> * Migrated to Jetpack Compose Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> * Removed unused layouts Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> * Fixed activity name Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> * Fixed duplicated title Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> * Cleanup Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> * Error tweaks Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> * Rewrite Create address book, Create collection, Delete collection * [WIP] Create calendar: more properties; use own color picker (other one was M3) * [WIP] Create calendar * Add missing properties for calendars * Support color, remove comments --------- Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me> Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
fea33ab60a
commit
d37718c58a
|
@ -202,7 +202,6 @@ dependencies {
|
|||
implementation(libs.commons.text)
|
||||
@Suppress("RedundantSuppression")
|
||||
implementation(libs.dnsjava)
|
||||
implementation(libs.jaredrummler.colorpicker)
|
||||
implementation(libs.mikepenz.aboutLibraries)
|
||||
implementation(libs.nsk90.kstatemachine)
|
||||
implementation(libs.okhttp.base)
|
||||
|
|
|
@ -150,7 +150,7 @@
|
|||
android:parentActivityName=".ui.account.AccountActivity" />
|
||||
<activity
|
||||
android:name=".ui.account.CreateCalendarActivity"
|
||||
android:label="@string/create_calendar"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:parentActivityName=".ui.account.AccountActivity" />
|
||||
<activity
|
||||
android:name=".ui.account.AccountSettingsActivity"
|
||||
|
|
|
@ -27,8 +27,8 @@ interface HomeSetDao {
|
|||
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind")
|
||||
fun getBindableByService(serviceId: Long): List<HomeSet>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM homeset WHERE serviceId=:serviceId AND privBind")
|
||||
fun hasBindableByServiceLive(serviceId: Long): LiveData<Boolean>
|
||||
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind")
|
||||
fun getLiveBindableByService(serviceId: Long): LiveData<List<HomeSet>>
|
||||
|
||||
@Insert
|
||||
fun insert(homeSet: HomeSet): Long
|
||||
|
|
|
@ -63,7 +63,6 @@ import at.bitfire.cert4android.CustomCertStore
|
|||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
|
||||
|
@ -74,6 +73,7 @@ import at.bitfire.davdroid.ui.widget.Setting
|
|||
import at.bitfire.davdroid.ui.widget.SettingsHeader
|
||||
import at.bitfire.davdroid.ui.widget.SwitchSetting
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -109,9 +109,7 @@ class AppSettingsActivity: AppCompatActivity() {
|
|||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
onSupportNavigateUp()
|
||||
}) {
|
||||
IconButton(onClick = { onSupportNavigateUp() }) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up))
|
||||
}
|
||||
},
|
||||
|
|
|
@ -15,10 +15,10 @@ import android.provider.ContactsContract
|
|||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.switchMap
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -26,36 +26,46 @@ import androidx.paging.Pager
|
|||
import androidx.paging.PagingConfig
|
||||
import androidx.work.WorkInfo
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.XmlUtils
|
||||
import at.bitfire.dav4jvm.property.caldav.NS_APPLE_ICAL
|
||||
import at.bitfire.dav4jvm.property.caldav.NS_CALDAV
|
||||
import at.bitfire.dav4jvm.property.carddav.NS_CARDDAV
|
||||
import at.bitfire.dav4jvm.property.webdav.NS_WEBDAV
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.syncadapter.AccountsCleanupWorker
|
||||
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.ical4android.util.DateUtils
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import net.fortuna.ical4j.model.Calendar
|
||||
import java.io.StringWriter
|
||||
import java.util.Optional
|
||||
import java.util.logging.Level
|
||||
|
||||
class AccountModel @AssistedInject constructor(
|
||||
application: Application,
|
||||
val context: Application,
|
||||
val db: AppDatabase,
|
||||
@Assisted val account: Account
|
||||
): AndroidViewModel(application), OnAccountsUpdateListener {
|
||||
): ViewModel(), OnAccountsUpdateListener {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
@ -69,7 +79,7 @@ class AccountModel @AssistedInject constructor(
|
|||
/** whether the account is invalid and the AccountActivity shall be closed */
|
||||
val invalid = MutableLiveData<Boolean>()
|
||||
|
||||
private val settings = AccountSettings(application, account)
|
||||
private val settings = AccountSettings(context, account)
|
||||
private val refreshSettingsSignal = MutableLiveData(Unit)
|
||||
val showOnlyPersonal = refreshSettingsSignal.switchMap<Unit, AccountSettings.ShowOnlyPersonal> {
|
||||
object : LiveData<AccountSettings.ShowOnlyPersonal>() {
|
||||
|
@ -85,23 +95,25 @@ class AccountModel @AssistedInject constructor(
|
|||
refreshSettingsSignal.postValue(Unit)
|
||||
}
|
||||
|
||||
val context = getApplication<Application>()
|
||||
val accountManager: AccountManager = AccountManager.get(context)
|
||||
|
||||
val cardDavSvc = db.serviceDao().getLiveByAccountAndType(account.name, Service.TYPE_CARDDAV)
|
||||
val canCreateAddressBook = cardDavSvc.switchMap { svc ->
|
||||
val bindableAddressBookHomesets = cardDavSvc.switchMap { svc ->
|
||||
if (svc != null)
|
||||
db.homeSetDao().hasBindableByServiceLive(svc.id)
|
||||
db.homeSetDao().getLiveBindableByService(svc.id)
|
||||
else
|
||||
MutableLiveData(false)
|
||||
MutableLiveData(emptyList())
|
||||
}
|
||||
val canCreateAddressBook = bindableAddressBookHomesets.map { homeSets ->
|
||||
homeSets.isNotEmpty()
|
||||
}
|
||||
val cardDavRefreshing = cardDavSvc.switchMap { svc ->
|
||||
if (svc == null)
|
||||
return@switchMap null
|
||||
RefreshCollectionsWorker.exists(application, RefreshCollectionsWorker.workerName(svc.id))
|
||||
RefreshCollectionsWorker.exists(context, RefreshCollectionsWorker.workerName(svc.id))
|
||||
}
|
||||
val cardDavSyncPending = BaseSyncWorker.exists(
|
||||
getApplication(),
|
||||
context,
|
||||
listOf(WorkInfo.State.ENQUEUED),
|
||||
account,
|
||||
listOf(context.getString(R.string.address_books_authority)),
|
||||
|
@ -111,7 +123,7 @@ class AccountModel @AssistedInject constructor(
|
|||
}
|
||||
)
|
||||
val cardDavSyncing = BaseSyncWorker.exists(
|
||||
getApplication(),
|
||||
context,
|
||||
listOf(WorkInfo.State.RUNNING),
|
||||
account,
|
||||
listOf(context.getString(R.string.address_books_authority))
|
||||
|
@ -120,20 +132,23 @@ class AccountModel @AssistedInject constructor(
|
|||
|
||||
private val tasksProvider = TaskUtils.currentProviderLive(context)
|
||||
val calDavSvc = db.serviceDao().getLiveByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||
val canCreateCalendar = calDavSvc.switchMap { svc ->
|
||||
val bindableCalendarHomesets = calDavSvc.switchMap { svc ->
|
||||
if (svc != null)
|
||||
db.homeSetDao().hasBindableByServiceLive(svc.id)
|
||||
db.homeSetDao().getLiveBindableByService(svc.id)
|
||||
else
|
||||
MutableLiveData(false)
|
||||
MutableLiveData(emptyList())
|
||||
}
|
||||
val canCreateCalendar = bindableCalendarHomesets.map { homeSets ->
|
||||
homeSets.isNotEmpty()
|
||||
}
|
||||
val calDavRefreshing = calDavSvc.switchMap { svc ->
|
||||
if (svc == null)
|
||||
return@switchMap null
|
||||
RefreshCollectionsWorker.exists(application, RefreshCollectionsWorker.workerName(svc.id))
|
||||
RefreshCollectionsWorker.exists(context, RefreshCollectionsWorker.workerName(svc.id))
|
||||
}
|
||||
val calDavSyncPending = tasksProvider.switchMap { tasks ->
|
||||
BaseSyncWorker.exists(
|
||||
getApplication(),
|
||||
context,
|
||||
listOf(WorkInfo.State.ENQUEUED),
|
||||
account,
|
||||
listOfNotNull(CalendarContract.AUTHORITY, tasks?.authority),
|
||||
|
@ -145,7 +160,7 @@ class AccountModel @AssistedInject constructor(
|
|||
}
|
||||
val calDavSyncing = tasksProvider.switchMap { tasks ->
|
||||
BaseSyncWorker.exists(
|
||||
getApplication(),
|
||||
context,
|
||||
listOf(WorkInfo.State.RUNNING),
|
||||
account,
|
||||
listOfNotNull(CalendarContract.AUTHORITY, tasks?.authority)
|
||||
|
@ -155,7 +170,6 @@ class AccountModel @AssistedInject constructor(
|
|||
val webcalPager = CollectionPager(db, calDavSvc, Collection.TYPE_WEBCAL, showOnlyPersonal)
|
||||
|
||||
val renameAccountError = MutableLiveData<String>()
|
||||
val deleteCollectionResult = MutableLiveData<Optional<Exception>>()
|
||||
|
||||
|
||||
init {
|
||||
|
@ -252,7 +266,6 @@ class AccountModel @AssistedInject constructor(
|
|||
fun onAccountRenamed(accountManager: AccountManager, oldAccount: Account, newName: String, syncIntervals: List<Pair<String, Long?>>) {
|
||||
// account has now been renamed
|
||||
Logger.log.info("Updating account name references")
|
||||
val context: Application = getApplication()
|
||||
|
||||
// disable periodic workers of old account
|
||||
syncIntervals.forEach { (authority, _) ->
|
||||
|
@ -329,9 +342,192 @@ class AccountModel @AssistedInject constructor(
|
|||
}, null)
|
||||
}
|
||||
|
||||
|
||||
val createCollectionResult = MutableLiveData<Optional<Exception>>()
|
||||
/**
|
||||
* Creates a WebDAV collection using MKCOL or MKCALENDAR.
|
||||
*
|
||||
* @param homeSet home set into which the collection shall be created
|
||||
* @param addressBook *true* if an address book shall be created, *false* if a calendar should be created
|
||||
* @param name name (path segment) of the collection
|
||||
*/
|
||||
fun createCollection(
|
||||
homeSet: HomeSet,
|
||||
addressBook: Boolean,
|
||||
name: String,
|
||||
displayName: String?,
|
||||
description: String?,
|
||||
color: Int? = null,
|
||||
timeZoneId: String? = null,
|
||||
supportsVEVENT: Boolean? = null,
|
||||
supportsVTODO: Boolean? = null,
|
||||
supportsVJOURNAL: Boolean? = null
|
||||
) = viewModelScope.launch(Dispatchers.IO) {
|
||||
HttpClient.Builder(context, AccountSettings(context, account))
|
||||
.setForeground(true)
|
||||
.build().use { httpClient ->
|
||||
try {
|
||||
// delete on server
|
||||
val url = homeSet.url.newBuilder()
|
||||
.addPathSegment(name)
|
||||
.addPathSegment("") // trailing slash
|
||||
.build()
|
||||
val dav = DavResource(httpClient.okHttpClient, url)
|
||||
|
||||
val xml = generateMkColXml(
|
||||
addressBook = addressBook,
|
||||
displayName = displayName,
|
||||
description = description,
|
||||
color = color,
|
||||
timezoneDef = timeZoneId?.let { tzId ->
|
||||
DateUtils.ical4jTimeZone(tzId)?.let { tz ->
|
||||
val cal = Calendar()
|
||||
cal.components += tz.vTimeZone
|
||||
cal.toString()
|
||||
}
|
||||
},
|
||||
supportsVEVENT = supportsVEVENT,
|
||||
supportsVTODO = supportsVTODO,
|
||||
supportsVJOURNAL = supportsVJOURNAL
|
||||
)
|
||||
|
||||
dav.mkCol(
|
||||
xmlBody = xml,
|
||||
method = if (addressBook) "MKCOL" else "MKCALENDAR"
|
||||
) {
|
||||
// success, otherwise an exception would have been thrown
|
||||
}
|
||||
|
||||
// no HTTP error -> create collection locally
|
||||
val collection = Collection(
|
||||
serviceId = homeSet.serviceId,
|
||||
homeSetId = homeSet.id,
|
||||
url = url,
|
||||
type = if (addressBook) Collection.TYPE_ADDRESSBOOK else Collection.TYPE_CALENDAR,
|
||||
displayName = displayName,
|
||||
description = description
|
||||
)
|
||||
db.collectionDao().insert(collection)
|
||||
|
||||
// trigger service detection (because the collection may actually have other properties than the ones we have inserted)
|
||||
RefreshCollectionsWorker.enqueue(context, homeSet.serviceId)
|
||||
|
||||
// post success
|
||||
createCollectionResult.postValue(Optional.empty())
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't create collection", e)
|
||||
// post error
|
||||
createCollectionResult.postValue(Optional.of(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateMkColXml(
|
||||
addressBook: Boolean,
|
||||
displayName: String?,
|
||||
description: String?,
|
||||
color: Int? = null,
|
||||
timezoneDef: String? = null,
|
||||
supportsVEVENT: Boolean? = null,
|
||||
supportsVTODO: Boolean? = null,
|
||||
supportsVJOURNAL: Boolean? = null
|
||||
): String {
|
||||
val writer = StringWriter()
|
||||
val serializer = XmlUtils.newSerializer()
|
||||
serializer.apply {
|
||||
setOutput(writer)
|
||||
|
||||
startDocument("UTF-8", null)
|
||||
setPrefix("", NS_WEBDAV)
|
||||
setPrefix("CAL", NS_CALDAV)
|
||||
setPrefix("CARD", NS_CARDDAV)
|
||||
|
||||
if (addressBook)
|
||||
startTag(NS_WEBDAV, "mkcol")
|
||||
else
|
||||
startTag(NS_CALDAV, "mkcalendar")
|
||||
startTag(NS_WEBDAV, "set")
|
||||
startTag(NS_WEBDAV, "prop")
|
||||
|
||||
startTag(NS_WEBDAV, "resourcetype")
|
||||
startTag(NS_WEBDAV, "collection")
|
||||
endTag(NS_WEBDAV, "collection")
|
||||
if (addressBook) {
|
||||
startTag(NS_CARDDAV, "addressbook")
|
||||
endTag(NS_CARDDAV, "addressbook")
|
||||
} else {
|
||||
startTag(NS_CALDAV, "calendar")
|
||||
endTag(NS_CALDAV, "calendar")
|
||||
}
|
||||
endTag(NS_WEBDAV, "resourcetype")
|
||||
|
||||
displayName?.let {
|
||||
startTag(NS_WEBDAV, "displayname")
|
||||
text(it)
|
||||
endTag(NS_WEBDAV, "displayname")
|
||||
}
|
||||
|
||||
if (addressBook) {
|
||||
// addressbook-specific properties
|
||||
description?.let {
|
||||
startTag(NS_CARDDAV, "addressbook-description")
|
||||
text(it)
|
||||
endTag(NS_CARDDAV, "addressbook-description")
|
||||
}
|
||||
|
||||
} else {
|
||||
// calendar-specific properties
|
||||
description?.let {
|
||||
startTag(NS_CALDAV, "calendar-description")
|
||||
text(it)
|
||||
endTag(NS_CALDAV, "calendar-description")
|
||||
}
|
||||
color?.let {
|
||||
startTag(NS_APPLE_ICAL, "calendar-color")
|
||||
text(DavUtils.ARGBtoCalDAVColor(it))
|
||||
endTag(NS_APPLE_ICAL, "calendar-color")
|
||||
}
|
||||
timezoneDef?.let {
|
||||
startTag(NS_CALDAV, "calendar-timezone")
|
||||
cdsect(it)
|
||||
endTag(NS_CALDAV, "calendar-timezone")
|
||||
}
|
||||
|
||||
if (supportsVEVENT != null || supportsVTODO != null || supportsVJOURNAL != null) {
|
||||
// only if there's at least one explicitly supported calendar component set, otherwise don't include the property
|
||||
if (supportsVEVENT != false) {
|
||||
startTag(NS_CALDAV, "comp")
|
||||
attribute(null, "name", "VEVENT")
|
||||
endTag(NS_CALDAV, "comp")
|
||||
}
|
||||
if (supportsVTODO != false) {
|
||||
startTag(NS_CALDAV, "comp")
|
||||
attribute(null, "name", "VTODO")
|
||||
endTag(NS_CALDAV, "comp")
|
||||
}
|
||||
if (supportsVJOURNAL != false) {
|
||||
startTag(NS_CALDAV, "comp")
|
||||
attribute(null, "name", "VJOURNAL")
|
||||
endTag(NS_CALDAV, "comp")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endTag(NS_WEBDAV, "prop")
|
||||
endTag(NS_WEBDAV, "set")
|
||||
if (addressBook)
|
||||
endTag(NS_WEBDAV, "mkcol")
|
||||
else
|
||||
endTag(NS_CALDAV, "mkcalendar")
|
||||
endDocument()
|
||||
}
|
||||
return writer.toString()
|
||||
}
|
||||
|
||||
val deleteCollectionResult = MutableLiveData<Optional<Exception>>()
|
||||
/** Deletes the given collection from the database and the server. */
|
||||
fun deleteCollection(collection: Collection) = viewModelScope.launch(Dispatchers.IO) {
|
||||
HttpClient.Builder(getApplication(), AccountSettings(getApplication(), account))
|
||||
HttpClient.Builder(context, AccountSettings(context, account))
|
||||
.setForeground(true)
|
||||
.build().use { httpClient ->
|
||||
try {
|
||||
|
@ -378,7 +574,7 @@ class AccountModel @AssistedInject constructor(
|
|||
val interestingAuthorities = listOfNotNull(
|
||||
ContactsContract.AUTHORITY,
|
||||
CalendarContract.AUTHORITY,
|
||||
TaskUtils.currentProvider(getApplication())?.authority
|
||||
TaskUtils.currentProvider(context)?.authority
|
||||
)
|
||||
val result = mutableMapOf<String, Long>()
|
||||
for (authority in interestingAuthorities) {
|
||||
|
@ -398,7 +594,7 @@ class AccountModel @AssistedInject constructor(
|
|||
* @return the application name of authority (ie "jtx Board")
|
||||
*/
|
||||
private fun getAppNameFromAuthority(authority: String): String {
|
||||
val packageManager = getApplication<Application>().packageManager
|
||||
val packageManager = context.packageManager
|
||||
val packageName = packageManager.resolveContentProvider(authority, 0)?.packageName ?: authority
|
||||
return try {
|
||||
val appInfo = packageManager.getPackageInfo(packageName, 0).applicationInfo
|
||||
|
@ -413,7 +609,7 @@ class AccountModel @AssistedInject constructor(
|
|||
class CollectionPager(
|
||||
val db: AppDatabase,
|
||||
service: LiveData<Service?>,
|
||||
val collectionType: String,
|
||||
private val collectionType: String,
|
||||
showOnlyPersonal: LiveData<AccountSettings.ShowOnlyPersonal>
|
||||
) : MediatorLiveData<Pager<Int, Collection>?>() {
|
||||
|
||||
|
|
|
@ -11,48 +11,44 @@ import androidx.activity.compose.setContent
|
|||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.ui.AppTheme
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import at.bitfire.davdroid.ui.widget.ExceptionInfoDialog
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.UUID
|
||||
|
@ -65,41 +61,109 @@ class CreateAddressBookActivity: AppCompatActivity() {
|
|||
const val EXTRA_ACCOUNT = "account"
|
||||
}
|
||||
|
||||
@Inject lateinit var modelFactory: Model.Factory
|
||||
val model by viewModels<Model> {
|
||||
val account by lazy { intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") }
|
||||
|
||||
@Inject
|
||||
lateinit var modelFactory: AccountModel.Factory
|
||||
val model by viewModels<AccountModel> {
|
||||
object: ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
val account = intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set")
|
||||
return modelFactory.create(account) as T
|
||||
}
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
modelFactory.create(account) as T
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
val displayName by model.displayName.observeAsState()
|
||||
val description by model.description.observeAsState()
|
||||
val homeSet by model.homeSet.observeAsState()
|
||||
val homeSets by model.homeSets.observeAsState()
|
||||
var displayName by remember { mutableStateOf("") }
|
||||
var description by remember { mutableStateOf("") }
|
||||
var homeSet by remember { mutableStateOf<HomeSet?>(null) }
|
||||
|
||||
Content(
|
||||
isCreateEnabled =
|
||||
displayName != null &&
|
||||
homeSet != null,
|
||||
displayName = displayName,
|
||||
onDisplayNameChange = model.displayName::setValue,
|
||||
description = description,
|
||||
onDescriptionChange = model.description::setValue,
|
||||
homeSet = homeSet,
|
||||
homeSets = homeSets,
|
||||
onHomeSetClicked = model.homeSet::setValue
|
||||
)
|
||||
var isCreating by remember { mutableStateOf(false) }
|
||||
model.createCollectionResult.observeAsState().value?.let { result ->
|
||||
if (result.isEmpty)
|
||||
finish()
|
||||
else
|
||||
ExceptionInfoDialog(
|
||||
exception = result.get(),
|
||||
onDismiss = {
|
||||
isCreating = false
|
||||
model.createCollectionResult.value = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val onCreateCollection = {
|
||||
if (!isCreating) {
|
||||
isCreating = true
|
||||
homeSet?.let { homeSet ->
|
||||
model.createCollection(
|
||||
homeSet = homeSet,
|
||||
addressBook = true,
|
||||
name = UUID.randomUUID().toString(),
|
||||
displayName = StringUtils.trimToNull(displayName),
|
||||
description = StringUtils.trimToNull(description)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val homeSets by model.bindableAddressBookHomesets.observeAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.create_addressbook)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onSupportNavigateUp() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.navigate_up))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
val isCreateEnabled = !isCreating && displayName.isNotEmpty() && homeSet != null
|
||||
IconButton(
|
||||
enabled = isCreateEnabled,
|
||||
onClick = { onCreateCollection() }
|
||||
) {
|
||||
Text(stringResource(R.string.create_collection_create).uppercase())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
if (isCreating)
|
||||
LinearProgressIndicator(
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
homeSets?.let { homeSets ->
|
||||
AddressBookForm(
|
||||
displayName = displayName,
|
||||
onDisplayNameChange = { displayName = it },
|
||||
description = description,
|
||||
onDescriptionChange = { description = it },
|
||||
homeSets = homeSets,
|
||||
homeSet = homeSet,
|
||||
onHomeSetSelected = { homeSet = it },
|
||||
onCreateCollection = {
|
||||
onCreateCollection()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,191 +176,75 @@ class CreateAddressBookActivity: AppCompatActivity() {
|
|||
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
isCreateEnabled: Boolean = false,
|
||||
displayName: String? = null,
|
||||
private fun AddressBookForm(
|
||||
displayName: String,
|
||||
onDisplayNameChange: (String) -> Unit = {},
|
||||
description: String? = null,
|
||||
description: String,
|
||||
onDescriptionChange: (String) -> Unit = {},
|
||||
homeSet: HomeSet? = null,
|
||||
homeSets: List<HomeSet>? = null,
|
||||
onHomeSetClicked: (HomeSet) -> Unit = {}
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { TopBar(isCreateEnabled) }
|
||||
) { paddingValues ->
|
||||
CreateAddressBookForm(
|
||||
paddingValues,
|
||||
displayName,
|
||||
onDisplayNameChange,
|
||||
description,
|
||||
onDescriptionChange,
|
||||
homeSet,
|
||||
homeSets,
|
||||
onHomeSetClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, showSystemUi = true)
|
||||
private fun Content_Preview() {
|
||||
Content(
|
||||
displayName = "Display Name",
|
||||
description = "Description",
|
||||
homeSets = listOf(
|
||||
HomeSet(1, 0, false, "http://example.com/".toHttpUrl()),
|
||||
HomeSet(2, 0, false, "http://example.com/".toHttpUrl(), displayName = "Home Set 2"),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateAddressBookForm(
|
||||
paddingValues: PaddingValues,
|
||||
displayName: String?,
|
||||
onDisplayNameChange: (String) -> Unit,
|
||||
description: String?,
|
||||
onDescriptionChange: (String) -> Unit,
|
||||
homeSet: HomeSet?,
|
||||
homeSets: List<HomeSet>?,
|
||||
onHomeSetClicked: (HomeSet) -> Unit
|
||||
homeSets: List<HomeSet>,
|
||||
onHomeSetSelected: (HomeSet) -> Unit = {},
|
||||
onCreateCollection: () -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(8.dp)
|
||||
Column(Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
OutlinedTextField(
|
||||
value = displayName ?: "",
|
||||
value = displayName,
|
||||
onValueChange = onDisplayNameChange,
|
||||
label = { Text(stringResource(R.string.create_collection_display_name)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = description ?: "",
|
||||
value = description,
|
||||
onValueChange = onDescriptionChange,
|
||||
label = { Text(stringResource(R.string.create_collection_description_optional)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
onCreateCollection()
|
||||
}
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.create_collection_home_set),
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp)
|
||||
HomeSetSelection(
|
||||
homeSet = homeSet,
|
||||
homeSets = homeSets,
|
||||
onHomeSetSelected = onHomeSetSelected
|
||||
)
|
||||
if (homeSets != null) {
|
||||
for (item in homeSets) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = homeSet == item,
|
||||
onClick = { onHomeSetClicked(item) }
|
||||
)
|
||||
Text(
|
||||
text = item.displayName ?: item.url.encodedPath,
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopBar(isCreateEnabled: Boolean) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.create_addressbook)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = ::finish) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
enabled = isCreateEnabled,
|
||||
onClick = ::onCreateCollection
|
||||
) {
|
||||
Text(stringResource(R.string.create_collection_create).uppercase())
|
||||
}
|
||||
}
|
||||
@Preview
|
||||
private fun AddressBookForm_Preview() {
|
||||
AddressBookForm(
|
||||
displayName = "Display Name",
|
||||
description = "Some longer description that is optional",
|
||||
homeSets = listOf(
|
||||
HomeSet(1, 0, false, "http://example.com/".toHttpUrl()),
|
||||
HomeSet(2, 0, false, "http://example.com/".toHttpUrl(), displayName = "Home Set 2")
|
||||
),
|
||||
homeSet = null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun onCreateCollection() {
|
||||
var ok = true
|
||||
|
||||
val args = Bundle()
|
||||
args.putString(CreateCollectionFragment.ARG_SERVICE_TYPE, Service.TYPE_CARDDAV)
|
||||
|
||||
val parent = model.homeSet.value
|
||||
if (parent != null) {
|
||||
args.putString(
|
||||
CreateCollectionFragment.ARG_URL,
|
||||
parent.url.resolve(UUID.randomUUID().toString() + "/").toString()
|
||||
)
|
||||
} else {
|
||||
ok = false
|
||||
}
|
||||
|
||||
val displayName = model.displayName.value
|
||||
if (displayName.isNullOrBlank())
|
||||
ok = false
|
||||
else
|
||||
args.putString(CreateCollectionFragment.ARG_DISPLAY_NAME, displayName)
|
||||
|
||||
StringUtils.trimToNull(model.description.value)?.let {
|
||||
args.putString(CreateCollectionFragment.ARG_DESCRIPTION, it)
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
args.putParcelable(CreateCollectionFragment.ARG_ACCOUNT, model.account)
|
||||
args.putString(CreateCollectionFragment.ARG_TYPE, Collection.TYPE_ADDRESSBOOK)
|
||||
val frag = CreateCollectionFragment()
|
||||
frag.arguments = args
|
||||
frag.show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Model @AssistedInject constructor(
|
||||
val db: AppDatabase,
|
||||
@Assisted val account: Account
|
||||
) : ViewModel() {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(account: Account): Model
|
||||
}
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
val description = MutableLiveData<String>()
|
||||
val homeSets = MutableLiveData<List<HomeSet>>()
|
||||
var homeSet = MutableLiveData<HomeSet>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// load account info
|
||||
db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV)?.let { service ->
|
||||
val homesets = db.homeSetDao().getBindableByService(service.id)
|
||||
homeSets.postValue(homesets)
|
||||
|
||||
if (homeSet.value == null)
|
||||
homeSet.postValue(homesets.firstOrNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -5,263 +5,430 @@
|
|||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.ActivityCreateCalendarBinding
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.ui.HomeSetAdapter
|
||||
import at.bitfire.ical4android.util.DateUtils
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import at.bitfire.davdroid.ui.AppTheme
|
||||
import at.bitfire.davdroid.ui.widget.CalendarColorPickerDialog
|
||||
import at.bitfire.davdroid.ui.widget.ExceptionInfoDialog
|
||||
import at.bitfire.davdroid.ui.widget.MultipleChoiceInputDialog
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.fortuna.ical4j.model.Calendar
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.text.Collator
|
||||
import java.time.ZoneId
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
|
||||
class CreateCalendarActivity: AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ACCOUNT = "account"
|
||||
}
|
||||
|
||||
@Inject lateinit var modelFactory: Model.Factory
|
||||
val model by viewModels<Model> {
|
||||
val account by lazy { intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") }
|
||||
|
||||
@Inject
|
||||
lateinit var modelFactory: AccountModel.Factory
|
||||
val accountModel by viewModels<AccountModel> {
|
||||
object: ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
val account = intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set")
|
||||
return modelFactory.create(account) as T
|
||||
}
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
modelFactory.create(account) as T
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var binding: ActivityCreateCalendarBinding
|
||||
val model: Model by viewModels()
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_create_calendar)
|
||||
binding.lifecycleOwner = this
|
||||
binding.model = model
|
||||
setContent {
|
||||
AppTheme {
|
||||
var displayName by remember { mutableStateOf("") }
|
||||
var color by remember { mutableIntStateOf(Constants.DAVDROID_GREEN_RGBA) }
|
||||
var description by remember { mutableStateOf("") }
|
||||
var homeSet by remember { mutableStateOf<HomeSet?>(null) }
|
||||
var timeZoneId by remember { mutableStateOf<String>(ZoneId.systemDefault().id) }
|
||||
var supportVEVENT by remember { mutableStateOf(true) }
|
||||
var supportVTODO by remember { mutableStateOf(false) }
|
||||
var supportVJOURNAL by remember { mutableStateOf(false) }
|
||||
|
||||
binding.color.setOnClickListener {
|
||||
ColorPickerDialog.newBuilder()
|
||||
.setShowAlphaSlider(false)
|
||||
.setColor((binding.color.background as ColorDrawable).color)
|
||||
.show(this)
|
||||
}
|
||||
|
||||
val homeSetAdapter = HomeSetAdapter(this)
|
||||
model.homeSets.observe(this) { homeSets ->
|
||||
homeSetAdapter.clear()
|
||||
if (homeSets.isNotEmpty()) {
|
||||
homeSetAdapter.addAll(homeSets)
|
||||
val firstHomeSet = homeSets.first()
|
||||
binding.homeset.setText(firstHomeSet.url.toString(), false)
|
||||
model.homeSet = firstHomeSet
|
||||
}
|
||||
}
|
||||
binding.homeset.setAdapter(homeSetAdapter)
|
||||
binding.homeset.setOnItemClickListener { parent, _, position, _ ->
|
||||
model.homeSet = parent.getItemAtPosition(position) as HomeSet?
|
||||
}
|
||||
|
||||
binding.timezone.setAdapter(TimeZoneAdapter(this))
|
||||
binding.timezone.setText(TimeZone.getDefault().id, false)
|
||||
}
|
||||
|
||||
override fun onColorSelected(dialogId: Int, rgb: Int) {
|
||||
model.color.value = rgb
|
||||
}
|
||||
|
||||
override fun onDialogDismissed(dialogId: Int) {
|
||||
// color selection dismissed
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_create_collection, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) =
|
||||
if (item.itemId == android.R.id.home) {
|
||||
val intent = Intent(this, AccountActivity::class.java)
|
||||
intent.putExtra(AccountActivity.EXTRA_ACCOUNT, model.account)
|
||||
NavUtils.navigateUpTo(this, intent)
|
||||
true
|
||||
} else
|
||||
false
|
||||
|
||||
fun onCreateCollection(item: MenuItem) {
|
||||
var ok = true
|
||||
|
||||
val args = Bundle()
|
||||
args.putString(CreateCollectionFragment.ARG_SERVICE_TYPE, Service.TYPE_CALDAV)
|
||||
|
||||
val parent = model.homeSet
|
||||
if (parent != null) {
|
||||
binding.homesetLayout.error = null
|
||||
args.putString(
|
||||
CreateCollectionFragment.ARG_URL,
|
||||
parent.url.resolve(UUID.randomUUID().toString() + "/").toString()
|
||||
)
|
||||
} else {
|
||||
binding.homesetLayout.error = getString(R.string.create_collection_home_set_required)
|
||||
ok = false
|
||||
}
|
||||
|
||||
val displayName = model.displayName.value
|
||||
if (displayName.isNullOrBlank()) {
|
||||
model.displayNameError.value = getString(R.string.create_collection_display_name_required)
|
||||
ok = false
|
||||
} else {
|
||||
args.putString(CreateCollectionFragment.ARG_DISPLAY_NAME, displayName)
|
||||
model.displayNameError.value = null
|
||||
}
|
||||
|
||||
StringUtils.trimToNull(model.description.value)?.let {
|
||||
args.putString(CreateCollectionFragment.ARG_DESCRIPTION, it)
|
||||
}
|
||||
|
||||
model.color.value?.let {
|
||||
args.putInt(CreateCollectionFragment.ARG_COLOR, it)
|
||||
}
|
||||
|
||||
val tzId = binding.timezone.text?.toString()
|
||||
if (tzId.isNullOrBlank())
|
||||
ok = false
|
||||
else {
|
||||
DateUtils.ical4jTimeZone(tzId)?.let { tz ->
|
||||
val cal = Calendar()
|
||||
cal.components += tz.vTimeZone
|
||||
args.putString(CreateCollectionFragment.ARG_TIMEZONE, cal.toString())
|
||||
}
|
||||
model.timezoneError.value = null
|
||||
}
|
||||
|
||||
val supportsVEVENT = model.supportVEVENT.value ?: false
|
||||
val supportsVTODO = model.supportVTODO.value ?: false
|
||||
val supportsVJOURNAL = model.supportVJOURNAL.value ?: false
|
||||
if (!supportsVEVENT && !supportsVTODO && !supportsVJOURNAL) {
|
||||
ok = false
|
||||
model.typeError.value = ""
|
||||
} else
|
||||
model.typeError.value = null
|
||||
|
||||
if (supportsVEVENT || supportsVTODO || supportsVJOURNAL) {
|
||||
// only if there's at least one component set not supported; don't include
|
||||
// information about supported components otherwise (means: everything supported)
|
||||
args.putBoolean(CreateCollectionFragment.ARG_SUPPORTS_VEVENT, supportsVEVENT)
|
||||
args.putBoolean(CreateCollectionFragment.ARG_SUPPORTS_VTODO, supportsVTODO)
|
||||
args.putBoolean(CreateCollectionFragment.ARG_SUPPORTS_VJOURNAL, supportsVJOURNAL)
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
args.putParcelable(CreateCollectionFragment.ARG_ACCOUNT, model.account)
|
||||
args.putString(CreateCollectionFragment.ARG_TYPE, Collection.TYPE_CALENDAR)
|
||||
val frag = CreateCollectionFragment()
|
||||
frag.arguments = args
|
||||
frag.show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TimeZoneAdapter(context: Context): ArrayAdapter<String>(context, R.layout.text_list_item, android.R.id.text1) {
|
||||
|
||||
init {
|
||||
addAll(TimeZone.getAvailableIDs().toList())
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val tzId = getItem(position)!!
|
||||
val tz = ZoneId.of(tzId)
|
||||
|
||||
val v: View = convertView ?: LayoutInflater.from(context).inflate(R.layout.text_list_item, parent, false)
|
||||
v.findViewById<TextView>(android.R.id.text1).text = tz.id
|
||||
v.findViewById<TextView>(android.R.id.text2).text = tz.getDisplayName(TextStyle.FULL, Locale.getDefault())
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup) =
|
||||
getView(position, convertView, parent)
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Model @AssistedInject constructor(
|
||||
val db: AppDatabase,
|
||||
@Assisted val account: Account
|
||||
): ViewModel() {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(account: Account): Model
|
||||
}
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
val displayNameError = MutableLiveData<String>()
|
||||
|
||||
val description = MutableLiveData<String>()
|
||||
val color = MutableLiveData<Int>()
|
||||
|
||||
val homeSets = MutableLiveData<List<HomeSet>>()
|
||||
var homeSet: HomeSet? = null
|
||||
|
||||
val timezoneError = MutableLiveData<String>()
|
||||
|
||||
val typeError = MutableLiveData<String>()
|
||||
val supportVEVENT = MutableLiveData<Boolean>()
|
||||
val supportVTODO = MutableLiveData<Boolean>()
|
||||
val supportVJOURNAL = MutableLiveData<Boolean>()
|
||||
|
||||
init {
|
||||
color.value = Constants.DAVDROID_GREEN_RGBA
|
||||
|
||||
supportVEVENT.value = true
|
||||
supportVTODO.value = true
|
||||
supportVJOURNAL.value = true
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// load account info
|
||||
db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)?.let { service ->
|
||||
homeSets.postValue(db.homeSetDao().getBindableByService(service.id))
|
||||
var isCreating by remember { mutableStateOf(false) }
|
||||
accountModel.createCollectionResult.observeAsState().value?.let { result ->
|
||||
if (result.isEmpty)
|
||||
finish()
|
||||
else
|
||||
ExceptionInfoDialog(
|
||||
exception = result.get(),
|
||||
onDismiss = {
|
||||
isCreating = false
|
||||
accountModel.createCollectionResult.value = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val onCreateCollection = {
|
||||
if (!isCreating) {
|
||||
isCreating = true
|
||||
homeSet?.let { homeSet ->
|
||||
accountModel.createCollection(
|
||||
homeSet = homeSet,
|
||||
addressBook = false,
|
||||
name = UUID.randomUUID().toString(),
|
||||
displayName = StringUtils.trimToNull(displayName),
|
||||
description = StringUtils.trimToNull(description),
|
||||
color = color,
|
||||
timeZoneId = timeZoneId,
|
||||
supportsVEVENT = supportVEVENT,
|
||||
supportsVTODO = supportVTODO,
|
||||
supportsVJOURNAL = supportVJOURNAL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val homeSets by accountModel.bindableCalendarHomesets.observeAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.create_calendar)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onSupportNavigateUp() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.navigate_up))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
val isCreateEnabled = !isCreating && displayName.isNotBlank() && homeSet != null
|
||||
IconButton(
|
||||
enabled = isCreateEnabled,
|
||||
onClick = { onCreateCollection() }
|
||||
) {
|
||||
Text(stringResource(R.string.create_collection_create).uppercase())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
if (isCreating)
|
||||
LinearProgressIndicator(
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
homeSets?.let { homeSets ->
|
||||
CalendarForm(
|
||||
displayName = displayName,
|
||||
onDisplayNameChange = { displayName = it },
|
||||
color = color,
|
||||
onColorChange = { color = it },
|
||||
description = description,
|
||||
onDescriptionChange = { description = it },
|
||||
timeZoneId = timeZoneId,
|
||||
onTimeZoneSelected = { timeZoneId = it },
|
||||
supportVEVENT = supportVEVENT,
|
||||
onSupportVEVENTChange = { supportVEVENT = it },
|
||||
supportVTODO = supportVTODO,
|
||||
onSupportVTODOChange = { supportVTODO = it },
|
||||
supportVJOURNAL = supportVJOURNAL,
|
||||
onSupportVJOURNALChange = { supportVJOURNAL = it },
|
||||
homeSet = homeSet,
|
||||
homeSets = homeSets,
|
||||
onHomeSetSelected = { homeSet = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun supportShouldUpRecreateTask(targetIntent: Intent) = true
|
||||
|
||||
override fun onPrepareSupportNavigateUpTaskStack(builder: TaskStackBuilder) {
|
||||
builder.editIntentAt(builder.intentCount - 1)?.putExtra(AccountActivity.EXTRA_ACCOUNT, accountModel.account)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun CalendarForm(
|
||||
displayName: String,
|
||||
onDisplayNameChange: (String) -> Unit = {},
|
||||
color: Int,
|
||||
onColorChange: (Int) -> Unit = {},
|
||||
description: String,
|
||||
onDescriptionChange: (String) -> Unit = {},
|
||||
timeZoneId: String,
|
||||
onTimeZoneSelected: (String) -> Unit = {},
|
||||
supportVEVENT: Boolean,
|
||||
onSupportVEVENTChange: (Boolean) -> Unit = {},
|
||||
supportVTODO: Boolean,
|
||||
onSupportVTODOChange: (Boolean) -> Unit = {},
|
||||
supportVJOURNAL: Boolean,
|
||||
onSupportVJOURNALChange: (Boolean) -> Unit = {},
|
||||
homeSet: HomeSet?,
|
||||
homeSets: List<HomeSet>,
|
||||
onHomeSetSelected: (HomeSet) -> Unit = {}
|
||||
) {
|
||||
Column(Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
OutlinedTextField(
|
||||
value = displayName,
|
||||
onValueChange = onDisplayNameChange,
|
||||
label = { Text(stringResource(R.string.create_collection_display_name)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
var showColorPicker by remember { mutableStateOf(false) }
|
||||
Box(Modifier
|
||||
.background(color = Color(color), shape = CircleShape)
|
||||
.clickable {
|
||||
showColorPicker = true
|
||||
}
|
||||
.size(32.dp)
|
||||
)
|
||||
if (showColorPicker) {
|
||||
CalendarColorPickerDialog(
|
||||
onSelectColor = {
|
||||
onColorChange(it)
|
||||
showColorPicker = false
|
||||
},
|
||||
onDismiss = { showColorPicker = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = onDescriptionChange,
|
||||
label = { Text(stringResource(R.string.create_collection_description_optional)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(R.string.create_calendar_time_zone),
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
Text(
|
||||
ZoneId.of(timeZoneId).getDisplayName(TextStyle.FULL_STANDALONE, Locale.getDefault()),
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
}
|
||||
|
||||
var showTimeZoneDialog by remember { mutableStateOf(false) }
|
||||
TextButton(
|
||||
enabled =
|
||||
if (LocalInspectionMode.current)
|
||||
true
|
||||
else
|
||||
model.timeZoneDefs.observeAsState().value != null,
|
||||
onClick = { showTimeZoneDialog = true }
|
||||
) {
|
||||
Text("Select timezone".uppercase())
|
||||
}
|
||||
if (showTimeZoneDialog) {
|
||||
model.timeZoneDefs.observeAsState().value?.let { timeZoneDefs ->
|
||||
MultipleChoiceInputDialog(
|
||||
title = "Select timezone",
|
||||
namesAndValues = timeZoneDefs,
|
||||
initialValue = timeZoneId,
|
||||
onValueSelected = {
|
||||
onTimeZoneSelected(it)
|
||||
showTimeZoneDialog = false
|
||||
},
|
||||
onDismiss = { showTimeZoneDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.create_calendar_type),
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
CheckboxRow(
|
||||
labelId = R.string.create_calendar_type_vevent,
|
||||
checked = supportVEVENT,
|
||||
onCheckedChange = onSupportVEVENTChange
|
||||
)
|
||||
CheckboxRow(
|
||||
labelId = R.string.create_calendar_type_vtodo,
|
||||
checked = supportVTODO,
|
||||
onCheckedChange = onSupportVTODOChange
|
||||
)
|
||||
CheckboxRow(
|
||||
labelId = R.string.create_calendar_type_vjournal,
|
||||
checked = supportVJOURNAL,
|
||||
onCheckedChange = onSupportVJOURNALChange
|
||||
)
|
||||
|
||||
HomeSetSelection(
|
||||
homeSet = homeSet,
|
||||
homeSets = homeSets,
|
||||
onHomeSetSelected = onHomeSetSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckboxRow(
|
||||
@StringRes labelId: Int,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
Text(
|
||||
text = stringResource(labelId),
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier
|
||||
.clickable { onCheckedChange(!checked) }
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun CalendarForm_Preview() {
|
||||
CalendarForm(
|
||||
displayName = "My Calendar",
|
||||
color = Color.Magenta.toArgb(),
|
||||
description = "This is my calendar",
|
||||
timeZoneId = "Europe/Vienna",
|
||||
supportVEVENT = true,
|
||||
supportVTODO = false,
|
||||
supportVJOURNAL = false,
|
||||
homeSet = null,
|
||||
homeSets = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class Model @Inject constructor() : ViewModel() {
|
||||
|
||||
/**
|
||||
* List of available time zones as <display name, ID> pairs.
|
||||
*/
|
||||
val timeZoneDefs = MutableLiveData<List<Pair<String, String>>>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val timeZones = mutableListOf<Pair<String, String>>()
|
||||
|
||||
// iterate over Android time zones and take those with ical4j VTIMEZONE into consideration
|
||||
val locale = Locale.getDefault()
|
||||
for (id in ZoneId.getAvailableZoneIds()) {
|
||||
timeZones += Pair(
|
||||
ZoneId.of(id).getDisplayName(TextStyle.FULL, locale),
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
val collator = Collator.getInstance()
|
||||
timeZoneDefs.postValue(timeZones.sortedBy { collator.getCollationKey(it.first) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
|
||||
@Composable
|
||||
fun HomeSetSelection(
|
||||
homeSet: HomeSet?,
|
||||
homeSets: List<HomeSet>,
|
||||
onHomeSetSelected: (HomeSet) -> Unit
|
||||
) {
|
||||
// select first home set if none is selected
|
||||
LaunchedEffect(homeSets) {
|
||||
if (homeSet == null)
|
||||
homeSets.firstOrNull()?.let(onHomeSetSelected)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.create_collection_home_set),
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp, bottom = 8.dp)
|
||||
)
|
||||
for (item in homeSets) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = homeSet == item,
|
||||
onClick = { onHomeSetSelected(item) }
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.clickable { onHomeSetSelected(item) }
|
||||
.weight(1f)) {
|
||||
Text(
|
||||
text = item.displayName ?: DavUtils.lastSegmentOfUrl(item.url),
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
Text(
|
||||
text = item.url.encodedPath,
|
||||
style = MaterialTheme.typography.caption.copy(fontFamily = FontFamily.Monospace)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,269 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.ui.account
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.XmlUtils
|
||||
import at.bitfire.dav4jvm.property.caldav.NS_APPLE_ICAL
|
||||
import at.bitfire.dav4jvm.property.caldav.NS_CALDAV
|
||||
import at.bitfire.dav4jvm.property.carddav.NS_CARDDAV
|
||||
import at.bitfire.dav4jvm.property.webdav.NS_WEBDAV
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.ExceptionInfoFragment
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.io.IOException
|
||||
import java.io.StringWriter
|
||||
import java.util.logging.Level
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CreateCollectionFragment: DialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val ARG_ACCOUNT = "account"
|
||||
const val ARG_SERVICE_TYPE = "serviceType"
|
||||
|
||||
const val ARG_TYPE = "type"
|
||||
const val ARG_URL = "url"
|
||||
const val ARG_DISPLAY_NAME = "displayName"
|
||||
const val ARG_DESCRIPTION = "description"
|
||||
|
||||
// CalDAV only
|
||||
const val ARG_COLOR = "color"
|
||||
const val ARG_TIMEZONE = "timezone"
|
||||
const val ARG_SUPPORTS_VEVENT = "supportsVEVENT"
|
||||
const val ARG_SUPPORTS_VTODO = "supportsVTODO"
|
||||
const val ARG_SUPPORTS_VJOURNAL = "supportsVJOURNAL"
|
||||
}
|
||||
|
||||
@Inject lateinit var modelFactory: Model.Factory
|
||||
val model by viewModels<Model> {
|
||||
object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
val args = requireArguments()
|
||||
|
||||
val account: Account = args.getParcelable(ARG_ACCOUNT) ?: throw IllegalArgumentException("ARG_ACCOUNT required")
|
||||
val serviceType = args.getString(ARG_SERVICE_TYPE) ?: throw java.lang.IllegalArgumentException("ARG_SERVICE_TYPE required")
|
||||
val collection = Collection(
|
||||
type = args.getString(ARG_TYPE) ?: throw IllegalArgumentException("ARG_TYPE required"),
|
||||
url = (args.getString(ARG_URL) ?: throw IllegalArgumentException("ARG_URL required")).toHttpUrl(),
|
||||
displayName = args.getString(ARG_DISPLAY_NAME),
|
||||
description = args.getString(ARG_DESCRIPTION),
|
||||
|
||||
color = args.ifDefined(ARG_COLOR) { it.getInt(ARG_COLOR) },
|
||||
timezone = args.getString(ARG_TIMEZONE),
|
||||
supportsVEVENT = args.ifDefined(ARG_SUPPORTS_VEVENT) { it.getBoolean(ARG_SUPPORTS_VEVENT) },
|
||||
supportsVTODO = args.ifDefined(ARG_SUPPORTS_VTODO) { it.getBoolean(ARG_SUPPORTS_VTODO) },
|
||||
supportsVJOURNAL = args.ifDefined(ARG_SUPPORTS_VJOURNAL) { it.getBoolean(ARG_SUPPORTS_VJOURNAL) },
|
||||
|
||||
sync = true /* by default, sync collections which just have been created */
|
||||
)
|
||||
|
||||
return modelFactory.create(account, serviceType, collection) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
model.createCollection().observe(this, { exception ->
|
||||
if (exception == null)
|
||||
requireActivity().finish()
|
||||
else {
|
||||
dismiss()
|
||||
parentFragmentManager.beginTransaction()
|
||||
.add(ExceptionInfoFragment.newInstance(exception, model.account), null)
|
||||
.commit()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun<T> Bundle.ifDefined(name: String, getter: (Bundle) -> T): T? =
|
||||
if (containsKey(name))
|
||||
getter(this)
|
||||
else
|
||||
null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val v = inflater.inflate(R.layout.create_collection, container, false)
|
||||
isCancelable = false
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
class Model @AssistedInject constructor(
|
||||
application: Application,
|
||||
val db: AppDatabase,
|
||||
@Assisted val account: Account,
|
||||
@Assisted val serviceType: String,
|
||||
@Assisted val collection: Collection
|
||||
): AndroidViewModel(application) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(account: Account, serviceType: String, collection: Collection): Model
|
||||
}
|
||||
|
||||
val result = MutableLiveData<Exception>()
|
||||
|
||||
fun createCollection(): LiveData<Exception> {
|
||||
viewModelScope.launch(Dispatchers.IO + NonCancellable) {
|
||||
HttpClient.Builder(getApplication(), AccountSettings(getApplication(), account))
|
||||
.setForeground(true)
|
||||
.build().use { httpClient ->
|
||||
try {
|
||||
val dav = DavResource(httpClient.okHttpClient, collection.url)
|
||||
|
||||
// create collection on remote server
|
||||
dav.mkCol(generateXml()) {}
|
||||
|
||||
// no HTTP error -> create collection locally
|
||||
db.serviceDao().getByAccountAndType(account.name, serviceType)?.let { service ->
|
||||
collection.serviceId = service.id
|
||||
db.collectionDao().insert(collection)
|
||||
|
||||
// trigger service detection (because the collection may have other properties than the ones we have inserted)
|
||||
RefreshCollectionsWorker.enqueue(getApplication(), service.id)
|
||||
}
|
||||
|
||||
// post success
|
||||
result.postValue(null)
|
||||
} catch (e: Exception) {
|
||||
// post error
|
||||
result.postValue(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun generateXml(): String {
|
||||
val writer = StringWriter()
|
||||
try {
|
||||
val serializer = XmlUtils.newSerializer()
|
||||
with(serializer) {
|
||||
setOutput(writer)
|
||||
startDocument("UTF-8", null)
|
||||
setPrefix("", NS_WEBDAV)
|
||||
setPrefix("CAL", NS_CALDAV)
|
||||
setPrefix("CARD", NS_CARDDAV)
|
||||
|
||||
startTag(NS_WEBDAV, "mkcol")
|
||||
startTag(NS_WEBDAV, "set")
|
||||
startTag(NS_WEBDAV, "prop")
|
||||
startTag(NS_WEBDAV, "resourcetype")
|
||||
startTag(NS_WEBDAV, "collection")
|
||||
endTag(NS_WEBDAV, "collection")
|
||||
if (collection.type == Collection.TYPE_ADDRESSBOOK) {
|
||||
startTag(NS_CARDDAV, "addressbook")
|
||||
endTag(NS_CARDDAV, "addressbook")
|
||||
} else if (collection.type == Collection.TYPE_CALENDAR) {
|
||||
startTag(NS_CALDAV, "calendar")
|
||||
endTag(NS_CALDAV, "calendar")
|
||||
}
|
||||
endTag(NS_WEBDAV, "resourcetype")
|
||||
collection.displayName?.let {
|
||||
startTag(NS_WEBDAV, "displayname")
|
||||
text(it)
|
||||
endTag(NS_WEBDAV, "displayname")
|
||||
}
|
||||
|
||||
// addressbook-specific properties
|
||||
if (collection.type == Collection.TYPE_ADDRESSBOOK) {
|
||||
collection.description?.let {
|
||||
startTag(NS_CARDDAV, "addressbook-description")
|
||||
text(it)
|
||||
endTag(NS_CARDDAV, "addressbook-description")
|
||||
}
|
||||
}
|
||||
|
||||
// calendar-specific properties
|
||||
if (collection.type == Collection.TYPE_CALENDAR) {
|
||||
collection.description?.let {
|
||||
startTag(NS_CALDAV, "calendar-description")
|
||||
text(it)
|
||||
endTag(NS_CALDAV, "calendar-description")
|
||||
}
|
||||
|
||||
collection.color?.let {
|
||||
startTag(NS_APPLE_ICAL, "calendar-color")
|
||||
text(DavUtils.ARGBtoCalDAVColor(it))
|
||||
endTag(NS_APPLE_ICAL, "calendar-color")
|
||||
}
|
||||
|
||||
collection.timezone?.let {
|
||||
startTag(NS_CALDAV, "calendar-timezone")
|
||||
cdsect(it)
|
||||
endTag(NS_CALDAV, "calendar-timezone")
|
||||
}
|
||||
|
||||
if (collection.supportsVEVENT != null || collection.supportsVTODO != null || collection.supportsVJOURNAL != null) {
|
||||
// only if there's at least one explicitly supported calendar component set, otherwise don't include the property
|
||||
startTag(NS_CALDAV, "supported-calendar-component-set")
|
||||
if (collection.supportsVEVENT != false) {
|
||||
startTag(NS_CALDAV, "comp")
|
||||
attribute(null, "name", "VEVENT")
|
||||
endTag(NS_CALDAV, "comp")
|
||||
}
|
||||
if (collection.supportsVTODO != false) {
|
||||
startTag(NS_CALDAV, "comp")
|
||||
attribute(null, "name", "VTODO")
|
||||
endTag(NS_CALDAV, "comp")
|
||||
}
|
||||
if (collection.supportsVJOURNAL != false) {
|
||||
startTag(NS_CALDAV, "comp")
|
||||
attribute(null, "name", "VJOURNAL")
|
||||
endTag(NS_CALDAV, "comp")
|
||||
}
|
||||
endTag(NS_CALDAV, "supported-calendar-component-set")
|
||||
}
|
||||
}
|
||||
|
||||
endTag(NS_WEBDAV, "prop")
|
||||
endTag(NS_WEBDAV, "set")
|
||||
endTag(NS_WEBDAV, "mkcol")
|
||||
endDocument()
|
||||
}
|
||||
} catch(e: IOException) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't assemble Extended MKCOL request", e)
|
||||
}
|
||||
|
||||
return writer.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -27,7 +27,7 @@ import androidx.compose.ui.window.DialogProperties
|
|||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.ui.ExceptionInfoDialog
|
||||
import at.bitfire.davdroid.ui.widget.ExceptionInfoDialog
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.util.Optional
|
||||
|
||||
|
@ -101,7 +101,7 @@ fun DeleteCollectionDialog_Content(
|
|||
ExceptionInfoDialog(
|
||||
exception = exception,
|
||||
remoteResource = collection.url,
|
||||
onDismissRequest = onCancel
|
||||
onDismiss = onCancel
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ fun RenameAccountDialog(
|
|||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
var accountName by remember { mutableStateOf(TextFieldValue(oldName, selection = TextRange(oldName.length))) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
|
@ -43,6 +42,8 @@ fun RenameAccountDialog(
|
|||
stringResource(R.string.account_rename_new_name_description),
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
TextField(
|
||||
value = accountName,
|
||||
onValueChange = { accountName = it },
|
||||
|
@ -50,15 +51,18 @@ fun RenameAccountDialog(
|
|||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Go
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onGo = {
|
||||
onDone = {
|
||||
onRenameAccount(accountName.text)
|
||||
}
|
||||
),
|
||||
modifier = Modifier.focusRequester(focusRequester)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
|
@ -74,17 +78,8 @@ fun RenameAccountDialog(
|
|||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.cancel).uppercase())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// request focus on the first composition
|
||||
var requestFocus = remember { true }
|
||||
LaunchedEffect(requestFocus) {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
requestFocus = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package at.bitfire.davdroid.ui.widget
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import at.bitfire.ical4android.Css3Color
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@Preview
|
||||
fun CalendarColorPickerDialog(
|
||||
onSelectColor: (color: Int) -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
val colors = remember {
|
||||
Css3Color.entries.sortedBy { css3Color ->
|
||||
Color(css3Color.argb).luminance()
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(Modifier.verticalScroll(rememberScrollState())) {
|
||||
FlowRow(Modifier.padding(8.dp)) {
|
||||
for (color in colors) {
|
||||
Box(Modifier.padding(2.dp)) {
|
||||
Box(Modifier
|
||||
.background(color = Color(color.argb), shape = CircleShape)
|
||||
.clickable { onSelectColor(color.argb) }
|
||||
.size(32.dp)
|
||||
.padding(8.dp)
|
||||
.semantics {
|
||||
contentDescription = color.name
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,11 +2,9 @@
|
|||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
package at.bitfire.davdroid.ui.widget
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
@ -20,66 +18,21 @@ import androidx.compose.material.icons.rounded.Error
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.davdroid.R
|
||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
import okhttp3.HttpUrl
|
||||
import java.io.IOException
|
||||
|
||||
class ExceptionInfoFragment: DialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val ARG_ACCOUNT = "account"
|
||||
const val ARG_EXCEPTION = "exception"
|
||||
|
||||
fun newInstance(exception: Exception, account: Account?): ExceptionInfoFragment {
|
||||
val frag = ExceptionInfoFragment()
|
||||
val args = Bundle(2)
|
||||
args.putSerializable(ARG_EXCEPTION, exception)
|
||||
args.putParcelable(ARG_ACCOUNT, account)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val args = requireNotNull(arguments)
|
||||
val exception = args.getSerializable(ARG_EXCEPTION) as Exception
|
||||
val account: Account? = BundleCompat.getParcelable(args, ARG_ACCOUNT, Account::class.java)
|
||||
|
||||
val dialog = Dialog(requireContext()).apply {
|
||||
setContentView(
|
||||
ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
AppTheme {
|
||||
ExceptionInfoDialog(
|
||||
exception = exception,
|
||||
account = account
|
||||
) { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
isCancelable = false
|
||||
return dialog
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExceptionInfoDialog(
|
||||
exception: Throwable,
|
||||
account: Account? = null,
|
||||
remoteResource: HttpUrl? = null,
|
||||
onDismissRequest: () -> Unit
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
|
@ -90,7 +43,7 @@ fun ExceptionInfoDialog(
|
|||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
@ -124,7 +77,7 @@ fun ExceptionInfoDialog(
|
|||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.ok).uppercase())
|
||||
}
|
||||
}
|
|
@ -3,11 +3,13 @@ package at.bitfire.davdroid.ui.widget
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Text
|
||||
|
@ -29,6 +31,8 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
|
||||
@Composable
|
||||
fun EditTextInputDialog(
|
||||
|
@ -43,7 +47,6 @@ fun EditTextInputDialog(
|
|||
initialValue ?: "", selection = TextRange(initialValue?.length ?: 0)
|
||||
))
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
|
@ -54,6 +57,7 @@ fun EditTextInputDialog(
|
|||
)
|
||||
},
|
||||
text = {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
TextField(
|
||||
value = textValue,
|
||||
onValueChange = { textValue = it },
|
||||
|
@ -70,6 +74,9 @@ fun EditTextInputDialog(
|
|||
),
|
||||
modifier = Modifier.focusRequester(focusRequester)
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
|
@ -90,14 +97,6 @@ fun EditTextInputDialog(
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
var requestFocus = remember { true }
|
||||
LaunchedEffect(requestFocus) {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
requestFocus = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -118,38 +117,51 @@ fun MultipleChoiceInputDialog(
|
|||
onValueSelected: (String) -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
for ((name, value) in namesAndValues)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = value == initialValue,
|
||||
onClick = {
|
||||
onValueSelected(value)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
Text(
|
||||
name,
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier.clickable {
|
||||
onValueSelected(value)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card {
|
||||
Column {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
LazyColumn(Modifier.padding(8.dp)) {
|
||||
items(
|
||||
count = namesAndValues.size,
|
||||
key = { index -> namesAndValues[index].second },
|
||||
itemContent = { index ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val (name, value) = namesAndValues[index]
|
||||
RadioButton(
|
||||
selected = value == initialValue,
|
||||
onClick = {
|
||||
onValueSelected(value)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
Text(
|
||||
name,
|
||||
style = MaterialTheme.typography.body1,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
onValueSelected(value)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
buttons = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".ui.account.CreateCalendarActivity">
|
||||
|
||||
<data>
|
||||
<import type="android.view.View"/>
|
||||
<variable
|
||||
name="model"
|
||||
type="at.bitfire.davdroid.ui.account.CreateCalendarActivity.Model"/>
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/activity_margin">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/display_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/create_collection_display_name"
|
||||
app:error="@{model.displayNameError}"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/color">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@={model.displayName}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/color"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:background="@{model.color}"
|
||||
android:contentDescription="@string/create_collection_color"
|
||||
app:layout_constraintStart_toEndOf="@id/display_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/display_name"
|
||||
app:layout_constraintBottom_toBottomOf="@id/display_name"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/create_collection_description"
|
||||
app:helperText="@string/create_collection_optional"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/display_name">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@={model.description}" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/homeset_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/description"
|
||||
android:layout_marginTop="8dp"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:hint="@string/create_collection_home_set">
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/homeset"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/timezone_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/homeset_layout"
|
||||
android:layout_marginTop="8dp"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:hint="@string/create_calendar_time_zone"
|
||||
app:error="@{model.timezoneError}">
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/timezone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/create_calendar_type"
|
||||
app:error="@{model.typeError}"
|
||||
app:layout_constraintTop_toBottomOf="@id/timezone_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:flexWrap="wrap"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/type">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/support_vevent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:checked="@={model.supportVEVENT}"
|
||||
android:text="@string/create_calendar_type_vevent"/>
|
||||
<CheckBox
|
||||
android:id="@+id/support_vtodo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:checked="@={model.supportVTODO}"
|
||||
android:text="@string/create_calendar_type_vtodo"/>
|
||||
<CheckBox
|
||||
android:id="@+id/support_vjournal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:checked="@={model.supportVJOURNAL}"
|
||||
android:text="@string/create_calendar_type_vjournal"/>
|
||||
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</layout>
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:id="@+id/create"
|
||||
android:title="@string/create_collection_create"
|
||||
android:onClick="onCreateCollection"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
</menu>
|
|
@ -392,7 +392,7 @@
|
|||
<!-- collection management -->
|
||||
<string name="create_addressbook">Create address book</string>
|
||||
<string name="create_calendar">Create calendar</string>
|
||||
<string name="create_calendar_time_zone">Time zone</string>
|
||||
<string name="create_calendar_time_zone">Default time zone</string>
|
||||
<string name="create_calendar_type">Possible calendar entries</string>
|
||||
<string name="create_calendar_type_vevent">Events</string>
|
||||
<string name="create_calendar_type_vtodo">Tasks</string>
|
||||
|
|
|
@ -106,7 +106,6 @@ dnsjava = { module = "dnsjava:dnsjava", version.ref = "dnsjava" }
|
|||
hilt-android-base = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
|
||||
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
|
||||
jaredrummler-colorpicker = { module = "com.jaredrummler:colorpicker", version.ref = "jaredrummler-colorpicker" }
|
||||
junit = { module = "junit:junit", version = "4.13.2" }
|
||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||
|
|
Loading…
Reference in a new issue