Не подтверждена Коммит edd6588e создал по автору Stanislav Radchenko's avatar Stanislav Radchenko Зафиксировано автором GitHub
Просмотр файлов

Refactoring the booking deletion logic (#355)

* feat: implement EventOrchestrator for coordinated refresh management

- Added `EventOrchestrator` class to handle user interaction states, refresh triggers, and idle time configurations.
- Integrated `EventOrchestrator` in RootComponent and MainComponent for refresh event processing.
- Introduced `TouchEventDispatcher` to track touch events and update interaction states.
- Updated `AppActivity` to create and configure `EventOrchestrator` and install `TouchEventDispatcher`.
- Refactored event handling in MainComponent to leverage orchestrated refresh events.
- Registered `EventOrchestrator` in the `domainModule` for dependency injection.

* feat: add TimeReceiver for synchronized time handling

- Introduced `TimeReceiver` to provide synchronized time updates across iOS and Android platforms.
- Implemented platform-specific logic for time synchronization using timers (iOS) and broadcast receivers (Android).
- Added `CurrentTimeHolder` singleton for shared time state management.
- Updated `MainComponent` to integrate `CurrentTimeHolder` for real-time date and time updates.
- Refactored legacy timer logic in `MainComponent` for time-to-next-event calculation.
- Registered and managed lifecycle of `TimeReceiver` in `AppActivity`.

* Revert "feat: implement EventOrchestrator for coordinated refresh management"

This reverts commit e2fcaded

* feat: improve slot deletion and update handling

- Introduced `deletionProgress` and `startTimeMillis` to manage slot deletion state.
- Enhanced `BorderIndicator` to support custom start progress for animations.
- Refactored slot update function to preserve deletion states and open MultiSlots.
- Replaced `UpdateDate` intent with an enhanced `UpdateRequest` intent for room and date handling.
- Optimized MultiSlot and sub-slot deletion logic.
- Added utility methods for slot comparisons and extraction of `DeleteSlot` instances.

* feat: implement inactivity tracking and date reset system

- Added `InactivityManager`, `InactivityWindowCallback`, and `InactivityLifecycleCallbacks` to manage user inactivity and trigger callbacks.
- Introduced `DateResetManager` to handle date reset operations upon inactivity.
- Updated `App` to initialize inactivity tracking and integrate with `DateResetManager`.
- Enhanced `SlotComponent` with a new intent `InactivityTimeout` to reset multi-slot states.
- Refactored `MainComponent` to register inactivity-driven date resets and synchronize selected date with the current date.
- Improved UI components to utilize real-time date updates from `CurrentTimeHolder`.

* `refactor: remove slot deletion logic from SlotComponent and UI`

The deleted logic and components related to `SlotUi.DeleteSlot` streamline the architecture and shift delete responsibility to the `BookingEditorComponent`. Adjustments include UI removal of delete-related views and simplification of slot handling logic.

* fix: update date mapping logic and bump version to 0.0.3

Correct `selectDate` parameter usage in `DateTimeView`.
Upgrade project version from 0.0.2 to 0.0.3 in `gradle.properties`.
владелец 674bfbf5
......@@ -10,6 +10,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import band.effective.office.tablet.root.RootComponent
import band.effective.office.tablet.time.TimeReceiver
import com.arkivanov.decompose.defaultComponentContext
class AppActivity : ComponentActivity() {
......@@ -18,15 +19,25 @@ class AppActivity : ComponentActivity() {
var isRunKioskMode = false
}
val timeReceiver by lazy { TimeReceiver(this) }
@RequiresApi(Build.VERSION_CODES.P)
override fun onCreate(savedInstanceState: Bundle?) {
runKioskMode()
super.onCreate(savedInstanceState)
timeReceiver.register()
enableEdgeToEdge()
val root = RootComponent(componentContext = defaultComponentContext())
setContent { App(root) }
}
override fun onDestroy() {
// Unregister the time receiver
timeReceiver.unregister()
super.onDestroy()
}
@RequiresApi(Build.VERSION_CODES.P)
private fun runKioskMode(){
val context = this
......
package band.effective.office.tablet
import android.app.Application
import android.util.Log
import band.effective.office.tablet.core.domain.manager.DateResetManager
import band.effective.office.tablet.core.domain.model.SettingsManager
import band.effective.office.tablet.core.ui.inactivity.InactivityLifecycleCallbacks
import band.effective.office.tablet.di.KoinInitializer
import com.google.firebase.messaging.FirebaseMessaging
import com.russhwolf.settings.SharedPreferencesSettings
import org.koin.android.ext.android.get
import org.koin.core.qualifier.named
import kotlin.time.Duration.Companion.minutes
class App : Application() {
......@@ -24,6 +26,7 @@ class App : Application() {
)
)
subscribeOnFirebaseTopics()
initializeInactivitySystem()
}
private fun subscribeOnFirebaseTopics() {
......@@ -32,4 +35,14 @@ class App : Application() {
FirebaseMessaging.getInstance().subscribeToTopic(topic)
}
}
private fun initializeInactivitySystem() {
InactivityLifecycleCallbacks.initialize(
application = this,
timeoutMs = 1.minutes.inWholeMilliseconds,
callback = {
DateResetManager.resetDate()
}
)
}
}
\ Нет новой строки в конце файла
package band.effective.office.tablet.time
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder
import kotlinx.coroutines.flow.StateFlow
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
/**
* A broadcast receiver that listens for time-related broadcasts and emits the current time.
*/
actual class TimeReceiver(private val context: Context) {
actual val currentTime: StateFlow<LocalDateTime> = CurrentTimeHolder.currentTime
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_TIME_TICK, Intent.ACTION_TIME_CHANGED -> {
CurrentTimeHolder.updateTime(getCurrentTime())
}
}
}
}
/**
* Registers the broadcast receiver to listen for time-related broadcasts.
*/
fun register() {
val filter = IntentFilter().apply {
addAction(Intent.ACTION_TIME_TICK)
addAction(Intent.ACTION_TIME_CHANGED)
}
context.registerReceiver(receiver, filter)
}
/**
* Unregisters the broadcast receiver.
*/
fun unregister() {
context.unregisterReceiver(receiver)
}
/**
* Gets the current time as a LocalDateTime.
*/
private fun getCurrentTime(): LocalDateTime {
return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
}
}
......@@ -2,7 +2,6 @@ package band.effective.office.tablet.root
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.model.Slot
import band.effective.office.tablet.core.domain.useCase.CheckSettingsUseCase
import band.effective.office.tablet.core.domain.useCase.ResourceDisposerUseCase
import band.effective.office.tablet.core.ui.common.ModalWindow
......@@ -153,16 +152,10 @@ class RootComponent(
componentContext = componentContext,
initialEvent = config.event,
roomName = config.room,
onDeleteEvent = ::handleDeleteEvent,
onCloseRequest = modalNavigation::dismiss,
)
}
private fun handleDeleteEvent(slot: Slot) {
val mainComponent = (childStack.value.active.instance as? Child.MainChild)?.component
mainComponent?.handleDeleteEvent(slot)
}
private fun createFastBookingComponent(
config: ModalWindowsConfig.FastEvent,
componentContext: ComponentContext
......
package band.effective.office.tablet.time
import kotlinx.coroutines.flow.StateFlow
import kotlinx.datetime.LocalDateTime
/**
* A receiver that emits the current time.
*/
expect class TimeReceiver {
/**
* A flow that emits the current time.
*/
val currentTime: StateFlow<LocalDateTime>
}
package band.effective.office.tablet.time
import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder
import kotlinx.coroutines.flow.StateFlow
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSDefaultRunLoopMode
import platform.Foundation.NSRunLoop
import platform.Foundation.NSDate
import platform.Foundation.NSTimer
import platform.Foundation.timeIntervalSince1970
/**
* iOS implementation of TimeReceiver that uses a timer to update the current time.
*/
actual class TimeReceiver {
actual val currentTime: StateFlow<LocalDateTime> = CurrentTimeHolder.currentTime
private var timer: NSTimer? = null
init {
// Update time every minute
timer = NSTimer.scheduledTimerWithTimeInterval(
60.0, // 60 seconds
true, // repeats
{
CurrentTimeHolder.updateTime(getCurrentTime())
}
)
// Add timer to the main run loop
NSRunLoop.mainRunLoop.addTimer(timer!!, NSDefaultRunLoopMode)
}
/**
* Gets the current time as a LocalDateTime.
*/
private fun getCurrentTime(): LocalDateTime {
return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
}
}
package band.effective.office.tablet.core.domain.manager
import band.effective.office.tablet.core.domain.util.currentLocalDateTime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDateTime
/**
* Singleton manager that handles date reset operations.
* This class facilitates communication between different parts of the application
* that need to reset dates, such as when inactivity is detected.
*/
object DateResetManager {
// Create a CoroutineScope with a SupervisorJob to handle suspending functions
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
// Use a suspending function type for the callback
private var dateResetCallback: (suspend (LocalDateTime) -> Unit)? = null
/**
* Registers a callback that will be called when the date needs to be reset.
*
* @param callback The callback to execute when the date needs to be reset.
* The callback receives the current local date and time.
*/
fun registerDateResetCallback(callback: suspend (LocalDateTime) -> Unit) {
dateResetCallback = callback
}
/**
* Unregisters the date reset callback.
*/
fun unregisterDateResetCallback() {
dateResetCallback = null
}
/**
* Triggers a date reset with the current local date and time.
* This will call the registered callback if one exists.
*/
fun resetDate() {
val callback = dateResetCallback ?: return
// Launch a coroutine to execute the suspending callback
coroutineScope.launch {
callback(currentLocalDateTime)
}
}
}
......@@ -61,8 +61,8 @@ class SlotUseCase(
if (isEmpty()) return listOf(eventInfo.toSlot())
val list = this.toMutableList()
list.removeEmptySlot(eventInfo)
val predSlot = list.firstOrNull { it.finish > eventInfo.startTime } ?: list.first()
val predSlotIndex = list.indexOf(predSlot)
val predSlot = list.firstOrNull { it.finish > eventInfo.startTime }
val predSlotIndex = list.indexOf(predSlot).let { if (it == -1) 0 else it }
list.add(predSlotIndex, eventInfo.toSlot())
return list
}
......
package band.effective.office.tablet.core.ui.inactivity
import android.app.Activity
import android.app.Application
import android.os.Bundle
/**
* ActivityLifecycleCallbacks implementation that hooks into all activities
* and sets up the Window.Callback wrapper to track user inactivity.
*/
class InactivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
// Get the original Window.Callback
val originalCallback = activity.window.callback
// Set our custom Window.Callback wrapper
activity.window.callback = InactivityWindowCallback(originalCallback)
}
override fun onActivityStarted(activity: Activity) {
// No implementation needed
}
override fun onActivityResumed(activity: Activity) {
// Reset the inactivity timer when an activity is resumed
InactivityManager.resetTimer()
}
override fun onActivityPaused(activity: Activity) {
// No implementation needed
}
override fun onActivityStopped(activity: Activity) {
// No implementation needed
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
// No implementation needed
}
override fun onActivityDestroyed(activity: Activity) {
// No implementation needed
}
companion object {
/**
* Registers the InactivityLifecycleCallbacks with the Application
* and starts tracking user inactivity.
*
* @param application The Application instance.
* @param timeoutMs The inactivity timeout in milliseconds. Default is 1 minute.
* @param callback The callback to execute when inactivity is detected.
*/
fun initialize(
application: Application,
timeoutMs: Long = InactivityManager.DEFAULT_INACTIVITY_TIMEOUT_MS,
callback: (() -> Unit)? = null,
) {
// Register the ActivityLifecycleCallbacks
application.registerActivityLifecycleCallbacks(InactivityLifecycleCallbacks())
// Start tracking inactivity
InactivityManager.startTracking(timeoutMs) {
// Execute the original callback
callback?.invoke()
}
}
}
}
package band.effective.office.tablet.core.ui.inactivity
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Singleton manager that tracks user inactivity across the application.
* It resets a timer whenever there is any UI interaction (touch event, key event)
* and executes a callback after a specified period of inactivity.
*/
object InactivityManager {
/**
* Default inactivity timeout in milliseconds (1 minute)
*/
const val DEFAULT_INACTIVITY_TIMEOUT_MS = 60000L
private val handler = Handler(Looper.getMainLooper())
private var inactivityTimeoutMs = DEFAULT_INACTIVITY_TIMEOUT_MS
private var inactivityCallback: (() -> Unit)? = null
private var isTracking = false
private val _inactivityState = MutableStateFlow(false)
val inactivityState: StateFlow<Boolean> = _inactivityState.asStateFlow()
private val inactivityRunnable = Runnable {
_inactivityState.value = true
inactivityCallback?.invoke()
}
/**
* Starts tracking user inactivity.
*
* @param timeoutMs The inactivity timeout in milliseconds. Default is 1 minute.
* @param callback The callback to execute when inactivity is detected.
*/
fun startTracking(
timeoutMs: Long = DEFAULT_INACTIVITY_TIMEOUT_MS,
callback: (() -> Unit)? = null
) {
inactivityTimeoutMs = timeoutMs
inactivityCallback = callback
isTracking = true
resetTimer()
}
/**
* Stops tracking user inactivity.
*/
fun stopTracking() {
isTracking = false
handler.removeCallbacks(inactivityRunnable)
_inactivityState.value = false
}
/**
* Resets the inactivity timer. This should be called whenever a user interaction is detected.
*/
fun resetTimer() {
if (!isTracking) return
handler.removeCallbacks(inactivityRunnable)
_inactivityState.value = false
handler.postDelayed(inactivityRunnable, inactivityTimeoutMs)
}
/**
* Notifies the manager that a user interaction has occurred.
* This will reset the inactivity timer.
*/
fun onUserInteraction() {
resetTimer()
}
}
package band.effective.office.tablet.core.ui.inactivity
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.Window
/**
* A Window.Callback wrapper that intercepts touch and key events to track user activity.
* It preserves the original Window.Callback behavior by delegating to it after intercepting events.
*
* @param original The original Window.Callback to delegate to.
*/
class InactivityWindowCallback(private val original: Window.Callback) : Window.Callback by original {
/**
* Intercepts touch events and notifies the InactivityManager before delegating to the original callback.
*
* @param event The touch event.
* @return The result of the original callback's dispatchTouchEvent method.
*/
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
// Notify the InactivityManager that a user interaction has occurred
InactivityManager.onUserInteraction()
// Delegate to the original callback
return original.dispatchTouchEvent(event)
}
/**
* Intercepts key events and notifies the InactivityManager before delegating to the original callback.
*
* @param event The key event.
* @return The result of the original callback's dispatchKeyEvent method.
*/
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
// Notify the InactivityManager that a user interaction has occurred
InactivityManager.onUserInteraction()
// Delegate to the original callback
return original.dispatchKeyEvent(event)
}
}
\ Нет новой строки в конце файла
......@@ -47,7 +47,7 @@ import band.effective.office.tablet.core.ui.utils.DateDisplayMapper
fun DateTimeView(
modifier: Modifier,
selectDate: LocalDateTime,
currentDate: LocalDateTime? = null,
currentDate: LocalDateTime,
increment: () -> Unit,
decrement: () -> Unit,
onOpenDateTimePickerModal: () -> Unit,
......@@ -90,7 +90,7 @@ private fun RowScope.NextDateButton(increment: () -> Unit) {
private fun RowScope.SelectedDate(
onOpenDateTimePickerModal: () -> Unit,
selectDate: LocalDateTime,
currentDate: LocalDateTime?
currentDate: LocalDateTime
) {
val timeDayMonthDateFormat = remember { dateTimeFormat }
val dayMonthDateFormat = remember { dayMonthFormat }
......@@ -101,42 +101,67 @@ private fun RowScope.SelectedDate(
else AnimatedContentTransitionScope.SlideDirection.Right
}
val displayedFormat by remember(selectDate) {
mutableStateOf(
if (currentDate != null && selectDate.date > currentDate.date) dayMonthDateFormat else timeDayMonthDateFormat
)
}
LaunchedEffect(selectDate) {
previousDate = selectDate
}
Button(
modifier = Modifier
.fillMaxHeight()
.weight(4f)
.clip(RoundedCornerShape(15.dp)),
onClick = onOpenDateTimePickerModal,
colors = ButtonDefaults.buttonColors(
containerColor = LocalCustomColorsPalette.current.elevationBackground
),
contentPadding = PaddingValues(0.dp)
) {
AnimatedContent(
targetState = DateDisplayMapper.map(
selectDate = selectDate,
currentDate = currentDate
if (currentDate.date < selectDate.date) {
Button(
modifier = Modifier
.fillMaxHeight()
.weight(4f)
.clip(RoundedCornerShape(15.dp)),
onClick = onOpenDateTimePickerModal,
colors = ButtonDefaults.buttonColors(
containerColor = LocalCustomColorsPalette.current.elevationBackground
),
contentPadding = PaddingValues(0.dp)
) {
AnimatedContent(
targetState = DateDisplayMapper.map(
selectDate = selectDate,
currentDate = currentDate
),
transitionSpec = {
slideIntoContainer(slideDirection) + fadeIn() with
slideOutOfContainer(slideDirection.opposite()) + fadeOut()
},
label = "AnimatedDateChange"
) { formattedDate ->
Text(
text = formattedDate,
style = MaterialTheme.typography.h6
)
}
}
} else {
Button(
modifier = Modifier
.fillMaxHeight()
.weight(4f)
.clip(RoundedCornerShape(15.dp)),
onClick = onOpenDateTimePickerModal,
colors = ButtonDefaults.buttonColors(
containerColor = LocalCustomColorsPalette.current.elevationBackground
),
transitionSpec = {
slideIntoContainer(slideDirection) + fadeIn() with
slideOutOfContainer(slideDirection.opposite()) + fadeOut()
},
label = "AnimatedDateChange"
) { formattedDate ->
Text(
text = formattedDate,
style = MaterialTheme.typography.h6
)
contentPadding = PaddingValues(0.dp)
) {
AnimatedContent(
targetState = DateDisplayMapper.map(
selectDate = currentDate,
currentDate = currentDate
),
transitionSpec = {
slideIntoContainer(slideDirection) + fadeIn() with
slideOutOfContainer(slideDirection.opposite()) + fadeOut()
},
label = "AnimatedDateChange"
) { formattedDate ->
Text(
text = formattedDate,
style = MaterialTheme.typography.h6
)
}
}
}
}
......
......@@ -197,7 +197,8 @@ private fun BookingEditor(
increment = incrementData,
decrement = decrementData,
onOpenDateTimePickerModal = onOpenDateTimePickerModal,
showTitle = true
showTitle = true,
currentDate = selectData,
)
Spacer(modifier = Modifier.height(15.dp))
EventDurationView(
......
package band.effective.office.tablet.feature.bookingEditor.presentation
import band.effective.office.tablet.core.domain.OfficeTime
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.Organizer
import band.effective.office.tablet.core.domain.model.Slot
import band.effective.office.tablet.core.domain.unbox
import band.effective.office.tablet.core.domain.useCase.CheckBookingUseCase
import band.effective.office.tablet.core.domain.useCase.CreateBookingUseCase
import band.effective.office.tablet.core.domain.useCase.DeleteBookingUseCase
import band.effective.office.tablet.core.domain.useCase.OrganizersInfoUseCase
import band.effective.office.tablet.core.domain.useCase.UpdateBookingUseCase
import band.effective.office.tablet.core.domain.util.asInstant
......@@ -24,6 +24,7 @@ import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
......@@ -41,11 +42,12 @@ import org.koin.core.component.inject
* Component responsible for editing booking events.
* Handles creating new bookings and updating existing ones.
*/
const val DELETE_SUCCESS_DELAY = 2000L
class BookingEditorComponent(
componentContext: ComponentContext,
initialEvent: EventInfo,
val roomName: String,
private val onDeleteEvent: (Slot) -> Unit,
private val onCloseRequest: () -> Unit,
) : ComponentContext by componentContext, KoinComponent, ModalWindow {
......@@ -68,6 +70,7 @@ class BookingEditorComponent(
private val checkBookingUseCase: CheckBookingUseCase by inject()
private val updateBookingUseCase: UpdateBookingUseCase by inject()
private val createBookingUseCase: CreateBookingUseCase by inject()
private val deleteBookingUseCase: DeleteBookingUseCase by inject()
// Mappers
private val eventInfoMapper: EventInfoMapper by inject()
......@@ -158,9 +161,25 @@ class BookingEditorComponent(
*/
private fun deleteEvent() = coroutineScope.launch {
mutableState.update { it.copy(isLoadDelete = true) }
onDeleteEvent(eventInfoMapper.mapToSlot(state.value.event))
mutableState.update { it.copy(isLoadDelete = false) }
onCloseRequest()
val deleteResult = withContext(Dispatchers.IO) {
deleteBookingUseCase(roomName, state.value.event)
}
when (deleteResult) {
is Either.Error -> {
mutableState.update {
it.copy(
isLoadDelete = false,
isErrorDelete = true,
)
}
}
is Either.Success -> {
delay(DELETE_SUCCESS_DELAY)
onCloseRequest()
}
}
}
/**
......
......@@ -90,8 +90,15 @@ fun RoomInfoLeftPanel(
SlotView(
slotUi = it,
onClick = { slotComponent.sendIntent(SlotIntent.ClickToEdit(this)) },
onToggle = { (it as? SlotUi.MultiSlot)?.let { slotComponent.sendIntent(SlotIntent.ClickToToggle(it)) } },
onCancel = { deleteSlot -> slotComponent.sendIntent(SlotIntent.OnCancelDelete(deleteSlot)) }
onToggle = {
(it as? SlotUi.MultiSlot)?.let {
slotComponent.sendIntent(
SlotIntent.ClickToToggle(
it
)
)
}
},
)
}
Spacer(Modifier.height(20.dp))
......
package band.effective.office.tablet.feature.main.domain
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
/**
* A singleton that holds the current time.
*/
object CurrentTimeHolder {
private val defaultTimeZone = TimeZone.Companion.currentSystemDefault()
private val _currentTime = MutableStateFlow(Clock.System.now().toLocalDateTime(defaultTimeZone))
val currentTime: StateFlow<LocalDateTime> = _currentTime.asStateFlow()
/**
* Updates the current time.
*/
fun updateTime(time: LocalDateTime) {
_currentTime.value = time
}
/**
* Gets the current time.
*/
fun getCurrentTime(): LocalDateTime {
return _currentTime.value
}
}
\ Нет новой строки в конце файла
......@@ -2,9 +2,9 @@ package band.effective.office.tablet.feature.main.presentation.main
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorWithData
import band.effective.office.tablet.core.domain.manager.DateResetManager
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
import band.effective.office.tablet.core.domain.model.Slot
import band.effective.office.tablet.core.domain.useCase.CheckSettingsUseCase
import band.effective.office.tablet.core.domain.useCase.DeleteBookingUseCase
import band.effective.office.tablet.core.domain.useCase.RoomInfoUseCase
......@@ -15,6 +15,7 @@ import band.effective.office.tablet.core.domain.util.currentLocalDateTime
import band.effective.office.tablet.core.domain.util.minus
import band.effective.office.tablet.core.domain.util.plus
import band.effective.office.tablet.core.ui.utils.componentCoroutineScope
import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder
import band.effective.office.tablet.feature.main.domain.GetRoomIndexUseCase
import band.effective.office.tablet.feature.main.domain.GetTimeToNextEventUseCase
import band.effective.office.tablet.feature.slot.presentation.SlotComponent
......@@ -22,7 +23,6 @@ import band.effective.office.tablet.feature.slot.presentation.SlotIntent
import com.arkivanov.decompose.ComponentContext
import kotlin.math.abs
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.Dispatchers
......@@ -94,6 +94,23 @@ class MainComponent(
// Set up event listeners
setupEventListeners()
// Initialize date reset manager
initializeDateResetManager()
}
/**
* Initializes the DateResetManager to handle date reset on inactivity.
* This registers a callback that will reset the selected date and current room
* when inactivity is detected.
*/
private fun initializeDateResetManager() {
DateResetManager.registerDateResetCallback { date ->
mutableState.update { it.copy(selectedDate = date) }
reboot(refresh = true, resetSelectRoom = true)
updateTimeToNextEvent()
slotComponent.sendIntent(SlotIntent.InactivityTimeout)
}
}
/**
......@@ -110,13 +127,6 @@ class MainComponent(
}
}
// Update time to next event periodically
timerUseCase.timer(coroutineScope, 1.seconds) { _ ->
withContext(Dispatchers.Main) {
updateTimeToNextEvent()
}
}
// Listen for room info changes
coroutineScope.launch(Dispatchers.Main) {
roomInfoUseCase.subscribe().collect { roomsInfo ->
......@@ -126,12 +136,8 @@ class MainComponent(
}
}
// reset select date
currentTimeTimer.start(1.minutes) {
withContext(Dispatchers.Main) {
mutableState.update { it.copy(selectedDate = currentLocalDateTime) }
slotComponent.sendIntent(SlotIntent.UpdateDate(currentLocalDateTime))
}
coroutineScope.launch {
CurrentTimeHolder.currentTime.collect { updateTimeToNextEvent() }
}
}
......@@ -199,34 +205,6 @@ class MainComponent(
}
}
/**
* Handles deleting an event.
*/
fun handleDeleteEvent(slot: Slot) {
slotComponent.sendIntent(
SlotIntent.Delete(
slot = slot,
onDelete = {
deleteEventFromSlot(slot)
}
)
)
}
/**
* Deletes an event from a slot.
*/
private fun deleteEventFromSlot(slot: Slot) {
coroutineScope.launch {
(slot as? Slot.EventSlot)?.eventInfo?.let { eventInfo ->
deleteBookingUseCase(
eventInfo = eventInfo,
roomName = getCurrentRoomName()
)
}
}
}
/**
* Updates the selected date.
*/
......@@ -239,7 +217,8 @@ class MainComponent(
// Only update if the new date is not in the past
if (newDate.date >= currentLocalDateTime.date) {
mutableState.update { it.copy(selectedDate = newDate) }
slotComponent.sendIntent(SlotIntent.UpdateDate(newDate))
val selectedRoom = state.value.roomList[state.value.indexSelectRoom]
slotComponent.sendIntent(SlotIntent.UpdateRequest(selectedRoom.name, state.value.selectedDate))
}
}
......@@ -270,18 +249,10 @@ class MainComponent(
val selectedRoom = state.value.roomList.getOrNull(index)
if (selectedRoom != null) {
updateComponents(selectedRoom, state.value.selectedDate)
slotComponent.sendIntent(SlotIntent.UpdateRequest(room = selectedRoom.name, state.value.selectedDate))
}
}
/**
* Updates child components with new room and date information.
*/
private fun updateComponents(roomInfo: RoomInfo, date: LocalDateTime) {
slotComponent.sendIntent(SlotIntent.UpdateRequest(room = roomInfo.name))
slotComponent.sendIntent(SlotIntent.UpdateDate(date))
}
/**
* Data class to hold the result of loading rooms.
*/
......@@ -348,7 +319,7 @@ class MainComponent(
)
} else {
val selectedRoom = roomsResult.roomList[roomsResult.indexSelectRoom.coerceIn(0, roomsResult.roomList.size - 1)]
updateComponents(selectedRoom, it.selectedDate)
slotComponent.sendIntent(SlotIntent.UpdateRequest(selectedRoom.name, state.value.selectedDate))
it.copy(
isLoad = false,
isData = roomsResult.isSuccess,
......@@ -386,7 +357,7 @@ class MainComponent(
loadRooms(roomIndex)
currentState.roomList.getOrNull(roomIndex)?.let { roomInfo ->
updateComponents(roomInfo, currentState.selectedDate)
slotComponent.sendIntent(SlotIntent.UpdateRequest(roomInfo.name, currentState.selectedDate))
}
}
......
......@@ -11,12 +11,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import band.effective.office.tablet.core.ui.LoadMainScreen
import band.effective.office.tablet.core.ui.common.ErrorMainScreen
import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class)
@Composable
fun MainScreen(component: MainComponent) {
val state by component.state.collectAsState()
val currentDate by CurrentTimeHolder.currentTime.collectAsState()
Column(
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
......@@ -39,7 +41,7 @@ fun MainScreen(component: MainComponent) {
onIncrementData = { component.sendIntent(Intent.OnUpdateSelectDate(updateInDays = 1)) },
onDecrementData = { component.sendIntent(Intent.OnUpdateSelectDate(updateInDays = -1)) },
selectedDate = state.selectedDate,
currentDate = state.currentDate,
currentDate = currentDate,
onOpenDateTimePickerModalRequest = {}, // TODO
)
}
......
......@@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDateTime
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
......@@ -60,6 +59,24 @@ class SlotComponent(
}
}
private fun resetAllMultiSlotStates() {
val slots = state.value.slots.toMutableList()
var hasChanges = false
// Find all MultiSlot instances that are open and close them
slots.forEachIndexed { index, slot ->
if (slot is SlotUi.MultiSlot && slot.isOpen) {
slots[index] = slot.copy(isOpen = false)
hasChanges = true
}
}
// Only update state if there were changes
if (hasChanges) {
mutableState.update { it.copy(slots = slots) }
}
}
private suspend fun updateSlots(uiSlots: List<SlotUi>) = withContext(Dispatchers.Main.immediate) {
if (uiSlots.isNotEmpty()) {
val firstSlotStartInstant = uiSlots.first().slot.start.asInstant
......@@ -74,10 +91,8 @@ class SlotComponent(
when (intent) {
is SlotIntent.ClickToEdit -> handleClickToEdit(intent.slot)
is SlotIntent.ClickToToggle -> openMultislot(intent.slot)
is SlotIntent.Delete -> deleteSlot(intent)
is SlotIntent.OnCancelDelete -> cancelDeletingSlot(intent)
is SlotIntent.UpdateDate -> updateDate(intent.newDate)
is SlotIntent.UpdateRequest -> updateRequest(intent)
SlotIntent.InactivityTimeout -> resetAllMultiSlotStates()
}
}
......@@ -90,83 +105,20 @@ class SlotComponent(
}
private fun updateRequest(intent: SlotIntent.UpdateRequest) = coroutineScope.launch {
roomInfoUseCase.getRoom(intent.room)?.let { roomInfo ->
val slots = getSlotsByRoomUseCase(roomInfo = roomInfo)
roomInfoUseCase.getRoom(room = intent.room)?.let { roomInfo ->
val slots = getSlotsByRoomUseCase(
roomInfo = roomInfo,
start = maxOf(
OfficeTime.startWorkTime(intent.newDate.date).asInstant,
currentInstant,
).asLocalDateTime
)
val uiSlots = slots.map(slotUiMapper::map)
mutableState.update { it.copy(slots = uiSlots) }
}
}
private fun cancelDeletingSlot(intent: SlotIntent.OnCancelDelete) {
val slots = state.value.slots
val original = intent.slot.original
val newSlots = if (intent.slot.mainSlotIndex == null) {
slots.toMutableList().apply { this[intent.slot.index] = original }
} else {
val mainSlot =
(slots[intent.slot.mainSlotIndex as Int] as SlotUi.MultiSlot).run {
copy(
subSlots = subSlots.toMutableList()
.apply { this[intent.slot.index] = original }
)
}
slots.toMutableList().apply { this[intent.slot.mainSlotIndex as Int] = mainSlot }
// Use updateSlots instead of directly updating state to preserve DeleteSlot and isOpen states
updateSlots(uiSlots)
}
mutableState.update { it.copy(slots = newSlots) }
}
private fun deleteSlot(intent: SlotIntent.Delete) {
val slots = state.value.slots
var mainSlot: SlotUi.MultiSlot? = null
val uiSlot = slots.firstOrNull { it.slot == intent.slot }
?: slots.mapNotNull { (it as? SlotUi.MultiSlot)?.subSlots }.flatten()
.firstOrNull { it.slot == intent.slot }
?.apply {
mainSlot = slots.mapNotNull { it as? SlotUi.MultiSlot }
.first { it.subSlots.contains(this) }
}
when {
uiSlot == null -> {}
mainSlot != null -> {
val indexInMultiSlot = mainSlot!!.subSlots.indexOf(uiSlot)
val indexMultiSlot = slots.indexOf(mainSlot)
val newMainSlot = mainSlot!!.copy(
subSlots = mainSlot!!.subSlots.toMutableList().apply {
this[indexInMultiSlot] =
SlotUi.DeleteSlot(
slot = intent.slot,
onDelete = intent.onDelete,
original = uiSlot,
index = indexInMultiSlot,
mainSlotIndex = indexMultiSlot
)
})
mutableState.update {
it.copy(slots = slots.toMutableList().apply {
this[indexMultiSlot] = newMainSlot
})
}
}
else -> {
val index = slots.indexOf(uiSlot)
mutableState.update {
it.copy(
slots = slots.toMutableList().apply {
this[index] =
SlotUi.DeleteSlot(
slot = intent.slot,
onDelete = intent.onDelete,
original = uiSlot,
index = index,
mainSlotIndex = null
)
}
)
}
}
}
}
private fun Slot.execute() = when (this) {
is Slot.EmptySlot -> executeFreeSlot(this)
is Slot.EventSlot -> executeEventSlot(this)
......@@ -205,20 +157,6 @@ class SlotComponent(
isLoading = false,
)
private fun updateDate(newDate: LocalDateTime) = coroutineScope.launch {
roomInfoUseCase.getRoom(room = roomName())?.let { roomInfo ->
val slots = getSlotsByRoomUseCase(
roomInfo = roomInfo,
start = maxOf(
OfficeTime.startWorkTime(newDate.date).asInstant,
currentInstant,
).asLocalDateTime
)
val uiSlots = slots.map(slotUiMapper::map)
mutableState.update { it.copy(slots = uiSlots) }
}
}
private fun setupRoomAvailabilityWatcher() {
updateTimer.init(SLOT_UPDATE_INTERVAL_MINUTES) {
withContext(Dispatchers.Main) {
......
package band.effective.office.tablet.feature.slot.presentation
import band.effective.office.tablet.core.domain.model.Slot
import kotlinx.datetime.LocalDateTime
sealed interface SlotIntent {
data class ClickToEdit(val slot: SlotUi) : SlotIntent
data class ClickToToggle(val slot: SlotUi.MultiSlot) : SlotIntent
data class UpdateDate(val newDate: LocalDateTime) : SlotIntent
data class UpdateRequest(val room: String) : SlotIntent
data class Delete(val slot: Slot, val onDelete: () -> Unit) : SlotIntent
data class OnCancelDelete(val slot: SlotUi.DeleteSlot) : SlotIntent
data class UpdateRequest(val room: String, val newDate: LocalDateTime) : SlotIntent
data object InactivityTimeout: SlotIntent
}
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать