From c18a32e59286674aaacf4108bbe11956c6291b7d Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Wed, 8 May 2024 23:03:30 +0200 Subject: [PATCH 01/27] Refactor details screen - first batch --- .../jtx/ui/detail/DetailBottomAppBar.kt | 222 +------------ .../jtx/ui/detail/DetailScreenContent.kt | 13 +- .../jtx/ui/detail/DetailsCardAlarms.kt | 126 ++++--- .../jtx/ui/detail/DetailsCardAttachments.kt | 127 ++++---- .../jtx/ui/detail/DetailsCardCollections.kt | 142 ++------ .../jtx/ui/detail/DetailsCardContact.kt | 182 +++++------ .../techbee/jtx/ui/detail/DetailsCardDates.kt | 65 ++-- ...DetailsCardStatusClassificationPriority.kt | 93 +++++- .../techbee/jtx/ui/detail/DetailsCardUrl.kt | 134 +++++--- .../at/techbee/jtx/ui/detail/DetailsScreen.kt | 307 ++++++++++++------ .../java/at/techbee/jtx/ui/list/ListCard.kt | 25 +- .../jtx/ui/list/ListQuickAddElement.kt | 8 +- .../jtx/ui/reusable/cards/AlarmCard.kt | 171 +++------- .../jtx/ui/reusable/cards/AttachmentCard.kt | 140 ++++---- .../ui/reusable/cards/HorizontalDateCard.kt | 81 ++--- .../dialogs/CollectionSelectorDialog.kt | 12 +- .../CollectionsMoveCollectionDialog.kt | 12 +- .../reusable/elements/CollectionInfoColumn.kt | 259 +++++++++++++-- .../reusable/elements/CollectionsSpinner.kt | 51 ++- app/src/main/res/values/strings.xml | 2 + 20 files changed, 1126 insertions(+), 1046 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt index 80b048cd3..1d6a01213 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt @@ -8,15 +8,9 @@ package at.techbee.jtx.ui.detail -import android.accounts.Account -import android.content.ContentResolver import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.keyframes -import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -29,46 +23,28 @@ import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.List import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.EditOff -import androidx.compose.material.icons.outlined.CloudSync import androidx.compose.material.icons.outlined.Code -import androidx.compose.material.icons.outlined.ContentCopy -import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DriveFileRenameOutline import androidx.compose.material.icons.outlined.FormatBold import androidx.compose.material.icons.outlined.FormatItalic import androidx.compose.material.icons.outlined.FormatStrikethrough import androidx.compose.material.icons.outlined.HorizontalRule -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.outlined.Sync import androidx.compose.material.icons.outlined.TextFormat import androidx.compose.material3.BottomAppBar import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SheetState -import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -77,63 +53,24 @@ import at.techbee.jtx.R import at.techbee.jtx.database.ICalCollection import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE import at.techbee.jtx.database.ICalObject -import at.techbee.jtx.database.Module import at.techbee.jtx.flavored.BillingManager import at.techbee.jtx.util.SyncApp -import at.techbee.jtx.util.SyncUtil -import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @Composable fun DetailBottomAppBar( - icalObject: ICalObject?, - seriesElement: ICalObject?, + iCalObject: ICalObject?, collection: ICalCollection?, isEditMode: MutableState, markdownState: MutableState, isProActionAvailable: Boolean, changeState: MutableState, - detailsBottomSheetState: SheetState, - onDeleteClicked: () -> Unit, - onCopyRequested: (Module) -> Unit, - onRevertClicked: () -> Unit, + onRevertClicked: () -> Unit ) { - if (icalObject == null || collection == null) + if (iCalObject == null || collection == null) return val context = LocalContext.current - val scope = rememberCoroutineScope() - var copyOptionsExpanded by remember { mutableStateOf(false) } - - val syncIconAnimation = rememberInfiniteTransition(label = "syncIconAnimation") - val angle by syncIconAnimation.animateFloat( - initialValue = 0f, - targetValue = -360f, - animationSpec = infiniteRepeatable( - animation = keyframes { - durationMillis = 2000 - } - ), label = "syncIconAnimationAngle" - ) - - val isPreview = LocalInspectionMode.current - val lifecycleOwner = LocalLifecycleOwner.current - var isSyncInProgress by remember { mutableStateOf(false) } - DisposableEffect(lifecycleOwner) { - - val listener = if (isPreview) - null - else { - ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE) { - isSyncInProgress = SyncUtil.isJtxSyncRunningFor(setOf(Account(collection.accountName, collection.accountType))) - } - } - onDispose { - if (!isPreview) - ContentResolver.removeStatusChangeListener(listener) - } - } BottomAppBar( @@ -147,82 +84,6 @@ fun DetailBottomAppBar( } } - AnimatedVisibility( - isEditMode.value - && (markdownState.value == MarkdownState.DISABLED || markdownState.value == MarkdownState.CLOSED) - ) { - IconButton(onClick = { - scope.launch { - if (detailsBottomSheetState.isVisible) - detailsBottomSheetState.hide() - else - detailsBottomSheetState.show() - } - } - ) { - Icon( - Icons.Outlined.Settings, - contentDescription = stringResource(id = R.string.preferences) - ) - } - } - - AnimatedVisibility( - !isEditMode.value - && !collection.readonly - && isProActionAvailable - ) { - IconButton(onClick = { copyOptionsExpanded = true }) { - Icon( - Icons.Outlined.ContentCopy, - contentDescription = stringResource(id = R.string.menu_view_copy_item), - ) - DropdownMenu( - expanded = copyOptionsExpanded, - onDismissRequest = { copyOptionsExpanded = false } - ) { - if (collection.supportsVJOURNAL) { - DropdownMenuItem( - text = { Text(stringResource(id = R.string.menu_view_copy_as_journal)) }, - onClick = { - onCopyRequested(Module.JOURNAL) - copyOptionsExpanded = false - } - ) - DropdownMenuItem( - text = { Text(stringResource(id = R.string.menu_view_copy_as_note)) }, - onClick = { - onCopyRequested(Module.NOTE) - copyOptionsExpanded = false - } - ) - } - if (collection.supportsVTODO) { - DropdownMenuItem( - text = { Text(stringResource(id = R.string.menu_view_copy_as_todo)) }, - onClick = { - onCopyRequested(Module.TODO) - copyOptionsExpanded = false - } - ) - } - } - } - } - - if( - !collection.readonly - && isProActionAvailable - && (markdownState.value == MarkdownState.DISABLED || markdownState.value == MarkdownState.CLOSED) - ) { - IconButton(onClick = { onDeleteClicked() }) { - Icon( - Icons.Outlined.Delete, - contentDescription = stringResource(id = R.string.delete), - ) - } - } - AnimatedVisibility( isEditMode.value && changeState.value != DetailViewModel.DetailChangeState.UNCHANGED @@ -236,41 +97,6 @@ fun DetailBottomAppBar( } } - AnimatedVisibility( - collection.accountType != LOCAL_ACCOUNT_TYPE - && (isSyncInProgress || seriesElement?.dirty ?: icalObject.dirty) - && (markdownState.value == MarkdownState.DISABLED || markdownState.value == MarkdownState.CLOSED) - ) { - IconButton( - onClick = { - if (!isSyncInProgress) { - collection.getAccount().let { SyncUtil.syncAccounts(setOf(it)) } - SyncUtil.showSyncRequestedToast(context) - } - }, - enabled = seriesElement?.dirty ?: icalObject.dirty && !isSyncInProgress - ) { - Crossfade(isSyncInProgress, label = "isSyncInProgress") { synchronizing -> - if (synchronizing) { - Icon( - Icons.Outlined.Sync, - contentDescription = stringResource(id = R.string.sync_in_progress), - modifier = Modifier - .graphicsLayer { - rotationZ = angle - } - .alpha(0.3f), - tint = MaterialTheme.colorScheme.primary, - ) - } else { - Icon( - Icons.Outlined.CloudSync, - contentDescription = stringResource(id = R.string.upload_pending), - ) - } - } - } - } AnimatedVisibility((changeState.value == DetailViewModel.DetailChangeState.CHANGEUNSAVED || changeState.value == DetailViewModel.DetailChangeState.CHANGESAVING @@ -396,7 +222,6 @@ fun DetailBottomAppBar( } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_View() { @@ -408,23 +233,18 @@ fun DetailBottomAppBar_Preview_View() { } DetailBottomAppBar( - icalObject = ICalObject.createNote().apply { dirty = true }, - seriesElement = null, + iCalObject = ICalObject.createNote().apply { dirty = true }, collection = collection, isEditMode = remember { mutableStateOf(false) }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGEUNSAVED) }, - detailsBottomSheetState = rememberModalBottomSheetState(), - onDeleteClicked = { }, - onCopyRequested = { }, onRevertClicked = { } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_edit() { @@ -436,22 +256,17 @@ fun DetailBottomAppBar_Preview_edit() { } DetailBottomAppBar( - icalObject = ICalObject.createNote().apply { dirty = true }, - seriesElement = null, + iCalObject = ICalObject.createNote().apply { dirty = true }, collection = collection, isEditMode = remember { mutableStateOf(true) }, isProActionAvailable = true, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, - detailsBottomSheetState = rememberModalBottomSheetState(), - onDeleteClicked = { }, - onCopyRequested = { }, onRevertClicked = { } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_edit_markdown() { @@ -463,22 +278,17 @@ fun DetailBottomAppBar_Preview_edit_markdown() { } DetailBottomAppBar( - icalObject = ICalObject.createNote().apply { dirty = true }, - seriesElement = null, + iCalObject = ICalObject.createNote().apply { dirty = true }, collection = collection, isEditMode = remember { mutableStateOf(true) }, isProActionAvailable = true, markdownState = remember { mutableStateOf(MarkdownState.OBSERVING) }, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, - detailsBottomSheetState = rememberModalBottomSheetState(), - onDeleteClicked = { }, - onCopyRequested = { }, onRevertClicked = { } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_View_readonly() { @@ -490,22 +300,17 @@ fun DetailBottomAppBar_Preview_View_readonly() { } DetailBottomAppBar( - icalObject = ICalObject.createNote().apply { dirty = false }, - seriesElement = null, + iCalObject = ICalObject.createNote().apply { dirty = false }, collection = collection, isEditMode = remember { mutableStateOf(false) }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) }, - detailsBottomSheetState = rememberModalBottomSheetState(), - onDeleteClicked = { }, - onCopyRequested = { }, onRevertClicked = { } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_View_proOnly() { @@ -517,23 +322,18 @@ fun DetailBottomAppBar_Preview_View_proOnly() { } DetailBottomAppBar( - icalObject = ICalObject.createNote().apply { dirty = false }, - seriesElement = null, + iCalObject = ICalObject.createNote().apply { dirty = false }, collection = collection, isEditMode = remember { mutableStateOf(false) }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = false, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) }, - detailsBottomSheetState = rememberModalBottomSheetState(), - onDeleteClicked = { }, - onCopyRequested = { }, onRevertClicked = { } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_View_local() { @@ -547,16 +347,12 @@ fun DetailBottomAppBar_Preview_View_local() { BillingManager.getInstance().initialise(LocalContext.current.applicationContext) DetailBottomAppBar( - icalObject = ICalObject.createNote().apply { dirty = true }, - seriesElement = null, + iCalObject = ICalObject.createNote().apply { dirty = true }, collection = collection, isEditMode = remember { mutableStateOf(false) }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, - detailsBottomSheetState = rememberModalBottomSheetState(), - onDeleteClicked = { }, - onCopyRequested = { }, onRevertClicked = { } ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index 717842174..f3401f270 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -352,7 +352,7 @@ fun DetailScreenContent( DetailsCardCollections( iCalObject = iCalObject, - isEditMode = isEditMode.value, + seriesElement = seriesElement, isChild = isChild.value, originalCollection = collection, color = color, @@ -368,7 +368,7 @@ fun DetailScreenContent( DetailsCardDates( icalObject = iCalObject, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly?:true, enableDtstart = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] ?: true || iCalObject.getModuleFromString() == Module.JOURNAL, enableDue = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] ?: true, enableCompleted = detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] @@ -397,7 +397,6 @@ fun DetailScreenContent( } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, - toggleEditMode = { isEditMode.value = !isEditMode.value }, modifier = detailElementModifier ) } @@ -793,7 +792,7 @@ fun DetailScreenContent( if(iCalObject.contact?.isNotBlank() == true || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] == true || showAllOptions))) { DetailsCardContact( initialContact = iCalObject.contact ?: "", - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly?:true, onContactUpdated = { newContact -> iCalObject.contact = newContact.ifEmpty { null } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED @@ -806,7 +805,7 @@ fun DetailScreenContent( if(iCalObject.url?.isNotEmpty() == true || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] == true || showAllOptions))) { DetailsCardUrl( initialUrl = iCalObject.url ?: "", - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly?:true, onUrlUpdated = { newUrl -> iCalObject.url = newUrl.ifEmpty { null } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED @@ -855,7 +854,7 @@ fun DetailScreenContent( if(attachments.isNotEmpty() || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] == true || showAllOptions))) { DetailsCardAttachments( attachments = attachments, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly?:true, isRemoteCollection = collection?.accountType != LOCAL_ACCOUNT_TYPE, player = player, onAttachmentsUpdated = { @@ -870,7 +869,7 @@ fun DetailScreenContent( DetailsCardAlarms( alarms = alarms, icalObject = iCalObject, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly?:true, onAlarmsUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAlarms.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAlarms.kt index 4a784b6b0..59abaee63 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAlarms.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAlarms.kt @@ -12,14 +12,27 @@ import android.Manifest import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +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.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Alarm import androidx.compose.material.icons.outlined.AlarmAdd -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,7 +64,7 @@ import kotlin.time.Duration.Companion.minutes fun DetailsCardAlarms( alarms: SnapshotStateList, icalObject: ICalObject, - isEditMode: Boolean, + isReadOnly: Boolean, onAlarmsUpdated: () -> Unit, modifier: Modifier = Modifier ) { @@ -125,7 +138,7 @@ fun DetailsCardAlarms( AlarmCard( alarm = alarm, icalObject = icalObject, - isEditMode = isEditMode, + isReadOnly = isReadOnly, onAlarmDeleted = { alarms.remove(alarm) onAlarmsUpdated() @@ -141,53 +154,76 @@ fun DetailsCardAlarms( } } - AnimatedVisibility(isEditMode) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .padding(top = 8.dp) - ) { + if(!isReadOnly) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(top = 8.dp) + ) { - TextButton(onClick = { showDateTimePicker = true }) { - Icon(Icons.Outlined.AlarmAdd, null, modifier = Modifier.padding(end = 8.dp)) - Text(stringResource(R.string.alarms_date_time)) - } + TextButton(onClick = { showDateTimePicker = true }) { + Icon(Icons.Outlined.AlarmAdd, null, modifier = Modifier.padding(end = 8.dp)) + Text(stringResource(R.string.alarms_date_time)) + } - AnimatedVisibility(icalObject.dtstart != null || icalObject.due != null) { - TextButton(onClick = { showDurationPicker = true }) { - Icon(Icons.Outlined.AlarmAdd, null, modifier = Modifier.padding(end = 8.dp)) - Text(stringResource(id = R.string.alarms_duration)) - } + AnimatedVisibility(icalObject.dtstart != null || icalObject.due != null) { + TextButton(onClick = { showDurationPicker = true }) { + Icon(Icons.Outlined.AlarmAdd, null, modifier = Modifier.padding(end = 8.dp)) + Text(stringResource(id = R.string.alarms_duration)) } + } - val alarmOnStart = Alarm.createDisplayAlarm((0).minutes, AlarmRelativeTo.START, icalObject.dtstart ?: System.currentTimeMillis(), icalObject.dtstartTimezone) - AnimatedVisibility(icalObject.dtstart != null && alarms.none { it.triggerTime == alarmOnStart.triggerTime }) { - TextButton(onClick = { - alarms.add(Alarm.createDisplayAlarm((0).minutes, AlarmRelativeTo.START, icalObject.dtstart!!, icalObject.dtstartTimezone)) - onAlarmsUpdated() - }) { - Icon(Icons.Outlined.AlarmAdd, null, modifier = Modifier.padding(end = 8.dp)) - Text(stringResource(id = R.string.alarms_onstart)) - } + val alarmOnStart = Alarm.createDisplayAlarm( + (0).minutes, + AlarmRelativeTo.START, + icalObject.dtstart ?: System.currentTimeMillis(), + icalObject.dtstartTimezone + ) + AnimatedVisibility(icalObject.dtstart != null && alarms.none { it.triggerTime == alarmOnStart.triggerTime }) { + TextButton(onClick = { + alarms.add( + Alarm.createDisplayAlarm( + (0).minutes, + AlarmRelativeTo.START, + icalObject.dtstart!!, + icalObject.dtstartTimezone + ) + ) + onAlarmsUpdated() + }) { + Icon(Icons.Outlined.AlarmAdd, null, modifier = Modifier.padding(end = 8.dp)) + Text(stringResource(id = R.string.alarms_onstart)) } + } - val alarmOnDue = Alarm.createDisplayAlarm((0).minutes, AlarmRelativeTo.END, icalObject.due ?: System.currentTimeMillis(), icalObject.dueTimezone) - AnimatedVisibility(icalObject.due != null && alarms.none { it.triggerTime == alarmOnDue.triggerTime }) { - TextButton(onClick = { - alarms.add(Alarm.createDisplayAlarm((0).minutes, AlarmRelativeTo.END, icalObject.due!!, icalObject.dueTimezone)) - onAlarmsUpdated() - }) { - Icon(Icons.Outlined.AlarmAdd, null, modifier = Modifier.padding(end = 8.dp)) - Text(stringResource(id = R.string.alarms_ondue)) - } + val alarmOnDue = Alarm.createDisplayAlarm( + (0).minutes, + AlarmRelativeTo.END, + icalObject.due ?: System.currentTimeMillis(), + icalObject.dueTimezone + ) + AnimatedVisibility(icalObject.due != null && alarms.none { it.triggerTime == alarmOnDue.triggerTime }) { + TextButton(onClick = { + alarms.add( + Alarm.createDisplayAlarm( + (0).minutes, + AlarmRelativeTo.END, + icalObject.due!!, + icalObject.dueTimezone + ) + ) + onAlarmsUpdated() + }) { + Icon(Icons.Outlined.AlarmAdd, null, modifier = Modifier.padding(end = 8.dp)) + Text(stringResource(id = R.string.alarms_ondue)) } } } - + } } } @@ -221,7 +257,7 @@ fun DetailsCardAlarms_Preview() { due = System.currentTimeMillis() dueTimezone = null }, - isEditMode = false, + isReadOnly = false, onAlarmsUpdated = { } ) } @@ -230,7 +266,7 @@ fun DetailsCardAlarms_Preview() { @Preview(showBackground = true) @Composable -fun DetailsCardAlarms_Preview_edit() { +fun DetailsCardAlarms_Preview_readOnly() { MaterialTheme { DetailsCardAlarms( alarms = remember { mutableStateListOf( @@ -243,7 +279,7 @@ fun DetailsCardAlarms_Preview_edit() { due = System.currentTimeMillis() dueTimezone = null }, - isEditMode = true, + isReadOnly = true, onAlarmsUpdated = { } ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAttachments.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAttachments.kt index 5b3643946..6b9b5a5f6 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAttachments.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAttachments.kt @@ -20,16 +20,15 @@ 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.material.icons.Icons import androidx.compose.material.icons.outlined.AddLink import androidx.compose.material.icons.outlined.Attachment import androidx.compose.material.icons.outlined.CameraAlt import androidx.compose.material.icons.outlined.Upload -import androidx.compose.material3.Button import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -56,7 +55,7 @@ import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon @Composable fun DetailsCardAttachments( attachments: SnapshotStateList, - isEditMode: Boolean, + isReadOnly: Boolean, isRemoteCollection: Boolean, player: MediaPlayer?, onAttachmentsUpdated: () -> Unit, @@ -105,7 +104,42 @@ fun DetailsCardAttachments( .padding(8.dp), ) { - HeadlineWithIcon(icon = Icons.Outlined.Attachment, iconDesc = headline, text = headline) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + HeadlineWithIcon(icon = Icons.Outlined.Attachment, iconDesc = headline, text = headline) + + if(!isReadOnly) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.horizontalScroll(rememberScrollState()) + ) { + + IconButton(onClick = { pickFileLauncher.launch("*/*") }) { + Icon(Icons.Outlined.Upload, stringResource(id = R.string.edit_attachment_button_text)) + } + // don't show the button if the device does not have a camera + if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) { + IconButton(onClick = { + Attachment.getNewAttachmentUriForPhoto(context)?.let { + newPictureUri.value = it + takePictureLauncher.launch(newPictureUri.value) + } + }) { + Icon( + Icons.Outlined.CameraAlt, + stringResource(id = R.string.edit_take_picture_button_text) + ) + } + } + IconButton(onClick = { showAddLinkAttachmentDialog = true }) { + Icon(Icons.Outlined.AddLink, stringResource(id = R.string.edit_add_link_button_text)) + } + } + } + } AnimatedVisibility(attachments.isNotEmpty()) { Column( @@ -114,31 +148,10 @@ fun DetailsCardAttachments( .fillMaxWidth() ) { - Row(modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - attachments.asReversed().filter { it.fmttype?.startsWith("image/") == true } - .forEach { attachment -> - AttachmentCard( - attachment = attachment, - isEditMode = isEditMode, - isRemoteCollection = isRemoteCollection, - player = player, - onAttachmentDeleted = { - attachments.remove(attachment) - onAttachmentsUpdated() - }, - modifier = Modifier.size(100.dp, 140.dp) - ) - } - } - - attachments.asReversed().filter { it.fmttype == null || it.fmttype?.startsWith("image/") == false }.forEach { attachment -> + attachments.asReversed().forEach { attachment -> AttachmentCard( attachment = attachment, - isEditMode = isEditMode, + isReadOnly = isReadOnly, isRemoteCollection = isRemoteCollection, player = player, onAttachmentDeleted = { @@ -150,48 +163,13 @@ fun DetailsCardAttachments( } } - AnimatedVisibility(isEditMode) { - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .padding(top = 8.dp) - ) { - - Button(onClick = { pickFileLauncher.launch("*/*") }) { - Icon(Icons.Outlined.Upload, stringResource(id = R.string.edit_attachment_button_text)) - } - // don't show the button if the device does not have a camera - if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) { - Button(onClick = { - Attachment.getNewAttachmentUriForPhoto(context)?.let { - newPictureUri.value = it - takePictureLauncher.launch(newPictureUri.value) - } - }) { - Icon( - Icons.Outlined.CameraAlt, - stringResource(id = R.string.edit_take_picture_button_text) - ) - } - } - Button(onClick = { showAddLinkAttachmentDialog = true }) { - Icon(Icons.Outlined.AddLink, stringResource(id = R.string.edit_add_link_button_text)) - } - } - } - - AnimatedVisibility((isEditMode && attachments.any { (it.getFilesize(context)?:0) > 100000 } && isRemoteCollection) || LocalInspectionMode.current) { + AnimatedVisibility((attachments.any { (it.getFilesize(context)?:0) > 100000 } && isRemoteCollection) || LocalInspectionMode.current) { Text( stringResource(id = R.string.details_attachment_beta_info), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error ) } - } } } @@ -203,7 +181,7 @@ fun DetailsCardAttachments_Preview() { DetailsCardAttachments( attachments = remember { mutableStateListOf(Attachment(filename = "test.pdf")) }, - isEditMode = false, + isReadOnly = false, isRemoteCollection = true, player = null, onAttachmentsUpdated = { } @@ -218,7 +196,7 @@ fun DetailsCardAttachments_Preview_Images() { DetailsCardAttachments( attachments = remember { mutableStateListOf(Attachment(filename = "test.pdf"), Attachment(filename = "image.jpg", fmttype = "image/jpg"), Attachment(filename = "image2.jpg", fmttype = "image/jpg")) }, - isEditMode = false, + isReadOnly = false, isRemoteCollection = true, player = null, onAttachmentsUpdated = { } @@ -233,7 +211,7 @@ fun DetailsCardAttachments_Preview_edit() { MaterialTheme { DetailsCardAttachments( attachments = remember { mutableStateListOf(Attachment(filename = "test.pdf")) }, - isEditMode = true, + isReadOnly = true, isRemoteCollection = true, player = null, onAttachmentsUpdated = { } @@ -249,7 +227,22 @@ fun DetailsCardAttachments_Preview_Images_edit() { DetailsCardAttachments( attachments = remember { mutableStateListOf(Attachment(filename = "test.pdf"), Attachment(filename = "image.jpg", fmttype = "image/jpg"), Attachment(filename = "image2.jpg", fmttype = "image/jpg")) }, - isEditMode = true, + isReadOnly = false, + isRemoteCollection = true, + player = null, + onAttachmentsUpdated = { } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun DetailsCardAttachments_Preview_Images_readOnly() { + MaterialTheme { + + DetailsCardAttachments( + attachments = remember { mutableStateListOf(Attachment(filename = "test.pdf"), Attachment(filename = "image.jpg", fmttype = "image/jpg"), Attachment(filename = "image2.jpg", fmttype = "image/jpg")) }, + isReadOnly = true, isRemoteCollection = true, player = null, onAttachmentsUpdated = { } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt index f1c310330..14794eb9c 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt @@ -1,49 +1,28 @@ package at.techbee.jtx.ui.detail import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ColorLens -import androidx.compose.material.icons.outlined.FolderOpen -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import at.techbee.jtx.R import at.techbee.jtx.database.ICalCollection -import at.techbee.jtx.database.ICalDatabase +import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE import at.techbee.jtx.database.ICalObject -import at.techbee.jtx.ui.reusable.dialogs.ColorPickerDialog -import at.techbee.jtx.ui.reusable.elements.CollectionInfoColumn import at.techbee.jtx.ui.reusable.elements.CollectionsSpinner -import at.techbee.jtx.ui.reusable.elements.ListBadge import at.techbee.jtx.ui.theme.jtxCardBorderStrokeWidth @Composable fun DetailsCardCollections( iCalObject: ICalObject?, - isEditMode: Boolean, + seriesElement: ICalObject?, isChild: Boolean, originalCollection: ICalCollection, color: MutableState, @@ -55,101 +34,32 @@ fun DetailsCardCollections( modifier: Modifier = Modifier ) { - var showColorPicker by rememberSaveable { mutableStateOf(false) } - val context = LocalContext.current - - if (showColorPicker) { - ColorPickerDialog( - initialColor = color.value, - onColorChanged = { newColor -> - color.value = newColor - iCalObject?.color = newColor - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED - }, - onDismiss = { - showColorPicker = false - }, - additionalColorsInt = ICalDatabase - .getInstance(context) - .iCalDatabaseDao() - .getAllColors() - .observeAsState( - initial = emptyList() - ).value - ) + fun onColorPicked(newColor: Int?) { + color.value = newColor + iCalObject?.color = newColor + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED } - if(!isEditMode || isChild) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Card( - colors = CardDefaults.elevatedCardColors(), - elevation = CardDefaults.elevatedCardElevation(), - border = color.value?.let { BorderStroke(jtxCardBorderStrokeWidth, Color(it)) }, - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier.padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - ListBadge( - icon = Icons.Outlined.FolderOpen, - iconDesc = stringResource(id = R.string.collection), - containerColor = originalCollection.color?.let { - Color( - it - ) - } ?: MaterialTheme.colorScheme.primaryContainer, - isAccessibilityMode = true - ) - CollectionInfoColumn( - collection = originalCollection, - modifier = Modifier.fillMaxWidth() - ) + CollectionsSpinner( + collections = allPossibleCollections, + preselected = originalCollection, + includeReadOnly = false, + includeVJOURNAL = includeVJOURNAL, + includeVTODO = includeVTODO, + onSelectionChanged = { newCollection -> + if (iCalObject?.collectionId != newCollection.collectionId) { + onMoveToNewCollection(newCollection) } - } - } - } else { - Card( - colors = CardDefaults.elevatedCardColors(), - elevation = CardDefaults.elevatedCardElevation(), - border = color.value?.let { BorderStroke(jtxCardBorderStrokeWidth, Color(it)) }, - modifier = modifier - ) { - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - - CollectionsSpinner( - collections = allPossibleCollections, - preselected = originalCollection, - includeReadOnly = false, - includeVJOURNAL = includeVJOURNAL, - includeVTODO = includeVTODO, - onSelectionChanged = { newCollection -> - if (iCalObject?.collectionId != newCollection.collectionId) { - onMoveToNewCollection(newCollection) - } - }, - enabled = iCalObject?.recurid.isNullOrEmpty(), - modifier = Modifier - .weight(1f) - .padding(4.dp) - ) - - IconButton(onClick = { showColorPicker = true }) { - Icon(Icons.Outlined.ColorLens, stringResource(id = R.string.color)) - } - } - } - } + }, + showColorPicker = !originalCollection.readonly, + onColorPicked = { newColor -> onColorPicked(newColor)}, + showSyncButton = (originalCollection.accountType != LOCAL_ACCOUNT_TYPE + && seriesElement?.dirty ?: iCalObject?.dirty ?: false), + enableSelector = !originalCollection.readonly && !isChild && iCalObject?.recurid.isNullOrEmpty(), + modifier = modifier, + border = color.value?.let { BorderStroke(jtxCardBorderStrokeWidth, Color(it)) } + ) } @@ -163,7 +73,7 @@ fun DetailsCardCollections_edit() { DetailsCardCollections( iCalObject = ICalObject.createJournal("MySummary"), - isEditMode = true, + seriesElement = null, isChild = false, originalCollection = ICalCollection.createLocalCollection(context).apply { this.displayName = "Test" }, color = remember { mutableStateOf(Color.Blue.toArgb()) }, @@ -186,7 +96,7 @@ fun DetailsCardCollections_read() { DetailsCardCollections( iCalObject = ICalObject.createJournal("MySummary"), - isEditMode = false, + seriesElement = null, isChild = false, originalCollection = ICalCollection.createLocalCollection(context).apply { this.displayName = "Test" }, color = remember { mutableStateOf(Color.Blue.toArgb()) }, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardContact.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardContact.kt index 6a52be99b..310d52038 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardContact.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardContact.kt @@ -12,9 +12,7 @@ import android.Manifest import android.content.Intent import android.net.Uri import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.border import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -22,20 +20,20 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Call -import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.ContactMail import androidx.compose.material.icons.outlined.Mail import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.InputChip +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -46,7 +44,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode @@ -74,7 +74,7 @@ import kotlinx.coroutines.launch @Composable fun DetailsCardContact( initialContact: String, - isEditMode: Boolean, + isReadOnly: Boolean, onContactUpdated: (String) -> Unit, modifier: Modifier = Modifier ) { @@ -82,26 +82,29 @@ fun DetailsCardContact( val context = LocalContext.current // preview would break if rememberPermissionState is used for preview, so we set it to null only for preview! val contactsPermissionState = if (!LocalInspectionMode.current) rememberPermissionState(permission = Manifest.permission.READ_CONTACTS) else null + val focusRequester = remember { FocusRequester() } + var checkPermission by remember { mutableStateOf(false) } var contact by rememberSaveable { mutableStateOf(initialContact) } val headline = stringResource(id = R.string.contact) - val bringIntoViewRequester = remember { BringIntoViewRequester() } val coroutineScope = rememberCoroutineScope() + val bringIntoViewRequester = remember { BringIntoViewRequester() } val searchContacts = remember { mutableStateListOf() } - val foundTelephoneNumber = UiUtil.extractTelephoneNumbers(contact) - val foundEmail = UiUtil.extractEmailAddresses(contact) + val foundTelephoneNumber = UiUtil.extractTelephoneNumbers(contact).firstOrNull() + val foundEmail = UiUtil.extractEmailAddresses(contact).firstOrNull() - ElevatedCard(modifier = modifier) { + ElevatedCard( + modifier = modifier, + onClick = { focusRequester.requestFocus() } + ) { Column( modifier = Modifier .fillMaxWidth() .padding(8.dp), ) { - Crossfade(isEditMode, label = "isEditModeContact") { - if(!it) { Row { Column(modifier = Modifier.weight(1f)) { @@ -110,16 +113,73 @@ fun DetailsCardContact( iconDesc = headline, text = headline ) - Text( - text = contact, - modifier = Modifier.fillMaxWidth() + + + + BasicTextField( + value = contact, + textStyle = LocalTextStyle.current, + onValueChange = { newValue -> + contact = newValue + onContactUpdated(contact) + + coroutineScope.launch { + searchContacts.clear() + if(newValue.length >= 3 && contactsPermissionState?.status?.isGranted == true) + searchContacts.addAll(UiUtil.getLocalContacts(context, newValue)) + } + }, + enabled = !isReadOnly, + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { searchContacts.clear() }), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + if (focusState.hasFocus) + checkPermission = true + } ) + + AnimatedVisibility(searchContacts.isNotEmpty()) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + ) { + + searchContacts.forEach { searchContact -> + + if(searchContact.getDisplayString() == contact) + return@forEach + + InputChip( + onClick = { + contact = searchContact.getDisplayString() + onContactUpdated(contact) + }, + label = { Text(searchContact.getDisplayString()) }, + leadingIcon = { + Icon(Icons.Outlined.ContactMail, stringResource(id = R.string.contact)) + }, + selected = false, + modifier = Modifier.onPlaced { + coroutineScope.launch { + bringIntoViewRequester.bringIntoView() + } + } + ) + } + } + } } - foundTelephoneNumber.firstOrNull()?.let { + AnimatedVisibility(!foundTelephoneNumber.isNullOrEmpty()) { IconButton(onClick = { val intent = Intent(Intent.ACTION_DIAL).apply { - data = Uri.parse("tel:$it") + data = Uri.parse("tel:$foundTelephoneNumber") } if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) @@ -127,100 +187,30 @@ fun DetailsCardContact( }) { Icon(Icons.Outlined.Call, stringResource( id = R.string.call_contact, - it + foundTelephoneNumber?:"" )) } } - foundEmail.firstOrNull()?.let { + AnimatedVisibility(!foundEmail.isNullOrEmpty()) { IconButton(onClick = { val intent = Intent(Intent.ACTION_SEND) intent.type = "message/rfc822" - intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(it)) + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(foundEmail)) context.startActivity(intent) }) { Icon(Icons.Outlined.Mail, stringResource( id = R.string.email_contact, - it + foundEmail?:"" )) } } - } - } else { - - Column(modifier = Modifier.fillMaxWidth()) { - AnimatedVisibility(contact.isNotEmpty() && searchContacts.isNotEmpty()) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - ) { - - searchContacts.forEach { searchContact -> - - if(searchContact.getDisplayString() == contact) - return@forEach - - InputChip( - onClick = { - contact = searchContact.getDisplayString() - onContactUpdated(contact) - }, - label = { Text(searchContact.getDisplayString()) }, - leadingIcon = { - Icon(Icons.Outlined.ContactMail, stringResource(id = R.string.contact)) - }, - selected = false, - modifier = Modifier.onPlaced { - coroutineScope.launch { - bringIntoViewRequester.bringIntoView() - } - } - ) - } - } - } - - OutlinedTextField( - value = contact, - leadingIcon = { Icon(Icons.Outlined.ContactMail, headline) }, - trailingIcon = { - IconButton(onClick = { - contact = "" - onContactUpdated(contact) - }) { - AnimatedVisibility(contact.isNotEmpty()) { - Icon(Icons.Outlined.Clear, stringResource(id = R.string.delete)) - } - } - }, - singleLine = true, - label = { Text(headline) }, - onValueChange = { newValue -> - contact = newValue - onContactUpdated(contact) - - coroutineScope.launch { - searchContacts.clear() - if(newValue.length >= 3 && contactsPermissionState?.status?.isGranted == true) - searchContacts.addAll(UiUtil.getLocalContacts(context, newValue)) - } - }, - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - modifier = Modifier - .fillMaxWidth() - .border(0.dp, Color.Transparent) - .bringIntoViewRequester(bringIntoViewRequester) - ) - } - } } } } - if(contactsPermissionState?.status?.shouldShowRationale == false && !contactsPermissionState.status.isGranted) { // second part = permission is NOT permanently denied! + if(checkPermission && contactsPermissionState?.status?.shouldShowRationale == false && !contactsPermissionState.status.isGranted) { // second part = permission is NOT permanently denied! RequestPermissionDialog( text = stringResource(id = R.string.edit_fragment_app_permission_message), onConfirm = { contactsPermissionState.launchPermissionRequest() } @@ -234,7 +224,7 @@ fun DetailsCardContact_Preview() { MaterialTheme { DetailsCardContact( initialContact = "John Doe, +1 555 5545", - isEditMode = false, + isReadOnly = false, onContactUpdated = { } ) } @@ -247,7 +237,7 @@ fun DetailsCardContact_Preview_tel() { MaterialTheme { DetailsCardContact( initialContact = "John Doe, +43 676 12 34 567, john@doe.com", - isEditMode = false, + isReadOnly = false, onContactUpdated = { } ) } @@ -260,7 +250,7 @@ fun DetailsCardContact_Preview_edit() { MaterialTheme { DetailsCardContact( initialContact = "John Doe, +1 555 5545", - isEditMode = true, + isReadOnly = true, onContactUpdated = { } ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDates.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDates.kt index f605f20f8..84b219697 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDates.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDates.kt @@ -13,8 +13,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -39,15 +37,14 @@ import java.time.ZonedDateTime @Composable fun DetailsCardDates( icalObject: ICalObject, - isEditMode: Boolean, enableDtstart: Boolean, enableDue: Boolean, enableCompleted: Boolean, allowCompletedChange: Boolean, + isReadOnly: Boolean, onDtstartChanged: (Long?, String?) -> Unit, onDueChanged: (Long?, String?) -> Unit, onCompletedChanged: (Long?, String?) -> Unit, - toggleEditMode: () -> Unit, modifier: Modifier = Modifier ) { @@ -63,17 +60,16 @@ fun DetailsCardDates( var completed by remember { mutableStateOf(icalObject.completed) } var completedTimezone by remember { mutableStateOf(icalObject.completedTimezone) } - ElevatedCard(modifier = modifier) { Column( - modifier = Modifier.fillMaxWidth().padding(if(isEditMode) 4.dp else 0.dp), + modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp), ) { if((icalObject.module == Module.JOURNAL.name || icalObject.module == Module.TODO.name) - && (dtstart != null || (isEditMode && (enableDtstart || icalObject.getModuleFromString() == Module.JOURNAL)))) { + && (dtstart != null || (enableDtstart || icalObject.getModuleFromString() == Module.JOURNAL))) { HorizontalDateCard( datetime = dtstart, timezone = dtstartTimezone, - isEditMode = isEditMode, + isReadOnly = isReadOnly, onDateTimeChanged = { datetime, timezone -> if((due ?: Long.MAX_VALUE) <= (datetime ?: Long.MIN_VALUE)) { Toast.makeText(context, context.getText(R.string.edit_validation_errors_dialog_due_date_before_dtstart), Toast.LENGTH_LONG).show() @@ -106,16 +102,15 @@ fun DetailsCardDates( null, allowNull = icalObject.module == Module.TODO.name, dateOnly = false, - toggleEditMode = toggleEditMode ) } AnimatedVisibility (icalObject.module == Module.TODO.name - && (due != null || (isEditMode && enableDue))) { + && (due != null || enableDue)) { HorizontalDateCard( datetime = due, timezone = dueTimezone, - isEditMode = isEditMode, + isReadOnly = isReadOnly, onDateTimeChanged = { datetime, timezone -> if((datetime ?: Long.MAX_VALUE) <= (dtstart ?: Long.MIN_VALUE)) { Toast.makeText( @@ -144,16 +139,15 @@ fun DetailsCardDates( pickerMinDate = dtstart?.let { Instant.ofEpochMilli(it).atZone(DateTimeUtils.requireTzId(dtstartTimezone)) }, labelTop = stringResource(id = R.string.due), allowNull = icalObject.module == Module.TODO.name, - dateOnly = false, - toggleEditMode = toggleEditMode + dateOnly = false ) } AnimatedVisibility (icalObject.module == Module.TODO.name - && (completed != null || (isEditMode && enableCompleted))) { + && (completed != null || enableCompleted)) { HorizontalDateCard( datetime = completed, timezone = completedTimezone, - isEditMode = isEditMode, + isReadOnly = isReadOnly && allowCompletedChange, onDateTimeChanged = { datetime, timezone -> completed = datetime completedTimezone = timezone @@ -162,13 +156,11 @@ fun DetailsCardDates( pickerMinDate = dtstart?.let { Instant.ofEpochMilli(it).atZone(DateTimeUtils.requireTzId(dtstartTimezone)) }, labelTop = stringResource(id = R.string.completed), allowNull = icalObject.module == Module.TODO.name, - dateOnly = false, - enabled = allowCompletedChange, - toggleEditMode = toggleEditMode + dateOnly = false ) } } - } + } @Preview(showBackground = true) @@ -177,15 +169,14 @@ fun DetailsCardDates_Journal_Preview() { MaterialTheme { DetailsCardDates( icalObject = ICalObject.createJournal(), - isEditMode = false, + isReadOnly = false, enableDtstart = true, enableDue = false, enableCompleted = false, allowCompletedChange = true, onDtstartChanged = { _, _ -> }, onDueChanged = { _, _ -> }, - onCompletedChanged = { _, _ -> }, - toggleEditMode = {} + onCompletedChanged = { _, _ -> } ) } } @@ -203,15 +194,14 @@ fun DetailsCardDates_Todo_Preview() { this.completed = System.currentTimeMillis() this.completedTimezone = TZ_ALLDAY }, - isEditMode = false, + isReadOnly = false, enableDtstart = true, enableDue = false, enableCompleted = false, allowCompletedChange = true, onDtstartChanged = { _, _ -> }, onDueChanged = { _, _ -> }, - onCompletedChanged = { _, _ -> }, - toggleEditMode = {} + onCompletedChanged = { _, _ -> } ) } } @@ -224,15 +214,14 @@ fun DetailsCardDates_Journal_edit_Preview() { MaterialTheme { DetailsCardDates( icalObject = ICalObject.createJournal(), - isEditMode = true, + isReadOnly = false, enableDtstart = true, enableDue = true, enableCompleted = false, allowCompletedChange = true, onDtstartChanged = { _, _ -> }, onDueChanged = { _, _ -> }, - onCompletedChanged = { _, _ -> }, - toggleEditMode = {} + onCompletedChanged = { _, _ -> } ) } } @@ -248,15 +237,14 @@ fun DetailsCardDates_Todo_edit_Preview() { this.due = System.currentTimeMillis() this.dueTimezone = TZ_ALLDAY }, - isEditMode = true, + isReadOnly = false, enableDtstart = true, enableDue = false, enableCompleted = true, allowCompletedChange = true, onDtstartChanged = { _, _ -> }, onDueChanged = { _, _ -> }, - onCompletedChanged = { _, _ -> }, - toggleEditMode = {} + onCompletedChanged = { _, _ -> } ) } } @@ -271,15 +259,14 @@ fun DetailsCardDates_Todo_edit_Preview_completed_hidden() { this.due = System.currentTimeMillis() this.dueTimezone = TZ_ALLDAY }, - isEditMode = true, + isReadOnly = false, enableDtstart = true, enableDue = false, enableCompleted = false, allowCompletedChange = true, onDtstartChanged = { _, _ -> }, onDueChanged = { _, _ -> }, - onCompletedChanged = { _, _ -> }, - toggleEditMode = {} + onCompletedChanged = { _, _ -> } ) } } @@ -290,15 +277,14 @@ fun DetailsCardDates_Note_edit_Preview() { MaterialTheme { DetailsCardDates( icalObject = ICalObject.createNote(), - isEditMode = true, + isReadOnly = false, enableDtstart = true, enableDue = true, enableCompleted = true, allowCompletedChange = true, onDtstartChanged = { _, _ -> }, onDueChanged = { _, _ -> }, - onCompletedChanged = { _, _ -> }, - toggleEditMode = {} + onCompletedChanged = { _, _ -> } ) } } @@ -309,15 +295,14 @@ fun DetailsCardDates_Note_Preview() { MaterialTheme { DetailsCardDates( icalObject = ICalObject.createNote(), - isEditMode = false, + isReadOnly = false, enableDtstart = true, enableDue = true, enableCompleted = true, allowCompletedChange = true, onDtstartChanged = { _, _ -> }, onDueChanged = { _, _ -> }, - onCompletedChanged = { _, _ -> }, - toggleEditMode = {} + onCompletedChanged = { _, _ -> } ) } } \ No newline at end of file diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt index 4ee063beb..f5718e7a0 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt @@ -73,15 +73,47 @@ fun DetailsCardStatusClassificationPriority( if(!isEditMode && (icalObject.status?.isNotEmpty() == true || icalObject.xstatus?.isNotEmpty() == true)) { ElevatedAssistChip( + enabled = allowStatusChange, label = { if(icalObject.xstatus?.isNotEmpty() == true) Text(icalObject.xstatus!!) else Text( - text = Status.values().find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "", + text = Status.entries.find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "", maxLines = 1, overflow = TextOverflow.Ellipsis ) + + DropdownMenu( + expanded = statusMenuExpanded, + onDismissRequest = { statusMenuExpanded = false } + ) { + + Status.valuesFor(icalObject.getModuleFromString()).forEach { status -> + DropdownMenuItem( + text = { Text(stringResource(id = status.stringResource)) }, + onClick = { + icalObject.status = status.status + icalObject.xstatus = null + statusMenuExpanded = false + onStatusChanged(status) + } + ) + } + extendedStatuses + .filter { it.module == icalObject.getModuleFromString() } + .forEach { storedStatus -> + DropdownMenuItem( + text = { Text(storedStatus.xstatus) }, + onClick = { + icalObject.xstatus = storedStatus.xstatus + icalObject.status = storedStatus.rfcStatus.status + statusMenuExpanded = false + onStatusChanged(storedStatus.rfcStatus) + } + ) + } + } }, leadingIcon = { Icon( @@ -89,17 +121,21 @@ fun DetailsCardStatusClassificationPriority( stringResource(id = R.string.status) ) }, - onClick = { }, + onClick = { statusMenuExpanded = true }, modifier = Modifier.weight(0.33f) ) } else if(isEditMode && (enableStatus || !icalObject.status.isNullOrEmpty() || !icalObject.xstatus.isNullOrEmpty())) { AssistChip( enabled = allowStatusChange, label = { - if(!icalObject.xstatus.isNullOrEmpty()) + if(icalObject.xstatus?.isNotEmpty() == true) Text(icalObject.xstatus!!) else - Text(Status.values().find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "") + Text( + text = Status.entries.find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) DropdownMenu( expanded = statusMenuExpanded, @@ -147,22 +183,43 @@ fun DetailsCardStatusClassificationPriority( if(!isEditMode && !icalObject.classification.isNullOrEmpty()) { ElevatedAssistChip( label = { - Text( Classification.values().find { it.classification == icalObject.classification}?.stringResource?.let { stringResource(id = it)}?: icalObject.classification ?:"", maxLines = 1, overflow = TextOverflow.Ellipsis) - }, + Text( + Classification.entries.find { it.classification == icalObject.classification }?.stringResource?.let { stringResource(id = it) }?: icalObject.classification ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + DropdownMenu( + expanded = classificationMenuExpanded, + onDismissRequest = { classificationMenuExpanded = false } + ) { + + Classification.entries.forEach { clazzification -> + DropdownMenuItem( + text = { Text(stringResource(id = clazzification.stringResource)) }, + onClick = { + icalObject.classification = clazzification.classification + classificationMenuExpanded = false + onClassificationChanged(clazzification) + } + ) + } + } + }, leadingIcon = { Icon( Icons.Outlined.GppMaybe, stringResource(id = R.string.classification) ) }, - onClick = { }, + onClick = { classificationMenuExpanded = true }, modifier = Modifier.weight(0.33f) ) } else if(isEditMode && (enableClassification || !icalObject.classification.isNullOrEmpty())) { AssistChip( label = { Text( - Classification.values().find { it.classification == icalObject.classification }?.stringResource?.let { stringResource(id = it) }?: icalObject.classification ?: "", + Classification.entries.find { it.classification == icalObject.classification }?.stringResource?.let { stringResource(id = it) }?: icalObject.classification ?: "", maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -172,7 +229,7 @@ fun DetailsCardStatusClassificationPriority( onDismissRequest = { classificationMenuExpanded = false } ) { - Classification.values().forEach { clazzification -> + Classification.entries.forEach { clazzification -> DropdownMenuItem( text = { Text(stringResource(id = clazzification.stringResource)) }, onClick = { @@ -209,6 +266,22 @@ fun DetailsCardStatusClassificationPriority( maxLines = 1, overflow = TextOverflow.Ellipsis ) + + DropdownMenu( + expanded = priorityMenuExpanded, + onDismissRequest = { priorityMenuExpanded = false } + ) { + stringArrayResource(id = R.array.priority).forEachIndexed { index, prio -> + DropdownMenuItem( + text = { Text(prio) }, + onClick = { + icalObject.priority = if(index == 0) null else index + priorityMenuExpanded = false + onPriorityChanged(icalObject.priority) + } + ) + } + } }, leadingIcon = { Icon( @@ -216,7 +289,7 @@ fun DetailsCardStatusClassificationPriority( stringResource(id = R.string.priority) ) }, - onClick = { }, + onClick = { priorityMenuExpanded = true }, modifier = Modifier.weight(0.33f) ) } else if(isEditMode && (enablePriority || icalObject.priority in 1..9)) { diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt index 53dc9b752..262dc3fd7 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt @@ -10,17 +10,30 @@ package at.techbee.jtx.ui.detail import android.content.ActivityNotFoundException import android.util.Log -import androidx.compose.animation.Crossfade +import androidx.compose.animation.AnimatedVisibility 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.text.BasicTextField import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.Link -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -30,65 +43,71 @@ import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon import at.techbee.jtx.util.UiUtil -@OptIn(ExperimentalMaterial3Api::class) @Composable fun DetailsCardUrl( initialUrl: String, - isEditMode: Boolean, + isReadOnly: Boolean, onUrlUpdated: (String) -> Unit, modifier: Modifier = Modifier ) { val headline = stringResource(id = R.string.url) var url by rememberSaveable { mutableStateOf(initialUrl) } + val isValidURL = url.isNotEmpty() && UiUtil.isValidURL(url) val uriHandler = LocalUriHandler.current + val focusRequester = remember { FocusRequester() } - ElevatedCard(modifier = modifier, onClick = { - try { - if (url.isNotBlank() && !isEditMode) - uriHandler.openUri(url) - } catch (e: ActivityNotFoundException) { - Log.d("PropertyCardUrl", "Failed opening Uri $url\n$e") - } - }) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp),) { - - Crossfade(isEditMode) { - if(!it) { - - Column { - HeadlineWithIcon(icon = Icons.Outlined.Link, iconDesc = headline, text = headline) - Text(url) - } - } else { - - OutlinedTextField( - value = url, - leadingIcon = { Icon(Icons.Outlined.Link, headline) }, - trailingIcon = { - IconButton(onClick = { - url = "" - onUrlUpdated(url) - }) { - if (url.isNotEmpty()) - Icon(Icons.Outlined.Clear, stringResource(id = R.string.delete)) - } - }, - singleLine = true, - isError = url.isNotEmpty() && !UiUtil.isValidURL(url), - label = { Text(headline) }, - onValueChange = { newUrl -> - url = newUrl - onUrlUpdated(url) - }, - modifier = Modifier - .fillMaxWidth() + ElevatedCard( + modifier = modifier, + onClick = { focusRequester.requestFocus() } + ) { + + Row { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .weight(1f) + ) { + HeadlineWithIcon(icon = Icons.Outlined.Link, iconDesc = headline, text = headline) + + BasicTextField( + value = url, + textStyle = LocalTextStyle.current, + onValueChange = { newUrl -> + url = newUrl + onUrlUpdated(url) + }, + enabled = !isReadOnly, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .focusRequester(focusRequester) + ) + + AnimatedVisibility(!isValidURL) { + Text( + text = stringResource(id = R.string.invalid_url_message), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error ) } + } + + AnimatedVisibility(isValidURL) { + IconButton(onClick = { + try { + uriHandler.openUri(url) + } catch (e: ActivityNotFoundException) { + Log.d("PropertyCardUrl", "Failed opening Uri $url\n$e") + } + }) { + Icon( + Icons.AutoMirrored.Outlined.OpenInNew, + stringResource(id = R.string.open_in_browser) + ) + } } } } @@ -100,7 +119,7 @@ fun DetailsCardUrl_Preview() { MaterialTheme { DetailsCardUrl( initialUrl = "www.orf.at", - isEditMode = false, + isReadOnly = false, onUrlUpdated = { } ) } @@ -113,7 +132,20 @@ fun DetailsCardUrl_Preview_edit() { MaterialTheme { DetailsCardUrl( initialUrl = "www.bitfire.at", - isEditMode = true, + isReadOnly = true, + onUrlUpdated = { } + ) + } +} + + +@Preview(showBackground = true) +@Composable +fun DetailsCardUrl_Preview_invalid_URL() { + MaterialTheme { + DetailsCardUrl( + initialUrl = "invalid url", + isReadOnly = true, onUrlUpdated = { } ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt index 95d115892..9f949c900 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt @@ -17,16 +17,22 @@ import android.os.Build import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.EventNote import androidx.compose.material.icons.automirrored.outlined.Note import androidx.compose.material.icons.automirrored.outlined.NoteAdd import androidx.compose.material.icons.outlined.AddTask +import androidx.compose.material.icons.outlined.ChevronRight +import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ContentPaste +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.KeyboardArrowDown import androidx.compose.material.icons.outlined.Mail import androidx.compose.material.icons.outlined.TaskAlt +import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -314,6 +320,9 @@ fun DetailsScreen( onAddSubtask = { subtaskText -> newSubtaskTextToProcess = subtaskText }, actions = { val menuExpanded = remember { mutableStateOf(false) } + val shareOptionsExpanded = remember { mutableStateOf(false) } + val copyConvertOptionsExpanded = remember { mutableStateOf(false) } + OverflowMenu(menuExpanded = menuExpanded) { @@ -372,60 +381,82 @@ fun DetailsScreen( HorizontalDivider() if (!isEditMode.value) { + DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_share_mail)) }, - onClick = { - detailViewModel.shareAsText(context) - menuExpanded.value = false - }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.Mail, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - } - ) - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_share_ics)) }, - onClick = { - detailViewModel.shareAsICS(context) - menuExpanded.value = false - }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.Description, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - } + text = { Text(text = stringResource(R.string.menu_view_share)) }, + onClick = { shareOptionsExpanded.value = !shareOptionsExpanded.value }, + trailingIcon = { Icon(if(shareOptionsExpanded.value) Icons.Outlined.KeyboardArrowDown else Icons.Outlined.ChevronRight, null) } ) - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_copy_to_clipboard)) }, - onClick = { - scope.launch(Dispatchers.IO) { - ICalDatabase - .getInstance(context) - .iCalDatabaseDao() - .getSync(iCalObject.value?.id!!) - ?.let { - val text = it.getShareText(context) - val clipboardManager = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - clipboardManager.setPrimaryClip(ClipData.newPlainText("", text)) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) // Only show a toast for Android 12 and lower. - detailViewModel.toastMessage.value = context.getString(R.string.menu_view_copy_to_clipboard_copied) - } + + AnimatedVisibility(shareOptionsExpanded.value) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.menu_view_share_mail)) }, + onClick = { + detailViewModel.shareAsText(context) + menuExpanded.value = false + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Mail, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) } - menuExpanded.value = false - }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.ContentPaste, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface + ) + } + AnimatedVisibility(shareOptionsExpanded.value) { + + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.menu_view_share_ics)) }, + onClick = { + detailViewModel.shareAsICS(context) + menuExpanded.value = false + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Description, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } ) - } - ) + } + AnimatedVisibility(shareOptionsExpanded.value) { + + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.menu_view_copy_to_clipboard)) }, + onClick = { + scope.launch(Dispatchers.IO) { + ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getSync(iCalObject.value?.id!!) + ?.let { + val text = it.getShareText(context) + val clipboardManager = + context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + clipboardManager.setPrimaryClip( + ClipData.newPlainText( + "", + text + ) + ) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) // Only show a toast for Android 12 and lower. + detailViewModel.toastMessage.value = + context.getString(R.string.menu_view_copy_to_clipboard_copied) + } + } + menuExpanded.value = false + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.ContentPaste, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + ) + } } @@ -442,60 +473,152 @@ fun DetailsScreen( HorizontalDivider() - CheckboxWithText( - text = stringResource(id = R.string.menu_view_markdown_formatting), - onCheckedChange = { - detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] = it - detailViewModel.detailSettings.save() - }, - isSelected = detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] ?: true, - ) + if(!isEditMode.value + && collection.value?.readonly == false + && isProActionAvailable) { - HorizontalDivider() + DropdownMenuItem( + text = { Text(text = stringResource(R.string.menu_view_copy_convert)) }, + onClick = { copyConvertOptionsExpanded.value = !copyConvertOptionsExpanded.value }, + trailingIcon = { Icon(if(copyConvertOptionsExpanded.value) Icons.Outlined.KeyboardArrowDown else Icons.Outlined.ChevronRight, null) } + ) + + + if(collection.value?.supportsVJOURNAL == true) { + AnimatedVisibility (copyConvertOptionsExpanded.value && iCalObject.value?.getModuleFromString() != Module.JOURNAL) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.menu_view_convert_to_journal)) }, + onClick = { + detailViewModel.convertTo(Module.JOURNAL) + menuExpanded.value = false + }, + leadingIcon = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.EventNote, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + ) + } + AnimatedVisibility (copyConvertOptionsExpanded.value && iCalObject.value?.getModuleFromString() != Module.NOTE) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.menu_view_convert_to_note)) }, + onClick = { + detailViewModel.convertTo(Module.NOTE) + menuExpanded.value = false + }, + leadingIcon = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.Note, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + ) + } + } + if(collection.value?.supportsVTODO == true) { + AnimatedVisibility(copyConvertOptionsExpanded.value && iCalObject.value?.getModuleFromString() != Module.TODO) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.menu_view_convert_to_task)) }, + onClick = { + detailViewModel.convertTo(Module.TODO) + menuExpanded.value = false + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.TaskAlt, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + ) + } + } - if(collection.value?.readonly == false && collection.value?.supportsVJOURNAL == true) { - if (iCalObject.value?.module != Module.JOURNAL.name) { + AnimatedVisibility (copyConvertOptionsExpanded.value) { + HorizontalDivider() + } + + AnimatedVisibility (copyConvertOptionsExpanded.value && collection.value?.supportsVJOURNAL == true) { DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_convert_to_journal)) }, - onClick = { detailViewModel.convertTo(Module.JOURNAL) }, - leadingIcon = { - Icon( - imageVector = Icons.AutoMirrored.Outlined.EventNote, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) + leadingIcon = { Icon(Icons.Outlined.ContentCopy, null) }, + text = { Text(stringResource(id = R.string.menu_view_copy_as_journal)) }, + onClick = { + detailViewModel.createCopy(Module.JOURNAL) + menuExpanded.value = false } ) } - if (iCalObject.value?.module != Module.NOTE.name) { + AnimatedVisibility (copyConvertOptionsExpanded.value && collection.value?.supportsVJOURNAL == true) { + DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_convert_to_note)) }, - onClick = { detailViewModel.convertTo(Module.NOTE) }, - leadingIcon = { - Icon( - imageVector = Icons.AutoMirrored.Outlined.Note, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) + leadingIcon = { Icon(Icons.Outlined.ContentCopy, null) }, + text = { Text(stringResource(id = R.string.menu_view_copy_as_note)) }, + onClick = { + detailViewModel.createCopy(Module.NOTE) + menuExpanded.value = false } ) } - } - if(collection.value?.readonly == false && collection.value?.supportsVTODO == true) { - if(iCalObject.value?.module != Module.TODO.name) { + AnimatedVisibility (copyConvertOptionsExpanded.value && collection.value?.supportsVTODO == true) { DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_convert_to_task)) }, - onClick = { detailViewModel.convertTo(Module.TODO) }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.TaskAlt, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) + leadingIcon = { Icon(Icons.Outlined.ContentCopy, null) }, + text = { Text(stringResource(id = R.string.menu_view_copy_as_todo)) }, + onClick = { + detailViewModel.createCopy(Module.TODO) + menuExpanded.value = false } ) } + + HorizontalDivider() + } + + if( + collection.value?.readonly == false + && isProActionAvailable + && (markdownState.value == MarkdownState.DISABLED || markdownState.value == MarkdownState.CLOSED) + ) { + DropdownMenuItem( + leadingIcon = { Icon(Icons.Outlined.Delete, null) }, + text = { Text(stringResource(id = R.string.delete)) }, + onClick = { + showDeleteDialog = true + menuExpanded.value = false + } + ) + + HorizontalDivider() + } + + + CheckboxWithText( + text = stringResource(id = R.string.menu_view_markdown_formatting), + onCheckedChange = { + detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] = it + detailViewModel.detailSettings.save() + }, + isSelected = detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] ?: true, + ) + + HorizontalDivider() + + DropdownMenuItem( + leadingIcon = { Icon(Icons.Outlined.Visibility, null) }, + onClick = { + scope.launch { + if (detailsBottomSheetState.isVisible) + detailsBottomSheetState.hide() + else + detailsBottomSheetState.show() + } + menuExpanded.value = false + }, + text = { Text(stringResource(id = R.string.preferences)) } + ) } } ) @@ -583,16 +706,12 @@ fun DetailsScreen( }, bottomBar = { DetailBottomAppBar( - icalObject = iCalObject.value, - seriesElement = seriesElement.value, + iCalObject = iCalObject.value, collection = collection.value, isEditMode = isEditMode, markdownState = markdownState, isProActionAvailable = isProActionAvailable, changeState = detailViewModel.changeState, - detailsBottomSheetState = detailsBottomSheetState, - onDeleteClicked = { showDeleteDialog = true }, - onCopyRequested = { newModule -> detailViewModel.createCopy(newModule) }, onRevertClicked = { showRevertDialog = true } ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt index 97b229deb..077f2608c 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt @@ -13,7 +13,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -22,7 +21,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AttachFile import androidx.compose.material.icons.outlined.Forum @@ -417,29 +415,10 @@ fun ListCard( AnimatedVisibility(visible = isAttachmentsExpanded) { Column(verticalArrangement = Arrangement.spacedBy(1.dp)) { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - attachments.asReversed().filter { it.fmttype?.startsWith("image/") == true } - .forEach { attachment -> - AttachmentCard( - attachment = attachment, - isEditMode = false, - isRemoteCollection = false, // ATTENTION: We pass false here, because the warning for large file sizes is only relevant for edit mode - player = player, - onAttachmentDeleted = { /* nothing to do, no edit here */ }, - modifier = Modifier.size(100.dp, 140.dp) - ) - } - } - - attachments.asReversed().filter { it.fmttype == null || it.fmttype?.startsWith("image/") == false }.forEach { attachment -> + attachments.asReversed().forEach { attachment -> AttachmentCard( attachment = attachment, - isEditMode = false, + isReadOnly = true, isRemoteCollection = false, // ATTENTION: We pass false here, because the warning for large file sizes is only relevant for edit mode player = player, onAttachmentDeleted = { /* nothing to do, no edit here */ }, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt index c6483a44d..f79043d6d 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt @@ -265,7 +265,11 @@ fun ListQuickAddElement( currentModule = Module.TODO else if (currentModule == Module.TODO && currentCollection?.supportsVTODO == false) currentModule = Module.NOTE - } + }, + showSyncButton = false, + showColorPicker = false, + enableSelector = true, + onColorPicked = { } ) OutlinedTextField( @@ -355,7 +359,7 @@ fun ListQuickAddElement( return@forEach AttachmentCard( attachment = attachment, - isEditMode = true, + isReadOnly = false, isRemoteCollection = currentCollection?.accountType != LOCAL_ACCOUNT_TYPE, player = player, onAttachmentDeleted = { currentAttachments.remove(attachment) }, diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/AlarmCard.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/AlarmCard.kt index 231223f45..e14dd94d2 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/AlarmCard.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/AlarmCard.kt @@ -17,11 +17,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Alarm import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -39,7 +37,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.techbee.jtx.R import at.techbee.jtx.database.ICalObject -import at.techbee.jtx.database.ICalObject.Companion.TZ_ALLDAY import at.techbee.jtx.database.properties.Alarm import at.techbee.jtx.database.properties.AlarmRelativeTo import at.techbee.jtx.ui.reusable.dialogs.DatePickerDialog @@ -50,12 +47,11 @@ import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.minutes -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AlarmCard( alarm: Alarm, icalObject: ICalObject, - isEditMode: Boolean, + isReadOnly: Boolean, modifier: Modifier = Modifier, onAlarmDeleted: () -> Unit, onAlarmChanged: (Alarm) -> Unit @@ -93,131 +89,70 @@ fun AlarmCard( ) } - if (isEditMode) { - OutlinedCard( - onClick = { - if (alarm.triggerRelativeDuration != null) - showDurationPickerDialog = true - else if (alarm.triggerTime != null) - showDateTimePickerDialog = true - }, - modifier = modifier + ElevatedCard( + onClick = { + if(isReadOnly) + return@ElevatedCard + else if (alarm.triggerRelativeDuration != null) + showDurationPickerDialog = true + else if (alarm.triggerTime != null) + showDateTimePickerDialog = true + }, + modifier = modifier + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Outlined.Alarm, stringResource(R.string.alarms)) + Icon(Icons.Outlined.Alarm, stringResource(R.string.alarms)) - Column( - modifier = Modifier - .padding(horizontal = 8.dp) - .align(alignment = Alignment.CenterVertically) - .weight(1f) - ) { - if (alarm.triggerRelativeDuration != null) { - alarm.getTriggerDurationAsString(context)?.let { durationText -> - Text( - text = durationText, - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - if (alarm.triggerTime != null) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .align(alignment = Alignment.CenterVertically) + .weight(1f) + ) { + if (alarm.triggerRelativeDuration != null) { + alarm.getTriggerDurationAsString(context)?.let { durationText -> Text( - text = DateTimeUtils.convertLongToFullDateTimeString( - alarm.triggerTime, - alarm.triggerTimezone - ), + text = durationText, modifier = Modifier .padding(horizontal = 8.dp) .fillMaxWidth(), maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontStyle = FontStyle.Italic, - fontWeight = FontWeight.Bold + overflow = TextOverflow.Ellipsis ) } } - - IconButton(onClick = { onAlarmDeleted() }) { - Icon(Icons.Outlined.Delete, stringResource(id = R.string.delete)) + if (alarm.triggerTime != null) { + Text( + text = DateTimeUtils.convertLongToFullDateTimeString( + alarm.triggerTime, + alarm.triggerTimezone + ), + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.Bold + ) } } - } - } else { - ElevatedCard( - modifier = modifier - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Outlined.Alarm, stringResource(R.string.alarms)) - - Column( - modifier = Modifier - .padding(horizontal = 8.dp) - .align(alignment = Alignment.CenterVertically) - .weight(1f) - ) { - if (alarm.triggerRelativeDuration != null) { - alarm.getTriggerDurationAsString(context)?.let { durationText -> - Text( - text = durationText, - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - if (alarm.triggerTime != null) { - Text( - text = DateTimeUtils.convertLongToFullDateTimeString( - alarm.triggerTime, - alarm.triggerTimezone - ), - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontStyle = FontStyle.Italic, - fontWeight = FontWeight.Bold - ) - - if(alarm.triggerTimezone != null && alarm.triggerTimezone != TZ_ALLDAY && alarm.triggerTimezone != ZoneId.systemDefault().id) { - Text( - text = DateTimeUtils.convertLongToFullDateTimeString( - alarm.triggerTime, - ZoneId.systemDefault().id - ), - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } + if(!isReadOnly) { + IconButton(onClick = { onAlarmDeleted() }) { + Icon(Icons.Outlined.Delete, stringResource(id = R.string.delete)) } } } } + } @Preview(showBackground = true) @@ -227,7 +162,7 @@ fun AlarmCardPreview_DateTime_view() { AlarmCard( alarm = Alarm.createDisplayAlarm(System.currentTimeMillis(), null), icalObject = ICalObject.createTodo().apply { dtstart = System.currentTimeMillis() }, - isEditMode = false, + isReadOnly = false, onAlarmDeleted = { }, onAlarmChanged = { } ) @@ -246,7 +181,7 @@ fun AlarmCardPreview_Duration_START_view() { null ), icalObject = ICalObject.createTodo().apply { dtstart = System.currentTimeMillis() }, - isEditMode = false, + isReadOnly = false, onAlarmDeleted = { }, onAlarmChanged = { } ) @@ -269,7 +204,7 @@ fun AlarmCardPreview_Duration_END_view() { due = System.currentTimeMillis() dueTimezone = null }, - isEditMode = false, + isReadOnly = false, onAlarmDeleted = { }, onAlarmChanged = { } ) @@ -292,7 +227,7 @@ fun AlarmCardPreview_Duration_END_view_timezone() { due = System.currentTimeMillis() dueTimezone = ZoneId.of("Mexico/General").id }, - isEditMode = false, + isReadOnly = true, onAlarmDeleted = { }, onAlarmChanged = { } ) @@ -301,14 +236,14 @@ fun AlarmCardPreview_Duration_END_view_timezone() { @Preview(showBackground = true) @Composable -fun AlarmCardPreview_edit() { +fun AlarmCardPreview_readOnly() { MaterialTheme { AlarmCard( alarm = Alarm( triggerTime = System.currentTimeMillis() ), icalObject = ICalObject.createTodo().apply { dtstart = System.currentTimeMillis() }, - isEditMode = true, + isReadOnly = true, onAlarmDeleted = { }, onAlarmChanged = { } ) diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/AttachmentCard.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/AttachmentCard.kt index 662a7c283..af83d635c 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/AttachmentCard.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/AttachmentCard.kt @@ -13,19 +13,28 @@ import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.OpenInNew -import androidx.compose.material.icons.outlined.* -import androidx.compose.material3.* +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.ImageNotSupported +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode @@ -41,11 +50,10 @@ import at.techbee.jtx.util.UiUtil import java.io.IOException -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AttachmentCard( attachment: Attachment, - isEditMode: Boolean, + isReadOnly: Boolean, isRemoteCollection: Boolean, player: MediaPlayer?, modifier: Modifier = Modifier, @@ -53,8 +61,8 @@ fun AttachmentCard( ) { val context = LocalContext.current - val preview = attachment.getPreview(context) - val filesize = attachment.getFilesize(context) + val preview = if(LocalInspectionMode.current) null else attachment.getPreview(context) + val filesize = if(LocalInspectionMode.current) 10000000L else attachment.getFilesize(context) val resultExportFilepath = remember { mutableStateOf(null) } val launcherExportSingle = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(attachment.fmttype?:"*/*")) { @@ -82,53 +90,49 @@ fun AttachmentCard( } } - Card( - modifier = modifier, - colors = if(isEditMode) CardDefaults.outlinedCardColors() else CardDefaults.elevatedCardColors(), - elevation = if(isEditMode) CardDefaults.outlinedCardElevation() else CardDefaults.elevatedCardElevation(), - border = if(isEditMode) CardDefaults.outlinedCardBorder() else null, - onClick = { if(!isEditMode) attachment.openFile(context) }, + ElevatedCard( + onClick = { attachment.openFile(context) }, + modifier = modifier ) { - if (attachment.fmttype?.startsWith("image/") == true) { - //preview - if (preview == null) - Icon( - Icons.Outlined.ImageNotSupported, - attachment.fmttype, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 100.dp, max = 140.dp) - .padding(4.dp) - ) - else - Image( - bitmap = preview.asImageBitmap(), - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 100.dp, max = 140.dp) - .padding(4.dp) - ) - } else if((attachment.fmttype?.startsWith("audio/") == true || attachment.fmttype?.startsWith("video/") == true) && attachment.uri != null) { - Uri.parse(attachment.uri)?.let { - AudioPlaybackElement( - uri = it, - player = player, - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) - } - } - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + //horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { + if (attachment.fmttype?.startsWith("image/") == true) { + //preview + if (preview == null) + Icon( + Icons.Outlined.ImageNotSupported, + attachment.fmttype, + modifier = Modifier + //.fillMaxWidth() + .heightIn(min = 50.dp, max = 100.dp) + .padding(4.dp) + ) + else + Image( + bitmap = preview.asImageBitmap(), + contentDescription = null, + modifier = Modifier + //.fillMaxWidth() + .heightIn(min = 50.dp, max = 100.dp) + .padding(4.dp) + ) + } else if((attachment.fmttype?.startsWith("audio/") == true || attachment.fmttype?.startsWith("video/") == true) && attachment.uri != null) { + Uri.parse(attachment.uri)?.let { + AudioPlaybackElement( + uri = it, + player = player, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } + } + Text( attachment.getFilenameOrLink() ?: "", modifier = Modifier @@ -139,37 +143,35 @@ fun AttachmentCard( overflow = TextOverflow.Ellipsis ) - AnimatedVisibility(isEditMode && LocalInspectionMode.current || ((filesize?: 0) > 100000 && isRemoteCollection)) { - Icon(Icons.Outlined.Warning, null, tint = MaterialTheme.colorScheme.error) - } - - AnimatedVisibility(isEditMode) { - IconButton(onClick = { onAttachmentDeleted() }) { - Icon(Icons.Outlined.Delete, stringResource(id = R.string.delete)) - } - } - - AnimatedVisibility(!isEditMode && filesize != null) { + if(filesize != null) { Text( - text = UiUtil.getAttachmentSizeString(filesize!!), + text = UiUtil.getAttachmentSizeString(filesize), maxLines = 1, - fontStyle = FontStyle.Italic + fontStyle = FontStyle.Italic, + color = if(!isReadOnly && filesize > 100000 && isRemoteCollection) MaterialTheme.colorScheme.error else Color.Unspecified ) } - AnimatedVisibility(!isEditMode && attachment.uri?.startsWith("content://") == true) { + + if(attachment.uri?.startsWith("content://") == true) { IconButton(onClick = { launcherExportSingle.launch(attachment.filename) }) { Icon(Icons.Outlined.Download, stringResource(id = R.string.save)) } } - AnimatedVisibility(!isEditMode && attachment.uri?.startsWith("http") == true) { + if(attachment.uri?.startsWith("http") == true) { IconButton(onClick = { attachment.openFile(context) }) { Icon(Icons.AutoMirrored.Outlined.OpenInNew, stringResource(id = R.string.open_in_browser)) } } + + if(!isReadOnly) { + IconButton(onClick = { onAttachmentDeleted() }) { + Icon(Icons.Outlined.Delete, stringResource(id = R.string.delete)) + } + } } } } @@ -180,7 +182,7 @@ fun AttachmentCardPreview_view() { MaterialTheme { AttachmentCard( attachment = Attachment.getSample(), - isEditMode = false, + isReadOnly = false, isRemoteCollection = true, player = null, onAttachmentDeleted = { } @@ -194,7 +196,7 @@ fun AttachmentCardPreview_edit() { MaterialTheme { AttachmentCard( attachment = Attachment.getSample(), - isEditMode = true, + isReadOnly = true, isRemoteCollection = true, player = null, onAttachmentDeleted = { } @@ -209,7 +211,7 @@ fun AttachmentCardPreview_view_with_preview() { MaterialTheme { AttachmentCard( attachment = Attachment.getSample(), - isEditMode = false, + isReadOnly = false, isRemoteCollection = true, player = null, onAttachmentDeleted = { } @@ -220,11 +222,11 @@ fun AttachmentCardPreview_view_with_preview() { @Preview(showBackground = true) @Composable -fun AttachmentCardPreview_edit_with_preview() { +fun AttachmentCardPreview_readOnly() { MaterialTheme { AttachmentCard( attachment = Attachment.getSample(), - isEditMode = true, + isReadOnly = true, isRemoteCollection = true, player = null, onAttachmentDeleted = { } diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/HorizontalDateCard.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/HorizontalDateCard.kt index 07364c129..b47188299 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/HorizontalDateCard.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/HorizontalDateCard.kt @@ -8,13 +8,10 @@ package at.techbee.jtx.ui.reusable.cards -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -23,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle @@ -41,21 +37,18 @@ import at.techbee.jtx.util.DateTimeUtils import java.time.ZonedDateTime -@OptIn(ExperimentalMaterial3Api::class) @Composable fun HorizontalDateCard( datetime: Long?, timezone: String?, - isEditMode: Boolean, allowNull: Boolean, dateOnly: Boolean, + isReadOnly: Boolean, modifier: Modifier = Modifier, labelTop: String? = null, pickerMinDate: ZonedDateTime? = null, pickerMaxDate: ZonedDateTime? = null, - enabled: Boolean = true, - onDateTimeChanged: (Long?, String?) -> Unit, - toggleEditMode: () -> Unit + onDateTimeChanged: (Long?, String?) -> Unit ) { var showDatePickerDialog by rememberSaveable { mutableStateOf(false) } @@ -64,17 +57,11 @@ fun HorizontalDateCard( val settingDisplayTimezone = DropdownSetting.SETTING_DISPLAY_TIMEZONE.getSetting(prefs) - Card( + ElevatedCard( onClick = { - if(!isEditMode) - toggleEditMode() - showDatePickerDialog = true + if(!isReadOnly) + showDatePickerDialog = true }, - shape = if(isEditMode) CardDefaults.outlinedShape else CardDefaults.elevatedShape, - colors = if(isEditMode) CardDefaults.outlinedCardColors() else CardDefaults.elevatedCardColors(), - elevation = if(isEditMode) CardDefaults.outlinedCardElevation() else CardDefaults.elevatedCardElevation(), - border = if(isEditMode) CardDefaults.outlinedCardBorder() else BorderStroke(0.dp, Color.Transparent), - enabled = !isEditMode || enabled, modifier = modifier ) { @@ -104,29 +91,27 @@ fun HorizontalDateCard( else -> null } ), - fontStyle = if (!isEditMode) FontStyle.Italic else null, - fontWeight = if (!isEditMode) FontWeight.Bold else null + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.Bold ) } - if((isEditMode && timezone != null && timezone != TZ_ALLDAY) - || (timezone != null && timezone != TZ_ALLDAY && + if((timezone != null && timezone != TZ_ALLDAY && (settingDisplayTimezone == DropdownSettingOption.DISPLAY_TIMEZONE_ORIGINAL - || settingDisplayTimezone == DropdownSettingOption.DISPLAY_TIMEZONE_LOCAL_AND_ORIGINAL) - ) + || settingDisplayTimezone == DropdownSettingOption.DISPLAY_TIMEZONE_LOCAL_AND_ORIGINAL)) ) { Text( DateTimeUtils.convertLongToFullDateTimeString( datetime, timezone ), - fontStyle = if (!isEditMode && settingDisplayTimezone == DropdownSettingOption.DISPLAY_TIMEZONE_ORIGINAL) FontStyle.Italic else null, - fontWeight = if (!isEditMode && settingDisplayTimezone == DropdownSettingOption.DISPLAY_TIMEZONE_ORIGINAL) FontWeight.Bold else null + fontStyle = if (settingDisplayTimezone == DropdownSettingOption.DISPLAY_TIMEZONE_ORIGINAL) FontStyle.Italic else null, + fontWeight = if (settingDisplayTimezone == DropdownSettingOption.DISPLAY_TIMEZONE_ORIGINAL) FontWeight.Bold else null ) } } else { Text( text = stringResource(id = R.string.not_set2), - fontStyle = if(!isEditMode) FontStyle.Italic else null + fontStyle = FontStyle.Italic ) } } @@ -156,10 +141,9 @@ fun HorizontalDateCard_Preview_Allday() { datetime = System.currentTimeMillis(), timezone = ICalObject.TZ_ALLDAY, allowNull = true, - isEditMode = false, + isReadOnly = false, dateOnly = false, - onDateTimeChanged = { _, _ -> }, - toggleEditMode = { } + onDateTimeChanged = { _, _ -> } ) } } @@ -172,10 +156,9 @@ fun HorizontalDateCard_Preview_Allday_edit() { datetime = System.currentTimeMillis(), timezone = ICalObject.TZ_ALLDAY, allowNull = true, - isEditMode = true, + isReadOnly = true, dateOnly = false, - onDateTimeChanged = { _, _ -> }, - toggleEditMode = { } + onDateTimeChanged = { _, _ -> } ) } } @@ -187,12 +170,11 @@ fun HorizontalDateCard_Preview_WithTime() { HorizontalDateCard( datetime = System.currentTimeMillis(), timezone = null, - isEditMode = false, + isReadOnly = false, allowNull = true, dateOnly = false, onDateTimeChanged = { _, _ -> }, - labelTop = stringResource(id = R.string.completed), - toggleEditMode = { } + labelTop = stringResource(id = R.string.completed) ) } } @@ -204,11 +186,10 @@ fun HorizontalDateCard_Preview_WithTimezone() { HorizontalDateCard( datetime = System.currentTimeMillis(), timezone = "Europe/Vienna", - isEditMode = false, + isReadOnly = false, allowNull = true, dateOnly = false, - onDateTimeChanged = { _, _ -> }, - toggleEditMode = { } + onDateTimeChanged = { _, _ -> } ) } } @@ -220,11 +201,10 @@ fun HorizontalDateCard_Preview_WithTimezone2() { HorizontalDateCard( datetime = System.currentTimeMillis(), timezone = "Africa/Addis_Ababa", - isEditMode = false, + isReadOnly = false, allowNull = true, dateOnly = false, - onDateTimeChanged = { _, _ -> }, - toggleEditMode = { } + onDateTimeChanged = { _, _ -> } ) } } @@ -236,11 +216,10 @@ fun HorizontalDateCard_Preview_SameOffset() { HorizontalDateCard( datetime = System.currentTimeMillis(), timezone = "Europe/Rome", - isEditMode = false, + isReadOnly = false, allowNull = true, dateOnly = false, - onDateTimeChanged = { _, _ -> }, - toggleEditMode = { } + onDateTimeChanged = { _, _ -> } ) } } @@ -252,12 +231,11 @@ fun HorizontalDateCard_Preview_NotSet() { HorizontalDateCard( datetime = null, timezone = null, - isEditMode = false, + isReadOnly = false, allowNull = true, dateOnly = false, onDateTimeChanged = { _, _ -> }, - labelTop = stringResource(id = R.string.due), - toggleEditMode = { } + labelTop = stringResource(id = R.string.due) ) } } @@ -270,12 +248,11 @@ fun HorizontalDateCard_Preview_edit_NotSet() { HorizontalDateCard( datetime = null, timezone = null, - isEditMode = true, + isReadOnly = true, allowNull = true, dateOnly = false, onDateTimeChanged = { _, _ -> }, - labelTop = stringResource(id = R.string.due), - toggleEditMode = { } + labelTop = stringResource(id = R.string.due) ) } } diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt index a610c62c7..13079a478 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt @@ -14,8 +14,12 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource @@ -55,7 +59,11 @@ fun CollectionSelectorDialog( includeReadOnly = false, includeVJOURNAL = if(module == Module.JOURNAL || module == Module.NOTE) true else null, includeVTODO = if(module == Module.TODO) true else null, - onSelectionChanged = { selected -> selectedCollection = selected } + onSelectionChanged = { selected -> selectedCollection = selected }, + showSyncButton = false, + showColorPicker = false, + enableSelector = true, + onColorPicked = { } ) } } diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt index d19646313..80ca051bb 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt @@ -14,7 +14,11 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource @@ -48,7 +52,11 @@ fun CollectionsMoveCollectionDialog( includeReadOnly = false, includeVJOURNAL = if((current.numJournals?:0) + (current.numNotes?: 0) > 0) true else null, includeVTODO = if((current.numTodos?:0) > 0) true else null, - onSelectionChanged = { selected -> newCollection = selected } + onSelectionChanged = { selected -> newCollection = selected }, + showSyncButton = false, + showColorPicker = false, + enableSelector = true, + onColorPicked = { } ) Text(stringResource(id = R.string.collection_dialog_move_info)) diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt index 489ba94e6..074dc37d5 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt @@ -1,60 +1,233 @@ package at.techbee.jtx.ui.reusable.elements +import android.accounts.Account +import android.content.ContentResolver import android.net.Uri +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowDropDown +import androidx.compose.material.icons.outlined.CloudSync +import androidx.compose.material.icons.outlined.ColorLens +import androidx.compose.material.icons.outlined.EditOff +import androidx.compose.material.icons.outlined.Sync +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import at.techbee.jtx.R import at.techbee.jtx.database.ICalCollection import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE +import at.techbee.jtx.database.ICalDatabase +import at.techbee.jtx.ui.reusable.dialogs.ColorPickerDialog +import at.techbee.jtx.util.SyncUtil @Composable -fun CollectionInfoColumn(collection: ICalCollection, modifier: Modifier = Modifier) { - Column(modifier = modifier) { +fun CollectionInfoColumn( + collection: ICalCollection, + showSyncButton: Boolean, + showColorPicker: Boolean, + showDropdownArrow: Boolean, + modifier: Modifier = Modifier, + initialColor: Int? = null, + onColorPicked: (Int?) -> Unit +) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + val context = LocalContext.current + val isPreview = LocalInspectionMode.current + val lifecycleOwner = LocalLifecycleOwner.current + + var showColorPickerDialog by rememberSaveable { mutableStateOf(false) } + + val syncIconAnimation = rememberInfiniteTransition(label = "syncIconAnimation") + val angle by syncIconAnimation.animateFloat( + initialValue = 0f, + targetValue = -360f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 2000 + } + ), label = "syncIconAnimationAngle" + ) + + var isSyncInProgress by remember { mutableStateOf(false) } + DisposableEffect(lifecycleOwner) { + + val listener = if (isPreview) + null + else { + ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE) { + isSyncInProgress = SyncUtil.isJtxSyncRunningFor(setOf(Account(collection.accountName, collection.accountType))) + } + } + onDispose { + if (!isPreview) + ContentResolver.removeStatusChangeListener(listener) + } + } + + if (showColorPickerDialog) { + ColorPickerDialog( + initialColor = initialColor, + onColorChanged = { newColor -> + onColorPicked(newColor) + }, + onDismiss = { + showColorPickerDialog = false + }, + additionalColorsInt = ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getAllColors() + .observeAsState(initial = emptyList()) + .value + ) + } + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, ) { - collection.displayName?.let { - Text( - text = it, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + collection.displayName?.let { + Text( + text = it, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + ) + } + collection.accountName?.let { + Text( + text = it, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .alpha(0.5f) + .weight(1f) + ) + } } - collection.accountName?.let { + if (collection.accountType != LOCAL_ACCOUNT_TYPE) { + val url = try { + Uri.parse(collection.url).host + } catch (e: NullPointerException) { + null + } Text( - text = it, + text = url ?: "", style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.alpha(0.5f).weight(1f) + modifier = Modifier.alpha(0.5f) ) } } - if(collection.accountType != LOCAL_ACCOUNT_TYPE) { - val url = try { Uri.parse(collection.url).host } catch (e: NullPointerException) { null } - Text( - text = url ?: "", - style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.alpha(0.5f) + + if(showDropdownArrow) { + Icon( + imageVector = Icons.Outlined.ArrowDropDown, + contentDescription = null, + modifier = Modifier.padding(horizontal = 8.dp) ) } + + if(collection.readonly) { + Icon( + imageVector = Icons.Outlined.EditOff, + contentDescription = stringResource(id = R.string.readyonly), + modifier = Modifier.padding(horizontal = 8.dp) + ) + } + + AnimatedVisibility(showSyncButton || showColorPicker) { + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + VerticalDivider(modifier.height(30.dp)) + + AnimatedVisibility(showSyncButton) { + + + IconButton( + onClick = { + if (!isSyncInProgress) { + collection.getAccount().let { SyncUtil.syncAccounts(setOf(it)) } + SyncUtil.showSyncRequestedToast(context) + } + }, + enabled = showSyncButton && !isSyncInProgress + ) { + Crossfade(isSyncInProgress, label = "isSyncInProgress") { synchronizing -> + if (synchronizing) { + Icon( + Icons.Outlined.Sync, + contentDescription = stringResource(id = R.string.sync_in_progress), + modifier = Modifier + .graphicsLayer { + rotationZ = angle + } + .alpha(0.3f), + tint = MaterialTheme.colorScheme.primary, + ) + } else { + Icon( + Icons.Outlined.CloudSync, + contentDescription = stringResource(id = R.string.upload_pending), + ) + } + } + } + } + + if(showColorPicker) { + IconButton(onClick = { showColorPickerDialog = true }) { + Icon(Icons.Outlined.ColorLens, stringResource(id = R.string.color)) + } + } + } + } } } @@ -71,7 +244,13 @@ fun CollectionInfoColumn_Preview() { accountName = "My account", accountType = LOCAL_ACCOUNT_TYPE ) - CollectionInfoColumn(collection1) + CollectionInfoColumn( + collection = collection1, + showSyncButton = false, + showColorPicker = true, + showDropdownArrow = false, + onColorPicked = { } + ) } } @@ -89,6 +268,36 @@ fun CollectionInfoColumn_Preview_REMOTE() { accountType = "Remote", url = "https://www.example.com/whatever/219348729384/mine" ) - CollectionInfoColumn(collection1) + CollectionInfoColumn( + collection = collection1, + showSyncButton = true, + showColorPicker = true, + showDropdownArrow = true, + onColorPicked = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun CollectionInfoColumn_Preview_READONLY() { + MaterialTheme { + val collection1 = ICalCollection( + collectionId = 1L, + color = Color.Cyan.toArgb(), + displayName = "Collection Display Name", + description = "Here comes the desc", + accountName = "My account", + accountType = "Remote", + url = "https://www.example.com/whatever/219348729384/mine", + readonly = true + ) + CollectionInfoColumn( + collection = collection1, + showSyncButton = false, + showColorPicker = false, + showDropdownArrow = false, + onColorPicked = {} + ) } } \ No newline at end of file diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionsSpinner.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionsSpinner.kt index 64b934e36..4e1abd250 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionsSpinner.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionsSpinner.kt @@ -1,18 +1,18 @@ package at.techbee.jtx.ui.reusable.elements import android.widget.Toast +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.FolderOpen +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -21,7 +21,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext @@ -38,11 +37,15 @@ import at.techbee.jtx.flavored.BillingManager fun CollectionsSpinner( collections: List, preselected: ICalCollection, + enableSelector: Boolean, modifier: Modifier = Modifier, includeReadOnly: Boolean, + showSyncButton: Boolean, + showColorPicker: Boolean, + onColorPicked: (Int?) -> Unit, includeVJOURNAL: Boolean? = null, includeVTODO: Boolean? = null, - enabled: Boolean = true, + border: BorderStroke? = null, onSelectionChanged: (collection: ICalCollection) -> Unit ) { @@ -51,10 +54,13 @@ fun CollectionsSpinner( var expanded by remember { mutableStateOf(false) } // initial value val isProPurchased by if(LocalInspectionMode.current) remember { mutableStateOf(true)} else BillingManager.getInstance().isProPurchased.observeAsState(true) - OutlinedCard( + Card( + colors = CardDefaults.elevatedCardColors(), + elevation = CardDefaults.elevatedCardElevation(), modifier = modifier, + border = border, onClick = { - if (enabled) + if (!selected.readonly && enableSelector) expanded = !expanded } ) { @@ -75,11 +81,12 @@ fun CollectionsSpinner( ) CollectionInfoColumn( collection = selected, - modifier = Modifier - .weight(1f) - .alpha(if (!enabled) 0.5f else 1f) + showSyncButton = showSyncButton, + showColorPicker = showColorPicker, + onColorPicked = onColorPicked, + showDropdownArrow = !selected.readonly, + //modifier = Modifier.alpha(if (!enabled) 0.5f else 1f) ) - Icon(Icons.Outlined.ArrowDropDown, null) DropdownMenu( expanded = expanded, @@ -108,7 +115,13 @@ fun CollectionsSpinner( } }, text = { - CollectionInfoColumn(collection = collection) + CollectionInfoColumn( + collection = collection, + showSyncButton = false, + showColorPicker = false, + showDropdownArrow = false, + onColorPicked = { } + ) } ) } @@ -150,9 +163,13 @@ fun CollectionsSpinner_Preview() { CollectionsSpinner( listOf(collection1, collection2, collection3), preselected = collection2, + enableSelector = true, includeReadOnly = true, includeVJOURNAL = true, includeVTODO = true, + showSyncButton = true, + showColorPicker = true, + onColorPicked = { }, onSelectionChanged = { }, modifier = Modifier.fillMaxWidth() ) @@ -175,11 +192,14 @@ fun CollectionsSpinner_Preview_notenabled() { CollectionsSpinner( listOf(collection1), preselected = collection1, + enableSelector = false, includeReadOnly = true, includeVJOURNAL = true, includeVTODO = true, + showSyncButton = false, + showColorPicker = false, + onColorPicked = { }, onSelectionChanged = { }, - enabled = false, modifier = Modifier.fillMaxWidth() ) } @@ -202,11 +222,14 @@ fun CollectionsSpinner_Preview_no_color() { CollectionsSpinner( listOf(collection1), preselected = collection1, + enableSelector = true, includeReadOnly = true, includeVJOURNAL = true, includeVTODO = true, + showSyncButton = true, + showColorPicker = true, + onColorPicked = { }, onSelectionChanged = { }, - enabled = false, modifier = Modifier.fillMaxWidth() ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ede468d2..0c6ae718e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -549,6 +549,8 @@ Thank you!" "jtx Board Pro" "No app found to send this entry." "Close" + "Share" + "Copy/Convert" "Share as E-Mail" "Copy to clipboard" "Copied" From 406602683effdadcabb70567bc6e7e5b5800b44e Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Tue, 14 May 2024 21:31:39 +0200 Subject: [PATCH 02/27] Location done --- .../jtx/ui/detail/DetailScreenContent.kt | 2 +- .../jtx/ui/detail/DetailsCardLocation.kt | 464 ++++++++---------- .../reusable/dialogs/LocationPickerDialog.kt | 90 +++- .../main/java/at/techbee/jtx/util/UiUtil.kt | 6 + 4 files changed, 290 insertions(+), 272 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index f3401f270..df5a5b6ee 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -821,7 +821,7 @@ fun DetailScreenContent( initialGeoLat = iCalObject.geoLat, initialGeoLong = iCalObject.geoLong, initialGeofenceRadius = iCalObject.geofenceRadius, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly?:true, onLocationUpdated = { newLocation, newGeoLat, newGeoLong -> if (newGeoLat != null && newGeoLong != null) { iCalObject.geoLat = newGeoLat diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardLocation.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardLocation.kt index 253a16598..9ce82829d 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardLocation.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardLocation.kt @@ -21,21 +21,49 @@ import android.net.Uri import android.os.Build import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.OpenInNew -import androidx.compose.material.icons.outlined.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.outlined.ArrowDropDown +import androidx.compose.material.icons.outlined.Contacts +import androidx.compose.material.icons.outlined.EditLocation +import androidx.compose.material.icons.outlined.Map +import androidx.compose.material.icons.outlined.MyLocation +import androidx.compose.material.icons.outlined.Place +import androidx.compose.material3.AssistChip +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource @@ -59,9 +87,13 @@ import at.techbee.jtx.ui.reusable.dialogs.LocationPickerDialog import at.techbee.jtx.ui.reusable.dialogs.RequestPermissionDialog import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon import at.techbee.jtx.util.UiUtil -import com.google.accompanist.permissions.* +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale import java.net.URLEncoder -import java.util.* +import java.util.Locale import kotlin.math.roundToInt @@ -73,7 +105,7 @@ fun DetailsCardLocation( initialGeoLat: Double?, initialGeoLong: Double?, initialGeofenceRadius: Int?, - isEditMode: Boolean, + isReadOnly: Boolean, onLocationUpdated: (String, Double?, Double?) -> Unit, onGeofenceRadiusUpdatd: (Int?) -> Unit, modifier: Modifier = Modifier @@ -84,6 +116,8 @@ fun DetailsCardLocation( var showLocationPickerDialog by rememberSaveable { mutableStateOf(false) } var showRequestGeofencePermissionsDialog by rememberSaveable { mutableStateOf(false) } val openPermissionsIntent = Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null)) + val focusRequester = remember { FocusRequester() } + var isLocationTextFieldFocused by remember { mutableStateOf(false) } var location by rememberSaveable { mutableStateOf(initialLocation ?: "") } var geoLat by rememberSaveable { mutableStateOf(initialGeoLat) } @@ -185,235 +219,153 @@ fun DetailsCardLocation( } - ElevatedCard(modifier = modifier) { + ElevatedCard( + onClick = { + if(!isReadOnly) + focusRequester.requestFocus() + }, + modifier = modifier + ) { Column( modifier = Modifier .fillMaxWidth() .padding(8.dp), ) { - Crossfade(isEditMode, label = "toggleEditModeForMap") { editMode -> - if (!editMode) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - HeadlineWithIcon( - icon = Icons.Outlined.Place, - iconDesc = headline, - text = headline - ) - Text( - text = location, - modifier = Modifier.fillMaxWidth() - ) - if(geoLat != null && geoLong != null) { - Text(ICalObject.getLatLongString(geoLat, geoLong) ?: "") - } - } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + HeadlineWithIcon( + icon = Icons.Outlined.Place, + iconDesc = headline, + text = headline + ) - if((geoLat != null && geoLong != null) || location.isNotEmpty()) { - - IconButton(onClick = { - val uri = if(geoLat == null && geoLong == null && location.isNotEmpty()) { - Uri.parse("geo:0,0?q=$location") - } else if (geoLat != null && geoLong != null && location.isEmpty()) { - val latLngParam = "%.5f".format(Locale.ENGLISH, geoLat) + "," + "%.5f".format(Locale.ENGLISH, geoLong) - Uri.parse("geo:0,0?q=$latLngParam(${URLEncoder.encode(location, Charsets.UTF_8.name())})") - } else { - val latLngParam = "%.5f".format(Locale.ENGLISH, geoLat) + "," + "%.5f".format(Locale.ENGLISH, geoLong) - Uri.parse("geo:$latLngParam") - } - val geoIntent = Intent(Intent.ACTION_VIEW, uri) - try { - context.startActivity(geoIntent) - } catch (e: ActivityNotFoundException) { - context.startActivity( - Intent(Intent.ACTION_VIEW, ICalObject.getMapLink(geoLat, geoLong, location, BuildFlavor.getCurrent())) - ) - } - }) { - Icon( - Icons.AutoMirrored.Outlined.OpenInNew, - stringResource(id = R.string.open_in_browser) - ) + BasicTextField( + value = location, + textStyle = LocalTextStyle.current, + + onValueChange = { newLocation -> + location = newLocation + onLocationUpdated(newLocation, geoLat, geoLong) + }, + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + enabled = !isReadOnly, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = if(isLocationTextFieldFocused || location.isNotBlank()) 8.dp else 4.dp) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + isLocationTextFieldFocused = focusState.hasFocus } - } + ) + if(geoLat != null && geoLong != null) { + Text(ICalObject.getLatLongString(geoLat, geoLong) ?: "") } - } else { - Column { - if (allLocations.isNotEmpty() || allLocalAddresses.isNotEmpty()) { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - items(allLocations.filter { it.location?.lowercase()?.contains(location.lowercase()) == true }) { locationLatLng -> - AssistChip( - onClick = { - location = locationLatLng.location ?: "" - geoLat = locationLatLng.geoLat - geoLong = locationLatLng.geoLong - geoLatText = geoLat?.toString()?:"" - geoLongText = geoLong?.toString()?:"" - onLocationUpdated(location, geoLat, geoLong) - }, - label = { - val displayString = - if (locationLatLng.geoLat != null && locationLatLng.geoLong != null) - "${locationLatLng.location} (${String.format("%.5f", locationLatLng.geoLat)},${String.format("%.5f", locationLatLng.geoLong)})" - else - "${locationLatLng.location}" - Text(displayString) - }, - leadingIcon = { - Icon(Icons.Outlined.EditLocation, null) - }, - modifier = Modifier.alpha( - if (locationLatLng.location == location && locationLatLng.geoLat == geoLat && locationLatLng.geoLong == geoLong) 1f else 0.4f - ) - ) - } + } - items(allLocalAddresses.toList()) { addressBookLocation -> - AssistChip( - onClick = { - location = addressBookLocation - geoLat = null - geoLong = null - geoLatText = "" - geoLongText = "" - onLocationUpdated(location, null, null) - }, - label = { Text(addressBookLocation) }, - leadingIcon = { Icon(Icons.Outlined.Contacts, null) }, - modifier = Modifier.alpha( - if (addressBookLocation == location) 1f else 0.4f - ) - ) - } - } + IconButton(onClick = { + locationUpdateRequested = true + }) { + Icon(Icons.Outlined.MyLocation, stringResource(R.string.current_location)) + } + + IconButton(onClick = { showLocationPickerDialog = true }) { + Icon(Icons.Outlined.Map, stringResource(id = R.string.location)) + } + + if((geoLat != null && geoLong != null) || location.isNotEmpty()) { + + IconButton(onClick = { + val uri = if(geoLat == null && geoLong == null && location.isNotEmpty()) { + Uri.parse("geo:0,0?q=$location") + } else if (geoLat != null && geoLong != null && location.isEmpty()) { + val latLngParam = "%.5f".format(Locale.ENGLISH, geoLat) + "," + "%.5f".format(Locale.ENGLISH, geoLong) + Uri.parse("geo:0,0?q=$latLngParam(${URLEncoder.encode(location, Charsets.UTF_8.name())})") + } else { + val latLngParam = "%.5f".format(Locale.ENGLISH, geoLat) + "," + "%.5f".format(Locale.ENGLISH, geoLong) + Uri.parse("geo:$latLngParam") } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - value = location, - leadingIcon = { Icon(Icons.Outlined.EditLocation, headline) }, - trailingIcon = { - IconButton(onClick = { - location = "" - onLocationUpdated(location, geoLat, geoLong) - }) { - if (location.isNotEmpty()) - Icon( - Icons.Outlined.Clear, - stringResource(id = R.string.delete) - ) - } - }, - singleLine = true, - label = { Text(headline) }, - onValueChange = { newLocation -> - location = newLocation - onLocationUpdated(newLocation, geoLat, geoLong) - }, - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - modifier = Modifier.weight(1f) + + val geoIntent = Intent(Intent.ACTION_VIEW, uri) + try { + context.startActivity(geoIntent) + } catch (e: ActivityNotFoundException) { + context.startActivity( + Intent(Intent.ACTION_VIEW, ICalObject.getMapLink(geoLat, geoLong, location, BuildFlavor.getCurrent())) ) - IconButton(onClick = { showLocationPickerDialog = true }) { - Icon(Icons.Outlined.Map, stringResource(id = R.string.location)) - } } + }) { + Icon( + Icons.AutoMirrored.Outlined.OpenInNew, + stringResource(id = R.string.open_in_browser) + ) } } + } - AnimatedVisibility(isEditMode) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - value = geoLatText, - singleLine = true, - label = { Text(stringResource(R.string.latitude)) }, - onValueChange = { newLat -> - geoLatText = newLat - geoLat = newLat.toDoubleOrNull() - onLocationUpdated(location, geoLat, geoLong) - }, - trailingIcon = { - AnimatedVisibility(geoLat != null) { - IconButton(onClick = { + Column { + AnimatedVisibility (isLocationTextFieldFocused && (allLocations.isNotEmpty() || allLocalAddresses.isNotEmpty())) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(allLocations.filter { it.location?.lowercase()?.contains(location.lowercase()) == true }) { locationLatLng -> + AssistChip( + onClick = { + location = locationLatLng.location ?: "" + geoLat = locationLatLng.geoLat + geoLong = locationLatLng.geoLong + geoLatText = geoLat?.toString()?:"" + geoLongText = geoLong?.toString()?:"" + onLocationUpdated(location, geoLat, geoLong) + }, + label = { + val displayString = + if (locationLatLng.geoLat != null && locationLatLng.geoLong != null) + "${locationLatLng.location} (${UiUtil.doubleTo5DecimalString(locationLatLng.geoLat)},${UiUtil.doubleTo5DecimalString(locationLatLng.geoLong)})" + else + "${locationLatLng.location}" + Text(displayString) + }, + leadingIcon = { + Icon(Icons.Outlined.EditLocation, null) + }, + modifier = Modifier.alpha( + if (locationLatLng.location == location && locationLatLng.geoLat == geoLat && locationLatLng.geoLong == geoLong) 1f else 0.4f + ) + ) + } + + items(allLocalAddresses.toList()) { addressBookLocation -> + AssistChip( + onClick = { + location = addressBookLocation geoLat = null - geoLatText = "" - }) { - Icon(Icons.Outlined.Close, stringResource(id = R.string.delete)) - } - } - }, - isError = (geoLatText.isNotEmpty() && geoLatText.toDoubleOrNull() == null) - || (geoLongText.isNotEmpty() && geoLongText.toDoubleOrNull() == null) - || (geoLatText.isEmpty() && geoLongText.isNotEmpty()) - || (geoLatText.isNotEmpty() && geoLongText.isEmpty()), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Done), - modifier = Modifier.weight(1f) - ) - OutlinedTextField( - value = geoLongText, - singleLine = true, - label = { Text(stringResource(R.string.longitude)) }, - onValueChange = { newLong -> - geoLongText = newLong - geoLong = newLong.toDoubleOrNull() - onLocationUpdated(location, geoLat, geoLong) - }, - trailingIcon = { - AnimatedVisibility(geoLong != null) { - IconButton(onClick = { geoLong = null + geoLatText = "" geoLongText = "" - }) { - Icon(Icons.Outlined.Close, stringResource(id = R.string.delete)) - } - } - }, - isError = (geoLongText.isNotEmpty() && geoLongText.toDoubleOrNull() == null) - || (geoLatText.isEmpty() && geoLongText.isNotEmpty()) - || (geoLatText.isNotEmpty() && geoLongText.isEmpty()), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Done), - modifier = Modifier.weight(1f) - ) - - IconButton(onClick = { - locationUpdateRequested = true - }) { - Icon(Icons.Outlined.MyLocation, stringResource(R.string.current_location)) + onLocationUpdated(location, null, null) + }, + label = { Text(addressBookLocation) }, + leadingIcon = { Icon(Icons.Outlined.Contacts, null) }, + modifier = Modifier.alpha( + if (addressBookLocation == location) 1f else 0.4f + ) + ) + } } } } - AnimatedVisibility(geoLat != null && geoLong != null && !isEditMode && !LocalInspectionMode.current) { - MapComposable( - initialLocation = location, - initialGeoLat = geoLat, - initialGeoLong = geoLong, - isEditMode = false, - enableCurrentLocation = false, - onLocationUpdated = { _, _, _ -> /* only view, no update here */ }, - modifier = Modifier - .fillMaxWidth() - .height(200.dp) - .padding(top = 8.dp) - ) - } + //GEOFENCING if (BuildFlavor.getCurrent() == BuildFlavor.GPLAY) { AnimatedVisibility(geoLat != null && geoLong != null) { Row( @@ -428,8 +380,7 @@ fun DetailsCardLocation( Icon( painter = painterResource(R.drawable.ic_geofence_radius), - contentDescription = null, - modifier = Modifier.padding(horizontal = if (isEditMode) 8.dp else 0.dp) + contentDescription = null ) Text( stringResource( @@ -442,46 +393,44 @@ fun DetailsCardLocation( ) ) - AnimatedVisibility(isEditMode) { - IconButton( - onClick = { geofenceOptionsExpanded = true }, + IconButton( + onClick = { geofenceOptionsExpanded = true }, + ) { + + DropdownMenu( + expanded = geofenceOptionsExpanded, + onDismissRequest = { geofenceOptionsExpanded = false } ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.off)) }, + onClick = { + geofenceRadius = null + onGeofenceRadiusUpdatd(null) + geofenceOptionsExpanded = false + } + ) - DropdownMenu( - expanded = geofenceOptionsExpanded, - onDismissRequest = { geofenceOptionsExpanded = false } - ) { + listOf(50, 200, 500).forEach { DropdownMenuItem( - text = { Text(stringResource(R.string.off)) }, + text = { + val metersInFeet = (((it * 3.281) / 50).roundToInt() * 50) + if (useFeet) + Text(text = stringResource(R.string.geofence_radius_feet, metersInFeet)) + else + Text(text = stringResource(R.string.geofence_radius_meter, it)) + }, onClick = { - geofenceRadius = null - onGeofenceRadiusUpdatd(null) + if (geofencePermissionState?.allPermissionsGranted != true) + showRequestGeofencePermissionsDialog = true + geofenceRadius = it + onGeofenceRadiusUpdatd(it) geofenceOptionsExpanded = false } ) - - listOf(50, 200, 500).forEach { - DropdownMenuItem( - text = { - val metersInFeet = (((it * 3.281) / 50).roundToInt() * 50) - if (useFeet) - Text(text = stringResource(R.string.geofence_radius_feet, metersInFeet)) - else - Text(text = stringResource(R.string.geofence_radius_meter, it)) - }, - onClick = { - if (geofencePermissionState?.allPermissionsGranted != true) - showRequestGeofencePermissionsDialog = true - geofenceRadius = it - onGeofenceRadiusUpdatd(it) - geofenceOptionsExpanded = false - } - ) - } } - - Icon(Icons.Outlined.ArrowDropDown, stringResource(R.string.geofence_options)) } + + Icon(Icons.Outlined.ArrowDropDown, stringResource(R.string.geofence_options)) } } } @@ -507,6 +456,21 @@ fun DetailsCardLocation( } } } + + AnimatedVisibility(geoLat != null && geoLong != null && !LocalInspectionMode.current) { + MapComposable( + initialLocation = location, + initialGeoLat = geoLat, + initialGeoLong = geoLong, + isEditMode = false, + enableCurrentLocation = false, + onLocationUpdated = { _, _, _ -> /* only view, no update here */ }, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .padding(top = 8.dp) + ) + } } } } @@ -526,7 +490,7 @@ fun DetailsCardLocation_Preview() { initialGeoLat = null, initialGeoLong = null, initialGeofenceRadius = null, - isEditMode = false, + isReadOnly = false, onLocationUpdated = { _, _, _ -> }, onGeofenceRadiusUpdatd = {} ) @@ -542,7 +506,7 @@ fun DetailsCardLocation_Preview_withGeo() { initialGeoLat = 23.447378, initialGeoLong = 73.272838, initialGeofenceRadius = null, - isEditMode = false, + isReadOnly = false, onLocationUpdated = { _, _, _ -> }, onGeofenceRadiusUpdatd = {} ) @@ -558,7 +522,7 @@ fun DetailsCardLocation_Preview_withGeoDE() { initialGeoLat = 23.447378, initialGeoLong = 73.272838, initialGeofenceRadius = null, - isEditMode = false, + isReadOnly = false, onLocationUpdated = { _, _, _ -> }, onGeofenceRadiusUpdatd = {} ) @@ -568,14 +532,14 @@ fun DetailsCardLocation_Preview_withGeoDE() { @Preview(showBackground = true) @Composable -fun DetailsCardLocation_Preview_edit() { +fun DetailsCardLocation_Preview_readonly() { MaterialTheme { DetailsCardLocation( initialLocation = "Vienna, Stephansplatz", initialGeoLat = null, initialGeoLong = null, initialGeofenceRadius = null, - isEditMode = true, + isReadOnly = true, onLocationUpdated = { _, _, _ -> }, onGeofenceRadiusUpdatd = {} ) @@ -585,14 +549,14 @@ fun DetailsCardLocation_Preview_edit() { @Preview(showBackground = true) @Composable -fun DetailsCardLocation_Preview_edit_with_geo() { +fun DetailsCardLocation_Preview_readonly_with_geo() { MaterialTheme { DetailsCardLocation( initialLocation = "Vienna, Stephansplatz", initialGeoLat = 23.447378, initialGeoLong = 73.272838, initialGeofenceRadius = null, - isEditMode = true, + isReadOnly = true, onLocationUpdated = { _, _, _ -> }, onGeofenceRadiusUpdatd = {} ) diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/LocationPickerDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/LocationPickerDialog.kt index 6b017130a..2c74b2220 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/LocationPickerDialog.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/LocationPickerDialog.kt @@ -8,11 +8,16 @@ package at.techbee.jtx.ui.reusable.dialogs +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -20,12 +25,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.techbee.jtx.R import at.techbee.jtx.flavored.MapComposable +import at.techbee.jtx.util.UiUtil +import java.text.NumberFormat +import java.text.ParseException @Composable @@ -39,36 +50,73 @@ fun LocationPickerDialog( ) { var location by rememberSaveable { mutableStateOf(initialLocation) } - var lat by rememberSaveable { mutableStateOf(initialGeoLat) } - var long by rememberSaveable { mutableStateOf(initialGeoLong) } + var lat by rememberSaveable { mutableStateOf(UiUtil.doubleTo5DecimalString(initialGeoLat)?:"") } + var long by rememberSaveable { mutableStateOf(UiUtil.doubleTo5DecimalString(initialGeoLong)?:"") } + + val latDouble = try { NumberFormat.getInstance().parse(lat)?.toDouble() } catch (e: ParseException) { null } + val longDouble = try { NumberFormat.getInstance().parse(long)?.toDouble() } catch (e: ParseException) { null } AlertDialog( onDismissRequest = { onDismiss() }, title = { Text(stringResource(id = R.string.location)) }, text = { - MapComposable( - initialLocation = location, - initialGeoLat = lat, - initialGeoLong = long, - isEditMode = true, - enableCurrentLocation = enableCurrentLocation, - onLocationUpdated = { newLocation, newLat, newLong -> - location = newLocation - lat = newLat - long = newLong - }, - modifier = Modifier - .fillMaxWidth() - .height(400.dp) - .padding(top = 8.dp) - ) - }, + + Column { + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = lat, + singleLine = true, + label = { Text(stringResource(R.string.latitude)) }, + onValueChange = { newLat -> + lat = newLat + }, + isError = (latDouble != null && longDouble == null) || (latDouble == null && longDouble != null), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Done), + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = long, + singleLine = true, + label = { Text(stringResource(R.string.longitude)) }, + onValueChange = { newLong -> + long = newLong + }, + isError = (latDouble != null && longDouble == null) || (latDouble == null && longDouble != null), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Done), + modifier = Modifier.weight(1f) + ) + } + + + MapComposable( + initialLocation = location, + initialGeoLat = latDouble, + initialGeoLong = longDouble, + isEditMode = true, + enableCurrentLocation = enableCurrentLocation, + onLocationUpdated = { newLocation, newLat, newLong -> + location = newLocation + lat = UiUtil.doubleTo5DecimalString(newLat)?:"" + long = UiUtil.doubleTo5DecimalString(newLong)?:"" + }, + modifier = Modifier + .fillMaxWidth() + .height(400.dp) + .padding(top = 8.dp) + ) + } + }, confirmButton = { TextButton( onClick = { - onConfirm(location, lat, long) + onConfirm(location, latDouble, longDouble) onDismiss() - } + }, + enabled = (latDouble == null && longDouble == null) || (latDouble != null && longDouble != null) ) { Text(stringResource(id = R.string.ok)) } diff --git a/app/src/main/java/at/techbee/jtx/util/UiUtil.kt b/app/src/main/java/at/techbee/jtx/util/UiUtil.kt index 77d82e4ed..1b5cf626d 100644 --- a/app/src/main/java/at/techbee/jtx/util/UiUtil.kt +++ b/app/src/main/java/at/techbee/jtx/util/UiUtil.kt @@ -188,4 +188,10 @@ object UiUtil { println(color.toString() + " " + a*100) return a > 0.5 } + + /** + * @param double number to format + * @return the double number as a string with 5 decimals + */ + fun doubleTo5DecimalString(double: Double?) = double?.let { String.format(Locale.getDefault(), "%.5f", it) } } \ No newline at end of file From 279a31ecd4c58ad35bb3b43414082c4ceaabb7fa Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Tue, 14 May 2024 21:32:03 +0200 Subject: [PATCH 03/27] removed Jtx20009ReleaseInfoDialog.kt obsolete release info dialog --- .../dialogs/Jtx20009ReleaseInfoDialog.kt | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/Jtx20009ReleaseInfoDialog.kt diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/Jtx20009ReleaseInfoDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/Jtx20009ReleaseInfoDialog.kt deleted file mode 100644 index 1b3b8612b..000000000 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/Jtx20009ReleaseInfoDialog.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) Techbee e.U. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package at.techbee.jtx.ui.reusable.dialogs - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import at.techbee.jtx.R -import com.arnyminerz.markdowntext.MarkdownText - - -@Composable -fun Jtx20009ReleaseInfoDialog( - onOK: () -> Unit, -) { - - AlertDialog( - onDismissRequest = { onOK() }, - title = { Text("jtx Board 2.0.9") }, - text = { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - MarkdownText("Dear fellow jtx Board users," + System.lineSeparator() + System.lineSeparator() + - "your jtx Board has been updated to version 2.0.9!" + System.lineSeparator() + System.lineSeparator() + - "In this version settings have been moved to other places:" + System.lineSeparator() + - "- The new flat view option in the list replaces the setting to show subtasks of journals and notes in tasklist" + System.lineSeparator() + - "- Markdown can now be enabled/disabled within the detail view of an entry" + System.lineSeparator() + - "- Autosave can now be enabled/disabled within the edit view of an entry" + System.lineSeparator() + - "- The setting to show only one recurring entry in the future is now available in the list view as \"limit recurring\"" + System.lineSeparator() + System.lineSeparator() + - "**IMPORTANT: Due to the changes in the settings the changed options might have been restored to default values.**" + System.lineSeparator() + System.lineSeparator() + - "By the way the widget is almost ready ;-)" + System.lineSeparator() + - "- Patrick") - } - }, - confirmButton = { - TextButton( - onClick = { onOK() } - ) { - Text(stringResource(id = R.string.close)) - } - }, - ) - } - -@Preview(showBackground = true) -@Composable -fun Jtx20009ReleaseInfoDialog_Preview() { - MaterialTheme { - Jtx20009ReleaseInfoDialog( - onOK = { }, - ) - } -} From b263bfd7c2f1e6033e3d2d3250c288e5cbdff205 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 18 May 2024 22:31:27 +0200 Subject: [PATCH 04/27] Refactored recurring element --- .../jtx/ui/detail/DetailScreenContent.kt | 2 +- .../techbee/jtx/ui/detail/DetailsCardRecur.kt | 646 +++--------------- .../jtx/ui/reusable/dialogs/RecurDialog.kt | 567 +++++++++++++++ .../dialogs/UnsupportedRRuleDialog.kt | 64 -- app/src/main/res/values/strings.xml | 2 + 5 files changed, 656 insertions(+), 625 deletions(-) create mode 100644 app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/RecurDialog.kt delete mode 100644 app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/UnsupportedRRuleDialog.kt diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index df5a5b6ee..ad1f372e3 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -887,7 +887,7 @@ fun DetailScreenContent( icalObject = iCalObject, seriesInstances = seriesInstances.value, seriesElement = seriesElement, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly ?: true, hasChildren = subtasks.value.isNotEmpty() || subnotes.value.isNotEmpty(), onRecurUpdated = { updatedRRule -> iCalObject.rrule = updatedRRule?.toString() diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt index 4e5a15479..14b87e62c 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt @@ -9,7 +9,6 @@ package at.techbee.jtx.ui.detail import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -19,34 +18,25 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.EventRepeat -import androidx.compose.material3.AssistChip import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration @@ -54,34 +44,21 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.techbee.jtx.R import at.techbee.jtx.database.ICalObject -import at.techbee.jtx.database.ICalObject.Companion.TZ_ALLDAY -import at.techbee.jtx.ui.reusable.dialogs.DatePickerDialog import at.techbee.jtx.ui.reusable.dialogs.DetachFromSeriesDialog -import at.techbee.jtx.ui.reusable.dialogs.UnsupportedRRuleDialog +import at.techbee.jtx.ui.reusable.dialogs.RecurDialog import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon import at.techbee.jtx.util.DateTimeUtils -import at.techbee.jtx.util.DateTimeUtils.requireTzId -import at.techbee.jtx.util.UiUtil.asDayOfWeek -import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.NumberList import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.Recur.Frequency -import net.fortuna.ical4j.model.WeekDay -import net.fortuna.ical4j.model.WeekDayList -import java.time.DayOfWeek -import java.time.Instant -import java.time.ZonedDateTime -import java.util.Locale -import kotlin.math.absoluteValue @OptIn(ExperimentalLayoutApi::class) @Composable fun DetailsCardRecur( icalObject: ICalObject, + isReadOnly: Boolean, seriesInstances: List, seriesElement: ICalObject?, - isEditMode: Boolean, hasChildren: Boolean, onRecurUpdated: (Recur?) -> Unit, goToDetail: (itemId: Long, editMode: Boolean, list: List) -> Unit, @@ -89,83 +66,11 @@ fun DetailsCardRecur( modifier: Modifier = Modifier ) { - val headline = stringResource(id = R.string.recurrence) - val dtstartWeekday = when (ZonedDateTime.ofInstant(Instant.ofEpochMilli(icalObject.dtstart?:0L), requireTzId(icalObject.dtstartTimezone)).dayOfWeek) { - DayOfWeek.MONDAY -> WeekDay.MO - DayOfWeek.TUESDAY -> WeekDay.TU - DayOfWeek.WEDNESDAY -> WeekDay.WE - DayOfWeek.THURSDAY -> WeekDay.TH - DayOfWeek.FRIDAY -> WeekDay.FR - DayOfWeek.SATURDAY -> WeekDay.SA - DayOfWeek.SUNDAY -> WeekDay.SU - else -> null - } - //var updatedRRule by rememberSaveable { mutableStateOf(icalObject.getRecur()) } - - var isRecurActivated by rememberSaveable { mutableStateOf(icalObject.getRecur() != null) } - var frequency by rememberSaveable { mutableStateOf(icalObject.getRecur()?.frequency) } - var interval by rememberSaveable { mutableStateOf(icalObject.getRecur()?.interval?.let { if(it<=0) null else it }) } - var count by rememberSaveable { mutableStateOf(icalObject.getRecur()?.count?.let { if (it<=0) null else it }) } - var until by rememberSaveable { mutableStateOf(icalObject.getRecur()?.until) } - val dayList = remember { icalObject.getRecur()?.dayList?.toMutableStateList() ?: mutableStateListOf() } - val monthDayList = remember { mutableStateListOf(icalObject.getRecur()?.monthDayList?.firstOrNull() ?: 1) } - - var frequencyExpanded by rememberSaveable { mutableStateOf(false) } - var intervalExpanded by rememberSaveable { mutableStateOf(false) } - var monthDayListExpanded by rememberSaveable { mutableStateOf(false) } - var endAfterExpaneded by rememberSaveable { mutableStateOf(false) } - var endsExpanded by rememberSaveable { mutableStateOf(false) } - var showDatepicker by rememberSaveable { mutableStateOf(false) } + var showRecurDialog by rememberSaveable { mutableStateOf(false) } var showDetachSingleFromSeriesDialog by rememberSaveable { mutableStateOf(false) } var showDetachAllFromSeriesDialog by rememberSaveable { mutableStateOf(false) } - - - fun buildRRule(): Recur? { - if(!isRecurActivated) - return null - else { - val updatedRRule = Recur.Builder().apply { - if(interval != null && interval!! > 1) - interval(interval!!) - until?.let { until(it) } - count?.let { count(it) } - frequency(frequency ?: Frequency.DAILY) - - if(frequency == Frequency.WEEKLY || dayList.isNotEmpty()) { // there might be a dayList also for DAILY recurrences coming from Thunderbird! - val newDayList = WeekDayList().apply { - dayList.forEach { weekDay -> this.add(weekDay) } - if(!dayList.contains(dtstartWeekday)) - dayList.add(dtstartWeekday) - } - dayList(newDayList) - } - if(frequency == Frequency.MONTHLY) { - val newMonthList = NumberList().apply { - monthDayList.forEach { monthDay -> this.add(monthDay) } - } - monthDayList(newMonthList) - } - }.build() - return updatedRRule - } - } - - if (showDatepicker) { - DatePickerDialog( - datetime = until?.time ?: icalObject.dtstart ?: System.currentTimeMillis(), - timezone = TZ_ALLDAY, - dateOnly = true, - allowNull = false, - onConfirm = { datetime, _ -> - datetime?.let { until = Date(it) } - onRecurUpdated(buildRRule()) - }, - onDismiss = { showDatepicker = false } - ) - } - if (showDetachSingleFromSeriesDialog) { DetachFromSeriesDialog( detachAll = false, @@ -182,29 +87,22 @@ fun DetailsCardRecur( ) } - icalObject.getRecur()?.let { recur -> - if(isEditMode && (recur.experimentalValues?.isNotEmpty() == true - || recur.hourList?.isNotEmpty() == true - || recur.minuteList?.isNotEmpty() == true - || recur.monthList?.isNotEmpty() == true - || recur.secondList?.isNotEmpty() == true - || recur.setPosList?.isNotEmpty() == true - || recur.skip != null - || recur.weekNoList?.isNotEmpty() == true - || recur.weekStartDay != null - || recur.yearDayList?.isNotEmpty() == true - || (recur.monthDayList?.size?:0) > 1) - ) { - UnsupportedRRuleDialog( - onConfirm = { }, - onDismiss = { goToDetail(icalObject.id, false, emptyList()) } - ) - } + if(showRecurDialog && icalObject.dtstart != null) { + RecurDialog( + dtstart = icalObject.dtstart!!, + dtstartTimezone = icalObject.dtstartTimezone, + onRecurUpdated = onRecurUpdated, + onDismiss = { showRecurDialog = false } + ) } - - - ElevatedCard(modifier = modifier) { + ElevatedCard( + onClick = { + if(icalObject.dtstart != null && icalObject.recurid == null && !isReadOnly) + showRecurDialog = true + }, + modifier = modifier + ) { Column( modifier = Modifier @@ -219,365 +117,40 @@ fun DetailsCardRecur( ) { HeadlineWithIcon( icon = Icons.Outlined.EventRepeat, - iconDesc = headline, - text = headline + iconDesc = null, + text = stringResource(id = R.string.recurrence) ) + } - AnimatedVisibility(isEditMode && icalObject.recurid == null) { - Switch( - checked = isRecurActivated, - enabled = icalObject.dtstart != null, - onCheckedChange = { - isRecurActivated = it - if (it) { - frequency = Frequency.DAILY - count = 1 - interval = 1 - until = null - //dayList = null - //monthDayList = null - } - onRecurUpdated(buildRRule()) - } - ) + icalObject.getRecur()?.let { recur -> + if(recur.experimentalValues?.isNotEmpty() == true + || recur.hourList?.isNotEmpty() == true + || recur.minuteList?.isNotEmpty() == true + || recur.monthList?.isNotEmpty() == true + || recur.secondList?.isNotEmpty() == true + || recur.setPosList?.isNotEmpty() == true + || recur.skip != null + || recur.weekNoList?.isNotEmpty() == true + || recur.weekStartDay != null + || recur.yearDayList?.isNotEmpty() == true + || (recur.monthDayList?.size?:0) > 1 + ) { + Text(stringResource(id = R.string.details_recur_unknown_rrule_dialog_message)) } } - AnimatedVisibility(isEditMode && icalObject.dtstart == null) { + AnimatedVisibility(icalObject.dtstart == null) { Text( text = stringResource(id = R.string.edit_recur_toast_requires_start_date), style = MaterialTheme.typography.bodySmall ) } - AnimatedVisibility(isEditMode && isRecurActivated) { - - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - ) { - - - Row( - horizontalArrangement = Arrangement.spacedBy( - 8.dp, - Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.horizontalScroll(rememberScrollState()) - ) { - Text(stringResource(id = R.string.edit_recur_repeat_every_x)) - - AssistChip( - onClick = { intervalExpanded = true }, - label = { - Text( - if(interval == null || interval!! < 1) - "1" - else - interval?.toString() ?: "1" - ) - - DropdownMenu( - expanded = intervalExpanded, - onDismissRequest = { intervalExpanded = false } - ) { - for (number in 1..100) { - DropdownMenuItem( - onClick = { - interval = number - intervalExpanded = false - onRecurUpdated(buildRRule()) - }, - text = { Text("$number") } - ) - } - } - } - ) - - AssistChip( - onClick = { frequencyExpanded = true }, - label = { - Text( - when (frequency) { - Frequency.YEARLY -> stringResource(id = R.string.edit_recur_year) - Frequency.MONTHLY -> stringResource(id = R.string.edit_recur_month) - Frequency.WEEKLY -> stringResource(id = R.string.edit_recur_week) - Frequency.DAILY -> stringResource(id = R.string.edit_recur_day) - Frequency.HOURLY -> stringResource(id = R.string.edit_recur_hour) - Frequency.MINUTELY -> stringResource(id = R.string.edit_recur_minute) - Frequency.SECONDLY -> stringResource(id = R.string.edit_recur_second) - else -> "not supported" - } - ) - - DropdownMenu( - expanded = frequencyExpanded, - onDismissRequest = { frequencyExpanded = false } - ) { - - Frequency.entries.reversed().forEach { frequency2select -> - if(icalObject.dtstartTimezone == TZ_ALLDAY - && listOf(Frequency.SECONDLY, Frequency.MINUTELY, Frequency.HOURLY).contains(frequency2select)) - return@forEach - if(frequency2select == Frequency.SECONDLY) - return@forEach - - DropdownMenuItem( - onClick = { - frequency = frequency2select - onRecurUpdated(buildRRule()) - frequencyExpanded = false - }, - text = { - Text( - when (frequency2select) { - Frequency.YEARLY -> stringResource(id = R.string.edit_recur_year) - Frequency.MONTHLY -> stringResource(id = R.string.edit_recur_month) - Frequency.WEEKLY -> stringResource(id = R.string.edit_recur_week) - Frequency.DAILY -> stringResource(id = R.string.edit_recur_day) - Frequency.HOURLY -> stringResource(id = R.string.edit_recur_hour) - Frequency.MINUTELY -> stringResource(id = R.string.edit_recur_minute) - //Frequency.SECONDLY -> stringResource(id = R.string.edit_recur_second) - else -> frequency2select.name - } - ) - } - ) - } - } - } - ) - } - - - AnimatedVisibility(frequency == Frequency.WEEKLY || dayList.isNotEmpty()) { - - val weekdays = if (DateTimeUtils.isLocalizedWeekstartMonday()) - listOf( - WeekDay.MO, - WeekDay.TU, - WeekDay.WE, - WeekDay.TH, - WeekDay.FR, - WeekDay.SA, - WeekDay.SU - ) - else - listOf( - WeekDay.SU, - WeekDay.MO, - WeekDay.TU, - WeekDay.WE, - WeekDay.TH, - WeekDay.FR, - WeekDay.SA - ) - - Row( - horizontalArrangement = Arrangement.spacedBy( - 8.dp, - Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.horizontalScroll(rememberScrollState()) - ) { - Text(stringResource(id = R.string.edit_recur_on_weekday)) - - weekdays.forEach { weekday -> - FilterChip( - selected = dayList.contains(weekday), - onClick = { - if (dayList.contains(weekday)) - dayList.remove(weekday) - else - (dayList).add(weekday) - onRecurUpdated(buildRRule()) - }, - enabled = dtstartWeekday != weekday, - label = { - Text( - weekday.asDayOfWeek()?.getDisplayName( - java.time.format.TextStyle.SHORT, - Locale.getDefault() - ) ?: "" - ) - } - ) - } - } - } - - AnimatedVisibility(frequency == Frequency.MONTHLY) { - - Row( - horizontalArrangement = Arrangement.spacedBy( - 8.dp, - Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.horizontalScroll(rememberScrollState()) - ) { - Text(stringResource(id = R.string.edit_recur_on_the_x_day_of_month)) - - AssistChip( - onClick = { monthDayListExpanded = true }, - label = { - val monthDay = monthDayList.firstOrNull()?:1 - Text( - if(monthDay < 0) - stringResource(id = R.string.edit_recur_LAST_day_of_the_month) + if(monthDay < -1) " - ${monthDay.absoluteValue-1}" else "" - else - DateTimeUtils.getLocalizedOrdinalFor(monthDay) - ) - - DropdownMenu( - expanded = monthDayListExpanded, - onDismissRequest = { monthDayListExpanded = false } - ) { - for (number in 1..31) { - DropdownMenuItem( - onClick = { - monthDayList.clear() - monthDayList.add(number) - monthDayListExpanded = false - onRecurUpdated(buildRRule()) - }, - text = { - Text(DateTimeUtils.getLocalizedOrdinalFor(number)) - } - ) - } - - for (number in 1..31) { - DropdownMenuItem( - onClick = { - monthDayList.clear() - monthDayList.add(number*(-1)) - monthDayListExpanded = false - onRecurUpdated(buildRRule()) - }, - text = { - Text(stringResource(id = R.string.edit_recur_LAST_day_of_the_month) + if(number > 1) " - ${number-1}" else "") - } - ) - } - } - } - ) - - Text(stringResource(id = R.string.edit_recur_x_day_of_the_month)) - } - - } - - - Row( - horizontalArrangement = Arrangement.spacedBy( - 8.dp, - Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.horizontalScroll(rememberScrollState()) - ) { - - AssistChip( - onClick = { endsExpanded = true }, - label = { - - Text( - when { - count != null -> stringResource(id = R.string.edit_recur_ends_after) - until != null -> stringResource(id = R.string.edit_recur_ends_on) - else -> stringResource(id = R.string.edit_recur_ends_never) - } - ) - - DropdownMenu( - expanded = endsExpanded, - onDismissRequest = { endsExpanded = false } - ) { - DropdownMenuItem( - onClick = { - count = 1 - until = null - endsExpanded = false - onRecurUpdated(buildRRule()) - }, - text = { Text(stringResource(id = R.string.edit_recur_ends_after)) } - ) - DropdownMenuItem( - onClick = { - count = null - until = Date(icalObject.dtstart ?: System.currentTimeMillis()) - endsExpanded = false - onRecurUpdated(buildRRule()) - }, - text = { Text(stringResource(id = R.string.edit_recur_ends_on)) } - ) - DropdownMenuItem( - onClick = { - count = null - until = null - endsExpanded = false - onRecurUpdated(buildRRule()) - }, - text = { Text(stringResource(id = R.string.edit_recur_ends_never)) } - ) - } - } - ) - - AnimatedVisibility(count != null) { - AssistChip( - onClick = { endAfterExpaneded = true }, - label = { - Text((count?:1).toString()) - - DropdownMenu( - expanded = endAfterExpaneded, - onDismissRequest = { endAfterExpaneded = false } - ) { - for (number in 1..100) { - DropdownMenuItem( - onClick = { - count = number - endAfterExpaneded = false - onRecurUpdated(buildRRule()) - }, - text = { - Text(number.toString()) - } - ) - } - } - } - ) - } - - AnimatedVisibility(count != null) { - Text(stringResource(R.string.edit_recur_x_times)) - } - - AnimatedVisibility(until != null) { - AssistChip( - onClick = { showDatepicker = true }, - label = { - Text( - DateTimeUtils.convertLongToFullDateString( - until?.time, - TZ_ALLDAY - ) - ) - } - ) - } - } - } + AnimatedVisibility(icalObject.dtstart != null && icalObject.recurid == null && icalObject.rrule == null) { + Text( + text = stringResource(id = R.string.recur_not_set), + style = MaterialTheme.typography.bodySmall + ) } if(icalObject.recurid != null) { @@ -603,7 +176,7 @@ fun DetailsCardRecur( .fillMaxWidth() .padding(8.dp) ) { - if(!isEditMode && !icalObject.recurid.isNullOrEmpty()) { + if(!icalObject.recurid.isNullOrEmpty()) { Button( onClick = { seriesElement?.id?.let { goToDetail(it, false, emptyList()) } @@ -619,7 +192,7 @@ fun DetailsCardRecur( } } - if(!isEditMode && !icalObject.rrule.isNullOrEmpty()) { + if(seriesInstances.isNotEmpty() && icalObject.recurid == null) { Button( onClick = { showDetachAllFromSeriesDialog = true } ) { @@ -628,87 +201,61 @@ fun DetailsCardRecur( } } - if(isEditMode) - icalObject.rrule = buildRRule()?.toString() - Column( verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { - if (isEditMode) { - val instances = icalObject.getInstancesFromRrule() - if (instances.isNotEmpty()) - Text( - text = stringResource(R.string.preview), - style = MaterialTheme.typography.labelMedium, - fontStyle = FontStyle.Italic - ) - instances.forEach { instanceDate -> + var showAllInstances by remember { mutableStateOf(false) } + + if(showAllInstances) { + seriesInstances.forEach { instance -> ElevatedCard( + onClick = { + goToDetail(instance.id, false, seriesInstances.map { it.id }) + }, modifier = Modifier .fillMaxWidth() .heightIn(min = 48.dp) ) { - Text( - text = DateTimeUtils.convertLongToFullDateTimeString(instanceDate, icalObject.dtstartTimezone), - modifier = Modifier.padding(vertical = 16.dp, horizontal = 8.dp) - ) - } - } - } else { - var showAllInstances by remember { mutableStateOf(false) } - - if(showAllInstances) { - seriesInstances.forEach { instance -> - ElevatedCard( - onClick = { - goToDetail(instance.id, false, seriesInstances.map { it.id }) - }, + Row( modifier = Modifier .fillMaxWidth() - .heightIn(min = 48.dp) + .padding(vertical = 16.dp, horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = DateTimeUtils.convertLongToFullDateTimeString( - instance.dtstart, - instance.dtstartTimezone - ), - modifier = Modifier.weight(1f) + Text( + text = DateTimeUtils.convertLongToFullDateTimeString( + instance.dtstart, + instance.dtstartTimezone + ), + modifier = Modifier.weight(1f) + ) + if (instance.sequence == 0L) { + Icon( + Icons.Outlined.EventRepeat, + stringResource(R.string.list_item_recurring), + modifier = Modifier + .size(14.dp) + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_recur_exception), + stringResource(R.string.list_item_edited_recurring), + modifier = Modifier + .size(14.dp) ) - if (instance.sequence == 0L) { - Icon( - Icons.Outlined.EventRepeat, - stringResource(R.string.list_item_recurring), - modifier = Modifier - .size(14.dp) - ) - } else { - Icon( - painter = painterResource(R.drawable.ic_recur_exception), - stringResource(R.string.list_item_edited_recurring), - modifier = Modifier - .size(14.dp) - ) - } } } } - } else { - TextButton(onClick = { showAllInstances = true }) { - Text(stringResource(id = R.string.details_show_all_instances, seriesInstances.size)) - } + } + } else if(icalObject.rrule != null && seriesInstances.isNotEmpty()) { + TextButton(onClick = { showAllInstances = true }) { + Text(stringResource(id = R.string.details_show_all_instances, seriesInstances.size)) } } - val exceptions = DateTimeUtils.getLongListfromCSVString(icalObject.exdate) if(exceptions.isNotEmpty()) Text( @@ -719,6 +266,7 @@ fun DetailsCardRecur( .fillMaxWidth() .padding(top = 8.dp, bottom = 4.dp) ) + Column( verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, @@ -806,7 +354,7 @@ fun DetailsCardRecur_Preview() { } ), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, @@ -817,7 +365,7 @@ fun DetailsCardRecur_Preview() { @Preview(showBackground = true) @Composable -fun DetailsCardRecur_Preview_edit() { +fun DetailsCardRecur_Preview_read_only2() { MaterialTheme { val recur = Recur @@ -845,7 +393,7 @@ fun DetailsCardRecur_Preview_edit() { } ), seriesElement = null, - isEditMode = true, + isReadOnly = true, hasChildren = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, @@ -878,7 +426,7 @@ fun DetailsCardRecur_Preview_unchanged_recur() { } ), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, @@ -911,7 +459,7 @@ fun DetailsCardRecur_Preview_changed_recur() { } ), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = true, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, @@ -942,7 +490,7 @@ fun DetailsCardRecur_Preview_off() { } ), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, @@ -953,7 +501,7 @@ fun DetailsCardRecur_Preview_off() { @Preview(showBackground = true) @Composable -fun DetailsCardRecur_Preview_edit_off() { +fun DetailsCardRecur_Preview_read_only() { MaterialTheme { DetailsCardRecur( @@ -973,7 +521,7 @@ fun DetailsCardRecur_Preview_edit_off() { } ), seriesElement = null, - isEditMode = true, + isReadOnly = true, hasChildren = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, @@ -982,28 +530,6 @@ fun DetailsCardRecur_Preview_edit_off() { } } -@Preview(showBackground = true) -@Composable -fun DetailsCardRecur_Preview_edit_no_dtstart() { - MaterialTheme { - - DetailsCardRecur( - icalObject = ICalObject.createTodo().apply { - dtstart = null - dtstartTimezone = null - due = null - dueTimezone = null - }, - seriesInstances = emptyList(), - seriesElement = null, - isEditMode = true, - hasChildren = false, - onRecurUpdated = { }, - goToDetail = { _, _, _ -> }, - unlinkFromSeries = { _, _, _ -> } - ) - } -} @Preview(showBackground = true) @Composable @@ -1019,7 +545,7 @@ fun DetailsCardRecur_Preview_view_no_dtstart() { }, seriesInstances = emptyList(), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/RecurDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/RecurDialog.kt new file mode 100644 index 000000000..751d6d3a9 --- /dev/null +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/RecurDialog.kt @@ -0,0 +1,567 @@ +/* + * Copyright (c) Techbee e.U. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.techbee.jtx.ui.reusable.dialogs + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AssistChip +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import at.techbee.jtx.R +import at.techbee.jtx.database.ICalObject +import at.techbee.jtx.util.DateTimeUtils +import at.techbee.jtx.util.UiUtil.asDayOfWeek +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.NumberList +import net.fortuna.ical4j.model.Recur +import net.fortuna.ical4j.model.Recur.Frequency +import net.fortuna.ical4j.model.WeekDay +import net.fortuna.ical4j.model.WeekDayList +import java.time.DayOfWeek +import java.time.Instant +import java.time.ZonedDateTime +import java.time.format.TextStyle +import java.util.Locale +import kotlin.math.absoluteValue + + +@Composable +fun RecurDialog( + dtstart: Long, + dtstartTimezone: String?, + onRecurUpdated: (Recur?) -> Unit, + onDismiss: () -> Unit +) { + + val shadowICalObject = ICalObject.createJournal().apply { + this.dtstart = dtstart + this.dtstartTimezone = dtstartTimezone + } + + val dtstartWeekday = when (ZonedDateTime.ofInstant( + Instant.ofEpochMilli(shadowICalObject.dtstart ?: 0L), + DateTimeUtils.requireTzId(shadowICalObject.dtstartTimezone) + ).dayOfWeek) { + DayOfWeek.MONDAY -> WeekDay.MO + DayOfWeek.TUESDAY -> WeekDay.TU + DayOfWeek.WEDNESDAY -> WeekDay.WE + DayOfWeek.THURSDAY -> WeekDay.TH + DayOfWeek.FRIDAY -> WeekDay.FR + DayOfWeek.SATURDAY -> WeekDay.SA + DayOfWeek.SUNDAY -> WeekDay.SU + else -> null + } + //var currentRRule by rememberSaveable { mutableStateOf(icalObject.getRecur()) } + + var frequency by rememberSaveable { mutableStateOf(if (shadowICalObject.getRecur() == null) Frequency.DAILY else shadowICalObject.getRecur()?.frequency) } + var interval by rememberSaveable { mutableStateOf(if (shadowICalObject.getRecur() == null) 1 else shadowICalObject.getRecur()?.interval?.let { if (it <= 0) null else it }) } + var count by rememberSaveable { mutableStateOf(if (shadowICalObject.getRecur() == null) 1 else shadowICalObject.getRecur()?.count?.let { if (it <= 0) null else it }) } + var until by rememberSaveable { mutableStateOf(shadowICalObject.getRecur()?.until) } + val dayList = + remember { shadowICalObject.getRecur()?.dayList?.toMutableStateList() ?: mutableStateListOf() } + val monthDayList = + remember { mutableStateListOf(shadowICalObject.getRecur()?.monthDayList?.firstOrNull() ?: 1) } + + var frequencyExpanded by rememberSaveable { mutableStateOf(false) } + var intervalExpanded by rememberSaveable { mutableStateOf(false) } + var monthDayListExpanded by rememberSaveable { mutableStateOf(false) } + var endAfterExpaneded by rememberSaveable { mutableStateOf(false) } + var endsExpanded by rememberSaveable { mutableStateOf(false) } + var showDatepicker by rememberSaveable { mutableStateOf(false) } + + + fun buildRRule(): Recur? { + val updatedRRule = Recur.Builder().apply { + if (interval != null && interval!! > 1) + interval(interval!!) + until?.let { until(it) } + count?.let { count(it) } + frequency(frequency ?: Frequency.DAILY) + + if (frequency == Frequency.WEEKLY || dayList.isNotEmpty()) { // there might be a dayList also for DAILY recurrences coming from Thunderbird! + val newDayList = WeekDayList().apply { + dayList.forEach { weekDay -> this.add(weekDay) } + if (!dayList.contains(dtstartWeekday)) + dayList.add(dtstartWeekday) + } + dayList(newDayList) + } + if (frequency == Frequency.MONTHLY) { + val newMonthList = NumberList().apply { + monthDayList.forEach { monthDay -> this.add(monthDay) } + } + monthDayList(newMonthList) + } + }.build() + return updatedRRule + } + + if (showDatepicker) { + DatePickerDialog( + datetime = until?.time ?: shadowICalObject.dtstart ?: System.currentTimeMillis(), + timezone = ICalObject.TZ_ALLDAY, + dateOnly = true, + allowNull = false, + onConfirm = { datetime, _ -> + datetime?.let { until = Date(it) } + //onRecurUpdated(buildRRule()) + }, + onDismiss = { showDatepicker = false } + ) + } + + + + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { Text(text = stringResource(id = R.string.recurrence)) }, + properties = DialogProperties(usePlatformDefaultWidth = false), // Workaround due to Google Issue: https://issuetracker.google.com/issues/194911971?pli=1 + text = { + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + //.verticalScroll(rememberScrollState()), + ) { + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + + Row( + horizontalArrangement = Arrangement.spacedBy( + 8.dp, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.horizontalScroll(rememberScrollState()) + ) { + Text(stringResource(id = R.string.edit_recur_repeat_every_x)) + + AssistChip( + onClick = { intervalExpanded = true }, + label = { + Text( + if (interval == null || interval!! < 1) + "1" + else + interval?.toString() ?: "1" + ) + + DropdownMenu( + expanded = intervalExpanded, + onDismissRequest = { intervalExpanded = false } + ) { + for (number in 1..100) { + DropdownMenuItem( + onClick = { + interval = number + intervalExpanded = false + }, + text = { Text("$number") } + ) + } + } + } + ) + + AssistChip( + onClick = { frequencyExpanded = true }, + label = { + Text( + when (frequency) { + Frequency.YEARLY -> stringResource(id = R.string.edit_recur_year) + Frequency.MONTHLY -> stringResource(id = R.string.edit_recur_month) + Frequency.WEEKLY -> stringResource(id = R.string.edit_recur_week) + Frequency.DAILY -> stringResource(id = R.string.edit_recur_day) + Frequency.HOURLY -> stringResource(id = R.string.edit_recur_hour) + Frequency.MINUTELY -> stringResource(id = R.string.edit_recur_minute) + Frequency.SECONDLY -> stringResource(id = R.string.edit_recur_second) + else -> "not supported" + } + ) + + DropdownMenu( + expanded = frequencyExpanded, + onDismissRequest = { frequencyExpanded = false } + ) { + + Recur.Frequency.entries.reversed().forEach { frequency2select -> + if (shadowICalObject.dtstartTimezone == ICalObject.TZ_ALLDAY + && listOf( + Frequency.SECONDLY, + Frequency.MINUTELY, + Frequency.HOURLY + ).contains(frequency2select) + ) + return@forEach + if (frequency2select == Frequency.SECONDLY) + return@forEach + + DropdownMenuItem( + onClick = { + frequency = frequency2select + frequencyExpanded = false + }, + text = { + Text( + when (frequency2select) { + Frequency.YEARLY -> stringResource(id = R.string.edit_recur_year) + Frequency.MONTHLY -> stringResource(id = R.string.edit_recur_month) + Frequency.WEEKLY -> stringResource(id = R.string.edit_recur_week) + Frequency.DAILY -> stringResource(id = R.string.edit_recur_day) + Frequency.HOURLY -> stringResource(id = R.string.edit_recur_hour) + Frequency.MINUTELY -> stringResource(id = R.string.edit_recur_minute) + //Frequency.SECONDLY -> stringResource(id = R.string.edit_recur_second) + else -> frequency2select.name + } + ) + } + ) + } + } + } + ) + } + + AnimatedVisibility(frequency == Frequency.WEEKLY || dayList.isNotEmpty()) { + + val weekdays = if (DateTimeUtils.isLocalizedWeekstartMonday()) + listOf( + WeekDay.MO, + WeekDay.TU, + WeekDay.WE, + WeekDay.TH, + WeekDay.FR, + WeekDay.SA, + WeekDay.SU + ) + else + listOf( + WeekDay.SU, + WeekDay.MO, + WeekDay.TU, + WeekDay.WE, + WeekDay.TH, + WeekDay.FR, + WeekDay.SA + ) + + Row( + horizontalArrangement = Arrangement.spacedBy( + 8.dp, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.horizontalScroll(rememberScrollState()) + ) { + Text(stringResource(id = R.string.edit_recur_on_weekday)) + + weekdays.forEach { weekday -> + FilterChip( + selected = dayList.contains(weekday), + onClick = { + if (dayList.contains(weekday)) + dayList.remove(weekday) + else + (dayList).add(weekday) + }, + enabled = dtstartWeekday != weekday, + label = { + Text( + weekday.asDayOfWeek()?.getDisplayName( + TextStyle.SHORT, + Locale.getDefault() + ) ?: "" + ) + } + ) + } + } + } + + AnimatedVisibility(frequency == Frequency.MONTHLY) { + + Row( + horizontalArrangement = Arrangement.spacedBy( + 8.dp, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.horizontalScroll(rememberScrollState()) + ) { + Text(stringResource(id = R.string.edit_recur_on_the_x_day_of_month)) + + AssistChip( + onClick = { monthDayListExpanded = true }, + label = { + val monthDay = monthDayList.firstOrNull() ?: 1 + Text( + if (monthDay < 0) + stringResource(id = R.string.edit_recur_LAST_day_of_the_month) + if (monthDay < -1) " - ${monthDay.absoluteValue - 1}" else "" + else + DateTimeUtils.getLocalizedOrdinalFor(monthDay) + ) + + DropdownMenu( + expanded = monthDayListExpanded, + onDismissRequest = { monthDayListExpanded = false } + ) { + for (number in 1..31) { + DropdownMenuItem( + onClick = { + monthDayList.clear() + monthDayList.add(number) + monthDayListExpanded = false + }, + text = { + Text(DateTimeUtils.getLocalizedOrdinalFor(number)) + } + ) + } + + for (number in 1..31) { + DropdownMenuItem( + onClick = { + monthDayList.clear() + monthDayList.add(number * (-1)) + monthDayListExpanded = false + }, + text = { + Text(stringResource(id = R.string.edit_recur_LAST_day_of_the_month) + if (number > 1) " - ${number - 1}" else "") + } + ) + } + } + } + ) + Text(stringResource(id = R.string.edit_recur_x_day_of_the_month)) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy( + 8.dp, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.horizontalScroll(rememberScrollState()) + ) { + + AssistChip( + onClick = { endsExpanded = true }, + label = { + + Text( + when { + count != null -> stringResource(id = R.string.edit_recur_ends_after) + until != null -> stringResource(id = R.string.edit_recur_ends_on) + else -> stringResource(id = R.string.edit_recur_ends_never) + } + ) + + DropdownMenu( + expanded = endsExpanded, + onDismissRequest = { endsExpanded = false } + ) { + DropdownMenuItem( + onClick = { + count = 1 + until = null + endsExpanded = false + }, + text = { Text(stringResource(id = R.string.edit_recur_ends_after)) } + ) + DropdownMenuItem( + onClick = { + count = null + until = Date( + shadowICalObject.dtstart ?: System.currentTimeMillis() + ) + endsExpanded = false + }, + text = { Text(stringResource(id = R.string.edit_recur_ends_on)) } + ) + DropdownMenuItem( + onClick = { + count = null + until = null + endsExpanded = false + }, + text = { Text(stringResource(id = R.string.edit_recur_ends_never)) } + ) + } + } + ) + + AnimatedVisibility(count != null) { + AssistChip( + onClick = { endAfterExpaneded = true }, + label = { + Text((count ?: 1).toString()) + + DropdownMenu( + expanded = endAfterExpaneded, + onDismissRequest = { endAfterExpaneded = false } + ) { + for (number in 1..100) { + DropdownMenuItem( + onClick = { + count = number + endAfterExpaneded = false + }, + text = { + Text(number.toString()) + } + ) + } + } + } + ) + } + + AnimatedVisibility(count != null) { + Text(stringResource(R.string.edit_recur_x_times)) + } + + AnimatedVisibility(until != null) { + AssistChip( + onClick = { showDatepicker = true }, + label = { + Text( + DateTimeUtils.convertLongToFullDateString( + until?.time, + ICalObject.TZ_ALLDAY + ) + ) + } + ) + } + } + } + + + shadowICalObject.rrule = buildRRule()?.toString() + + Column( + verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + val instances = shadowICalObject.getInstancesFromRrule() + if (instances.isNotEmpty()) + Text( + text = stringResource(R.string.preview), + style = MaterialTheme.typography.labelMedium, + fontStyle = FontStyle.Italic + ) + instances.forEach { instanceDate -> + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + ) { + Text( + text = DateTimeUtils.convertLongToFullDateTimeString( + instanceDate, + shadowICalObject.dtstartTimezone + ), + modifier = Modifier.padding(vertical = 16.dp, horizontal = 8.dp) + ) + } + } + } + } + }, + confirmButton = { + Row( + modifier = Modifier.fillMaxWidth() + ) { + TextButton( + onClick = { + onRecurUpdated(null) + onDismiss() + } + ) { + Text(stringResource(R.string.list_item_remove_recurrence)) + } + + Spacer(modifier = Modifier.weight(1f)) + + TextButton( + onClick = { + onDismiss() + } + ) { + Text(stringResource(id = R.string.cancel)) + } + + TextButton( + onClick = { + onRecurUpdated(if(shadowICalObject.getInstancesFromRrule().size > 1) buildRRule() else null) + onDismiss() + } + ) { + Text(stringResource(id = R.string.save)) + } + } + } + ) +} + + +@Preview(showBackground = true) +@Composable +fun RecurDialog_Preview() { + MaterialTheme { + RecurDialog( + dtstart = System.currentTimeMillis(), + dtstartTimezone = null, + onDismiss = {}, + onRecurUpdated = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/UnsupportedRRuleDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/UnsupportedRRuleDialog.kt deleted file mode 100644 index fecd9bfdc..000000000 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/UnsupportedRRuleDialog.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) Techbee e.U. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package at.techbee.jtx.ui.reusable.dialogs - -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import at.techbee.jtx.R - - -@Composable -fun UnsupportedRRuleDialog( - onConfirm: () -> Unit, - onDismiss: () -> Unit -) { - - - AlertDialog( - onDismissRequest = { onDismiss() }, - title = { Text(stringResource(id = R.string.details_recur_unknown_rrule_dialog_title)) }, - text = { Text(stringResource(id = R.string.details_recur_unknown_rrule_dialog_message)) }, - confirmButton = { - TextButton( - onClick = { - onConfirm() - onDismiss() - } - ) { - Text(stringResource(id = R.string.ok)) - } - }, - dismissButton = { - TextButton( - onClick = { - onDismiss() - } - ) { - Text( stringResource(id = R.string.cancel)) - } - } - ) - } - -@Preview(showBackground = true) -@Composable -fun UnsupportedRRuleDialog_Preview() { - MaterialTheme { - UnsupportedRRuleDialog( - onConfirm = { }, - onDismiss = { } - ) - } -} - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c6ae718e..b31d9e168 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -90,6 +90,7 @@ "The first line is interpreted as summary, the rest as description. Hashtags are interpreted as categories (eg #category)." "Recurring entry" "Edited recurring entry" + "Remove recurrence" "Scroll to top" "App Permission" @@ -451,6 +452,7 @@ Thank you!" "This entry was either deleted in the background or could not be loaded." "* Synchronizing attachments is an experimental feature. We strongly encourage to use only small attachments (up to 100 KB) as larger ones could lead to long synchronization times and unexpected behaviour." "This entry is part of a series. This entry will copy any changes made for the series." + "No reccurrence rule set." "This entry is part of a series but was changed. This entry will not be affected by changes of the series." "Attention: Series-elements mirror the subtasks and linked notes from the series. If you would like to change subtasks and linked notes only for a series-element, please unlink it from the series first." "Go to series" From 50caffef978986d15e004da755b8b30c15e8092b Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 19 May 2024 22:07:54 +0200 Subject: [PATCH 05/27] comments done --- .../jtx/ui/detail/DetailScreenContent.kt | 2 +- .../jtx/ui/detail/DetailsCardComments.kt | 89 ++++++++--------- .../techbee/jtx/ui/detail/DetailsCardUrl.kt | 12 +-- .../jtx/ui/reusable/cards/CommentCard.kt | 61 ++---------- .../dialogs/AddLinkAttachmentsDialog.kt | 2 +- .../ui/reusable/dialogs/EditCommentDialog.kt | 97 ++++++++++++++----- .../main/java/at/techbee/jtx/util/UiUtil.kt | 4 +- app/src/main/res/values/strings.xml | 1 + .../java/at/techbee/jtx/util/UiUtilTest.kt | 3 + 9 files changed, 138 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index ad1f372e3..5444d2600 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -842,7 +842,7 @@ fun DetailScreenContent( if(comments.isNotEmpty() || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] == true || showAllOptions))) { DetailsCardComments( comments = comments, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly?:true, onCommentsUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt index 3189fab9a..db322cf25 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt @@ -9,13 +9,11 @@ package at.techbee.jtx.ui.detail import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement 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.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Comment import androidx.compose.material.icons.outlined.AddComment @@ -23,8 +21,6 @@ import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -33,30 +29,39 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.techbee.jtx.R import at.techbee.jtx.database.properties.Comment import at.techbee.jtx.ui.reusable.cards.CommentCard +import at.techbee.jtx.ui.reusable.dialogs.EditCommentDialog import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon @Composable fun DetailsCardComments( comments: SnapshotStateList, - isEditMode: Boolean, + isReadOnly: Boolean, onCommentsUpdated: () -> Unit, modifier: Modifier = Modifier ) { val headline = stringResource(id = R.string.comments) - var newComment by rememberSaveable { mutableStateOf("") } + var showAddCommentDialog by rememberSaveable { mutableStateOf(false) } + + if(showAddCommentDialog) { + EditCommentDialog( + comment = Comment(), + onConfirm = { newComment -> + comments.add(newComment) + onCommentsUpdated() + }, + onDismiss = { showAddCommentDialog = false }, + onDelete = { }) + } ElevatedCard(modifier = modifier) { @@ -66,18 +71,36 @@ fun DetailsCardComments( .padding(8.dp), ) { - HeadlineWithIcon(icon = Icons.AutoMirrored.Outlined.Comment, iconDesc = headline, text = headline) + Row( + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + HeadlineWithIcon( + icon = Icons.AutoMirrored.Outlined.Comment, + iconDesc = headline, + text = headline + ) + + if(!isReadOnly) { + IconButton(onClick = { + showAddCommentDialog = true + }) { + Icon(Icons.Outlined.AddComment, stringResource(id = R.string.edit_comment_helper)) + } + } + } AnimatedVisibility(comments.isNotEmpty()) { Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier .fillMaxWidth() ) { comments.forEach { comment -> CommentCard( comment = comment, - isEditMode = isEditMode, + isReadOnly = isReadOnly, onCommentDeleted = { comments.remove(comment) onCommentsUpdated() @@ -90,38 +113,6 @@ fun DetailsCardComments( } } } - - AnimatedVisibility(isEditMode) { - OutlinedTextField( - value = newComment, - trailingIcon = { - AnimatedVisibility(newComment.isNotEmpty()) { - IconButton(onClick = { - comments.add(Comment(text = newComment)) - onCommentsUpdated() - newComment = "" - }) { - Icon( - Icons.Outlined.AddComment, - stringResource(id = R.string.edit_comment_helper) - ) - } - } - }, - label = { Text(stringResource(id = R.string.edit_comment_helper)) }, - onValueChange = { newValue -> newComment = newValue }, - isError = newComment.isNotEmpty(), - modifier = Modifier - .fillMaxWidth() - .border(0.dp, Color.Transparent), - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - comments.add(Comment(text = newComment)) - onCommentsUpdated() - newComment = "" - }) - ) - } } } } @@ -136,7 +127,7 @@ fun DetailsCardComments_Preview() { Comment(text = "First comment"), Comment(text = "Second comment\nthat's a bit longer. Here's also a bit more text to see how it reacts when there should be a line break.") ) }, - isEditMode = false, + isReadOnly = false, onCommentsUpdated = { } ) } @@ -145,14 +136,14 @@ fun DetailsCardComments_Preview() { @Preview(showBackground = true) @Composable -fun DetailsCardComments_Preview_edit() { +fun DetailsCardComments_Preview_readonly() { MaterialTheme { DetailsCardComments( comments = remember { mutableStateListOf( Comment(text = "First comment"), Comment(text = "Second comment\nthat's a bit longer. Here's also a bit more text to see how it reacts when there should be a line break.") ) }, - isEditMode = true, + isReadOnly = true, onCommentsUpdated = { } ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt index 262dc3fd7..8492e5d87 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt @@ -53,7 +53,7 @@ fun DetailsCardUrl( val headline = stringResource(id = R.string.url) var url by rememberSaveable { mutableStateOf(initialUrl) } - val isValidURL = url.isNotEmpty() && UiUtil.isValidURL(url) + val isValidURL = UiUtil.isValidURL(url) val uriHandler = LocalUriHandler.current val focusRequester = remember { FocusRequester() } @@ -85,7 +85,7 @@ fun DetailsCardUrl( .focusRequester(focusRequester) ) - AnimatedVisibility(!isValidURL) { + AnimatedVisibility(!isReadOnly && url.isNotBlank() && !isValidURL) { Text( text = stringResource(id = R.string.invalid_url_message), style = MaterialTheme.typography.labelSmall, @@ -128,11 +128,11 @@ fun DetailsCardUrl_Preview() { @Preview(showBackground = true) @Composable -fun DetailsCardUrl_Preview_edit() { +fun DetailsCardUrl_Preview_emptyUrl() { MaterialTheme { DetailsCardUrl( - initialUrl = "www.bitfire.at", - isReadOnly = true, + initialUrl = "", + isReadOnly = false, onUrlUpdated = { } ) } @@ -145,7 +145,7 @@ fun DetailsCardUrl_Preview_invalid_URL() { MaterialTheme { DetailsCardUrl( initialUrl = "invalid url", - isReadOnly = true, + isReadOnly = false, onUrlUpdated = { } ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt index 682c467e1..17d9fb0ab 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt @@ -12,14 +12,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -28,19 +22,16 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import at.techbee.jtx.R import at.techbee.jtx.database.properties.Comment import at.techbee.jtx.ui.reusable.dialogs.EditCommentDialog -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CommentCard( comment: Comment, - isEditMode: Boolean, + isReadOnly: Boolean, modifier: Modifier = Modifier, onCommentDeleted: () -> Unit, onCommentUpdated: (Comment) -> Unit @@ -52,37 +43,17 @@ fun CommentCard( EditCommentDialog( comment = comment, onConfirm = { updatedComment -> onCommentUpdated(updatedComment) }, - onDismiss = { showCommentEditDialog = false } + onDismiss = { showCommentEditDialog = false }, + onDelete = onCommentDeleted ) } - if (isEditMode) { - OutlinedCard( + ElevatedCard( modifier = modifier, - onClick = {showCommentEditDialog = true} - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - comment.text, - modifier = Modifier - .padding(start = 8.dp, end = 8.dp) - .align(alignment = Alignment.CenterVertically) - .weight(1f) - ) - IconButton(onClick = { onCommentDeleted() }) { - Icon(Icons.Outlined.Delete, stringResource(id = R.string.delete)) - } + onClick = { + if(!isReadOnly) + showCommentEditDialog = true } - } - } else { - ElevatedCard( - modifier = modifier ) { Row( modifier = Modifier @@ -100,31 +71,19 @@ fun CommentCard( ) } } - } -} -@Preview(showBackground = true) -@Composable -fun CommentCardPreview_view() { - MaterialTheme { - CommentCard( - comment = Comment(text = "This is my comment"), - isEditMode = false, - onCommentDeleted = { }, - onCommentUpdated = { } - ) - } } @Preview(showBackground = true) @Composable -fun CommentCardPreview_edit() { +fun CommentCardPreview() { MaterialTheme { CommentCard( comment = Comment(text = "This is my comment"), - isEditMode = true, + isReadOnly = false, onCommentDeleted = { }, onCommentUpdated = { } ) } } + diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/AddLinkAttachmentsDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/AddLinkAttachmentsDialog.kt index d6353389b..499c8322b 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/AddLinkAttachmentsDialog.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/AddLinkAttachmentsDialog.kt @@ -82,7 +82,7 @@ fun AddAttachmentLinkDialog( confirmButton = { TextButton( onClick = { - if (currentText.isNotBlank() && UiUtil.isValidURL(currentText)) { + if (UiUtil.isValidURL(currentText)) { onConfirm(currentText) onDismiss() } else { diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditCommentDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditCommentDialog.kt index 191447b81..2d6b1e1e5 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditCommentDialog.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditCommentDialog.kt @@ -10,9 +10,13 @@ package at.techbee.jtx.ui.reusable.dialogs import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Comment +import androidx.compose.material.icons.automirrored.outlined.InsertComment import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -20,14 +24,22 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable 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.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import at.techbee.jtx.R import at.techbee.jtx.database.properties.Comment @@ -36,14 +48,28 @@ import at.techbee.jtx.database.properties.Comment fun EditCommentDialog( comment: Comment, onConfirm: (Comment) -> Unit, - onDismiss: () -> Unit + onDismiss: () -> Unit, + onDelete: () -> Unit ) { var currentText by rememberSaveable { mutableStateOf(comment.text) } + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } AlertDialog( onDismissRequest = { onDismiss() }, - title = { Text(stringResource(id = R.string.edit_comment)) }, + title = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.AutoMirrored.Outlined.InsertComment, null) + Text(stringResource(id = if(comment.commentId == 0L) R.string.edit_comment_helper else R.string.edit_comment)) + } + }, text = { Column( @@ -59,28 +85,51 @@ fun EditCommentDialog( currentText = newText }, maxLines = 4, - leadingIcon = { Icon(Icons.AutoMirrored.Outlined.Comment, null) } + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + if(currentText.isNotBlank()) { + onConfirm(comment.apply { text = currentText }) + onDismiss() + } + }), + modifier = Modifier.focusRequester(focusRequester) ) } }, confirmButton = { - TextButton( - enabled = currentText.isNotBlank(), - onClick = { - onConfirm(comment.apply { text = currentText }) + + Row { + + if(comment.text.isNotEmpty()) { + TextButton( + onClick = { + onDelete() + onDismiss() + } + ) { + Text(stringResource(id = R.string.delete)) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + TextButton( + onClick = { onDismiss() + } + ) { + Text(stringResource(id = R.string.cancel)) } - ) { - Text(stringResource(id = R.string.ok)) - } - }, - dismissButton = { - TextButton( - onClick = { - onDismiss() + + TextButton( + enabled = currentText.isNotBlank(), + onClick = { + onConfirm(comment.apply { text = currentText }) + onDismiss() + } + ) { + Text(stringResource(id = R.string.save)) } - ) { - Text(stringResource(id = R.string.cancel)) } } ) @@ -92,22 +141,24 @@ fun EditCommentDialog_Preview() { MaterialTheme { EditCommentDialog( - comment = Comment(text = "this is my comment"), + comment = Comment(), onConfirm = { }, - onDismiss = { } + onDismiss = { }, + onDelete = { } ) } } @Preview(showBackground = true) @Composable -fun EditCommentDialog_Preview_blank() { +fun EditCommentDialog_Preview_new() { MaterialTheme { EditCommentDialog( - comment = Comment(text = ""), + comment = Comment(text = "asdf", commentId = 1L), onConfirm = { }, - onDismiss = { } + onDismiss = { }, + onDelete = { } ) } } diff --git a/app/src/main/java/at/techbee/jtx/util/UiUtil.kt b/app/src/main/java/at/techbee/jtx/util/UiUtil.kt index 1b5cf626d..8bb367b2d 100644 --- a/app/src/main/java/at/techbee/jtx/util/UiUtil.kt +++ b/app/src/main/java/at/techbee/jtx/util/UiUtil.kt @@ -25,11 +25,11 @@ import java.util.regex.Matcher object UiUtil { fun isValidURL(urlString: String?): Boolean { - return PatternsCompat.WEB_URL.matcher(urlString.toString()).matches() + return !urlString.isNullOrEmpty() && PatternsCompat.WEB_URL.matcher(urlString).matches() } fun isValidEmail(emailString: String?): Boolean { - return emailString?.isNotEmpty() == true && PatternsCompat.EMAIL_ADDRESS.matcher(emailString).matches() + return !emailString.isNullOrEmpty() && PatternsCompat.EMAIL_ADDRESS.matcher(emailString).matches() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b31d9e168..112187ae3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -462,6 +462,7 @@ Thank you!" "Unlink from parent" "Remove the relation as a sub-entry? The entry will remain as a standalone entry." "Edit comment" + "Add comment" "Add a subnote" "Edit subtask" "Edit subnote" diff --git a/app/src/test/java/at/techbee/jtx/util/UiUtilTest.kt b/app/src/test/java/at/techbee/jtx/util/UiUtilTest.kt index bc0d10205..39ff020cb 100644 --- a/app/src/test/java/at/techbee/jtx/util/UiUtilTest.kt +++ b/app/src/test/java/at/techbee/jtx/util/UiUtilTest.kt @@ -63,6 +63,9 @@ class UiUtilTest { @Test fun isValidURL_testTrue4() = assertTrue(isValidURL("https://www.example.com/asdf")) @Test fun isValidURL_testFalse1() = assertFalse(isValidURL("AABB")) @Test fun isValidURL_testFalse2() = assertFalse(isValidURL("asdf://AABB.com")) + @Test fun isValidURL_test_empty() = assertFalse(isValidURL("")) + @Test fun isValidURL_test_spaces() = assertFalse(isValidURL(" ")) + @Test fun getAttachmentSizeString_bytes() = assertEquals("100 Bytes", getAttachmentSizeString(100)) @Test fun getAttachmentSizeString_kilobytes() = assertEquals("1 KB", getAttachmentSizeString(1024)) From 443f433f8b98a48b3a0b8ad94abd068530caa347 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Mon, 20 May 2024 13:07:01 +0200 Subject: [PATCH 06/27] summary and description done --- .../jtx/ui/detail/DetailBottomAppBar.kt | 4 +- .../jtx/ui/detail/DetailOptionsBottomSheet.kt | 3 +- .../jtx/ui/detail/DetailScreenContent.kt | 194 +++----------- .../jtx/ui/detail/DetailsCardDescription.kt | 238 ++++++++++++++++++ .../jtx/ui/detail/DetailsCardSummary.kt | 117 +++++++++ .../ui/detail/models/DetailsScreenSection.kt | 9 +- .../benchmark/BaselineProfileGenerator.kt | 1 - .../at/techbee/benchmark/StartupBenchmark.kt | 6 +- 8 files changed, 397 insertions(+), 175 deletions(-) create mode 100644 app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt create mode 100644 app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt index 1d6a01213..0c1fa6923 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt @@ -138,7 +138,7 @@ fun DetailBottomAppBar( } // Icons for Markdown formatting - AnimatedVisibility(isEditMode.value && markdownState.value != MarkdownState.DISABLED && markdownState.value != MarkdownState.CLOSED) { + AnimatedVisibility(markdownState.value != MarkdownState.DISABLED && markdownState.value != MarkdownState.CLOSED) { Row( verticalAlignment = Alignment.CenterVertically ) { @@ -148,7 +148,7 @@ fun DetailBottomAppBar( VerticalDivider(modifier = Modifier.height(40.dp)) } } - AnimatedVisibility(isEditMode.value && markdownState.value != MarkdownState.DISABLED && markdownState.value != MarkdownState.CLOSED) { + AnimatedVisibility(markdownState.value != MarkdownState.DISABLED && markdownState.value != MarkdownState.CLOSED) { Row( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt index 09f6f90bf..5546314d0 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt @@ -176,7 +176,8 @@ fun DetailOptionsBottomSheet( enabled = when (setting) { DetailsScreenSection.COLLECTION -> true DetailsScreenSection.DATES -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] != false - DetailsScreenSection.SUMMARYDESCRIPTION -> true + DetailsScreenSection.SUMMARY -> true //TODO + DetailsScreenSection.DESCRIPTION -> true //TODO DetailsScreenSection.PROGRESS -> module == Module.TODO DetailsScreenSection.STATUSCLASSIFICATIONPRIORITY -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false DetailsScreenSection.CATEGORIES -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != false diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index 5444d2600..fbc3513b9 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -20,8 +20,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.NavigateBefore import androidx.compose.material.icons.automirrored.outlined.NavigateNext @@ -30,9 +28,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -49,24 +45,14 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.getTextBeforeSelection import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData @@ -99,9 +85,7 @@ import at.techbee.jtx.ui.reusable.elements.ProgressElement import at.techbee.jtx.ui.settings.DropdownSettingOption import at.techbee.jtx.ui.settings.SettingsStateHolder import at.techbee.jtx.util.DateTimeUtils -import com.arnyminerz.markdowntext.MarkdownText import kotlinx.coroutines.delay -import org.apache.commons.lang3.StringUtils import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -217,22 +201,26 @@ fun DetailScreenContent( } val color = rememberSaveable { mutableStateOf(iCalObject.color) } - var summary by rememberSaveable { mutableStateOf(iCalObject.summary ?:"") } - var description by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(iCalObject.description ?: "")) } + //var summary by rememberSaveable { mutableStateOf(iCalObject.summary ?:"") } + //var description by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(iCalObject.description ?: "")) } // make sure the values get propagated in the iCalObject again when the orientation changes + /* if(iCalObject.summary != summary.ifEmpty { null } || iCalObject.description != description.text.ifEmpty { null } || iCalObject.color != color.value) { iCalObject.summary = summary.ifEmpty { null } iCalObject.description = description.text.ifEmpty { null } iCalObject.color = color.value } + */ // Apply Markdown on recomposition if applicable, then set back to OBSERVING + /* TODO if (markdownState.value != MarkdownState.DISABLED && markdownState.value != MarkdownState.CLOSED) { description = markdownState.value.format(description) markdownState.value = MarkdownState.OBSERVING } + */ val isProPurchased = BillingManager.getInstance().isProPurchased.observeAsState(true) val allPossibleCollections = allWriteableCollections.value.filter { @@ -401,155 +389,33 @@ fun DetailScreenContent( ) } - DetailsScreenSection.SUMMARYDESCRIPTION -> { - if(!isEditMode.value && (summary.isNotBlank() || description.text.isNotBlank())) { - SelectionContainer(modifier = detailElementModifier) { - ElevatedCard( - onClick = { - if (collection?.readonly == false) - isEditMode.value = true - }, - modifier = Modifier - .fillMaxWidth() - .testTag("benchmark:DetailSummary") - ) { - - if (summary.isNotBlank()) - Text( - summary.trim(), - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - style = MaterialTheme.typography.titleMedium - ) - - if (description.text.isNotBlank()) { - if (detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] != false) - MarkdownText( - markdown = description.text.trim(), - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - style = TextStyle( - textDirection = TextDirection.Content, - fontFamily = LocalTextStyle.current.fontFamily - ), - onClick = { - if (collection?.readonly == false) - isEditMode.value = true - } - ) - else - Text( - text = description.text.trim(), - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - ) - } - } - } - } - - if(isEditMode.value) { - ElevatedCard( - modifier = detailElementModifier - .fillMaxWidth() - .testTag("benchmark:DetailSummaryCardEdit") - ) { - - if(summary.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUMMARY] == true || showAllOptions) { - OutlinedTextField( - value = summary, - onValueChange = { - summary = it - iCalObject.summary = it.ifEmpty { null } - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED - }, - label = { Text(stringResource(id = R.string.summary)) }, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default - ), - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) - } - - if(description.text.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DESCRIPTION] == true || showAllOptions) { - OutlinedTextField( - value = description, - onValueChange = { - - // START Create bulletpoint if previous line started with a bulletpoint - val enteredCharIndex = - StringUtils.indexOfDifference(it.text, description.text) - val enteredCharIsReturn = - enteredCharIndex >= 0 - && it.text.substring(enteredCharIndex) - .startsWith(System.lineSeparator()) - && it.text.length > description.text.length // excludes backspace! + DetailsScreenSection.SUMMARY -> { + DetailsCardSummary( + initialSummary = iCalObject.summary, + isReadOnly = collection?.readonly?:true, + onSummaryUpdated = { + iCalObject.summary = it + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + }, + modifier = detailElementModifier.testTag("benchmark:DetailSummary") + ) + } - val before = it.getTextBeforeSelection(Int.MAX_VALUE) - val after = - if (it.selection.start < it.annotatedString.lastIndex) it.annotatedString.subSequence( - it.selection.start, - it.annotatedString.lastIndex + 1 - ) else AnnotatedString("") - val lines = before.split(System.lineSeparator()) - val previous = - if (lines.lastIndex > 1) lines[lines.lastIndex - 1] else before - val nextLineStartWith = when { - previous.startsWith("- [ ] ") || previous.startsWith("- [x]") -> "- [ ] " - previous.startsWith("* ") -> "* " - previous.startsWith("- ") -> "- " - else -> null - } + DetailsScreenSection.DESCRIPTION -> { + DetailsCardDescription( + initialDescription = iCalObject.description, + isReadOnly = collection?.readonly?:true, + onDescriptionUpdated = { + iCalObject.description = it + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + }, + markdownState = markdownState, + isMarkdownEnabled = detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] != false, + modifier = detailElementModifier + ) + } - description = - if (description.text != it.text && (nextLineStartWith != null) && enteredCharIsReturn) - TextFieldValue( - annotatedString = before.plus( - AnnotatedString( - nextLineStartWith - ) - ).plus(after), - selection = TextRange(it.selection.start + nextLineStartWith.length) - ) - else - it - // END Create bulletpoint if previous line started with a bulletpoint - iCalObject.description = it.text.ifEmpty { null } - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED - }, - label = { Text(stringResource(id = R.string.description)) }, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default - ), - minLines = 3, - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .onFocusChanged { focusState -> - if ( - focusState.hasFocus - && markdownState.value == MarkdownState.DISABLED - && detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] != false - ) - markdownState.value = MarkdownState.OBSERVING - else if (!focusState.hasFocus) - markdownState.value = MarkdownState.DISABLED - } - ) - } - } - } - } DetailsScreenSection.PROGRESS -> { if(iCalObject.module == Module.TODO.name) { diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt new file mode 100644 index 000000000..6875d6eeb --- /dev/null +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt @@ -0,0 +1,238 @@ +/* + * Copyright (c) Techbee e.U. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.techbee.jtx.ui.detail + +import android.util.Log +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Arrangement +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.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Summarize +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.getTextBeforeSelection +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.techbee.jtx.R +import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon +import com.arnyminerz.markdowntext.MarkdownText +import org.apache.commons.lang3.StringUtils + + +@Composable +fun DetailsCardDescription( + initialDescription: String?, + isReadOnly: Boolean, + markdownState: MutableState, + isMarkdownEnabled: Boolean, + onDescriptionUpdated: (String?) -> Unit, + modifier: Modifier = Modifier +) { + + val focusRequester = remember { FocusRequester() } + var focusRequested by remember { mutableStateOf(false) } + var isDescriptionFocused by rememberSaveable { mutableStateOf(false) } + var description by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf( + TextFieldValue(initialDescription?:"") + ) } + + LaunchedEffect(focusRequested, isDescriptionFocused) { + if(focusRequested) { + try { + focusRequester.requestFocus() + focusRequested = false + } catch (e: Exception) { + Log.d("DetailsCardDescription", "Requesting Focus failed") + } + } + } + + + ElevatedCard( + onClick = { + if(!isReadOnly) { + focusRequested = true + } + }, + modifier = modifier + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + HeadlineWithIcon( + icon = Icons.Outlined.Summarize, + iconDesc = null, + text = stringResource(id = R.string.description) + ) + + Crossfade(!focusRequested && !isDescriptionFocused && isMarkdownEnabled, + label = "descriptionWithMarkdown" + ) { withMarkdown -> + + if(withMarkdown) { + MarkdownText( + markdown = description.text.trim(), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + style = TextStyle( + textDirection = TextDirection.Content, + fontFamily = LocalTextStyle.current.fontFamily + ), + onClick = { + if (!isReadOnly) { + focusRequested = true + } + } + ) + } else { + BasicTextField( + value = description, + textStyle = LocalTextStyle.current, + onValueChange = { + + // START Create bulletpoint if previous line started with a bulletpoint + val enteredCharIndex = + StringUtils.indexOfDifference(it.text, description.text) + val enteredCharIsReturn = + enteredCharIndex >= 0 + && it.text.substring(enteredCharIndex) + .startsWith(System.lineSeparator()) + && it.text.length > description.text.length // excludes backspace! + + val before = it.getTextBeforeSelection(Int.MAX_VALUE) + val after = + if (it.selection.start < it.annotatedString.lastIndex) it.annotatedString.subSequence( + it.selection.start, + it.annotatedString.lastIndex + 1 + ) else AnnotatedString("") + val lines = before.split(System.lineSeparator()) + val previous = + if (lines.lastIndex > 1) lines[lines.lastIndex - 1] else before + val nextLineStartWith = when { + previous.startsWith("- [ ] ") || previous.startsWith("- [x]") -> "- [ ] " + previous.startsWith("* ") -> "* " + previous.startsWith("- ") -> "- " + else -> null + } + + description = + if (description.text != it.text && (nextLineStartWith != null) && enteredCharIsReturn) + TextFieldValue( + annotatedString = before.plus( + AnnotatedString( + nextLineStartWith + ) + ).plus(after), + selection = TextRange(it.selection.start + nextLineStartWith.length) + ) + else + it + // END Create bulletpoint if previous line started with a bulletpoint + + onDescriptionUpdated(description.text.ifBlank { null }) + }, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + enabled = !isReadOnly, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = if (isDescriptionFocused || description.text.isNotBlank()) 8.dp else 4.dp) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + isDescriptionFocused = focusState.hasFocus + + if ( + focusState.hasFocus + && markdownState.value == MarkdownState.DISABLED + && isMarkdownEnabled + ) + markdownState.value = MarkdownState.OBSERVING + else if (!focusState.hasFocus) + markdownState.value = MarkdownState.DISABLED + } + ) + } + } + } + } + } + } +} + + +@Preview(showBackground = true) +@Composable +fun DetailsCardDescription_Preview_no_Markdown() { + MaterialTheme { + DetailsCardDescription( + initialDescription = "Test" + System.lineSeparator() + "***Tester***", + isReadOnly = false, + onDescriptionUpdated = { }, + markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, + isMarkdownEnabled = false + ) + } +} + + +@Preview(showBackground = true) +@Composable +fun DetailsCardDescription_Preview_with_Markdown() { + MaterialTheme { + DetailsCardDescription( + initialDescription = "Test" + System.lineSeparator() + "***Tester***", + isReadOnly = false, + onDescriptionUpdated = { }, + markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, + isMarkdownEnabled = true + ) + } +} + diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt new file mode 100644 index 000000000..7f971892d --- /dev/null +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) Techbee e.U. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.techbee.jtx.ui.detail + +import androidx.compose.foundation.layout.Arrangement +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.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Summarize +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.techbee.jtx.R +import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon + + +@Composable +fun DetailsCardSummary( + initialSummary: String?, + isReadOnly: Boolean, + onSummaryUpdated: (String?) -> Unit, + modifier: Modifier = Modifier +) { + + val focusRequester = remember { FocusRequester() } + var isSummaryFocused by rememberSaveable { mutableStateOf(false) } + var summary by rememberSaveable { mutableStateOf(initialSummary) } + + ElevatedCard( + onClick = { + if(!isReadOnly) + focusRequester.requestFocus() + }, + modifier = modifier + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + HeadlineWithIcon( + icon = Icons.Outlined.Summarize, + iconDesc = null, + text = stringResource(id = R.string.summary) + ) + + + BasicTextField( + value = summary?:"", + textStyle = LocalTextStyle.current, + onValueChange = { newSummary -> + summary = newSummary.ifBlank { null } + onSummaryUpdated(summary) + }, + keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + enabled = !isReadOnly, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = if (isSummaryFocused || !summary.isNullOrBlank()) 8.dp else 4.dp) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + isSummaryFocused = focusState.hasFocus + } + ) + } + } + } + } +} + + +@Preview(showBackground = true) +@Composable +fun DetailsCardSummary_Preview() { + MaterialTheme { + DetailsCardSummary( + initialSummary = "Test", + isReadOnly = false, + onSummaryUpdated = { } + ) + } +} + diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt b/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt index 30933d441..48fab6761 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt @@ -9,7 +9,8 @@ enum class DetailsScreenSection( ) { COLLECTION(R.string.collection), DATES(R.string.date), - SUMMARYDESCRIPTION(R.string.summary_description), + SUMMARY(R.string.summary), + DESCRIPTION(R.string.description), PROGRESS(R.string.progress), STATUSCLASSIFICATIONPRIORITY(R.string.status_classification_priority), CATEGORIES(R.string.categories), @@ -29,9 +30,9 @@ enum class DetailsScreenSection( companion object { fun entriesFor(module: Module): List { return when(module) { - Module.JOURNAL -> listOf(COLLECTION, DATES, SUMMARYDESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, RECURRENCE) - Module.NOTE -> listOf(COLLECTION, SUMMARYDESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS) - Module.TODO -> listOf(COLLECTION, DATES, SUMMARYDESCRIPTION, PROGRESS, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, RESOURCES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, ALARMS, RECURRENCE) + Module.JOURNAL -> listOf(COLLECTION, DATES, SUMMARY, DESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, RECURRENCE) + Module.NOTE -> listOf(COLLECTION, SUMMARY, DESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS) + Module.TODO -> listOf(COLLECTION, DATES, SUMMARY, DESCRIPTION, PROGRESS, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, RESOURCES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, ALARMS, RECURRENCE) } } } diff --git a/benchmark/src/main/java/at/techbee/benchmark/BaselineProfileGenerator.kt b/benchmark/src/main/java/at/techbee/benchmark/BaselineProfileGenerator.kt index 8e2a07d8e..5ae7e3e85 100644 --- a/benchmark/src/main/java/at/techbee/benchmark/BaselineProfileGenerator.kt +++ b/benchmark/src/main/java/at/techbee/benchmark/BaselineProfileGenerator.kt @@ -42,6 +42,5 @@ class BaselineProfileGenerator { device.findObject(By.res(BENCHMARK_TAG_LISTCARD)).click() device.wait(Until.hasObject(By.res(BENCHMARK_TAG_DETAILSUMMARY)), 30_000) device.findObject(By.res(BENCHMARK_TAG_DETAILSUMMARY)).click() - device.wait(Until.hasObject(By.res(BENCHMARK_TAG_DETAILSUMMARYCARDEDIT)), 30_000) } } \ No newline at end of file diff --git a/benchmark/src/main/java/at/techbee/benchmark/StartupBenchmark.kt b/benchmark/src/main/java/at/techbee/benchmark/StartupBenchmark.kt index 98360f3da..732a28a8a 100644 --- a/benchmark/src/main/java/at/techbee/benchmark/StartupBenchmark.kt +++ b/benchmark/src/main/java/at/techbee/benchmark/StartupBenchmark.kt @@ -2,7 +2,9 @@ package at.techbee.benchmark import android.os.Build import androidx.annotation.RequiresApi -import androidx.benchmark.macro.* +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.uiautomator.By @@ -13,7 +15,6 @@ import org.junit.runner.RunWith const val BENCHMARK_TAG_LISTCARD = "benchmark:ListCard" const val BENCHMARK_TAG_DETAILSUMMARY = "benchmark:DetailSummary" -const val BENCHMARK_TAG_DETAILSUMMARYCARDEDIT = "benchmark:DetailSummaryCardEdit" /** * This is an example startup benchmark. @@ -90,7 +91,6 @@ class StartupBenchmark { device.wait(Until.hasObject(By.res(BENCHMARK_TAG_DETAILSUMMARY)), 30_000) device.findObject(By.res(BENCHMARK_TAG_DETAILSUMMARY)).click() - device.wait(Until.hasObject(By.res(BENCHMARK_TAG_DETAILSUMMARYCARDEDIT)), 30_000) } } From 39260a9298a609b6941e990e95167bd384286064 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 25 May 2024 20:43:53 +0200 Subject: [PATCH 07/27] updated comments card --- .../jtx/ui/detail/DetailsCardComments.kt | 40 ++++++++++++++----- .../jtx/ui/reusable/cards/CommentCard.kt | 40 +++++++++---------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt index db322cf25..730fefd5b 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt @@ -12,15 +12,17 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Comment import androidx.compose.material.icons.outlined.AddComment import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -31,6 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -81,14 +84,6 @@ fun DetailsCardComments( iconDesc = headline, text = headline ) - - if(!isReadOnly) { - IconButton(onClick = { - showAddCommentDialog = true - }) { - Icon(Icons.Outlined.AddComment, stringResource(id = R.string.edit_comment_helper)) - } - } } AnimatedVisibility(comments.isNotEmpty()) { @@ -108,8 +103,33 @@ fun DetailsCardComments( onCommentUpdated = { updatedComment -> comment.text = updatedComment.text onCommentsUpdated() - } + }, + ) + } + } + } + + if(!isReadOnly) { + + ElevatedCard( + onClick = { + showAddCommentDialog = true + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .alpha(0.5f), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Outlined.AddComment, + stringResource(id = R.string.edit_comment_helper) ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(id = R.string.add_comment)) } } } diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt index 17d9fb0ab..0ba3efe53 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt @@ -11,6 +11,7 @@ package at.techbee.jtx.ui.reusable.cards import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme @@ -48,29 +49,28 @@ fun CommentCard( ) } - ElevatedCard( - modifier = modifier, - onClick = { - if(!isReadOnly) - showCommentEditDialog = true - } + ElevatedCard( + modifier = modifier, + onClick = { + if(!isReadOnly) + showCommentEditDialog = true + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .heightIn(min = 38.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically ) { - Row( + Text( + comment.text, modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - comment.text, - modifier = Modifier - .padding(start = 8.dp, end = 8.dp) - .align(alignment = Alignment.CenterVertically) - .weight(1f) - ) - } + .padding(start = 8.dp, end = 8.dp) + ) } + } } From 4088e6ae93171f1d634e08371443a7573bd2049d Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 25 May 2024 20:53:43 +0200 Subject: [PATCH 08/27] allways show showAllOptions button and last edited info --- .../jtx/ui/detail/DetailScreenContent.kt | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index fbc3513b9..1f06d2185 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -779,9 +779,8 @@ fun DetailScreenContent( } } - if(isEditMode.value && !showAllOptions) { - item { - + if(!showAllOptions) { + item { TextButton( onClick = { showAllOptions = true }, modifier = detailElementModifier.fillMaxWidth() @@ -791,37 +790,36 @@ fun DetailScreenContent( } } - if(!isEditMode.value) { - item { - Column( - horizontalAlignment = Alignment.End, - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - ) { - Text( - stringResource( - id = R.string.view_created_text, - DateTimeUtils.convertLongToFullDateTimeString(iCalObject.created, null) - ), - style = MaterialTheme.typography.bodySmall, - fontStyle = FontStyle.Italic - ) - Text( - stringResource( - id = R.string.view_last_modified_text, - DateTimeUtils.convertLongToFullDateTimeString( - iCalObject.lastModified, - null - ) - ), - style = MaterialTheme.typography.bodySmall, - fontStyle = FontStyle.Italic - ) - } + item { + Column( + horizontalAlignment = Alignment.End, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) { + Text( + stringResource( + id = R.string.view_created_text, + DateTimeUtils.convertLongToFullDateTimeString(iCalObject.created, null) + ), + style = MaterialTheme.typography.bodySmall, + fontStyle = FontStyle.Italic + ) + Text( + stringResource( + id = R.string.view_last_modified_text, + DateTimeUtils.convertLongToFullDateTimeString( + iCalObject.lastModified, + null + ) + ), + style = MaterialTheme.typography.bodySmall, + fontStyle = FontStyle.Italic + ) } } + if(!isEditMode.value) { item { val curIndex = icalObjectIdList.indexOf(observedICalObject.value?.id ?: 0) From e24d8ccf8d3f9c7d542630de98c4c9d51432466d Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 26 May 2024 08:25:57 +0200 Subject: [PATCH 09/27] minor adaption in description field --- .../java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt index 6875d6eeb..9a06b5116 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt @@ -115,9 +115,7 @@ fun DetailsCardDescription( if(withMarkdown) { MarkdownText( markdown = description.text.trim(), - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), + modifier = Modifier.fillMaxWidth(), style = TextStyle( textDirection = TextDirection.Content, fontFamily = LocalTextStyle.current.fontFamily From c111d0e46368ba6d4ca4513bfe68aff3aeb0b556 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 26 May 2024 09:25:14 +0200 Subject: [PATCH 10/27] status classification priority done --- .../jtx/ui/detail/DetailScreenContent.kt | 2 +- ...DetailsCardStatusClassificationPriority.kt | 165 +++--------------- 2 files changed, 25 insertions(+), 142 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index 1f06d2185..20e7c8592 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -455,7 +455,7 @@ fun DetailScreenContent( DetailsCardStatusClassificationPriority( icalObject = iCalObject, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly ?: true, enableStatus = detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] ?: true || showAllOptions, enableClassification = detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] ?: true || showAllOptions, enablePriority = detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] ?: true || showAllOptions, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt index f5718e7a0..146dcb05c 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt @@ -16,7 +16,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AssignmentLate import androidx.compose.material.icons.outlined.GppMaybe import androidx.compose.material.icons.outlined.PublishedWithChanges -import androidx.compose.material3.AssistChip import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ElevatedAssistChip @@ -47,7 +46,7 @@ import at.techbee.jtx.database.locals.ExtendedStatus @Composable fun DetailsCardStatusClassificationPriority( icalObject: ICalObject, - isEditMode: Boolean, + isReadOnly: Boolean, enableStatus: Boolean, enableClassification: Boolean, enablePriority: Boolean, @@ -71,71 +70,18 @@ fun DetailsCardStatusClassificationPriority( var classificationMenuExpanded by remember { mutableStateOf(false) } var priorityMenuExpanded by remember { mutableStateOf(false) } - if(!isEditMode && (icalObject.status?.isNotEmpty() == true || icalObject.xstatus?.isNotEmpty() == true)) { + if(enableStatus || !icalObject.status.isNullOrEmpty() || !icalObject.xstatus.isNullOrEmpty()) { ElevatedAssistChip( enabled = allowStatusChange, label = { - if(icalObject.xstatus?.isNotEmpty() == true) - Text(icalObject.xstatus!!) - else - Text( - text = Status.entries.find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - DropdownMenu( - expanded = statusMenuExpanded, - onDismissRequest = { statusMenuExpanded = false } - ) { - - Status.valuesFor(icalObject.getModuleFromString()).forEach { status -> - DropdownMenuItem( - text = { Text(stringResource(id = status.stringResource)) }, - onClick = { - icalObject.status = status.status - icalObject.xstatus = null - statusMenuExpanded = false - onStatusChanged(status) - } - ) - } - extendedStatuses - .filter { it.module == icalObject.getModuleFromString() } - .forEach { storedStatus -> - DropdownMenuItem( - text = { Text(storedStatus.xstatus) }, - onClick = { - icalObject.xstatus = storedStatus.xstatus - icalObject.status = storedStatus.rfcStatus.status - statusMenuExpanded = false - onStatusChanged(storedStatus.rfcStatus) - } - ) - } - } - }, - leadingIcon = { - Icon( - Icons.Outlined.PublishedWithChanges, - stringResource(id = R.string.status) + Text( + text = if(!icalObject.xstatus.isNullOrEmpty()) + icalObject.xstatus!! + else + Status.entries.find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis ) - }, - onClick = { statusMenuExpanded = true }, - modifier = Modifier.weight(0.33f) - ) - } else if(isEditMode && (enableStatus || !icalObject.status.isNullOrEmpty() || !icalObject.xstatus.isNullOrEmpty())) { - AssistChip( - enabled = allowStatusChange, - label = { - if(icalObject.xstatus?.isNotEmpty() == true) - Text(icalObject.xstatus!!) - else - Text( - text = Status.entries.find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) DropdownMenu( expanded = statusMenuExpanded, @@ -174,13 +120,16 @@ fun DetailsCardStatusClassificationPriority( stringResource(id = R.string.status) ) }, - onClick = { statusMenuExpanded = true }, + onClick = { + if(!isReadOnly) + statusMenuExpanded = true + }, modifier = Modifier.weight(0.33f) ) } - if(!isEditMode && !icalObject.classification.isNullOrEmpty()) { + if(enableClassification || !icalObject.classification.isNullOrEmpty()) { ElevatedAssistChip( label = { Text( @@ -212,42 +161,10 @@ fun DetailsCardStatusClassificationPriority( stringResource(id = R.string.classification) ) }, - onClick = { classificationMenuExpanded = true }, - modifier = Modifier.weight(0.33f) - ) - } else if(isEditMode && (enableClassification || !icalObject.classification.isNullOrEmpty())) { - AssistChip( - label = { - Text( - Classification.entries.find { it.classification == icalObject.classification }?.stringResource?.let { stringResource(id = it) }?: icalObject.classification ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - DropdownMenu( - expanded = classificationMenuExpanded, - onDismissRequest = { classificationMenuExpanded = false } - ) { - - Classification.entries.forEach { clazzification -> - DropdownMenuItem( - text = { Text(stringResource(id = clazzification.stringResource)) }, - onClick = { - icalObject.classification = clazzification.classification - classificationMenuExpanded = false - onClassificationChanged(clazzification) - } - ) - } - } - }, - leadingIcon = { - Icon( - Icons.Outlined.GppMaybe, - stringResource(id = R.string.classification) - ) + onClick = { + if(!isReadOnly) + classificationMenuExpanded = true }, - onClick = { classificationMenuExpanded = true }, modifier = Modifier.weight(0.33f) ) } @@ -255,7 +172,7 @@ fun DetailsCardStatusClassificationPriority( val priorityStrings = stringArrayResource(id = R.array.priority) if (icalObject.component == Component.VTODO.name) { - if(!isEditMode && icalObject.priority in 1..9) { + if(enablePriority || icalObject.priority in 1..9) { ElevatedAssistChip( label = { Text( @@ -289,44 +206,10 @@ fun DetailsCardStatusClassificationPriority( stringResource(id = R.string.priority) ) }, - onClick = { priorityMenuExpanded = true }, - modifier = Modifier.weight(0.33f) - ) - } else if(isEditMode && (enablePriority || icalObject.priority in 1..9)) { - AssistChip( - label = { - Text( - if (icalObject.priority in priorityStrings.indices) - stringArrayResource(id = R.array.priority)[icalObject.priority?:0] - else - stringArrayResource(id = R.array.priority)[0], - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - DropdownMenu( - expanded = priorityMenuExpanded, - onDismissRequest = { priorityMenuExpanded = false } - ) { - stringArrayResource(id = R.array.priority).forEachIndexed { index, prio -> - DropdownMenuItem( - text = { Text(prio) }, - onClick = { - icalObject.priority = if(index == 0) null else index - priorityMenuExpanded = false - onPriorityChanged(icalObject.priority) - } - ) - } - } - }, - leadingIcon = { - Icon( - Icons.Outlined.AssignmentLate, - stringResource(id = R.string.priority) - ) + onClick = { + if(!isReadOnly) + priorityMenuExpanded = true }, - onClick = { priorityMenuExpanded = true }, modifier = Modifier.weight(0.33f) ) } @@ -341,7 +224,7 @@ fun DetailsCardStatusClassificationPriority_Journal_Preview() { MaterialTheme { DetailsCardStatusClassificationPriority( icalObject = ICalObject.createJournal(), - isEditMode = false, + isReadOnly = false, enableStatus = false, enableClassification = false, enablePriority = false, @@ -360,7 +243,7 @@ fun DetailsCardStatusClassificationPriority_Todo_Preview() { MaterialTheme { DetailsCardStatusClassificationPriority( icalObject = ICalObject.createTodo(), - isEditMode = true, + isReadOnly = true, enableStatus = true, enableClassification = true, enablePriority = true, @@ -379,7 +262,7 @@ fun DetailsCardStatusClassificationPriority_Todo_Preview2() { MaterialTheme { DetailsCardStatusClassificationPriority( icalObject = ICalObject.createTodo(), - isEditMode = true, + isReadOnly = true, enableStatus = true, enableClassification = false, enablePriority = false, From 25199ef2bc6a91124fe0acbe3fcf145c98163e9e Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 26 May 2024 09:25:34 +0200 Subject: [PATCH 11/27] removed edit mode variable from bottom app bar --- .../jtx/ui/detail/DetailBottomAppBar.kt | 23 ++++++++----------- .../at/techbee/jtx/ui/detail/DetailsScreen.kt | 1 - 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt index 0c1fa6923..0ad8bc0e0 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt @@ -21,8 +21,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.List -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.EditOff import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.DriveFileRenameOutline import androidx.compose.material.icons.outlined.FormatBold @@ -60,7 +58,6 @@ import at.techbee.jtx.util.SyncApp fun DetailBottomAppBar( iCalObject: ICalObject?, collection: ICalCollection?, - isEditMode: MutableState, markdownState: MutableState, isProActionAvailable: Boolean, changeState: MutableState, @@ -85,8 +82,7 @@ fun DetailBottomAppBar( } AnimatedVisibility( - isEditMode.value - && changeState.value != DetailViewModel.DetailChangeState.UNCHANGED + changeState.value != DetailViewModel.DetailChangeState.UNCHANGED && (markdownState.value == MarkdownState.DISABLED || markdownState.value == MarkdownState.CLOSED) ) { IconButton(onClick = { onRevertClicked() }) { @@ -201,11 +197,15 @@ fun DetailBottomAppBar( context.getText(R.string.buypro_snackbar_remote_entries_blocked), Toast.LENGTH_LONG ).show() - else if (!collection.readonly) - isEditMode.value = !isEditMode.value + + //TODO + }, - containerColor = if (collection.readonly || !isProActionAvailable) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.primaryContainer + //containerColor = if (collection.readonly || !isProActionAvailable) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.primaryContainer ) { + //TODO: restrict editing on remote collections!!! + Icon(painterResource(id = R.drawable.ic_save_move_outline), stringResource(id = R.string.save)) + /* Crossfade(targetState = isEditMode.value, label = "fab_icon_content") { isEditMode -> if (isEditMode) { Icon(painterResource(id = R.drawable.ic_save_move_outline), stringResource(id = R.string.save)) @@ -216,6 +216,7 @@ fun DetailBottomAppBar( Icon(Icons.Filled.Edit, stringResource(id = R.string.edit)) } } + */ } } ) @@ -235,7 +236,6 @@ fun DetailBottomAppBar_Preview_View() { DetailBottomAppBar( iCalObject = ICalObject.createNote().apply { dirty = true }, collection = collection, - isEditMode = remember { mutableStateOf(false) }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGEUNSAVED) }, @@ -258,7 +258,6 @@ fun DetailBottomAppBar_Preview_edit() { DetailBottomAppBar( iCalObject = ICalObject.createNote().apply { dirty = true }, collection = collection, - isEditMode = remember { mutableStateOf(true) }, isProActionAvailable = true, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, @@ -280,7 +279,6 @@ fun DetailBottomAppBar_Preview_edit_markdown() { DetailBottomAppBar( iCalObject = ICalObject.createNote().apply { dirty = true }, collection = collection, - isEditMode = remember { mutableStateOf(true) }, isProActionAvailable = true, markdownState = remember { mutableStateOf(MarkdownState.OBSERVING) }, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, @@ -302,7 +300,6 @@ fun DetailBottomAppBar_Preview_View_readonly() { DetailBottomAppBar( iCalObject = ICalObject.createNote().apply { dirty = false }, collection = collection, - isEditMode = remember { mutableStateOf(false) }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) }, @@ -324,7 +321,6 @@ fun DetailBottomAppBar_Preview_View_proOnly() { DetailBottomAppBar( iCalObject = ICalObject.createNote().apply { dirty = false }, collection = collection, - isEditMode = remember { mutableStateOf(false) }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = false, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) }, @@ -349,7 +345,6 @@ fun DetailBottomAppBar_Preview_View_local() { DetailBottomAppBar( iCalObject = ICalObject.createNote().apply { dirty = true }, collection = collection, - isEditMode = remember { mutableStateOf(false) }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt index 9f949c900..29698d08e 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt @@ -708,7 +708,6 @@ fun DetailsScreen( DetailBottomAppBar( iCalObject = iCalObject.value, collection = collection.value, - isEditMode = isEditMode, markdownState = markdownState, isProActionAvailable = isProActionAvailable, changeState = detailViewModel.changeState, From 986e3eb557ce2eb7e128aeaeae3099b7a73f36a9 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 26 May 2024 09:36:38 +0200 Subject: [PATCH 12/27] Moved also revert option to menu --- .../jtx/ui/detail/DetailBottomAppBar.kt | 32 ++++--------------- .../at/techbee/jtx/ui/detail/DetailsScreen.kt | 15 +++++---- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt index 0ad8bc0e0..61518be14 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt @@ -60,8 +60,7 @@ fun DetailBottomAppBar( collection: ICalCollection?, markdownState: MutableState, isProActionAvailable: Boolean, - changeState: MutableState, - onRevertClicked: () -> Unit + changeState: MutableState ) { if (iCalObject == null || collection == null) @@ -81,19 +80,6 @@ fun DetailBottomAppBar( } } - AnimatedVisibility( - changeState.value != DetailViewModel.DetailChangeState.UNCHANGED - && (markdownState.value == MarkdownState.DISABLED || markdownState.value == MarkdownState.CLOSED) - ) { - IconButton(onClick = { onRevertClicked() }) { - Icon( - painterResource(id = R.drawable.ic_revert), - contentDescription = stringResource(id = R.string.revert) - ) - } - } - - AnimatedVisibility((changeState.value == DetailViewModel.DetailChangeState.CHANGEUNSAVED || changeState.value == DetailViewModel.DetailChangeState.CHANGESAVING || changeState.value == DetailViewModel.DetailChangeState.CHANGESAVED) @@ -238,8 +224,7 @@ fun DetailBottomAppBar_Preview_View() { collection = collection, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, - changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGEUNSAVED) }, - onRevertClicked = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGEUNSAVED) } ) } } @@ -261,7 +246,6 @@ fun DetailBottomAppBar_Preview_edit() { isProActionAvailable = true, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, - onRevertClicked = { } ) } } @@ -281,8 +265,7 @@ fun DetailBottomAppBar_Preview_edit_markdown() { collection = collection, isProActionAvailable = true, markdownState = remember { mutableStateOf(MarkdownState.OBSERVING) }, - changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, - onRevertClicked = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) } ) } } @@ -302,8 +285,7 @@ fun DetailBottomAppBar_Preview_View_readonly() { collection = collection, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, - changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) }, - onRevertClicked = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) } ) } } @@ -323,8 +305,7 @@ fun DetailBottomAppBar_Preview_View_proOnly() { collection = collection, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = false, - changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) }, - onRevertClicked = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) } ) } } @@ -347,8 +328,7 @@ fun DetailBottomAppBar_Preview_View_local() { collection = collection, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isProActionAvailable = true, - changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) }, - onRevertClicked = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) } ) } } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt index 29698d08e..93a70a4b6 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt @@ -577,11 +577,7 @@ fun DetailsScreen( } - if( - collection.value?.readonly == false - && isProActionAvailable - && (markdownState.value == MarkdownState.DISABLED || markdownState.value == MarkdownState.CLOSED) - ) { + if(collection.value?.readonly == false && isProActionAvailable) { DropdownMenuItem( leadingIcon = { Icon(Icons.Outlined.Delete, null) }, text = { Text(stringResource(id = R.string.delete)) }, @@ -591,6 +587,12 @@ fun DetailsScreen( } ) + DropdownMenuItem( + leadingIcon = { Icon(painterResource(id = R.drawable.ic_revert), null) }, + text = { Text(stringResource(id = R.string.revert)) }, + onClick = { showRevertDialog = true } + ) + HorizontalDivider() } @@ -710,8 +712,7 @@ fun DetailsScreen( collection = collection.value, markdownState = markdownState, isProActionAvailable = isProActionAvailable, - changeState = detailViewModel.changeState, - onRevertClicked = { showRevertDialog = true } + changeState = detailViewModel.changeState ) } ) From 21e0a64090280c39f101e5bdedbfeca1eab92b95 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 26 May 2024 13:33:02 +0200 Subject: [PATCH 13/27] Categories and Resources done (moved to dialog) --- .../techbee/jtx/database/ICalDatabaseDao.kt | 30 ++- .../jtx/ui/detail/DetailScreenContent.kt | 77 +++--- .../techbee/jtx/ui/detail/DetailViewModel.kt | 47 +++- .../jtx/ui/detail/DetailsCardCategories.kt | 217 ++++----------- .../jtx/ui/detail/DetailsCardResources.kt | 213 ++++----------- .../at/techbee/jtx/ui/detail/DetailsScreen.kt | 10 +- .../reusable/dialogs/EditCategoriesDialog.kt | 248 ++++++++++++++++++ .../reusable/dialogs/EditResourcesDialog.kt | 248 ++++++++++++++++++ 8 files changed, 696 insertions(+), 394 deletions(-) create mode 100644 app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditCategoriesDialog.kt create mode 100644 app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditResourcesDialog.kt diff --git a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt index 77c605552..870f38ec4 100644 --- a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt +++ b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt @@ -585,7 +585,7 @@ interface ICalDatabaseDao { suspend fun updateToDeleted(id: Long, lastModified: Long) @Query("UPDATE $TABLE_NAME_ICALOBJECT SET $COLUMN_LAST_MODIFIED = :lastModified, $COLUMN_SEQUENCE = $COLUMN_SEQUENCE + 1, $COLUMN_DIRTY = 1 WHERE $COLUMN_ID = :id") - suspend fun updateSetDirty(id: Long, lastModified: Long) + suspend fun updateSetDirty(id: Long, lastModified: Long = System.currentTimeMillis()) @Query("UPDATE $TABLE_NAME_ICALOBJECT SET $COLUMN_SUBTASKS_EXPANDED = :isSubtasksExpanded, $COLUMN_SUBNOTES_EXPANDED = :isSubnotesExpanded, $COLUMN_ATTACHMENTS_EXPANDED = :isAttachmentsExpanded, $COLUMN_PARENTS_EXPANDED = :isParentsExpanded WHERE $COLUMN_ID = :id") suspend fun updateExpanded( @@ -1527,6 +1527,34 @@ interface ICalDatabaseDao { } } + + @Transaction + suspend fun updateCategories(iCalObjectId: Long, uid: String, categories: List) { + + deleteCategories(iCalObjectId) + categories.forEach { it.icalObjectId = iCalObjectId } + upsertCategories(categories) + updateSetDirty(iCalObjectId) + makeSeriesDirty(uid) + } + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertCategories(categories: List) + + + @Transaction + suspend fun updateResources(iCalObjectId: Long, uid: String, resources: List) { + + deleteResources(iCalObjectId) + resources.forEach { it.icalObjectId = iCalObjectId } + upsertResources(resources) + updateSetDirty(iCalObjectId) + makeSeriesDirty(uid) + } + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertResources(resources: List) + @Transaction suspend fun saveAll( icalObject: ICalObject, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index 20e7c8592..006bf0e1d 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -62,13 +62,11 @@ import at.techbee.jtx.R import at.techbee.jtx.database.Component import at.techbee.jtx.database.ICalCollection import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE +import at.techbee.jtx.database.ICalDatabase import at.techbee.jtx.database.ICalObject import at.techbee.jtx.database.Module import at.techbee.jtx.database.Status -import at.techbee.jtx.database.locals.ExtendedStatus -import at.techbee.jtx.database.locals.StoredCategory import at.techbee.jtx.database.locals.StoredListSettingData -import at.techbee.jtx.database.locals.StoredResource import at.techbee.jtx.database.properties.Alarm import at.techbee.jtx.database.properties.AlarmRelativeTo import at.techbee.jtx.database.properties.Attachment @@ -108,11 +106,6 @@ fun DetailScreenContent( parentsLive: LiveData>, isChildLive: LiveData, allWriteableCollectionsLive: LiveData>, - allCategoriesLive: LiveData>, - allResourcesLive: LiveData>, - storedCategories: List, - storedResources: List, - extendedStatuses: List, detailSettings: DetailSettings, icalObjectIdList: List, seriesInstancesLive: LiveData>, @@ -136,6 +129,8 @@ fun DetailScreenContent( onSubEntryDeleted: (icalObjectId: Long) -> Unit, onSubEntryUpdated: (icalObjectId: Long, newText: String) -> Unit, onUnlinkSubEntry: (icalObjectId: Long, parentUID: String?) -> Unit, + onCategoriesUpdated: (List) -> Unit, + onResourcesUpdated: (List) -> Unit, goToDetail: (itemId: Long, editMode: Boolean, list: List, popBackStack: Boolean) -> Unit, goBack: () -> Unit, goToFilteredList: (StoredListSettingData) -> Unit, @@ -460,7 +455,11 @@ fun DetailScreenContent( enableClassification = detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] ?: true || showAllOptions, enablePriority = detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] ?: true || showAllOptions, allowStatusChange = !(linkProgressToSubtasks && subtasks.value.isNotEmpty()), - extendedStatuses = extendedStatuses, + extendedStatuses = ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getStoredStatuses() + .observeAsState(emptyList()).value, onStatusChanged = { newStatus -> if (keepStatusProgressCompletedInSync && iCalObject.getModuleFromString() == Module.TODO) { when (newStatus) { @@ -489,15 +488,19 @@ fun DetailScreenContent( } DetailsScreenSection.CATEGORIES -> { - if(categories.isNotEmpty() || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != false || showAllOptions))) { + if(categories.isNotEmpty() || (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != false || showAllOptions)) { DetailsCardCategories( categories = categories, - storedCategories = storedCategories, - isEditMode = isEditMode.value, + storedCategories = ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getStoredCategories() + .observeAsState(emptyList()).value, + isReadOnly = collection?.readonly?: true, onCategoriesUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + onCategoriesUpdated(it) }, - allCategoriesLive = allCategoriesLive, onGoToFilteredList = goToFilteredList, modifier = detailElementModifier ) @@ -628,16 +631,20 @@ fun DetailScreenContent( } } DetailsScreenSection.RESOURCES -> { - if(resources.isNotEmpty() || (isEditMode.value && iCalObject.getModuleFromString() == Module.TODO && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_RESOURCES] == true || showAllOptions))) { + if(iCalObject.getModuleFromString() == Module.TODO && (resources.isNotEmpty() || (detailSettings.detailSetting[DetailSettingsOption.ENABLE_RESOURCES] == true || showAllOptions))) { DetailsCardResources( resources = resources, - storedResources = storedResources, - isEditMode = isEditMode.value, + storedResources = ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getStoredResources() + .observeAsState(emptyList()).value, + isReadOnly = collection?.readonly?: true, onResourcesUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + onResourcesUpdated(it) }, onGoToFilteredList = goToFilteredList, - allResourcesLive = allResourcesLive, modifier = detailElementModifier ) } @@ -921,11 +928,6 @@ fun DetailScreenContent_JOURNAL() { markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, scrollToSectionState = remember { mutableStateOf(null) }, allWriteableCollectionsLive = MutableLiveData(listOf(ICalCollection.createLocalCollection(LocalContext.current))), - allCategoriesLive = MutableLiveData(emptyList()), - allResourcesLive = MutableLiveData(emptyList()), - storedCategories = emptyList(), - storedResources = emptyList(), - extendedStatuses = emptyList(), detailSettings = detailSettings, icalObjectIdList = emptyList(), saveEntry = { }, @@ -941,7 +943,9 @@ fun DetailScreenContent_JOURNAL() { onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, - onUpdateSortOrder = { } + onUpdateSortOrder = { }, + onCategoriesUpdated = { }, + onResourcesUpdated = { } ) } } @@ -979,11 +983,6 @@ fun DetailScreenContent_TODO_editInitially() { isChildLive = MutableLiveData(false), player = null, allWriteableCollectionsLive = MutableLiveData(listOf(ICalCollection.createLocalCollection(LocalContext.current))), - allCategoriesLive = MutableLiveData(emptyList()), - allResourcesLive = MutableLiveData(emptyList()), - storedCategories = emptyList(), - storedResources = emptyList(), - extendedStatuses = emptyList(), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, @@ -1008,7 +1007,9 @@ fun DetailScreenContent_TODO_editInitially() { onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, - onUpdateSortOrder = { } + onUpdateSortOrder = { }, + onCategoriesUpdated = { }, + onResourcesUpdated = { } ) } } @@ -1046,11 +1047,6 @@ fun DetailScreenContent_TODO_editInitially_isChild() { isChildLive = MutableLiveData(true), player = null, allWriteableCollectionsLive = MutableLiveData(listOf(ICalCollection.createLocalCollection(LocalContext.current))), - allCategoriesLive = MutableLiveData(emptyList()), - allResourcesLive = MutableLiveData(emptyList()), - storedCategories = emptyList(), - storedResources = emptyList(), - extendedStatuses = emptyList(), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, @@ -1075,7 +1071,9 @@ fun DetailScreenContent_TODO_editInitially_isChild() { onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, - onUpdateSortOrder = { } + onUpdateSortOrder = { }, + onCategoriesUpdated = { }, + onResourcesUpdated = { } ) } } @@ -1107,11 +1105,6 @@ fun DetailScreenContent_failedLoading() { isChildLive = MutableLiveData(true), player = null, allWriteableCollectionsLive = MutableLiveData(listOf(ICalCollection.createLocalCollection(LocalContext.current))), - allCategoriesLive = MutableLiveData(emptyList()), - allResourcesLive = MutableLiveData(emptyList()), - storedCategories = emptyList(), - storedResources = emptyList(), - extendedStatuses = emptyList(), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, @@ -1136,7 +1129,9 @@ fun DetailScreenContent_failedLoading() { onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, - onUpdateSortOrder = { } + onUpdateSortOrder = { }, + onCategoriesUpdated = { }, + onResourcesUpdated = { } ) } } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt index d3e93dd23..1ed3cbc50 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt @@ -91,11 +91,6 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) val mutableAttachments = mutableStateListOf() val mutableAlarms = mutableStateListOf() - var allCategories = databaseDao.getAllCategoriesAsText() - var allResources = databaseDao.getAllResourcesAsText() - val storedCategories = databaseDao.getStoredCategories() - val storedResources = databaseDao.getStoredResources() - val storedStatuses = databaseDao.getStoredStatuses() var allWriteableCollections = databaseDao.getAllWriteableCollections() private var selectFromAllListQuery: MutableLiveData = @@ -239,7 +234,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) settingKeepStatusProgressCompletedInSync = settingsStateHolder.settingKeepStatusProgressCompletedInSync.value, settingLinkProgressToSubtasks = settingsStateHolder.settingLinkProgressToSubtasks.value ) - onChangeDone() + onChangeDone(updateNotifications = true, updateGeofences = false) withContext(Dispatchers.Main) { changeState.value = DetailChangeState.CHANGESAVED } } } @@ -266,7 +261,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) fun updateSortOrder(list: List) { viewModelScope.launch(Dispatchers.IO) { databaseDao.updateSortOrder(list.map { it.id }) - onChangeDone() + onChangeDone(updateNotifications = false, updateGeofences = false) } } @@ -286,7 +281,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) changeState.value = DetailChangeState.CHANGESAVED } } - onChangeDone() + onChangeDone(updateNotifications = false, updateGeofences = false) } } @@ -301,7 +296,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) parentId = mainICalObjectId!!, childrenIds = newSubEntries.map { it.id } ) - onChangeDone() + onChangeDone(updateNotifications = false, updateGeofences = false) } } @@ -315,7 +310,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) parentIds = newParents.map { it.id }, childId = mainICalObjectId!! ) - onChangeDone() + onChangeDone(updateNotifications = false, updateGeofences = false) } } @@ -513,6 +508,30 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) } } + fun updateCategories(categories: List) { + mutableCategories.clear() + mutableCategories.addAll(categories) + viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { changeState.value = DetailChangeState.LOADING } + val uid = mutableICalObject?.uid!! + databaseDao.updateCategories(mainICalObjectId!!, uid, categories) + onChangeDone(updateNotifications = false, updateGeofences = false) + withContext(Dispatchers.Main) { changeState.value = DetailChangeState.CHANGESAVED } + } + } + + fun updateResources(resources: List) { + mutableResources.clear() + mutableResources.addAll(resources) + viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { changeState.value = DetailChangeState.LOADING } + val uid = mutableICalObject?.uid!! + databaseDao.updateResources(mainICalObjectId!!, uid, resources) + onChangeDone(updateNotifications = false, updateGeofences = false) + withContext(Dispatchers.Main) { changeState.value = DetailChangeState.CHANGESAVED } + } + } + fun createCopy(newModule: Module) { viewModelScope.launch(Dispatchers.IO) { @@ -690,11 +709,13 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) * sets geofences * updates the widget */ - private suspend fun onChangeDone() { + private suspend fun onChangeDone(updateNotifications: Boolean = true, updateGeofences: Boolean = true) { SyncUtil.notifyContentObservers(getApplication()) - NotificationPublisher.scheduleNextNotifications(getApplication()) - GeofenceClient(_application).setGeofences() ListWidget().updateAll(getApplication()) + if(updateNotifications) + NotificationPublisher.scheduleNextNotifications(getApplication()) + if(updateGeofences) + GeofenceClient(_application).setGeofences() } enum class DetailChangeState { LOADING, UNCHANGED, CHANGEUNSAVED, SAVINGREQUESTED, CHANGESAVING, CHANGESAVED, DELETING, DELETED, DELETED_BACK_TO_LIST, SQLERROR } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCategories.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCategories.kt index ba4942110..f935e9ba8 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCategories.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCategories.kt @@ -8,32 +8,20 @@ package at.techbee.jtx.ui.detail -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Label -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.NewLabel +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.ElevatedAssistChip import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.InputChip -import androidx.compose.material3.InputChipDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -48,18 +36,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import at.techbee.jtx.R +import at.techbee.jtx.database.ICalDatabase import at.techbee.jtx.database.locals.StoredCategory import at.techbee.jtx.database.locals.StoredListSettingData import at.techbee.jtx.database.properties.Category +import at.techbee.jtx.ui.reusable.dialogs.EditCategoriesDialog import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon import at.techbee.jtx.ui.theme.getContrastSurfaceColorFor @@ -68,35 +54,30 @@ import at.techbee.jtx.ui.theme.getContrastSurfaceColorFor @Composable fun DetailsCardCategories( categories: SnapshotStateList, - isEditMode: Boolean, - allCategoriesLive: LiveData>, + isReadOnly: Boolean, storedCategories: List, - onCategoriesUpdated: () -> Unit, + onCategoriesUpdated: (List) -> Unit, onGoToFilteredList: (StoredListSettingData) -> Unit, modifier: Modifier = Modifier ) { - val headline = stringResource(id = R.string.categories) - var newCategory by rememberSaveable { mutableStateOf("") } - val allCategories by allCategoriesLive.observeAsState(emptyList()) - - val mergedCategories = mutableListOf() - mergedCategories.addAll(storedCategories) - allCategories.forEach { cat -> if(mergedCategories.none { it.category == cat }) mergedCategories.add(StoredCategory(cat, null)) } - - fun addCategory() { - if (newCategory.isNotEmpty() && categories.none { existing -> existing.text == newCategory }) { - val careSensitiveCategory = - allCategories.firstOrNull { it == newCategory } - ?: allCategories.firstOrNull { it.lowercase() == newCategory.lowercase() } - ?: newCategory - categories.add(Category(text = careSensitiveCategory)) - onCategoriesUpdated() - } - newCategory = "" + var showEditCategoriesDialog by rememberSaveable { mutableStateOf(false) } + val context = LocalContext.current + + if(showEditCategoriesDialog) { + EditCategoriesDialog( + initialCategories = categories, + allCategories = ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getAllCategoriesAsText() + .observeAsState(emptyList()).value, + storedCategories = storedCategories, + onCategoriesUpdated = onCategoriesUpdated, + onDismiss = { showEditCategoriesDialog = false } + ) } - ElevatedCard(modifier = modifier) { Column( modifier = Modifier @@ -104,118 +85,35 @@ fun DetailsCardCategories( .padding(8.dp), ) { - HeadlineWithIcon(icon = Icons.AutoMirrored.Outlined.Label, iconDesc = headline, text = headline) - - AnimatedVisibility(categories.isNotEmpty()) { - FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - categories.forEach { category -> - if(!isEditMode) { - ElevatedAssistChip( - onClick = { onGoToFilteredList(StoredListSettingData(searchCategories = listOf(category.text))) }, - label = { Text(category.text) }, - colors = StoredCategory.getColorForCategory(category.text, storedCategories)?.let { AssistChipDefaults.elevatedAssistChipColors( - containerColor = it, - labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(it) - ) }?: AssistChipDefaults.elevatedAssistChipColors(), - ) - } else { - InputChip( - onClick = { }, - label = { Text(category.text) }, - trailingIcon = { - IconButton( - onClick = { - categories.remove(category) - onCategoriesUpdated() - }, - content = { - Icon( - Icons.Outlined.Close, - stringResource(id = R.string.delete) - ) - }, - modifier = Modifier.size(24.dp) - ) - }, - colors = StoredCategory.getColorForCategory(category.text, storedCategories)?.let { InputChipDefaults.inputChipColors( - containerColor = it, - labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(it) - ) }?: InputChipDefaults.inputChipColors(), - selected = false - ) - } - } - } - } - - val categoriesToSelectFiltered = mergedCategories.filter { all -> - all.category.lowercase().contains(newCategory.lowercase()) - && categories.none { existing -> existing.text.lowercase() == all.category.lowercase() } - } - AnimatedVisibility(categoriesToSelectFiltered.isNotEmpty() && isEditMode) { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - - items(categoriesToSelectFiltered) { category -> - InputChip( - onClick = { - categories.add(Category(text = category.category)) - onCategoriesUpdated() - newCategory = "" - }, - label = { Text(category.category) }, - leadingIcon = { - Icon( - Icons.Outlined.NewLabel, - stringResource(id = R.string.add) - ) - }, - selected = false, - colors = category.color?.let { InputChipDefaults.inputChipColors( - containerColor = Color(it), - labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(Color(it)) - ) }?: InputChipDefaults.inputChipColors(), - modifier = Modifier.alpha(0.4f) - ) - } - } - } - - Crossfade(isEditMode, label = "categoryEditMode") { - if (it) { - - OutlinedTextField( - value = newCategory, - leadingIcon = { Icon(Icons.AutoMirrored.Outlined.Label, headline) }, - trailingIcon = { - if (newCategory.isNotEmpty()) { - IconButton(onClick = { - addCategory() - }) { - Icon( - Icons.Outlined.NewLabel, - stringResource(id = R.string.add) - ) - } - } - }, - singleLine = true, - label = { Text(headline) }, - onValueChange = { newCategoryName -> newCategory = newCategoryName }, - modifier = Modifier.fillMaxWidth(), - isError = newCategory.isNotEmpty(), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions(onDone = { - addCategory() - }) + HeadlineWithIcon( + icon = Icons.AutoMirrored.Outlined.Label, + iconDesc = null, + text = stringResource(id = R.string.categories) + ) + + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + categories.forEach { category -> + ElevatedAssistChip( + onClick = { onGoToFilteredList(StoredListSettingData(searchCategories = listOf(category.text))) }, + label = { Text(category.text) }, + colors = StoredCategory.getColorForCategory(category.text, storedCategories)?.let { AssistChipDefaults.elevatedAssistChipColors( + containerColor = it, + labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(it) + ) }?: AssistChipDefaults.elevatedAssistChipColors(), ) } + + ElevatedAssistChip( + onClick = { + if(!isReadOnly) + showEditCategoriesDialog = true + }, + label = { Icon( + Icons.Outlined.Edit, + stringResource(id = R.string.edit) + ) }, + modifier = Modifier.alpha(0.4f) + ) } } } @@ -227,27 +125,10 @@ fun DetailsCardCategories_Preview() { MaterialTheme { DetailsCardCategories( categories = remember { mutableStateListOf(Category(text = "asdf")) }, - isEditMode = false, - allCategoriesLive = MutableLiveData(listOf("category1", "category2", "Whatever")), + isReadOnly = false, storedCategories = listOf(StoredCategory("category1", Color.Green.toArgb())), onCategoriesUpdated = { }, onGoToFilteredList = { } ) } } - - -@Preview(showBackground = true) -@Composable -fun DetailsCardCategories_Preview_edit() { - MaterialTheme { - DetailsCardCategories( - categories = remember { mutableStateListOf(Category(text = "asdf")) }, - isEditMode = true, - allCategoriesLive = MutableLiveData(listOf("category1", "category2", "Whatever")), - storedCategories = listOf(StoredCategory("category1", Color.Green.toArgb())), - onCategoriesUpdated = { }, - onGoToFilteredList = { } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardResources.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardResources.kt index eec41ab11..b113e68a1 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardResources.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardResources.kt @@ -8,33 +8,20 @@ package at.techbee.jtx.ui.detail -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.NewLabel +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.WorkOutline import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.ElevatedAssistChip import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.InputChip -import androidx.compose.material3.InputChipDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -49,53 +36,46 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import at.techbee.jtx.R +import at.techbee.jtx.database.ICalDatabase import at.techbee.jtx.database.locals.StoredListSettingData import at.techbee.jtx.database.locals.StoredResource import at.techbee.jtx.database.properties.Resource +import at.techbee.jtx.ui.reusable.dialogs.EditResourcesDialog import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon import at.techbee.jtx.ui.theme.getContrastSurfaceColorFor -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable fun DetailsCardResources( resources: SnapshotStateList, - isEditMode: Boolean, - allResourcesLive: LiveData>, + isReadOnly: Boolean, storedResources: List, - onResourcesUpdated: () -> Unit, + onResourcesUpdated: (List) -> Unit, onGoToFilteredList: (StoredListSettingData) -> Unit, modifier: Modifier = Modifier ) { - val allResources by allResourcesLive.observeAsState(emptyList()) - - val headline = stringResource(id = R.string.resources) - var newResource by rememberSaveable { mutableStateOf("") } - - val mergedResources = mutableListOf() - mergedResources.addAll(storedResources) - allResources.forEach { resource -> if(mergedResources.none { it.resource == resource }) mergedResources.add(StoredResource(resource, null)) } - - fun addResource() { - if (newResource.isNotEmpty() && resources.none { existing -> existing.text == newResource }) { - val caseSensitiveResource = - allResources.firstOrNull { it == newResource } - ?: allResources.firstOrNull { it.lowercase() == newResource.lowercase() } - ?: newResource - resources.add(Resource(text = caseSensitiveResource)) - onResourcesUpdated() - } - newResource = "" + var showEditResourcesDialog by rememberSaveable { mutableStateOf(false) } + val context = LocalContext.current + + if(showEditResourcesDialog) { + EditResourcesDialog( + initialResources = resources, + allResources = ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getAllResourcesAsText() + .observeAsState(emptyList()).value, + storedResources = storedResources, + onResourcesUpdated = onResourcesUpdated, + onDismiss = { showEditResourcesDialog = false } + ) } ElevatedCard(modifier = modifier) { @@ -105,112 +85,35 @@ fun DetailsCardResources( .padding(8.dp), ) { - HeadlineWithIcon(icon = Icons.Outlined.WorkOutline, iconDesc = headline, text = headline) - - AnimatedVisibility(resources.isNotEmpty()) { - FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - resources.forEach { resource -> - if(!isEditMode) { - ElevatedAssistChip( - onClick = { onGoToFilteredList(StoredListSettingData(searchResources = listOf(resource.text?:""))) }, - label = { Text(resource.text ?: "") }, - colors = StoredResource.getColorForResource(resource.text?:"", storedResources)?.let { AssistChipDefaults.elevatedAssistChipColors( - containerColor = it, - labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(it) - ) }?: AssistChipDefaults.elevatedAssistChipColors(), - ) - } else { - InputChip( - onClick = { }, - label = { Text(resource.text ?: "") }, - trailingIcon = { - IconButton( - onClick = { - resources.remove(resource) - onResourcesUpdated() - }, - content = { Icon(Icons.Outlined.Close, stringResource(id = R.string.delete)) }, - modifier = Modifier.size(24.dp) - ) - }, - colors = StoredResource.getColorForResource(resource.text?:"", storedResources)?.let { InputChipDefaults.inputChipColors( - containerColor = it, - labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(it) - ) }?: InputChipDefaults.inputChipColors(), - selected = false - ) - } - } - } - } - - val resourcesToSelectFiltered = mergedResources.filter { all -> - all.resource.lowercase().contains(newResource.lowercase()) - && resources.none { existing -> existing.text?.lowercase() == all.resource.lowercase() } - } - - AnimatedVisibility(resourcesToSelectFiltered.isNotEmpty() && isEditMode) { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - items(resourcesToSelectFiltered) { resource -> - InputChip( - onClick = { - resources.add(Resource(text = resource.resource)) - onResourcesUpdated() - newResource = "" - }, - label = { Text(resource.resource) }, - leadingIcon = { - Icon( - Icons.Outlined.NewLabel, - stringResource(id = R.string.add) - ) - }, - selected = false, - colors = resource.color?.let { InputChipDefaults.inputChipColors( - containerColor = Color(it), - labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(Color(it)) - ) }?: InputChipDefaults.inputChipColors(), - modifier = Modifier.alpha(0.4f) - ) - } - } - } - - - Crossfade(isEditMode, label = "newResourceIsEditMode") { - if (it) { - - OutlinedTextField( - value = newResource, - leadingIcon = { Icon(Icons.Outlined.WorkOutline, headline) }, - trailingIcon = { - if (newResource.isNotEmpty()) { - IconButton(onClick = { - addResource() - }) { - Icon( - Icons.Outlined.NewLabel, - stringResource(id = R.string.add) - ) - } - } - }, - singleLine = true, - label = { Text(headline) }, - onValueChange = { newResourceName -> - newResource = newResourceName - }, - isError = newResource.isNotEmpty(), - modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - addResource() - }) + HeadlineWithIcon( + icon = Icons.Outlined.WorkOutline, + iconDesc = null, + text = stringResource(id = R.string.resources) + ) + + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + resources.forEach { resource -> + ElevatedAssistChip( + onClick = { onGoToFilteredList(StoredListSettingData(searchResources = listOf(resource.text?:""))) }, + label = { Text(resource.text ?: "") }, + colors = StoredResource.getColorForResource(resource.text?:"", storedResources)?.let { AssistChipDefaults.elevatedAssistChipColors( + containerColor = it, + labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(it) + ) }?: AssistChipDefaults.elevatedAssistChipColors(), ) } + + ElevatedAssistChip( + onClick = { + if(!isReadOnly) + showEditResourcesDialog = true + }, + label = { Icon( + Icons.Outlined.Edit, + stringResource(id = R.string.edit) + ) }, + modifier = Modifier.alpha(0.4f) + ) } } } @@ -222,8 +125,7 @@ fun DetailsCardResources_Preview() { MaterialTheme { DetailsCardResources( resources = remember { mutableStateListOf(Resource(text = "asdf")) }, - isEditMode = false, - allResourcesLive = MutableLiveData(listOf("projector", "overhead-thingy", "Whatever")), + isReadOnly = false, storedResources = listOf(StoredResource("projector", Color.Green.toArgb())), onResourcesUpdated = { }, onGoToFilteredList = { } @@ -231,18 +133,3 @@ fun DetailsCardResources_Preview() { } } - -@Preview(showBackground = true) -@Composable -fun DetailsCardResources_Preview_edit() { - MaterialTheme { - DetailsCardResources( - resources = remember { mutableStateListOf(Resource(text = "asdf")) }, - isEditMode = true, - allResourcesLive = MutableLiveData(listOf("projector", "overhead-thingy", "Whatever")), - storedResources = listOf(StoredResource("projector", Color.Green.toArgb())), - onResourcesUpdated = { }, - onGoToFilteredList = { } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt index 93a70a4b6..db70a3d97 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt @@ -120,9 +120,6 @@ fun DetailsScreen( val collection = detailViewModel.collection.observeAsState() val seriesElement = detailViewModel.seriesElement.observeAsState(null) - val storedCategories by detailViewModel.storedCategories.observeAsState(emptyList()) - val storedResources by detailViewModel.storedResources.observeAsState(emptyList()) - val storedStatuses by detailViewModel.storedStatuses.observeAsState(emptyList()) val isProPurchased = BillingManager.getInstance().isProPurchased.observeAsState(true) val isProActionAvailable by remember(isProPurchased, collection) { derivedStateOf { isProPurchased.value || collection.value?.accountType == ICalCollection.LOCAL_ACCOUNT_TYPE } } @@ -644,11 +641,6 @@ fun DetailsScreen( subnotesLive = detailViewModel.relatedSubnotes, isChildLive = detailViewModel.isChild, allWriteableCollectionsLive = detailViewModel.allWriteableCollections, - allCategoriesLive = detailViewModel.allCategories, - allResourcesLive = detailViewModel.allResources, - storedCategories = storedCategories, - storedResources = storedResources, - extendedStatuses = storedStatuses, detailSettings = detailViewModel.detailSettings, icalObjectIdList = icalObjectIdList, seriesInstancesLive = detailViewModel.seriesInstances, @@ -678,6 +670,8 @@ fun DetailsScreen( onSubEntryDeleted = { icalObjectId -> detailViewModel.deleteById(icalObjectId) }, onSubEntryUpdated = { icalObjectId, newText -> detailViewModel.updateSummary(icalObjectId, newText) }, onUnlinkSubEntry = { icalObjectId, parentUID -> detailViewModel.unlinkFromParent(icalObjectId, parentUID) }, + onCategoriesUpdated = { categories -> detailViewModel.updateCategories(categories) }, + onResourcesUpdated = { resources -> detailViewModel.updateResources(resources) }, player = detailViewModel.mediaPlayer, goToDetail = { itemId, editMode, list, popBackStack -> if(popBackStack) diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditCategoriesDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditCategoriesDialog.kt new file mode 100644 index 000000000..059659d7f --- /dev/null +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditCategoriesDialog.kt @@ -0,0 +1,248 @@ +/* + * Copyright (c) Techbee e.U. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.techbee.jtx.ui.reusable.dialogs + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Label +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.NewLabel +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChip +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.techbee.jtx.R +import at.techbee.jtx.database.locals.StoredCategory +import at.techbee.jtx.database.properties.Category +import at.techbee.jtx.ui.theme.getContrastSurfaceColorFor + + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun EditCategoriesDialog( + initialCategories: List, + allCategories: List, + storedCategories: List, + onCategoriesUpdated: (List) -> Unit, + onDismiss: () -> Unit +) { + + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + + val currentCategories = remember { mutableStateListOf().apply { addAll(initialCategories) }} + var newCategory by rememberSaveable { mutableStateOf("") } + + val mergedCategories = mutableListOf() + mergedCategories.addAll(storedCategories) + allCategories.forEach { cat -> if(mergedCategories.none { it.category == cat }) mergedCategories.add(StoredCategory(cat, null)) } + + fun addCategory() { + if (newCategory.isNotEmpty() && currentCategories.none { existing -> existing.text == newCategory }) { + val caseSensitiveCategory = + allCategories.firstOrNull { it == newCategory } + ?: allCategories.firstOrNull { it.lowercase() == newCategory.lowercase() } + ?: newCategory + currentCategories.add(Category(text = caseSensitiveCategory)) + } + newCategory = "" + } + + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.AutoMirrored.Outlined.Label, null) + Text(stringResource(id = R.string.categories)) + } + }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + AnimatedVisibility(currentCategories.isNotEmpty()) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + currentCategories.forEach { category -> + InputChip( + onClick = { }, + label = { Text(category.text) }, + trailingIcon = { + IconButton( + onClick = { + currentCategories.remove(category) + }, + content = { + Icon( + Icons.Outlined.Close, + stringResource(id = R.string.delete) + ) + }, + modifier = Modifier.size(24.dp) + ) + }, + colors = StoredCategory.getColorForCategory(category.text, storedCategories)?.let { InputChipDefaults.inputChipColors( + containerColor = it, + labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(it) + ) }?: InputChipDefaults.inputChipColors(), + selected = false + ) + + } + } + } + + val categoriesToSelectFiltered = mergedCategories.filter { all -> + all.category.lowercase().contains(newCategory.lowercase()) + && currentCategories.none { existing -> existing.text.lowercase() == all.category.lowercase() } + } + AnimatedVisibility(categoriesToSelectFiltered.isNotEmpty()) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + + categoriesToSelectFiltered.filterIndexed { index, _ -> index < 10 }.forEach { category -> + InputChip( + onClick = { + currentCategories.add(Category(text = category.category)) + newCategory = "" + }, + label = { Text(category.category) }, + leadingIcon = { + Icon( + Icons.Outlined.NewLabel, + stringResource(id = R.string.add) + ) + }, + selected = false, + colors = category.color?.let { InputChipDefaults.inputChipColors( + containerColor = Color(it), + labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(Color(it)) + ) }?: InputChipDefaults.inputChipColors(), + modifier = Modifier.alpha(0.4f) + ) + } + } + } + + OutlinedTextField( + value = newCategory, + trailingIcon = { + AnimatedVisibility(newCategory.isNotEmpty()) { + IconButton(onClick = { + addCategory() + }) { + Icon( + Icons.Outlined.NewLabel, + stringResource(id = R.string.add) + ) + } + } + }, + singleLine = true, + label = { Text(stringResource(id = R.string.category)) }, + onValueChange = { newCategoryName -> newCategory = newCategoryName }, + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), + isError = newCategory.isNotEmpty(), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions(onDone = { + addCategory() + }) + ) + } + }, + confirmButton = { + TextButton( + onClick = { + if(newCategory.isNotEmpty()) + addCategory() + onCategoriesUpdated(currentCategories) + onDismiss() + }, + ) { + Text(stringResource(id = R.string.save)) + } + + }, + dismissButton = { + TextButton( + onClick = { + onDismiss() + } + ) { + Text(stringResource(id = R.string.cancel)) + } + }, + ) +} + +@Preview(showBackground = true) +@Composable +fun EditCategoriesDialog_Preview() { + MaterialTheme { + EditCategoriesDialog( + initialCategories = listOf(Category(text = "asdf")), + allCategories = listOf("category1", "category2", "Whatever"), + storedCategories = listOf(StoredCategory("category1", Color.Green.toArgb())), + onCategoriesUpdated = { } + ) { + + } + } +} + diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditResourcesDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditResourcesDialog.kt new file mode 100644 index 000000000..3374243b2 --- /dev/null +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditResourcesDialog.kt @@ -0,0 +1,248 @@ +/* + * Copyright (c) Techbee e.U. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.techbee.jtx.ui.reusable.dialogs + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.WorkOutline +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChip +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.techbee.jtx.R +import at.techbee.jtx.database.locals.StoredResource +import at.techbee.jtx.database.properties.Resource +import at.techbee.jtx.ui.theme.getContrastSurfaceColorFor + + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun EditResourcesDialog( + initialResources: List, + allResources: List, + storedResources: List, + onResourcesUpdated: (List) -> Unit, + onDismiss: () -> Unit +) { + + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + + val currentResources = remember { mutableStateListOf().apply { addAll(initialResources) }} + var newResource by rememberSaveable { mutableStateOf("") } + + val mergedResources = mutableListOf() + mergedResources.addAll(storedResources) + allResources.forEach { resource -> if(mergedResources.none { it.resource == resource }) mergedResources.add(StoredResource(resource, null)) } + + fun addResource() { + if (newResource.isNotEmpty() && currentResources.none { existing -> existing.text == newResource }) { + val caseSensitiveResource = + allResources.firstOrNull { it == newResource } + ?: allResources.firstOrNull { it.lowercase() == newResource.lowercase() } + ?: newResource + currentResources.add(Resource(text = caseSensitiveResource)) + } + newResource = "" + } + + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Outlined.WorkOutline, null) + Text(stringResource(id = R.string.resources)) + } + }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + + AnimatedVisibility(currentResources.isNotEmpty()) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + currentResources.forEach { resource -> + InputChip( + onClick = { }, + label = { Text(resource.text?:"") }, + trailingIcon = { + IconButton( + onClick = { + currentResources.remove(resource) + }, + content = { + Icon( + Icons.Outlined.Close, + stringResource(id = R.string.delete) + ) + }, + modifier = Modifier.size(24.dp) + ) + }, + colors = StoredResource.getColorForResource(resource.text, storedResources)?.let { InputChipDefaults.inputChipColors( + containerColor = it, + labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(it) + ) }?: InputChipDefaults.inputChipColors(), + selected = false + ) + + } + } + } + + val resourcesToSelectFiltered = mergedResources.filter { all -> + all.resource.lowercase().contains(newResource.lowercase()) + && currentResources.none { existing -> existing.text?.lowercase() == all.resource.lowercase() } + } + AnimatedVisibility(resourcesToSelectFiltered.isNotEmpty()) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + + resourcesToSelectFiltered.filterIndexed { index, _ -> index < 10 }.forEach { resource -> + InputChip( + onClick = { + currentResources.add(Resource(text = resource.resource)) + newResource = "" + }, + label = { Text(resource.resource) }, + leadingIcon = { + Icon( + Icons.Outlined.WorkOutline, + stringResource(id = R.string.add) + ) + }, + selected = false, + colors = resource.color?.let { InputChipDefaults.inputChipColors( + containerColor = Color(it), + labelColor = MaterialTheme.colorScheme.getContrastSurfaceColorFor(Color(it)) + ) }?: InputChipDefaults.inputChipColors(), + modifier = Modifier.alpha(0.4f) + ) + } + } + } + + OutlinedTextField( + value = newResource, + trailingIcon = { + AnimatedVisibility(newResource.isNotEmpty()) { + IconButton(onClick = { + addResource() + }) { + Icon( + Icons.Outlined.WorkOutline, + stringResource(id = R.string.add) + ) + } + } + }, + singleLine = true, + label = { Text(stringResource(id = R.string.resource)) }, + onValueChange = { newResourceName -> newResource = newResourceName }, + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), + isError = newResource.isNotEmpty(), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions(onDone = { + addResource() + }) + ) + } + }, + confirmButton = { + TextButton( + onClick = { + if(newResource.isNotEmpty()) + addResource() + onResourcesUpdated(currentResources) + onDismiss() + }, + ) { + Text(stringResource(id = R.string.save)) + } + + }, + dismissButton = { + TextButton( + onClick = { + onDismiss() + } + ) { + Text(stringResource(id = R.string.cancel)) + } + }, + ) +} + +@Preview(showBackground = true) +@Composable +fun EditResourcesDialog_Preview() { + MaterialTheme { + EditResourcesDialog( + initialResources = listOf(Resource(text = "asdf")), + allResources = listOf("res1", "res2", "Whatever"), + storedResources = listOf(StoredResource("res2", Color.Green.toArgb())), + onResourcesUpdated = { } + ) { + + } + } +} + From 1ca9be7b6f69e7f8c5b00bf9db58d54ee323fe4b Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Wed, 5 Jun 2024 22:42:10 +0200 Subject: [PATCH 14/27] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0967e2286..e52f2d0f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /captures .externalNativeBuild /app/local.properties +.kotlin \ No newline at end of file From 9aed94bc22d6b3d6e42447f0cb212977eb0ee74e Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 9 Jun 2024 08:11:26 +0200 Subject: [PATCH 15/27] improved menu --- .../at/techbee/jtx/ui/detail/DetailsScreen.kt | 151 +++++++++--------- 1 file changed, 72 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt index da0d22740..e413c677e 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt @@ -377,103 +377,101 @@ fun DetailsScreen( HorizontalDivider() - if (!isEditMode.value) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.menu_view_share)) }, + onClick = { shareOptionsExpanded.value = !shareOptionsExpanded.value }, + trailingIcon = { Icon(if(shareOptionsExpanded.value) Icons.Outlined.KeyboardArrowDown else Icons.Outlined.ChevronRight, null) } + ) + + AnimatedVisibility(shareOptionsExpanded.value) { DropdownMenuItem( - text = { Text(text = stringResource(R.string.menu_view_share)) }, - onClick = { shareOptionsExpanded.value = !shareOptionsExpanded.value }, - trailingIcon = { Icon(if(shareOptionsExpanded.value) Icons.Outlined.KeyboardArrowDown else Icons.Outlined.ChevronRight, null) } + text = { Text(text = stringResource(id = R.string.menu_view_share_mail)) }, + onClick = { + detailViewModel.shareAsText(context) + menuExpanded.value = false + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Mail, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } ) + } + AnimatedVisibility(shareOptionsExpanded.value) { - AnimatedVisibility(shareOptionsExpanded.value) { - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_share_mail)) }, + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.menu_view_share_ics)) }, onClick = { - detailViewModel.shareAsText(context) + detailViewModel.shareAsICS(context) menuExpanded.value = false }, leadingIcon = { Icon( - imageVector = Icons.Outlined.Mail, + imageVector = Icons.Outlined.Description, contentDescription = null, tint = MaterialTheme.colorScheme.onSurface ) } ) - } - AnimatedVisibility(shareOptionsExpanded.value) { - - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_share_ics)) }, - onClick = { - detailViewModel.shareAsICS(context) - menuExpanded.value = false - }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.Description, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - } - ) - } - AnimatedVisibility(shareOptionsExpanded.value) { + } + AnimatedVisibility(shareOptionsExpanded.value) { - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.menu_view_copy_to_clipboard)) }, - onClick = { - val currentICalObjectId = detailViewModel.mutableICalObject?.id ?: return@DropdownMenuItem - - scope.launch(Dispatchers.IO) { - ICalDatabase - .getInstance(context) - .iCalDatabaseDao() - .getSync(currentICalObjectId) - ?.let { - val text = it.getShareText(context) - val clipboardManager = - context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - clipboardManager.setPrimaryClip( - ClipData.newPlainText( - "", - text - ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.menu_view_copy_to_clipboard)) }, + onClick = { + val currentICalObjectId = detailViewModel.mutableICalObject?.id ?: return@DropdownMenuItem + + scope.launch(Dispatchers.IO) { + ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getSync(currentICalObjectId) + ?.let { + val text = it.getShareText(context) + val clipboardManager = + context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + clipboardManager.setPrimaryClip( + ClipData.newPlainText( + "", + text ) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) // Only show a toast for Android 12 and lower. - detailViewModel.toastMessage.value = - context.getString(R.string.menu_view_copy_to_clipboard_copied) - } - } - menuExpanded.value = false - }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.ContentPaste, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) + ) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) // Only show a toast for Android 12 and lower. + detailViewModel.toastMessage.value = + context.getString(R.string.menu_view_copy_to_clipboard_copied) + } } - ) - } - } - - - if (isEditMode.value) { - CheckboxWithText( - text = stringResource(id = R.string.menu_view_autosave), - onCheckedChange = { - detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_AUTOSAVE] = it - detailViewModel.detailSettings.save() + menuExpanded.value = false }, - isSelected = detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_AUTOSAVE] ?: true, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.ContentPaste, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } ) } HorizontalDivider() - if(!isEditMode.value - && collection.value?.readonly == false + + CheckboxWithText( + text = stringResource(id = R.string.menu_view_autosave), + onCheckedChange = { + detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_AUTOSAVE] = it + detailViewModel.detailSettings.save() + }, + isSelected = detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_AUTOSAVE] ?: true, + ) + + + HorizontalDivider() + + if(collection.value?.readonly == false && isProActionAvailable) { DropdownMenuItem( @@ -536,10 +534,6 @@ fun DetailsScreen( } } - AnimatedVisibility (copyConvertOptionsExpanded.value) { - HorizontalDivider() - } - AnimatedVisibility (copyConvertOptionsExpanded.value && collection.value?.supportsVJOURNAL == true) { DropdownMenuItem( leadingIcon = { Icon(Icons.Outlined.ContentCopy, null) }, @@ -573,7 +567,6 @@ fun DetailsScreen( } HorizontalDivider() - } if(collection.value?.readonly == false && isProActionAvailable) { From 9206eedf3a7edca20401db3ec0061eb08f34aa4d Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 9 Jun 2024 08:24:27 +0200 Subject: [PATCH 16/27] Removed more editMode conditions --- .../jtx/ui/detail/DetailScreenContent.kt | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index bd1acd629..613648ba8 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -425,13 +425,14 @@ fun DetailScreenContent( DetailsScreenSection.STATUSCLASSIFICATIONPRIORITY -> { if( - (!isEditMode.value && (!iCalObject.status.isNullOrEmpty() || !iCalObject.xstatus.isNullOrEmpty() || !iCalObject.classification.isNullOrEmpty() || iCalObject.priority in 1..9)) - || (isEditMode.value - && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false - || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false - || (iCalObject.getModuleFromString() == Module.TODO && detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false) - || showAllOptions) - ) + !iCalObject.status.isNullOrEmpty() + || !iCalObject.xstatus.isNullOrEmpty() + || !iCalObject.classification.isNullOrEmpty() + || iCalObject.priority in 1..9 + || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false + || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false + || (iCalObject.getModuleFromString() == Module.TODO && detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false) + || showAllOptions ) { DetailsCardStatusClassificationPriority( @@ -493,7 +494,7 @@ fun DetailScreenContent( } } DetailsScreenSection.PARENTS -> { - if(parents.value.isNotEmpty() || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_PARENTS] != false || showAllOptions))) { + if(parents.value.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_PARENTS] != false || showAllOptions) { DetailsCardParents( parents = parents.value, isEditMode = isEditMode, @@ -531,7 +532,7 @@ fun DetailScreenContent( } } DetailsScreenSection.SUBTASKS -> { - if(subtasks.value.isNotEmpty() || (isEditMode.value && collection?.supportsVTODO == true && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBTASKS] != false || showAllOptions))) { + if(subtasks.value.isNotEmpty() || (collection?.supportsVTODO == true && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBTASKS] != false || showAllOptions))) { DetailsCardSubtasks( subtasks = subtasks.value, isEditMode = isEditMode, @@ -571,7 +572,7 @@ fun DetailScreenContent( } } DetailsScreenSection.SUBNOTES -> { - if(subnotes.value.isNotEmpty() || (isEditMode.value && collection?.supportsVJOURNAL == true && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBNOTES] == true || showAllOptions))) { + if(subnotes.value.isNotEmpty() || (collection?.supportsVJOURNAL == true && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBNOTES] == true || showAllOptions))) { DetailsCardSubnotes( subnotes = subnotes.value, isEditMode = isEditMode, @@ -636,7 +637,7 @@ fun DetailScreenContent( } } DetailsScreenSection.ATTENDEES -> { - if(attendees.isNotEmpty() || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] == true || showAllOptions))) { + if(attendees.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] == true || showAllOptions) { DetailsCardAttendees( attendees = attendees, isEditMode = isEditMode.value, @@ -648,7 +649,7 @@ fun DetailScreenContent( } } DetailsScreenSection.CONTACT -> { - if(iCalObject.contact?.isNotBlank() == true || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] == true || showAllOptions))) { + if(iCalObject.contact?.isNotBlank() == true || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] == true || showAllOptions) { DetailsCardContact( initialContact = iCalObject.contact ?: "", isReadOnly = collection?.readonly?:true, @@ -661,7 +662,7 @@ fun DetailScreenContent( } } DetailsScreenSection.URL -> { - if(iCalObject.url?.isNotEmpty() == true || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] == true || showAllOptions))) { + if(iCalObject.url?.isNotEmpty() == true || detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] == true || showAllOptions) { DetailsCardUrl( initialUrl = iCalObject.url ?: "", isReadOnly = collection?.readonly?:true, @@ -674,7 +675,7 @@ fun DetailScreenContent( } } DetailsScreenSection.LOCATION -> { - if((iCalObject.location?.isNotEmpty() == true || (iCalObject.geoLat != null && iCalObject.geoLong != null)) || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] == true || showAllOptions))) { + if((iCalObject.location?.isNotEmpty() == true || (iCalObject.geoLat != null && iCalObject.geoLong != null)) || detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] == true || showAllOptions) { DetailsCardLocation( initialLocation = iCalObject.location, initialGeoLat = iCalObject.geoLat, @@ -698,7 +699,7 @@ fun DetailScreenContent( } } DetailsScreenSection.COMMENTS -> { - if(comments.isNotEmpty() || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] == true || showAllOptions))) { + if(comments.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] == true || showAllOptions) { DetailsCardComments( comments = comments, isReadOnly = collection?.readonly?:true, @@ -710,7 +711,7 @@ fun DetailScreenContent( } } DetailsScreenSection.ATTACHMENTS -> { - if(attachments.isNotEmpty() || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] == true || showAllOptions))) { + if(attachments.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] == true || showAllOptions) { DetailsCardAttachments( attachments = attachments, isReadOnly = collection?.readonly?:true, @@ -724,7 +725,7 @@ fun DetailScreenContent( } } DetailsScreenSection.ALARMS -> { - if(alarms.isNotEmpty() || (isEditMode.value && iCalObject.module == Module.TODO.name && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] == true || showAllOptions))) { + if(alarms.isNotEmpty() || (iCalObject.module == Module.TODO.name && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] == true || showAllOptions))) { DetailsCardAlarms( alarms = alarms, icalObject = iCalObject, @@ -739,8 +740,9 @@ fun DetailScreenContent( DetailsScreenSection.RECURRENCE -> { if( iCalObject.rrule != null - || iCalObject.recurid != null - || (isEditMode.value && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_RECURRENCE] == true || (showAllOptions && iCalObject.module != Module.NOTE.name))) + || iCalObject.recurid != null + || detailSettings.detailSetting[DetailSettingsOption.ENABLE_RECURRENCE] == true + || (showAllOptions && iCalObject.module != Module.NOTE.name) ) { // only Todos have recur! DetailsCardRecur( icalObject = iCalObject, @@ -812,53 +814,51 @@ fun DetailScreenContent( } } - if(!isEditMode.value) { - item { - val curIndex = icalObjectIdList.indexOf(observedICalObject.value?.id ?: 0) - if (icalObjectIdList.size > 1 && curIndex >= 0) { - Row( - modifier = Modifier - .padding(vertical = 16.dp, horizontal = 8.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { + item { + val curIndex = icalObjectIdList.indexOf(observedICalObject.value?.id ?: 0) + if (icalObjectIdList.size > 1 && curIndex >= 0) { + Row( + modifier = Modifier + .padding(vertical = 16.dp, horizontal = 8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { - if (curIndex > 0) { - IconButton(onClick = { - goToDetail( - icalObjectIdList[curIndex - 1], - false, - icalObjectIdList, - true - ) - }) { - Icon( - Icons.AutoMirrored.Outlined.NavigateBefore, - stringResource(id = R.string.previous) - ) - } - } else { - Spacer(modifier = Modifier.size(48.dp)) + if (curIndex > 0) { + IconButton(onClick = { + goToDetail( + icalObjectIdList[curIndex - 1], + false, + icalObjectIdList, + true + ) + }) { + Icon( + Icons.AutoMirrored.Outlined.NavigateBefore, + stringResource(id = R.string.previous) + ) } - Text(text = "${icalObjectIdList.indexOf(observedICalObject.value?.id ?: 0) + 1}/${icalObjectIdList.size}") - if (curIndex != icalObjectIdList.lastIndex) { - IconButton(onClick = { - goToDetail( - icalObjectIdList[curIndex + 1], - false, - icalObjectIdList, - true - ) - }) { - Icon( - Icons.AutoMirrored.Outlined.NavigateNext, - stringResource(id = R.string.next) - ) - } - } else { - Spacer(modifier = Modifier.size(48.dp)) + } else { + Spacer(modifier = Modifier.size(48.dp)) + } + Text(text = "${icalObjectIdList.indexOf(observedICalObject.value?.id ?: 0) + 1}/${icalObjectIdList.size}") + if (curIndex != icalObjectIdList.lastIndex) { + IconButton(onClick = { + goToDetail( + icalObjectIdList[curIndex + 1], + false, + icalObjectIdList, + true + ) + }) { + Icon( + Icons.AutoMirrored.Outlined.NavigateNext, + stringResource(id = R.string.next) + ) } + } else { + Spacer(modifier = Modifier.size(48.dp)) } } } From ca7faddfad95286494b6abbf00e5e59b4865a106 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:58:05 +0200 Subject: [PATCH 17/27] edit summary/description/recur when added --- .../jtx/ui/detail/DetailScreenContent.kt | 410 ++++++++++++++---- .../jtx/ui/detail/DetailsCardDescription.kt | 17 +- .../techbee/jtx/ui/detail/DetailsCardRecur.kt | 14 + .../jtx/ui/detail/DetailsCardSummary.kt | 8 + 4 files changed, 357 insertions(+), 92 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index 613648ba8..6602a1b1d 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -9,8 +9,11 @@ package at.techbee.jtx.ui.detail import android.media.MediaPlayer +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -21,8 +24,20 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.InsertComment import androidx.compose.material.icons.automirrored.outlined.NavigateBefore import androidx.compose.material.icons.automirrored.outlined.NavigateNext +import androidx.compose.material.icons.outlined.AlarmAdd +import androidx.compose.material.icons.outlined.AttachFile +import androidx.compose.material.icons.outlined.ContactMail +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.EventRepeat +import androidx.compose.material.icons.outlined.Groups +import androidx.compose.material.icons.outlined.Link +import androidx.compose.material.icons.outlined.NewLabel +import androidx.compose.material.icons.outlined.Place +import androidx.compose.material.icons.outlined.ViewHeadline +import androidx.compose.material.icons.outlined.WorkOutline import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard @@ -85,6 +100,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +@OptIn(ExperimentalLayoutApi::class) @Composable fun DetailScreenContent( observedICalObject: State, @@ -131,13 +147,13 @@ fun DetailScreenContent( onResourcesUpdated: (List) -> Unit, goToDetail: (itemId: Long, editMode: Boolean, list: List, popBackStack: Boolean) -> Unit, goBack: () -> Unit, - goToFilteredList: (StoredListSettingData) -> Unit, + goToFilteredList: (StoredListSettingData) -> Unit, unlinkFromSeries: (instances: List, series: ICalObject?, deleteAfterUnlink: Boolean) -> Unit, onShowLinkExistingDialog: (modules: List, reltype: Reltype) -> Unit, onUpdateSortOrder: (List) -> Unit, - ) { +) { - if(iCalObject == null) + if (iCalObject == null) return val context = LocalContext.current @@ -148,7 +164,6 @@ fun DetailScreenContent( val isChild = isChildLive.observeAsState(false) val allWriteableCollections = allWriteableCollectionsLive.observeAsState(emptyList()) - var timeout by remember { mutableStateOf(false) } LaunchedEffect(timeout, observedICalObject.value) { if (observedICalObject.value == null && !timeout) { @@ -166,8 +181,14 @@ fun DetailScreenContent( .fillMaxSize() .padding(24.dp) ) { - Text(stringResource(id = R.string.sorry), style = MaterialTheme.typography.displayMedium) - Text(stringResource(id = R.string.details_entry_could_not_be_loaded), textAlign = TextAlign.Center) + Text( + stringResource(id = R.string.sorry), + style = MaterialTheme.typography.displayMedium + ) + Text( + stringResource(id = R.string.details_entry_could_not_be_loaded), + textAlign = TextAlign.Center + ) Button(onClick = { goBack() }) { Text(stringResource(id = R.string.back)) } @@ -223,9 +244,15 @@ fun DetailScreenContent( iCalObject.recurid = observedICalObject.value?.recurid iCalObject.uid = observedICalObject.value?.uid!! } - observedICalObject.value?.id?.let { iCalObject.id = it } // the icalObjectId might also have changed (when moving the entry to a new collection)! - observedICalObject.value?.uid?.let { iCalObject.uid = it } // the icalObjectId might also have changed (when moving the entry to a new collection)! - observedICalObject.value?.collectionId?.let { iCalObject.collectionId = it } // the collectionId might also have changed (when moving the entry to a new collection)! + observedICalObject.value?.id?.let { + iCalObject.id = it + } // the icalObjectId might also have changed (when moving the entry to a new collection)! + observedICalObject.value?.uid?.let { + iCalObject.uid = it + } // the icalObjectId might also have changed (when moving the entry to a new collection)! + observedICalObject.value?.collectionId?.let { + iCalObject.collectionId = it + } // the collectionId might also have changed (when moving the entry to a new collection)! var showAllOptions by rememberSaveable { mutableStateOf(false) } @@ -269,21 +296,22 @@ fun DetailScreenContent( } //handle autoAlarm - val autoAlarm = if (alarmSetting == DropdownSettingOption.AUTO_ALARM_ON_DUE && iCalObject.due != null) { - Alarm.createDisplayAlarm( - dur = (0).minutes, - alarmRelativeTo = AlarmRelativeTo.END, - referenceDate = iCalObject.due!!, - referenceTimezone = iCalObject.dueTimezone - ) - } else if (alarmSetting == DropdownSettingOption.AUTO_ALARM_ON_START && iCalObject.dtstart != null) { - Alarm.createDisplayAlarm( - dur = (0).minutes, - alarmRelativeTo = null, - referenceDate = iCalObject.dtstart!!, - referenceTimezone = iCalObject.dtstartTimezone - ) - } else null + val autoAlarm = + if (alarmSetting == DropdownSettingOption.AUTO_ALARM_ON_DUE && iCalObject.due != null) { + Alarm.createDisplayAlarm( + dur = (0).minutes, + alarmRelativeTo = AlarmRelativeTo.END, + referenceDate = iCalObject.due!!, + referenceTimezone = iCalObject.dueTimezone + ) + } else if (alarmSetting == DropdownSettingOption.AUTO_ALARM_ON_START && iCalObject.dtstart != null) { + Alarm.createDisplayAlarm( + dur = (0).minutes, + alarmRelativeTo = null, + referenceDate = iCalObject.dtstart!!, + referenceTimezone = iCalObject.dtstartTimezone + ) + } else null if (autoAlarm != null && alarms.none { alarm -> alarm.triggerRelativeDuration == autoAlarm.triggerRelativeDuration && alarm.triggerRelativeTo == autoAlarm.triggerRelativeTo }) alarms.add(autoAlarm) @@ -301,7 +329,7 @@ fun DetailScreenContent( LaunchedEffect(scrollToSectionState.value) { val sectionIndex = detailSettings.detailSettingOrder.indexOf(scrollToSectionState.value) - if(sectionIndex >= 0) { + if (sectionIndex >= 0) { listState.animateScrollToItem(sectionIndex) scrollToSectionState.value = null } @@ -314,11 +342,11 @@ fun DetailScreenContent( .padding(8.dp) ) { - items(detailSettings.detailSettingOrder) { detailsScreenSection -> + items(detailSettings.detailSettingOrder) { detailsScreenSection -> - when(detailsScreenSection) { + when (detailsScreenSection) { DetailsScreenSection.COLLECTION -> { - if(collection == null) + if (collection == null) return@items DetailsCardCollections( @@ -335,13 +363,15 @@ fun DetailScreenContent( modifier = detailElementModifier ) } + DetailsScreenSection.DATES -> { DetailsCardDates( icalObject = iCalObject, - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, enableDtstart = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] ?: true || iCalObject.getModuleFromString() == Module.JOURNAL, - enableDue = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] ?: true, + enableDue = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] + ?: true, enableCompleted = detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] ?: true, allowCompletedChange = !(linkProgressToSubtasks && subtasks.value.isNotEmpty()), @@ -375,7 +405,8 @@ fun DetailScreenContent( DetailsScreenSection.SUMMARY -> { DetailsCardSummary( initialSummary = iCalObject.summary, - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, + focusRequested = scrollToSectionState.value == DetailsScreenSection.SUMMARY, onSummaryUpdated = { iCalObject.summary = it changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED @@ -387,7 +418,8 @@ fun DetailScreenContent( DetailsScreenSection.DESCRIPTION -> { DetailsCardDescription( initialDescription = iCalObject.description, - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, + focusRequested = scrollToSectionState.value == DetailsScreenSection.DESCRIPTION, onDescriptionUpdated = { iCalObject.description = it changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED @@ -399,7 +431,7 @@ fun DetailScreenContent( } DetailsScreenSection.PROGRESS -> { - if(iCalObject.module == Module.TODO.name) { + if (iCalObject.module == Module.TODO.name) { ElevatedCard(modifier = detailElementModifier.fillMaxWidth()) { ProgressElement( label = null, @@ -414,7 +446,8 @@ fun DetailScreenContent( keepStatusProgressCompletedInSync ) onProgressChanged(itemId, newPercent) - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + changeState.value = + DetailViewModel.DetailChangeState.CHANGEUNSAVED }, showSlider = showProgressForMainTasks, modifier = Modifier.align(Alignment.End) @@ -424,15 +457,15 @@ fun DetailScreenContent( } DetailsScreenSection.STATUSCLASSIFICATIONPRIORITY -> { - if( + if ( !iCalObject.status.isNullOrEmpty() - || !iCalObject.xstatus.isNullOrEmpty() - || !iCalObject.classification.isNullOrEmpty() - || iCalObject.priority in 1..9 - || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false - || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false - || (iCalObject.getModuleFromString() == Module.TODO && detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false) - || showAllOptions + || !iCalObject.xstatus.isNullOrEmpty() + || !iCalObject.classification.isNullOrEmpty() + || iCalObject.priority in 1..9 + || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false + || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false + || (iCalObject.getModuleFromString() == Module.TODO && detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false) + || showAllOptions ) { DetailsCardStatusClassificationPriority( @@ -475,7 +508,7 @@ fun DetailScreenContent( } DetailsScreenSection.CATEGORIES -> { - if(categories.isNotEmpty() || (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != false || showAllOptions)) { + if (categories.isNotEmpty() || (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != false || showAllOptions)) { DetailsCardCategories( categories = categories, storedCategories = ICalDatabase @@ -483,7 +516,7 @@ fun DetailScreenContent( .iCalDatabaseDao() .getStoredCategories() .observeAsState(emptyList()).value, - isReadOnly = collection?.readonly?: true, + isReadOnly = collection?.readonly ?: true, onCategoriesUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED onCategoriesUpdated(it) @@ -493,8 +526,9 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.PARENTS -> { - if(parents.value.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_PARENTS] != false || showAllOptions) { + if (parents.value.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_PARENTS] != false || showAllOptions) { DetailsCardParents( parents = parents.value, isEditMode = isEditMode, @@ -531,8 +565,9 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.SUBTASKS -> { - if(subtasks.value.isNotEmpty() || (collection?.supportsVTODO == true && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBTASKS] != false || showAllOptions))) { + if (subtasks.value.isNotEmpty() || (collection?.supportsVTODO == true && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBTASKS] != false || showAllOptions))) { DetailsCardSubtasks( subtasks = subtasks.value, isEditMode = isEditMode, @@ -571,8 +606,9 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.SUBNOTES -> { - if(subnotes.value.isNotEmpty() || (collection?.supportsVJOURNAL == true && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBNOTES] == true || showAllOptions))) { + if (subnotes.value.isNotEmpty() || (collection?.supportsVJOURNAL == true && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBNOTES] == true || showAllOptions))) { DetailsCardSubnotes( subnotes = subnotes.value, isEditMode = isEditMode, @@ -617,8 +653,9 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.RESOURCES -> { - if(iCalObject.getModuleFromString() == Module.TODO && (resources.isNotEmpty() || (detailSettings.detailSetting[DetailSettingsOption.ENABLE_RESOURCES] == true || showAllOptions))) { + if (iCalObject.getModuleFromString() == Module.TODO && (resources.isNotEmpty() || (detailSettings.detailSetting[DetailSettingsOption.ENABLE_RESOURCES] == true || showAllOptions))) { DetailsCardResources( resources = resources, storedResources = ICalDatabase @@ -626,7 +663,7 @@ fun DetailScreenContent( .iCalDatabaseDao() .getStoredResources() .observeAsState(emptyList()).value, - isReadOnly = collection?.readonly?: true, + isReadOnly = collection?.readonly ?: true, onResourcesUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED onResourcesUpdated(it) @@ -636,8 +673,9 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.ATTENDEES -> { - if(attendees.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] == true || showAllOptions) { + if (attendees.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] == true || showAllOptions) { DetailsCardAttendees( attendees = attendees, isEditMode = isEditMode.value, @@ -648,11 +686,12 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.CONTACT -> { - if(iCalObject.contact?.isNotBlank() == true || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] == true || showAllOptions) { + if (iCalObject.contact?.isNotBlank() == true || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] == true || showAllOptions) { DetailsCardContact( initialContact = iCalObject.contact ?: "", - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, onContactUpdated = { newContact -> iCalObject.contact = newContact.ifEmpty { null } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED @@ -661,11 +700,12 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.URL -> { - if(iCalObject.url?.isNotEmpty() == true || detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] == true || showAllOptions) { + if (iCalObject.url?.isNotEmpty() == true || detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] == true || showAllOptions) { DetailsCardUrl( initialUrl = iCalObject.url ?: "", - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, onUrlUpdated = { newUrl -> iCalObject.url = newUrl.ifEmpty { null } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED @@ -674,14 +714,15 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.LOCATION -> { - if((iCalObject.location?.isNotEmpty() == true || (iCalObject.geoLat != null && iCalObject.geoLong != null)) || detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] == true || showAllOptions) { + if ((iCalObject.location?.isNotEmpty() == true || (iCalObject.geoLat != null && iCalObject.geoLong != null)) || detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] == true || showAllOptions) { DetailsCardLocation( initialLocation = iCalObject.location, initialGeoLat = iCalObject.geoLat, initialGeoLong = iCalObject.geoLong, initialGeofenceRadius = iCalObject.geofenceRadius, - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, onLocationUpdated = { newLocation, newGeoLat, newGeoLong -> if (newGeoLat != null && newGeoLong != null) { iCalObject.geoLat = newGeoLat @@ -698,11 +739,12 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.COMMENTS -> { - if(comments.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] == true || showAllOptions) { + if (comments.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] == true || showAllOptions) { DetailsCardComments( comments = comments, - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, onCommentsUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, @@ -710,11 +752,12 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.ATTACHMENTS -> { - if(attachments.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] == true || showAllOptions) { + if (attachments.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] == true || showAllOptions) { DetailsCardAttachments( attachments = attachments, - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, isRemoteCollection = collection?.accountType != LOCAL_ACCOUNT_TYPE, player = player, onAttachmentsUpdated = { @@ -724,12 +767,13 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.ALARMS -> { - if(alarms.isNotEmpty() || (iCalObject.module == Module.TODO.name && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] == true || showAllOptions))) { + if (alarms.isNotEmpty() || (iCalObject.module == Module.TODO.name && (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] == true || showAllOptions))) { DetailsCardAlarms( alarms = alarms, icalObject = iCalObject, - isReadOnly = collection?.readonly?:true, + isReadOnly = collection?.readonly ?: true, onAlarmsUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, @@ -737,12 +781,13 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.RECURRENCE -> { - if( + if ( iCalObject.rrule != null - || iCalObject.recurid != null - || detailSettings.detailSetting[DetailSettingsOption.ENABLE_RECURRENCE] == true - || (showAllOptions && iCalObject.module != Module.NOTE.name) + || iCalObject.recurid != null + || detailSettings.detailSetting[DetailSettingsOption.ENABLE_RECURRENCE] == true + || (showAllOptions && iCalObject.module != Module.NOTE.name) ) { // only Todos have recur! DetailsCardRecur( icalObject = iCalObject, @@ -750,6 +795,7 @@ fun DetailScreenContent( seriesElement = seriesElement, isReadOnly = collection?.readonly ?: true, hasChildren = subtasks.value.isNotEmpty() || subnotes.value.isNotEmpty(), + focusRequested = scrollToSectionState.value == DetailsScreenSection.RECURRENCE, onRecurUpdated = { updatedRRule -> iCalObject.rrule = updatedRRule?.toString() if (updatedRRule == null) { @@ -774,7 +820,171 @@ fun DetailScreenContent( } } - if(!showAllOptions) { + item { + FlowRow( + horizontalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.Center, + maxItemsInEachRow = 6, + modifier = Modifier + .padding(top = 16.dp) + .fillMaxWidth() + ) { + detailSettings.detailSettingOrder.forEach { section -> + when (section) { + DetailsScreenSection.COLLECTION -> {} // skip + DetailsScreenSection.DATES -> {} //TODO() refactor to show them individually + + DetailsScreenSection.SUMMARY -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUMMARY] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUMMARY] = true + scrollToSectionState.value = DetailsScreenSection.SUMMARY + }) { + Icon(Icons.Outlined.ViewHeadline, stringResource(id = R.string.summary)) + } + } + } + + + DetailsScreenSection.DESCRIPTION -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_DESCRIPTION] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_DESCRIPTION] = true + scrollToSectionState.value = DetailsScreenSection.DESCRIPTION + }) { + Icon(Icons.Outlined.Description, stringResource(id = R.string.description)) + } + } + } + + DetailsScreenSection.PROGRESS -> {} + + DetailsScreenSection.STATUSCLASSIFICATIONPRIORITY -> {} // TODO refactor first + + DetailsScreenSection.CATEGORIES -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] = true + scrollToSectionState.value = DetailsScreenSection.CATEGORIES + }) { + Icon(Icons.Outlined.NewLabel, stringResource(id = R.string.categories)) + } + } + } + + DetailsScreenSection.PARENTS -> {} //TODO refactor first + DetailsScreenSection.SUBTASKS -> {} //TODO refactor first + DetailsScreenSection.SUBNOTES -> {} // TODO refactor first + + DetailsScreenSection.RESOURCES -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_RESOURCES] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_RESOURCES] = true + scrollToSectionState.value = DetailsScreenSection.RESOURCES + }) { + Icon(Icons.Outlined.WorkOutline, stringResource(id = R.string.resources)) + } + } + } + + DetailsScreenSection.ATTENDEES -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] = true + scrollToSectionState.value = DetailsScreenSection.ATTENDEES + }) { + Icon(Icons.Outlined.Groups, stringResource(id = R.string.attendees)) + } + } + } + + + DetailsScreenSection.CONTACT -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] = true + scrollToSectionState.value = DetailsScreenSection.CONTACT + }) { + Icon(Icons.Outlined.ContactMail, stringResource(id = R.string.contact)) + } + } + } + + DetailsScreenSection.URL -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] = true + scrollToSectionState.value = DetailsScreenSection.URL + }) { + Icon(Icons.Outlined.Link, stringResource(id = R.string.url)) + } + } + } + + DetailsScreenSection.LOCATION -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] = true + scrollToSectionState.value = DetailsScreenSection.LOCATION + }) { + Icon(Icons.Outlined.Place, stringResource(id = R.string.location)) + } + } + } + DetailsScreenSection.COMMENTS -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] = true + scrollToSectionState.value = DetailsScreenSection.COMMENTS + }) { + Icon( + Icons.AutoMirrored.Outlined.InsertComment, + stringResource(id = R.string.comments) + ) + } + } + } + + DetailsScreenSection.ATTACHMENTS -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] = true + scrollToSectionState.value = DetailsScreenSection.ATTACHMENTS + }) { + Icon( + Icons.Outlined.AttachFile, + stringResource(id = R.string.attachments) + ) + } + } + } + DetailsScreenSection.ALARMS -> { + AnimatedVisibility(detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] = true + }) { + Icon(Icons.Outlined.AlarmAdd, stringResource(id = R.string.alarms)) + scrollToSectionState.value = DetailsScreenSection.ALARMS + } + } + } + + DetailsScreenSection.RECURRENCE -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_RECURRENCE] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_RECURRENCE] = true + scrollToSectionState.value = DetailsScreenSection.RECURRENCE + }) { + Icon(Icons.Outlined.EventRepeat, stringResource(id = R.string.recurrence)) + } + } + } + } + } + } + } + + if (!showAllOptions) { item { TextButton( onClick = { showAllOptions = true }, @@ -888,7 +1098,13 @@ fun DetailScreenContent_JOURNAL() { observedICalObject = remember { mutableStateOf(entity.property) }, collection = entity.ICalCollection, iCalObject = entity.property, - categories = remember { mutableStateListOf().apply { this.addAll(entity.categories ?: emptyList()) } }, + categories = remember { + mutableStateListOf().apply { + this.addAll( + entity.categories ?: emptyList() + ) + } + }, resources = remember { mutableStateListOf() }, attendees = remember { mutableStateListOf() }, comments = remember { mutableStateListOf() }, @@ -912,21 +1128,27 @@ fun DetailScreenContent_JOURNAL() { isSubnoteDragAndDropEnabled = true, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, scrollToSectionState = remember { mutableStateOf(null) }, - allWriteableCollectionsLive = MutableLiveData(listOf(ICalCollection.createLocalCollection(LocalContext.current))), + allWriteableCollectionsLive = MutableLiveData( + listOf( + ICalCollection.createLocalCollection( + LocalContext.current + ) + ) + ), detailSettings = detailSettings, icalObjectIdList = emptyList(), saveEntry = { }, onProgressChanged = { _, _ -> }, onMoveToNewCollection = { }, - onAudioSubEntryAdded = { _, _ -> }, - onSubEntryAdded = { _, _ -> }, + onAudioSubEntryAdded = { _, _ -> }, + onSubEntryAdded = { _, _ -> }, onSubEntryDeleted = { }, onSubEntryUpdated = { _, _ -> }, goToDetail = { _, _, _, _ -> }, goBack = { }, unlinkFromSeries = { _, _, _ -> }, - onUnlinkSubEntry = { _, _ -> }, - goToFilteredList = { }, + onUnlinkSubEntry = { _, _ -> }, + goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, onUpdateSortOrder = { }, onCategoriesUpdated = { }, @@ -968,7 +1190,13 @@ fun DetailScreenContent_TODO_editInitially() { seriesElement = null, isChildLive = MutableLiveData(false), player = null, - allWriteableCollectionsLive = MutableLiveData(listOf(ICalCollection.createLocalCollection(LocalContext.current))), + allWriteableCollectionsLive = MutableLiveData( + listOf( + ICalCollection.createLocalCollection( + LocalContext.current + ) + ) + ), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, @@ -983,14 +1211,14 @@ fun DetailScreenContent_TODO_editInitially() { saveEntry = { }, onProgressChanged = { _, _ -> }, onMoveToNewCollection = { }, - onAudioSubEntryAdded = { _, _ -> }, - onSubEntryAdded = { _, _ -> }, + onAudioSubEntryAdded = { _, _ -> }, + onSubEntryAdded = { _, _ -> }, onSubEntryDeleted = { }, onSubEntryUpdated = { _, _ -> }, goToDetail = { _, _, _, _ -> }, goBack = { }, unlinkFromSeries = { _, _, _ -> }, - onUnlinkSubEntry = { _, _ -> }, + onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, onUpdateSortOrder = { }, @@ -1033,7 +1261,13 @@ fun DetailScreenContent_TODO_editInitially_isChild() { seriesElement = null, isChildLive = MutableLiveData(true), player = null, - allWriteableCollectionsLive = MutableLiveData(listOf(ICalCollection.createLocalCollection(LocalContext.current))), + allWriteableCollectionsLive = MutableLiveData( + listOf( + ICalCollection.createLocalCollection( + LocalContext.current + ) + ) + ), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, @@ -1048,14 +1282,14 @@ fun DetailScreenContent_TODO_editInitially_isChild() { saveEntry = { }, onProgressChanged = { _, _ -> }, onMoveToNewCollection = { }, - onAudioSubEntryAdded = { _, _ -> }, - onSubEntryAdded = { _, _ -> }, + onAudioSubEntryAdded = { _, _ -> }, + onSubEntryAdded = { _, _ -> }, onSubEntryDeleted = { }, onSubEntryUpdated = { _, _ -> }, goToDetail = { _, _, _, _ -> }, goBack = { }, unlinkFromSeries = { _, _, _ -> }, - onUnlinkSubEntry = { _, _ -> }, + onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, onUpdateSortOrder = { }, @@ -1092,7 +1326,13 @@ fun DetailScreenContent_failedLoading() { seriesElement = null, isChildLive = MutableLiveData(true), player = null, - allWriteableCollectionsLive = MutableLiveData(listOf(ICalCollection.createLocalCollection(LocalContext.current))), + allWriteableCollectionsLive = MutableLiveData( + listOf( + ICalCollection.createLocalCollection( + LocalContext.current + ) + ) + ), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, @@ -1107,14 +1347,14 @@ fun DetailScreenContent_failedLoading() { saveEntry = { }, onProgressChanged = { _, _ -> }, onMoveToNewCollection = { }, - onAudioSubEntryAdded = { _, _ -> }, - onSubEntryAdded = { _, _ -> }, + onAudioSubEntryAdded = { _, _ -> }, + onSubEntryAdded = { _, _ -> }, onSubEntryDeleted = { }, onSubEntryUpdated = { _, _ -> }, goToDetail = { _, _, _, _ -> }, goBack = { }, unlinkFromSeries = { _, _, _ -> }, - onUnlinkSubEntry = { _, _ -> }, + onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, onUpdateSortOrder = { }, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt index 9a06b5116..76fc9e4fc 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt @@ -59,22 +59,23 @@ fun DetailsCardDescription( isReadOnly: Boolean, markdownState: MutableState, isMarkdownEnabled: Boolean, + focusRequested: Boolean, onDescriptionUpdated: (String?) -> Unit, modifier: Modifier = Modifier ) { val focusRequester = remember { FocusRequester() } - var focusRequested by remember { mutableStateOf(false) } + var focusRequestedInternally by remember { mutableStateOf(false) } var isDescriptionFocused by rememberSaveable { mutableStateOf(false) } var description by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(initialDescription?:"") ) } - LaunchedEffect(focusRequested, isDescriptionFocused) { - if(focusRequested) { + LaunchedEffect(focusRequested, focusRequestedInternally, isDescriptionFocused) { + if(focusRequested || focusRequestedInternally) { try { focusRequester.requestFocus() - focusRequested = false + focusRequestedInternally = false } catch (e: Exception) { Log.d("DetailsCardDescription", "Requesting Focus failed") } @@ -85,7 +86,7 @@ fun DetailsCardDescription( ElevatedCard( onClick = { if(!isReadOnly) { - focusRequested = true + focusRequestedInternally = true } }, modifier = modifier @@ -108,7 +109,7 @@ fun DetailsCardDescription( text = stringResource(id = R.string.description) ) - Crossfade(!focusRequested && !isDescriptionFocused && isMarkdownEnabled, + Crossfade(!focusRequested && !focusRequestedInternally && !isDescriptionFocused && isMarkdownEnabled, label = "descriptionWithMarkdown" ) { withMarkdown -> @@ -122,7 +123,7 @@ fun DetailsCardDescription( ), onClick = { if (!isReadOnly) { - focusRequested = true + focusRequestedInternally = true } } ) @@ -212,6 +213,7 @@ fun DetailsCardDescription_Preview_no_Markdown() { DetailsCardDescription( initialDescription = "Test" + System.lineSeparator() + "***Tester***", isReadOnly = false, + focusRequested = false, onDescriptionUpdated = { }, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isMarkdownEnabled = false @@ -228,6 +230,7 @@ fun DetailsCardDescription_Preview_with_Markdown() { initialDescription = "Test" + System.lineSeparator() + "***Tester***", isReadOnly = false, onDescriptionUpdated = { }, + focusRequested = false, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, isMarkdownEnabled = true ) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt index 14b87e62c..ad464abf8 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt @@ -27,6 +27,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -60,6 +61,7 @@ fun DetailsCardRecur( seriesInstances: List, seriesElement: ICalObject?, hasChildren: Boolean, + focusRequested: Boolean, onRecurUpdated: (Recur?) -> Unit, goToDetail: (itemId: Long, editMode: Boolean, list: List) -> Unit, unlinkFromSeries: (instances: List, series: ICalObject?, deleteAfterUnlink: Boolean) -> Unit, @@ -96,6 +98,11 @@ fun DetailsCardRecur( ) } + LaunchedEffect(focusRequested) { + if(focusRequested) + showRecurDialog = true + } + ElevatedCard( onClick = { if(icalObject.dtstart != null && icalObject.recurid == null && !isReadOnly) @@ -356,6 +363,7 @@ fun DetailsCardRecur_Preview() { seriesElement = null, isReadOnly = false, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -395,6 +403,7 @@ fun DetailsCardRecur_Preview_read_only2() { seriesElement = null, isReadOnly = true, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -428,6 +437,7 @@ fun DetailsCardRecur_Preview_unchanged_recur() { seriesElement = null, isReadOnly = false, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -461,6 +471,7 @@ fun DetailsCardRecur_Preview_changed_recur() { seriesElement = null, isReadOnly = false, hasChildren = true, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -492,6 +503,7 @@ fun DetailsCardRecur_Preview_off() { seriesElement = null, isReadOnly = false, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -523,6 +535,7 @@ fun DetailsCardRecur_Preview_read_only() { seriesElement = null, isReadOnly = true, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -547,6 +560,7 @@ fun DetailsCardRecur_Preview_view_no_dtstart() { seriesElement = null, isReadOnly = false, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt index 7f971892d..e7a3a923d 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.ElevatedCard import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,6 +46,7 @@ import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon fun DetailsCardSummary( initialSummary: String?, isReadOnly: Boolean, + focusRequested: Boolean, onSummaryUpdated: (String?) -> Unit, modifier: Modifier = Modifier ) { @@ -53,6 +55,11 @@ fun DetailsCardSummary( var isSummaryFocused by rememberSaveable { mutableStateOf(false) } var summary by rememberSaveable { mutableStateOf(initialSummary) } + LaunchedEffect(focusRequested) { + if(focusRequested && !isReadOnly) + focusRequester.requestFocus() + } + ElevatedCard( onClick = { if(!isReadOnly) @@ -110,6 +117,7 @@ fun DetailsCardSummary_Preview() { DetailsCardSummary( initialSummary = "Test", isReadOnly = false, + focusRequested = false, onSummaryUpdated = { } ) } From e94778d6558188084a16a3bc60fc197935083a0c Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 15 Jun 2024 19:58:48 +0200 Subject: [PATCH 18/27] refactored fetching collections to only observe in composable --- .../techbee/jtx/database/ICalDatabaseDao.kt | 8 ++-- .../jtx/ui/detail/DetailScreenContent.kt | 41 ++++--------------- .../techbee/jtx/ui/detail/DetailViewModel.kt | 2 - .../at/techbee/jtx/ui/detail/DetailsScreen.kt | 1 - .../jtx/ui/list/ListScreenTabContainer.kt | 8 ++-- .../dialogs/CollectionSelectorDialog.kt | 10 ++--- .../jtx/widgets/ListWidgetConfigContent.kt | 2 +- 7 files changed, 21 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt index b7a3429ee..23ae1d74f 100644 --- a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt +++ b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt @@ -126,8 +126,8 @@ interface ICalDatabaseDao { * * @return a list of [Collection] as LiveData> */ - @Query("SELECT * FROM $TABLE_NAME_COLLECTION WHERE $COLUMN_COLLECTION_READONLY = 0 AND ($COLUMN_COLLECTION_SUPPORTSVJOURNAL = 1 OR $COLUMN_COLLECTION_SUPPORTSVTODO = 1) ORDER BY $COLUMN_COLLECTION_ACCOUNT_NAME ASC") - fun getAllWriteableCollections(): LiveData> + @Query("SELECT * FROM $TABLE_NAME_COLLECTION WHERE $COLUMN_COLLECTION_READONLY = 0 AND ($COLUMN_COLLECTION_SUPPORTSVJOURNAL = :supportsVJOURNAL OR $COLUMN_COLLECTION_SUPPORTSVTODO = :supportsVTODO) ORDER BY $COLUMN_COLLECTION_ACCOUNT_NAME ASC") + fun getAllWriteableCollections(supportsVTODO: Boolean, supportsVJOURNAL: Boolean): LiveData> /** * Retrieve an list of all Collections ([Collection]) that have entries for a given module as a LiveData-List @@ -135,8 +135,8 @@ interface ICalDatabaseDao { * @return a list of [Collection] as LiveData> */ @Transaction - @Query("SELECT $TABLE_NAME_COLLECTION.* FROM $TABLE_NAME_COLLECTION WHERE $TABLE_NAME_COLLECTION.$COLUMN_COLLECTION_ID IN (SELECT $TABLE_NAME_ICALOBJECT.$COLUMN_ICALOBJECT_COLLECTIONID FROM $TABLE_NAME_ICALOBJECT WHERE $COLUMN_MODULE = :module) ORDER BY $COLUMN_COLLECTION_ACCOUNT_NAME ASC") - fun getAllCollections(module: String): LiveData> + @Query("SELECT * FROM $TABLE_NAME_COLLECTION WHERE $COLUMN_COLLECTION_SUPPORTSVTODO = :supportsVTODO OR $COLUMN_COLLECTION_SUPPORTSVJOURNAL = :supportsVJOURNAL ORDER BY $COLUMN_COLLECTION_ACCOUNT_NAME ASC") + fun getAllCollections(supportsVTODO: Boolean, supportsVJOURNAL: Boolean): LiveData> /** diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index 6602a1b1d..1c8503ca9 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -118,7 +118,6 @@ fun DetailScreenContent( subnotesLive: LiveData>, parentsLive: LiveData>, isChildLive: LiveData, - allWriteableCollectionsLive: LiveData>, detailSettings: DetailSettings, icalObjectIdList: List, seriesInstancesLive: LiveData>, @@ -157,12 +156,12 @@ fun DetailScreenContent( return val context = LocalContext.current + val database = ICalDatabase.getInstance(context).iCalDatabaseDao() val parents = parentsLive.observeAsState(emptyList()) val subtasks = subtasksLive.observeAsState(emptyList()) val subnotes = subnotesLive.observeAsState(emptyList()) val seriesInstances = seriesInstancesLive.observeAsState(emptyList()) val isChild = isChildLive.observeAsState(false) - val allWriteableCollections = allWriteableCollectionsLive.observeAsState(emptyList()) var timeout by remember { mutableStateOf(false) } LaunchedEffect(timeout, observedICalObject.value) { @@ -230,9 +229,6 @@ fun DetailScreenContent( */ val isProPurchased = BillingManager.getInstance().isProPurchased.observeAsState(true) - val allPossibleCollections = allWriteableCollections.value.filter { - it.accountType == LOCAL_ACCOUNT_TYPE || isProPurchased.value // filter remote collections if pro was not purchased - } // Update some fields in the background that might have changed (e.g. by creating a copy) if ((observedICalObject.value?.sequence ?: 0) > iCalObject.sequence) { @@ -356,7 +352,12 @@ fun DetailScreenContent( originalCollection = collection, color = color, changeState = changeState, - allPossibleCollections = allPossibleCollections, + allPossibleCollections = database.getAllWriteableCollections( + supportsVTODO = iCalObject.module == Module.TODO.name, + supportsVJOURNAL = iCalObject.module == Module.NOTE.name || iCalObject.module == Module.JOURNAL.name + ).observeAsState(emptyList()).value.filter { + it.accountType == LOCAL_ACCOUNT_TYPE || isProPurchased.value // filter remote collections if pro was not purchased + }, includeVJOURNAL = if (observedICalObject.value?.component == Component.VJOURNAL.name || subnotes.value.isNotEmpty()) true else null, includeVTODO = if (observedICalObject.value?.component == Component.VTODO.name || subtasks.value.isNotEmpty()) true else null, onMoveToNewCollection = onMoveToNewCollection, @@ -1128,13 +1129,6 @@ fun DetailScreenContent_JOURNAL() { isSubnoteDragAndDropEnabled = true, markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, scrollToSectionState = remember { mutableStateOf(null) }, - allWriteableCollectionsLive = MutableLiveData( - listOf( - ICalCollection.createLocalCollection( - LocalContext.current - ) - ) - ), detailSettings = detailSettings, icalObjectIdList = emptyList(), saveEntry = { }, @@ -1190,13 +1184,6 @@ fun DetailScreenContent_TODO_editInitially() { seriesElement = null, isChildLive = MutableLiveData(false), player = null, - allWriteableCollectionsLive = MutableLiveData( - listOf( - ICalCollection.createLocalCollection( - LocalContext.current - ) - ) - ), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, @@ -1261,13 +1248,6 @@ fun DetailScreenContent_TODO_editInitially_isChild() { seriesElement = null, isChildLive = MutableLiveData(true), player = null, - allWriteableCollectionsLive = MutableLiveData( - listOf( - ICalCollection.createLocalCollection( - LocalContext.current - ) - ) - ), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, @@ -1326,13 +1306,6 @@ fun DetailScreenContent_failedLoading() { seriesElement = null, isChildLive = MutableLiveData(true), player = null, - allWriteableCollectionsLive = MutableLiveData( - listOf( - ICalCollection.createLocalCollection( - LocalContext.current - ) - ) - ), detailSettings = detailSettings, icalObjectIdList = emptyList(), sliderIncrement = 10, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt index 64c20eda4..5c1fb3e7f 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt @@ -92,8 +92,6 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) val mutableAttachments = mutableStateListOf() val mutableAlarms = mutableStateListOf() - var allWriteableCollections = databaseDao.getAllWriteableCollections() - private var selectFromAllListQuery: MutableLiveData = MutableLiveData() var selectFromAllList: LiveData> = diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt index 093d6c8fd..40640c032 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt @@ -639,7 +639,6 @@ fun DetailsScreen( subtasksLive = detailViewModel.relatedSubtasks, subnotesLive = detailViewModel.relatedSubnotes, isChildLive = detailViewModel.isChild, - allWriteableCollectionsLive = detailViewModel.allWriteableCollections, detailSettings = detailViewModel.detailSettings, icalObjectIdList = icalObjectIdList, seriesInstancesLive = detailViewModel.seriesInstances, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt index 86ddcb85b..9cfe500c7 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt @@ -164,7 +164,7 @@ fun ListScreenTabContainer( enabledTabs.indexOf(ListTabDestination.Tasks) -> icalListViewModelTodos else -> icalListViewModelJournals // fallback, should not happen } - val allWriteableCollections = database.getAllWriteableCollections().observeAsState(emptyList()) + val allWriteableCollections = database.getAllWriteableCollections(supportsVTODO = true, supportsVJOURNAL = true).observeAsState(emptyList()) val isProPurchased = BillingManager.getInstance().isProPurchased.observeAsState(true) val allUsableCollections by remember(allWriteableCollections) { derivedStateOf { @@ -235,7 +235,7 @@ fun ListScreenTabContainer( module = getActiveViewModel().module, allCategories = database.getAllCategoriesAsText().observeAsState(emptyList()).value, allResources = database.getAllResourcesAsText().observeAsState(emptyList()).value, - allCollections = database.getAllWriteableCollections().observeAsState(emptyList()).value, + allCollections = database.getAllWriteableCollections(supportsVJOURNAL = getActiveViewModel().module == Module.NOTE || getActiveViewModel().module == Module.JOURNAL, supportsVTODO = getActiveViewModel().module == Module.TODO).observeAsState(emptyList()).value, selectFromAllListLive = getActiveViewModel().selectFromAllList, storedStatuses = database.getStoredStatuses().observeAsState(emptyList()).value, player = getActiveViewModel().mediaPlayer, @@ -256,7 +256,7 @@ fun ListScreenTabContainer( CollectionSelectorDialog( module = getActiveViewModel().module, presetCollectionId = getActiveViewModel().listSettings.topAppBarCollectionId.value, - allCollections = database.getAllCollections(module = getActiveViewModel().module.name).observeAsState(emptyList()).value, + allWritableCollections = database.getAllWriteableCollections(supportsVJOURNAL = (listViewModel.module == Module.JOURNAL || listViewModel.module == Module.NOTE), supportsVTODO = listViewModel.module == Module.TODO).observeAsState(emptyList()).value, onCollectionConfirmed = { selectedCollection -> getActiveViewModel().listSettings.topAppBarMode.value = ListTopAppBarMode.ADD_ENTRY getActiveViewModel().listSettings.topAppBarCollectionId.value = selectedCollection.collectionId @@ -364,7 +364,7 @@ fun ListScreenTabContainer( module = listViewModel.module, initialTab = filterSheetInitialTab, listSettings = listViewModel.listSettings, - allCollections = database.getAllCollections(module = listViewModel.module.name).observeAsState(emptyList()).value, + allCollections = database.getAllCollections(supportsVJOURNAL = (listViewModel.module == Module.JOURNAL || listViewModel.module == Module.NOTE), supportsVTODO = listViewModel.module == Module.TODO).observeAsState(emptyList()).value, allCategories = database.getAllCategoriesAsText().observeAsState(emptyList()).value, allResources = database.getAllResourcesAsText().observeAsState(emptyList()).value, storedStatuses = database.getStoredStatuses().observeAsState(emptyList()).value, diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt index 77f015493..72f980586 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt @@ -33,13 +33,13 @@ import at.techbee.jtx.ui.reusable.elements.CollectionsSpinner fun CollectionSelectorDialog( module: Module, presetCollectionId: Long, - allCollections: List, + allWritableCollections: List, onCollectionConfirmed: (selectedCollection: ICalCollection) -> Unit, onDismiss: () -> Unit ) { - if(allCollections.isEmpty()) + if(allWritableCollections.isEmpty()) return - var selectedCollection by remember { mutableStateOf(allCollections.find { it.collectionId == presetCollectionId } ?: allCollections.first()) } + var selectedCollection by remember { mutableStateOf(allWritableCollections.find { it.collectionId == presetCollectionId } ?: allWritableCollections.first()) } AlertDialog( onDismissRequest = { onDismiss() }, @@ -49,7 +49,7 @@ fun CollectionSelectorDialog( Column { CollectionsSpinner( - collections = allCollections, + collections = allWritableCollections, preselected = selectedCollection, includeReadOnly = false, includeVJOURNAL = if(module == Module.JOURNAL || module == Module.NOTE) true else null, @@ -134,7 +134,7 @@ fun CollectionSelectorDialog_Preview() { CollectionSelectorDialog( module = Module.JOURNAL, presetCollectionId = 3L, - allCollections = listOf(collection1, collection2, collection3, collection4), + allWritableCollections = listOf(collection1, collection2, collection3, collection4), onDismiss = { }, onCollectionConfirmed = {} ) diff --git a/app/src/main/java/at/techbee/jtx/widgets/ListWidgetConfigContent.kt b/app/src/main/java/at/techbee/jtx/widgets/ListWidgetConfigContent.kt index 5fa1064c9..0b94ea9e6 100644 --- a/app/src/main/java/at/techbee/jtx/widgets/ListWidgetConfigContent.kt +++ b/app/src/main/java/at/techbee/jtx/widgets/ListWidgetConfigContent.kt @@ -189,7 +189,7 @@ fun ListWidgetConfigContent( ListOptionsFilter( module = selectedModule.value, listSettings = listSettings, - allCollections = database.getAllCollections(module = selectedModule.value.name).observeAsState(emptyList()).value, + allCollections = database.getAllCollections(supportsVJOURNAL = (selectedModule.value == Module.JOURNAL || selectedModule.value == Module.NOTE), supportsVTODO = selectedModule.value == Module.TODO).observeAsState(emptyList()).value, allCategories = database.getAllCategoriesAsText().observeAsState(emptyList()).value, allResources = database.getAllResourcesAsText().observeAsState(emptyList()).value, extendedStatuses = database.getStoredStatuses().observeAsState(emptyList()).value, From 49820976c48d95cb78e61bca9c7206937bf3f1d2 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 15 Jun 2024 22:25:28 +0200 Subject: [PATCH 19/27] refactored long-click to select instead of edit --- .../techbee/jtx/ui/list/ListBottomAppBar.kt | 19 +- .../java/at/techbee/jtx/ui/list/ListCard.kt | 15 +- .../at/techbee/jtx/ui/list/ListCardCompact.kt | 5 +- .../java/at/techbee/jtx/ui/list/ListScreen.kt | 22 +-- .../techbee/jtx/ui/list/ListScreenCompact.kt | 9 +- .../at/techbee/jtx/ui/list/ListScreenGrid.kt | 11 +- .../techbee/jtx/ui/list/ListScreenKanban.kt | 5 +- .../at/techbee/jtx/ui/list/ListScreenList.kt | 14 +- .../jtx/ui/list/ListScreenTabContainer.kt | 170 ++++++++---------- .../at/techbee/jtx/ui/list/ListScreenWeek.kt | 9 +- 10 files changed, 111 insertions(+), 168 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListBottomAppBar.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListBottomAppBar.kt index a4d7081d5..c64777b74 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListBottomAppBar.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListBottomAppBar.kt @@ -29,7 +29,6 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterListOff -import androidx.compose.material.icons.outlined.LibraryAddCheck import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.Sync import androidx.compose.material.icons.outlined.SyncProblem @@ -82,7 +81,6 @@ fun ListBottomAppBar( isSyncInProgress: Boolean, listSettings: ListSettings, showQuickEntry: MutableState, - multiselectEnabled: MutableState, selectedEntries: SnapshotStateList, allowNewEntries: Boolean, isBiometricsEnabled: Boolean, @@ -167,17 +165,11 @@ fun ListBottomAppBar( BottomAppBar( actions = { - AnimatedVisibility(!multiselectEnabled.value) { + AnimatedVisibility(selectedEntries.isEmpty()) { Row( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically ) { - IconButton(onClick = { multiselectEnabled.value = true }) { - Icon( - Icons.Outlined.LibraryAddCheck, - contentDescription = "select multiple" - ) - } IconButton(onClick = { onFilterIconClicked() }) { Icon( Icons.Outlined.FilterList, @@ -288,14 +280,13 @@ fun ListBottomAppBar( } - AnimatedVisibility(multiselectEnabled.value) { + AnimatedVisibility(selectedEntries.isNotEmpty()) { Row( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically ) { IconButton(onClick = { selectedEntries.clear() - multiselectEnabled.value = false }) { Icon( Icons.Outlined.Close, @@ -354,7 +345,7 @@ fun ListBottomAppBar( }, floatingActionButton = { // TODO(b/228588827): Replace with Secondary FAB when available. - AnimatedVisibility(allowNewEntries && !multiselectEnabled.value) { + AnimatedVisibility(allowNewEntries && selectedEntries.isEmpty()) { FloatingActionButton( onClick = { onAddNewEntry() }, ) { @@ -390,7 +381,6 @@ fun ListBottomAppBar_Preview_Journal() { isBiometricsEnabled = false, isBiometricsUnlocked = false, incompatibleSyncApps = emptyList(), - multiselectEnabled = remember { mutableStateOf(false) }, selectedEntries = remember { mutableStateListOf() }, onAddNewEntry = { }, showQuickEntry = remember { mutableStateOf(true) }, @@ -423,7 +413,6 @@ fun ListBottomAppBar_Preview_Note() { isBiometricsEnabled = false, isBiometricsUnlocked = false, incompatibleSyncApps = listOf(SyncApp.DAVX5), - multiselectEnabled = remember { mutableStateOf(true) }, selectedEntries = remember { mutableStateListOf() }, onAddNewEntry = { }, showQuickEntry = remember { mutableStateOf(false) }, @@ -456,7 +445,6 @@ fun ListBottomAppBar_Preview_Todo() { incompatibleSyncApps = listOf(SyncApp.DAVX5), isBiometricsEnabled = true, isBiometricsUnlocked = false, - multiselectEnabled = remember { mutableStateOf(false) }, selectedEntries = remember { mutableStateListOf() }, onAddNewEntry = { }, showQuickEntry = remember { mutableStateOf(true) }, @@ -490,7 +478,6 @@ fun ListBottomAppBar_Preview_Todo_filterActive() { isBiometricsEnabled = true, isBiometricsUnlocked = true, incompatibleSyncApps = listOf(SyncApp.DAVX5), - multiselectEnabled = remember { mutableStateOf(false) }, selectedEntries = remember { mutableStateListOf() }, onAddNewEntry = { }, showQuickEntry = remember { mutableStateOf(true) }, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt index 077f2608c..f5d99e1b7 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt @@ -63,7 +63,6 @@ import at.techbee.jtx.database.properties.Attachment import at.techbee.jtx.database.properties.Category import at.techbee.jtx.database.properties.Resource import at.techbee.jtx.database.views.ICal4List -import at.techbee.jtx.flavored.BillingManager import at.techbee.jtx.ui.reusable.cards.AttachmentCard import at.techbee.jtx.ui.reusable.cards.SubnoteCard import at.techbee.jtx.ui.reusable.cards.SubtaskCard @@ -109,7 +108,7 @@ fun ListCard( isSubtaskDragAndDropEnabled: Boolean, isSubnoteDragAndDropEnabled: Boolean, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, - onLongClick: (itemId: Long, list: List) -> Unit, + onLongClick: (itemId: Long, isReadOnly: Boolean) -> Unit, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, onExpandedChanged: (itemId: Long, isSubtasksExpanded: Boolean, isSubnotesExpanded: Boolean, isParentsExpanded: Boolean, isAttachmentsExpanded: Boolean) -> Unit, onUpdateSortOrder: (List) -> Unit, @@ -457,8 +456,7 @@ fun ListCard( .combinedClickable( onClick = { onClick(subtask.id, subtasks, subtask.isReadOnly) }, onLongClick = { - if (!subtask.isReadOnly && BillingManager.getInstance().isProPurchased.value == true) - onLongClick(subtask.id, subtasks) + onLongClick(subtask.id, subtask.isReadOnly) } ) ) @@ -492,8 +490,7 @@ fun ListCard( .combinedClickable( onClick = { onClick(subnote.id, subnotes, subnote.isReadOnly) }, onLongClick = { - if (!subnote.isReadOnly && BillingManager.getInstance().isProPurchased.value == true) - onLongClick(subnote.id, subnotes) + onLongClick(subnote.id, subnote.isReadOnly) }, ), dragHandle = { if(isSubnoteDragAndDropEnabled) DragHandle(scope = this) }, @@ -530,8 +527,7 @@ fun ListCard( ) }, onLongClick = { - if (!parent.isReadOnly && BillingManager.getInstance().isProPurchased.value == true) - onLongClick(parent.id, parents) + onLongClick(parent.id, parent.isReadOnly) } ) ) @@ -551,8 +547,7 @@ fun ListCard( ) }, onLongClick = { - if (!parent.isReadOnly && BillingManager.getInstance().isProPurchased.value == true) - onLongClick(parent.id, parents) + onLongClick(parent.id, parent.isReadOnly) }, ), isEditMode = false, //no editing here diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListCardCompact.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListCardCompact.kt index 1a31e230c..18b134b08 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListCardCompact.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListCardCompact.kt @@ -69,7 +69,7 @@ fun ListCardCompact( modifier: Modifier = Modifier, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, - onLongClick: (itemId: Long, list: List) -> Unit, + onLongClick: (itemId: Long, isReadOnly: Boolean) -> Unit, onUpdateSortOrder: (List) -> Unit, dragHandle:@Composable () -> Unit = { } ) { @@ -171,8 +171,7 @@ fun ListCardCompact( .combinedClickable( onClick = { onClick(subtask.id, subtasks, subtask.isReadOnly) }, onLongClick = { - if (!subtask.isReadOnly) - onLongClick(subtask.id, subtasks) + onLongClick(subtask.id, subtask.isReadOnly) } ) ) diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt index eb6bd53b0..82b6e3198 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt @@ -21,7 +21,6 @@ import androidx.navigation.NavController import at.techbee.jtx.database.ICalDatabase import at.techbee.jtx.database.relations.ICal4ListRel import at.techbee.jtx.database.views.ICal4List -import at.techbee.jtx.flavored.BillingManager import at.techbee.jtx.ui.reusable.destinations.DetailDestination import at.techbee.jtx.ui.settings.SettingsStateHolder import at.techbee.jtx.util.SyncUtil @@ -52,17 +51,17 @@ fun ListScreen( ) fun processOnClick(itemId: Long, ical4list: List, isReadOnly: Boolean) { - if (listViewModel.multiselectEnabled.value && isReadOnly) + if(isReadOnly && listViewModel.selectedEntries.isNotEmpty()) return - else if (listViewModel.multiselectEnabled.value) + else if (listViewModel.selectedEntries.isNotEmpty()) if (listViewModel.selectedEntries.contains(itemId)) listViewModel.selectedEntries.remove(itemId) else listViewModel.selectedEntries.add(itemId) else navController.navigate(DetailDestination.Detail.getRoute(itemId, ical4list.map { it.id }, false)) } - fun processOnLongClick(itemId: Long, ical4list: List) { - if (!listViewModel.multiselectEnabled.value && BillingManager.getInstance().isProPurchased.value == true) - navController.navigate(DetailDestination.Detail.getRoute(itemId, ical4list.map { it.id }, true)) + fun processOnLongClick(itemId: Long, isReadOnly: Boolean) { + if(!isReadOnly) + listViewModel.selectedEntries.add(itemId) } fun processOnProgressChanged(itemId: Long, newPercent: Int, scrollOnce: Boolean = false) { @@ -73,6 +72,7 @@ fun ListScreen( ) } + Column(modifier = Modifier.fillMaxSize()) { when (listViewModel.listSettings.viewMode.value) { ViewMode.LIST -> { @@ -104,7 +104,7 @@ fun ListScreen( isSubtaskDragAndDropEnabled = listViewModel.listSettings.subtasksOrderBy.value == OrderBy.DRAG_AND_DROP, isSubnoteDragAndDropEnabled = listViewModel.listSettings.subnotesOrderBy.value == OrderBy.DRAG_AND_DROP, onClick = { itemId, ical4list, isReadOnly -> processOnClick(itemId, ical4list, isReadOnly) }, - onLongClick = { itemId, ical4list -> processOnLongClick(itemId, ical4list) }, + onLongClick = { itemId, isReadOnly -> processOnLongClick(itemId, isReadOnly) }, onProgressChanged = { itemId, newPercent -> processOnProgressChanged(itemId, newPercent) }, @@ -135,7 +135,7 @@ fun ListScreen( markdownEnabled = listViewModel.listSettings.markdownEnabled.value, player = listViewModel.mediaPlayer, onClick = { itemId, ical4list, isReadOnly -> processOnClick(itemId, ical4list, isReadOnly) }, - onLongClick = { itemId, ical4list -> processOnLongClick(itemId, ical4list) }, + onLongClick = { itemId, isReadOnly -> processOnLongClick(itemId, isReadOnly) }, onProgressChanged = { itemId, newPercent -> processOnProgressChanged(itemId, newPercent) }, @@ -158,7 +158,7 @@ fun ListScreen( isListDragAndDropEnabled = listViewModel.listSettings.orderBy.value == OrderBy.DRAG_AND_DROP || listViewModel.listSettings.orderBy2.value == OrderBy.DRAG_AND_DROP, isSubtaskDragAndDropEnabled = listViewModel.listSettings.subtasksOrderBy.value == OrderBy.DRAG_AND_DROP, onClick = { itemId, ical4list, isReadOnly -> processOnClick(itemId, ical4list, isReadOnly) }, - onLongClick = { itemId, ical4list -> processOnLongClick(itemId, ical4list) }, + onLongClick = { itemId, isReadOnly -> processOnLongClick(itemId, isReadOnly) }, onProgressChanged = { itemId, newPercent -> processOnProgressChanged(itemId, newPercent) }, onSyncRequested = { listViewModel.syncAccounts() }, onSaveListSettings = { listViewModel.saveListSettings() }, @@ -182,7 +182,7 @@ fun ListScreen( markdownEnabled = listViewModel.listSettings.markdownEnabled.value, player = listViewModel.mediaPlayer, onClick = { itemId, ical4list, isReadOnly -> processOnClick(itemId, ical4list, isReadOnly) }, - onLongClick = { itemId, ical4list -> processOnLongClick(itemId, ical4list) }, + onLongClick = { itemId, isReadOnly -> processOnLongClick(itemId, isReadOnly) }, onStatusChanged = { itemId, newStatus, scrollOnce -> listViewModel.updateStatus(itemId, newStatus, scrollOnce) }, onXStatusChanged = { itemId, newXStatus, scrollOnce -> listViewModel.updateXStatus(itemId, newXStatus, scrollOnce) }, onSwapCategories = { itemId, oldCategory, newCategory -> listViewModel.swapCategories(itemId, oldCategory, newCategory) }, @@ -195,7 +195,7 @@ fun ListScreen( selectedEntries = listViewModel.selectedEntries, scrollOnceId = listViewModel.scrollOnceId, onClick = { itemId, ical4list, isReadOnly -> processOnClick(itemId, ical4list, isReadOnly) }, - onLongClick = { itemId, ical4list -> processOnLongClick(itemId, ical4list) }, + onLongClick = { itemId, isReadOnly -> processOnLongClick(itemId, isReadOnly) }, ) } } diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenCompact.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenCompact.kt index 28157a0b8..667c067ab 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenCompact.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenCompact.kt @@ -95,7 +95,7 @@ fun ListScreenCompact( isSubtaskDragAndDropEnabled: Boolean, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, - onLongClick: (itemId: Long, list: List) -> Unit, + onLongClick: (itemId: Long, isReadOnly: Boolean) -> Unit, onSyncRequested: () -> Unit, onSaveListSettings: () -> Unit, onUpdateSortOrder: (List) -> Unit @@ -230,12 +230,7 @@ fun ListScreenCompact( ) }, onLongClick = { - if (!iCal4ListRelObject.iCal4List.isReadOnly) - onLongClick( - iCal4ListRelObject.iCal4List.id, - groupedList - .flatMap { it.value } - .map { it.iCal4List }) + onLongClick(iCal4ListRelObject.iCal4List.id, iCal4ListRelObject.iCal4List.isReadOnly) } ), onProgressChanged = onProgressChanged, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenGrid.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenGrid.kt index a7f8a812c..6bc70bfe3 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenGrid.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenGrid.kt @@ -87,7 +87,7 @@ fun ListScreenGrid( isListDragAndDropEnabled: Boolean, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, - onLongClick: (itemId: Long, list: List) -> Unit, + onLongClick: (itemId: Long, isReadOnly: Boolean) -> Unit, onSyncRequested: () -> Unit ) { @@ -175,10 +175,7 @@ fun ListScreenGrid( ) }, onLongClick = { - if (!iCal4ListRelObject.iCal4List.isReadOnly) - onLongClick( - iCal4ListRelObject.iCal4List.id, - list.map { it.iCal4List }) + onLongClick(iCal4ListRelObject.iCal4List.id, iCal4ListRelObject.iCal4List.isReadOnly) } ), onProgressChanged = onProgressChanged, @@ -271,7 +268,7 @@ fun ListScreenGrid_TODO() { player = null, onProgressChanged = { _, _ -> }, onClick = { _, _, _ -> }, - onLongClick = { _, _ -> }, + onLongClick = { _, _ -> }, onSyncRequested = { }, isListDragAndDropEnabled = true ) @@ -328,7 +325,7 @@ fun ListScreenGrid_JOURNAL() { player = null, onProgressChanged = { _, _ -> }, onClick = { _, _, _ -> }, - onLongClick = { _, _ -> }, + onLongClick = { _, _ -> }, onSyncRequested = { }, isListDragAndDropEnabled = false ) diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenKanban.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenKanban.kt index 1544038dd..6b2cd0a7b 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenKanban.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenKanban.kt @@ -95,7 +95,7 @@ fun ListScreenKanban( onXStatusChanged: (itemid: Long, status: ExtendedStatus, scrollOnce: Boolean) -> Unit, onSwapCategories: (itemid: Long, old: String, new: String) -> Unit, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, - onLongClick: (itemId: Long, list: List) -> Unit, + onLongClick: (itemId: Long, isReadOnly: Boolean) -> Unit, onSyncRequested: () -> Unit ) { @@ -210,8 +210,7 @@ fun ListScreenKanban( .combinedClickable( onClick = { onClick(iCal4ListRelObject.iCal4List.id, list.map { it.iCal4List }, iCal4ListRelObject.iCal4List.isReadOnly) }, onLongClick = { - if (!iCal4ListRelObject.iCal4List.isReadOnly) - onLongClick(iCal4ListRelObject.iCal4List.id, list.map { it.iCal4List }) + onLongClick(iCal4ListRelObject.iCal4List.id, iCal4ListRelObject.iCal4List.isReadOnly) } ) .fillMaxWidth() diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenList.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenList.kt index 352b7ae64..ec30674cf 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenList.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenList.kt @@ -72,7 +72,6 @@ import at.techbee.jtx.database.properties.Attachment import at.techbee.jtx.database.properties.Reltype import at.techbee.jtx.database.relations.ICal4ListRel import at.techbee.jtx.database.views.ICal4List -import at.techbee.jtx.flavored.BillingManager import at.techbee.jtx.ui.reusable.elements.DragHandleLazy import at.techbee.jtx.ui.settings.DropdownSettingOption import at.techbee.jtx.ui.theme.jtxCardCornerShape @@ -113,7 +112,7 @@ fun ListScreenList( isSubtaskDragAndDropEnabled: Boolean, isSubnoteDragAndDropEnabled: Boolean, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, - onLongClick: (itemId: Long, list: List) -> Unit, + onLongClick: (itemId: Long, isReadOnly: Boolean) -> Unit, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, onExpandedChanged: (itemId: Long, isSubtasksExpanded: Boolean, isSubnotesExpanded: Boolean, isParentsExpanded: Boolean, isAttachmentsExpanded: Boolean) -> Unit, onSyncRequested: () -> Unit, @@ -278,12 +277,7 @@ fun ListScreenList( ) }, onLongClick = { - if (!iCal4ListRelObject.iCal4List.isReadOnly && BillingManager.getInstance().isProPurchased.value == true) - onLongClick( - iCal4ListRelObject.iCal4List.id, - groupedList - .flatMap { it.value } - .map { it.iCal4List }) + onLongClick(iCal4ListRelObject.iCal4List.id, iCal4ListRelObject.iCal4List.isReadOnly) } ) .testTag("benchmark:ListCard") @@ -391,7 +385,7 @@ fun ListScreenList_TODO() { isSubnoteDragAndDropEnabled = true, onProgressChanged = { _, _ -> }, onClick = { _, _, _ -> }, - onLongClick = { _, _ -> }, + onLongClick = { _, _ -> }, listSettings = listSettings, onExpandedChanged = { _, _, _, _, _ -> }, onSyncRequested = { }, @@ -475,7 +469,7 @@ fun ListScreenList_JOURNAL() { isSubnoteDragAndDropEnabled = true, onProgressChanged = { _, _ -> }, onClick = { _, _, _ -> }, - onLongClick = { _, _ -> }, + onLongClick = { _, _ -> }, listSettings = listSettings, onExpandedChanged = { _, _, _, _, _ -> }, onSyncRequested = { }, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt index 9cfe500c7..8966f5548 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt @@ -154,15 +154,11 @@ fun ListScreenTabContainer( pageCount = { enabledTabs.size } ) - val icalListViewModelJournals: ListViewModelJournals = viewModel() - val icalListViewModelNotes: ListViewModelNotes = viewModel() - val icalListViewModelTodos: ListViewModelTodos = viewModel() - val listViewModel = when(pagerState.currentPage) { - enabledTabs.indexOf(ListTabDestination.Journals) -> icalListViewModelJournals - enabledTabs.indexOf(ListTabDestination.Notes) -> icalListViewModelNotes - enabledTabs.indexOf(ListTabDestination.Tasks) -> icalListViewModelTodos - else -> icalListViewModelJournals // fallback, should not happen + enabledTabs.indexOf(ListTabDestination.Journals) -> viewModel() + enabledTabs.indexOf(ListTabDestination.Notes) -> viewModel() + enabledTabs.indexOf(ListTabDestination.Tasks) -> viewModel() + else -> viewModel() // fallback, should not happen } val allWriteableCollections = database.getAllWriteableCollections(supportsVTODO = true, supportsVJOURNAL = true).observeAsState(emptyList()) val isProPurchased = BillingManager.getInstance().isProPurchased.observeAsState(true) @@ -190,30 +186,19 @@ fun ListScreenTabContainer( var showUpdateEntriesDialog by rememberSaveable { mutableStateOf(false) } var showCollectionSelectorDialog by rememberSaveable { mutableStateOf(false) } - - fun getActiveViewModel() = when (pagerState.currentPage) { - enabledTabs.indexOf(ListTabDestination.Journals) -> icalListViewModelJournals - enabledTabs.indexOf(ListTabDestination.Notes) -> icalListViewModelNotes - enabledTabs.indexOf(ListTabDestination.Tasks) -> icalListViewModelTodos - else -> icalListViewModelJournals // fallback, should not happen - } - - val goToEdit = getActiveViewModel().goToEdit.observeAsState() + val goToEdit = listViewModel.goToEdit.observeAsState() goToEdit.value?.let { icalObjectId -> - getActiveViewModel().goToEdit.value = null - navController.navigate(DetailDestination.Detail.getRoute(iCalObjectId = icalObjectId, icalObjectIdList = getActiveViewModel().iCal4ListRel.value?.map { it.iCal4List.id } ?: emptyList(), isEditMode = true)) + listViewModel.goToEdit.value = null + navController.navigate(DetailDestination.Detail.getRoute(iCalObjectId = icalObjectId, icalObjectIdList = listViewModel.iCal4ListRel.value?.map { it.iCal4List.id } ?: emptyList(), isEditMode = true)) } LaunchedEffect(storedListSettingData) { if (storedListSettingData != null) { - storedListSettingData.applyToListSettings(icalListViewModelJournals.listSettings) - storedListSettingData.applyToListSettings(icalListViewModelNotes.listSettings) - storedListSettingData.applyToListSettings(icalListViewModelTodos.listSettings) - getActiveViewModel().updateSearch(saveListSettings = false, isAuthenticated = globalStateHolder.isAuthenticated.value) + storedListSettingData.applyToListSettings(listViewModel.listSettings) + listViewModel.updateSearch(saveListSettings = false, isAuthenticated = globalStateHolder.isAuthenticated.value) } } - val drawerState = rememberDrawerState(DrawerValue.Closed) val filterSheetState = rememberModalBottomSheetState() var filterSheetInitialTab by remember { mutableStateOf(ListOptionsBottomSheetTabs.FILTER)} @@ -224,50 +209,50 @@ fun ListScreenTabContainer( if (showDeleteSelectedDialog) { DeleteSelectedDialog( - numEntriesToDelete = getActiveViewModel().selectedEntries.size, - onConfirm = { getActiveViewModel().deleteSelected() }, + numEntriesToDelete = listViewModel.selectedEntries.size, + onConfirm = { listViewModel.deleteSelected() }, onDismiss = { showDeleteSelectedDialog = false } ) } if (showUpdateEntriesDialog) { UpdateEntriesDialog( - module = getActiveViewModel().module, + module = listViewModel.module, allCategories = database.getAllCategoriesAsText().observeAsState(emptyList()).value, allResources = database.getAllResourcesAsText().observeAsState(emptyList()).value, - allCollections = database.getAllWriteableCollections(supportsVJOURNAL = getActiveViewModel().module == Module.NOTE || getActiveViewModel().module == Module.JOURNAL, supportsVTODO = getActiveViewModel().module == Module.TODO).observeAsState(emptyList()).value, - selectFromAllListLive = getActiveViewModel().selectFromAllList, + allCollections = database.getAllWriteableCollections(supportsVJOURNAL = listViewModel.module == Module.NOTE || listViewModel.module == Module.JOURNAL, supportsVTODO = listViewModel.module == Module.TODO).observeAsState(emptyList()).value, + selectFromAllListLive = listViewModel.selectFromAllList, storedStatuses = database.getStoredStatuses().observeAsState(emptyList()).value, - player = getActiveViewModel().mediaPlayer, - onSelectFromAllListSearchTextUpdated = { getActiveViewModel().updateSelectFromAllListQuery(searchText = it, isAuthenticated = globalStateHolder.isAuthenticated.value) }, - onCategoriesChanged = { addedCategories, deletedCategories -> getActiveViewModel().updateCategoriesOfSelected(addedCategories, deletedCategories) }, - onResourcesChanged = { addedResources, deletedResources -> getActiveViewModel().updateResourcesToSelected(addedResources, deletedResources) }, - onStatusChanged = { newStatus -> getActiveViewModel().updateStatusOfSelected(newStatus) }, - onXStatusChanged = { newXStatus -> getActiveViewModel().updateXStatusOfSelected(newXStatus) }, - onClassificationChanged = { newClassification -> getActiveViewModel().updateClassificationOfSelected(newClassification) }, - onPriorityChanged = { newPriority -> getActiveViewModel().updatePriorityOfSelected(newPriority) }, - onCollectionChanged = { newCollection -> getActiveViewModel().moveSelectedToNewCollection(newCollection) }, - onParentAdded = { addedParent -> getActiveViewModel().addNewParentToSelected(addedParent) }, + player = listViewModel.mediaPlayer, + onSelectFromAllListSearchTextUpdated = { listViewModel.updateSelectFromAllListQuery(searchText = it, isAuthenticated = globalStateHolder.isAuthenticated.value) }, + onCategoriesChanged = { addedCategories, deletedCategories -> listViewModel.updateCategoriesOfSelected(addedCategories, deletedCategories) }, + onResourcesChanged = { addedResources, deletedResources -> listViewModel.updateResourcesToSelected(addedResources, deletedResources) }, + onStatusChanged = { newStatus -> listViewModel.updateStatusOfSelected(newStatus) }, + onXStatusChanged = { newXStatus -> listViewModel.updateXStatusOfSelected(newXStatus) }, + onClassificationChanged = { newClassification -> listViewModel.updateClassificationOfSelected(newClassification) }, + onPriorityChanged = { newPriority -> listViewModel.updatePriorityOfSelected(newPriority) }, + onCollectionChanged = { newCollection -> listViewModel.moveSelectedToNewCollection(newCollection) }, + onParentAdded = { addedParent -> listViewModel.addNewParentToSelected(addedParent) }, onDismiss = { showUpdateEntriesDialog = false } ) } if (showCollectionSelectorDialog) { CollectionSelectorDialog( - module = getActiveViewModel().module, - presetCollectionId = getActiveViewModel().listSettings.topAppBarCollectionId.value, + module = listViewModel.module, + presetCollectionId = listViewModel.listSettings.topAppBarCollectionId.value, allWritableCollections = database.getAllWriteableCollections(supportsVJOURNAL = (listViewModel.module == Module.JOURNAL || listViewModel.module == Module.NOTE), supportsVTODO = listViewModel.module == Module.TODO).observeAsState(emptyList()).value, onCollectionConfirmed = { selectedCollection -> - getActiveViewModel().listSettings.topAppBarMode.value = ListTopAppBarMode.ADD_ENTRY - getActiveViewModel().listSettings.topAppBarCollectionId.value = selectedCollection.collectionId - getActiveViewModel().listSettings.saveToPrefs(getActiveViewModel().prefs) + listViewModel.listSettings.topAppBarMode.value = ListTopAppBarMode.ADD_ENTRY + listViewModel.listSettings.topAppBarCollectionId.value = selectedCollection.collectionId + listViewModel.listSettings.saveToPrefs(listViewModel.prefs) }, onDismiss = { showCollectionSelectorDialog = false } ) } - if(getActiveViewModel().sqlConstraintException.value) { - ErrorOnUpdateDialog(onConfirm = { getActiveViewModel().sqlConstraintException.value = false }) + if(listViewModel.sqlConstraintException.value) { + ErrorOnUpdateDialog(onConfirm = { listViewModel.sqlConstraintException.value = false }) } // reset search when tab changes @@ -283,7 +268,7 @@ fun ListScreenTabContainer( var lastIsAuthenticated by remember { mutableStateOf(false) } if(lastIsAuthenticated != globalStateHolder.isAuthenticated.value) { lastIsAuthenticated = globalStateHolder.isAuthenticated.value - getActiveViewModel().updateSearch(false, globalStateHolder.isAuthenticated.value) + listViewModel.updateSearch(false, globalStateHolder.isAuthenticated.value) } fun addNewEntry( @@ -349,7 +334,7 @@ fun ListScreenTabContainer( ) } else null - getActiveViewModel().insertQuickItem( + listViewModel.insertQuickItem( newICalObject, mergedCategories, attachments, @@ -386,7 +371,7 @@ fun ListScreenTabContainer( topBar = { ListTopAppBar( drawerState = drawerState, - listTopAppBarMode = getActiveViewModel().listSettings.topAppBarMode.value, + listTopAppBarMode = listViewModel.listSettings.topAppBarMode.value, module = listViewModel.module, searchText = listViewModel.listSettings.searchText, newEntryText = listViewModel.listSettings.newEntryText, @@ -426,35 +411,35 @@ fun ListScreenTabContainer( text = { Text( text = stringResource(id = R.string.search), - color = if(getActiveViewModel().listSettings.topAppBarMode.value == ListTopAppBarMode.SEARCH) MaterialTheme.colorScheme.primary else Color.Unspecified + color = if(listViewModel.listSettings.topAppBarMode.value == ListTopAppBarMode.SEARCH) MaterialTheme.colorScheme.primary else Color.Unspecified )}, leadingIcon = { Icon( imageVector = Icons.Outlined.Search, contentDescription = null, - tint = if(getActiveViewModel().listSettings.topAppBarMode.value == ListTopAppBarMode.SEARCH) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface + tint = if(listViewModel.listSettings.topAppBarMode.value == ListTopAppBarMode.SEARCH) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface ) }, onClick = { - getActiveViewModel().listSettings.topAppBarMode.value = ListTopAppBarMode.SEARCH - getActiveViewModel().listSettings.saveToPrefs(getActiveViewModel().prefs) - getActiveViewModel().listSettings.newEntryText.value = "" + listViewModel.listSettings.topAppBarMode.value = ListTopAppBarMode.SEARCH + listViewModel.listSettings.saveToPrefs(listViewModel.prefs) + listViewModel.listSettings.newEntryText.value = "" } ) DropdownMenuItem( text = { - when (getActiveViewModel().module) { - Module.JOURNAL -> Text(text = stringResource(id = R.string.toolbar_text_add_journal), color = if(getActiveViewModel().listSettings.topAppBarMode.value == ListTopAppBarMode.ADD_ENTRY) MaterialTheme.colorScheme.primary else Color.Unspecified) - Module.NOTE -> Text(text = stringResource(id = R.string.toolbar_text_add_note), color = if(getActiveViewModel().listSettings.topAppBarMode.value == ListTopAppBarMode.ADD_ENTRY) MaterialTheme.colorScheme.primary else Color.Unspecified) - Module.TODO -> Text(text = stringResource(id = R.string.toolbar_text_add_task), color = if(getActiveViewModel().listSettings.topAppBarMode.value == ListTopAppBarMode.ADD_ENTRY) MaterialTheme.colorScheme.primary else Color.Unspecified) + when (listViewModel.module) { + Module.JOURNAL -> Text(text = stringResource(id = R.string.toolbar_text_add_journal), color = if(listViewModel.listSettings.topAppBarMode.value == ListTopAppBarMode.ADD_ENTRY) MaterialTheme.colorScheme.primary else Color.Unspecified) + Module.NOTE -> Text(text = stringResource(id = R.string.toolbar_text_add_note), color = if(listViewModel.listSettings.topAppBarMode.value == ListTopAppBarMode.ADD_ENTRY) MaterialTheme.colorScheme.primary else Color.Unspecified) + Module.TODO -> Text(text = stringResource(id = R.string.toolbar_text_add_task), color = if(listViewModel.listSettings.topAppBarMode.value == ListTopAppBarMode.ADD_ENTRY) MaterialTheme.colorScheme.primary else Color.Unspecified) } }, leadingIcon = { Icon( imageVector = Icons.Outlined.Add, contentDescription = null, - tint = if(getActiveViewModel().listSettings.topAppBarMode.value == ListTopAppBarMode.ADD_ENTRY) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface + tint = if(listViewModel.listSettings.topAppBarMode.value == ListTopAppBarMode.ADD_ENTRY) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface ) }, onClick = { @@ -463,11 +448,11 @@ fun ListScreenTabContainer( showCollectionSelectorDialog = true topBarMenuExpanded = false } else { - getActiveViewModel().listSettings.topAppBarMode.value = ListTopAppBarMode.ADD_ENTRY - getActiveViewModel().listSettings.saveToPrefs(getActiveViewModel().prefs) + listViewModel.listSettings.topAppBarMode.value = ListTopAppBarMode.ADD_ENTRY + listViewModel.listSettings.saveToPrefs(listViewModel.prefs) topBarMenuExpanded = false } - getActiveViewModel().listSettings.searchText.value = null + listViewModel.listSettings.searchText.value = null }, trailingIcon = { IconButton(onClick = { @@ -480,19 +465,19 @@ fun ListScreenTabContainer( ) HorizontalDivider() - AnimatedVisibility(getActiveViewModel().listSettings.topAppBarMode.value == ListTopAppBarMode.SEARCH - && (getActiveViewModel().listSettings.viewMode.value == ViewMode.LIST || getActiveViewModel().listSettings.viewMode.value == ViewMode.COMPACT) + AnimatedVisibility(listViewModel.listSettings.topAppBarMode.value == ListTopAppBarMode.SEARCH + && (listViewModel.listSettings.viewMode.value == ViewMode.LIST || listViewModel.listSettings.viewMode.value == ViewMode.COMPACT) ) { CheckboxWithText( text = stringResource(R.string.list_show_only_search_matching_subentries), - isSelected = getActiveViewModel().listSettings.showOnlySearchMatchingSubentries.value, + isSelected = listViewModel.listSettings.showOnlySearchMatchingSubentries.value, onCheckedChange = { - getActiveViewModel().listSettings.showOnlySearchMatchingSubentries.value = it - getActiveViewModel().updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) + listViewModel.listSettings.showOnlySearchMatchingSubentries.value = it + listViewModel.updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) } ) } - AnimatedVisibility(getActiveViewModel().listSettings.topAppBarMode.value == ListTopAppBarMode.SEARCH) { + AnimatedVisibility(listViewModel.listSettings.topAppBarMode.value == ListTopAppBarMode.SEARCH) { HorizontalDivider() } @@ -507,7 +492,7 @@ fun ListScreenTabContainer( }, leadingIcon = { Icon(Icons.Outlined.Sync, null) }, onClick = { - getActiveViewModel().syncAccounts() + listViewModel.syncAccounts() topBarMenuExpanded = false } ) @@ -515,26 +500,26 @@ fun ListScreenTabContainer( } ViewMode.entries.forEach { viewMode -> - if(getActiveViewModel().module == Module.NOTE && viewMode == ViewMode.WEEK) + if(listViewModel.module == Module.NOTE && viewMode == ViewMode.WEEK) return@forEach RadiobuttonWithText( text = stringResource(id = viewMode.stringResource), - isSelected = getActiveViewModel().listSettings.viewMode.value == viewMode, + isSelected = listViewModel.listSettings.viewMode.value == viewMode, hasSettings = viewMode == ViewMode.KANBAN, onClick = { if ((!isProPurchased.value)) { Toast.makeText(context, R.string.buypro_snackbar_please_purchase_pro, Toast.LENGTH_LONG).show() } else { - getActiveViewModel().listSettings.viewMode.value = viewMode - getActiveViewModel().updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) + listViewModel.listSettings.viewMode.value = viewMode + listViewModel.updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) } topBarMenuExpanded = false }, onSettingsClicked = { if(viewMode == ViewMode.KANBAN) { if(isProPurchased.value) { - getActiveViewModel().listSettings.viewMode.value = viewMode + listViewModel.listSettings.viewMode.value = viewMode filterSheetInitialTab = ListOptionsBottomSheetTabs.KANBAN_SETTINGS scope.launch { filterSheetState.show() } } else { @@ -549,29 +534,29 @@ fun ListScreenTabContainer( CheckboxWithText( text = stringResource(R.string.menu_list_flat_view), subtext = stringResource(R.string.menu_list_flat_view_sub), - isSelected = getActiveViewModel().listSettings.flatView.value, + isSelected = listViewModel.listSettings.flatView.value, onCheckedChange = { - getActiveViewModel().listSettings.flatView.value = it - getActiveViewModel().updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) + listViewModel.listSettings.flatView.value = it + listViewModel.updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) } ) HorizontalDivider() CheckboxWithText( text = stringResource(R.string.menu_list_limit_recur_entries), subtext = stringResource(R.string.menu_list_limit_recur_entries_sub), - isSelected = getActiveViewModel().listSettings.showOneRecurEntryInFuture.value, + isSelected = listViewModel.listSettings.showOneRecurEntryInFuture.value, onCheckedChange = { - getActiveViewModel().listSettings.showOneRecurEntryInFuture.value = it - getActiveViewModel().updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) + listViewModel.listSettings.showOneRecurEntryInFuture.value = it + listViewModel.updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) } ) HorizontalDivider() CheckboxWithText( text = stringResource(R.string.menu_view_markdown_formatting), - isSelected = getActiveViewModel().listSettings.markdownEnabled.value, + isSelected = listViewModel.listSettings.markdownEnabled.value, onCheckedChange = { - getActiveViewModel().listSettings.markdownEnabled.value = it - getActiveViewModel().updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) + listViewModel.listSettings.markdownEnabled.value = it + listViewModel.updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value) } ) } @@ -622,7 +607,6 @@ fun ListScreenTabContainer( }, showQuickEntry = showQuickAdd, incompatibleSyncApps = SyncUtil.availableSyncApps(context).filter { !SyncUtil.isSyncAppCompatible(it, context) }, - multiselectEnabled = listViewModel.multiselectEnabled, selectedEntries = listViewModel.selectedEntries, listSettings = listViewModel.listSettings, isBiometricsEnabled = settingsStateHolder.settingProtectBiometric.value != DropdownSettingOption.PROTECT_BIOMETRIC_OFF, @@ -637,7 +621,7 @@ fun ListScreenTabContainer( } } }, - onGoToDateSelected = { id -> getActiveViewModel().scrollOnceId.postValue(id) }, + onGoToDateSelected = { id -> listViewModel.scrollOnceId.postValue(id) }, onDeleteSelectedClicked = { showDeleteSelectedDialog = true }, onUpdateSelectedClicked = { showUpdateEntriesDialog = true }, onToggleBiometricAuthentication = { @@ -688,7 +672,8 @@ fun ListScreenTabContainer( selected = pagerState.currentPage == enabledTabs.indexOf(enabledTab), onClick = { scope.launch { - pagerState.animateScrollToPage(enabledTabs.indexOf(enabledTab)) + pagerState.scrollToPage(enabledTabs.indexOf(enabledTab)) + //pagerState.animateScrollToPage(enabledTabs.indexOf(enabledTab)) } settingsStateHolder.lastUsedModule.value = enabledTab.module settingsStateHolder.lastUsedModule = settingsStateHolder.lastUsedModule // in order to save @@ -726,9 +711,9 @@ fun ListScreenTabContainer( // origin can be button click or an import through the intent ListQuickAddElement( presetModule = if (showQuickAdd.value) - getActiveViewModel().module // coming from button + listViewModel.module // coming from button else - globalStateHolder.icalFromIntentModule.value ?: getActiveViewModel().module, // coming from intent + globalStateHolder.icalFromIntentModule.value ?: listViewModel.module, // coming from intent enabledModules = enabledTabs.map { it.module }, presetText = globalStateHolder.icalFromIntentString.value ?: quickAddBackupText, // only relevant when coming from intent presetCategories = globalStateHolder.icalFromIntentCategories, // only relevant when coming from intent @@ -781,14 +766,9 @@ fun ListScreenTabContainer( state = pagerState, userScrollEnabled = !filterSheetState.isVisible, verticalAlignment = Alignment.Top - ) { page -> - + ) { ListScreen( - listViewModel = when (enabledTabs[page].module) { - Module.JOURNAL -> icalListViewModelJournals - Module.NOTE -> icalListViewModelNotes - Module.TODO -> icalListViewModelTodos - }, + listViewModel = listViewModel, navController = navController ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt index 11be77004..904b29fb9 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt @@ -77,7 +77,7 @@ fun ListScreenWeek( selectedEntries: SnapshotStateList, scrollOnceId: MutableLiveData, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, - onLongClick: (itemId: Long, list: List) -> Unit, + onLongClick: (itemId: Long, isReadOnly: Boolean) -> Unit, ) { val currentDate = remember { LocalDate.now() } @@ -196,7 +196,7 @@ fun Day( list: List, selectedEntries: SnapshotStateList, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, - onLongClick: (itemId: Long, list: List) -> Unit + onLongClick: (itemId: Long, isReadOnly: Boolean) -> Unit ) { val list4day = list.filter { @@ -254,10 +254,7 @@ fun Day( ) }, onLongClick = { - if (!iCal4ListRel.iCal4List.isReadOnly) - onLongClick( - iCal4ListRel.iCal4List.id, - list4day.map { it.iCal4List }) + onLongClick(iCal4ListRel.iCal4List.id, iCal4ListRel.iCal4List.isReadOnly) } ) .aspectRatio(1f) From 99bc13ddc3cb4f31b67bb0ed1b6082906ee16155 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 16 Jun 2024 08:03:55 +0200 Subject: [PATCH 20/27] removed unused variable --- app/src/main/java/at/techbee/jtx/ui/list/ListViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListViewModel.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListViewModel.kt index f4706dec4..bcb108fde 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListViewModel.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListViewModel.kt @@ -109,7 +109,6 @@ open class ListViewModel(application: Application, val module: Module) : Android var toastMessage = mutableStateOf(null) val selectedEntries = mutableStateListOf() - val multiselectEnabled = mutableStateOf(false) init { // only ad the welcomeEntries on first install and exclude all installs that didn't have this preference before (installed before 1641596400000L = 2022/01/08 From ce603b231b7b06e1af41d21594c98baf59211695 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 16 Jun 2024 08:06:32 +0200 Subject: [PATCH 21/27] fetch count more efficiently --- .../java/at/techbee/jtx/database/ICalDatabaseDao.kt | 12 +++++++----- .../at/techbee/jtx/ui/list/ListScreenTabContainer.kt | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt index 23ae1d74f..39ad35c85 100644 --- a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt +++ b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt @@ -131,7 +131,6 @@ interface ICalDatabaseDao { /** * Retrieve an list of all Collections ([Collection]) that have entries for a given module as a LiveData-List - * @param module (Module.name) for which there are existing entries for a collection * @return a list of [Collection] as LiveData> */ @Transaction @@ -233,12 +232,15 @@ interface ICalDatabaseDao { fun getCount(): Int /** - * Retrieve the number of items in the table of [ICal4List] for a specific module as Int. - * @param + * Retrieve the number of iCalObjects that are not deleted, + * that don't have an RRULE + * and that are not present in related to (meaning they are not sub-entries) + * for a specific module + * @param [module] * @return Int with the total number of [ICal4List] in the table for the given module. */ - @Query("SELECT count(*) FROM $VIEW_NAME_ICAL4LIST WHERE $COLUMN_MODULE = :module AND $VIEW_NAME_ICAL4LIST.isChildOfTodo = 0 AND $VIEW_NAME_ICAL4LIST.isChildOfJournal = 0 AND $VIEW_NAME_ICAL4LIST.isChildOfNote = 0 ") - fun getICal4ListCount(module: String): LiveData + @Query("SELECT count(*) FROM $TABLE_NAME_ICALOBJECT WHERE $COLUMN_MODULE = :module AND $COLUMN_RRULE IS NULL AND $COLUMN_DELETED = 0 AND $TABLE_NAME_ICALOBJECT.$COLUMN_ID NOT IN (SELECT $TABLE_NAME_RELATEDTO.$COLUMN_RELATEDTO_ICALOBJECT_ID FROM $TABLE_NAME_RELATEDTO)") + fun getCount4List(module: String): LiveData /** * Retrieve an [ICalObject] by Id as LiveData diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt index 8966f5548..705ab1382 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt @@ -698,7 +698,7 @@ fun ListScreenTabContainer( storedResources = database.getStoredResources().observeAsState(emptyList()).value, storedListSettings = database.getStoredListSettings(listOf(listViewModel.module.name)).observeAsState(emptyList()).value, numShownEntries = iCal4ListRel.size, - numAllEntries = database.getICal4ListCount(module = listViewModel.module.name).observeAsState(0).value, + numAllEntries = database.getCount4List(module = listViewModel.module.name).observeAsState(0).value, isFilterActive = listViewModel.listSettings.isFilterActive(), isAccessibilityMode = settingsStateHolder.settingAccessibilityMode.value, modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp) From 6636409b5f5329c56d13dc109d1229b1fcf00a6c Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sat, 13 Jul 2024 21:30:13 +0200 Subject: [PATCH 22/27] Attendees done --- .../techbee/jtx/database/ICalDatabaseDao.kt | 22 +- .../jtx/ui/detail/DetailScreenContent.kt | 8 +- .../techbee/jtx/ui/detail/DetailViewModel.kt | 12 + .../jtx/ui/detail/DetailsCardAttendees.kt | 311 +++++----------- .../at/techbee/jtx/ui/detail/DetailsScreen.kt | 1 + .../reusable/dialogs/EditAttendeesDialog.kt | 339 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 468 insertions(+), 226 deletions(-) create mode 100644 app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditAttendeesDialog.kt diff --git a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt index 39ad35c85..c67e014c3 100644 --- a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt +++ b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt @@ -42,6 +42,7 @@ import at.techbee.jtx.database.properties.COLUMN_ATTACHMENT_ICALOBJECT_ID import at.techbee.jtx.database.properties.COLUMN_ATTACHMENT_ID import at.techbee.jtx.database.properties.COLUMN_ATTACHMENT_URI import at.techbee.jtx.database.properties.COLUMN_ATTENDEE_ICALOBJECT_ID +import at.techbee.jtx.database.properties.COLUMN_ATTENDEE_ID import at.techbee.jtx.database.properties.COLUMN_CATEGORY_ICALOBJECT_ID import at.techbee.jtx.database.properties.COLUMN_CATEGORY_TEXT import at.techbee.jtx.database.properties.COLUMN_COMMENT_ICALOBJECT_ID @@ -98,6 +99,14 @@ interface ICalDatabaseDao { @Query("SELECT DISTINCT $COLUMN_RESOURCE_TEXT FROM $TABLE_NAME_RESOURCE WHERE $COLUMN_RESOURCE_ICALOBJECT_ID IN (SELECT $COLUMN_ID FROM $TABLE_NAME_ICALOBJECT WHERE $COLUMN_DELETED = 0) GROUP BY $COLUMN_RESOURCE_TEXT ORDER BY count(*) DESC, $COLUMN_RESOURCE_TEXT ASC") fun getAllResourcesAsText(): LiveData> + /** + * Retrieve an list of all Attendees as a LiveData-List + * @return a list of [Attendee] + */ + @Query("SELECT * FROM $TABLE_NAME_ATTENDEE WHERE $COLUMN_ATTENDEE_ICALOBJECT_ID IN (SELECT $COLUMN_ID FROM $TABLE_NAME_ICALOBJECT WHERE $COLUMN_DELETED = 0) ORDER BY $COLUMN_ATTENDEE_ID DESC") + fun getAllAttendees(): LiveData> + + /** * Retrieve an list of all DISTINCT Colors for ICalObjects as Int in a LiveData object */ @@ -1573,7 +1582,6 @@ iCalObject.percent != 100 @Transaction suspend fun updateResources(iCalObjectId: Long, uid: String, resources: List) { - deleteResources(iCalObjectId) resources.forEach { it.icalObjectId = iCalObjectId } upsertResources(resources) @@ -1584,6 +1592,18 @@ iCalObject.percent != 100 @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertResources(resources: List) + @Transaction + suspend fun updateAttendees(iCalObjectId: Long, uid: String, attendees: List) { + deleteAttendees(iCalObjectId) + attendees.forEach { it.icalObjectId = iCalObjectId } + upsertAttendees(attendees) + updateSetDirty(iCalObjectId) + makeSeriesDirty(uid) + } + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertAttendees(attendees: List) + @Transaction suspend fun saveAll( icalObject: ICalObject, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index 1c8503ca9..d5cc6c32e 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -144,6 +144,7 @@ fun DetailScreenContent( onUnlinkSubEntry: (icalObjectId: Long, parentUID: String?) -> Unit, onCategoriesUpdated: (List) -> Unit, onResourcesUpdated: (List) -> Unit, + onAttendeesUpdated: (List) -> Unit, goToDetail: (itemId: Long, editMode: Boolean, list: List, popBackStack: Boolean) -> Unit, goBack: () -> Unit, goToFilteredList: (StoredListSettingData) -> Unit, @@ -679,9 +680,10 @@ fun DetailScreenContent( if (attendees.isNotEmpty() || detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] == true || showAllOptions) { DetailsCardAttendees( attendees = attendees, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly ?: true, onAttendeesUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + onAttendeesUpdated(it) }, modifier = detailElementModifier ) @@ -1147,6 +1149,7 @@ fun DetailScreenContent_JOURNAL() { onUpdateSortOrder = { }, onCategoriesUpdated = { }, onResourcesUpdated = { }, + onAttendeesUpdated = { }, alarmSetting = DropdownSettingOption.AUTO_ALARM_ON_START ) } @@ -1211,6 +1214,7 @@ fun DetailScreenContent_TODO_editInitially() { onUpdateSortOrder = { }, onCategoriesUpdated = { }, onResourcesUpdated = { }, + onAttendeesUpdated = { }, alarmSetting = DropdownSettingOption.AUTO_ALARM_ON_START ) } @@ -1275,6 +1279,7 @@ fun DetailScreenContent_TODO_editInitially_isChild() { onUpdateSortOrder = { }, onCategoriesUpdated = { }, onResourcesUpdated = { }, + onAttendeesUpdated = { }, alarmSetting = DropdownSettingOption.AUTO_ALARM_ON_START ) } @@ -1333,6 +1338,7 @@ fun DetailScreenContent_failedLoading() { onUpdateSortOrder = { }, onCategoriesUpdated = { }, onResourcesUpdated = { }, + onAttendeesUpdated = { }, alarmSetting = DropdownSettingOption.AUTO_ALARM_ON_START ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt index 5c1fb3e7f..78d047f06 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt @@ -560,6 +560,18 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) } } + fun updateAttendees(attendees: List) { + mutableAttendees.clear() + mutableAttendees.addAll(attendees) + viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { changeState.value = DetailChangeState.LOADING } + val uid = mutableICalObject?.uid!! + databaseDao.updateAttendees(mainICalObjectId!!, uid, attendees) + onChangeDone(updateNotifications = false, updateGeofences = false) + withContext(Dispatchers.Main) { changeState.value = DetailChangeState.CHANGESAVED } + } + } + fun createCopy(newModule: Module) { viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAttendees.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAttendees.kt index 1791e50af..13252aca8 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAttendees.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardAttendees.kt @@ -8,98 +8,85 @@ package at.techbee.jtx.ui.detail -import android.Manifest import android.content.Intent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.relocation.bringIntoViewRequester -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.PersonAdd -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.Group +import androidx.compose.material.icons.outlined.ContactMail +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Groups -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ElevatedAssistChip import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.InputChip import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.techbee.jtx.R +import at.techbee.jtx.database.ICalDatabase import at.techbee.jtx.database.properties.Attendee import at.techbee.jtx.database.properties.Role -import at.techbee.jtx.ui.reusable.dialogs.RequestPermissionDialog +import at.techbee.jtx.ui.reusable.dialogs.EditAttendeesDialog import at.techbee.jtx.ui.reusable.elements.HeadlineWithIcon -import at.techbee.jtx.util.UiUtil -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState -import com.google.accompanist.permissions.shouldShowRationale -import kotlinx.coroutines.launch -@OptIn(ExperimentalFoundationApi::class, - ExperimentalPermissionsApi::class, ExperimentalLayoutApi::class -) @Composable fun DetailsCardAttendees( attendees: SnapshotStateList, - isEditMode: Boolean, - onAttendeesUpdated: () -> Unit, + isReadOnly: Boolean, + onAttendeesUpdated: (List) -> Unit, modifier: Modifier = Modifier ) { val context = LocalContext.current - // preview would break if rememberPermissionState is used for preview, so we set it to null only for preview! - val contactsPermissionState = if (!LocalInspectionMode.current) rememberPermissionState(permission = Manifest.permission.READ_CONTACTS) else null + var showEditAttendeesDialog by rememberSaveable { mutableStateOf(false) } + + if(showEditAttendeesDialog) { + EditAttendeesDialog( + initialAttendees = attendees, + allAttendees = ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getAllAttendees() + .observeAsState(emptyList()).value, + onAttendeesUpdated = onAttendeesUpdated, + onDismiss = { showEditAttendeesDialog = false } + ) + } - val searchAttendees = remember { mutableStateListOf() } + fun launchSendEmailIntent(attendees: List) { - val headline = stringResource(id = R.string.attendees) - var newAttendee by rememberSaveable { mutableStateOf("") } + val emails = attendees + .filter { it.caladdress.startsWith("mailto:") } + .map { it.caladdress.replaceFirst("mailto:", "") } + .toTypedArray() - val bringIntoViewRequester = remember { BringIntoViewRequester() } - val coroutineScope = rememberCoroutineScope() + if(emails.isEmpty()) + return + + val intent = Intent(Intent.ACTION_SEND) + intent.type = "message/rfc822" + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(emails)) + context.startActivity(intent) + } ElevatedCard(modifier = modifier) { Column( @@ -108,196 +95,60 @@ fun DetailsCardAttendees( .padding(8.dp), ) { - HeadlineWithIcon(icon = Icons.Outlined.Groups, iconDesc = headline, text = headline) + HeadlineWithIcon(icon = Icons.Outlined.Groups, iconDesc = null, text = stringResource(id = R.string.attendees)) - AnimatedVisibility(attendees.isNotEmpty()) { - FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + AnimatedVisibility(attendees.isNotEmpty() || !isReadOnly) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { attendees.forEach { attendee -> - val overflowMenuExpanded = remember { mutableStateOf(false) } - - if(!isEditMode) { - ElevatedAssistChip( - onClick = { - if(attendee.caladdress.startsWith("mailto:")) { - val mail = attendee.caladdress.replaceFirst("mailto:", "") - val intent = Intent(Intent.ACTION_SEND) - intent.type = "message/rfc822" - intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(mail)) - context.startActivity(intent) - } - }, - label = { Text(attendee.getDisplayString()) }, - leadingIcon = { - if (Role.entries.any { role -> role.name == attendee.role }) - Role.valueOf(attendee.role ?: Role.`REQ-PARTICIPANT`.name) - .Icon() - else - Role.`REQ-PARTICIPANT`.Icon() - } - ) - } else { - InputChip( - onClick = { overflowMenuExpanded.value = true }, - label = { Text(attendee.getDisplayString()) }, - leadingIcon = { - if (Role.entries.any { role -> role.name == attendee.role }) - Role.valueOf(attendee.role ?: Role.`REQ-PARTICIPANT`.name) - .Icon() - else - Role.`REQ-PARTICIPANT`.Icon() - }, - trailingIcon = { - IconButton( - onClick = { - attendees.remove(attendee) - onAttendeesUpdated() - }, - content = { Icon(Icons.Outlined.Close, stringResource(id = R.string.delete)) }, - modifier = Modifier.size(24.dp) - ) - }, - selected = false - ) - } - - - DropdownMenu( - expanded = overflowMenuExpanded.value, - onDismissRequest = { overflowMenuExpanded.value = false } - ) { - Role.entries.forEach { role -> - DropdownMenuItem( - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - role.Icon() - Text(stringResource(id = role.stringResource)) - } - }, - onClick = { - attendee.role = role.name - onAttendeesUpdated() - overflowMenuExpanded.value = false - }) - } - } - } - } - } - AnimatedVisibility(isEditMode && newAttendee.isNotEmpty()) { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - - val possibleAttendeesToSelect = searchAttendees.filter { all -> - all.getDisplayString().lowercase() - .contains(newAttendee.lowercase()) && attendees.none { existing -> - existing.getDisplayString().lowercase() == all.getDisplayString() - .lowercase() - } - } - items(possibleAttendeesToSelect) { attendee -> - InputChip( - onClick = { - attendees.add(attendee) - onAttendeesUpdated() - newAttendee = "" - coroutineScope.launch { bringIntoViewRequester.bringIntoView() } - }, + ElevatedAssistChip( + onClick = { launchSendEmailIntent(listOf(attendee)) }, label = { Text(attendee.getDisplayString()) }, leadingIcon = { - Icon( - Icons.Default.PersonAdd, - stringResource(id = R.string.add) - ) + if (Role.entries.any { role -> role.name == attendee.role }) + Role.valueOf(attendee.role ?: Role.`REQ-PARTICIPANT`.name) + .Icon() + else + Role.`REQ-PARTICIPANT`.Icon() }, - selected = false, - modifier = Modifier.alpha(0.4f) + modifier = Modifier + .fillMaxWidth() + .heightIn(48.dp) ) } - } - } - - Crossfade(isEditMode, label = "crossfade_attendee_edit") { - if (it) { - - OutlinedTextField( - value = newAttendee, - leadingIcon = { Icon(Icons.Outlined.Group, headline) }, - trailingIcon = { - if (newAttendee.isNotEmpty()) { - IconButton(onClick = { - val newAttendeeObject = if(UiUtil.isValidEmail(newAttendee)) - Attendee(caladdress = "mailto:$newAttendee") - else - Attendee(cn = newAttendee) - attendees.add(newAttendeeObject) - onAttendeesUpdated() - newAttendee = "" - }) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if(!isReadOnly) { + ElevatedAssistChip( + onClick = { showEditAttendeesDialog = true }, + label = { Icon( - Icons.Default.PersonAdd, - stringResource(id = R.string.add) + Icons.Outlined.Edit, + stringResource(id = R.string.edit) ) - } - } - }, - singleLine = true, - label = { Text(headline) }, - onValueChange = { newValue -> - newAttendee = newValue + }, + modifier = Modifier.alpha(0.4f) + ) + } - coroutineScope.launch { - searchAttendees.clear() - if(newValue.length >= 3 && contactsPermissionState?.status?.isGranted == true) - searchAttendees.addAll(UiUtil.getLocalContacts(context, newValue)) - bringIntoViewRequester.bringIntoView() - } - }, - isError = newAttendee.isNotEmpty(), - modifier = Modifier - .fillMaxWidth() - .border(0.dp, Color.Transparent) - .bringIntoViewRequester(bringIntoViewRequester), - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - //if(newAttendee.value.isNotEmpty() && attendees.value.none { existing -> existing.getDisplayString() == newAttendee.value } ) - val newAttendeeObject = if(UiUtil.isValidEmail(newAttendee)) - Attendee(caladdress = "mailto:$newAttendee") - else - Attendee(cn = newAttendee) - attendees.add(newAttendeeObject) - onAttendeesUpdated() - newAttendee = "" - coroutineScope.launch { bringIntoViewRequester.bringIntoView() } - }) - ) - } - } + if(attendees.any { it.caladdress.startsWith("mailto:") }) { + ElevatedAssistChip( + onClick = { + launchSendEmailIntent(attendees) + }, + label = { Icon( + Icons.Outlined.ContactMail, + stringResource(R.string.email_contact) + ) }, + modifier = Modifier.alpha(0.4f) + ) + } + } - Crossfade(isEditMode, label = "crossfade_attendee_info") { - if(it) { - Text( - text = stringResource(id = R.string.details_attendees_processing_info), - style = MaterialTheme.typography.bodySmall, - fontStyle = FontStyle.Italic - ) } } } } - - if(contactsPermissionState?.status?.shouldShowRationale == false && !contactsPermissionState.status.isGranted) { // second part = permission is NOT permanently denied! - RequestPermissionDialog( - text = stringResource(id = R.string.edit_fragment_app_permission_message), - onConfirm = { contactsPermissionState.launchPermissionRequest() } - ) - } } @Preview(showBackground = true) @@ -307,7 +158,7 @@ fun DetailsCardAttendees_Preview() { DetailsCardAttendees( attendees = remember { mutableStateListOf(Attendee(caladdress = "mailto:patrick@techbee.at", cn = "Patrick"), Attendee(caladdress = "mailto:info@techbee.at", cn = "Info")) }, - isEditMode = false, + isReadOnly = false, onAttendeesUpdated = { } ) } @@ -316,11 +167,23 @@ fun DetailsCardAttendees_Preview() { @Preview(showBackground = true) @Composable -fun DetailsCardAttendees_Preview_edit() { +fun DetailsCardAttendees_Preview_readonly() { MaterialTheme { DetailsCardAttendees( attendees = remember { mutableStateListOf(Attendee(caladdress = "mailto:patrick@techbee.at", cn = "Patrick")) }, - isEditMode = true, + isReadOnly = true, + onAttendeesUpdated = { } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun DetailsCardAttendees_Preview_without_caladdress() { + MaterialTheme { + DetailsCardAttendees( + attendees = remember { mutableStateListOf() }, + isReadOnly = true, onAttendeesUpdated = { } ) } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt index 40640c032..4f78dfc84 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsScreen.kt @@ -671,6 +671,7 @@ fun DetailsScreen( onUnlinkSubEntry = { icalObjectId, parentUID -> detailViewModel.unlinkFromParent(icalObjectId, parentUID) }, onCategoriesUpdated = { categories -> detailViewModel.updateCategories(categories) }, onResourcesUpdated = { resources -> detailViewModel.updateResources(resources) }, + onAttendeesUpdated = { attendees -> detailViewModel.updateAttendees(attendees) }, player = detailViewModel.mediaPlayer, goToDetail = { itemId, editMode, list, popBackStack -> if(popBackStack) diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditAttendeesDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditAttendeesDialog.kt new file mode 100644 index 000000000..e1cbd0dd6 --- /dev/null +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/EditAttendeesDialog.kt @@ -0,0 +1,339 @@ +/* + * Copyright (c) Techbee e.U. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.techbee.jtx.ui.reusable.dialogs + +import android.Manifest +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +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.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PersonAdd +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Group +import androidx.compose.material.icons.outlined.Groups +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import at.techbee.jtx.R +import at.techbee.jtx.database.properties.Attendee +import at.techbee.jtx.database.properties.Role +import at.techbee.jtx.util.UiUtil +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale +import kotlinx.coroutines.launch + + +@OptIn(ExperimentalLayoutApi::class, ExperimentalPermissionsApi::class, ExperimentalFoundationApi::class) +@Composable +fun EditAttendeesDialog( + initialAttendees: List, + allAttendees: List, + onAttendeesUpdated: (List) -> Unit, + onDismiss: () -> Unit +) { + + + val context = LocalContext.current + val bringIntoViewRequester = remember { BringIntoViewRequester() } + val focusRequester = remember { FocusRequester() } + val coroutineScope = rememberCoroutineScope() + // preview would break if rememberPermissionState is used for preview, so we set it to null only for preview! + val contactsPermissionState = + if (!LocalInspectionMode.current) rememberPermissionState(permission = Manifest.permission.READ_CONTACTS) else null + val searchAttendees = remember { mutableStateListOf() } + + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + + val currentAttendees = remember { mutableStateListOf().apply { addAll(initialAttendees) } } + var newAttendee by rememberSaveable { mutableStateOf("") } + + fun addAttendee() { + if (newAttendee.isEmpty()) + return + + val newAttendeeObject = if (UiUtil.isValidEmail(newAttendee)) + Attendee(caladdress = "mailto:$newAttendee") + else + Attendee(cn = newAttendee) + currentAttendees.add(newAttendeeObject) + newAttendee = "" + } + + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Outlined.Groups, null) + Text(stringResource(id = R.string.attendees)) + } + }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + AnimatedVisibility(currentAttendees.isNotEmpty()) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + currentAttendees.forEach { attendee -> + + val overflowMenuExpanded = remember { mutableStateOf(false) } + + InputChip( + onClick = { overflowMenuExpanded.value = true }, + label = { Text(attendee.getDisplayString()) }, + leadingIcon = { + if (Role.entries.any { role -> role.name == attendee.role }) + Role.valueOf(attendee.role ?: Role.`REQ-PARTICIPANT`.name) + .Icon() + else + Role.`REQ-PARTICIPANT`.Icon() + }, + trailingIcon = { + IconButton( + onClick = { + currentAttendees.remove(attendee) + }, + content = { + Icon( + Icons.Outlined.Close, + stringResource(id = R.string.delete) + ) + }, + modifier = Modifier.size(24.dp) + ) + }, + selected = false + ) + + DropdownMenu( + expanded = overflowMenuExpanded.value, + onDismissRequest = { overflowMenuExpanded.value = false } + ) { + Role.entries.forEach { role -> + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + role.Icon() + Text(stringResource(id = role.stringResource)) + } + }, + onClick = { + attendee.role = role.name + overflowMenuExpanded.value = false + } + ) + } + } + + } + } + } + + val possibleAttendeesToSelect = mutableListOf().apply { + if (newAttendee.isEmpty()) { + addAll(allAttendees) + } else { + addAll(allAttendees.filter { + (it.cn?.contains(newAttendee) == true || it.caladdress.contains(newAttendee)) + && currentAttendees.none { current -> current.getDisplayString() == it.getDisplayString() } + }) + addAll(searchAttendees.filter { searched -> + searched.getDisplayString().lowercase() + .contains(newAttendee.lowercase()) && currentAttendees.none { current -> + current.getDisplayString().lowercase() == searched.getDisplayString().lowercase() + } + }) + } + } + + AnimatedVisibility(newAttendee.isNotEmpty() && possibleAttendeesToSelect.isNotEmpty()) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(possibleAttendeesToSelect) { attendee -> + InputChip( + onClick = { + currentAttendees.add(attendee) + newAttendee = "" + coroutineScope.launch { bringIntoViewRequester.bringIntoView() } + }, + label = { Text(attendee.getDisplayString()) }, + leadingIcon = { + Icon( + Icons.Default.PersonAdd, + stringResource(id = R.string.add) + ) + }, + selected = false, + modifier = Modifier.alpha(0.4f) + ) + } + } + } + + + + OutlinedTextField( + value = newAttendee, + leadingIcon = { Icon(Icons.Outlined.Group, null) }, + trailingIcon = { + if (newAttendee.isNotEmpty()) { + IconButton(onClick = { + addAttendee() + }) { + Icon( + Icons.Default.PersonAdd, + stringResource(id = R.string.add) + ) + } + } + }, + singleLine = true, + label = { Text(stringResource(id = R.string.attendee)) }, + onValueChange = { newValue -> + newAttendee = newValue + + coroutineScope.launch { + searchAttendees.clear() + if (newValue.length >= 3 && contactsPermissionState?.status?.isGranted == true) + searchAttendees.addAll(UiUtil.getLocalContacts(context, newValue)) + bringIntoViewRequester.bringIntoView() + } + }, + isError = newAttendee.isNotEmpty(), + modifier = Modifier + .fillMaxWidth() + .border(0.dp, Color.Transparent) + .bringIntoViewRequester(bringIntoViewRequester) + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions(onDone = { + //if(newAttendee.value.isNotEmpty() && attendees.value.none { existing -> existing.getDisplayString() == newAttendee.value } ) + addAttendee() + coroutineScope.launch { bringIntoViewRequester.bringIntoView() } + }) + ) + + + //TODO: show only for remote entries! + Text( + text = stringResource(id = R.string.details_attendees_processing_info), + style = MaterialTheme.typography.bodySmall, + fontStyle = FontStyle.Italic + ) + } + + if(contactsPermissionState?.status?.shouldShowRationale == false && !contactsPermissionState.status.isGranted) { // second part = permission is NOT permanently denied! + RequestPermissionDialog( + text = stringResource(id = R.string.edit_fragment_app_permission_message), + onConfirm = { contactsPermissionState.launchPermissionRequest() } + ) + } + }, + confirmButton = { + TextButton( + onClick = { + if (newAttendee.isNotEmpty()) + addAttendee() + onAttendeesUpdated(currentAttendees) + onDismiss() + }, + ) { + Text(stringResource(id = R.string.save)) + } + + }, + dismissButton = { + TextButton( + onClick = { + onDismiss() + } + ) { + Text(stringResource(id = R.string.cancel)) + } + }, + ) +} + +@Preview(showBackground = true) +@Composable +fun EditAttendeesDialog_Preview() { + MaterialTheme { + EditAttendeesDialog( + initialAttendees = listOf(Attendee(cn = "asdf")), + allAttendees = emptyList(), + onAttendeesUpdated = {}, + onDismiss = {} + ) + } +} + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 112187ae3..01c8f104d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -280,6 +280,7 @@ "URL" "Location" "Attendees" + "Attendee" "Resources" "Organizer" "Contact" From ded6cabd493b5b18ff6e34090a1582661e577b97 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 14 Jul 2024 20:03:06 +0200 Subject: [PATCH 23/27] Refactored dates fields --- .../techbee/jtx/database/ICalDatabaseDao.kt | 12 ++ .../jtx/ui/detail/DetailOptionsBottomSheet.kt | 6 +- .../jtx/ui/detail/DetailScreenContent.kt | 143 +++++++++++++++--- .../techbee/jtx/ui/detail/DetailViewModel.kt | 11 ++ .../ui/detail/models/DetailsScreenSection.kt | 9 +- 5 files changed, 157 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt index c67e014c3..e6dc7a03b 100644 --- a/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt +++ b/app/src/main/java/at/techbee/jtx/database/ICalDatabaseDao.kt @@ -1604,6 +1604,18 @@ iCalObject.percent != 100 @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertAttendees(attendees: List) + /* + @Transaction + suspend fun updateDates(iCalObjectId: Long, uid: String, dtstart: Long?, dtstartTimezone: String?, due: Long?, dueTimezone: String?, completed: Long?, completedTimezone: String?) { + setDates(iCalObjectId, dtstart, dtstartTimezone, due, dueTimezone, completed, completedTimezone) + updateSetDirty(iCalObjectId) + makeSeriesDirty(uid) + } + + @Query("UPDATE $TABLE_NAME_ICALOBJECT SET $COLUMN_DTSTART = :dtstart, $COLUMN_DTSTART_TIMEZONE = :dtstartTimezone, $COLUMN_DUE = :due, $COLUMN_DUE_TIMEZONE = :dueTimezone, $COLUMN_COMPLETED = :completed, $COLUMN_COMPLETED_TIMEZONE = :completedTimezone WHERE $COLUMN_ID = :iCalObjectId") + suspend fun setDates(iCalObjectId: Long, dtstart: Long?, dtstartTimezone: String?, due: Long?, dueTimezone: String?, completed: Long?, completedTimezone: String?) + */ + @Transaction suspend fun saveAll( icalObject: ICalObject, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt index 5546314d0..c9724129a 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt @@ -175,7 +175,11 @@ fun DetailOptionsBottomSheet( onClick = { }, enabled = when (setting) { DetailsScreenSection.COLLECTION -> true - DetailsScreenSection.DATES -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] != false + DetailsScreenSection.DATE -> true //TODO + DetailsScreenSection.STARTED -> true //TODO + DetailsScreenSection.DUE -> true //TODO + DetailsScreenSection.COMPLETED -> true //TODO + //DetailsScreenSection.DATES -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] != false DetailsScreenSection.SUMMARY -> true //TODO DetailsScreenSection.DESCRIPTION -> true //TODO DetailsScreenSection.PROGRESS -> module == Module.TODO diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index d5cc6c32e..a5d4fd894 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -9,6 +9,7 @@ package at.techbee.jtx.ui.detail import android.media.MediaPlayer +import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -77,6 +78,7 @@ import at.techbee.jtx.database.ICalCollection import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE import at.techbee.jtx.database.ICalDatabase import at.techbee.jtx.database.ICalObject +import at.techbee.jtx.database.ICalObject.Companion.TZ_ALLDAY import at.techbee.jtx.database.Module import at.techbee.jtx.database.Status import at.techbee.jtx.database.locals.StoredListSettingData @@ -92,10 +94,13 @@ import at.techbee.jtx.database.relations.ICalEntity import at.techbee.jtx.database.views.ICal4List import at.techbee.jtx.flavored.BillingManager import at.techbee.jtx.ui.detail.models.DetailsScreenSection +import at.techbee.jtx.ui.reusable.cards.HorizontalDateCard import at.techbee.jtx.ui.reusable.elements.ProgressElement import at.techbee.jtx.ui.settings.DropdownSettingOption import at.techbee.jtx.util.DateTimeUtils import kotlinx.coroutines.delay +import java.time.Instant +import java.time.ZonedDateTime import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -145,6 +150,7 @@ fun DetailScreenContent( onCategoriesUpdated: (List) -> Unit, onResourcesUpdated: (List) -> Unit, onAttendeesUpdated: (List) -> Unit, + //onUpdateDates: (iCalObjectId: Long, uid: String, dtstart: Long?, dtstartTimezone: String?, due: Long?, dueTimezone: String?, completed: Long?, completedTimezone: String?) -> Unit, goToDetail: (itemId: Long, editMode: Boolean, list: List, popBackStack: Boolean) -> Unit, goBack: () -> Unit, goToFilteredList: (StoredListSettingData) -> Unit, @@ -366,38 +372,130 @@ fun DetailScreenContent( ) } - DetailsScreenSection.DATES -> { - - DetailsCardDates( - icalObject = iCalObject, - isReadOnly = collection?.readonly ?: true, - enableDtstart = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] ?: true || iCalObject.getModuleFromString() == Module.JOURNAL, + //TODO: Berücksichtigen!! + /* + enableDtstart = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] ?: true || iCalObject.getModuleFromString() == Module.JOURNAL, enableDue = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] ?: true, enableCompleted = detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] ?: true, - allowCompletedChange = !(linkProgressToSubtasks && subtasks.value.isNotEmpty()), - onDtstartChanged = { datetime, timezone -> + */ + + DetailsScreenSection.DATE -> { + HorizontalDateCard( + datetime = iCalObject.dtstart, + timezone = iCalObject.dtstartTimezone, + allowNull = false, + dateOnly = false, + isReadOnly = collection?.readonly ?: true, + labelTop = stringResource(id = DetailsScreenSection.DATE.stringRes), + onDateTimeChanged = { datetime, timezone -> iCalObject.dtstart = datetime iCalObject.dtstartTimezone = timezone updateAlarms() changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, - onDueChanged = { datetime, timezone -> - iCalObject.due = datetime - iCalObject.dueTimezone = timezone - updateAlarms() + modifier = detailElementModifier + ) + } + + DetailsScreenSection.STARTED -> { + HorizontalDateCard( + datetime = iCalObject.dtstart, + timezone = iCalObject.dtstartTimezone, + allowNull = true, + dateOnly = false, + isReadOnly = collection?.readonly ?: true, + labelTop = stringResource(id = DetailsScreenSection.STARTED.stringRes), + pickerMaxDate = iCalObject.due?.let { Instant.ofEpochMilli(it).atZone(DateTimeUtils.requireTzId(iCalObject.dueTimezone)) }, + onDateTimeChanged = { datetime, timezone -> + if((iCalObject.due ?: Long.MAX_VALUE) <= (datetime ?: Long.MIN_VALUE)) { + Toast.makeText(context, context.getText(R.string.edit_validation_errors_dialog_due_date_before_dtstart), Toast.LENGTH_LONG).show() + } else { + iCalObject.dtstart = datetime + iCalObject.dtstartTimezone = timezone + updateAlarms() + + if (datetime != null) { + + iCalObject.due?.let { + val dueZoned = + ZonedDateTime.ofInstant( + Instant.ofEpochMilli(it), + DateTimeUtils.requireTzId(iCalObject.dueTimezone) + ) + if ((iCalObject.dueTimezone == TZ_ALLDAY && iCalObject.dtstartTimezone != TZ_ALLDAY)) { + iCalObject.due = + dueZoned.withHour(0).withMinute(0).withZoneSameLocal(DateTimeUtils.requireTzId(timezone)) + .toInstant().toEpochMilli() + iCalObject.dueTimezone = timezone + } else if (iCalObject.dueTimezone != TZ_ALLDAY && iCalObject.dtstartTimezone == TZ_ALLDAY) { + iCalObject.due = + dueZoned.withHour(0).withMinute(0).withZoneSameLocal(DateTimeUtils.requireTzId(timezone)) + .toInstant().toEpochMilli() + iCalObject.dueTimezone = TZ_ALLDAY + } + } + } + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + } + }, + modifier = detailElementModifier + ) + } + + + DetailsScreenSection.DUE -> { + HorizontalDateCard( + datetime = iCalObject.due, + timezone = iCalObject.dueTimezone, + allowNull = true, + dateOnly = false, + isReadOnly = collection?.readonly ?: true, + labelTop = stringResource(id = DetailsScreenSection.DUE.stringRes), + pickerMinDate = iCalObject.dtstart?.let { Instant.ofEpochMilli(it).atZone(DateTimeUtils.requireTzId(iCalObject.dtstartTimezone)) }, + onDateTimeChanged = { datetime, timezone -> + if((datetime ?: Long.MAX_VALUE) <= (iCalObject.dtstart ?: Long.MIN_VALUE)) { + Toast.makeText( + context, + context.getText(R.string.edit_validation_errors_dialog_due_date_before_dtstart), + Toast.LENGTH_LONG + ).show() + } else { + iCalObject.due = datetime + iCalObject.dueTimezone = timezone + } + + if(datetime != null) { + iCalObject.dtstart?.let { + if ((iCalObject.dtstartTimezone == TZ_ALLDAY && iCalObject.dueTimezone != TZ_ALLDAY) || (iCalObject.dtstartTimezone != TZ_ALLDAY && iCalObject.dueTimezone == TZ_ALLDAY)) { + val dtstartZoned = + ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), DateTimeUtils.requireTzId(iCalObject.dtstartTimezone)) + iCalObject.dtstart = + dtstartZoned.withHour(0).withMinute(0).withZoneSameLocal(DateTimeUtils.requireTzId(timezone)) + .toInstant().toEpochMilli() + iCalObject.dtstartTimezone = timezone + } + } + } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, - onCompletedChanged = { datetime, timezone -> + modifier = detailElementModifier + ) + } + + DetailsScreenSection.COMPLETED -> { + HorizontalDateCard( + datetime = iCalObject.completed, + timezone = iCalObject.completedTimezone, + allowNull = true, + dateOnly = false, + isReadOnly = (collection?.readonly ?: true) && !(linkProgressToSubtasks && subtasks.value.isNotEmpty()), + labelTop = stringResource(id = DetailsScreenSection.COMPLETED.stringRes), + pickerMinDate = iCalObject.dtstart?.let { Instant.ofEpochMilli(it).atZone(DateTimeUtils.requireTzId(iCalObject.dtstartTimezone)) }, + onDateTimeChanged = { datetime, timezone -> iCalObject.completed = datetime iCalObject.completedTimezone = timezone - if (keepStatusProgressCompletedInSync) { - if (datetime == null) - iCalObject.setUpdatedProgress(null, true) - else - iCalObject.setUpdatedProgress(100, true) - } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, modifier = detailElementModifier @@ -835,7 +933,11 @@ fun DetailScreenContent( detailSettings.detailSettingOrder.forEach { section -> when (section) { DetailsScreenSection.COLLECTION -> {} // skip - DetailsScreenSection.DATES -> {} //TODO() refactor to show them individually + + DetailsScreenSection.DATE -> {} //TODO() add buttons + DetailsScreenSection.STARTED -> {} //TODO() add buttons + DetailsScreenSection.DUE -> {} //TODO() add buttons + DetailsScreenSection.COMPLETED -> {} //TODO() add buttons DetailsScreenSection.SUMMARY -> { AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUMMARY] != true) { @@ -982,6 +1084,7 @@ fun DetailScreenContent( } } } + } } } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt index 78d047f06..ffdafcfc1 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt @@ -572,6 +572,17 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) } } + /* + fun updateDates(iCalObjectId: Long, uid: String, dtstart: Long?, dtstartTimezone: String?, due: Long?, dueTimezone: String?, completed: Long?, completedTimezone: String?) { + viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { changeState.value = DetailChangeState.LOADING } + databaseDao.updateDates(iCalObjectId, uid, dtstart, dtstartTimezone, due, dueTimezone, completed, completedTimezone) + onChangeDone(updateNotifications = true, updateGeofences = false) + withContext(Dispatchers.Main) { changeState.value = DetailChangeState.CHANGESAVED } + } + } + */ + fun createCopy(newModule: Module) { viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt b/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt index 48fab6761..6b437401c 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt @@ -8,7 +8,10 @@ enum class DetailsScreenSection( @StringRes val stringRes: Int ) { COLLECTION(R.string.collection), - DATES(R.string.date), + DATE(R.string.date), + STARTED(R.string.started), + DUE(R.string.due), + COMPLETED(R.string.completed), SUMMARY(R.string.summary), DESCRIPTION(R.string.description), PROGRESS(R.string.progress), @@ -30,9 +33,9 @@ enum class DetailsScreenSection( companion object { fun entriesFor(module: Module): List { return when(module) { - Module.JOURNAL -> listOf(COLLECTION, DATES, SUMMARY, DESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, RECURRENCE) + Module.JOURNAL -> listOf(COLLECTION, DATE, SUMMARY, DESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, RECURRENCE) Module.NOTE -> listOf(COLLECTION, SUMMARY, DESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS) - Module.TODO -> listOf(COLLECTION, DATES, SUMMARY, DESCRIPTION, PROGRESS, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, RESOURCES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, ALARMS, RECURRENCE) + Module.TODO -> listOf(COLLECTION, STARTED, DUE, COMPLETED, SUMMARY, DESCRIPTION, PROGRESS, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, RESOURCES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, ALARMS, RECURRENCE) } } } From 012ce6be5bbd4200a6c04c64621ffad23ced8210 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Sun, 14 Jul 2024 20:47:37 +0200 Subject: [PATCH 24/27] Refactored status, classification, priority --- .../jtx/ui/detail/DetailOptionsBottomSheet.kt | 13 +- .../jtx/ui/detail/DetailScreenContent.kt | 252 +++++++++++++--- ...DetailsCardStatusClassificationPriority.kt | 276 ------------------ .../ui/detail/models/DetailsScreenSection.kt | 10 +- app/src/main/res/values/strings.xml | 1 - 5 files changed, 224 insertions(+), 328 deletions(-) delete mode 100644 app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt index c9724129a..ec9d1e75b 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailOptionsBottomSheet.kt @@ -175,15 +175,16 @@ fun DetailOptionsBottomSheet( onClick = { }, enabled = when (setting) { DetailsScreenSection.COLLECTION -> true - DetailsScreenSection.DATE -> true //TODO - DetailsScreenSection.STARTED -> true //TODO - DetailsScreenSection.DUE -> true //TODO - DetailsScreenSection.COMPLETED -> true //TODO - //DetailsScreenSection.DATES -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] != false + DetailsScreenSection.DATE -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] != false + DetailsScreenSection.STARTED -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] != false + DetailsScreenSection.DUE -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] != false + DetailsScreenSection.COMPLETED -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] != false DetailsScreenSection.SUMMARY -> true //TODO DetailsScreenSection.DESCRIPTION -> true //TODO DetailsScreenSection.PROGRESS -> module == Module.TODO - DetailsScreenSection.STATUSCLASSIFICATIONPRIORITY -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false || detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false + DetailsScreenSection.STATUS -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false + DetailsScreenSection.CLASSIFICATION -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false + DetailsScreenSection.PRIORITY -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false DetailsScreenSection.CATEGORIES -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != false DetailsScreenSection.PARENTS -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_PARENTS] != false DetailsScreenSection.SUBTASKS -> detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUBTASKS] != false diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index a5d4fd894..bfa462a80 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -29,18 +30,24 @@ import androidx.compose.material.icons.automirrored.outlined.InsertComment import androidx.compose.material.icons.automirrored.outlined.NavigateBefore import androidx.compose.material.icons.automirrored.outlined.NavigateNext import androidx.compose.material.icons.outlined.AlarmAdd +import androidx.compose.material.icons.outlined.AssignmentLate import androidx.compose.material.icons.outlined.AttachFile import androidx.compose.material.icons.outlined.ContactMail import androidx.compose.material.icons.outlined.Description import androidx.compose.material.icons.outlined.EventRepeat +import androidx.compose.material.icons.outlined.GppMaybe import androidx.compose.material.icons.outlined.Groups import androidx.compose.material.icons.outlined.Link import androidx.compose.material.icons.outlined.NewLabel import androidx.compose.material.icons.outlined.Place +import androidx.compose.material.icons.outlined.PublishedWithChanges import androidx.compose.material.icons.outlined.ViewHeadline import androidx.compose.material.icons.outlined.WorkOutline import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedAssistChip import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -65,14 +72,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import at.techbee.jtx.R +import at.techbee.jtx.database.Classification import at.techbee.jtx.database.Component import at.techbee.jtx.database.ICalCollection import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE @@ -328,6 +338,7 @@ fun DetailScreenContent( val detailElementModifier = Modifier .padding(top = 8.dp) .fillMaxWidth() + .heightIn(min = 48.dp) val listState = rememberLazyListState() LaunchedEffect(scrollToSectionState.value) { @@ -556,56 +567,215 @@ fun DetailScreenContent( } } - DetailsScreenSection.STATUSCLASSIFICATIONPRIORITY -> { - if ( - !iCalObject.status.isNullOrEmpty() - || !iCalObject.xstatus.isNullOrEmpty() - || !iCalObject.classification.isNullOrEmpty() - || iCalObject.priority in 1..9 - || detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false - || detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false - || (iCalObject.getModuleFromString() == Module.TODO && detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false) + DetailsScreenSection.STATUS -> { + var statusMenuExpanded by remember { mutableStateOf(false) } + + if(detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != false || showAllOptions - ) { + || !iCalObject.status.isNullOrEmpty() + || !iCalObject.xstatus.isNullOrEmpty()) + { + ElevatedAssistChip( + enabled = !(linkProgressToSubtasks && subtasks.value.isNotEmpty()), + label = { + + Column { + Text( + text = stringResource(id = R.string.status), + style = MaterialTheme.typography.labelMedium + ) - DetailsCardStatusClassificationPriority( - icalObject = iCalObject, - isReadOnly = collection?.readonly ?: true, - enableStatus = detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] ?: true || showAllOptions, - enableClassification = detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] ?: true || showAllOptions, - enablePriority = detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] ?: true || showAllOptions, - allowStatusChange = !(linkProgressToSubtasks && subtasks.value.isNotEmpty()), - extendedStatuses = ICalDatabase - .getInstance(context) - .iCalDatabaseDao() - .getStoredStatuses() - .observeAsState(emptyList()).value, - onStatusChanged = { newStatus -> - if (keepStatusProgressCompletedInSync && iCalObject.getModuleFromString() == Module.TODO) { - when (newStatus) { - Status.IN_PROCESS -> iCalObject.setUpdatedProgress( - if (iCalObject.percent !in 1..99) 1 else iCalObject.percent, - true + Text( + text = if (!iCalObject.xstatus.isNullOrEmpty()) + iCalObject.xstatus!! + else + Status.entries.find { it.status == iCalObject.status }?.stringResource?.let { stringResource(id = it) } + ?: iCalObject.status ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + DropdownMenu( + expanded = statusMenuExpanded, + onDismissRequest = { statusMenuExpanded = false } + ) { + + Status.valuesFor(iCalObject.getModuleFromString()).forEach { status -> + DropdownMenuItem( + text = { Text(stringResource(id = status.stringResource)) }, + onClick = { + iCalObject.status = status.status + iCalObject.xstatus = null + statusMenuExpanded = false + + if (keepStatusProgressCompletedInSync && iCalObject.getModuleFromString() == Module.TODO) { + when (status) { + Status.IN_PROCESS -> iCalObject.setUpdatedProgress( + if (iCalObject.percent !in 1..99) 1 else iCalObject.percent, + true + ) + Status.COMPLETED -> iCalObject.setUpdatedProgress(100, true) + else -> {} + } + } + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + } ) + } + ICalDatabase + .getInstance(context) + .iCalDatabaseDao() + .getStoredStatuses() + .observeAsState(emptyList()).value + .filter { it.module == iCalObject.getModuleFromString() } + .forEach { storedStatus -> + DropdownMenuItem( + text = { Text(storedStatus.xstatus) }, + onClick = { + iCalObject.xstatus = storedStatus.xstatus + iCalObject.status = storedStatus.rfcStatus.status + statusMenuExpanded = false + if (keepStatusProgressCompletedInSync && iCalObject.getModuleFromString() == Module.TODO) { + when (storedStatus.rfcStatus) { + Status.IN_PROCESS -> iCalObject.setUpdatedProgress( + if (iCalObject.percent !in 1..99) 1 else iCalObject.percent, + true + ) + Status.COMPLETED -> iCalObject.setUpdatedProgress(100, true) + else -> {} + } + } + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED } + ) + } + } + }, + leadingIcon = { + Icon( + Icons.Outlined.PublishedWithChanges, + stringResource(id = R.string.status) + ) + }, + onClick = { + if(collection?.readonly == false) + statusMenuExpanded = true + }, + modifier = detailElementModifier + ) + } + } + DetailsScreenSection.CLASSIFICATION -> { + var classificationMenuExpanded by remember { mutableStateOf(false) } - Status.COMPLETED -> iCalObject.setUpdatedProgress(100, true) - else -> {} + if(detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != false + || showAllOptions + || !iCalObject.classification.isNullOrEmpty()) { + ElevatedAssistChip( + label = { + Column { + Text( + text = stringResource(id = R.string.classification), + style = MaterialTheme.typography.labelMedium + ) + Text( + Classification.entries.find { it.classification == iCalObject.classification }?.stringResource?.let { + stringResource( + id = it + ) + } ?: iCalObject.classification ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + DropdownMenu( + expanded = classificationMenuExpanded, + onDismissRequest = { classificationMenuExpanded = false } + ) { + + Classification.entries.forEach { clazzification -> + DropdownMenuItem( + text = { Text(stringResource(id = clazzification.stringResource)) }, + onClick = { + iCalObject.classification = clazzification.classification + classificationMenuExpanded = false + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + } + ) } } - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, - onClassificationChanged = { newClassification -> - iCalObject.classification = newClassification.classification - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + leadingIcon = { + Icon( + Icons.Outlined.GppMaybe, + stringResource(id = R.string.classification) + ) }, - onPriorityChanged = { newPriority -> - iCalObject.priority = newPriority - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + onClick = { + if(collection?.readonly == false) + classificationMenuExpanded = true }, - modifier = detailElementModifier.fillMaxWidth() + modifier = detailElementModifier ) } } + DetailsScreenSection.PRIORITY -> { + var priorityMenuExpanded by remember { mutableStateOf(false) } + val priorityStrings = stringArrayResource(id = R.array.priority) + + if(iCalObject.priority in 1..9 + || (iCalObject.getModuleFromString() == Module.TODO && detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != false) + || showAllOptions) { + + ElevatedAssistChip( + label = { + Column { + Text( + text = stringResource(id = R.string.priority), + style = MaterialTheme.typography.labelMedium + ) + + Text( + if (iCalObject.priority in priorityStrings.indices) + stringArrayResource(id = R.array.priority)[iCalObject.priority ?: 0] + else + stringArrayResource(id = R.array.priority)[0], + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + DropdownMenu( + expanded = priorityMenuExpanded, + onDismissRequest = { priorityMenuExpanded = false } + ) { + stringArrayResource(id = R.array.priority).forEachIndexed { index, prio -> + DropdownMenuItem( + text = { Text(prio) }, + onClick = { + iCalObject.priority = if(index == 0) null else index + priorityMenuExpanded = false + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + } + ) + } + } + }, + leadingIcon = { + Icon( + Icons.Outlined.AssignmentLate, + stringResource(id = R.string.priority) + ) + }, + onClick = { + if(collection?.readonly == false) + priorityMenuExpanded = true + }, + modifier = detailElementModifier + ) + } + } DetailsScreenSection.CATEGORIES -> { if (categories.isNotEmpty() || (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != false || showAllOptions)) { @@ -963,8 +1133,9 @@ fun DetailScreenContent( } DetailsScreenSection.PROGRESS -> {} - - DetailsScreenSection.STATUSCLASSIFICATIONPRIORITY -> {} // TODO refactor first + DetailsScreenSection.STATUS -> {} // TODO + DetailsScreenSection.CLASSIFICATION -> {} // TODO + DetailsScreenSection.PRIORITY -> {} // TODO DetailsScreenSection.CATEGORIES -> { AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != true) { @@ -1084,7 +1255,6 @@ fun DetailScreenContent( } } } - } } } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt deleted file mode 100644 index 146dcb05c..000000000 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (c) Techbee e.U. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package at.techbee.jtx.ui.detail - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AssignmentLate -import androidx.compose.material.icons.outlined.GppMaybe -import androidx.compose.material.icons.outlined.PublishedWithChanges -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ElevatedAssistChip -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -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.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import at.techbee.jtx.R -import at.techbee.jtx.database.Classification -import at.techbee.jtx.database.Component -import at.techbee.jtx.database.ICalObject -import at.techbee.jtx.database.Status -import at.techbee.jtx.database.locals.ExtendedStatus - - -@Composable -fun DetailsCardStatusClassificationPriority( - icalObject: ICalObject, - isReadOnly: Boolean, - enableStatus: Boolean, - enableClassification: Boolean, - enablePriority: Boolean, - allowStatusChange: Boolean, - extendedStatuses: List, - onStatusChanged: (Status) -> Unit, - onClassificationChanged: (Classification) -> Unit, - onPriorityChanged: (Int?) -> Unit, - modifier: Modifier = Modifier -) { - - ElevatedCard(modifier = modifier) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - var statusMenuExpanded by remember { mutableStateOf(false) } - var classificationMenuExpanded by remember { mutableStateOf(false) } - var priorityMenuExpanded by remember { mutableStateOf(false) } - - if(enableStatus || !icalObject.status.isNullOrEmpty() || !icalObject.xstatus.isNullOrEmpty()) { - ElevatedAssistChip( - enabled = allowStatusChange, - label = { - Text( - text = if(!icalObject.xstatus.isNullOrEmpty()) - icalObject.xstatus!! - else - Status.entries.find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - DropdownMenu( - expanded = statusMenuExpanded, - onDismissRequest = { statusMenuExpanded = false } - ) { - - Status.valuesFor(icalObject.getModuleFromString()).forEach { status -> - DropdownMenuItem( - text = { Text(stringResource(id = status.stringResource)) }, - onClick = { - icalObject.status = status.status - icalObject.xstatus = null - statusMenuExpanded = false - onStatusChanged(status) - } - ) - } - extendedStatuses - .filter { it.module == icalObject.getModuleFromString() } - .forEach { storedStatus -> - DropdownMenuItem( - text = { Text(storedStatus.xstatus) }, - onClick = { - icalObject.xstatus = storedStatus.xstatus - icalObject.status = storedStatus.rfcStatus.status - statusMenuExpanded = false - onStatusChanged(storedStatus.rfcStatus) - } - ) - } - } - }, - leadingIcon = { - Icon( - Icons.Outlined.PublishedWithChanges, - stringResource(id = R.string.status) - ) - }, - onClick = { - if(!isReadOnly) - statusMenuExpanded = true - }, - modifier = Modifier.weight(0.33f) - ) - } - - - if(enableClassification || !icalObject.classification.isNullOrEmpty()) { - ElevatedAssistChip( - label = { - Text( - Classification.entries.find { it.classification == icalObject.classification }?.stringResource?.let { stringResource(id = it) }?: icalObject.classification ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - DropdownMenu( - expanded = classificationMenuExpanded, - onDismissRequest = { classificationMenuExpanded = false } - ) { - - Classification.entries.forEach { clazzification -> - DropdownMenuItem( - text = { Text(stringResource(id = clazzification.stringResource)) }, - onClick = { - icalObject.classification = clazzification.classification - classificationMenuExpanded = false - onClassificationChanged(clazzification) - } - ) - } - } - }, - leadingIcon = { - Icon( - Icons.Outlined.GppMaybe, - stringResource(id = R.string.classification) - ) - }, - onClick = { - if(!isReadOnly) - classificationMenuExpanded = true - }, - modifier = Modifier.weight(0.33f) - ) - } - - val priorityStrings = stringArrayResource(id = R.array.priority) - if (icalObject.component == Component.VTODO.name) { - - if(enablePriority || icalObject.priority in 1..9) { - ElevatedAssistChip( - label = { - Text( - if (icalObject.priority in priorityStrings.indices) - stringArrayResource(id = R.array.priority)[icalObject.priority?:0] - else - stringArrayResource(id = R.array.priority)[0], - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - DropdownMenu( - expanded = priorityMenuExpanded, - onDismissRequest = { priorityMenuExpanded = false } - ) { - stringArrayResource(id = R.array.priority).forEachIndexed { index, prio -> - DropdownMenuItem( - text = { Text(prio) }, - onClick = { - icalObject.priority = if(index == 0) null else index - priorityMenuExpanded = false - onPriorityChanged(icalObject.priority) - } - ) - } - } - }, - leadingIcon = { - Icon( - Icons.Outlined.AssignmentLate, - stringResource(id = R.string.priority) - ) - }, - onClick = { - if(!isReadOnly) - priorityMenuExpanded = true - }, - modifier = Modifier.weight(0.33f) - ) - } - } - } - } -} - -@Preview(showBackground = true) -@Composable -fun DetailsCardStatusClassificationPriority_Journal_Preview() { - MaterialTheme { - DetailsCardStatusClassificationPriority( - icalObject = ICalObject.createJournal(), - isReadOnly = false, - enableStatus = false, - enableClassification = false, - enablePriority = false, - allowStatusChange = true, - extendedStatuses = emptyList(), - onStatusChanged = { }, - onClassificationChanged = { }, - onPriorityChanged = { } - ) - } -} - -@Preview(showBackground = true) -@Composable -fun DetailsCardStatusClassificationPriority_Todo_Preview() { - MaterialTheme { - DetailsCardStatusClassificationPriority( - icalObject = ICalObject.createTodo(), - isReadOnly = true, - enableStatus = true, - enableClassification = true, - enablePriority = true, - allowStatusChange = true, - extendedStatuses = emptyList(), - onStatusChanged = { }, - onClassificationChanged = { }, - onPriorityChanged = { } - ) - } -} - -@Preview(showBackground = true) -@Composable -fun DetailsCardStatusClassificationPriority_Todo_Preview2() { - MaterialTheme { - DetailsCardStatusClassificationPriority( - icalObject = ICalObject.createTodo(), - isReadOnly = true, - enableStatus = true, - enableClassification = false, - enablePriority = false, - allowStatusChange = false, - extendedStatuses = emptyList(), - onStatusChanged = { }, - onClassificationChanged = { }, - onPriorityChanged = { } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt b/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt index 6b437401c..cc557dd31 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt @@ -15,7 +15,9 @@ enum class DetailsScreenSection( SUMMARY(R.string.summary), DESCRIPTION(R.string.description), PROGRESS(R.string.progress), - STATUSCLASSIFICATIONPRIORITY(R.string.status_classification_priority), + STATUS(R.string.status), + CLASSIFICATION(R.string.classification), + PRIORITY(R.string.priority), CATEGORIES(R.string.categories), PARENTS(R.string.linked_parents), SUBTASKS(R.string.subtasks), @@ -33,9 +35,9 @@ enum class DetailsScreenSection( companion object { fun entriesFor(module: Module): List { return when(module) { - Module.JOURNAL -> listOf(COLLECTION, DATE, SUMMARY, DESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, RECURRENCE) - Module.NOTE -> listOf(COLLECTION, SUMMARY, DESCRIPTION, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS) - Module.TODO -> listOf(COLLECTION, STARTED, DUE, COMPLETED, SUMMARY, DESCRIPTION, PROGRESS, STATUSCLASSIFICATIONPRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, RESOURCES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, ALARMS, RECURRENCE) + Module.JOURNAL -> listOf(COLLECTION, DATE, SUMMARY, DESCRIPTION, STATUS, CLASSIFICATION, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, RECURRENCE) + Module.NOTE -> listOf(COLLECTION, SUMMARY, DESCRIPTION, STATUS, CLASSIFICATION, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS) + Module.TODO -> listOf(COLLECTION, STARTED, DUE, COMPLETED, SUMMARY, DESCRIPTION, PROGRESS, STATUS, CLASSIFICATION, PRIORITY, CATEGORIES, PARENTS, SUBTASKS, SUBNOTES, RESOURCES, ATTENDEES, CONTACT, URL, LOCATION, COMMENTS, ATTACHMENTS, ALARMS, RECURRENCE) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01c8f104d..f111b7d4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -414,7 +414,6 @@ Thank you!" "Due (month)" "Week %1$d/%2$d" "Summary/Description" - "Status/Classification/Priority" "Timezone" "Not set" "Invert selection" From 73acc8fe385ba3f7ace740497ca345c7d3863714 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Wed, 17 Jul 2024 19:19:39 +0200 Subject: [PATCH 25/27] Put DetailScreenSection Icons into enum --- .../jtx/ui/detail/DetailScreenContent.kt | 147 ++++++++++-------- .../techbee/jtx/ui/detail/DetailSettings.kt | 9 ++ .../ui/detail/models/DetailsScreenSection.kt | 102 ++++++++++++ 3 files changed, 197 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt index bfa462a80..b501b6336 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailScreenContent.kt @@ -26,23 +26,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.InsertComment import androidx.compose.material.icons.automirrored.outlined.NavigateBefore import androidx.compose.material.icons.automirrored.outlined.NavigateNext -import androidx.compose.material.icons.outlined.AlarmAdd -import androidx.compose.material.icons.outlined.AssignmentLate -import androidx.compose.material.icons.outlined.AttachFile -import androidx.compose.material.icons.outlined.ContactMail -import androidx.compose.material.icons.outlined.Description -import androidx.compose.material.icons.outlined.EventRepeat -import androidx.compose.material.icons.outlined.GppMaybe -import androidx.compose.material.icons.outlined.Groups -import androidx.compose.material.icons.outlined.Link -import androidx.compose.material.icons.outlined.NewLabel -import androidx.compose.material.icons.outlined.Place -import androidx.compose.material.icons.outlined.PublishedWithChanges -import androidx.compose.material.icons.outlined.ViewHeadline -import androidx.compose.material.icons.outlined.WorkOutline import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu @@ -651,12 +636,7 @@ fun DetailScreenContent( } } }, - leadingIcon = { - Icon( - Icons.Outlined.PublishedWithChanges, - stringResource(id = R.string.status) - ) - }, + leadingIcon = { DetailsScreenSection.STATUS.Icon() }, onClick = { if(collection?.readonly == false) statusMenuExpanded = true @@ -706,12 +686,7 @@ fun DetailScreenContent( } } }, - leadingIcon = { - Icon( - Icons.Outlined.GppMaybe, - stringResource(id = R.string.classification) - ) - }, + leadingIcon = { DetailsScreenSection.CLASSIFICATION.Icon() }, onClick = { if(collection?.readonly == false) classificationMenuExpanded = true @@ -762,12 +737,7 @@ fun DetailScreenContent( } } }, - leadingIcon = { - Icon( - Icons.Outlined.AssignmentLate, - stringResource(id = R.string.priority) - ) - }, + leadingIcon = { DetailsScreenSection.PRIORITY.Icon() }, onClick = { if(collection?.readonly == false) priorityMenuExpanded = true @@ -1102,12 +1072,48 @@ fun DetailScreenContent( ) { detailSettings.detailSettingOrder.forEach { section -> when (section) { - DetailsScreenSection.COLLECTION -> {} // skip + DetailsScreenSection.COLLECTION -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_COLLECTION] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_COLLECTION] = true + scrollToSectionState.value = DetailsScreenSection.COLLECTION + }) { + DetailsScreenSection.COLLECTION.Icon() + } + } + } - DetailsScreenSection.DATE -> {} //TODO() add buttons - DetailsScreenSection.STARTED -> {} //TODO() add buttons - DetailsScreenSection.DUE -> {} //TODO() add buttons - DetailsScreenSection.COMPLETED -> {} //TODO() add buttons + DetailsScreenSection.DATE -> {} // cannot be hidden by user + DetailsScreenSection.STARTED -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] = true + scrollToSectionState.value = DetailsScreenSection.STARTED + }) { + DetailsScreenSection.STARTED.Icon() + } + } + } + DetailsScreenSection.DUE -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] = true + scrollToSectionState.value = DetailsScreenSection.DUE + }) { + DetailsScreenSection.DUE.Icon() + } + } + } + DetailsScreenSection.COMPLETED -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMPLETED] = true + scrollToSectionState.value = DetailsScreenSection.COMPLETED + }) { + DetailsScreenSection.COMPLETED.Icon() + } + } + } DetailsScreenSection.SUMMARY -> { AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUMMARY] != true) { @@ -1115,27 +1121,53 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUMMARY] = true scrollToSectionState.value = DetailsScreenSection.SUMMARY }) { - Icon(Icons.Outlined.ViewHeadline, stringResource(id = R.string.summary)) + DetailsScreenSection.SUMMARY.Icon() } } } - DetailsScreenSection.DESCRIPTION -> { AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_DESCRIPTION] != true) { IconButton(onClick = { detailSettings.detailSetting[DetailSettingsOption.ENABLE_DESCRIPTION] = true scrollToSectionState.value = DetailsScreenSection.DESCRIPTION }) { - Icon(Icons.Outlined.Description, stringResource(id = R.string.description)) + DetailsScreenSection.DESCRIPTION.Icon() } } } DetailsScreenSection.PROGRESS -> {} - DetailsScreenSection.STATUS -> {} // TODO - DetailsScreenSection.CLASSIFICATION -> {} // TODO - DetailsScreenSection.PRIORITY -> {} // TODO + DetailsScreenSection.STATUS -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_STATUS] = true + scrollToSectionState.value = DetailsScreenSection.STATUS + }) { + DetailsScreenSection.STATUS.Icon() + } + } + } + DetailsScreenSection.CLASSIFICATION -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_CLASSIFICATION] = true + scrollToSectionState.value = DetailsScreenSection.CLASSIFICATION + }) { + DetailsScreenSection.PRIORITY.Icon() + } + } + } + DetailsScreenSection.PRIORITY -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_PRIORITY] = true + scrollToSectionState.value = DetailsScreenSection.PRIORITY + }) { + DetailsScreenSection.PRIORITY.Icon() + } + } + } DetailsScreenSection.CATEGORIES -> { AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] != true) { @@ -1143,7 +1175,7 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] = true scrollToSectionState.value = DetailsScreenSection.CATEGORIES }) { - Icon(Icons.Outlined.NewLabel, stringResource(id = R.string.categories)) + DetailsScreenSection.CATEGORIES.Icon() } } } @@ -1158,7 +1190,7 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_RESOURCES] = true scrollToSectionState.value = DetailsScreenSection.RESOURCES }) { - Icon(Icons.Outlined.WorkOutline, stringResource(id = R.string.resources)) + DetailsScreenSection.RESOURCES.Icon() } } } @@ -1169,19 +1201,18 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] = true scrollToSectionState.value = DetailsScreenSection.ATTENDEES }) { - Icon(Icons.Outlined.Groups, stringResource(id = R.string.attendees)) + DetailsScreenSection.ATTENDEES.Icon() } } } - DetailsScreenSection.CONTACT -> { AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] != true) { IconButton(onClick = { detailSettings.detailSetting[DetailSettingsOption.ENABLE_CONTACT] = true scrollToSectionState.value = DetailsScreenSection.CONTACT }) { - Icon(Icons.Outlined.ContactMail, stringResource(id = R.string.contact)) + DetailsScreenSection.CONTACT.Icon() } } } @@ -1192,7 +1223,7 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] = true scrollToSectionState.value = DetailsScreenSection.URL }) { - Icon(Icons.Outlined.Link, stringResource(id = R.string.url)) + DetailsScreenSection.URL.Icon() } } } @@ -1203,7 +1234,7 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] = true scrollToSectionState.value = DetailsScreenSection.LOCATION }) { - Icon(Icons.Outlined.Place, stringResource(id = R.string.location)) + DetailsScreenSection.LOCATION.Icon() } } } @@ -1213,10 +1244,7 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] = true scrollToSectionState.value = DetailsScreenSection.COMMENTS }) { - Icon( - Icons.AutoMirrored.Outlined.InsertComment, - stringResource(id = R.string.comments) - ) + DetailsScreenSection.COMMENTS.Icon() } } } @@ -1227,10 +1255,7 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] = true scrollToSectionState.value = DetailsScreenSection.ATTACHMENTS }) { - Icon( - Icons.Outlined.AttachFile, - stringResource(id = R.string.attachments) - ) + DetailsScreenSection.ATTACHMENTS.Icon() } } } @@ -1239,7 +1264,7 @@ fun DetailScreenContent( IconButton(onClick = { detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] = true }) { - Icon(Icons.Outlined.AlarmAdd, stringResource(id = R.string.alarms)) + DetailsScreenSection.ALARMS.Icon() scrollToSectionState.value = DetailsScreenSection.ALARMS } } @@ -1251,7 +1276,7 @@ fun DetailScreenContent( detailSettings.detailSetting[DetailSettingsOption.ENABLE_RECURRENCE] = true scrollToSectionState.value = DetailsScreenSection.RECURRENCE }) { - Icon(Icons.Outlined.EventRepeat, stringResource(id = R.string.recurrence)) + DetailsScreenSection.RECURRENCE.Icon() } } } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailSettings.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailSettings.kt index 527642617..d96dcd5e3 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailSettings.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailSettings.kt @@ -35,6 +35,15 @@ enum class DetailSettingsOption( val possibleFor: List ) { + ENABLE_COLLECTION( + key = "enableCollection", + stringResource = R.string.collection, + group = DetailSettingsOptionGroup.ELEMENT, + defaultForJournals = true, + defaultForNotes = false, + defaultForTasks = true, + possibleFor = listOf(Module.JOURNAL, Module.NOTE, Module.TODO) + ), ENABLE_SUMMARY( key = "enableSummary", stringResource = R.string.summary, diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt b/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt index cc557dd31..12b293b53 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/models/DetailsScreenSection.kt @@ -1,6 +1,30 @@ package at.techbee.jtx.ui.detail.models import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.InsertComment +import androidx.compose.material.icons.outlined.AlarmAdd +import androidx.compose.material.icons.outlined.AssignmentLate +import androidx.compose.material.icons.outlined.AttachFile +import androidx.compose.material.icons.outlined.ContactMail +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.DoneAll +import androidx.compose.material.icons.outlined.EventRepeat +import androidx.compose.material.icons.outlined.FolderOpen +import androidx.compose.material.icons.outlined.GppMaybe +import androidx.compose.material.icons.outlined.Groups +import androidx.compose.material.icons.outlined.Link +import androidx.compose.material.icons.outlined.NewLabel +import androidx.compose.material.icons.outlined.Percent +import androidx.compose.material.icons.outlined.Place +import androidx.compose.material.icons.outlined.PublishedWithChanges +import androidx.compose.material.icons.outlined.Today +import androidx.compose.material.icons.outlined.ViewHeadline +import androidx.compose.material.icons.outlined.WorkOutline +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import at.techbee.jtx.R import at.techbee.jtx.database.Module @@ -41,4 +65,82 @@ enum class DetailsScreenSection( } } } + + @Composable + fun Icon() { + when(this) { + COLLECTION -> { + Icon(Icons.Outlined.FolderOpen, stringResource(id = R.string.collection)) + } + DATE -> { + Icon(Icons.Outlined.Today, stringResource(id = R.string.date)) + } + STARTED -> { + Icon(painterResource(id = R.drawable.ic_widget_start), stringResource(id = R.string.started)) + } + DUE -> { + Icon(painterResource(id = R.drawable.ic_widget_due), stringResource(id = R.string.due)) + } + COMPLETED -> { + Icon(Icons.Outlined.DoneAll, stringResource(id = R.string.completed)) + } + SUMMARY -> { + Icon(Icons.Outlined.ViewHeadline, stringResource(id = R.string.summary)) + } + DESCRIPTION -> { + Icon(Icons.Outlined.Description, stringResource(id = R.string.description)) + } + PROGRESS -> { + Icon(Icons.Outlined.Percent, stringResource(id = R.string.progress)) + } + STATUS -> { + Icon(Icons.Outlined.PublishedWithChanges, stringResource(id = R.string.status)) + } + CLASSIFICATION -> { + Icon(Icons.Outlined.GppMaybe, stringResource(id = R.string.classification)) + } + PRIORITY -> { + Icon(Icons.Outlined.AssignmentLate, stringResource(id = R.string.priority)) + } + CATEGORIES -> { + Icon(Icons.Outlined.NewLabel, stringResource(id = R.string.categories)) + } + PARENTS -> { + //TODO + } + SUBTASKS -> { + //TODO + } + SUBNOTES -> { + //TODO + } + RESOURCES -> { + Icon(Icons.Outlined.WorkOutline, stringResource(id = R.string.resources)) + } + ATTENDEES -> { + Icon(Icons.Outlined.Groups, stringResource(id = R.string.attendees)) + } + CONTACT -> { + Icon(Icons.Outlined.ContactMail, stringResource(id = R.string.contact)) + } + URL -> { + Icon(Icons.Outlined.Link, stringResource(id = R.string.url)) + } + LOCATION -> { + Icon(Icons.Outlined.Place, stringResource(id = R.string.location)) + } + COMMENTS -> { + Icon(Icons.AutoMirrored.Outlined.InsertComment, stringResource(id = R.string.comments)) + } + ATTACHMENTS -> { + Icon(Icons.Outlined.AttachFile, stringResource(id = R.string.attachments)) + } + ALARMS -> { + Icon(Icons.Outlined.AlarmAdd, stringResource(id = R.string.alarms)) + } + RECURRENCE -> { + Icon(Icons.Outlined.EventRepeat, stringResource(id = R.string.recurrence)) + } + } + } } \ No newline at end of file From c32c04b068d28bb087bb1fcc0919d313c222ff30 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:20:17 +0900 Subject: [PATCH 26/27] fixed merge problems --- .../jtx/ui/detail/DetailsCardCollections.kt | 48 +++++++++------- .../techbee/jtx/ui/detail/DetailsCardUrl.kt | 2 +- .../jtx/ui/list/ListQuickAddElement.kt | 4 +- .../dialogs/CollectionSelectorDialog.kt | 4 +- .../CollectionsMoveCollectionDialog.kt | 4 +- .../reusable/elements/CollectionInfoColumn.kt | 55 +++---------------- .../reusable/elements/CollectionsSpinner.kt | 14 +---- 7 files changed, 39 insertions(+), 92 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt index 1eb53e290..b6112cb55 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt @@ -1,8 +1,8 @@ package at.techbee.jtx.ui.detail +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ColorLens import androidx.compose.material3.Icon @@ -22,13 +22,14 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import at.techbee.jtx.R import at.techbee.jtx.database.ICalCollection +import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE import at.techbee.jtx.database.ICalDatabase import at.techbee.jtx.database.ICalObject import at.techbee.jtx.ui.reusable.dialogs.ColorPickerDialog import at.techbee.jtx.ui.reusable.elements.CollectionsSpinner +import at.techbee.jtx.ui.theme.jtxCardBorderStrokeWidth @Composable @@ -71,25 +72,30 @@ fun DetailsCardCollections( } - CollectionsSpinner( - collections = allPossibleCollections, - preselected = originalCollection, - includeReadOnly = false, - includeVJOURNAL = includeVJOURNAL, - includeVTODO = includeVTODO, - onSelectionChanged = { newCollection -> - if (iCalObject?.collectionId != newCollection.collectionId) { - onMoveToNewCollection(newCollection) + Row(modifier = modifier) { + + CollectionsSpinner( + collections = allPossibleCollections, + preselected = originalCollection, + includeReadOnly = false, + includeVJOURNAL = includeVJOURNAL, + includeVTODO = includeVTODO, + onSelectionChanged = { newCollection -> + if (iCalObject?.collectionId != newCollection.collectionId) { + onMoveToNewCollection(newCollection) + } + }, + showSyncButton = (originalCollection.accountType != LOCAL_ACCOUNT_TYPE + && seriesElement?.dirty ?: iCalObject?.dirty ?: false), + enableSelector = !originalCollection.readonly && !isChild && iCalObject?.recurid.isNullOrEmpty(), + modifier = Modifier.weight(1f), + border = color.value?.let { BorderStroke(jtxCardBorderStrokeWidth, Color(it)) } + ) + + if(!originalCollection.readonly) + IconButton(onClick = { showColorPicker = true }) { + Icon(Icons.Outlined.ColorLens, stringResource(id = R.string.color)) } - }, - enabled = iCalObject?.recurid.isNullOrEmpty(), - modifier = Modifier - .weight(1f) - .padding(4.dp) - ) - - IconButton(onClick = { showColorPicker = true }) { - Icon(Icons.Outlined.ColorLens, stringResource(id = R.string.color)) } } diff --git a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt index bce597e6c..819bc586b 100644 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardUrl.kt @@ -101,7 +101,7 @@ fun DetailsCardUrl( AnimatedVisibility(isValidURL) { IconButton(onClick = { try { - if (url.isNotBlank() && !isEditMode) + if (url.isNotBlank()) uriHandler.openUri(url) } catch (e: ActivityNotFoundException) { Log.d("PropertyCardUrl", "Failed opening Uri $url\n$e") diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt index f79043d6d..8bbfcf3cc 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt @@ -267,9 +267,7 @@ fun ListQuickAddElement( currentModule = Module.NOTE }, showSyncButton = false, - showColorPicker = false, - enableSelector = true, - onColorPicked = { } + enableSelector = true ) OutlinedTextField( diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt index 72f980586..b36ae3595 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt @@ -56,9 +56,7 @@ fun CollectionSelectorDialog( includeVTODO = if(module == Module.TODO) true else null, onSelectionChanged = { selected -> selectedCollection = selected }, showSyncButton = false, - showColorPicker = false, - enableSelector = true, - onColorPicked = { } + enableSelector = true ) } } diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt index 80ca051bb..0c4e83c0f 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt @@ -54,9 +54,7 @@ fun CollectionsMoveCollectionDialog( includeVTODO = if((current.numTodos?:0) > 0) true else null, onSelectionChanged = { selected -> newCollection = selected }, showSyncButton = false, - showColorPicker = false, - enableSelector = true, - onColorPicked = { } + enableSelector = true ) Text(stringResource(id = R.string.collection_dialog_move_info)) diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt index 074dc37d5..6d82f77d5 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt @@ -1,6 +1,5 @@ package at.techbee.jtx.ui.reusable.elements -import android.accounts.Account import android.content.ContentResolver import android.net.Uri import androidx.compose.animation.AnimatedVisibility @@ -17,7 +16,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.CloudSync -import androidx.compose.material.icons.outlined.ColorLens import androidx.compose.material.icons.outlined.EditOff import androidx.compose.material.icons.outlined.Sync import androidx.compose.material3.Icon @@ -28,10 +26,8 @@ import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -41,16 +37,14 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LocalLifecycleOwner import at.techbee.jtx.R import at.techbee.jtx.database.ICalCollection import at.techbee.jtx.database.ICalCollection.Factory.LOCAL_ACCOUNT_TYPE -import at.techbee.jtx.database.ICalDatabase -import at.techbee.jtx.ui.reusable.dialogs.ColorPickerDialog import at.techbee.jtx.util.SyncUtil @@ -58,19 +52,14 @@ import at.techbee.jtx.util.SyncUtil fun CollectionInfoColumn( collection: ICalCollection, showSyncButton: Boolean, - showColorPicker: Boolean, showDropdownArrow: Boolean, - modifier: Modifier = Modifier, - initialColor: Int? = null, - onColorPicked: (Int?) -> Unit + modifier: Modifier = Modifier ) { val context = LocalContext.current val isPreview = LocalInspectionMode.current val lifecycleOwner = LocalLifecycleOwner.current - var showColorPickerDialog by rememberSaveable { mutableStateOf(false) } - val syncIconAnimation = rememberInfiniteTransition(label = "syncIconAnimation") val angle by syncIconAnimation.animateFloat( initialValue = 0f, @@ -89,7 +78,7 @@ fun CollectionInfoColumn( null else { ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE) { - isSyncInProgress = SyncUtil.isJtxSyncRunningFor(setOf(Account(collection.accountName, collection.accountType))) + isSyncInProgress = SyncUtil.isJtxSyncRunningFor(setOf(collection.getAccount())) } } onDispose { @@ -98,24 +87,6 @@ fun CollectionInfoColumn( } } - if (showColorPickerDialog) { - ColorPickerDialog( - initialColor = initialColor, - onColorChanged = { newColor -> - onColorPicked(newColor) - }, - onDismiss = { - showColorPickerDialog = false - }, - additionalColorsInt = ICalDatabase - .getInstance(context) - .iCalDatabaseDao() - .getAllColors() - .observeAsState(initial = emptyList()) - .value - ) - } - Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically @@ -180,7 +151,7 @@ fun CollectionInfoColumn( ) } - AnimatedVisibility(showSyncButton || showColorPicker) { + AnimatedVisibility(showSyncButton) { Row( verticalAlignment = Alignment.CenterVertically @@ -220,12 +191,6 @@ fun CollectionInfoColumn( } } } - - if(showColorPicker) { - IconButton(onClick = { showColorPickerDialog = true }) { - Icon(Icons.Outlined.ColorLens, stringResource(id = R.string.color)) - } - } } } } @@ -247,9 +212,7 @@ fun CollectionInfoColumn_Preview() { CollectionInfoColumn( collection = collection1, showSyncButton = false, - showColorPicker = true, - showDropdownArrow = false, - onColorPicked = { } + showDropdownArrow = false ) } } @@ -271,9 +234,7 @@ fun CollectionInfoColumn_Preview_REMOTE() { CollectionInfoColumn( collection = collection1, showSyncButton = true, - showColorPicker = true, - showDropdownArrow = true, - onColorPicked = {} + showDropdownArrow = true ) } } @@ -295,9 +256,7 @@ fun CollectionInfoColumn_Preview_READONLY() { CollectionInfoColumn( collection = collection1, showSyncButton = false, - showColorPicker = false, - showDropdownArrow = false, - onColorPicked = {} + showDropdownArrow = false ) } } \ No newline at end of file diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionsSpinner.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionsSpinner.kt index 4e1abd250..49c41fcbe 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionsSpinner.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionsSpinner.kt @@ -41,8 +41,6 @@ fun CollectionsSpinner( modifier: Modifier = Modifier, includeReadOnly: Boolean, showSyncButton: Boolean, - showColorPicker: Boolean, - onColorPicked: (Int?) -> Unit, includeVJOURNAL: Boolean? = null, includeVTODO: Boolean? = null, border: BorderStroke? = null, @@ -82,8 +80,6 @@ fun CollectionsSpinner( CollectionInfoColumn( collection = selected, showSyncButton = showSyncButton, - showColorPicker = showColorPicker, - onColorPicked = onColorPicked, showDropdownArrow = !selected.readonly, //modifier = Modifier.alpha(if (!enabled) 0.5f else 1f) ) @@ -118,9 +114,7 @@ fun CollectionsSpinner( CollectionInfoColumn( collection = collection, showSyncButton = false, - showColorPicker = false, - showDropdownArrow = false, - onColorPicked = { } + showDropdownArrow = false ) } ) @@ -168,8 +162,6 @@ fun CollectionsSpinner_Preview() { includeVJOURNAL = true, includeVTODO = true, showSyncButton = true, - showColorPicker = true, - onColorPicked = { }, onSelectionChanged = { }, modifier = Modifier.fillMaxWidth() ) @@ -197,8 +189,6 @@ fun CollectionsSpinner_Preview_notenabled() { includeVJOURNAL = true, includeVTODO = true, showSyncButton = false, - showColorPicker = false, - onColorPicked = { }, onSelectionChanged = { }, modifier = Modifier.fillMaxWidth() ) @@ -227,8 +217,6 @@ fun CollectionsSpinner_Preview_no_color() { includeVJOURNAL = true, includeVTODO = true, showSyncButton = true, - showColorPicker = true, - onColorPicked = { }, onSelectionChanged = { }, modifier = Modifier.fillMaxWidth() ) From 4a26ef64887ae5af3272514be4ba640237209c39 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Wed, 14 May 2025 18:35:50 +0900 Subject: [PATCH 27/27] Fixed merge problem --- .../java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt index 0b672557b..760b21108 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt @@ -206,7 +206,7 @@ fun ListScreenTabContainer( outputStream.write(csvData.joinToString(separator = System.lineSeparator()).toByteArray()) } listViewModel.toastMessage.value = context.getString(R.string.list_toast_export_success) - } catch (e: IOException) { + } catch (_: IOException) { listViewModel.toastMessage.value = context.getString(R.string.list_toast_export_error) } } @@ -763,7 +763,7 @@ fun ListScreenTabContainer( storedResources = database.getStoredResources().observeAsState(emptyList()).value, storedListSettings = database.getStoredListSettings(listOf(listViewModel.module.name)).observeAsState(emptyList()).value, numShownEntries = iCal4ListRel.size, - numAllEntries = database.getCount4List(module = listViewModel.module.name).observeAsState(0).value, + numAllEntries = database.getICal4ListCount(module = listViewModel.module.name).observeAsState(0).value, isFilterActive = listViewModel.listSettings.isFilterActive(), isAccessibilityMode = settingsStateHolder.settingAccessibilityMode.value, modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)