update: merged Events and Calendar to remove the duplication.
Feat: image preview added to both journal and notes.
This commit is contained in:
@@ -26,10 +26,11 @@ import com.flux.data.model.NotesModel
|
||||
import com.flux.data.model.SettingsModel
|
||||
import com.flux.data.model.TodoModel
|
||||
import com.flux.data.model.WorkspaceModel
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Database(
|
||||
entities = [EventModel::class, LabelModel::class, EventInstanceModel::class, SettingsModel::class, NotesModel::class, HabitModel::class, HabitInstanceModel::class, WorkspaceModel::class, TodoModel::class, JournalModel::class],
|
||||
version = 3,
|
||||
version = 4,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converter::class)
|
||||
@@ -59,4 +60,72 @@ val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||
db.execSQL("ALTER TABLE HabitModel ADD COLUMN endDateTime INTEGER NOT NULL DEFAULT -1")
|
||||
db.execSQL("ALTER TABLE EventModel ADD COLUMN endDateTime INTEGER NOT NULL DEFAULT -1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
|
||||
// Add temp column
|
||||
db.execSQL("""
|
||||
ALTER TABLE WorkspaceModel
|
||||
ADD COLUMN selectedSpaces_new TEXT NOT NULL DEFAULT '[]'
|
||||
""")
|
||||
|
||||
val cursor = db.query(
|
||||
"SELECT workspaceId, selectedSpaces FROM WorkspaceModel"
|
||||
)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getString(0)
|
||||
val raw = cursor.getString(1)
|
||||
|
||||
val spaces = Json.decodeFromString<List<Int>>(raw)
|
||||
.toMutableSet()
|
||||
|
||||
// 1. Merge Calendar (4) → Events (3)
|
||||
if (spaces.remove(4)) {
|
||||
spaces.add(3)
|
||||
}
|
||||
|
||||
// 2. Downgrade IDs above 4
|
||||
val normalized = spaces.map {
|
||||
if (it > 4) it - 1 else it
|
||||
}.toSet()
|
||||
|
||||
val newJson = Json.encodeToString(normalized.toList())
|
||||
|
||||
db.execSQL(
|
||||
"UPDATE WorkspaceModel SET selectedSpaces_new = ? WHERE workspaceId = ?",
|
||||
arrayOf(newJson, id)
|
||||
)
|
||||
}
|
||||
|
||||
cursor.close()
|
||||
|
||||
// Recreate table (SQLite cannot drop columns)
|
||||
db.execSQL("""
|
||||
CREATE TABLE WorkspaceModel_new (
|
||||
workspaceId TEXT NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
colorInd INTEGER NOT NULL,
|
||||
cover TEXT NOT NULL,
|
||||
icon INTEGER NOT NULL,
|
||||
passKey TEXT NOT NULL,
|
||||
isPinned INTEGER NOT NULL,
|
||||
selectedSpaces TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
db.execSQL("""
|
||||
INSERT INTO WorkspaceModel_new
|
||||
SELECT
|
||||
workspaceId, title, description, colorInd,
|
||||
cover, icon, passKey, isPinned, selectedSpaces_new
|
||||
FROM WorkspaceModel
|
||||
""")
|
||||
|
||||
db.execSQL("DROP TABLE WorkspaceModel")
|
||||
db.execSQL("ALTER TABLE WorkspaceModel_new RENAME TO WorkspaceModel")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Notes
|
||||
import androidx.compose.material.icons.filled.Analytics
|
||||
import androidx.compose.material.icons.filled.AutoStories
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.Event
|
||||
import androidx.compose.material.icons.filled.EventAvailable
|
||||
import androidx.compose.material.icons.filled.TaskAlt
|
||||
@@ -44,9 +43,8 @@ fun getSpacesList(): List<Space> {
|
||||
Space(1, stringResource(R.string.Notes), Icons.AutoMirrored.Default.Notes),
|
||||
Space(2, stringResource(R.string.To_Do), Icons.Default.TaskAlt),
|
||||
Space(3, stringResource(R.string.Events), Icons.Default.Event),
|
||||
Space(4, stringResource(R.string.Calendar), Icons.Default.CalendarMonth),
|
||||
Space(5, stringResource(R.string.Journal), Icons.Default.AutoStories),
|
||||
Space(6, stringResource(R.string.Habits), Icons.Default.EventAvailable),
|
||||
Space(7, stringResource(R.string.Analytics), Icons.Default.Analytics)
|
||||
Space(4, stringResource(R.string.Journal), Icons.Default.AutoStories),
|
||||
Space(5, stringResource(R.string.Habits), Icons.Default.EventAvailable),
|
||||
Space(6, stringResource(R.string.Analytics), Icons.Default.Analytics)
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import com.flux.data.dao.WorkspaceDao
|
||||
import com.flux.data.database.FluxDatabase
|
||||
import com.flux.data.database.MIGRATION_1_2
|
||||
import com.flux.data.database.MIGRATION_2_3
|
||||
import com.flux.data.database.MIGRATION_3_4
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -35,7 +36,7 @@ object DataModule {
|
||||
FluxDatabase::class.java,
|
||||
"FluxDatabase"
|
||||
)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
.build()
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -204,7 +204,6 @@ val WorkspaceScreens =
|
||||
states.notesState.allLabels.filter { it.workspaceId == workspaceId },
|
||||
states.settings,
|
||||
states.notesState.isNotesLoading,
|
||||
states.eventState.isAllEventsLoading,
|
||||
states.eventState.isDatedEventLoading,
|
||||
states.todoState.isLoading,
|
||||
states.journalState.isLoading,
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.RemoveCircleOutline
|
||||
import androidx.compose.material.icons.outlined.Analytics
|
||||
import androidx.compose.material.icons.outlined.AutoStories
|
||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Event
|
||||
@@ -171,8 +170,8 @@ fun SpacesMenu(
|
||||
}
|
||||
if (selectedSpaces.contains(4)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Calendar)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.CalendarMonth, contentDescription = null) },
|
||||
text = { Text(stringResource(R.string.Journal)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.AutoStories, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(4)
|
||||
onDismiss()
|
||||
@@ -181,8 +180,8 @@ fun SpacesMenu(
|
||||
}
|
||||
if (selectedSpaces.contains(5)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Journal)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.AutoStories, contentDescription = null) },
|
||||
text = { Text(stringResource(R.string.Habits)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.EventAvailable, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(5)
|
||||
onDismiss()
|
||||
@@ -190,21 +189,11 @@ fun SpacesMenu(
|
||||
)
|
||||
}
|
||||
if (selectedSpaces.contains(6)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Habits)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.EventAvailable, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(6)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
if (selectedSpaces.contains(7)) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.Analytics)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Analytics, contentDescription = null) },
|
||||
onClick = {
|
||||
onConfirm(7)
|
||||
onConfirm(6)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -24,6 +25,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -34,6 +36,7 @@ import androidx.compose.material.icons.automirrored.outlined.FormatAlignLeft
|
||||
import androidx.compose.material.icons.automirrored.outlined.FormatAlignRight
|
||||
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||
import androidx.compose.material.icons.filled.AddPhotoAlternate
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.FormatStrikethrough
|
||||
import androidx.compose.material.icons.filled.Highlight
|
||||
@@ -56,12 +59,17 @@ import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.ParagraphStyle
|
||||
import androidx.compose.ui.text.PlatformSpanStyle
|
||||
@@ -74,6 +82,9 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.flux.R
|
||||
import com.flux.data.model.LabelModel
|
||||
import com.flux.data.model.NotesModel
|
||||
@@ -107,6 +118,50 @@ fun NotesInputCard(
|
||||
}
|
||||
}
|
||||
)
|
||||
var previewImage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
if (previewImage != null) {
|
||||
Dialog(
|
||||
onDismissRequest = { previewImage = null },
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.95f))
|
||||
) {
|
||||
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(previewImage),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { previewImage = null },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(16.dp)
|
||||
.size(48.dp)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.6f),
|
||||
shape = CircleShape
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -114,7 +169,7 @@ fun NotesInputCard(
|
||||
.padding(innerPadding)
|
||||
.imePadding(),
|
||||
) {
|
||||
Carousel(images) { onRemoveImage(it) }
|
||||
Carousel(images, {onRemoveImage(it)}) { previewImage=it }
|
||||
TextField(
|
||||
value = title,
|
||||
onValueChange = onTitleChange,
|
||||
|
||||
@@ -149,7 +149,7 @@ fun JournalToolBar(navController: NavController, workspaceId: String) {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU, Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
fun CalendarToolBar(navController: NavController, workspaceId: String, context: Context, selectedDate: Long, isMonthlyView: Boolean, onClick: (Boolean) -> Unit) {
|
||||
fun EventToolBar(navController: NavController, workspaceId: String, context: Context, selectedDate: Long, isMonthlyView: Boolean, onClick: (Boolean) -> Unit) {
|
||||
Row {
|
||||
IconButton({ onClick(!isMonthlyView) }) {
|
||||
Icon(
|
||||
@@ -213,32 +213,6 @@ fun HabitToolBar(context: Context, onAddClick: () -> Unit) {
|
||||
}) { Icon(Icons.Default.Add, null, tint = MaterialTheme.colorScheme.primary) }
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU, Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
fun EventToolBar(context: Context, navController: NavController, workspaceId: String) {
|
||||
IconButton({
|
||||
if (!canScheduleReminder(context)) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getText(R.string.Reminder_Permission),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
requestExactAlarmPermission(context)
|
||||
}
|
||||
if (!isNotificationPermissionGranted(context)) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getText(R.string.Notification_Permission),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
openAppNotificationSettings(context)
|
||||
}
|
||||
if (canScheduleReminder(context) && isNotificationPermissionGranted(context)) {
|
||||
navController.navigate(NavRoutes.NewEvent.withArgs(workspaceId, "", System.currentTimeMillis()))
|
||||
}
|
||||
}) { Icon(Icons.Default.Add, null, tint = MaterialTheme.colorScheme.primary) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotesToolBar(
|
||||
navController: NavController,
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package com.flux.ui.screens.calendar
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.data.model.EventInstanceModel
|
||||
import com.flux.data.model.EventModel
|
||||
import com.flux.navigation.Loader
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.ui.components.DailyViewCalendar
|
||||
import com.flux.ui.components.MonthlyViewCalendar
|
||||
import com.flux.ui.events.TaskEvents
|
||||
import com.flux.ui.screens.events.EmptyEvents
|
||||
import com.flux.ui.screens.events.EventCard
|
||||
import com.flux.ui.state.Settings
|
||||
import java.time.YearMonth
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun LazyListScope.calendarItems(
|
||||
navController: NavController,
|
||||
radius: Int,
|
||||
is24HourFormat: Boolean,
|
||||
isLoading: Boolean,
|
||||
workspaceId: String,
|
||||
selectedMonth: YearMonth,
|
||||
selectedDate: Long,
|
||||
settings: Settings,
|
||||
datedEvents: List<EventModel>,
|
||||
allEventInstances: List<EventInstanceModel>,
|
||||
onTaskEvents: (TaskEvents) -> Unit
|
||||
) {
|
||||
val isMonthlyView = settings.data.isCalendarMonthlyView
|
||||
|
||||
if (isMonthlyView) {
|
||||
item {
|
||||
MonthlyViewCalendar(
|
||||
selectedMonth, selectedDate,
|
||||
onMonthChange = {
|
||||
onTaskEvents(TaskEvents.ChangeMonth(it))
|
||||
},
|
||||
onDateChange = {
|
||||
onTaskEvents(TaskEvents.LoadDateTask(workspaceId, it))
|
||||
onTaskEvents(TaskEvents.ChangeDate(it))
|
||||
})
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
DailyViewCalendar(
|
||||
selectedMonth,
|
||||
selectedDate,
|
||||
onDateChange = {
|
||||
onTaskEvents(TaskEvents.LoadDateTask(workspaceId, it))
|
||||
onTaskEvents(TaskEvents.ChangeDate(it))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
item { Loader() }
|
||||
} else if (datedEvents.isEmpty()) {
|
||||
item { EmptyEvents() }
|
||||
} else {
|
||||
item { Spacer(Modifier.height(24.dp)) }
|
||||
|
||||
val pendingTasks = datedEvents.filter { task ->
|
||||
val instance = allEventInstances.find { it.eventId == task.id && it.instanceDate == selectedDate }
|
||||
instance == null
|
||||
}
|
||||
|
||||
val completedTasks = datedEvents.filter { task ->
|
||||
val instance = allEventInstances.find { it.eventId == task.id && it.instanceDate == selectedDate }
|
||||
instance != null
|
||||
}
|
||||
|
||||
if (pendingTasks.isNotEmpty()) {
|
||||
items(pendingTasks) { task ->
|
||||
EventCard(
|
||||
radius = radius,
|
||||
is24HourFormat = is24HourFormat,
|
||||
isPending = true,
|
||||
title = task.title,
|
||||
repeat = task.recurrence,
|
||||
startDateTime = task.startDateTime,
|
||||
onChangeStatus = { onTaskEvents(TaskEvents.ToggleStatus(true, task.id, workspaceId, selectedDate)) },
|
||||
onClick = { navController.navigate(NavRoutes.EventDetails.withArgs(workspaceId, task.id, selectedDate)) }
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
if (completedTasks.isNotEmpty()) {
|
||||
items(completedTasks) { task ->
|
||||
EventCard(
|
||||
radius = radius,
|
||||
is24HourFormat = is24HourFormat,
|
||||
isPending = false,
|
||||
title = task.title,
|
||||
repeat = task.recurrence,
|
||||
startDateTime = task.startDateTime,
|
||||
onChangeStatus = { onTaskEvents(TaskEvents.ToggleStatus(false, task.id, workspaceId, selectedDate)) },
|
||||
onClick = { navController.navigate(NavRoutes.EventDetails.withArgs(workspaceId, task.id, selectedDate)) }
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,24 +31,24 @@ 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.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.flux.R
|
||||
import com.flux.data.model.EventInstanceModel
|
||||
import com.flux.data.model.EventModel
|
||||
import com.flux.data.model.RecurrenceRule
|
||||
import com.flux.data.model.occursOn
|
||||
import com.flux.navigation.Loader
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.ui.components.DailyViewCalendar
|
||||
import com.flux.ui.components.MonthlyViewCalendar
|
||||
import com.flux.ui.components.shapeManager
|
||||
import com.flux.ui.events.TaskEvents
|
||||
import com.flux.ui.state.Settings
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@@ -58,128 +58,85 @@ fun LazyListScope.eventHomeItems(
|
||||
radius: Int,
|
||||
is24HourFormat: Boolean,
|
||||
isLoading: Boolean,
|
||||
allEvents: List<EventModel>,
|
||||
allEventInstances: List<EventInstanceModel>,
|
||||
workspaceId: String,
|
||||
selectedMonth: YearMonth,
|
||||
selectedDate: Long,
|
||||
settings: Settings,
|
||||
datedEvents: List<EventModel>,
|
||||
allEventInstances: List<EventInstanceModel>,
|
||||
onTaskEvents: (TaskEvents) -> Unit
|
||||
) {
|
||||
val today = LocalDate.now()
|
||||
val endOfMonth = today.withDayOfMonth(today.lengthOfMonth())
|
||||
val isMonthlyView = settings.data.isCalendarMonthlyView
|
||||
|
||||
// Today’s events
|
||||
val eventsToday = allEvents.filter { it.occursOn(today) }
|
||||
|
||||
// Upcoming (at least one occurrence before end of month, but not today)
|
||||
val upcomingEvents = allEvents.filter { event ->
|
||||
!event.occursOn(today) &&
|
||||
(1..ChronoUnit.DAYS.between(today, endOfMonth)).any { offset ->
|
||||
val date = today.plusDays(offset)
|
||||
event.occursOn(date)
|
||||
}
|
||||
}.distinctBy { it.id }
|
||||
if (isMonthlyView) {
|
||||
item {
|
||||
MonthlyViewCalendar(
|
||||
selectedMonth, selectedDate,
|
||||
onMonthChange = {
|
||||
onTaskEvents(TaskEvents.ChangeMonth(it))
|
||||
},
|
||||
onDateChange = {
|
||||
onTaskEvents(TaskEvents.LoadDateTask(workspaceId, it))
|
||||
onTaskEvents(TaskEvents.ChangeDate(it))
|
||||
})
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
DailyViewCalendar(
|
||||
selectedMonth,
|
||||
selectedDate,
|
||||
onDateChange = {
|
||||
onTaskEvents(TaskEvents.LoadDateTask(workspaceId, it))
|
||||
onTaskEvents(TaskEvents.ChangeDate(it))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
item { Loader() }
|
||||
} else if (datedEvents.isEmpty()) {
|
||||
item { EmptyEvents() }
|
||||
} else {
|
||||
if (eventsToday.isEmpty() && upcomingEvents.isEmpty()) {
|
||||
item { EmptyEvents() }
|
||||
} else {
|
||||
// Section for today
|
||||
if (eventsToday.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.Today),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(24.dp)) }
|
||||
|
||||
items(eventsToday) { event ->
|
||||
val instance = allEventInstances.find {
|
||||
it.eventId == event.id && it.instanceDate == today.toEpochDay()
|
||||
}
|
||||
val pendingTasks = datedEvents.filter { task ->
|
||||
val instance = allEventInstances.find { it.eventId == task.id && it.instanceDate == selectedDate }
|
||||
instance == null
|
||||
}
|
||||
|
||||
val completedTasks = datedEvents.filter { task ->
|
||||
val instance = allEventInstances.find { it.eventId == task.id && it.instanceDate == selectedDate }
|
||||
instance != null
|
||||
}
|
||||
|
||||
if (pendingTasks.isNotEmpty()) {
|
||||
items(pendingTasks) { task ->
|
||||
EventCard(
|
||||
radius = radius,
|
||||
is24HourFormat = is24HourFormat,
|
||||
isPending = instance == null,
|
||||
title = event.title,
|
||||
repeat = event.recurrence,
|
||||
startDateTime = event.startDateTime,
|
||||
onChangeStatus = {
|
||||
onTaskEvents(
|
||||
TaskEvents.ToggleStatus(
|
||||
instance == null,
|
||||
event.id,
|
||||
workspaceId,
|
||||
today.toEpochDay()
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = { navController.navigate(NavRoutes.EventDetails.withArgs(workspaceId, event.id, today.toEpochDay())) }
|
||||
isPending = true,
|
||||
title = task.title,
|
||||
repeat = task.recurrence,
|
||||
startDateTime = task.startDateTime,
|
||||
onChangeStatus = { onTaskEvents(TaskEvents.ToggleStatus(true, task.id, workspaceId, selectedDate)) },
|
||||
onClick = { navController.navigate(NavRoutes.EventDetails.withArgs(workspaceId, task.id, selectedDate)) }
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Section for upcoming
|
||||
if (upcomingEvents.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.Upcoming),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
if (completedTasks.isNotEmpty()) {
|
||||
items(completedTasks) { task ->
|
||||
EventCard(
|
||||
radius = radius,
|
||||
is24HourFormat = is24HourFormat,
|
||||
isPending = false,
|
||||
title = task.title,
|
||||
repeat = task.recurrence,
|
||||
startDateTime = task.startDateTime,
|
||||
onChangeStatus = { onTaskEvents(TaskEvents.ToggleStatus(false, task.id, workspaceId, selectedDate)) },
|
||||
onClick = { navController.navigate(NavRoutes.EventDetails.withArgs(workspaceId, task.id, selectedDate)) }
|
||||
)
|
||||
}
|
||||
|
||||
items(upcomingEvents) { event ->
|
||||
// Find the *next occurrence date* after today
|
||||
val nextDate = (1..ChronoUnit.DAYS.between(today, endOfMonth))
|
||||
.map { today.plusDays(it) }
|
||||
.firstOrNull { event.occursOn(it) }
|
||||
|
||||
if (nextDate != null) {
|
||||
val epochDay = nextDate.toEpochDay()
|
||||
|
||||
// Find if an instance already exists for that date
|
||||
val instance = allEventInstances.find {
|
||||
it.eventId == event.id && it.instanceDate == epochDay
|
||||
}
|
||||
|
||||
EventCard(
|
||||
radius = radius,
|
||||
is24HourFormat = is24HourFormat,
|
||||
isPending = instance == null,
|
||||
title = event.title,
|
||||
repeat = event.recurrence,
|
||||
startDateTime = event.startDateTime,
|
||||
onChangeStatus = {
|
||||
onTaskEvents(
|
||||
TaskEvents.ToggleStatus(
|
||||
instance == null,
|
||||
event.id,
|
||||
workspaceId,
|
||||
epochDay
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
navController.navigate(
|
||||
NavRoutes.EventDetails.withArgs(
|
||||
workspaceId,
|
||||
event.id,
|
||||
epochDay
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.flux.ui.screens.journal
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -13,10 +16,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -47,8 +53,11 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.flux.R
|
||||
import com.flux.data.model.JournalModel
|
||||
import com.flux.ui.components.DatePickerModal
|
||||
@@ -71,9 +80,7 @@ fun EditJournal(
|
||||
journal: JournalModel,
|
||||
onJournalEvents: (JournalEvents) -> Unit
|
||||
) {
|
||||
val isToday =
|
||||
LocalDate.now() == Instant.ofEpochMilli(journal.dateTime).atZone(ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
val isToday = LocalDate.now() == Instant.ofEpochMilli(journal.dateTime).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
val context = LocalContext.current
|
||||
val richTextState = rememberRichTextState()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -81,6 +88,7 @@ fun EditJournal(
|
||||
val pickedImages = rememberSaveable { mutableStateListOf<String>().apply { addAll(journal.images) } }
|
||||
var showDatePicker by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedDateTime by rememberSaveable { mutableLongStateOf(journal.dateTime) }
|
||||
var previewImage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent(),
|
||||
@@ -118,6 +126,49 @@ fun EditJournal(
|
||||
}, onDismiss = { showDatePicker = false })
|
||||
}
|
||||
|
||||
if (previewImage != null) {
|
||||
Dialog(
|
||||
onDismissRequest = { previewImage = null },
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.95f))
|
||||
) {
|
||||
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(previewImage),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { previewImage = null },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(16.dp)
|
||||
.size(48.dp)
|
||||
.background(
|
||||
color = Color.Black.copy(alpha = 0.6f),
|
||||
shape = CircleShape
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
topBar = {
|
||||
@@ -169,8 +220,7 @@ fun EditJournal(
|
||||
.padding(innerPadding)
|
||||
.imePadding()
|
||||
) {
|
||||
Carousel(pickedImages) { pickedImages.remove(it) }
|
||||
|
||||
Carousel(pickedImages, {pickedImages.remove(it)}) { previewImage=it }
|
||||
RichTextEditor(
|
||||
state = richTextState,
|
||||
interactionSource = interactionSource,
|
||||
@@ -203,7 +253,7 @@ fun EditJournal(
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Carousel(items: List<String>, onItemRemoved: (String) -> Unit) {
|
||||
fun Carousel(items: List<String>, onItemRemoved: (String) -> Unit, onClick: (String)->Unit) {
|
||||
if (items.isNotEmpty()) {
|
||||
HorizontalMultiBrowseCarousel(
|
||||
state = rememberCarouselState { items.count() },
|
||||
@@ -219,6 +269,7 @@ fun Carousel(items: List<String>, onItemRemoved: (String) -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(160.dp)
|
||||
.clickable{onClick(item)}
|
||||
.maskClip(MaterialTheme.shapes.extraLarge)
|
||||
) {
|
||||
AsyncImage(
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lightbulb
|
||||
import androidx.compose.material.icons.rounded.ChatBubble
|
||||
import androidx.compose.material.icons.rounded.Email
|
||||
import androidx.compose.material.icons.rounded.Feedback
|
||||
@@ -41,10 +42,27 @@ fun Contact(navController: NavController, radius: Int) {
|
||||
) {
|
||||
item {
|
||||
SettingOption(
|
||||
title = stringResource(R.string.Contact_desc),
|
||||
title = "Feature Request/Suggestion",
|
||||
description = "Give your ideas on github discussion.",
|
||||
icon = Icons.Default.Lightbulb,
|
||||
radius = shapeManager(radius = radius, isFirst = true),
|
||||
actionType = ActionType.LINK,
|
||||
linkClicked = {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"https://github.com/chindaronit/Flux/discussions/61".toUri()
|
||||
)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SettingOption(
|
||||
title = "Bug Report",
|
||||
description = stringResource(R.string.Contact_desc2),
|
||||
icon = Icons.Rounded.Feedback,
|
||||
radius = shapeManager(radius = radius, isBoth = true),
|
||||
radius = shapeManager(radius = radius, isLast = true),
|
||||
actionType = ActionType.LINK,
|
||||
linkClicked = {
|
||||
val intent = Intent(
|
||||
|
||||
@@ -50,7 +50,6 @@ import com.flux.data.model.getSpacesList
|
||||
import com.flux.navigation.NavRoutes
|
||||
import com.flux.other.icons
|
||||
import com.flux.ui.components.AddNewSpacesBottomSheet
|
||||
import com.flux.ui.components.CalendarToolBar
|
||||
import com.flux.ui.components.ChangeIconBottomSheet
|
||||
import com.flux.ui.components.DeleteAlert
|
||||
import com.flux.ui.components.EventToolBar
|
||||
@@ -72,7 +71,6 @@ import com.flux.ui.events.TaskEvents
|
||||
import com.flux.ui.events.TodoEvents
|
||||
import com.flux.ui.events.WorkspaceEvents
|
||||
import com.flux.ui.screens.analytics.analyticsItems
|
||||
import com.flux.ui.screens.calendar.calendarItems
|
||||
import com.flux.ui.screens.events.eventHomeItems
|
||||
import com.flux.ui.screens.habits.habitsHomeItems
|
||||
import com.flux.ui.screens.journal.journalHomeItems
|
||||
@@ -94,7 +92,6 @@ fun WorkspaceDetails(
|
||||
allLabels: List<LabelModel>,
|
||||
settings: Settings,
|
||||
isNotesLoading: Boolean,
|
||||
isAllEventsLoading: Boolean,
|
||||
isDatedTaskLoading: Boolean,
|
||||
isTodoLoading: Boolean,
|
||||
isJournalEntriesLoading: Boolean,
|
||||
@@ -312,8 +309,8 @@ fun WorkspaceDetails(
|
||||
if (spacesList.find { it.id == selectedSpaceId.intValue }?.title == stringResource(R.string.To_Do)) {
|
||||
TodoToolBar(navController, workspaceId)
|
||||
}
|
||||
if (spacesList.find { it.id == selectedSpaceId.intValue }?.title == stringResource(R.string.Calendar)) {
|
||||
CalendarToolBar(
|
||||
if (spacesList.find { it.id == selectedSpaceId.intValue }?.title == stringResource(R.string.Events)) {
|
||||
EventToolBar(
|
||||
navController,
|
||||
workspaceId,
|
||||
context,
|
||||
@@ -330,9 +327,6 @@ fun WorkspaceDetails(
|
||||
}
|
||||
)
|
||||
}
|
||||
if (spacesList.find { it.id == selectedSpaceId.intValue }?.title == stringResource(R.string.Events)) {
|
||||
EventToolBar(context, navController, workspaceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,21 +356,6 @@ fun WorkspaceDetails(
|
||||
onNotesEvents
|
||||
)
|
||||
}
|
||||
if (spacesList.find { it.id == selectedSpaceId.intValue }?.title == context.getString(R.string.Calendar)) {
|
||||
calendarItems(
|
||||
navController,
|
||||
radius,
|
||||
is24HourFormat,
|
||||
isDatedTaskLoading,
|
||||
workspaceId,
|
||||
selectedYearMonth,
|
||||
selectedDate,
|
||||
settings,
|
||||
datedEvents,
|
||||
allEventInstances,
|
||||
onTaskEvents
|
||||
)
|
||||
}
|
||||
if (spacesList.find { it.id == selectedSpaceId.intValue }?.title == context.getString(R.string.Journal)) {
|
||||
journalHomeItems(navController, isJournalEntriesLoading, workspaceId, allEntries)
|
||||
}
|
||||
@@ -408,10 +387,13 @@ fun WorkspaceDetails(
|
||||
navController,
|
||||
radius,
|
||||
is24HourFormat,
|
||||
isAllEventsLoading,
|
||||
allEvents,
|
||||
allEventInstances,
|
||||
isDatedTaskLoading,
|
||||
workspaceId,
|
||||
selectedYearMonth,
|
||||
selectedDate,
|
||||
settings,
|
||||
datedEvents,
|
||||
allEventInstances,
|
||||
onTaskEvents
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user