diff --git a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.android.kt b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.android.kt index babbbbe2df3c9fb6c550a004d9f9ee1c193c550c..262007a3a0dabba75d84fcd86fa52385e9617ddc 100644 --- a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.android.kt +++ b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.android.kt @@ -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 diff --git a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.kt b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.kt index c5c283e46b39e427bbf8d511c51159197d93e29d..446f999b6b5f87b0472ec5edd31ff8d3a47a8633 100644 --- a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.kt +++ b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.kt @@ -1,13 +1,15 @@ 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() + } + ) + } } \ No newline at end of file diff --git a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..60c5e84f787653ce72fa36b9c9b22c96b96cc4d4 --- /dev/null +++ b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt @@ -0,0 +1,56 @@ +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 = 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()) + } +} diff --git a/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/root/RootComponent.kt b/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/root/RootComponent.kt index 1ad530cd5a0a8f59f2ce70f00a431bd7b8af7759..d50101048ab69cd05f57073f75600c36ad2bbf88 100644 --- a/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/root/RootComponent.kt +++ b/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/root/RootComponent.kt @@ -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 diff --git a/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt b/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..59afa56c0fa56319ba169c2642ed8af41ee12bbf --- /dev/null +++ b/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt @@ -0,0 +1,14 @@ +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 +} diff --git a/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt b/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..17e7c1556180125be9e1adabf34fcf670f1978fe --- /dev/null +++ b/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt @@ -0,0 +1,44 @@ +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 = 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()) + } +} diff --git a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/manager/DateResetManager.kt b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/manager/DateResetManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..2324e09259948dba2c1aa6301941a1b7cb167fb4 --- /dev/null +++ b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/manager/DateResetManager.kt @@ -0,0 +1,51 @@ +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) + } + } +} diff --git a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SlotUseCase.kt b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SlotUseCase.kt index 470d539fe71aeecfcaa47c8322d4d4614e707819..bda9f984c0c04fb606154b4d54e39d38ad003134 100644 --- a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SlotUseCase.kt +++ b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SlotUseCase.kt @@ -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 } diff --git a/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityLifecycleCallbacks.kt b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityLifecycleCallbacks.kt new file mode 100644 index 0000000000000000000000000000000000000000..bfeb2d4ca6dba3a3d9af09f87f3d7ea9d6087acd --- /dev/null +++ b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityLifecycleCallbacks.kt @@ -0,0 +1,70 @@ +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() + } + } + } +} diff --git a/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityManager.kt b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..12eb6b0b2eef749dfae7d3119c60a4e6a60a5e06 --- /dev/null +++ b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityManager.kt @@ -0,0 +1,76 @@ +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 = _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() + } +} diff --git a/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityWindowCallback.kt b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityWindowCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..645bd35f60e7f22acd0d8c3c7c89b12095f39bc7 --- /dev/null +++ b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityWindowCallback.kt @@ -0,0 +1,42 @@ +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) + } +} \ No newline at end of file diff --git a/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/date/DateTimeView.kt b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/date/DateTimeView.kt index 7af3ec7d9b8d7960ca065f7214d5a3078ea5c670..810cd692781d139ebc3850eb880363c0bd690e80 100644 --- a/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/date/DateTimeView.kt +++ b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/date/DateTimeView.kt @@ -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 + ) + } } } } diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditor.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditor.kt index 92e6b5ee2f8a562fdb53985f8c9514283d8fc122..a80594a06482cee8d895e4039f034c7f1a742941 100644 --- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditor.kt +++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditor.kt @@ -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( diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt index 08f4e5a96841c05894a511bc596d6fbee767ca69..ae40b882613a973c3ce3294d3fad6d9efc37ef33 100644 --- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt +++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt @@ -1,12 +1,12 @@ 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() + } + } } /** diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/RoomInfoLeftPanel.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/RoomInfoLeftPanel.kt index 7171e485a82430803d3953f34b01c647298267d9..1e19aeae9d0d3fc9abeb55bbdb5438973cfc7bca 100644 --- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/RoomInfoLeftPanel.kt +++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/RoomInfoLeftPanel.kt @@ -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)) diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..50204e6104d50596b4b39c5fc21e8d49c0ca2ca2 --- /dev/null +++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt @@ -0,0 +1,33 @@ +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 = _currentTime.asStateFlow() + + /** + * Updates the current time. + */ + fun updateTime(time: LocalDateTime) { + _currentTime.value = time + } + + /** + * Gets the current time. + */ + fun getCurrentTime(): LocalDateTime { + return _currentTime.value + } +} \ No newline at end of file diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainComponent.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainComponent.kt index 2d293d84c52985c90d4bddb845c3d5702a9800ef..04dd80f5c079c8a178e4559ddbc832817941e37e 100644 --- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainComponent.kt +++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainComponent.kt @@ -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)) } } diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainScreen.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainScreen.kt index 629febe2eb66dfb61c2b06e1a7652d508f9aefd8..1b5be6381d7f97b7eb64f806f214b21c4be2f6fd 100644 --- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainScreen.kt +++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainScreen.kt @@ -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 ) } diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotComponent.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotComponent.kt index 74bf5858deb376d9000c387ed8969e75945a49a0..e140a019d38cf18c7964f50c3580678bccc4ed1d 100644 --- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotComponent.kt +++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotComponent.kt @@ -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) = 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) { diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotIntent.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotIntent.kt index bcb31aeefdfefcf3b2653d350b3fe784c1124523..ec6faa411220f4e825cf467d69f717a5b48ebbfd 100644 --- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotIntent.kt +++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotIntent.kt @@ -1,14 +1,11 @@ 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 } diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotUi.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotUi.kt index e61244349dbe2edcd9618890193fa25285499d26..2270986f32bcd6fb67878b7074126bd1b9905048 100644 --- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotUi.kt +++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotUi.kt @@ -9,14 +9,6 @@ sealed interface SlotUi { data class MultiSlot(override val slot: Slot, val subSlots: List, val isOpen: Boolean) : SlotUi - data class DeleteSlot( - override val slot: Slot, - val onDelete: () -> Unit, - val original: SlotUi, - val index: Int, - val mainSlotIndex: Int? - ) : SlotUi - data class NestedSlot(override val slot: Slot) : SlotUi data class LoadingSlot(override val slot: Slot) : SlotUi } diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/BorderIndicator.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/BorderIndicator.kt deleted file mode 100644 index 80610fc8e296adb2da74dbb39c06049d358e88ac..0000000000000000000000000000000000000000 --- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/BorderIndicator.kt +++ /dev/null @@ -1,91 +0,0 @@ -package band.effective.office.tablet.feature.slot.presentation.components - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -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.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.RoundRect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.PathMeasure -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import band.effective.office.tablet.core.ui.theme.deleteProgressColor -import kotlinx.coroutines.delay - -//https://stackoverflow.com/questions/75745905/rectangle-border-progress-bar -@Composable -fun BorderIndicator( - modifier: Modifier = Modifier.fillMaxSize(), - startDurationInSeconds: Int = 10, - stokeWidth: Dp, - onDispose: () -> Unit -) { - var targetValue by remember { - mutableStateOf(100f) - } - // This is the progress path which wis changed using path measure - val pathWithProgress by remember { mutableStateOf(Path()) } - - // using path - val pathMeasure by remember { mutableStateOf(PathMeasure()) } - - val path = remember { Path() } - - val progress by animateFloatAsState( - targetValue = targetValue, - animationSpec = tween(startDurationInSeconds * 1000, easing = LinearEasing), label = "" - ) - - Box(contentAlignment = Alignment.Center) { - Canvas(modifier = modifier) { - if (path.isEmpty) { - path.addRoundRect( - RoundRect( - Rect(offset = Offset.Zero, size), - cornerRadius = CornerRadius(100.dp.toPx(), 100.dp.toPx()) - ) - ) - } - pathWithProgress.reset() - pathMeasure.setPath(path, forceClosed = false) - pathMeasure.getSegment( - startDistance = 0f, - stopDistance = pathMeasure.length * progress / 100f, - destination = pathWithProgress, - startWithMoveTo = true - ) - - drawPath( - path = path, - style = Stroke(stokeWidth.toPx()), - color = Color.Gray - ) - - drawPath( - path = pathWithProgress, - style = Stroke(stokeWidth.toPx()), - color = deleteProgressColor - ) - } - } - LaunchedEffect(Unit) { - targetValue = 0f - delay(startDurationInSeconds * 1000L) - onDispose() - } -} \ No newline at end of file diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/DeletedSlotView.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/DeletedSlotView.kt deleted file mode 100644 index a0c0eb9dd9b4cb0fe325998cff5506a7f5f4da36..0000000000000000000000000000000000000000 --- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/DeletedSlotView.kt +++ /dev/null @@ -1,56 +0,0 @@ -package band.effective.office.tablet.feature.slot.presentation.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -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.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import band.effective.office.tablet.core.ui.Res -import band.effective.office.tablet.core.ui.cancel -import band.effective.office.tablet.core.ui.theme.h7 -import band.effective.office.tablet.feature.slot.presentation.SlotUi -import org.jetbrains.compose.resources.stringResource - -@Composable -fun DeletedSlotView( - modifier: Modifier = Modifier, - slotUi: SlotUi.DeleteSlot, - onCancel: (SlotUi.DeleteSlot) -> Unit, - paddingValues: PaddingValues -) { - Box(modifier = modifier.height(IntrinsicSize.Min).width(IntrinsicSize.Min)) { - var isCancelability by remember { mutableStateOf(true) } - CommonSlotView( - modifier = Modifier - .padding(paddingValues) - .fillMaxWidth(), - slotUi = slotUi - ) { - if (isCancelability) { - Text( - modifier = Modifier.clickable { onCancel(slotUi) }, - text = stringResource(Res.string.cancel), - style = MaterialTheme.typography.h7, - color = Color.White - ) - } - } - BorderIndicator(onDispose = { - slotUi.onDelete() - isCancelability = false - }, stokeWidth = 10.dp) - } -} diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/MultiSlotView.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/MultiSlotView.kt index d522e9f663c6e115d2a7354ea3903d9b9daf5efa..0fe0e9153f67ec1d46598cc0e093744ba2ab2412 100644 --- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/MultiSlotView.kt +++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/MultiSlotView.kt @@ -2,7 +2,6 @@ package band.effective.office.tablet.feature.slot.presentation.components import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -21,19 +20,16 @@ fun MultiSlotView( modifier: Modifier = Modifier, slotUi: SlotUi.MultiSlot, onItemClick: SlotUi.() -> Unit, - onToggle: SlotUi.() -> Unit, - onCancel: (SlotUi.DeleteSlot) -> Unit ) { Column(Modifier.animateContentSize()) { CommonSlotView( - modifier = modifier.clickable { onToggle(slotUi) }, + modifier = modifier, slotUi = slotUi ) { Image( modifier = Modifier .fillMaxHeight() - .rotate(if (slotUi.isOpen) 180f else 0f) - .clickable { onToggle(slotUi) }, + .rotate(if (slotUi.isOpen) 180f else 0f), painter = painterResource(Res.drawable.arrow_to_down), contentDescription = null ) @@ -45,8 +41,7 @@ fun MultiSlotView( SlotView( slotUi = it, onClick = onItemClick, - onToggle = onToggle, - onCancel = onCancel + onToggle = { /*Nothing*/ }, ) } } diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/SlotView.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/SlotView.kt index e96e33cc1b0e5515c8d4d85fae9a85707294444c..4c1b0dec435acb1eb6ba380ce6f1cefa50143236 100644 --- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/SlotView.kt +++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/SlotView.kt @@ -3,7 +3,6 @@ package band.effective.office.tablet.feature.slot.presentation.components import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape @@ -11,7 +10,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import band.effective.office.tablet.core.domain.model.Slot import band.effective.office.tablet.core.domain.util.freeTime @@ -30,44 +28,31 @@ fun SlotView( slotUi: SlotUi, onClick: SlotUi.() -> Unit, onToggle: SlotUi.() -> Unit, - onCancel: (SlotUi.DeleteSlot) -> Unit ) { val borderShape = CircleShape - val baseModifier = Modifier + + val itemModifier = Modifier .fillMaxWidth() .clip(borderShape) - .background(MaterialTheme.colorScheme.surface) - - val itemModifier = baseModifier - .run { - when (slotUi) { - is SlotUi.MultiSlot -> this - else -> clickable { slotUi.onClick() } - } - } .then( - if (slotUi !is SlotUi.DeleteSlot) Modifier.border( - width = 5.dp, - color = slotUi.borderColor(), - shape = borderShape - ).padding(vertical = 15.dp, horizontal = 30.dp) - else Modifier + if (slotUi is SlotUi.MultiSlot) + Modifier.clickable { slotUi.onToggle() } + else + Modifier.clickable { slotUi.onClick() } ) + .background(MaterialTheme.colorScheme.surface) + .border( + width = 5.dp, + color = slotUi.borderColor(), + shape = borderShape + ).padding(vertical = 15.dp, horizontal = 30.dp) when (slotUi) { - is SlotUi.DeleteSlot -> DeletedSlotView( - modifier = itemModifier, - slotUi = slotUi, - onCancel = onCancel, - paddingValues = PaddingValues(vertical = 15.dp, horizontal = 30.dp) - ) is SlotUi.MultiSlot -> MultiSlotView( modifier = itemModifier, slotUi = slotUi, onItemClick = onClick, - onToggle = onToggle, - onCancel = onCancel ) is SlotUi.SimpleSlot -> CommonSlotView( @@ -89,7 +74,6 @@ fun SlotView( @Composable private fun SlotUi.borderColor() = when (this) { - is SlotUi.DeleteSlot -> Color.Gray is SlotUi.MultiSlot -> LocalCustomColorsPalette.current.busyStatus is SlotUi.NestedSlot -> subslotColor is SlotUi.SimpleSlot -> when (slot) { diff --git a/gradle.properties b/gradle.properties index cfac532387f9fee05f205ef648a58e582b1c81f5..7ec2d05f8aad151003a60e4ddb008781ad87e318 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,6 @@ kotlin.incremental=true # Project properties group=band.effective.office -version=0.0.2 +version=0.0.3 android.useAndroidX=true