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)
|
implementation(libs.commons.text)
|
||||||
@Suppress("RedundantSuppression")
|
@Suppress("RedundantSuppression")
|
||||||
implementation(libs.dnsjava)
|
implementation(libs.dnsjava)
|
||||||
implementation(libs.jaredrummler.colorpicker)
|
|
||||||
implementation(libs.mikepenz.aboutLibraries)
|
implementation(libs.mikepenz.aboutLibraries)
|
||||||
implementation(libs.nsk90.kstatemachine)
|
implementation(libs.nsk90.kstatemachine)
|
||||||
implementation(libs.okhttp.base)
|
implementation(libs.okhttp.base)
|
||||||
|
|
|
@ -150,7 +150,7 @@
|
||||||
android:parentActivityName=".ui.account.AccountActivity" />
|
android:parentActivityName=".ui.account.AccountActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.account.CreateCalendarActivity"
|
android:name=".ui.account.CreateCalendarActivity"
|
||||||
android:label="@string/create_calendar"
|
android:theme="@style/AppTheme.NoActionBar"
|
||||||
android:parentActivityName=".ui.account.AccountActivity" />
|
android:parentActivityName=".ui.account.AccountActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.account.AccountSettingsActivity"
|
android:name=".ui.account.AccountSettingsActivity"
|
||||||
|
|
|
@ -27,8 +27,8 @@ interface HomeSetDao {
|
||||||
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind")
|
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind")
|
||||||
fun getBindableByService(serviceId: Long): List<HomeSet>
|
fun getBindableByService(serviceId: Long): List<HomeSet>
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM homeset WHERE serviceId=:serviceId AND privBind")
|
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind")
|
||||||
fun hasBindableByServiceLive(serviceId: Long): LiveData<Boolean>
|
fun getLiveBindableByService(serviceId: Long): LiveData<List<HomeSet>>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
fun insert(homeSet: HomeSet): Long
|
fun insert(homeSet: HomeSet): Long
|
||||||
|
|
|
@ -63,7 +63,6 @@ import at.bitfire.cert4android.CustomCertStore
|
||||||
import at.bitfire.davdroid.BuildConfig
|
import at.bitfire.davdroid.BuildConfig
|
||||||
import at.bitfire.davdroid.R
|
import at.bitfire.davdroid.R
|
||||||
import at.bitfire.davdroid.log.Logger
|
import at.bitfire.davdroid.log.Logger
|
||||||
import at.bitfire.davdroid.util.TaskUtils
|
|
||||||
import at.bitfire.davdroid.settings.Settings
|
import at.bitfire.davdroid.settings.Settings
|
||||||
import at.bitfire.davdroid.settings.SettingsManager
|
import at.bitfire.davdroid.settings.SettingsManager
|
||||||
import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage
|
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.SettingsHeader
|
||||||
import at.bitfire.davdroid.ui.widget.SwitchSetting
|
import at.bitfire.davdroid.ui.widget.SwitchSetting
|
||||||
import at.bitfire.davdroid.util.PermissionUtils
|
import at.bitfire.davdroid.util.PermissionUtils
|
||||||
|
import at.bitfire.davdroid.util.TaskUtils
|
||||||
import at.bitfire.ical4android.TaskProvider
|
import at.bitfire.ical4android.TaskProvider
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -109,9 +109,7 @@ class AppSettingsActivity: AppCompatActivity() {
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = { onSupportNavigateUp() }) {
|
||||||
onSupportNavigateUp()
|
|
||||||
}) {
|
|
||||||
Icon(Icons.AutoMirrored.Default.ArrowBack, stringResource(R.string.navigate_up))
|
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.MainThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.switchMap
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
@ -26,36 +26,46 @@ import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import at.bitfire.dav4jvm.DavResource
|
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.InvalidAccountException
|
||||||
import at.bitfire.davdroid.R
|
import at.bitfire.davdroid.R
|
||||||
import at.bitfire.davdroid.db.AppDatabase
|
import at.bitfire.davdroid.db.AppDatabase
|
||||||
import at.bitfire.davdroid.db.Collection
|
import at.bitfire.davdroid.db.Collection
|
||||||
|
import at.bitfire.davdroid.db.HomeSet
|
||||||
import at.bitfire.davdroid.db.Service
|
import at.bitfire.davdroid.db.Service
|
||||||
import at.bitfire.davdroid.log.Logger
|
import at.bitfire.davdroid.log.Logger
|
||||||
import at.bitfire.davdroid.network.HttpClient
|
import at.bitfire.davdroid.network.HttpClient
|
||||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||||
import at.bitfire.davdroid.resource.LocalTaskList
|
import at.bitfire.davdroid.resource.LocalTaskList
|
||||||
import at.bitfire.davdroid.util.TaskUtils
|
|
||||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||||
import at.bitfire.davdroid.settings.AccountSettings
|
import at.bitfire.davdroid.settings.AccountSettings
|
||||||
import at.bitfire.davdroid.syncadapter.AccountsCleanupWorker
|
import at.bitfire.davdroid.syncadapter.AccountsCleanupWorker
|
||||||
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
|
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
|
||||||
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
|
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
|
||||||
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
|
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.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import net.fortuna.ical4j.model.Calendar
|
||||||
|
import java.io.StringWriter
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
class AccountModel @AssistedInject constructor(
|
class AccountModel @AssistedInject constructor(
|
||||||
application: Application,
|
val context: Application,
|
||||||
val db: AppDatabase,
|
val db: AppDatabase,
|
||||||
@Assisted val account: Account
|
@Assisted val account: Account
|
||||||
): AndroidViewModel(application), OnAccountsUpdateListener {
|
): ViewModel(), OnAccountsUpdateListener {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
@ -69,7 +79,7 @@ class AccountModel @AssistedInject constructor(
|
||||||
/** whether the account is invalid and the AccountActivity shall be closed */
|
/** whether the account is invalid and the AccountActivity shall be closed */
|
||||||
val invalid = MutableLiveData<Boolean>()
|
val invalid = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
private val settings = AccountSettings(application, account)
|
private val settings = AccountSettings(context, account)
|
||||||
private val refreshSettingsSignal = MutableLiveData(Unit)
|
private val refreshSettingsSignal = MutableLiveData(Unit)
|
||||||
val showOnlyPersonal = refreshSettingsSignal.switchMap<Unit, AccountSettings.ShowOnlyPersonal> {
|
val showOnlyPersonal = refreshSettingsSignal.switchMap<Unit, AccountSettings.ShowOnlyPersonal> {
|
||||||
object : LiveData<AccountSettings.ShowOnlyPersonal>() {
|
object : LiveData<AccountSettings.ShowOnlyPersonal>() {
|
||||||
|
@ -85,23 +95,25 @@ class AccountModel @AssistedInject constructor(
|
||||||
refreshSettingsSignal.postValue(Unit)
|
refreshSettingsSignal.postValue(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = getApplication<Application>()
|
|
||||||
val accountManager: AccountManager = AccountManager.get(context)
|
val accountManager: AccountManager = AccountManager.get(context)
|
||||||
|
|
||||||
val cardDavSvc = db.serviceDao().getLiveByAccountAndType(account.name, Service.TYPE_CARDDAV)
|
val cardDavSvc = db.serviceDao().getLiveByAccountAndType(account.name, Service.TYPE_CARDDAV)
|
||||||
val canCreateAddressBook = cardDavSvc.switchMap { svc ->
|
val bindableAddressBookHomesets = cardDavSvc.switchMap { svc ->
|
||||||
if (svc != null)
|
if (svc != null)
|
||||||
db.homeSetDao().hasBindableByServiceLive(svc.id)
|
db.homeSetDao().getLiveBindableByService(svc.id)
|
||||||
else
|
else
|
||||||
MutableLiveData(false)
|
MutableLiveData(emptyList())
|
||||||
|
}
|
||||||
|
val canCreateAddressBook = bindableAddressBookHomesets.map { homeSets ->
|
||||||
|
homeSets.isNotEmpty()
|
||||||
}
|
}
|
||||||
val cardDavRefreshing = cardDavSvc.switchMap { svc ->
|
val cardDavRefreshing = cardDavSvc.switchMap { svc ->
|
||||||
if (svc == null)
|
if (svc == null)
|
||||||
return@switchMap null
|
return@switchMap null
|
||||||
RefreshCollectionsWorker.exists(application, RefreshCollectionsWorker.workerName(svc.id))
|
RefreshCollectionsWorker.exists(context, RefreshCollectionsWorker.workerName(svc.id))
|
||||||
}
|
}
|
||||||
val cardDavSyncPending = BaseSyncWorker.exists(
|
val cardDavSyncPending = BaseSyncWorker.exists(
|
||||||
getApplication(),
|
context,
|
||||||
listOf(WorkInfo.State.ENQUEUED),
|
listOf(WorkInfo.State.ENQUEUED),
|
||||||
account,
|
account,
|
||||||
listOf(context.getString(R.string.address_books_authority)),
|
listOf(context.getString(R.string.address_books_authority)),
|
||||||
|
@ -111,7 +123,7 @@ class AccountModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val cardDavSyncing = BaseSyncWorker.exists(
|
val cardDavSyncing = BaseSyncWorker.exists(
|
||||||
getApplication(),
|
context,
|
||||||
listOf(WorkInfo.State.RUNNING),
|
listOf(WorkInfo.State.RUNNING),
|
||||||
account,
|
account,
|
||||||
listOf(context.getString(R.string.address_books_authority))
|
listOf(context.getString(R.string.address_books_authority))
|
||||||
|
@ -120,20 +132,23 @@ class AccountModel @AssistedInject constructor(
|
||||||
|
|
||||||
private val tasksProvider = TaskUtils.currentProviderLive(context)
|
private val tasksProvider = TaskUtils.currentProviderLive(context)
|
||||||
val calDavSvc = db.serviceDao().getLiveByAccountAndType(account.name, Service.TYPE_CALDAV)
|
val calDavSvc = db.serviceDao().getLiveByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||||
val canCreateCalendar = calDavSvc.switchMap { svc ->
|
val bindableCalendarHomesets = calDavSvc.switchMap { svc ->
|
||||||
if (svc != null)
|
if (svc != null)
|
||||||
db.homeSetDao().hasBindableByServiceLive(svc.id)
|
db.homeSetDao().getLiveBindableByService(svc.id)
|
||||||
else
|
else
|
||||||
MutableLiveData(false)
|
MutableLiveData(emptyList())
|
||||||
|
}
|
||||||
|
val canCreateCalendar = bindableCalendarHomesets.map { homeSets ->
|
||||||
|
homeSets.isNotEmpty()
|
||||||
}
|
}
|
||||||
val calDavRefreshing = calDavSvc.switchMap { svc ->
|
val calDavRefreshing = calDavSvc.switchMap { svc ->
|
||||||
if (svc == null)
|
if (svc == null)
|
||||||
return@switchMap null
|
return@switchMap null
|
||||||
RefreshCollectionsWorker.exists(application, RefreshCollectionsWorker.workerName(svc.id))
|
RefreshCollectionsWorker.exists(context, RefreshCollectionsWorker.workerName(svc.id))
|
||||||
}
|
}
|
||||||
val calDavSyncPending = tasksProvider.switchMap { tasks ->
|
val calDavSyncPending = tasksProvider.switchMap { tasks ->
|
||||||
BaseSyncWorker.exists(
|
BaseSyncWorker.exists(
|
||||||
getApplication(),
|
context,
|
||||||
listOf(WorkInfo.State.ENQUEUED),
|
listOf(WorkInfo.State.ENQUEUED),
|
||||||
account,
|
account,
|
||||||
listOfNotNull(CalendarContract.AUTHORITY, tasks?.authority),
|
listOfNotNull(CalendarContract.AUTHORITY, tasks?.authority),
|
||||||
|
@ -145,7 +160,7 @@ class AccountModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
val calDavSyncing = tasksProvider.switchMap { tasks ->
|
val calDavSyncing = tasksProvider.switchMap { tasks ->
|
||||||
BaseSyncWorker.exists(
|
BaseSyncWorker.exists(
|
||||||
getApplication(),
|
context,
|
||||||
listOf(WorkInfo.State.RUNNING),
|
listOf(WorkInfo.State.RUNNING),
|
||||||
account,
|
account,
|
||||||
listOfNotNull(CalendarContract.AUTHORITY, tasks?.authority)
|
listOfNotNull(CalendarContract.AUTHORITY, tasks?.authority)
|
||||||
|
@ -155,7 +170,6 @@ class AccountModel @AssistedInject constructor(
|
||||||
val webcalPager = CollectionPager(db, calDavSvc, Collection.TYPE_WEBCAL, showOnlyPersonal)
|
val webcalPager = CollectionPager(db, calDavSvc, Collection.TYPE_WEBCAL, showOnlyPersonal)
|
||||||
|
|
||||||
val renameAccountError = MutableLiveData<String>()
|
val renameAccountError = MutableLiveData<String>()
|
||||||
val deleteCollectionResult = MutableLiveData<Optional<Exception>>()
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -252,7 +266,6 @@ class AccountModel @AssistedInject constructor(
|
||||||
fun onAccountRenamed(accountManager: AccountManager, oldAccount: Account, newName: String, syncIntervals: List<Pair<String, Long?>>) {
|
fun onAccountRenamed(accountManager: AccountManager, oldAccount: Account, newName: String, syncIntervals: List<Pair<String, Long?>>) {
|
||||||
// account has now been renamed
|
// account has now been renamed
|
||||||
Logger.log.info("Updating account name references")
|
Logger.log.info("Updating account name references")
|
||||||
val context: Application = getApplication()
|
|
||||||
|
|
||||||
// disable periodic workers of old account
|
// disable periodic workers of old account
|
||||||
syncIntervals.forEach { (authority, _) ->
|
syncIntervals.forEach { (authority, _) ->
|
||||||
|
@ -329,9 +342,192 @@ class AccountModel @AssistedInject constructor(
|
||||||
}, null)
|
}, 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. */
|
/** Deletes the given collection from the database and the server. */
|
||||||
fun deleteCollection(collection: Collection) = viewModelScope.launch(Dispatchers.IO) {
|
fun deleteCollection(collection: Collection) = viewModelScope.launch(Dispatchers.IO) {
|
||||||
HttpClient.Builder(getApplication(), AccountSettings(getApplication(), account))
|
HttpClient.Builder(context, AccountSettings(context, account))
|
||||||
.setForeground(true)
|
.setForeground(true)
|
||||||
.build().use { httpClient ->
|
.build().use { httpClient ->
|
||||||
try {
|
try {
|
||||||
|
@ -378,7 +574,7 @@ class AccountModel @AssistedInject constructor(
|
||||||
val interestingAuthorities = listOfNotNull(
|
val interestingAuthorities = listOfNotNull(
|
||||||
ContactsContract.AUTHORITY,
|
ContactsContract.AUTHORITY,
|
||||||
CalendarContract.AUTHORITY,
|
CalendarContract.AUTHORITY,
|
||||||
TaskUtils.currentProvider(getApplication())?.authority
|
TaskUtils.currentProvider(context)?.authority
|
||||||
)
|
)
|
||||||
val result = mutableMapOf<String, Long>()
|
val result = mutableMapOf<String, Long>()
|
||||||
for (authority in interestingAuthorities) {
|
for (authority in interestingAuthorities) {
|
||||||
|
@ -398,7 +594,7 @@ class AccountModel @AssistedInject constructor(
|
||||||
* @return the application name of authority (ie "jtx Board")
|
* @return the application name of authority (ie "jtx Board")
|
||||||
*/
|
*/
|
||||||
private fun getAppNameFromAuthority(authority: String): String {
|
private fun getAppNameFromAuthority(authority: String): String {
|
||||||
val packageManager = getApplication<Application>().packageManager
|
val packageManager = context.packageManager
|
||||||
val packageName = packageManager.resolveContentProvider(authority, 0)?.packageName ?: authority
|
val packageName = packageManager.resolveContentProvider(authority, 0)?.packageName ?: authority
|
||||||
return try {
|
return try {
|
||||||
val appInfo = packageManager.getPackageInfo(packageName, 0).applicationInfo
|
val appInfo = packageManager.getPackageInfo(packageName, 0).applicationInfo
|
||||||
|
@ -413,7 +609,7 @@ class AccountModel @AssistedInject constructor(
|
||||||
class CollectionPager(
|
class CollectionPager(
|
||||||
val db: AppDatabase,
|
val db: AppDatabase,
|
||||||
service: LiveData<Service?>,
|
service: LiveData<Service?>,
|
||||||
val collectionType: String,
|
private val collectionType: String,
|
||||||
showOnlyPersonal: LiveData<AccountSettings.ShowOnlyPersonal>
|
showOnlyPersonal: LiveData<AccountSettings.ShowOnlyPersonal>
|
||||||
) : MediatorLiveData<Pager<Int, Collection>?>() {
|
) : MediatorLiveData<Pager<Int, Collection>?>() {
|
||||||
|
|
||||||
|
|
|
@ -11,48 +11,44 @@ import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.layout.Column
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
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.foundation.verticalScroll
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.LinearProgressIndicator
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.OutlinedTextField
|
import androidx.compose.material.OutlinedTextField
|
||||||
import androidx.compose.material.RadioButton
|
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TopAppBar
|
import androidx.compose.material.TopAppBar
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
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.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.app.TaskStackBuilder
|
import androidx.core.app.TaskStackBuilder
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import at.bitfire.davdroid.R
|
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.HomeSet
|
||||||
import at.bitfire.davdroid.db.Service
|
|
||||||
import at.bitfire.davdroid.ui.AppTheme
|
import at.bitfire.davdroid.ui.AppTheme
|
||||||
import dagger.assisted.Assisted
|
import at.bitfire.davdroid.ui.widget.ExceptionInfoDialog
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
@ -65,41 +61,109 @@ class CreateAddressBookActivity: AppCompatActivity() {
|
||||||
const val EXTRA_ACCOUNT = "account"
|
const val EXTRA_ACCOUNT = "account"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var modelFactory: Model.Factory
|
val account by lazy { intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") }
|
||||||
val model by viewModels<Model> {
|
|
||||||
|
@Inject
|
||||||
|
lateinit var modelFactory: AccountModel.Factory
|
||||||
|
val model by viewModels<AccountModel> {
|
||||||
object: ViewModelProvider.Factory {
|
object: ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||||
val account = intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set")
|
modelFactory.create(account) as T
|
||||||
return modelFactory.create(account) as T
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
val displayName by model.displayName.observeAsState()
|
var displayName by remember { mutableStateOf("") }
|
||||||
val description by model.description.observeAsState()
|
var description by remember { mutableStateOf("") }
|
||||||
val homeSet by model.homeSet.observeAsState()
|
var homeSet by remember { mutableStateOf<HomeSet?>(null) }
|
||||||
val homeSets by model.homeSets.observeAsState()
|
|
||||||
|
|
||||||
Content(
|
var isCreating by remember { mutableStateOf(false) }
|
||||||
isCreateEnabled =
|
model.createCollectionResult.observeAsState().value?.let { result ->
|
||||||
displayName != null &&
|
if (result.isEmpty)
|
||||||
homeSet != null,
|
finish()
|
||||||
displayName = displayName,
|
else
|
||||||
onDisplayNameChange = model.displayName::setValue,
|
ExceptionInfoDialog(
|
||||||
description = description,
|
exception = result.get(),
|
||||||
onDescriptionChange = model.description::setValue,
|
onDismiss = {
|
||||||
homeSet = homeSet,
|
isCreating = false
|
||||||
homeSets = homeSets,
|
model.createCollectionResult.value = null
|
||||||
onHomeSetClicked = model.homeSet::setValue
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@Composable
|
||||||
private fun Content(
|
private fun AddressBookForm(
|
||||||
isCreateEnabled: Boolean = false,
|
displayName: String,
|
||||||
displayName: String? = null,
|
|
||||||
onDisplayNameChange: (String) -> Unit = {},
|
onDisplayNameChange: (String) -> Unit = {},
|
||||||
description: String? = null,
|
description: String,
|
||||||
onDescriptionChange: (String) -> Unit = {},
|
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?,
|
homeSet: HomeSet?,
|
||||||
homeSets: List<HomeSet>?,
|
homeSets: List<HomeSet>,
|
||||||
onHomeSetClicked: (HomeSet) -> Unit
|
onHomeSetSelected: (HomeSet) -> Unit = {},
|
||||||
|
onCreateCollection: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(Modifier
|
||||||
modifier = Modifier
|
.fillMaxWidth()
|
||||||
.fillMaxSize()
|
.padding(8.dp)
|
||||||
.padding(paddingValues)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = displayName ?: "",
|
value = displayName,
|
||||||
onValueChange = onDisplayNameChange,
|
onValueChange = onDisplayNameChange,
|
||||||
label = { Text(stringResource(R.string.create_collection_display_name)) },
|
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(
|
OutlinedTextField(
|
||||||
value = description ?: "",
|
value = description,
|
||||||
onValueChange = onDescriptionChange,
|
onValueChange = onDescriptionChange,
|
||||||
label = { Text(stringResource(R.string.create_collection_description_optional)) },
|
label = { Text(stringResource(R.string.create_collection_description_optional)) },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
onCreateCollection()
|
||||||
|
}
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
HomeSetSelection(
|
||||||
text = stringResource(R.string.create_collection_home_set),
|
homeSet = homeSet,
|
||||||
style = MaterialTheme.typography.body1,
|
homeSets = homeSets,
|
||||||
modifier = Modifier
|
onHomeSetSelected = onHomeSetSelected
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp)
|
|
||||||
)
|
)
|
||||||
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
|
@Composable
|
||||||
private fun TopBar(isCreateEnabled: Boolean) {
|
@Preview
|
||||||
TopAppBar(
|
private fun AddressBookForm_Preview() {
|
||||||
title = { Text(stringResource(R.string.create_addressbook)) },
|
AddressBookForm(
|
||||||
navigationIcon = {
|
displayName = "Display Name",
|
||||||
IconButton(onClick = ::finish) {
|
description = "Some longer description that is optional",
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
homeSets = listOf(
|
||||||
}
|
HomeSet(1, 0, false, "http://example.com/".toHttpUrl()),
|
||||||
},
|
HomeSet(2, 0, false, "http://example.com/".toHttpUrl(), displayName = "Home Set 2")
|
||||||
actions = {
|
),
|
||||||
IconButton(
|
homeSet = null
|
||||||
enabled = isCreateEnabled,
|
|
||||||
onClick = ::onCreateCollection
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.create_collection_create).uppercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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
|
package at.bitfire.davdroid.ui.account
|
||||||
|
|
||||||
import android.accounts.Account
|
import android.accounts.Account
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.activity.compose.setContent
|
||||||
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.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.NavUtils
|
import androidx.compose.foundation.background
|
||||||
import androidx.databinding.DataBindingUtil
|
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.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.bitfire.davdroid.Constants
|
import at.bitfire.davdroid.Constants
|
||||||
import at.bitfire.davdroid.R
|
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.HomeSet
|
||||||
import at.bitfire.davdroid.db.Service
|
import at.bitfire.davdroid.ui.AppTheme
|
||||||
import at.bitfire.davdroid.ui.HomeSetAdapter
|
import at.bitfire.davdroid.ui.widget.CalendarColorPickerDialog
|
||||||
import at.bitfire.ical4android.util.DateUtils
|
import at.bitfire.davdroid.ui.widget.ExceptionInfoDialog
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
import at.bitfire.davdroid.ui.widget.MultipleChoiceInputDialog
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.fortuna.ical4j.model.Calendar
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.text.Collator
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.TextStyle
|
import java.time.format.TextStyle
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class CreateCalendarActivity: AppCompatActivity(), ColorPickerDialogListener {
|
class CreateCalendarActivity: AppCompatActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_ACCOUNT = "account"
|
const val EXTRA_ACCOUNT = "account"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var modelFactory: Model.Factory
|
val account by lazy { intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set") }
|
||||||
val model by viewModels<Model> {
|
|
||||||
|
@Inject
|
||||||
|
lateinit var modelFactory: AccountModel.Factory
|
||||||
|
val accountModel by viewModels<AccountModel> {
|
||||||
object: ViewModelProvider.Factory {
|
object: ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||||
val account = intent.getParcelableExtra<Account>(EXTRA_ACCOUNT) ?: throw IllegalArgumentException("EXTRA_ACCOUNT must be set")
|
modelFactory.create(account) as T
|
||||||
return modelFactory.create(account) as T
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var binding: ActivityCreateCalendarBinding
|
val model: Model by viewModels()
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_create_calendar)
|
setContent {
|
||||||
binding.lifecycleOwner = this
|
AppTheme {
|
||||||
binding.model = model
|
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 {
|
var isCreating by remember { mutableStateOf(false) }
|
||||||
ColorPickerDialog.newBuilder()
|
accountModel.createCollectionResult.observeAsState().value?.let { result ->
|
||||||
.setShowAlphaSlider(false)
|
if (result.isEmpty)
|
||||||
.setColor((binding.color.background as ColorDrawable).color)
|
finish()
|
||||||
.show(this)
|
else
|
||||||
}
|
ExceptionInfoDialog(
|
||||||
|
exception = result.get(),
|
||||||
val homeSetAdapter = HomeSetAdapter(this)
|
onDismiss = {
|
||||||
model.homeSets.observe(this) { homeSets ->
|
isCreating = false
|
||||||
homeSetAdapter.clear()
|
accountModel.createCollectionResult.value = null
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import at.bitfire.davdroid.R
|
import at.bitfire.davdroid.R
|
||||||
import at.bitfire.davdroid.db.Collection
|
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 okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ fun DeleteCollectionDialog_Content(
|
||||||
ExceptionInfoDialog(
|
ExceptionInfoDialog(
|
||||||
exception = exception,
|
exception = exception,
|
||||||
remoteResource = collection.url,
|
remoteResource = collection.url,
|
||||||
onDismissRequest = onCancel
|
onDismiss = onCancel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,6 @@ fun RenameAccountDialog(
|
||||||
onDismiss: () -> Unit = {}
|
onDismiss: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var accountName by remember { mutableStateOf(TextFieldValue(oldName, selection = TextRange(oldName.length))) }
|
var accountName by remember { mutableStateOf(TextFieldValue(oldName, selection = TextRange(oldName.length))) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
|
@ -43,6 +42,8 @@ fun RenameAccountDialog(
|
||||||
stringResource(R.string.account_rename_new_name_description),
|
stringResource(R.string.account_rename_new_name_description),
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
TextField(
|
TextField(
|
||||||
value = accountName,
|
value = accountName,
|
||||||
onValueChange = { accountName = it },
|
onValueChange = { accountName = it },
|
||||||
|
@ -50,15 +51,18 @@ fun RenameAccountDialog(
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = KeyboardType.Email,
|
keyboardType = KeyboardType.Email,
|
||||||
imeAction = ImeAction.Go
|
imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onGo = {
|
onDone = {
|
||||||
onRenameAccount(accountName.text)
|
onRenameAccount(accountName.text)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
modifier = Modifier.focusRequester(focusRequester)
|
modifier = Modifier.focusRequester(focusRequester)
|
||||||
)
|
)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
}},
|
}},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -74,17 +78,8 @@ fun RenameAccountDialog(
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text(stringResource(android.R.string.cancel).uppercase())
|
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
|
@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.
|
* 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.accounts.Account
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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.dav4jvm.exception.HttpException
|
||||||
import at.bitfire.davdroid.R
|
import at.bitfire.davdroid.R
|
||||||
import com.google.accompanist.themeadapter.material.MdcTheme
|
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import java.io.IOException
|
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
|
@Composable
|
||||||
fun ExceptionInfoDialog(
|
fun ExceptionInfoDialog(
|
||||||
exception: Throwable,
|
exception: Throwable,
|
||||||
account: Account? = null,
|
account: Account? = null,
|
||||||
remoteResource: HttpUrl? = null,
|
remoteResource: HttpUrl? = null,
|
||||||
onDismissRequest: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
@ -90,7 +43,7 @@ fun ExceptionInfoDialog(
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismiss,
|
||||||
title = {
|
title = {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
@ -124,7 +77,7 @@ fun ExceptionInfoDialog(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onDismissRequest) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text(stringResource(android.R.string.ok).uppercase())
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.AlertDialog
|
import androidx.compose.material.AlertDialog
|
||||||
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.RadioButton
|
import androidx.compose.material.RadioButton
|
||||||
import androidx.compose.material.Text
|
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.KeyboardType
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditTextInputDialog(
|
fun EditTextInputDialog(
|
||||||
|
@ -43,7 +47,6 @@ fun EditTextInputDialog(
|
||||||
initialValue ?: "", selection = TextRange(initialValue?.length ?: 0)
|
initialValue ?: "", selection = TextRange(initialValue?.length ?: 0)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
|
@ -54,6 +57,7 @@ fun EditTextInputDialog(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
TextField(
|
TextField(
|
||||||
value = textValue,
|
value = textValue,
|
||||||
onValueChange = { textValue = it },
|
onValueChange = { textValue = it },
|
||||||
|
@ -70,6 +74,9 @@ fun EditTextInputDialog(
|
||||||
),
|
),
|
||||||
modifier = Modifier.focusRequester(focusRequester)
|
modifier = Modifier.focusRequester(focusRequester)
|
||||||
)
|
)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -90,14 +97,6 @@ fun EditTextInputDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var requestFocus = remember { true }
|
|
||||||
LaunchedEffect(requestFocus) {
|
|
||||||
if (requestFocus) {
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
requestFocus = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -118,38 +117,51 @@ fun MultipleChoiceInputDialog(
|
||||||
onValueSelected: (String) -> Unit = {},
|
onValueSelected: (String) -> Unit = {},
|
||||||
onDismiss: () -> Unit = {},
|
onDismiss: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
onDismissRequest = onDismiss,
|
Card {
|
||||||
title = {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style = MaterialTheme.typography.body1
|
style = MaterialTheme.typography.body1,
|
||||||
)
|
modifier = Modifier
|
||||||
},
|
.fillMaxWidth()
|
||||||
text = {
|
.padding(8.dp)
|
||||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
)
|
||||||
for ((name, value) in namesAndValues)
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
LazyColumn(Modifier.padding(8.dp)) {
|
||||||
RadioButton(
|
items(
|
||||||
selected = value == initialValue,
|
count = namesAndValues.size,
|
||||||
onClick = {
|
key = { index -> namesAndValues[index].second },
|
||||||
onValueSelected(value)
|
itemContent = { index ->
|
||||||
onDismiss()
|
Row(
|
||||||
}
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
)
|
modifier = Modifier.fillMaxWidth()
|
||||||
Text(
|
) {
|
||||||
name,
|
val (name, value) = namesAndValues[index]
|
||||||
style = MaterialTheme.typography.body1,
|
RadioButton(
|
||||||
modifier = Modifier.clickable {
|
selected = value == initialValue,
|
||||||
onValueSelected(value)
|
onClick = {
|
||||||
onDismiss()
|
onValueSelected(value)
|
||||||
}
|
onDismiss()
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.clickable {
|
||||||
|
onValueSelected(value)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
buttons = {}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 -->
|
<!-- collection management -->
|
||||||
<string name="create_addressbook">Create address book</string>
|
<string name="create_addressbook">Create address book</string>
|
||||||
<string name="create_calendar">Create calendar</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">Possible calendar entries</string>
|
||||||
<string name="create_calendar_type_vevent">Events</string>
|
<string name="create_calendar_type_vevent">Events</string>
|
||||||
<string name="create_calendar_type_vtodo">Tasks</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-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-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
|
||||||
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", 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" }
|
junit = { module = "junit:junit", version = "4.13.2" }
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||||
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
|
|
Loading…
Reference in a new issue