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 a681d7cb3..306bc0bcf 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 */ @@ -126,18 +135,16 @@ 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 - * @param module (Module.name) for which there are existing entries for a collection * @return a list of [Collection] as LiveData> */ @Transaction - @Query("SELECT collection.* " + - "FROM $TABLE_NAME_COLLECTION collection WHERE collection.$COLUMN_COLLECTION_ID IN (SELECT ical.$COLUMN_ICALOBJECT_COLLECTIONID FROM $TABLE_NAME_ICALOBJECT ical 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> /** * Retrieve an list of all Collections ([Collection]) @@ -242,8 +249,11 @@ 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 ical4list WHERE $COLUMN_MODULE = :module AND ical4list.isChildOfTodo = 0 AND ical4list.isChildOfJournal = 0 AND ical4list.isChildOfNote = 0 ") @@ -1567,6 +1577,57 @@ 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 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 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/DetailBottomAppBar.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailBottomAppBar.kt index 35f5323e5..d8f1b5609 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 @@ -12,10 +12,6 @@ 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 @@ -26,45 +22,27 @@ 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.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.res.painterResource @@ -76,63 +54,22 @@ 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, + changeState: MutableState ) { - 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(collection.getAccount())) - } - } - onDispose { - if (!isPreview) - ContentResolver.removeStatusChangeListener(listener) - } - } BottomAppBar( @@ -146,131 +83,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 - && (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( - 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 || changeState.value == DetailViewModel.DetailChangeState.CHANGESAVED) @@ -311,7 +123,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 ) { @@ -321,7 +133,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() @@ -374,11 +186,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)) @@ -389,13 +205,13 @@ fun DetailBottomAppBar( Icon(Icons.Filled.Edit, stringResource(id = R.string.edit)) } } + */ } } ) } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_View() { @@ -407,23 +223,16 @@ 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 = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGEUNSAVED) } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_edit() { @@ -435,22 +244,15 @@ 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() { @@ -462,22 +264,15 @@ 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 = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_View_readonly() { @@ -489,22 +284,15 @@ 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 = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_View_proOnly() { @@ -516,23 +304,16 @@ 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 = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVED) } ) } } -@OptIn(ExperimentalMaterial3Api::class) @Preview(showBackground = true) @Composable fun DetailBottomAppBar_Preview_View_local() { @@ -546,17 +327,11 @@ 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 = { } + changeState = remember { mutableStateOf(DetailViewModel.DetailChangeState.CHANGESAVING) } ) } } 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..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,10 +175,16 @@ 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.SUMMARYDESCRIPTION -> true + 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 0927bdbbc..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 @@ -9,30 +9,34 @@ 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 +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 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 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 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 -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,38 +53,30 @@ 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.testTag +import androidx.compose.ui.res.stringArrayResource 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.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 +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.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 @@ -93,16 +89,18 @@ 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 com.arnyminerz.markdowntext.MarkdownText import kotlinx.coroutines.delay -import org.apache.commons.lang3.StringUtils +import java.time.Instant +import java.time.ZonedDateTime import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +@OptIn(ExperimentalLayoutApi::class) @Composable fun DetailScreenContent( observedICalObject: State, @@ -120,12 +118,6 @@ fun DetailScreenContent( subnotesLive: LiveData>, parentsLive: LiveData>, isChildLive: LiveData, - allWriteableCollectionsLive: LiveData>, - allCategoriesLive: LiveData>, - allResourcesLive: LiveData>, - storedCategories: List, - storedResources: List, - extendedStatuses: List, detailSettings: DetailSettings, icalObjectIdList: List, seriesInstancesLive: LiveData>, @@ -150,23 +142,28 @@ fun DetailScreenContent( onSubEntryDeleted: (icalObjectId: Long) -> Unit, onSubEntryUpdated: (icalObjectId: Long, newText: String) -> Unit, onUnlinkSubEntry: (icalObjectId: Long, parentUID: String?) -> Unit, + 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, + 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 + 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) { @@ -185,8 +182,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)) } @@ -206,27 +209,28 @@ 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 { - 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) { @@ -238,9 +242,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) } @@ -284,21 +294,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) @@ -312,11 +323,12 @@ fun DetailScreenContent( val detailElementModifier = Modifier .padding(top = 8.dp) .fillMaxWidth() + .heightIn(min = 48.dp) val listState = rememberLazyListState() LaunchedEffect(scrollToSectionState.value) { val sectionIndex = detailSettings.detailSettingOrder.indexOf(scrollToSectionState.value) - if(sectionIndex >= 0) { + if (sectionIndex >= 0) { listState.animateScrollToItem(sectionIndex) scrollToSectionState.value = null } @@ -329,217 +341,193 @@ 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( iCalObject = iCalObject, - isEditMode = isEditMode.value, + seriesElement = seriesElement, isChild = isChild.value, 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, modifier = detailElementModifier ) } - DetailsScreenSection.DATES -> { - DetailsCardDates( - icalObject = iCalObject, - isEditMode = isEditMode.value, - enableDtstart = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DTSTART] ?: true || iCalObject.getModuleFromString() == Module.JOURNAL, - enableDue = detailSettings.detailSetting[DetailSettingsOption.ENABLE_DUE] ?: true, + //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() - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED - }, - onCompletedChanged = { datetime, timezone -> - iCalObject.completed = datetime - iCalObject.completedTimezone = timezone - if (keepStatusProgressCompletedInSync) { - if (datetime == null) - iCalObject.setUpdatedProgress(null, true) - else - iCalObject.setUpdatedProgress(100, true) + 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 } - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, - toggleEditMode = { isEditMode.value = !isEditMode.value }, modifier = detailElementModifier ) } - 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 - ) + 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 (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(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 + }, + modifier = detailElementModifier + ) + } - 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) - ) - } + 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 + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + }, + modifier = detailElementModifier + ) + } - 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! - - 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.SUMMARY -> { + DetailsCardSummary( + initialSummary = iCalObject.summary, + isReadOnly = collection?.readonly ?: true, + focusRequested = scrollToSectionState.value == DetailsScreenSection.SUMMARY, + onSummaryUpdated = { + iCalObject.summary = it + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + }, + modifier = detailElementModifier.testTag("benchmark:DetailSummary") + ) + } - 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.DESCRIPTION -> { + DetailsCardDescription( + initialDescription = iCalObject.description, + isReadOnly = collection?.readonly ?: true, + focusRequested = scrollToSectionState.value == DetailsScreenSection.DESCRIPTION, + onDescriptionUpdated = { + iCalObject.description = it + changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + }, + markdownState = markdownState, + isMarkdownEnabled = detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] != false, + modifier = detailElementModifier + ) } DetailsScreenSection.PROGRESS -> { - if(iCalObject.module == Module.TODO.name) { + if (iCalObject.module == Module.TODO.name) { ElevatedCard(modifier = detailElementModifier.fillMaxWidth()) { ProgressElement( label = null, @@ -554,7 +542,8 @@ fun DetailScreenContent( keepStatusProgressCompletedInSync ) onProgressChanged(itemId, newPercent) - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + changeState.value = + DetailViewModel.DetailChangeState.CHANGEUNSAVED }, showSlider = showProgressForMainTasks, modifier = Modifier.align(Alignment.End) @@ -563,69 +552,223 @@ 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) - ) - ) { + DetailsScreenSection.STATUS -> { + var statusMenuExpanded by remember { mutableStateOf(false) } - DetailsCardStatusClassificationPriority( - icalObject = iCalObject, - isEditMode = isEditMode.value, - 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 = extendedStatuses, - 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 - ) + 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 + ) - Status.COMPLETED -> iCalObject.setUpdatedProgress(100, true) - else -> {} + 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 } + ) + } } - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, - onClassificationChanged = { newClassification -> - iCalObject.classification = newClassification.classification - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + leadingIcon = { DetailsScreenSection.STATUS.Icon() }, + onClick = { + if(collection?.readonly == false) + statusMenuExpanded = true }, - onPriorityChanged = { newPriority -> - iCalObject.priority = newPriority - changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + modifier = detailElementModifier + ) + } + } + DetailsScreenSection.CLASSIFICATION -> { + var classificationMenuExpanded by remember { mutableStateOf(false) } + + 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 + } + ) + } + } + }, + leadingIcon = { DetailsScreenSection.CLASSIFICATION.Icon() }, + 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 = { DetailsScreenSection.PRIORITY.Icon() }, + onClick = { + if(collection?.readonly == false) + priorityMenuExpanded = true + }, + modifier = detailElementModifier + ) + } + } 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 ) } } + 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, @@ -662,8 +805,9 @@ 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, @@ -702,8 +846,9 @@ 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, @@ -748,38 +893,46 @@ 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 ) } } + 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, + isReadOnly = collection?.readonly ?: true, onAttendeesUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED + onAttendeesUpdated(it) }, modifier = detailElementModifier ) } } + 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 ?: "", - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly ?: true, onContactUpdated = { newContact -> iCalObject.contact = newContact.ifEmpty { null } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED @@ -788,11 +941,12 @@ 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 ?: "", - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly ?: true, onUrlUpdated = { newUrl -> iCalObject.url = newUrl.ifEmpty { null } changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED @@ -801,14 +955,15 @@ 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, 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 @@ -825,11 +980,12 @@ 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, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly ?: true, onCommentsUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, @@ -837,11 +993,12 @@ 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, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly ?: true, isRemoteCollection = collection?.accountType != LOCAL_ACCOUNT_TYPE, player = player, onAttachmentsUpdated = { @@ -851,12 +1008,13 @@ 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, - isEditMode = isEditMode.value, + isReadOnly = collection?.readonly ?: true, onAlarmsUpdated = { changeState.value = DetailViewModel.DetailChangeState.CHANGEUNSAVED }, @@ -864,18 +1022,21 @@ fun DetailScreenContent( ) } } + DetailsScreenSection.RECURRENCE -> { - if( + 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, seriesInstances = seriesInstances.value, seriesElement = seriesElement, - isEditMode = isEditMode.value, + 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) { @@ -900,9 +1061,232 @@ fun DetailScreenContent( } } - if(isEditMode.value && !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 -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_COLLECTION] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_COLLECTION] = true + scrollToSectionState.value = DetailsScreenSection.COLLECTION + }) { + DetailsScreenSection.COLLECTION.Icon() + } + } + } + + 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) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_SUMMARY] = true + scrollToSectionState.value = DetailsScreenSection.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 + }) { + DetailsScreenSection.DESCRIPTION.Icon() + } + } + } + + DetailsScreenSection.PROGRESS -> {} + 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) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_CATEGORIES] = true + scrollToSectionState.value = DetailsScreenSection.CATEGORIES + }) { + DetailsScreenSection.CATEGORIES.Icon() + } + } + } + + 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 + }) { + DetailsScreenSection.RESOURCES.Icon() + } + } + } + + DetailsScreenSection.ATTENDEES -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTENDEES] = true + scrollToSectionState.value = DetailsScreenSection.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 + }) { + DetailsScreenSection.CONTACT.Icon() + } + } + } + + DetailsScreenSection.URL -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_URL] = true + scrollToSectionState.value = DetailsScreenSection.URL + }) { + DetailsScreenSection.URL.Icon() + } + } + } + + DetailsScreenSection.LOCATION -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_LOCATION] = true + scrollToSectionState.value = DetailsScreenSection.LOCATION + }) { + DetailsScreenSection.LOCATION.Icon() + } + } + } + DetailsScreenSection.COMMENTS -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_COMMENTS] = true + scrollToSectionState.value = DetailsScreenSection.COMMENTS + }) { + DetailsScreenSection.COMMENTS.Icon() + } + } + } + + DetailsScreenSection.ATTACHMENTS -> { + AnimatedVisibility (detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_ATTACHMENTS] = true + scrollToSectionState.value = DetailsScreenSection.ATTACHMENTS + }) { + DetailsScreenSection.ATTACHMENTS.Icon() + } + } + } + DetailsScreenSection.ALARMS -> { + AnimatedVisibility(detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] != true) { + IconButton(onClick = { + detailSettings.detailSetting[DetailSettingsOption.ENABLE_ALARMS] = true + }) { + DetailsScreenSection.ALARMS.Icon() + 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 + }) { + DetailsScreenSection.RECURRENCE.Icon() + } + } + } + } + } + } + } + + if (!showAllOptions) { + item { TextButton( onClick = { showAllOptions = true }, modifier = detailElementModifier.fillMaxWidth() @@ -912,84 +1296,80 @@ 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) - 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)) + 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) + ) } - 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)) } } } @@ -1019,7 +1399,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() }, @@ -1043,28 +1429,25 @@ fun DetailScreenContent_JOURNAL() { isSubnoteDragAndDropEnabled = true, 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 = { }, onProgressChanged = { _, _ -> }, onMoveToNewCollection = { }, - onAudioSubEntryAdded = { _, _ -> }, - onSubEntryAdded = { _, _ -> }, + onAudioSubEntryAdded = { _, _ -> }, + onSubEntryAdded = { _, _ -> }, onSubEntryDeleted = { }, onSubEntryUpdated = { _, _ -> }, goToDetail = { _, _, _, _ -> }, goBack = { }, unlinkFromSeries = { _, _, _ -> }, - onUnlinkSubEntry = { _, _ -> }, - goToFilteredList = { }, + onUnlinkSubEntry = { _, _ -> }, + goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, onUpdateSortOrder = { }, + onCategoriesUpdated = { }, + onResourcesUpdated = { }, + onAttendeesUpdated = { }, alarmSetting = DropdownSettingOption.AUTO_ALARM_ON_START ) } @@ -1102,12 +1485,6 @@ fun DetailScreenContent_TODO_editInitially() { seriesElement = null, 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, @@ -1122,17 +1499,20 @@ fun DetailScreenContent_TODO_editInitially() { saveEntry = { }, onProgressChanged = { _, _ -> }, onMoveToNewCollection = { }, - onAudioSubEntryAdded = { _, _ -> }, - onSubEntryAdded = { _, _ -> }, + onAudioSubEntryAdded = { _, _ -> }, + onSubEntryAdded = { _, _ -> }, onSubEntryDeleted = { }, onSubEntryUpdated = { _, _ -> }, goToDetail = { _, _, _, _ -> }, goBack = { }, unlinkFromSeries = { _, _, _ -> }, - onUnlinkSubEntry = { _, _ -> }, + onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, onUpdateSortOrder = { }, + onCategoriesUpdated = { }, + onResourcesUpdated = { }, + onAttendeesUpdated = { }, alarmSetting = DropdownSettingOption.AUTO_ALARM_ON_START ) } @@ -1170,12 +1550,6 @@ fun DetailScreenContent_TODO_editInitially_isChild() { seriesElement = null, 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, @@ -1190,17 +1564,20 @@ fun DetailScreenContent_TODO_editInitially_isChild() { saveEntry = { }, onProgressChanged = { _, _ -> }, onMoveToNewCollection = { }, - onAudioSubEntryAdded = { _, _ -> }, - onSubEntryAdded = { _, _ -> }, + onAudioSubEntryAdded = { _, _ -> }, + onSubEntryAdded = { _, _ -> }, onSubEntryDeleted = { }, onSubEntryUpdated = { _, _ -> }, goToDetail = { _, _, _, _ -> }, goBack = { }, unlinkFromSeries = { _, _, _ -> }, - onUnlinkSubEntry = { _, _ -> }, + onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, onUpdateSortOrder = { }, + onCategoriesUpdated = { }, + onResourcesUpdated = { }, + onAttendeesUpdated = { }, alarmSetting = DropdownSettingOption.AUTO_ALARM_ON_START ) } @@ -1232,12 +1609,6 @@ fun DetailScreenContent_failedLoading() { seriesElement = null, 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, @@ -1252,17 +1623,20 @@ fun DetailScreenContent_failedLoading() { saveEntry = { }, onProgressChanged = { _, _ -> }, onMoveToNewCollection = { }, - onAudioSubEntryAdded = { _, _ -> }, - onSubEntryAdded = { _, _ -> }, + onAudioSubEntryAdded = { _, _ -> }, + onSubEntryAdded = { _, _ -> }, onSubEntryDeleted = { }, onSubEntryUpdated = { _, _ -> }, goToDetail = { _, _, _, _ -> }, goBack = { }, unlinkFromSeries = { _, _, _ -> }, - onUnlinkSubEntry = { _, _ -> }, + onUnlinkSubEntry = { _, _ -> }, goToFilteredList = { }, onShowLinkExistingDialog = { _, _ -> }, onUpdateSortOrder = { }, + onCategoriesUpdated = { }, + onResourcesUpdated = { }, + onAttendeesUpdated = { }, alarmSetting = DropdownSettingOption.AUTO_ALARM_ON_START ) } 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/DetailViewModel.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailViewModel.kt index ace6ddc07..311f0cc4c 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,13 +92,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 = MutableLiveData() var selectFromAllList: LiveData> = @@ -244,7 +237,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) NotificationManagerCompat.from(_application).cancel(id.toInt()) databaseDao.setAlarmNotification(id, false) } - onChangeDone() + onChangeDone(updateNotifications = true, updateGeofences = false) withContext(Dispatchers.Main) { changeState.value = DetailChangeState.CHANGESAVED } } } @@ -271,7 +264,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) } } @@ -291,7 +284,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) changeState.value = DetailChangeState.CHANGESAVED } } - onChangeDone() + onChangeDone(updateNotifications = false, updateGeofences = false) } } @@ -306,7 +299,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) parentId = mainICalObjectId!!, childrenIds = newSubEntries.map { it.id } ) - onChangeDone() + onChangeDone(updateNotifications = false, updateGeofences = false) } } @@ -320,7 +313,7 @@ class DetailViewModel(application: Application) : AndroidViewModel(application) parentIds = newParents.map { it.id }, childId = mainICalObjectId!! ) - onChangeDone() + onChangeDone(updateNotifications = false, updateGeofences = false) } } @@ -554,6 +547,53 @@ 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 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 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) { val parentUID = relatedParents.value?.firstOrNull()?.uid @@ -734,11 +774,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/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 57a2462bb..ac9c55ea4 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(it) + } + }) { + 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(it) - } - }) { - 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/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/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/DetailsCardCollections.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardCollections.kt index e754527ad..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,15 +1,10 @@ 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 @@ -21,29 +16,26 @@ 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.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.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, @@ -79,76 +71,31 @@ fun DetailsCardCollections( ) } - 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() - ) - } - } - } - } 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)) + + 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)) } - } } } @@ -163,7 +110,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 +133,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/DetailsCardComments.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardComments.kt index 3189fab9a..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 @@ -9,21 +9,19 @@ 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.Spacer 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.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.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -33,30 +31,40 @@ 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.draw.alpha 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 +74,28 @@ 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 + ) + } 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() @@ -85,42 +103,35 @@ fun DetailsCardComments( onCommentUpdated = { updatedComment -> comment.text = updatedComment.text onCommentsUpdated() - } + }, ) } } } - 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 = "" - }) - ) + 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)) + } + } } } } @@ -136,7 +147,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 +156,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/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/DetailsCardDescription.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt new file mode 100644 index 000000000..76fc9e4fc --- /dev/null +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardDescription.kt @@ -0,0 +1,239 @@ +/* + * 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, + focusRequested: Boolean, + onDescriptionUpdated: (String?) -> Unit, + modifier: Modifier = Modifier +) { + + val focusRequester = remember { FocusRequester() } + var focusRequestedInternally by remember { mutableStateOf(false) } + var isDescriptionFocused by rememberSaveable { mutableStateOf(false) } + var description by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf( + TextFieldValue(initialDescription?:"") + ) } + + LaunchedEffect(focusRequested, focusRequestedInternally, isDescriptionFocused) { + if(focusRequested || focusRequestedInternally) { + try { + focusRequester.requestFocus() + focusRequestedInternally = false + } catch (e: Exception) { + Log.d("DetailsCardDescription", "Requesting Focus failed") + } + } + } + + + ElevatedCard( + onClick = { + if(!isReadOnly) { + focusRequestedInternally = 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 && !focusRequestedInternally && !isDescriptionFocused && isMarkdownEnabled, + label = "descriptionWithMarkdown" + ) { withMarkdown -> + + if(withMarkdown) { + MarkdownText( + markdown = description.text.trim(), + modifier = Modifier.fillMaxWidth(), + style = TextStyle( + textDirection = TextDirection.Content, + fontFamily = LocalTextStyle.current.fontFamily + ), + onClick = { + if (!isReadOnly) { + focusRequestedInternally = 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, + focusRequested = 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 = { }, + focusRequested = false, + markdownState = remember { mutableStateOf(MarkdownState.DISABLED) }, + isMarkdownEnabled = true + ) + } +} + 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 a039f8b41..6fd41b144 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,7 +21,6 @@ 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.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -30,12 +29,11 @@ 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.ArrowDropDown -import androidx.compose.material.icons.outlined.Clear -import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Contacts import androidx.compose.material.icons.outlined.EditLocation import androidx.compose.material.icons.outlined.Map @@ -47,8 +45,8 @@ 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.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -63,6 +61,9 @@ 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 @@ -103,7 +104,7 @@ fun DetailsCardLocation( initialGeoLat: Double?, initialGeoLong: Double?, initialGeofenceRadius: Int?, - isEditMode: Boolean, + isReadOnly: Boolean, onLocationUpdated: (String, Double?, Double?) -> Unit, onGeofenceRadiusUpdatd: (Int?) -> Unit, modifier: Modifier = Modifier @@ -114,6 +115,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) } @@ -215,235 +218,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( @@ -458,8 +379,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( @@ -472,46 +392,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)) } } } @@ -537,6 +455,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) + ) + } } } } @@ -556,7 +489,7 @@ fun DetailsCardLocation_Preview() { initialGeoLat = null, initialGeoLong = null, initialGeofenceRadius = null, - isEditMode = false, + isReadOnly = false, onLocationUpdated = { _, _, _ -> }, onGeofenceRadiusUpdatd = {} ) @@ -572,7 +505,7 @@ fun DetailsCardLocation_Preview_withGeo() { initialGeoLat = 23.447378, initialGeoLong = 73.272838, initialGeofenceRadius = null, - isEditMode = false, + isReadOnly = false, onLocationUpdated = { _, _, _ -> }, onGeofenceRadiusUpdatd = {} ) @@ -588,7 +521,7 @@ fun DetailsCardLocation_Preview_withGeoDE() { initialGeoLat = 23.447378, initialGeoLong = 73.272838, initialGeofenceRadius = null, - isEditMode = false, + isReadOnly = false, onLocationUpdated = { _, _, _ -> }, onGeofenceRadiusUpdatd = {} ) @@ -598,14 +531,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 = {} ) @@ -615,14 +548,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/detail/DetailsCardRecur.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardRecur.kt index 4e5a15479..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 @@ -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,26 @@ 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.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.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,118 +45,34 @@ 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, + focusRequested: Boolean, onRecurUpdated: (Recur?) -> Unit, goToDetail: (itemId: Long, editMode: Boolean, list: List) -> Unit, unlinkFromSeries: (instances: List, series: ICalObject?, deleteAfterUnlink: Boolean) -> Unit, 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 +89,27 @@ 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 } + ) } + LaunchedEffect(focusRequested) { + if(focusRequested) + showRecurDialog = true + } - - ElevatedCard(modifier = modifier) { + ElevatedCard( + onClick = { + if(icalObject.dtstart != null && icalObject.recurid == null && !isReadOnly) + showRecurDialog = true + }, + modifier = modifier + ) { Column( modifier = Modifier @@ -219,365 +124,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 +183,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 +199,7 @@ fun DetailsCardRecur( } } - if(!isEditMode && !icalObject.rrule.isNullOrEmpty()) { + if(seriesInstances.isNotEmpty() && icalObject.recurid == null) { Button( onClick = { showDetachAllFromSeriesDialog = true } ) { @@ -628,87 +208,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 +273,7 @@ fun DetailsCardRecur( .fillMaxWidth() .padding(top = 8.dp, bottom = 4.dp) ) + Column( verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, @@ -806,8 +361,9 @@ fun DetailsCardRecur_Preview() { } ), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -817,7 +373,7 @@ fun DetailsCardRecur_Preview() { @Preview(showBackground = true) @Composable -fun DetailsCardRecur_Preview_edit() { +fun DetailsCardRecur_Preview_read_only2() { MaterialTheme { val recur = Recur @@ -845,8 +401,9 @@ fun DetailsCardRecur_Preview_edit() { } ), seriesElement = null, - isEditMode = true, + isReadOnly = true, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -878,8 +435,9 @@ fun DetailsCardRecur_Preview_unchanged_recur() { } ), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -911,8 +469,9 @@ fun DetailsCardRecur_Preview_changed_recur() { } ), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = true, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -942,8 +501,9 @@ fun DetailsCardRecur_Preview_off() { } ), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -953,7 +513,7 @@ fun DetailsCardRecur_Preview_off() { @Preview(showBackground = true) @Composable -fun DetailsCardRecur_Preview_edit_off() { +fun DetailsCardRecur_Preview_read_only() { MaterialTheme { DetailsCardRecur( @@ -973,8 +533,9 @@ fun DetailsCardRecur_Preview_edit_off() { } ), seriesElement = null, - isEditMode = true, + isReadOnly = true, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } @@ -982,28 +543,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,8 +558,9 @@ fun DetailsCardRecur_Preview_view_no_dtstart() { }, seriesInstances = emptyList(), seriesElement = null, - isEditMode = false, + isReadOnly = false, hasChildren = false, + focusRequested = false, onRecurUpdated = { }, goToDetail = { _, _, _ -> }, unlinkFromSeries = { _, _, _ -> } 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/DetailsCardStatusClassificationPriority.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt deleted file mode 100644 index 4ee063beb..000000000 --- a/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardStatusClassificationPriority.kt +++ /dev/null @@ -1,320 +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.AssistChip -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, - isEditMode: 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(!isEditMode && (icalObject.status?.isNotEmpty() == true || icalObject.xstatus?.isNotEmpty() == true)) { - ElevatedAssistChip( - 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 ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - leadingIcon = { - Icon( - Icons.Outlined.PublishedWithChanges, - stringResource(id = R.string.status) - ) - }, - onClick = { }, - modifier = Modifier.weight(0.33f) - ) - } else if(isEditMode && (enableStatus || !icalObject.status.isNullOrEmpty() || !icalObject.xstatus.isNullOrEmpty())) { - AssistChip( - enabled = allowStatusChange, - label = { - if(!icalObject.xstatus.isNullOrEmpty()) - Text(icalObject.xstatus!!) - else - Text(Status.values().find { it.status == icalObject.status }?.stringResource?.let { stringResource(id = it) }?: icalObject.status ?: "") - - 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 = { statusMenuExpanded = true }, - modifier = Modifier.weight(0.33f) - ) - } - - - 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) - }, - leadingIcon = { - Icon( - Icons.Outlined.GppMaybe, - stringResource(id = R.string.classification) - ) - }, - onClick = { }, - 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 ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - DropdownMenu( - expanded = classificationMenuExpanded, - onDismissRequest = { classificationMenuExpanded = false } - ) { - - Classification.values().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 = { classificationMenuExpanded = true }, - modifier = Modifier.weight(0.33f) - ) - } - - val priorityStrings = stringArrayResource(id = R.array.priority) - if (icalObject.component == Component.VTODO.name) { - - if(!isEditMode && 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 - ) - }, - leadingIcon = { - Icon( - Icons.Outlined.AssignmentLate, - stringResource(id = R.string.priority) - ) - }, - onClick = { }, - 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 = { priorityMenuExpanded = true }, - modifier = Modifier.weight(0.33f) - ) - } - } - } - } -} - -@Preview(showBackground = true) -@Composable -fun DetailsCardStatusClassificationPriority_Journal_Preview() { - MaterialTheme { - DetailsCardStatusClassificationPriority( - icalObject = ICalObject.createJournal(), - isEditMode = 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(), - isEditMode = 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(), - isEditMode = 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/DetailsCardSummary.kt b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt new file mode 100644 index 000000000..e7a3a923d --- /dev/null +++ b/app/src/main/java/at/techbee/jtx/ui/detail/DetailsCardSummary.kt @@ -0,0 +1,125 @@ +/* + * 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.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.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, + focusRequested: Boolean, + onSummaryUpdated: (String?) -> Unit, + modifier: Modifier = Modifier +) { + + val focusRequester = remember { FocusRequester() } + var isSummaryFocused by rememberSaveable { mutableStateOf(false) } + var summary by rememberSaveable { mutableStateOf(initialSummary) } + + LaunchedEffect(focusRequested) { + if(focusRequested && !isReadOnly) + focusRequester.requestFocus() + } + + 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, + focusRequested = false, + onSummaryUpdated = { } + ) + } +} + 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 476e668cc..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 @@ -11,25 +11,30 @@ package at.techbee.jtx.ui.detail import android.content.ActivityNotFoundException import android.util.Log import android.widget.Toast -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.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.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.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource @@ -43,66 +48,74 @@ import at.techbee.jtx.util.UiUtil @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 = UiUtil.isValidURL(url) val uriHandler = LocalUriHandler.current + val focusRequester = remember { FocusRequester() } val context = LocalContext.current - ElevatedCard(modifier = modifier, onClick = { - try { - if (url.isNotBlank() && !isEditMode) - uriHandler.openUri(url) - } catch (e: ActivityNotFoundException) { - Log.d("PropertyCardUrl", "Failed opening Uri $url\n$e") - Toast.makeText(context, e.message?:"", Toast.LENGTH_LONG).show() - } catch (e: IllegalArgumentException) { - Log.d("PropertyCardUrl", "Failed opening Uri $url$e") - Toast.makeText(context, e.message?:"", Toast.LENGTH_LONG).show() - } - }) { - 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(!isReadOnly && url.isNotBlank() && !isValidURL) { + Text( + text = stringResource(id = R.string.invalid_url_message), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error ) } + } + + AnimatedVisibility(isValidURL) { + IconButton(onClick = { + try { + if (url.isNotBlank()) + uriHandler.openUri(url) + } catch (e: ActivityNotFoundException) { + Log.d("PropertyCardUrl", "Failed opening Uri $url\n$e") + Toast.makeText(context, e.message?:"", Toast.LENGTH_LONG).show() + } catch (e: IllegalArgumentException) { + Log.d("PropertyCardUrl", "Failed opening Uri $url$e") + Toast.makeText(context, e.message?:"", Toast.LENGTH_LONG).show() + } + }) { + Icon( + Icons.AutoMirrored.Outlined.OpenInNew, + stringResource(id = R.string.open_in_browser) + ) + } } } } @@ -114,7 +127,7 @@ fun DetailsCardUrl_Preview() { MaterialTheme { DetailsCardUrl( initialUrl = "www.orf.at", - isEditMode = false, + isReadOnly = false, onUrlUpdated = { } ) } @@ -123,11 +136,24 @@ fun DetailsCardUrl_Preview() { @Preview(showBackground = true) @Composable -fun DetailsCardUrl_Preview_edit() { +fun DetailsCardUrl_Preview_emptyUrl() { + MaterialTheme { + DetailsCardUrl( + initialUrl = "", + isReadOnly = false, + onUrlUpdated = { } + ) + } +} + + +@Preview(showBackground = true) +@Composable +fun DetailsCardUrl_Preview_invalid_URL() { MaterialTheme { DetailsCardUrl( - initialUrl = "www.bitfire.at", - isEditMode = true, + initialUrl = "invalid url", + isReadOnly = false, 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 b08c68f79..fb9e0a58c 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 @@ -114,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 } } @@ -326,6 +329,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) { @@ -383,7 +389,14 @@ 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(id = R.string.menu_view_share_mail)) }, onClick = { @@ -398,24 +411,31 @@ fun DetailsScreen( ) } ) + } + 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 - ) - } - ) + 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 = { val currentICalObjectId = detailViewModel.mutableICalObject?.id ?: return@DropdownMenuItem + scope.launch(Dispatchers.IO) { ICalDatabase .getInstance(context) @@ -423,10 +443,17 @@ fun DetailsScreen( .getSync(currentICalObjectId) ?.let { val text = it.getShareText(context) - val clipboardManager = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - clipboardManager.setPrimaryClip(ClipData.newPlainText("", text)) + 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) + detailViewModel.toastMessage.value = + context.getString(R.string.menu_view_copy_to_clipboard_copied) } } menuExpanded.value = false @@ -441,74 +468,163 @@ fun DetailsScreen( ) } - - if (isEditMode.value) { - 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() + CheckboxWithText( - text = stringResource(id = R.string.menu_view_markdown_formatting), + text = stringResource(id = R.string.menu_view_autosave), onCheckedChange = { - detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] = it + detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_AUTOSAVE] = it detailViewModel.detailSettings.save() }, - isSelected = detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_MARKDOWN] ?: true, + isSelected = detailViewModel.detailSettings.detailSetting[DetailSettingsOption.ENABLE_AUTOSAVE] ?: true, ) + HorizontalDivider() - if(collection.value?.readonly == false && collection.value?.supportsVJOURNAL == true) { - if (iCalObject.value?.module != Module.JOURNAL.name) { + if(collection.value?.readonly == false + && isProActionAvailable) { + + 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 + ) + } + ) + } + } + + 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) { + DropdownMenuItem( + leadingIcon = { Icon(Icons.Outlined.Delete, null) }, + text = { Text(stringResource(id = R.string.delete)) }, + onClick = { + showDeleteDialog = true + menuExpanded.value = false + } + ) + + DropdownMenuItem( + leadingIcon = { Icon(painterResource(id = R.drawable.ic_revert), null) }, + text = { Text(stringResource(id = R.string.revert)) }, + onClick = { showRevertDialog = true } + ) + + 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)) } + ) } } ) @@ -531,12 +647,6 @@ fun DetailsScreen( subtasksLive = detailViewModel.relatedSubtasks, 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, @@ -567,6 +677,9 @@ 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) }, + onAttendeesUpdated = { attendees -> detailViewModel.updateAttendees(attendees) }, player = detailViewModel.mediaPlayer, goToDetail = { itemId, editMode, list, popBackStack -> if(popBackStack) @@ -597,17 +710,11 @@ 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 } + changeState = detailViewModel.changeState ) } ) 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..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 @@ -8,10 +32,16 @@ enum class DetailsScreenSection( @StringRes val stringRes: Int ) { COLLECTION(R.string.collection), - DATES(R.string.date), - SUMMARYDESCRIPTION(R.string.summary_description), + 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), - 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), @@ -29,9 +59,87 @@ 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, 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) + } + } + } + + @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)) } } } 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 a2085b929..2cbc49622 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 @@ -65,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 @@ -111,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, @@ -420,29 +417,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 */ }, @@ -481,8 +459,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) } ) ) @@ -516,8 +493,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) }, @@ -554,8 +530,7 @@ fun ListCard( ) }, onLongClick = { - if (!parent.isReadOnly && BillingManager.getInstance().isProPurchased.value == true) - onLongClick(parent.id, parents) + onLongClick(parent.id, parent.isReadOnly) } ) ) @@ -575,8 +550,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 a063715fc..aa75ed2b1 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/ListQuickAddElement.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListQuickAddElement.kt index c6483a44d..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 @@ -265,7 +265,9 @@ fun ListQuickAddElement( currentModule = Module.TODO else if (currentModule == Module.TODO && currentCollection?.supportsVTODO == false) currentModule = Module.NOTE - } + }, + showSyncButton = false, + enableSelector = true ) OutlinedTextField( @@ -355,7 +357,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/list/ListScreen.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt index 64eb5dfd4..089cbfaf8 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 @@ -18,7 +18,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 @@ -47,17 +46,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) { @@ -183,7 +182,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 dd4ad4242..8d421d719 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 @@ -89,7 +89,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, onSaveListSettings: () -> Unit, onUpdateSortOrder: (List) -> Unit ) { @@ -179,52 +179,47 @@ fun ListScreenCompact( } } - ReorderableItem( - reorderableLazyListState, - key = iCal4ListRelObject.iCal4List.id - ) { _ -> - ListCardCompact( - iCal4ListRelObject.iCal4List, - storedCategories = storedCategories, - storedStatuses = storedStatuses, - subtasks = currentSubtasks, - progressUpdateDisabled = settingLinkProgressToSubtasks && currentSubtasks.isNotEmpty(), - selected = selectedEntries, - player = player, - isSubtaskDragAndDropEnabled = isSubtaskDragAndDropEnabled, - dragHandle = { - if(isListDragAndDropEnabled) - DragHandleLazy(this) - }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 4.dp, bottom = 4.dp) - .clip(jtxCardCornerShape) - .combinedClickable( - onClick = { - onClick( - iCal4ListRelObject.iCal4List.id, - groupedList - .flatMap { it.value } - .map { it.iCal4List }, - iCal4ListRelObject.iCal4List.isReadOnly, - ) - }, - onLongClick = { - if (!iCal4ListRelObject.iCal4List.isReadOnly) - onLongClick( + ReorderableItem( + reorderableLazyListState, + key = iCal4ListRelObject.iCal4List.id + ) { _ -> + ListCardCompact( + iCal4ListRelObject.iCal4List, + storedCategories = storedCategories, + storedStatuses = storedStatuses, + subtasks = currentSubtasks, + progressUpdateDisabled = settingLinkProgressToSubtasks && currentSubtasks.isNotEmpty(), + selected = selectedEntries, + player = player, + isSubtaskDragAndDropEnabled = isSubtaskDragAndDropEnabled, + dragHandle = { + if(isListDragAndDropEnabled) + DragHandleLazy(this) + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp, bottom = 4.dp) + .clip(jtxCardCornerShape) + .combinedClickable( + onClick = { + onClick( iCal4ListRelObject.iCal4List.id, groupedList .flatMap { it.value } - .map { it.iCal4List }) - } - ), - onProgressChanged = onProgressChanged, - onClick = onClick, - onLongClick = onLongClick, - onUpdateSortOrder = onUpdateSortOrder - ) - } + .map { it.iCal4List }, + iCal4ListRelObject.iCal4List.isReadOnly, + ) + }, + onLongClick = { + onLongClick(iCal4ListRelObject.iCal4List.id, iCal4ListRelObject.iCal4List.isReadOnly) + } + ), + onProgressChanged = onProgressChanged, + onClick = onClick, + onLongClick = onLongClick, + onUpdateSortOrder = onUpdateSortOrder + ) + } if (iCal4ListRelObject != group.last()) HorizontalDivider( 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 070b51a62..728079f9f 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 @@ -81,7 +81,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 ) { val context = LocalContext.current @@ -131,41 +131,38 @@ fun ListScreenGrid( key = iCal4ListRelObject.iCal4List.id ) { - ListCardGrid( - iCal4ListRelObject.iCal4List, - storedCategories = storedCategories, - storedStatuses = storedStatuses, - selected = selectedEntries.contains(iCal4ListRelObject.iCal4List.id), - progressUpdateDisabled = settingLinkProgressToSubtasks && currentSubtasks.isNotEmpty(), - markdownEnabled = markdownEnabled, - player = player, - modifier = Modifier - .fillMaxWidth() - .clip(jtxCardCornerShape) - .combinedClickable( - onClick = { - onClick( - iCal4ListRelObject.iCal4List.id, - list.map { it.iCal4List }, - iCal4ListRelObject.iCal4List.isReadOnly - ) - }, - onLongClick = { - if (!iCal4ListRelObject.iCal4List.isReadOnly) - onLongClick( + ListCardGrid( + iCal4ListRelObject.iCal4List, + storedCategories = storedCategories, + storedStatuses = storedStatuses, + selected = selectedEntries.contains(iCal4ListRelObject.iCal4List.id), + progressUpdateDisabled = settingLinkProgressToSubtasks && currentSubtasks.isNotEmpty(), + markdownEnabled = markdownEnabled, + player = player, + modifier = Modifier + .fillMaxWidth() + .clip(jtxCardCornerShape) + .combinedClickable( + onClick = { + onClick( iCal4ListRelObject.iCal4List.id, - list.map { it.iCal4List }) - } - ), - onProgressChanged = onProgressChanged, - dragHandle = { - if(isListDragAndDropEnabled) - DragHandleLazy(this) - }, - ) + list.map { it.iCal4List }, + iCal4ListRelObject.iCal4List.isReadOnly + ) + }, + onLongClick = { + onLongClick(iCal4ListRelObject.iCal4List.id, iCal4ListRelObject.iCal4List.isReadOnly) + } + ), + onProgressChanged = onProgressChanged, + dragHandle = { + if(isListDragAndDropEnabled) + DragHandleLazy(this) + }, + ) + } } } - } Crossfade(gridState.canScrollBackward, label = "showScrollUp") { if (it) { @@ -238,7 +235,7 @@ fun ListScreenGrid_TODO() { player = null, onProgressChanged = { _, _ -> }, onClick = { _, _, _ -> }, - onLongClick = { _, _ -> }, + onLongClick = { _, _ -> }, isListDragAndDropEnabled = true ) } @@ -293,7 +290,7 @@ fun ListScreenGrid_JOURNAL() { player = null, onProgressChanged = { _, _ -> }, onClick = { _, _, _ -> }, - onLongClick = { _, _ -> }, + onLongClick = { _, _ -> }, 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 42d75bd6e..714fa43fb 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 @@ -87,7 +87,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 ) { val context = LocalContext.current @@ -175,36 +175,35 @@ fun ListScreenKanban( var offsetX by remember { mutableFloatStateOf(0f) } // see https://developer.android.com/jetpack/compose/gestures val maxOffset = 50f - ListCardKanban( - iCal4ListRelObject.iCal4List, - storedCategories = storedCategories, - storedStatuses = storedStatuses, - selected = selectedEntries.contains(iCal4ListRelObject.iCal4List.id), - markdownEnabled = markdownEnabled, - player = player, - modifier = Modifier - .clip(jtxCardCornerShape) - .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 }) - } - ) - .fillMaxWidth() - .offset { IntOffset(offsetX.roundToInt(), 0) } - .draggable( - orientation = Orientation.Horizontal, - state = rememberDraggableState { delta -> - if (iCal4ListRelObject.iCal4List.isReadOnly) // no drag state for read only objects! - return@rememberDraggableState - if (settingLinkProgressToSubtasks && currentSubtasks.isNotEmpty()) - return@rememberDraggableState // no drag is status depends on subtasks - if (abs(offsetX) <= maxOffset) // once maxOffset is reached, we don't update anymore - offsetX += delta - }, - onDragStopped = { - if (abs(offsetX) > maxOffset / 2 && !iCal4ListRelObject.iCal4List.isReadOnly) { + ListCardKanban( + iCal4ListRelObject.iCal4List, + storedCategories = storedCategories, + storedStatuses = storedStatuses, + selected = selectedEntries.contains(iCal4ListRelObject.iCal4List.id), + markdownEnabled = markdownEnabled, + player = player, + modifier = Modifier + .clip(jtxCardCornerShape) + .combinedClickable( + onClick = { onClick(iCal4ListRelObject.iCal4List.id, list.map { it.iCal4List }, iCal4ListRelObject.iCal4List.isReadOnly) }, + onLongClick = { + onLongClick(iCal4ListRelObject.iCal4List.id, iCal4ListRelObject.iCal4List.isReadOnly) + } + ) + .fillMaxWidth() + .offset { IntOffset(offsetX.roundToInt(), 0) } + .draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { delta -> + if (iCal4ListRelObject.iCal4List.isReadOnly) // no drag state for read only objects! + return@rememberDraggableState + if (settingLinkProgressToSubtasks && currentSubtasks.isNotEmpty()) + return@rememberDraggableState // no drag is status depends on subtasks + if (abs(offsetX) <= maxOffset) // once maxOffset is reached, we don't update anymore + offsetX += delta + }, + onDragStopped = { + if (abs(offsetX) > maxOffset / 2 && !iCal4ListRelObject.iCal4List.isReadOnly) { val draggedToColumn = when { offsetX < 0f && index > 0 -> index - 1 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 9f55fe6d6..b87d530cb 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 @@ -67,7 +67,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 @@ -107,7 +106,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, onSaveListSettings: () -> Unit, @@ -207,72 +206,67 @@ fun ListScreenList( } } - ReorderableItem(reorderableLazyListState, key = iCal4ListRelObject.iCal4List.id) { - ListCard( - iCalObject = iCal4ListRelObject.iCal4List, - categories = iCal4ListRelObject.categories, - resources = iCal4ListRelObject.resources, - subtasks = currentSubtasks, - subnotes = currentSubnotes, - parents = currentParents, - storedCategories = storedCategories, - storedResources = storedResources, - storedStatuses = storedStatuses, - selected = selectedEntries, - attachments = currentAttachments ?: emptyList(), - isSubtasksExpandedDefault = isSubtasksExpandedDefault, - isSubnotesExpandedDefault = isSubnotesExpandedDefault, - isAttachmentsExpandedDefault = isAttachmentsExpandedDefault, - isParentsExpandedDefault = isParentsExpandedDefault, - settingShowProgressMaintasks = settingShowProgressMaintasks, - settingShowProgressSubtasks = settingShowProgressSubtasks, - settingDisplayTimezone = settingDisplayTimezone, - settingIsAccessibilityMode = settingIsAccessibilityMode, - progressIncrement = settingProgressIncrement.getProgressStepKeyAsInt(), - linkProgressToSubtasks = settingLinkProgressToSubtasks, - markdownEnabled = markdownEnabled, - onClick = onClick, - onLongClick = onLongClick, - onProgressChanged = onProgressChanged, - onExpandedChanged = onExpandedChanged, - onUpdateSortOrder = onUpdateSortOrder, - player = player, - isSubtaskDragAndDropEnabled = isSubtaskDragAndDropEnabled, - isSubnoteDragAndDropEnabled = isSubnoteDragAndDropEnabled, - dragHandle = { - if(isListDragAndDropEnabled) - DragHandleLazy(this) - }, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp) - .clip(jtxCardCornerShape) - .combinedClickable( - onClick = { - onClick( - iCal4ListRelObject.iCal4List.id, - groupedList - .flatMap { it.value } - .map { it.iCal4List }, - iCal4ListRelObject.iCal4List.isReadOnly - ) - }, - onLongClick = { - if (!iCal4ListRelObject.iCal4List.isReadOnly && BillingManager.getInstance().isProPurchased.value == true) - onLongClick( + ReorderableItem(reorderableLazyListState, key = iCal4ListRelObject.iCal4List.id) { + ListCard( + iCalObject = iCal4ListRelObject.iCal4List, + categories = iCal4ListRelObject.categories, + resources = iCal4ListRelObject.resources, + subtasks = currentSubtasks, + subnotes = currentSubnotes, + parents = currentParents, + storedCategories = storedCategories, + storedResources = storedResources, + storedStatuses = storedStatuses, + selected = selectedEntries, + attachments = currentAttachments ?: emptyList(), + isSubtasksExpandedDefault = isSubtasksExpandedDefault, + isSubnotesExpandedDefault = isSubnotesExpandedDefault, + isAttachmentsExpandedDefault = isAttachmentsExpandedDefault, + isParentsExpandedDefault = isParentsExpandedDefault, + settingShowProgressMaintasks = settingShowProgressMaintasks, + settingShowProgressSubtasks = settingShowProgressSubtasks, + settingDisplayTimezone = settingDisplayTimezone, + settingIsAccessibilityMode = settingIsAccessibilityMode, + progressIncrement = settingProgressIncrement.getProgressStepKeyAsInt(), + linkProgressToSubtasks = settingLinkProgressToSubtasks, + markdownEnabled = markdownEnabled, + onClick = onClick, + onLongClick = onLongClick, + onProgressChanged = onProgressChanged, + onExpandedChanged = onExpandedChanged, + onUpdateSortOrder = onUpdateSortOrder, + player = player, + isSubtaskDragAndDropEnabled = isSubtaskDragAndDropEnabled, + isSubnoteDragAndDropEnabled = isSubnoteDragAndDropEnabled, + dragHandle = { + if(isListDragAndDropEnabled) + DragHandleLazy(this) + }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + .clip(jtxCardCornerShape) + .combinedClickable( + onClick = { + onClick( iCal4ListRelObject.iCal4List.id, groupedList .flatMap { it.value } - .map { it.iCal4List }) - } - ) - .testTag("benchmark:ListCard") - ) + .map { it.iCal4List }, + iCal4ListRelObject.iCal4List.isReadOnly + ) + }, + onLongClick = { + onLongClick(iCal4ListRelObject.iCal4List.id, iCal4ListRelObject.iCal4List.isReadOnly) + } + ) + .testTag("benchmark:ListCard") + ) + } } } } } - } Crossfade(listState.canScrollBackward, label = "showScrollUp") { if (it) { @@ -365,7 +359,7 @@ fun ListScreenList_TODO() { isSubnoteDragAndDropEnabled = true, onProgressChanged = { _, _ -> }, onClick = { _, _, _ -> }, - onLongClick = { _, _ -> }, + onLongClick = { _, _ -> }, listSettings = listSettings, onExpandedChanged = { _, _, _, _, _ -> }, onSaveListSettings = { }, @@ -448,7 +442,7 @@ fun ListScreenList_JOURNAL() { isSubnoteDragAndDropEnabled = true, onProgressChanged = { _, _ -> }, onClick = { _, _, _ -> }, - onLongClick = { _, _ -> }, + onLongClick = { _, _ -> }, listSettings = listSettings, onExpandedChanged = { _, _, _, _, _ -> }, onSaveListSettings = { }, 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 1a147b24c..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 @@ -164,17 +164,13 @@ 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().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 { @@ -210,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) } } @@ -226,30 +222,19 @@ fun ListScreenTabContainer( val isPullRefreshEnabled = remember { SyncUtil.availableSyncApps(context).any { SyncUtil.isSyncAppCompatible(it, context) } && settingsStateHolder.settingSyncOnPullRefresh.value } - - 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)} @@ -260,50 +245,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().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, - allCollections = database.getAllCollections(module = getActiveViewModel().module.name).observeAsState(emptyList()).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 @@ -319,7 +304,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( @@ -385,7 +370,7 @@ fun ListScreenTabContainer( ) } else null - getActiveViewModel().insertQuickItem( + listViewModel.insertQuickItem( newICalObject, mergedCategories, attachments, @@ -400,7 +385,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, @@ -422,7 +407,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, @@ -462,35 +447,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 = { @@ -499,11 +484,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 = { @@ -516,19 +501,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() } @@ -543,7 +528,7 @@ fun ListScreenTabContainer( }, leadingIcon = { Icon(Icons.Outlined.Sync, null) }, onClick = { - getActiveViewModel().syncAccounts() + listViewModel.syncAccounts() topBarMenuExpanded = false } ) @@ -551,26 +536,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 { @@ -585,29 +570,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) } ) HorizontalDivider() @@ -687,7 +672,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, @@ -702,7 +686,7 @@ fun ListScreenTabContainer( } } }, - onGoToDateSelected = { id -> getActiveViewModel().scrollOnceId.postValue(id) }, + onGoToDateSelected = { id -> listViewModel.scrollOnceId.postValue(id) }, onDeleteSelectedClicked = { showDeleteSelectedDialog = true }, onUpdateSelectedClicked = { showUpdateEntriesDialog = true }, onToggleBiometricAuthentication = { @@ -753,7 +737,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 @@ -791,9 +776,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 @@ -866,21 +851,13 @@ fun ListScreenTabContainer( modifier = Modifier.fillMaxSize() ) { ListScreen( - listViewModel = when (enabledTabs[page].module) { - Module.JOURNAL -> icalListViewModelJournals - Module.NOTE -> icalListViewModelNotes - Module.TODO -> icalListViewModelTodos - }, + listViewModel = listViewModel, navController = navController ) } } else { 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) 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 535ec6d70..1a912e9fd 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 @@ -108,7 +108,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 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 49159da22..84050f851 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,9 +13,7 @@ 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.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn @@ -25,10 +23,7 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.ImageNotSupported -import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -39,6 +34,7 @@ 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 @@ -54,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, @@ -66,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?:"*/*")) { @@ -95,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 @@ -152,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 = { attachment.filename?.let { launcherExportSingle.launch(it) } }) { 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)) + } + } } } } @@ -193,7 +182,7 @@ fun AttachmentCardPreview_view() { MaterialTheme { AttachmentCard( attachment = Attachment.getSample(), - isEditMode = false, + isReadOnly = false, isRemoteCollection = true, player = null, onAttachmentDeleted = { } @@ -207,7 +196,7 @@ fun AttachmentCardPreview_edit() { MaterialTheme { AttachmentCard( attachment = Attachment.getSample(), - isEditMode = true, + isReadOnly = true, isRemoteCollection = true, player = null, onAttachmentDeleted = { } @@ -222,7 +211,7 @@ fun AttachmentCardPreview_view_with_preview() { MaterialTheme { AttachmentCard( attachment = Attachment.getSample(), - isEditMode = false, + isReadOnly = false, isRemoteCollection = true, player = null, onAttachmentDeleted = { } @@ -233,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/CommentCard.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/cards/CommentCard.kt index 682c467e1..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,15 +11,10 @@ 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.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 +23,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,79 +44,46 @@ fun CommentCard( EditCommentDialog( comment = comment, onConfirm = { updatedComment -> onCommentUpdated(updatedComment) }, - onDismiss = { showCommentEditDialog = false } + onDismiss = { showCommentEditDialog = false }, + onDelete = onCommentDeleted ) } - if (isEditMode) { - OutlinedCard( - 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)) - } - } + ElevatedCard( + modifier = modifier, + onClick = { + if(!isReadOnly) + showCommentEditDialog = true } - } else { - ElevatedCard( - modifier = modifier + ) { + 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) + ) } } -} -@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/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/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/CollectionSelectorDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionSelectorDialog.kt index 09d37f942..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 @@ -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,12 +49,14 @@ fun CollectionSelectorDialog( Column { CollectionsSpinner( - collections = allCollections, + collections = allWritableCollections, preselected = selectedCollection, 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, + enableSelector = true ) } } @@ -130,7 +132,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/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/dialogs/CollectionsMoveCollectionDialog.kt index d19646313..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 @@ -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,9 @@ 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, + enableSelector = true ) Text(stringResource(id = R.string.collection_dialog_move_info)) 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/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/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/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 = { } + ) { + + } + } +} + 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 = { }, - ) - } -} 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/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/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/elements/CollectionInfoColumn.kt index 489ba94e6..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,60 +1,198 @@ package at.techbee.jtx.ui.reusable.elements +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.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.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.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.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.util.SyncUtil @Composable -fun CollectionInfoColumn(collection: ICalCollection, modifier: Modifier = Modifier) { - Column(modifier = modifier) { +fun CollectionInfoColumn( + collection: ICalCollection, + showSyncButton: Boolean, + showDropdownArrow: Boolean, + modifier: Modifier = Modifier +) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + val context = LocalContext.current + val isPreview = LocalInspectionMode.current + val lifecycleOwner = LocalLifecycleOwner.current + + 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(collection.getAccount())) + } + } + onDispose { + if (!isPreview) + ContentResolver.removeStatusChangeListener(listener) + } + } + + 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) { + + 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), + ) + } + } + } + } + } + } } } @@ -71,7 +209,11 @@ fun CollectionInfoColumn_Preview() { accountName = "My account", accountType = LOCAL_ACCOUNT_TYPE ) - CollectionInfoColumn(collection1) + CollectionInfoColumn( + collection = collection1, + showSyncButton = false, + showDropdownArrow = false + ) } } @@ -89,6 +231,32 @@ fun CollectionInfoColumn_Preview_REMOTE() { accountType = "Remote", url = "https://www.example.com/whatever/219348729384/mine" ) - CollectionInfoColumn(collection1) + CollectionInfoColumn( + collection = collection1, + showSyncButton = true, + showDropdownArrow = true + ) + } +} + +@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, + 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 64b934e36..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 @@ -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,13 @@ import at.techbee.jtx.flavored.BillingManager fun CollectionsSpinner( collections: List, preselected: ICalCollection, + enableSelector: Boolean, modifier: Modifier = Modifier, includeReadOnly: Boolean, + showSyncButton: Boolean, includeVJOURNAL: Boolean? = null, includeVTODO: Boolean? = null, - enabled: Boolean = true, + border: BorderStroke? = null, onSelectionChanged: (collection: ICalCollection) -> Unit ) { @@ -51,10 +52,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 +79,10 @@ fun CollectionsSpinner( ) CollectionInfoColumn( collection = selected, - modifier = Modifier - .weight(1f) - .alpha(if (!enabled) 0.5f else 1f) + showSyncButton = showSyncButton, + showDropdownArrow = !selected.readonly, + //modifier = Modifier.alpha(if (!enabled) 0.5f else 1f) ) - Icon(Icons.Outlined.ArrowDropDown, null) DropdownMenu( expanded = expanded, @@ -108,7 +111,11 @@ fun CollectionsSpinner( } }, text = { - CollectionInfoColumn(collection = collection) + CollectionInfoColumn( + collection = collection, + showSyncButton = false, + showDropdownArrow = false + ) } ) } @@ -150,9 +157,11 @@ fun CollectionsSpinner_Preview() { CollectionsSpinner( listOf(collection1, collection2, collection3), preselected = collection2, + enableSelector = true, includeReadOnly = true, includeVJOURNAL = true, includeVTODO = true, + showSyncButton = true, onSelectionChanged = { }, modifier = Modifier.fillMaxWidth() ) @@ -175,11 +184,12 @@ fun CollectionsSpinner_Preview_notenabled() { CollectionsSpinner( listOf(collection1), preselected = collection1, + enableSelector = false, includeReadOnly = true, includeVJOURNAL = true, includeVTODO = true, + showSyncButton = false, onSelectionChanged = { }, - enabled = false, modifier = Modifier.fillMaxWidth() ) } @@ -202,11 +212,12 @@ fun CollectionsSpinner_Preview_no_color() { CollectionsSpinner( listOf(collection1), preselected = collection1, + enableSelector = true, includeReadOnly = true, includeVJOURNAL = true, includeVTODO = true, + showSyncButton = true, onSelectionChanged = { }, - enabled = false, modifier = Modifier.fillMaxWidth() ) } 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..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() } @@ -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 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 dd2b3eeff..c8d3a29df 100644 --- a/app/src/main/java/at/techbee/jtx/widgets/ListWidgetConfigContent.kt +++ b/app/src/main/java/at/techbee/jtx/widgets/ListWidgetConfigContent.kt @@ -188,7 +188,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, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 554f96893..cc53a4b82 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" @@ -282,6 +283,7 @@ "URL" "Location" "Attendees" + "Attendee" "Resources" "Organizer" "Contact" @@ -424,7 +426,6 @@ Thank you!" "Due (month)" "Week %1$d/%2$d" "Summary/Description" - "Status/Classification/Priority" "Timezone" "Not set" "Invert selection" @@ -463,6 +464,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" @@ -472,6 +474,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" @@ -561,6 +564,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" 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))