├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── iosApp
├── iosApp
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── app-icon-1024.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── iOSApp.swift
│ ├── Info.plist
│ └── ContentView.swift
├── Configuration
│ └── Config.xcconfig
├── .gitignore
└── iosApp.xcodeproj
│ └── project.pbxproj
├── composeApp
├── src
│ ├── androidMain
│ │ ├── res
│ │ │ ├── values
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── macaosoftware
│ │ │ │ └── ui
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── webMain
│ │ ├── resources
│ │ │ ├── styles.css
│ │ │ └── index.html
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── macaosoftware
│ │ │ └── ui
│ │ │ └── main.kt
│ ├── commonMain
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── macaosoftware
│ │ │ │ └── ui
│ │ │ │ ├── ui
│ │ │ │ ├── SlotsViewType.kt
│ │ │ │ ├── AlertDialogEventsState.kt
│ │ │ │ ├── model
│ │ │ │ │ └── AllDayEvent.kt
│ │ │ │ ├── BottomSheetEventsState.kt
│ │ │ │ ├── DayScheduleAppAlertDialog.kt
│ │ │ │ ├── DayScheduleAppActionsBottomView.kt
│ │ │ │ ├── DayScheduleApp.kt
│ │ │ │ └── DayScheduleAppViewModel.kt
│ │ │ │ └── data
│ │ │ │ ├── Constants.kt
│ │ │ │ ├── TimeSlotsDataSample.kt
│ │ │ │ ├── EpgSlotsDataSample.kt
│ │ │ │ └── DecimalSlotsDataSample.kt
│ │ └── composeResources
│ │ │ └── drawable
│ │ │ └── compose-multiplatform.xml
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── macaosoftware
│ │ │ └── ui
│ │ │ └── MainViewController.kt
│ ├── commonTest
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── macaosoftware
│ │ │ └── ui
│ │ │ └── ComposeAppCommonTest.kt
│ └── jvmMain
│ │ └── kotlin
│ │ └── com
│ │ └── macaosoftware
│ │ └── ui
│ │ └── main.kt
├── webpack.config.d
│ └── watch.js
└── build.gradle.kts
├── daily-agenda-view
├── src
│ ├── webMain
│ │ └── resources
│ │ │ ├── styles.css
│ │ │ └── index.html
│ ├── androidMain
│ │ └── AndroidManifest.xml
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── macaosoftware
│ │ │ └── ui
│ │ │ └── MainViewController.kt
│ ├── commonTest
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── macaosoftware
│ │ │ └── ui
│ │ │ └── ComposeAppCommonTest.kt
│ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── macaosoftware
│ │ └── ui
│ │ └── dailyagenda
│ │ ├── epgslots
│ │ ├── EpgSlotsDataUpdater.kt
│ │ ├── EpgSlotsState.kt
│ │ ├── EpgSlotsView.kt
│ │ ├── EpgSlotsStateController.kt
│ │ └── EpgSlotsLayout.kt
│ │ ├── slotslayer
│ │ ├── SlotsLayerState.kt
│ │ └── SlotsLayer.kt
│ │ ├── marker
│ │ ├── CurrentTimeMarkerView.kt
│ │ └── CurrentTimeMarkerStateController.kt
│ │ ├── decimalslots
│ │ ├── DecimalSlotsView.kt
│ │ ├── RtlCustomRow.kt
│ │ ├── DecimalSlotsStateController.kt
│ │ ├── DecimalSlotsBaseLayoutState.kt
│ │ ├── DecimalSlotsBaseLayoutStateController.kt
│ │ ├── DecimalSlotsDataUpdater.kt
│ │ ├── DecimalSlotsBaseLayout.kt
│ │ └── LayoutUtil.kt
│ │ └── timeslots
│ │ ├── TimeSlotsView.kt
│ │ ├── TimeSlotsDataUpdater.kt
│ │ ├── TimeSlotsStateController.kt
│ │ └── TimeSlotsState.kt
└── build.gradle.kts
├── gradle.properties
├── kotlin-js-store
└── wasm
│ └── yarn.lock
├── .gitignore
├── settings.gradle.kts
├── .github
└── workflows
│ └── deploy-page.yaml
├── gradlew.bat
├── gradlew
└── README.md
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | DailyAgendaView
3 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/composeApp/src/webMain/resources/styles.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | overflow: hidden;
7 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/ui/SlotsViewType.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.ui
2 |
3 | enum class SlotsViewType {
4 | Decimal, Timeline, Epg
5 | }
--------------------------------------------------------------------------------
/daily-agenda-view/src/webMain/resources/styles.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | overflow: hidden;
7 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/data/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.data
2 |
3 | object Constants {
4 |
5 | const val EmptyDescription = ""
6 | }
--------------------------------------------------------------------------------
/daily-agenda-view/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pablichjenkov/daily-agenda-view/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iOSApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 |
3 | PRODUCT_NAME=DailyAgendaView
4 | PRODUCT_BUNDLE_IDENTIFIER=com.macaosoftware.ui.DailyAgendaView$(TEAM_ID)
5 |
6 | CURRENT_PROJECT_VERSION=1
7 | MARKETING_VERSION=1.0
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/iosMain/kotlin/com/macaosoftware/ui/MainViewController.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui
2 |
3 | import androidx.compose.ui.window.ComposeUIViewController
4 |
5 | fun MainViewController() = ComposeUIViewController {
6 |
7 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/ui/AlertDialogEventsState.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.ui
2 |
3 | sealed interface AlertDialogEventsState {
4 | object Hidden : AlertDialogEventsState
5 | class ShowingInfo(val text: String) : AlertDialogEventsState
6 | }
7 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/com/macaosoftware/ui/MainViewController.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui
2 |
3 | import androidx.compose.ui.window.ComposeUIViewController
4 | import com.macaosoftware.ui.ui.DayScheduleApp
5 |
6 | fun MainViewController() = ComposeUIViewController { DayScheduleApp() }
--------------------------------------------------------------------------------
/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/composeApp/src/commonTest/kotlin/com/macaosoftware/ui/ComposeAppCommonTest.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 |
6 | class ComposeAppCommonTest {
7 |
8 | @Test
9 | fun example() {
10 | assertEquals(3, 1 + 2)
11 | }
12 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Kotlin
2 | kotlin.code.style=official
3 | kotlin.daemon.jvmargs=-Xmx4g
4 |
5 | #Gradle
6 | org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8
7 | org.gradle.configuration-cache=true
8 | org.gradle.caching=true
9 |
10 | #Android
11 | android.nonTransitiveRClass=true
12 | android.useAndroidX=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonTest/kotlin/com/macaosoftware/ui/ComposeAppCommonTest.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 |
6 | class ComposeAppCommonTest {
7 |
8 | @Test
9 | fun example() {
10 | assertEquals(3, 1 + 2)
11 | }
12 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/webMain/kotlin/com/macaosoftware/ui/main.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.window.ComposeViewport
5 | import com.macaosoftware.ui.ui.DayScheduleApp
6 |
7 | @OptIn(ExperimentalComposeUiApi::class)
8 | fun main() {
9 | ComposeViewport {
10 | DayScheduleApp()
11 | }
12 | }
--------------------------------------------------------------------------------
/kotlin-js-store/wasm/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@js-joda/core@3.2.0":
6 | version "3.2.0"
7 | resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
8 | integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==
9 |
--------------------------------------------------------------------------------
/composeApp/src/webMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DailyAgendaView
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/composeApp/src/jvmMain/kotlin/com/macaosoftware/ui/main.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui
2 |
3 | import androidx.compose.ui.window.Window
4 | import androidx.compose.ui.window.application
5 | import com.macaosoftware.ui.ui.DayScheduleApp
6 |
7 | fun main() = application {
8 | Window(
9 | onCloseRequest = ::exitApplication,
10 | title = "DailyAgendaView",
11 | ) {
12 | DayScheduleApp()
13 | }
14 | }
--------------------------------------------------------------------------------
/daily-agenda-view/src/webMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DailyAgendaView
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import ComposeApp
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | MainViewControllerKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea()
17 | }
18 | }
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | .kotlin
12 |
13 | **/build/
14 | xcuserdata
15 | !src/**/build/
16 | local.properties
17 | .idea
18 | .DS_Store
19 | captures
20 | .externalNativeBuild
21 | .cxx
22 | *.xcodeproj/*
23 | !*.xcodeproj/project.pbxproj
24 | !*.xcodeproj/xcshareddata/
25 | !*.xcodeproj/project.xcworkspace/
26 | !*.xcworkspace/contents.xcworkspacedata
27 | **/xcshareddata/WorkspaceSettings.xcsettings
28 | node_modules/
29 |
30 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/epgslots/EpgSlotsDataUpdater.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.epgslots
2 |
3 | class EpgSlotsDataUpdater internal constructor(
4 | val epgSlotsStateController: EpgSlotsStateController
5 | ) {
6 |
7 | fun addChannel(epgChannel: EpgChannel): Boolean {
8 | return epgSlotsStateController.epgChannels.add(epgChannel)
9 | }
10 |
11 | internal fun commit() {
12 | epgSlotsStateController.updateState()
13 | }
14 |
15 | fun postUpdate(block: EpgSlotsDataUpdater.() -> Unit) {
16 | this.block()
17 | commit()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/ui/model/AllDayEvent.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.ui.model
2 |
3 | import com.macaosoftware.ui.dailyagenda.timeslots.LocalTimeEvent
4 | import kotlin.uuid.ExperimentalUuidApi
5 | import kotlin.uuid.Uuid
6 |
7 | @OptIn(ExperimentalUuidApi::class)
8 | data class AllDayEvent(
9 | val uuid: Uuid,
10 | val title: String,
11 | val description: String
12 | ) {
13 |
14 | override fun equals(other: Any?): Boolean {
15 | if (this === other) return true
16 | if (other !is LocalTimeEvent) return false
17 | return other.uuid == uuid
18 | }
19 |
20 | override fun hashCode(): Int {
21 | return uuid.hashCode()
22 | }
23 | }
--------------------------------------------------------------------------------
/composeApp/webpack.config.d/watch.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Temporary workaround for [KT-80582](https://youtrack.jetbrains.com/issue/KT-80582)
3 | *
4 | * This file should be safe to be removed once the ticket is closed and the project is updated to Kotlin version which solves that issue.
5 | */
6 | config.watchOptions = config.watchOptions || {
7 | ignored: ["**/*.kt", "**/node_modules"]
8 | }
9 |
10 | if (config.devServer) {
11 | config.devServer.static = config.devServer.static.map(file => {
12 | if (typeof file === "string") {
13 | return {
14 | directory: file,
15 | watch: false,
16 | }
17 | } else {
18 | return file
19 | }
20 | })
21 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/com/macaosoftware/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.tooling.preview.Preview
9 | import com.macaosoftware.ui.ui.DayScheduleApp
10 |
11 | class MainActivity : ComponentActivity() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | enableEdgeToEdge()
14 | super.onCreate(savedInstanceState)
15 | setContent {
16 | DayScheduleApp()
17 | }
18 | }
19 | }
20 |
21 | @Preview
22 | @Composable
23 | fun DayScheduleAppAndroidPreview() {
24 | DayScheduleApp()
25 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | },
9 | {
10 | "appearances" : [
11 | {
12 | "appearance" : "luminosity",
13 | "value" : "dark"
14 | }
15 | ],
16 | "idiom" : "universal",
17 | "platform" : "ios",
18 | "size" : "1024x1024"
19 | },
20 | {
21 | "appearances" : [
22 | {
23 | "appearance" : "luminosity",
24 | "value" : "tinted"
25 | }
26 | ],
27 | "idiom" : "universal",
28 | "platform" : "ios",
29 | "size" : "1024x1024"
30 | }
31 | ],
32 | "info" : {
33 | "author" : "xcode",
34 | "version" : 1
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/slotslayer/SlotsLayerState.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.slotslayer
2 |
3 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsBaseLayoutState
4 | import com.macaosoftware.ui.dailyagenda.decimalslots.Slot
5 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgSlotsState
6 |
7 | data class SlotsLayerState(
8 | val slots: List,
9 | val slotHeight: Int
10 | )
11 |
12 | internal fun DecimalSlotsBaseLayoutState.getSlotsLayerState(): SlotsLayerState {
13 | return SlotsLayerState(
14 | slots = slots,
15 | slotHeight = config.slotHeight
16 | )
17 | }
18 |
19 | internal fun EpgSlotsState.getSlotsLayerState(): SlotsLayerState {
20 | return SlotsLayerState(
21 | slots = slots,
22 | slotHeight = epgChannelSlotConfig.timeSlotConfig.slotHeight
23 | )
24 | }
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/marker/CurrentTimeMarkerView.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.marker
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.offset
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.unit.dp
12 |
13 | @Composable
14 | internal fun CurrentTimeMarkerView(
15 | modifier: Modifier = Modifier,
16 | currentTimeMarkerStateController: CurrentTimeMarkerStateController
17 | ) {
18 |
19 | val state = currentTimeMarkerStateController.state
20 |
21 | Box(modifier = modifier
22 | .offset(y = state.value.offsetY)
23 | .height(height = 1.dp)
24 | .fillMaxWidth()
25 | .background(Color.Red)
26 | )
27 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | mavenContent {
5 | includeGroupAndSubgroups("androidx")
6 | includeGroupAndSubgroups("com.android")
7 | includeGroupAndSubgroups("com.google")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 |
15 | dependencyResolutionManagement {
16 | repositories {
17 | google {
18 | mavenContent {
19 | includeGroupAndSubgroups("androidx")
20 | includeGroupAndSubgroups("com.android")
21 | includeGroupAndSubgroups("com.google")
22 | }
23 | }
24 | mavenCentral()
25 | }
26 | }
27 |
28 | plugins {
29 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
30 | }
31 |
32 | rootProject.name = "daily-agenda-view"
33 | // enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") // not working on github actions
34 | include(":composeApp")
35 | include(":daily-agenda-view")
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/ui/BottomSheetEventsState.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.ui
2 |
3 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalEvent
4 | import com.macaosoftware.ui.dailyagenda.timeslots.LocalTimeEvent
5 |
6 | sealed interface BottomSheetEventsState {
7 | object Hidden : BottomSheetEventsState
8 |
9 | class ShowTimedEventRequested(val localTimeEvent: LocalTimeEvent) :
10 | BottomSheetEventsState
11 |
12 | object AddTimedEventRequested : BottomSheetEventsState
13 |
14 | class RemoveTimedEventRequested(val localTimeEvent: LocalTimeEvent) :
15 | BottomSheetEventsState
16 |
17 | class ShowDecimalEventRequested(val decimalEvent: DecimalEvent) : BottomSheetEventsState
18 |
19 | object AddDecimalEventRequested : BottomSheetEventsState
20 |
21 | class RemoveDecimalEventRequested(val decimalEvent: DecimalEvent) : BottomSheetEventsState
22 |
23 | class ShowEpgEventRequested(val epgEvent: LocalTimeEvent) : BottomSheetEventsState
24 |
25 | object AddEpgEventRequested : BottomSheetEventsState
26 |
27 | class RemoveEpgEventRequested(val epgEvent: LocalTimeEvent) : BottomSheetEventsState
28 | }
--------------------------------------------------------------------------------
/.github/workflows/deploy-page.yaml:
--------------------------------------------------------------------------------
1 | name: Build & Deploy KMP Web App
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | permissions:
8 | contents: read
9 | pages: write
10 | id-token: write
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Check out repository
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up JDK 17
20 | uses: actions/setup-java@v4
21 | with:
22 | distribution: temurin
23 | java-version: '17'
24 |
25 | - name: Setup Gradle
26 | uses: gradle/actions/setup-gradle@v4
27 |
28 | - name: Build KMP Web artifact
29 | run: ./gradlew wasmJsBrowserDistribution
30 |
31 | - name: Configure GitHub Pages
32 | id: pages
33 | uses: actions/configure-pages@v5
34 |
35 | - name: Upload Pages artifact
36 | uses: actions/upload-pages-artifact@v3
37 | with:
38 | path: composeApp/build/dist/wasmJs/productionExecutable
39 |
40 | deploy:
41 | needs: build
42 | runs-on: ubuntu-latest
43 | environment:
44 | name: github-pages
45 | url: ${{ steps.deployment.outputs.page_url }}
46 | steps:
47 | - name: Deploy to GitHub Pages
48 | id: deployment
49 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/epgslots/EpgSlotsState.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.epgslots
2 |
3 | import com.macaosoftware.ui.dailyagenda.decimalslots.Slot
4 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotConfig
5 | import com.macaosoftware.ui.dailyagenda.timeslots.LocalTimeEvent
6 | import com.macaosoftware.ui.dailyagenda.timeslots.TimeSlotConfig
7 | import com.macaosoftware.ui.dailyagenda.timeslots.toSlotConfig
8 | import kotlinx.datetime.LocalTime
9 |
10 | data class EpgSlotsState(
11 | val slots: List,
12 | val epgChannels: List,
13 | val epgChannelSlotConfig: EpgChannelSlotConfig,
14 | val epgChannelHeight: Float
15 | )
16 |
17 | data class EpgChannel(
18 | val name: String,
19 | val events: List
20 | )
21 |
22 | data class EpgChannelSlotConfig(
23 | val channelWidth: Int = 88,
24 | val topHeaderHeight: Int = 56,
25 | val timeSlotConfig: TimeSlotConfig = TimeSlotConfig(
26 | startSlotTime = LocalTime(0, 0),
27 | endSlotTime = LocalTime(23, 59),
28 | useAmPm = true,
29 | slotScale = 2,
30 | slotHeight = 48,
31 | timelineLeftPadding = 72
32 | )
33 | )
34 |
35 | internal fun EpgChannelSlotConfig.toSlotConfig(): DecimalSlotConfig {
36 | return timeSlotConfig.toSlotConfig()
37 | }
38 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/decimalslots/DecimalSlotsView.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.decimalslots
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.rememberScrollState
6 | import androidx.compose.foundation.verticalScroll
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import com.macaosoftware.ui.dailyagenda.slotslayer.SlotsLayer
10 | import com.macaosoftware.ui.dailyagenda.slotslayer.getSlotsLayerState
11 |
12 | @Composable
13 | fun DecimalSlotsView(
14 | decimalSlotsStateController: DecimalSlotsStateController,
15 | eventContentProvider: @Composable (decimalEvent: DecimalEvent) -> Unit
16 | ) {
17 | val dailyAgendaState =
18 | decimalSlotsStateController.decimalSlotsBaseLayoutStateController.state.value ?: return
19 | val scrollState = rememberScrollState()
20 | Box(
21 | modifier = Modifier
22 | .fillMaxSize()
23 | .verticalScroll(scrollState)
24 | ) {
25 | SlotsLayer(slotsLayerState = dailyAgendaState.getSlotsLayerState())
26 | DecimalSlotsBaseLayout(
27 | decimalSlotsBaseLayoutState = dailyAgendaState,
28 | eventContentProvider = eventContentProvider
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/decimalslots/RtlCustomRow.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.decimalslots
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.layout.Layout
6 |
7 | @Composable
8 | internal fun RtlCustomRow(
9 | modifier: Modifier = Modifier,
10 | content: @Composable () -> Unit
11 | ) {
12 | Layout(
13 | modifier = modifier,
14 | content = content
15 | ) { measurables, constraints ->
16 |
17 | // 1. Measure children
18 | val placeables = measurables.map { measurable ->
19 | measurable.measure(constraints)
20 | }
21 |
22 | // 2. Calculate layout size (e.g., sum of widths for a horizontal row)
23 | var currentX = 0
24 | var maxHeight = 0
25 | placeables.forEach { placeable ->
26 | currentX += placeable.width
27 | if (placeable.height > maxHeight) {
28 | maxHeight = placeable.height
29 | }
30 | }
31 |
32 | // 3. Place children
33 | layout(width = currentX, height = maxHeight) {
34 | var xPosition = constraints.maxWidth
35 | placeables.forEach { placeable ->
36 |
37 | xPosition -= placeable.width
38 | placeable.placeRelative(x = xPosition, y = 0)
39 | //xPosition += placeable.width
40 |
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/marker/CurrentTimeMarkerStateController.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.marker
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.compose.ui.unit.Dp
5 | import androidx.compose.ui.unit.dp
6 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotConfig
7 | import com.macaosoftware.ui.dailyagenda.timeslots.fromLocalTimeToValue
8 | import kotlinx.datetime.LocalTime
9 | import kotlinx.datetime.TimeZone
10 | import kotlinx.datetime.toLocalDateTime
11 | import kotlin.time.Clock
12 | import kotlin.time.ExperimentalTime
13 |
14 | data class CurrentTimeMarkerState(val offsetY: Dp)
15 |
16 | internal class CurrentTimeMarkerStateController(
17 | decimalSlotConfig: DecimalSlotConfig
18 | ) {
19 |
20 | val state = mutableStateOf(value = CurrentTimeMarkerState(offsetY = 0.dp))
21 |
22 | init {
23 | val localTime = getCurrentLocalTime()
24 | val currentTimeAsDecimal = fromLocalTimeToValue(localTime)
25 | val offsetY =
26 | (currentTimeAsDecimal - decimalSlotConfig.initialSlotValue) * (decimalSlotConfig.slotScale * decimalSlotConfig.slotHeight)
27 |
28 |
29 | state.value = CurrentTimeMarkerState(offsetY = offsetY.dp)
30 | }
31 |
32 | @OptIn(ExperimentalTime::class)
33 | fun getCurrentLocalTime(): LocalTime {
34 | val now = Clock.System.now()
35 | val timeZone = TimeZone.currentSystemDefault()
36 | val localDateTime = now.toLocalDateTime(timeZone)
37 | return localDateTime.time
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/slotslayer/SlotsLayer.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.slotslayer
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material3.HorizontalDivider
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.dp
15 | import com.macaosoftware.ui.dailyagenda.decimalslots.Slot
16 |
17 |
18 | @Composable
19 | internal fun SlotsLayer(
20 | modifier: Modifier = Modifier,
21 | slotsLayerState: SlotsLayerState
22 | ) {
23 | Column(
24 | modifier = modifier.fillMaxSize()
25 | ) {
26 | slotsLayerState.slots.forEach { slot ->
27 | SlotLine(slot = slot, slotHeight = slotsLayerState.slotHeight)
28 | }
29 | }
30 | }
31 |
32 | @Composable
33 | private fun SlotLine(slot: Slot, slotHeight: Int) {
34 | Box(
35 | modifier = Modifier
36 | .fillMaxWidth()
37 | .height(height = slotHeight.dp)
38 | ) {
39 | HorizontalDivider(
40 | modifier = Modifier
41 | .fillMaxWidth()
42 | .padding(horizontal = 8.dp),
43 | color = Color.Black
44 | )
45 | Text(text = slot.title)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
14 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
29 |
30 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/ui/DayScheduleAppAlertDialog.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.ui
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.BasicAlertDialog
7 | import androidx.compose.material3.Button
8 | import androidx.compose.material3.Card
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 |
15 | @OptIn(ExperimentalMaterial3Api::class)
16 | @Composable
17 | internal fun DayScheduleAppAlertDialog(
18 | alertDialogEventsState: AlertDialogEventsState,
19 | alertDialogUiActionListener: DayScheduleAppViewModel.AlertDialogUiActionListener
20 | ) {
21 | when (alertDialogEventsState) {
22 | AlertDialogEventsState.Hidden -> {
23 | // no-op
24 | }
25 |
26 | is AlertDialogEventsState.ShowingInfo -> {
27 | BasicAlertDialog(
28 | onDismissRequest = { alertDialogUiActionListener.hideAlert() }
29 | ) {
30 | Card(
31 | modifier = Modifier.fillMaxWidth()
32 | ) {
33 | Column(modifier = Modifier.padding(16.dp)) {
34 | Text(text = alertDialogEventsState.text)
35 | Button(onClick = { alertDialogUiActionListener.hideAlert() }) {
36 | Text("Dismiss")
37 | }
38 | }
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/decimalslots/DecimalSlotsStateController.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.decimalslots
2 |
3 | open class DecimalSlotsStateController(
4 | val decimalSlotConfig: DecimalSlotConfig,
5 | eventsArrangement: EventsArrangement = EventsArrangement.MixedDirections()
6 | ) {
7 |
8 | val slotScale = decimalSlotConfig.slotScale
9 | val slotHeight = decimalSlotConfig.slotHeight
10 | val slotUnit = 1.0F / slotScale
11 | val firstSlotIndex = (slotScale * decimalSlotConfig.initialSlotValue.toInt())
12 |
13 | private val lastSlotIndex = decimalSlotConfig.lastSlotValue.toInt() * slotScale
14 |
15 |
16 | internal val decimalSlotsBaseLayoutStateController = DecimalSlotsBaseLayoutStateController(
17 | decimalSlotConfig = decimalSlotConfig,
18 | slots = createSlots(firstSlotIndex, lastSlotIndex),
19 | eventsArrangement = eventsArrangement
20 | )
21 |
22 | val decimalSlotsDataUpdater = DecimalSlotsDataUpdater(decimalSlotsBaseLayoutStateController)
23 |
24 | fun createSlots(
25 | firstSlotIndex: Int,
26 | lastSlotIndex: Int
27 | ): List {
28 | val slots = mutableListOf()
29 | for (i in firstSlotIndex..lastSlotIndex) {
30 | val slotStartValue = i * slotUnit
31 | slots.add(
32 | Slot(
33 | title = "$slotStartValue",
34 | startValue = slotStartValue,
35 | endValue = slotStartValue + slotUnit
36 | )
37 | )
38 | }
39 | return slots
40 | }
41 |
42 | fun getTimeSlotsData(): Map> {
43 | return decimalSlotsBaseLayoutStateController.slotToDecimalEventMapSorted
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/timeslots/TimeSlotsView.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.timeslots
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.rememberScrollState
6 | import androidx.compose.foundation.verticalScroll
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.Modifier
10 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsBaseLayout
11 | import com.macaosoftware.ui.dailyagenda.marker.CurrentTimeMarkerStateController
12 | import com.macaosoftware.ui.dailyagenda.marker.CurrentTimeMarkerView
13 | import com.macaosoftware.ui.dailyagenda.slotslayer.SlotsLayer
14 | import com.macaosoftware.ui.dailyagenda.slotslayer.getSlotsLayerState
15 |
16 | @Composable
17 | fun TimeSlotsView(
18 | timeSlotsStateController: TimeSlotsStateController,
19 | eventContentProvider: @Composable (event: LocalTimeEvent) -> Unit
20 | ) {
21 | val dailyAgendaState =
22 | timeSlotsStateController.decimalSlotsBaseLayoutStateController.state.value ?: return
23 | val scrollState = rememberScrollState()
24 | val currentTimeMarkerStateController = remember {
25 | CurrentTimeMarkerStateController(decimalSlotConfig = timeSlotsStateController.slotConfig)
26 | }
27 | Box(
28 | modifier = Modifier.fillMaxSize().verticalScroll(scrollState)
29 | ) {
30 | SlotsLayer(slotsLayerState = dailyAgendaState.getSlotsLayerState())
31 | DecimalSlotsBaseLayout(
32 | decimalSlotsBaseLayoutState = dailyAgendaState,
33 | eventContentProvider = { event ->
34 | // Intercept the event to apply the toLocalTimeEvent() transformation
35 | eventContentProvider.invoke(event.toLocalTimeEvent())
36 | }
37 | )
38 | CurrentTimeMarkerView(
39 | currentTimeMarkerStateController = currentTimeMarkerStateController
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/timeslots/TimeSlotsDataUpdater.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.timeslots
2 |
3 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsBaseLayoutStateController
4 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsDataUpdater
5 | import kotlinx.datetime.LocalTime
6 | import kotlin.uuid.ExperimentalUuidApi
7 | import kotlin.uuid.Uuid
8 |
9 | class TimeSlotsDataUpdater internal constructor(
10 | decimalSlotsBaseLayoutStateController: DecimalSlotsBaseLayoutStateController
11 | ) {
12 |
13 | private val decimalSlotsDataUpdater =
14 | DecimalSlotsDataUpdater(decimalSlotsBaseLayoutStateController)
15 |
16 | @OptIn(ExperimentalUuidApi::class)
17 | fun addEvent(
18 | uuid: Uuid = Uuid.random(),
19 | title: String,
20 | description: String,
21 | startTime: LocalTime,
22 | endTime: LocalTime
23 | ): Boolean {
24 | return decimalSlotsDataUpdater.addDecimalEvent(
25 | uuid = uuid,
26 | title = title,
27 | description = description,
28 | startValue = fromLocalTimeToValue(localTime = startTime),
29 | endValue = fromLocalTimeToValue(localTime = endTime)
30 | )
31 | }
32 |
33 | fun addEvent(event: LocalTimeEvent): Boolean {
34 | return decimalSlotsDataUpdater.addDecimalEvent(decimalEvent = event.toDecimalSegment())
35 | }
36 |
37 | fun addEventList(startTime: LocalTime, events: List) {
38 | return decimalSlotsDataUpdater.addDecimalEventList(
39 | startValue = fromLocalTimeToValue(localTime = startTime),
40 | segments = events.map { it.toDecimalSegment() }
41 | )
42 | }
43 |
44 | fun removeEvent(event: LocalTimeEvent): Boolean {
45 | return decimalSlotsDataUpdater.removeDecimalEvent(decimalEvent = event.toDecimalSegment())
46 | }
47 |
48 | fun removeEventByTitle(eventTitle: String): Boolean {
49 | return decimalSlotsDataUpdater.removeDecimalEventByTittle(eventTitle = eventTitle)
50 | }
51 |
52 | fun postUpdate(block: TimeSlotsDataUpdater.() -> Unit) {
53 | this.block()
54 | decimalSlotsDataUpdater.commit()
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/epgslots/EpgSlotsView.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.epgslots
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.derivedStateOf
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.unit.dp
14 | import com.macaosoftware.ui.dailyagenda.decimalslots.toDp
15 | import com.macaosoftware.ui.dailyagenda.marker.CurrentTimeMarkerStateController
16 | import com.macaosoftware.ui.dailyagenda.marker.CurrentTimeMarkerView
17 | import com.macaosoftware.ui.dailyagenda.slotslayer.SlotsLayer
18 | import com.macaosoftware.ui.dailyagenda.slotslayer.getSlotsLayerState
19 | import com.macaosoftware.ui.dailyagenda.timeslots.LocalTimeEvent
20 |
21 | @Composable
22 | fun EpgSlotsView(
23 | epgSlotsStateController: EpgSlotsStateController,
24 | eventContentProvider: @Composable (localTimeEvent: LocalTimeEvent) -> Unit
25 | ) {
26 | val epgSlotsState = epgSlotsStateController.state.value ?: return
27 | val scrollState = rememberScrollState()
28 | val currentTimeMarkerStateController = remember {
29 | CurrentTimeMarkerStateController(decimalSlotConfig = epgSlotsState.epgChannelSlotConfig.toSlotConfig())
30 | }
31 | val scrollOffset by remember { derivedStateOf { scrollState.value } }
32 | Box(
33 | modifier = Modifier.fillMaxSize().verticalScroll(scrollState)
34 | ) {
35 | SlotsLayer(
36 | modifier = Modifier.padding(top = epgSlotsState.epgChannelSlotConfig.topHeaderHeight.dp),
37 | slotsLayerState = epgSlotsState.getSlotsLayerState()
38 | )
39 | EpgSlotsLayout(
40 | epgSlotsState = epgSlotsState,
41 | eventContentProvider = eventContentProvider,
42 | scrollOffset = scrollOffset.toDp()
43 | )
44 | CurrentTimeMarkerView(
45 | modifier = Modifier.padding(top = epgSlotsState.epgChannelSlotConfig.topHeaderHeight.dp),
46 | currentTimeMarkerStateController = currentTimeMarkerStateController
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | dailyAgendaView = "1.5.1"
3 | kotlin = "2.2.21"
4 | agp = "8.13.1"
5 | android-compileSdk = "36"
6 | android-minSdk = "24"
7 | android-targetSdk = "36"
8 | composeMultiplatform = "1.9.3"
9 | composeHotReload = "1.0.0"
10 |
11 | androidx-activity = "1.12.0"
12 | androidx-core = "1.17.0"
13 | androidx-lifecycle = "2.9.6"
14 |
15 | kotlinx-coroutines = "1.10.2"
16 | kotlinx-datetime = "0.7.1"
17 |
18 | vanniktechMavenPublish = "0.35.0"
19 |
20 | junit = "4.13.2"
21 | androidx-testExt = "1.3.0"
22 | androidx-espresso = "3.7.0"
23 |
24 | [libraries]
25 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
26 | kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
27 | junit = { module = "junit:junit", version.ref = "junit" }
28 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
29 | androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
30 | androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
31 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
32 | androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
33 | androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
34 | kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
35 | kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
36 |
37 | [plugins]
38 | androidApplication = { id = "com.android.application", version.ref = "agp" }
39 | androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
40 | composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" }
41 | composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
42 | composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
43 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
44 | vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
45 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/timeslots/TimeSlotsStateController.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.timeslots
2 |
3 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsBaseLayoutStateController
4 | import com.macaosoftware.ui.dailyagenda.decimalslots.EventsArrangement
5 | import com.macaosoftware.ui.dailyagenda.decimalslots.Slot
6 |
7 | class TimeSlotsStateController(
8 | val timeSlotConfig: TimeSlotConfig = TimeSlotConfig(),
9 | eventsArrangement: EventsArrangement = EventsArrangement.MixedDirections()
10 | ) {
11 |
12 | val slotConfig = timeSlotConfig.toSlotConfig()
13 | val slotScale = slotConfig.slotScale
14 | val slotHeight = slotConfig.slotHeight
15 | val slotUnit = 1.0F / slotScale
16 | val firstSlotIndex = (slotScale * slotConfig.initialSlotValue.toInt())
17 | private val lastSlotIndex = (slotConfig.lastSlotValue * slotScale).toInt()
18 |
19 | internal val decimalSlotsBaseLayoutStateController = DecimalSlotsBaseLayoutStateController(
20 | decimalSlotConfig = slotConfig,
21 | slots = createSlots(firstSlotIndex, lastSlotIndex),
22 | eventsArrangement = eventsArrangement
23 | )
24 |
25 | val timeSlotsDataUpdater = TimeSlotsDataUpdater(
26 | decimalSlotsBaseLayoutStateController = decimalSlotsBaseLayoutStateController
27 | )
28 |
29 | fun createSlots(
30 | firstSlotIndex: Int,
31 | lastSlotIndex: Int
32 | ): List {
33 | val slots = mutableListOf()
34 | for (i in firstSlotIndex..lastSlotIndex) {
35 | val slotStartValue = i * slotUnit
36 | val title = fromDecimalValueToTimeText(slotStartValue, timeSlotConfig.useAmPm)
37 |
38 | slots.add(
39 | Slot(
40 | title = title,
41 | startValue = slotStartValue,
42 | endValue = slotStartValue + slotUnit
43 | )
44 | )
45 | }
46 | return slots
47 | }
48 |
49 | fun getTimeSlotsData(): Map> {
50 | val result = mutableMapOf>()
51 | decimalSlotsBaseLayoutStateController.slotToDecimalEventMapSorted.forEach { entry ->
52 | result.put(
53 | key = entry.key.toLocalTimeSlot(),
54 | value = entry.value.map { it.toLocalTimeEvent() }
55 | )
56 | }
57 | return result
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/epgslots/EpgSlotsStateController.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.epgslots
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import com.macaosoftware.ui.dailyagenda.decimalslots.Slot
5 | import com.macaosoftware.ui.dailyagenda.timeslots.fromDecimalValueToTimeText
6 | import com.macaosoftware.ui.dailyagenda.timeslots.toSlotConfig
7 |
8 | class EpgSlotsStateController(
9 | val epgChannelSlotConfig: EpgChannelSlotConfig
10 | ) {
11 |
12 | val timeSlotConfig = epgChannelSlotConfig.timeSlotConfig
13 | val slotConfig = timeSlotConfig.toSlotConfig()
14 | val slotScale = slotConfig.slotScale
15 | val slotHeight = slotConfig.slotHeight
16 | val slotUnit = 1.0F / slotScale
17 | val firstSlotIndex = (slotScale * slotConfig.initialSlotValue.toInt())
18 | private val lastSlotIndex = (slotConfig.lastSlotValue * slotScale).toInt()
19 | internal val epgChannelHeight =
20 | ((slotConfig.lastSlotValue - slotConfig.initialSlotValue) * slotScale * slotHeight) +
21 | epgChannelSlotConfig.topHeaderHeight
22 |
23 | private val slots = createSlots(firstSlotIndex, lastSlotIndex)
24 |
25 | internal val epgChannels: MutableList = mutableListOf()
26 |
27 | val state = mutableStateOf(value = null)
28 |
29 | val epgSlotsDataUpdater = EpgSlotsDataUpdater(
30 | epgSlotsStateController = this
31 | )
32 |
33 | fun createSlots(
34 | firstSlotIndex: Int,
35 | lastSlotIndex: Int
36 | ): List {
37 | val slots = mutableListOf()
38 | for (i in firstSlotIndex..lastSlotIndex) {
39 | val slotStartValue = i * slotUnit
40 | val title = fromDecimalValueToTimeText(slotStartValue, timeSlotConfig.useAmPm)
41 | slots.add(
42 | Slot(
43 | title = title,
44 | startValue = slotStartValue,
45 | endValue = slotStartValue + slotUnit
46 | )
47 | )
48 | }
49 | return slots
50 | }
51 |
52 | private fun computeNextState(): EpgSlotsState {
53 | return EpgSlotsState(
54 | slots = slots,
55 | epgChannels = epgChannels,
56 | epgChannelSlotConfig = epgChannelSlotConfig,
57 | epgChannelHeight = epgChannelHeight
58 | )
59 | }
60 |
61 | internal fun updateState() {
62 | state.value = computeNextState()
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/decimalslots/DecimalSlotsBaseLayoutState.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.decimalslots
2 |
3 | import androidx.compose.ui.unit.Dp
4 | import androidx.compose.ui.unit.dp
5 | import kotlin.uuid.ExperimentalUuidApi
6 | import kotlin.uuid.Uuid
7 |
8 | internal data class DecimalSlotsBaseLayoutState(
9 | val slots: List,
10 | val slotToDecimalEventMap: Map>,
11 | val slotInfoMap: Map,
12 | val maxColumns: Int,
13 | val config: Config
14 | )
15 |
16 | @ConsistentCopyVisibility
17 | data class Slot internal constructor(
18 | val title: String,
19 | val startValue: Float,
20 | val endValue: Float
21 | )
22 |
23 | // TODO: Change var with val SlotInfo.copy()
24 | @ConsistentCopyVisibility
25 | data class SlotInfo internal constructor(
26 | var numberOfContainingEvents: Int,
27 | var numberOfColumnsLeft: Int,
28 | var numberOfColumnsRight: Int,
29 | ) {
30 | fun getTotalColumnSpans() = numberOfColumnsLeft + numberOfColumnsRight
31 | }
32 |
33 | @OptIn(ExperimentalUuidApi::class)
34 | data class DecimalEvent(
35 | val uuid: Uuid,
36 | val title: String,
37 | val description: String,
38 | val startValue: Float,
39 | val endValue: Float
40 | ) {
41 |
42 | override fun equals(other: Any?): Boolean {
43 | if (this === other) return true
44 | if (other !is DecimalEvent) return false
45 | return other.uuid == uuid
46 | }
47 |
48 | override fun hashCode(): Int {
49 | return uuid.hashCode()
50 | }
51 | }
52 |
53 | internal class OffsetInfo(
54 | var leftStartOffset: Dp = 0.dp,
55 | var leftAccumulated: Dp = 0.dp,
56 | var rightStartOffset: Dp = 0.dp,
57 | var rightAccumulated: Dp = 0.dp
58 | ) {
59 | fun getTotalLeftOffset() = leftStartOffset + leftAccumulated
60 |
61 | fun getTotalRightOffset() = rightStartOffset + rightAccumulated
62 | }
63 |
64 | data class DecimalSlotConfig(
65 | val initialSlotValue: Float = 0.0F,
66 | val lastSlotValue: Float = 20.0F,
67 | val slotScale: Int = 2,
68 | val slotHeight: Int = 64,
69 | val timelineLeftPadding: Int = 72
70 | )
71 |
72 | @ConsistentCopyVisibility
73 | data class Config internal constructor(
74 | val eventsArrangement: EventsArrangement = EventsArrangement.MixedDirections(),
75 | val initialSlotValue: Float,
76 | val lastSlotValue: Float,
77 | val slotScale: Int,
78 | val slotHeight: Int,
79 | val timelineLeftPadding: Int
80 | )
81 |
82 | sealed interface EventsArrangement {
83 |
84 | class MixedDirections(
85 | val eventWidthType: EventWidthType = EventWidthType.FixedSizeFillLastEvent,
86 | ) : EventsArrangement
87 |
88 | class LeftToRight(
89 | val lastEventFillRow: Boolean = true
90 | ) : EventsArrangement
91 |
92 | class RightToLeft(
93 | val lastEventFillRow: Boolean = true
94 | ) : EventsArrangement
95 | }
96 |
97 | enum class EventWidthType { MaxVariableSize, FixedSize, FixedSizeFillLastEvent }
98 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/daily-agenda-view/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.androidLibrary
2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4 |
5 | plugins {
6 | alias(libs.plugins.kotlinMultiplatform)
7 | alias(libs.plugins.androidKmpLibrary)
8 | alias(libs.plugins.composeMultiplatform)
9 | alias(libs.plugins.composeCompiler)
10 | alias(libs.plugins.vanniktech.mavenPublish)
11 | }
12 |
13 | kotlin {
14 | androidLibrary {
15 | namespace = "com.macaosoftware.ui.dailyagenda"
16 | compileSdk = libs.versions.android.compileSdk.get().toInt()
17 | minSdk = libs.versions.android.minSdk.get().toInt()
18 |
19 | withJava() // enable java compilation support
20 | withHostTestBuilder {}.configure {}
21 | withDeviceTestBuilder {
22 | sourceSetTreeName = "test"
23 | }
24 |
25 | compilations.configureEach {
26 | compilerOptions.configure {
27 | jvmTarget.set(
28 | JvmTarget.JVM_17
29 | )
30 | }
31 | }
32 | }
33 |
34 | listOf(
35 | iosArm64(),
36 | iosSimulatorArm64()
37 | ).forEach { iosTarget ->
38 | iosTarget.binaries.framework {
39 | baseName = "ComposeApp"
40 | isStatic = true
41 | }
42 | }
43 |
44 | jvm()
45 |
46 | js {
47 | browser()
48 | }
49 |
50 | @OptIn(ExperimentalWasmDsl::class)
51 | wasmJs {
52 | browser()
53 | }
54 |
55 | sourceSets {
56 | commonMain.dependencies {
57 | implementation(compose.runtime)
58 | implementation(compose.ui)
59 | implementation(compose.foundation)
60 | implementation(compose.material3)
61 | implementation(compose.components.uiToolingPreview)
62 | implementation(libs.kotlinx.datetime)
63 | }
64 | commonTest.dependencies {
65 | implementation(libs.kotlin.test)
66 | }
67 | androidMain.dependencies {
68 | implementation(compose.preview)
69 | }
70 | jvmMain.dependencies {
71 | implementation(compose.desktop.currentOs)
72 | implementation(libs.kotlinx.coroutinesSwing)
73 | }
74 | }
75 | }
76 |
77 | mavenPublishing {
78 | publishToMavenCentral(automaticRelease = true)
79 | signAllPublications()
80 |
81 | coordinates(
82 | groupId = "io.github.pablichjenkov",
83 | artifactId = "daily-agenda-view",
84 | version = libs.versions.dailyAgendaView.get()
85 | )
86 |
87 | pom {
88 | val projectGitUrl = "https://github.com/pablichjenkov/daily-agenda-view"
89 | name = "daily-agenda-view"
90 | description = "Ui component that displays daily events along a vertical timeline"
91 | inceptionYear = "2025"
92 | url = projectGitUrl
93 | licenses {
94 | license {
95 | name.set("The Unlicense")
96 | url.set("https://unlicense.org")
97 | }
98 | }
99 | developers {
100 | developer {
101 | id = "pablichjenkov"
102 | }
103 | }
104 | scm {
105 | connection.set("scm:git:$projectGitUrl")
106 | developerConnection.set("scm:git:$projectGitUrl")
107 | url.set(projectGitUrl)
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4 |
5 | plugins {
6 | alias(libs.plugins.kotlinMultiplatform)
7 | alias(libs.plugins.composeMultiplatform)
8 | alias(libs.plugins.composeCompiler)
9 | alias(libs.plugins.androidApplication)
10 | alias(libs.plugins.composeHotReload)
11 | }
12 |
13 | kotlin {
14 | androidTarget {
15 | compilerOptions {
16 | jvmTarget.set(JvmTarget.JVM_17)
17 | }
18 | }
19 |
20 | listOf(
21 | iosArm64(),
22 | iosSimulatorArm64()
23 | ).forEach { iosTarget ->
24 | iosTarget.binaries.framework {
25 | baseName = "ComposeApp"
26 | isStatic = true
27 | }
28 | }
29 |
30 | jvm()
31 |
32 | js {
33 | browser()
34 | binaries.executable()
35 | }
36 |
37 | @OptIn(ExperimentalWasmDsl::class)
38 | wasmJs {
39 | browser()
40 | binaries.executable()
41 | }
42 |
43 | sourceSets {
44 | commonMain.dependencies {
45 | // implementation(projects.dailyAgendaView); not working on github actions
46 | implementation(project(":daily-agenda-view"))
47 | implementation(compose.runtime)
48 | implementation(compose.ui)
49 | implementation(compose.foundation)
50 | implementation(compose.material3)
51 | implementation(compose.components.resources)
52 | implementation(compose.components.uiToolingPreview)
53 | implementation(libs.androidx.lifecycle.viewmodelCompose)
54 | implementation(libs.androidx.lifecycle.runtimeCompose)
55 | implementation(libs.kotlinx.datetime)
56 | }
57 | commonTest.dependencies {
58 | implementation(libs.kotlin.test)
59 | }
60 | androidMain.dependencies {
61 | implementation(compose.preview)
62 | implementation(libs.androidx.activity.compose)
63 | }
64 | jvmMain.dependencies {
65 | implementation(compose.desktop.currentOs)
66 | implementation(libs.kotlinx.coroutinesSwing)
67 | }
68 | }
69 | }
70 |
71 | android {
72 | namespace = "com.macaosoftware.ui"
73 | compileSdk = libs.versions.android.compileSdk.get().toInt()
74 |
75 | defaultConfig {
76 | applicationId = "com.macaosoftware.ui"
77 | minSdk = libs.versions.android.minSdk.get().toInt()
78 | targetSdk = libs.versions.android.targetSdk.get().toInt()
79 | versionCode = 1
80 | versionName = "1.0"
81 | }
82 | packaging {
83 | resources {
84 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
85 | }
86 | }
87 | buildTypes {
88 | getByName("release") {
89 | isMinifyEnabled = false
90 | }
91 | }
92 | compileOptions {
93 | sourceCompatibility = JavaVersion.VERSION_17
94 | targetCompatibility = JavaVersion.VERSION_17
95 | }
96 | }
97 |
98 | dependencies {
99 | debugImplementation(compose.uiTooling)
100 | }
101 |
102 | compose.desktop {
103 | application {
104 | mainClass = "com.macaosoftware.ui.MainKt"
105 |
106 | nativeDistributions {
107 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
108 | packageName = "com.macaosoftware.ui"
109 | packageVersion = "1.0.0"
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/ui/DayScheduleAppActionsBottomView.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.ui
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.BoxScope
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.wrapContentSize
8 | import androidx.compose.material3.DropdownMenu
9 | import androidx.compose.material3.DropdownMenuItem
10 | import androidx.compose.material3.FloatingActionButton
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.setValue
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.unit.dp
20 |
21 | @Composable
22 | internal fun BoxScope.DayScheduleAppActionsBottomView(
23 | slotsViewType: SlotsViewType,
24 | uiActionListener: DayScheduleAppViewModel.UiActionListener
25 | ) {
26 | Row(
27 | modifier = Modifier
28 | .align(Alignment.BottomEnd)
29 | .padding(8.dp)
30 | .wrapContentSize(),
31 | horizontalArrangement = Arrangement.spacedBy(16.dp)
32 | ) {
33 | FloatingActionButton(
34 | modifier = Modifier.wrapContentSize(),
35 | onClick = {
36 | uiActionListener.showAddEventForm(slotsViewType)
37 | }
38 | ) {
39 | Text(
40 | modifier = Modifier.padding(16.dp),
41 | text = "Add Event"
42 | )
43 | }
44 | var expanded by remember { mutableStateOf(false) }
45 | FloatingActionButton(
46 | modifier = Modifier.wrapContentSize(),
47 | onClick = {
48 | expanded = true
49 | }
50 | ) {
51 | DropdownMenu(
52 | expanded = expanded,
53 | onDismissRequest = {
54 | expanded = false
55 | }
56 | ) {
57 | DropdownMenuItem(
58 | text = { Text("Timeline Axis") },
59 | onClick = {
60 | expanded = false
61 | uiActionListener.toggleAxisType(SlotsViewType.Timeline)
62 | }
63 | )
64 | DropdownMenuItem(
65 | text = { Text("Decimal Axis") },
66 | onClick = {
67 | expanded = false
68 | uiActionListener.toggleAxisType(SlotsViewType.Decimal)
69 | }
70 | )
71 | DropdownMenuItem(
72 | text = { Text("Epg Axis") },
73 | onClick = {
74 | expanded = false
75 | uiActionListener.toggleAxisType(SlotsViewType.Epg)
76 | }
77 | )
78 | }
79 | val axisText = when (slotsViewType) {
80 | SlotsViewType.Decimal -> {
81 | "Decimal Axis"
82 | }
83 |
84 | SlotsViewType.Timeline -> {
85 | "Timeline Axis"
86 | }
87 |
88 | SlotsViewType.Epg -> {
89 | "Epg Axis"
90 | }
91 | }
92 | Text(
93 | modifier = Modifier.padding(16.dp),
94 | text = axisText
95 | )
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/epgslots/EpgSlotsLayout.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.epgslots
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.offset
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.foundation.lazy.LazyRow
13 | import androidx.compose.foundation.lazy.items
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.unit.Dp
20 | import androidx.compose.ui.unit.dp
21 | import androidx.compose.ui.zIndex
22 | import com.macaosoftware.ui.dailyagenda.timeslots.LocalTimeEvent
23 | import com.macaosoftware.ui.dailyagenda.timeslots.fromLocalTimeToValue
24 | import com.macaosoftware.ui.dailyagenda.timeslots.toSlotConfig
25 |
26 | @Composable
27 | internal fun EpgSlotsLayout(
28 | epgSlotsState: EpgSlotsState,
29 | scrollOffset: Dp,
30 | eventContentProvider: @Composable (localTimeEvent: LocalTimeEvent) -> Unit
31 | ) {
32 | val timelineLeftPadding = epgSlotsState.epgChannelSlotConfig.timeSlotConfig.timelineLeftPadding
33 | val channelWidth = epgSlotsState.epgChannelSlotConfig.channelWidth
34 | val topHeaderHeight = epgSlotsState.epgChannelSlotConfig.topHeaderHeight
35 | val totalHeight = epgSlotsState.epgChannelHeight
36 | LazyRow(
37 | modifier = Modifier.height(height = totalHeight.dp)
38 | .padding(start = timelineLeftPadding.dp)
39 | ) {
40 | items(
41 | items = epgSlotsState.epgChannels,
42 | key = { epgChannel ->
43 | epgChannel.name
44 | }
45 | ) { epgChannel ->
46 | Column(
47 | modifier = Modifier.width(width = channelWidth.dp)
48 | .padding(all = 1.dp).background(color = Color.Gray.copy(alpha = 0.5f))
49 | ) {
50 | Box(
51 | Modifier.height(height = topHeaderHeight.dp)
52 | .width(width = epgSlotsState.epgChannelSlotConfig.channelWidth.dp)
53 | .offset(y = scrollOffset)
54 | .padding(all = 1.dp)
55 | .background(color = Color.Gray)
56 | .zIndex(zIndex = 1f)
57 | ) {
58 | Text(
59 | modifier = Modifier.align(Alignment.Center),
60 | text = epgChannel.name
61 | )
62 | }
63 | ChannelColumn(
64 | epgChannel = epgChannel,
65 | epgChannelSlotConfig = epgSlotsState.epgChannelSlotConfig,
66 | eventContentProvider = eventContentProvider
67 | )
68 | }
69 | }
70 | }
71 | }
72 |
73 | @Composable
74 | fun ChannelColumn(
75 | epgChannel: EpgChannel,
76 | epgChannelSlotConfig: EpgChannelSlotConfig,
77 | eventContentProvider: @Composable (localTimeEvent: LocalTimeEvent) -> Unit
78 | ) {
79 | val slotsConfig = epgChannelSlotConfig.timeSlotConfig.toSlotConfig()
80 | val initialSlotValue = slotsConfig.initialSlotValue
81 | Box(modifier = Modifier.fillMaxSize()) {
82 | val scaleMultiplier = slotsConfig.slotHeight * slotsConfig.slotScale
83 | epgChannel.events.forEach { localTimeEvent ->
84 | val startValue = fromLocalTimeToValue(localTimeEvent.startTime)
85 | val endValue = fromLocalTimeToValue(localTimeEvent.endTime)
86 | val offsetY = (startValue - initialSlotValue) * scaleMultiplier
87 | val programHeight = (endValue - startValue) * scaleMultiplier
88 | Box(
89 | modifier = Modifier.offset(y = offsetY.dp)
90 | .fillMaxWidth()
91 | .height(height = programHeight.dp)
92 |
93 | ) { eventContentProvider.invoke(localTimeEvent) }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/timeslots/TimeSlotsState.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.timeslots
2 |
3 | import com.macaosoftware.ui.dailyagenda.decimalslots.Config
4 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalEvent
5 | import com.macaosoftware.ui.dailyagenda.decimalslots.Slot
6 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotConfig
7 | import kotlinx.datetime.LocalTime
8 | import kotlin.uuid.ExperimentalUuidApi
9 | import kotlin.uuid.Uuid
10 |
11 | private const val HOUR_AM = "AM"
12 | private const val HOUR_PM = "PM"
13 | private const val MINUTES_IN_ONE_HOUR = 60
14 |
15 | @OptIn(ExperimentalUuidApi::class)
16 | data class LocalTimeEvent(
17 | val uuid: Uuid,
18 | val title: String,
19 | val description: String,
20 | val startTime: LocalTime,
21 | val endTime: LocalTime
22 | ) {
23 |
24 | override fun equals(other: Any?): Boolean {
25 | if (this === other) return true
26 | if (other !is LocalTimeEvent) return false
27 | return other.uuid == uuid
28 | }
29 |
30 | override fun hashCode(): Int {
31 | return uuid.hashCode()
32 | }
33 | }
34 |
35 | data class LocalTimeSlot(
36 | val title: String,
37 | val startTime: LocalTime,
38 | val endTime: LocalTime
39 | )
40 |
41 | @OptIn(ExperimentalUuidApi::class)
42 | fun DecimalEvent.toLocalTimeEvent(): LocalTimeEvent {
43 | val startLocalTime = fromValueToLocalTime(value = startValue)
44 | val endLocalTime = fromValueToLocalTime(value = endValue)
45 | return LocalTimeEvent(
46 | uuid = uuid,
47 | title = title,
48 | description = description,
49 | startTime = startLocalTime,
50 | endTime = endLocalTime
51 | )
52 | }
53 |
54 | @OptIn(ExperimentalUuidApi::class)
55 | fun LocalTimeEvent.toDecimalSegment(): DecimalEvent {
56 | val startTimeValue = fromLocalTimeToValue(localTime = startTime)
57 | val endTimeValue = fromLocalTimeToValue(localTime = endTime)
58 | return DecimalEvent(
59 | uuid = uuid,
60 | title = title,
61 | description = description,
62 | startValue = startTimeValue,
63 | endValue = endTimeValue
64 | )
65 | }
66 |
67 | fun Slot.toLocalTimeSlot(): LocalTimeSlot {
68 | return LocalTimeSlot(
69 | title = title,
70 | startTime = fromValueToLocalTime(value = startValue),
71 | endTime = fromValueToLocalTime(value = endValue),
72 | )
73 | }
74 |
75 | fun LocalTimeSlot.toSlot(): Slot {
76 | return Slot(
77 | title = title,
78 | startValue = fromLocalTimeToValue(localTime = startTime),
79 | endValue = fromLocalTimeToValue(localTime = endTime)
80 | )
81 | }
82 |
83 | data class TimeSlotConfig(
84 | val startSlotTime: LocalTime = LocalTime(0, 0),
85 | val endSlotTime: LocalTime = LocalTime(23, 59),
86 | val useAmPm: Boolean = true,
87 | val slotScale: Int = 2,
88 | val slotHeight: Int = 48,
89 | val timelineLeftPadding: Int = 72
90 | )
91 |
92 | internal fun fromLocalTimeToValue(localTime: LocalTime): Float {
93 | val minuteFraction = localTime.minute.toFloat() / MINUTES_IN_ONE_HOUR
94 | return localTime.hour.toFloat() + minuteFraction
95 | }
96 |
97 | fun fromValueToLocalTime(value: Float): LocalTime {
98 | val remaining = value % 1
99 | val minutes = (remaining * MINUTES_IN_ONE_HOUR).toInt()
100 | val hours = value.toInt()
101 | return LocalTime(hour = hours, minute = minutes)
102 | }
103 |
104 | fun fromDecimalValueToTimeText(
105 | slotStartValue: Float,
106 | useAmPm: Boolean
107 | ): String {
108 | val remaining = slotStartValue % 1
109 | val minutes = (remaining * MINUTES_IN_ONE_HOUR).toInt()
110 | val minutesTwoDigitFormat = if (minutes > 9) {
111 | minutes.toString()
112 | } else {
113 | "0$minutes"
114 | }
115 |
116 | if (!useAmPm) {
117 | val units = slotStartValue.toInt()
118 | return "$units:$minutesTwoDigitFormat"
119 | }
120 |
121 | val slotStartValueInt = slotStartValue.toInt()
122 | val hourUnits = slotStartValueInt % 12
123 | val hourUnitsFormatted = hourUnits.takeIf { it != 0 } ?: 12
124 | val amPmSuffix = if (slotStartValue < 12F) HOUR_AM else HOUR_PM
125 | return "$hourUnitsFormatted:$minutesTwoDigitFormat:$amPmSuffix"
126 | }
127 |
128 | fun TimeSlotConfig.toSlotConfig(): DecimalSlotConfig {
129 | return DecimalSlotConfig(
130 | initialSlotValue = fromLocalTimeToValue(startSlotTime),
131 | lastSlotValue = fromLocalTimeToValue(endSlotTime),
132 | slotScale = slotScale,
133 | slotHeight = slotHeight,
134 | timelineLeftPadding = timelineLeftPadding
135 | )
136 | }
137 |
138 | fun Config.toSlotConfig(): DecimalSlotConfig {
139 | return DecimalSlotConfig(
140 | initialSlotValue = initialSlotValue,
141 | lastSlotValue = lastSlotValue,
142 | slotScale = slotScale,
143 | slotHeight = slotHeight,
144 | timelineLeftPadding = timelineLeftPadding
145 | )
146 | }
147 |
--------------------------------------------------------------------------------
/iosApp/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,xcode,objective-c,osx
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,xcode,objective-c,osx
3 |
4 | ### Objective-C ###
5 | # Xcode
6 | #
7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
8 |
9 | ## User settings
10 | xcuserdata/
11 |
12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
13 | *.xcscmblueprint
14 | *.xccheckout
15 |
16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
17 | build/
18 | DerivedData/
19 | *.moved-aside
20 | *.pbxuser
21 | !default.pbxuser
22 | *.mode1v3
23 | !default.mode1v3
24 | *.mode2v3
25 | !default.mode2v3
26 | *.perspectivev3
27 | !default.perspectivev3
28 | *.o
29 | *.LinkFileList
30 |
31 | ## Obj-C/Swift specific
32 | *.hmap
33 |
34 | ## App packaging
35 | *.ipa
36 | *.dSYM.zip
37 | *.dSYM
38 |
39 | # CocoaPods
40 | # We recommend against adding the Pods directory to your .gitignore. However
41 | # you should judge for yourself, the pros and cons are mentioned at:
42 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
43 | # Pods/
44 | # Add this line if you want to avoid checking in source code from the Xcode workspace
45 | # *.xcworkspace
46 |
47 | # Carthage
48 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
49 | # Carthage/Checkouts
50 |
51 | Carthage/Build/
52 |
53 | # fastlane
54 | # It is recommended to not store the screenshots in the git repo.
55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
56 | # For more information about the recommended setup visit:
57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
58 |
59 | fastlane/report.xml
60 | fastlane/Preview.html
61 | fastlane/screenshots/**/*.png
62 | fastlane/test_output
63 |
64 | # Code Injection
65 | # After new code Injection tools there's a generated folder /iOSInjectionProject
66 | # https://github.com/johnno1962/injectionforxcode
67 |
68 | iOSInjectionProject/
69 |
70 | ### Objective-C Patch ###
71 |
72 | ### OSX ###
73 | # General
74 | .DS_Store
75 | .AppleDouble
76 | .LSOverride
77 |
78 | # Icon must end with two \r
79 | Icon
80 |
81 |
82 | # Thumbnails
83 | ._*
84 |
85 | # Files that might appear in the root of a volume
86 | .DocumentRevisions-V100
87 | .fseventsd
88 | .Spotlight-V100
89 | .TemporaryItems
90 | .Trashes
91 | .VolumeIcon.icns
92 | .com.apple.timemachine.donotpresent
93 |
94 | # Directories potentially created on remote AFP share
95 | .AppleDB
96 | .AppleDesktop
97 | Network Trash Folder
98 | Temporary Items
99 | .apdisk
100 |
101 | ### Swift ###
102 | # Xcode
103 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
104 |
105 |
106 |
107 |
108 |
109 |
110 | ## Playgrounds
111 | timeline.xctimeline
112 | playground.xcworkspace
113 |
114 | # Swift Package Manager
115 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
116 | # Packages/
117 | # Package.pins
118 | # Package.resolved
119 | # *.xcodeproj
120 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
121 | # hence it is not needed unless you have added a package configuration file to your project
122 | # .swiftpm
123 |
124 | .build/
125 |
126 | # CocoaPods
127 | # We recommend against adding the Pods directory to your .gitignore. However
128 | # you should judge for yourself, the pros and cons are mentioned at:
129 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
130 | # Pods/
131 | # Add this line if you want to avoid checking in source code from the Xcode workspace
132 | # *.xcworkspace
133 |
134 | # Carthage
135 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
136 | # Carthage/Checkouts
137 |
138 |
139 | # Accio dependency management
140 | Dependencies/
141 | .accio/
142 |
143 | # fastlane
144 | # It is recommended to not store the screenshots in the git repo.
145 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
146 | # For more information about the recommended setup visit:
147 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
148 |
149 |
150 | # Code Injection
151 | # After new code Injection tools there's a generated folder /iOSInjectionProject
152 | # https://github.com/johnno1962/injectionforxcode
153 |
154 |
155 | ### Xcode ###
156 |
157 | ## Xcode 8 and earlier
158 |
159 | ### Xcode Patch ###
160 | *.xcodeproj/*
161 | !*.xcodeproj/project.pbxproj
162 | !*.xcodeproj/xcshareddata/
163 | !*.xcodeproj/project.xcworkspace/
164 | !*.xcworkspace/contents.xcworkspacedata
165 | /*.gcno
166 | **/xcshareddata/WorkspaceSettings.xcsettings
167 |
168 | # Xcode per-user config
169 | *.mode1
170 | *.perspective
171 | *.xcworkspace
172 | xcuserdata
173 |
174 | # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,objective-c,osx
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/decimalslots/DecimalSlotsBaseLayoutStateController.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.decimalslots
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import kotlin.math.abs
5 |
6 | class DecimalSlotsBaseLayoutStateController(
7 | val slots: List,
8 | decimalSlotConfig: DecimalSlotConfig,
9 | private val eventsArrangement: EventsArrangement
10 | ) {
11 |
12 | val slotUnit = 1.0F / decimalSlotConfig.slotScale
13 |
14 | private val config = Config(
15 | eventsArrangement = eventsArrangement,
16 | initialSlotValue = slots[0].startValue,
17 | lastSlotValue = slots[slots.lastIndex].startValue,
18 | slotScale = decimalSlotConfig.slotScale,
19 | slotHeight = decimalSlotConfig.slotHeight,
20 | timelineLeftPadding = 72
21 | )
22 |
23 | internal val slotToDecimalEventMapSorted: MutableMap> = mutableMapOf()
24 |
25 | internal val state = mutableStateOf(value = null)
26 |
27 | init {
28 | slots.forEach { slot -> slotToDecimalEventMapSorted[slot] = mutableListOf() }
29 | }
30 |
31 | fun getSlotForValue(startValue: Float): Slot {
32 | return slots.find { abs(x = startValue - it.startValue) < slotUnit }
33 | ?: error("startTime: $startValue must be between ${config.initialSlotValue} and ${config.lastSlotValue}")
34 | }
35 |
36 | private fun computeNextState(): DecimalSlotsBaseLayoutState {
37 | val result = computeSlotInfo(
38 | slots = slots,
39 | slotToDecimalEventMap = slotToDecimalEventMapSorted,
40 | config = config
41 | )
42 | return DecimalSlotsBaseLayoutState(
43 | slots = slots,
44 | slotToDecimalEventMap = slotToDecimalEventMapSorted,
45 | slotInfoMap = result.slotInfoMap,
46 | maxColumns = result.maxColumns,
47 | config = config
48 | )
49 | }
50 |
51 | internal fun updateState() {
52 | state.value = computeNextState()
53 | }
54 |
55 | /**
56 | * Computes the amount of events that are contained by the given slop. The events will be the
57 | * sum of earlier slots events plus this slot's events.
58 | * */
59 | private fun computeSlotInfo(
60 | slots: List,
61 | slotToDecimalEventMap: Map>,
62 | config: Config
63 | ): ComputeSlotInfoResult {
64 |
65 | val slotInfoMap = mutableMapOf()
66 | var maxColumns = 1
67 |
68 | var isLeftIter = when (config.eventsArrangement) {
69 | is EventsArrangement.LeftToRight,
70 | is EventsArrangement.MixedDirections -> true
71 |
72 | is EventsArrangement.RightToLeft -> false
73 | }
74 |
75 | slots.forEachIndexed { idx, slotIter ->
76 |
77 | val slotLeftColumnMap = mutableMapOf()
78 | val slotRightColumnMap = mutableMapOf()
79 |
80 | slotToDecimalEventMap[slotIter]?.forEachIndexed { idx, event ->
81 |
82 | val eventSlot = getSlotForValue(startValue = event.startValue)
83 |
84 | getSlotsIncludeStartSlot(event, eventSlot, slots).forEach { containingSlot ->
85 | // Update column mark
86 | if (isLeftIter) {
87 | slotLeftColumnMap.put(containingSlot, idx + 1)
88 | } else {
89 | slotRightColumnMap.put(containingSlot, idx + 1)
90 | }
91 |
92 | // Update events counter
93 | val currentSlotInfo =
94 | slotInfoMap.getOrPut(containingSlot) { SlotInfo(0, 0, 0) }
95 | currentSlotInfo.numberOfContainingEvents++
96 | }
97 | }
98 |
99 | val iterSlotInfo = slotInfoMap.getOrPut(slotIter) { SlotInfo(0, 0, 0) }
100 | if (isLeftIter) {
101 | slotLeftColumnMap.entries.forEach { entry ->
102 | if (entry.key.title != slotIter.title) {
103 | val entrySlotInfo = slotInfoMap.getOrPut(entry.key) { SlotInfo(0, 0, 0) }
104 | entrySlotInfo.numberOfColumnsLeft =
105 | iterSlotInfo.numberOfColumnsLeft + entry.value
106 | }
107 | }
108 | iterSlotInfo.numberOfColumnsLeft += slotLeftColumnMap.entries.firstOrNull()?.value
109 | ?: 0
110 | } else {
111 | slotRightColumnMap.entries.forEach { entry ->
112 | if (entry.key.title != slotIter.title) {
113 | val entrySlotInfo = slotInfoMap.getOrPut(entry.key) { SlotInfo(0, 0, 0) }
114 | entrySlotInfo.numberOfColumnsRight =
115 | iterSlotInfo.numberOfColumnsRight + entry.value
116 | }
117 | }
118 | iterSlotInfo.numberOfColumnsRight += slotRightColumnMap.entries.firstOrNull()?.value
119 | ?: 0
120 | }
121 |
122 | // In the case of eventsArrangement == EventsArrangement.MixedDirections, then
123 | // lets change the layout direction.
124 | if (config.eventsArrangement is EventsArrangement.MixedDirections) {
125 | isLeftIter = !isLeftIter
126 | }
127 | }
128 |
129 | slotInfoMap.entries.fold(1) { acc, entry ->
130 | val slotColumns = entry.value.getTotalColumnSpans()
131 | if (slotColumns > maxColumns) maxColumns = slotColumns
132 | maxColumns
133 | }
134 | println("DailyAgendaState: maxColumns: $maxColumns")
135 | return ComputeSlotInfoResult(slotInfoMap, maxColumns)
136 | }
137 |
138 | }
139 |
140 | private class ComputeSlotInfoResult(
141 | val slotInfoMap: Map,
142 | val maxColumns: Int
143 | )
144 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/decimalslots/DecimalSlotsDataUpdater.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.decimalslots
2 |
3 | import kotlin.uuid.ExperimentalUuidApi
4 | import kotlin.uuid.Uuid
5 |
6 | open class DecimalSlotsDataUpdater internal constructor(
7 | val decimalSlotsBaseLayoutStateController: DecimalSlotsBaseLayoutStateController
8 | ) {
9 |
10 | private var isListOperation = false
11 | private val slotToDecimalEventMapSortedTemp: MutableMap> =
12 | mutableMapOf()
13 |
14 | @OptIn(ExperimentalUuidApi::class)
15 | fun addDecimalEvent(
16 | uuid: Uuid = Uuid.random(),
17 | title: String,
18 | description: String,
19 | startValue: Float,
20 | endValue: Float,
21 | ): Boolean = addDecimalEvent(
22 | DecimalEvent(
23 | uuid = uuid,
24 | startValue = startValue,
25 | endValue = endValue,
26 | title = title,
27 | description = description
28 | )
29 | )
30 |
31 | @OptIn(ExperimentalUuidApi::class)
32 | fun addDecimalEvent(decimalEvent: DecimalEvent): Boolean {
33 | val eventSlot =
34 | decimalSlotsBaseLayoutStateController.getSlotForValue(startValue = decimalEvent.startValue)
35 | val siblingEvents =
36 | decimalSlotsBaseLayoutStateController.slotToDecimalEventMapSorted[eventSlot] ?: mutableListOf()
37 |
38 | var insertionIndex = 0
39 | for (idx in siblingEvents.lastIndex downTo 0) {
40 | if (siblingEvents[idx].endValue >= decimalEvent.endValue) {
41 | insertionIndex = idx + 1; break
42 | }
43 | }
44 | siblingEvents.add(insertionIndex, decimalEvent)
45 | return true
46 | }
47 |
48 | fun addDecimalEventList(startValue: Float, segments: List) {
49 | isListOperation = true
50 | val slot = decimalSlotsBaseLayoutStateController.getSlotForValue(startValue = startValue)
51 |
52 | if (slotToDecimalEventMapSortedTemp.contains(slot)) {
53 | slotToDecimalEventMapSortedTemp[slot]!!.addAll(elements = segments)
54 | } else {
55 | slotToDecimalEventMapSortedTemp.put(slot, segments.toMutableList())
56 | }
57 | }
58 |
59 | fun traverseSlotsForDecimalEvent(predicate: (DecimalEvent) -> Boolean): SlotTraversalResult? {
60 | var decimalEventMatching: DecimalEvent? = null
61 | val entryMatching =
62 | decimalSlotsBaseLayoutStateController.slotToDecimalEventMapSorted.entries.find { entry ->
63 | decimalEventMatching = entry.value.find { event -> predicate.invoke(event) }
64 | decimalEventMatching != null
65 | }
66 | return decimalEventMatching?.let {
67 | SlotTraversalResult(entryMatching!!, decimalEventMatching)
68 | }
69 | }
70 |
71 | fun removeDecimalEventByTittle(eventTitle: String): Boolean {
72 | val decimalEventMatchingResult = traverseSlotsForDecimalEvent { event ->
73 | eventTitle == event.title
74 | } ?: return false
75 |
76 | val entry = decimalEventMatchingResult.entry
77 | val decimalEvent = decimalEventMatchingResult.decimalEvent
78 |
79 | return entry.value.remove(decimalEvent)
80 | }
81 |
82 | fun removeDecimalEvent(decimalEvent: DecimalEvent): Boolean {
83 | val eventSlot =
84 | decimalSlotsBaseLayoutStateController.getSlotForValue(startValue = decimalEvent.startValue)
85 | val siblingEvents =
86 | decimalSlotsBaseLayoutStateController.slotToDecimalEventMapSorted[eventSlot] ?: return false
87 |
88 | return siblingEvents.remove(decimalEvent)
89 | }
90 |
91 | internal fun commit() {
92 |
93 | if (isListOperation) {
94 | val slotToDecimalEventMapSortedMerge: MutableMap> =
95 | mutableMapOf()
96 |
97 | for (slot in decimalSlotsBaseLayoutStateController.slots) {
98 | val addedSegments: MutableList? =
99 | slotToDecimalEventMapSortedTemp[slot]
100 | val existingSegments: MutableList =
101 | decimalSlotsBaseLayoutStateController.slotToDecimalEventMapSorted[slot]!!
102 |
103 | if (addedSegments != null) {
104 | addedSegments.addAll(existingSegments)
105 | slotToDecimalEventMapSortedMerge.put(slot, addedSegments)
106 | } else {
107 | slotToDecimalEventMapSortedMerge.put(slot, existingSegments)
108 | }
109 | }
110 |
111 | /**
112 | * Sort the events to maximize spacing when the layout runs.
113 | * */
114 | val endTimeComparator =
115 | Comparator { decimalEvent1: DecimalEvent, decimalEvent2: DecimalEvent ->
116 | val diff = decimalEvent2.endValue - decimalEvent1.endValue
117 | when {
118 | (diff > 0F) -> 1
119 | (diff < 0F) -> -1
120 | else -> 0
121 | }
122 | }
123 | slotToDecimalEventMapSortedMerge.entries.forEach { entry ->
124 | val eventsSortedByEndTime =
125 | entry.value.sortedWith(endTimeComparator).toMutableList()
126 | decimalSlotsBaseLayoutStateController.slotToDecimalEventMapSorted.put(
127 | entry.key,
128 | eventsSortedByEndTime
129 | )
130 | }
131 | isListOperation = false
132 | }
133 |
134 | decimalSlotsBaseLayoutStateController.updateState()
135 | }
136 |
137 | fun postUpdate(block: DecimalSlotsDataUpdater.() -> Unit) {
138 | this.block()
139 | commit()
140 | }
141 |
142 | }
143 |
144 | class SlotTraversalResult(
145 | val entry: MutableMap.MutableEntry>,
146 | val decimalEvent: DecimalEvent
147 | )
148 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/data/TimeSlotsDataSample.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.data
2 |
3 | import com.macaosoftware.ui.dailyagenda.timeslots.LocalTimeEvent
4 | import com.macaosoftware.ui.dailyagenda.timeslots.TimeSlotsStateController
5 | import kotlinx.datetime.LocalTime
6 | import kotlin.uuid.ExperimentalUuidApi
7 | import kotlin.uuid.Uuid
8 |
9 | @OptIn(ExperimentalUuidApi::class)
10 | class TimeSlotsDataSample(timeSlotsStateController: TimeSlotsStateController) {
11 |
12 | init {
13 | timeSlotsStateController.timeSlotsDataUpdater.postUpdate {
14 | addEvent(
15 | uuid = Uuid.random(),
16 | title = "EV0",
17 | description = Constants.EmptyDescription,
18 | startTime = LocalTime(hour = 8, minute = 0),
19 | endTime = LocalTime(hour = 8, minute = 30),
20 | )
21 | addEventList(
22 | startTime = LocalTime(hour = 8, minute = 0),
23 | events = createLocalTimeEventsFor800AM()
24 | )
25 | addEventList(
26 | startTime = LocalTime(hour = 8, minute = 30),
27 | events = createLocalTimeEventsFor830AM()
28 | )
29 | addEventList(
30 | startTime = LocalTime(hour = 9, minute = 0),
31 | events = createLocalTimeEventsFor900AM()
32 | )
33 | addEventList(
34 | startTime = LocalTime(hour = 9, minute = 30),
35 | events = createLocalTimeEventsFor930AM()
36 | )
37 | addEvent(
38 | uuid = Uuid.random(),
39 | title = "EVL",
40 | description = Constants.EmptyDescription,
41 | startTime = LocalTime(hour = 8, minute = 0),
42 | endTime = LocalTime(hour = 9, minute = 0),
43 | )
44 | }
45 | }
46 |
47 | fun createLocalTimeEventsFor800AM(): List {
48 | return listOf(
49 | LocalTimeEvent(
50 | uuid = Uuid.random(),
51 | title = "Ev1",
52 | description = Constants.EmptyDescription,
53 | startTime = LocalTime(8, 15),
54 | endTime = LocalTime(11, 0)
55 | ),
56 | LocalTimeEvent(
57 | uuid = Uuid.random(),
58 | title = "Ev2",
59 | description = Constants.EmptyDescription,
60 | startTime = LocalTime(8, 0),
61 | endTime = LocalTime(8, 45)
62 | ),
63 | LocalTimeEvent(
64 | uuid = Uuid.random(),
65 | title = "Ev3",
66 | description = Constants.EmptyDescription,
67 | startTime = LocalTime(8, 6),
68 | endTime = LocalTime(8, 25)
69 | ),
70 | )
71 | }
72 |
73 | fun createLocalTimeEventsFor830AM(): List {
74 | return listOf(
75 | LocalTimeEvent(
76 | uuid = Uuid.random(),
77 | title = "Ev4",
78 | description = Constants.EmptyDescription,
79 | startTime = LocalTime(8, 40),
80 | endTime = LocalTime(11, 5)
81 | ),
82 | LocalTimeEvent(
83 | uuid = Uuid.random(),
84 | title = "Ev5",
85 | description = Constants.EmptyDescription,
86 | startTime = LocalTime(8, 50),
87 | endTime = LocalTime(9, 30)
88 | ),
89 | LocalTimeEvent(
90 | uuid = Uuid.random(),
91 | title = "Ev6",
92 | description = Constants.EmptyDescription,
93 | startTime = LocalTime(8, 30),
94 | endTime = LocalTime(9, 30)
95 | ),
96 | LocalTimeEvent(
97 | uuid = Uuid.random(),
98 | title = "Ev7",
99 | description = Constants.EmptyDescription,
100 | startTime = LocalTime(8, 30),
101 | endTime = LocalTime(9, 0)
102 | )
103 | )
104 | }
105 |
106 | fun createLocalTimeEventsFor900AM(): List {
107 | return listOf(
108 | LocalTimeEvent(
109 | uuid = Uuid.random(),
110 | title = "Ev8",
111 | description = Constants.EmptyDescription,
112 | startTime = LocalTime(9, 0),
113 | endTime = LocalTime(9, 30)
114 | ),
115 | LocalTimeEvent(
116 | uuid = Uuid.random(),
117 | title = "Ev9",
118 | description = Constants.EmptyDescription,
119 | startTime = LocalTime(9, 12),
120 | endTime = LocalTime(10, 0)
121 | ),
122 | LocalTimeEvent(
123 | uuid = Uuid.random(),
124 | title = "Ev10",
125 | description = Constants.EmptyDescription,
126 | startTime = LocalTime(9, 15),
127 | endTime = LocalTime(10, 0)
128 | )
129 | )
130 | }
131 |
132 | fun createLocalTimeEventsFor930AM(): List {
133 | return listOf(
134 | LocalTimeEvent(
135 | uuid = Uuid.random(),
136 | title = "Ev11",
137 | description = Constants.EmptyDescription,
138 | startTime = LocalTime(9, 30),
139 | endTime = LocalTime(11, 0)
140 | ),
141 | LocalTimeEvent(
142 | uuid = Uuid.random(),
143 | title = "Ev12",
144 | description = Constants.EmptyDescription,
145 | startTime = LocalTime(9, 30),
146 | endTime = LocalTime(10, 30)
147 | ),
148 | LocalTimeEvent(
149 | uuid = Uuid.random(),
150 | title = "Ev13",
151 | description = Constants.EmptyDescription,
152 | startTime = LocalTime(9, 50),
153 | endTime = LocalTime(10, 25)
154 | ),
155 | LocalTimeEvent(
156 | uuid = Uuid.random(),
157 | title = "Ev14",
158 | description = Constants.EmptyDescription,
159 | startTime = LocalTime(9, 40),
160 | endTime = LocalTime(10, 0)
161 | ),
162 | LocalTimeEvent(
163 | uuid = Uuid.random(),
164 | title = "Ev15",
165 | description = Constants.EmptyDescription,
166 | startTime = LocalTime(9, 55),
167 | endTime = LocalTime(10, 12)
168 | )
169 | )
170 | }
171 |
172 | }
173 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/data/EpgSlotsDataSample.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.data
2 |
3 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgChannel
4 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgSlotsStateController
5 | import com.macaosoftware.ui.dailyagenda.timeslots.LocalTimeEvent
6 | import kotlinx.datetime.LocalTime
7 | import kotlin.uuid.ExperimentalUuidApi
8 | import kotlin.uuid.Uuid
9 |
10 | @OptIn(ExperimentalUuidApi::class)
11 | class EpgSlotsDataSample(epgSlotsStateController: EpgSlotsStateController) {
12 |
13 | init {
14 | epgSlotsStateController.epgSlotsDataUpdater.postUpdate {
15 | addChannel(
16 | EpgChannel(
17 | name = "Ch1",
18 | events = listOf(
19 | LocalTimeEvent(
20 | uuid = Uuid.random(),
21 | title = "Ev1",
22 | description = Constants.EmptyDescription,
23 | startTime = LocalTime(9, 0),
24 | endTime = LocalTime(10, 0)
25 | ),
26 | LocalTimeEvent(
27 | uuid = Uuid.random(),
28 | title = "Ev2",
29 | description = Constants.EmptyDescription,
30 | startTime = LocalTime(10, 0),
31 | endTime = LocalTime(11, 30)
32 | )
33 | )
34 | )
35 | )
36 |
37 | addChannel(
38 | EpgChannel(
39 | name = "Ch2",
40 | events = listOf(
41 | LocalTimeEvent(
42 | uuid = Uuid.random(),
43 | title = "Ev3",
44 | description = Constants.EmptyDescription,
45 | startTime = LocalTime(9, 30),
46 | endTime = LocalTime(10, 15)
47 | ),
48 | LocalTimeEvent(
49 | uuid = Uuid.random(),
50 | title = "Ev4",
51 | description = Constants.EmptyDescription,
52 | startTime = LocalTime(10, 30),
53 | endTime = LocalTime(11, 0)
54 | )
55 | )
56 | )
57 | )
58 | addChannel(
59 | EpgChannel(
60 | name = "Ch3",
61 | events = listOf(
62 | LocalTimeEvent(
63 | uuid = Uuid.random(),
64 | title = "Ev5",
65 | description = Constants.EmptyDescription,
66 | startTime = LocalTime(10, 0),
67 | endTime = LocalTime(11, 0)
68 | ),
69 | LocalTimeEvent(
70 | uuid = Uuid.random(),
71 | title = "Ev6",
72 | description = Constants.EmptyDescription,
73 | startTime = LocalTime(12, 0),
74 | endTime = LocalTime(13, 30)
75 | )
76 | )
77 | )
78 | )
79 | addChannel(
80 | EpgChannel(
81 | name = "Ch4",
82 | events = listOf(
83 | LocalTimeEvent(
84 | uuid = Uuid.random(),
85 | title = "Ev6",
86 | description = Constants.EmptyDescription,
87 | startTime = LocalTime(9, 30),
88 | endTime = LocalTime(11, 0)
89 | ),
90 | LocalTimeEvent(
91 | uuid = Uuid.random(),
92 | title = "Ev7",
93 | description = Constants.EmptyDescription,
94 | startTime = LocalTime(11, 0),
95 | endTime = LocalTime(11, 45)
96 | )
97 | )
98 | )
99 | )
100 | addChannel(
101 | EpgChannel(
102 | name = "Ch5",
103 | events = listOf(
104 | LocalTimeEvent(
105 | uuid = Uuid.random(),
106 | title = "Ev8",
107 | description = Constants.EmptyDescription,
108 | startTime = LocalTime(8, 30),
109 | endTime = LocalTime(10, 0)
110 | ),
111 | LocalTimeEvent(
112 | uuid = Uuid.random(),
113 | title = "Ev9",
114 | description = Constants.EmptyDescription,
115 | startTime = LocalTime(11, 0),
116 | endTime = LocalTime(11, 30)
117 | )
118 | )
119 | )
120 | )
121 | addChannel(
122 | EpgChannel(
123 | name = "Ch6",
124 | events = listOf(
125 | LocalTimeEvent(
126 | uuid = Uuid.random(),
127 | title = "Ev10",
128 | description = Constants.EmptyDescription,
129 | startTime = LocalTime(7, 0),
130 | endTime = LocalTime(8, 0)
131 | ),
132 | LocalTimeEvent(
133 | uuid = Uuid.random(),
134 | title = "Ev11",
135 | description = Constants.EmptyDescription,
136 | startTime = LocalTime(8, 0),
137 | endTime = LocalTime(9, 30)
138 | ),
139 | LocalTimeEvent(
140 | uuid = Uuid.random(),
141 | title = "Ev12",
142 | description = Constants.EmptyDescription,
143 | startTime = LocalTime(9, 30),
144 | endTime = LocalTime(10, 30)
145 | ),
146 | LocalTimeEvent(
147 | uuid = Uuid.random(),
148 | title = "Ev13",
149 | description = Constants.EmptyDescription,
150 | startTime = LocalTime(10, 30),
151 | endTime = LocalTime(11, 0)
152 | ),
153 | LocalTimeEvent(
154 | uuid = Uuid.random(),
155 | title = "Ev14",
156 | description = Constants.EmptyDescription,
157 | startTime = LocalTime(11, 0),
158 | endTime = LocalTime(12, 0)
159 | )
160 | )
161 | )
162 | )
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/decimalslots/DecimalSlotsBaseLayout.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.decimalslots
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.offset
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.foundation.layout.wrapContentSize
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalDensity
15 | import androidx.compose.ui.platform.LocalWindowInfo
16 | import androidx.compose.ui.unit.Dp
17 | import androidx.compose.ui.unit.dp
18 |
19 | @Composable
20 | internal fun DecimalSlotsBaseLayout(
21 | decimalSlotsBaseLayoutState: DecimalSlotsBaseLayoutState,
22 | eventContentProvider: @Composable (decimalEvent: DecimalEvent) -> Unit
23 | ) {
24 | Box(
25 | modifier = Modifier
26 | .padding(start = decimalSlotsBaseLayoutState.config.timelineLeftPadding.dp)
27 | .fillMaxSize()
28 | ) {
29 |
30 | val containerSize = LocalWindowInfo.current.containerSize
31 | val density = LocalDensity.current.density
32 |
33 | val eventContainerWidth =
34 | (containerSize.width.dp / density) - decimalSlotsBaseLayoutState.config.timelineLeftPadding.dp
35 |
36 | LeftThenRightLayout(
37 | decimalSlotsBaseLayoutState = decimalSlotsBaseLayoutState,
38 | eventContainerWidth = eventContainerWidth,
39 | eventContentProvider = eventContentProvider
40 | )
41 | }
42 | }
43 |
44 | @Composable
45 | private fun LeftThenRightLayout(
46 | decimalSlotsBaseLayoutState: DecimalSlotsBaseLayoutState,
47 | eventContainerWidth: Dp,
48 | eventContentProvider: @Composable (decimalEvent: DecimalEvent) -> Unit
49 | ) {
50 | val config = decimalSlotsBaseLayoutState.config
51 | val minimumWidth = eventContainerWidth / decimalSlotsBaseLayoutState.maxColumns
52 |
53 | var arrangeToTheLeft = remember(key1 = decimalSlotsBaseLayoutState, key2 = eventContainerWidth) {
54 | when (config.eventsArrangement) {
55 | is EventsArrangement.LeftToRight,
56 | is EventsArrangement.MixedDirections -> true
57 |
58 | is EventsArrangement.RightToLeft -> false
59 | }
60 | }
61 |
62 | val offsetInfoMap = remember(key1 = decimalSlotsBaseLayoutState, key2 = eventContainerWidth) {
63 | val offsetMap = mutableMapOf()
64 | for (i in 0..decimalSlotsBaseLayoutState.slots.lastIndex) {
65 | offsetMap.put(decimalSlotsBaseLayoutState.slots[i], OffsetInfo())
66 | }
67 | offsetMap
68 | }
69 |
70 | decimalSlotsBaseLayoutState.slotToDecimalEventMap.entries.forEach { entry ->
71 | val slot = entry.key
72 | val numbersOfSlots = (slot.startValue - config.initialSlotValue) * config.slotScale
73 | val offsetY = (numbersOfSlots * config.slotHeight).dp
74 |
75 | var offsetXAbsolute: Dp = 0.dp
76 | var offsetInfo: OffsetInfo
77 | val offsetX = if (arrangeToTheLeft) {
78 | offsetInfo = offsetInfoMap[slot] ?: OffsetInfo()
79 | offsetXAbsolute = offsetInfo.getTotalLeftOffset()
80 | offsetXAbsolute
81 | } else {
82 | offsetInfo = offsetInfoMap[slot] ?: OffsetInfo()
83 | offsetXAbsolute = offsetInfo.getTotalRightOffset()
84 | -offsetXAbsolute
85 | }
86 |
87 | val slotRemainingWidth = eventContainerWidth - offsetXAbsolute
88 |
89 | if (arrangeToTheLeft) {
90 | Row(
91 | modifier = Modifier
92 | .offset(y = offsetY, x = offsetX)
93 | .wrapContentSize()
94 | ) {
95 | entry.value.forEachIndexed { idx, event ->
96 | val eventTranslation = getEventTranslationInSlot(event, slot, config)
97 | val eventHeight = getEventHeight(event, config)
98 | val eventWidth = getEventWidthFromLeft(
99 | decimalSlotsBaseLayoutState = decimalSlotsBaseLayoutState,
100 | decimalEvent = event,
101 | eventSlot = slot,
102 | amountOfEventsInSameSlot = entry.value.size,
103 | currentEventIndex = idx,
104 | eventContainerWidth = eventContainerWidth,
105 | offsetInfo = offsetInfo,
106 | slotRemainingWidth = slotRemainingWidth,
107 | minimumWidth = minimumWidth
108 | )
109 | updateEventOffsetX(
110 | decimalSlotsBaseLayoutState = decimalSlotsBaseLayoutState,
111 | decimalEvent = event,
112 | eventSlot = slot,
113 | slotOffsetInfoMap = offsetInfoMap,
114 | eventWidth = eventWidth,
115 | isLeft = true
116 | )
117 | Box(
118 | modifier = Modifier
119 | .offset(y = eventTranslation)
120 | .height(height = eventHeight)
121 | .width(width = eventWidth)
122 | ) { eventContentProvider.invoke(event) }
123 | }
124 | }
125 |
126 | } else {
127 | RtlCustomRow(
128 | modifier = Modifier
129 | .offset(y = offsetY, x = offsetX)
130 | .wrapContentSize(),
131 | ) {
132 | entry.value.forEachIndexed { idx, event ->
133 | val eventTranslation = getEventTranslationInSlot(event, slot, config)
134 | val eventHeight = getEventHeight(event, config)
135 | val eventWidth = getEventWidthFromRight(
136 | decimalSlotsBaseLayoutState = decimalSlotsBaseLayoutState,
137 | decimalEvent = event,
138 | eventSlot = slot,
139 | amountOfEventsInSameSlot = entry.value.size,
140 | currentEventIndex = idx,
141 | eventContainerWidth = eventContainerWidth,
142 | offsetInfo = offsetInfo,
143 | slotRemainingWidth = slotRemainingWidth,
144 | minimumWidth = minimumWidth
145 | )
146 | updateEventOffsetX(
147 | decimalSlotsBaseLayoutState = decimalSlotsBaseLayoutState,
148 | decimalEvent = event,
149 | eventSlot = slot,
150 | slotOffsetInfoMap = offsetInfoMap,
151 | eventWidth = eventWidth,
152 | isLeft = false
153 | )
154 | Box(
155 | modifier = Modifier
156 | .offset(y = eventTranslation)
157 | .height(height = eventHeight)
158 | .width(width = eventWidth)
159 | ) { eventContentProvider.invoke(event) }
160 | }
161 | }
162 | }
163 |
164 | if (decimalSlotsBaseLayoutState.config.eventsArrangement is EventsArrangement.MixedDirections) {
165 | arrangeToTheLeft = !arrangeToTheLeft
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/data/DecimalSlotsDataSample.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.data
2 |
3 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsStateController
4 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalEvent
5 | import kotlin.uuid.ExperimentalUuidApi
6 | import kotlin.uuid.Uuid
7 |
8 | class DecimalSlotsDataSample(decimalSlotsStateController: DecimalSlotsStateController) {
9 |
10 | init {
11 | decimalSlotsStateController.decimalSlotsDataUpdater.postUpdate {
12 | addDecimalEventList(
13 | startValue = 8.0F,
14 | segments = createSegmentsFor8()
15 | )
16 | addDecimalEventList(
17 | startValue = 8.5F,
18 | segments = createEventsFor8_5()
19 | )
20 | addDecimalEventList(
21 | startValue = 9.0F,
22 | segments = createEventsFor9()
23 | )
24 | addDecimalEventList(
25 | startValue = 9.5F,
26 | segments = createEventsFor9_5()
27 | )
28 | addDecimalEventList(
29 | startValue = 10.0F,
30 | segments = createEventsFor10()
31 | )
32 | addDecimalEventList(
33 | startValue = 10.5F,
34 | segments = createEventsFor10_5()
35 | )
36 | }
37 | }
38 |
39 | @OptIn(ExperimentalUuidApi::class)
40 | private fun createSegmentsFor8(): List {
41 | return listOf(
42 | DecimalEvent(
43 | uuid = Uuid.random(),
44 | title = "Ev1",
45 | description = Constants.EmptyDescription,
46 | startValue = 8.0F,
47 | endValue = 10.0F
48 | ),
49 | DecimalEvent(
50 | uuid = Uuid.random(),
51 | title = "Ev2",
52 | description = Constants.EmptyDescription,
53 | startValue = 8.0F,
54 | endValue = 9.5F
55 | ),
56 | DecimalEvent(
57 | uuid = Uuid.random(),
58 | title = "Ev3",
59 | description = Constants.EmptyDescription,
60 | startValue = 8.0F,
61 | endValue = 9.0F
62 | ),
63 | DecimalEvent(
64 | uuid = Uuid.random(),
65 | title = "Ev4",
66 | description = Constants.EmptyDescription,
67 | startValue = 8.0F,
68 | endValue = 8.75F
69 | ),
70 | DecimalEvent(
71 | uuid = Uuid.random(),
72 | title = "Ev5",
73 | description = Constants.EmptyDescription,
74 | startValue = 8.0F,
75 | endValue = 8.25F
76 | ),
77 | )
78 | }
79 |
80 | @OptIn(ExperimentalUuidApi::class)
81 | private fun createEventsFor8_5(): List {
82 | return listOf(
83 | DecimalEvent(
84 | uuid = Uuid.random(),
85 | title = "Ev6",
86 | description = Constants.EmptyDescription,
87 | startValue = 8.5F,
88 | endValue = 11.0F
89 | ),
90 | DecimalEvent(
91 | uuid = Uuid.random(),
92 | title = "Ev7",
93 | description = Constants.EmptyDescription,
94 | startValue = 8.5F,
95 | endValue = 9.5F
96 | ),
97 | DecimalEvent(
98 | uuid = Uuid.random(),
99 | title = "Ev8",
100 | description = Constants.EmptyDescription,
101 | startValue = 8.5F,
102 | endValue = 9.0F
103 | ),
104 | )
105 | }
106 |
107 | @OptIn(ExperimentalUuidApi::class)
108 | private fun createEventsFor9(): List {
109 | return listOf(
110 | DecimalEvent(
111 | uuid = Uuid.random(),
112 | title = "Ev9",
113 | description = Constants.EmptyDescription,
114 | startValue = 9.0F,
115 | endValue = 10.0F
116 | ),
117 | DecimalEvent(
118 | uuid = Uuid.random(),
119 | title = "Ev10",
120 | description = Constants.EmptyDescription,
121 | startValue = 9.0F,
122 | endValue = 10.0F
123 | )
124 | )
125 | }
126 |
127 | @OptIn(ExperimentalUuidApi::class)
128 | private fun createEventsFor9_5(): List {
129 | return listOf(
130 | DecimalEvent(
131 | uuid = Uuid.random(),
132 | title = "Ev11",
133 | description = Constants.EmptyDescription,
134 | startValue = 9.5F,
135 | endValue = 11.0F
136 | ),
137 | DecimalEvent(
138 | uuid = Uuid.random(),
139 | title = "Ev12",
140 | description = Constants.EmptyDescription,
141 | startValue = 9.5F,
142 | endValue = 10.5F
143 | ),
144 | DecimalEvent(
145 | uuid = Uuid.random(),
146 | title = "Ev13",
147 | description = Constants.EmptyDescription,
148 | startValue = 9.5F,
149 | endValue = 10.0F
150 | ),
151 | DecimalEvent(
152 | uuid = Uuid.random(),
153 | title = "Ev14",
154 | description = Constants.EmptyDescription,
155 | startValue = 9.5F,
156 | endValue = 10.0F
157 | ),
158 | DecimalEvent(
159 | uuid = Uuid.random(),
160 | title = "Ev15",
161 | description = Constants.EmptyDescription,
162 | startValue = 9.5F,
163 | endValue = 10.0F
164 | ),
165 | DecimalEvent(
166 | uuid = Uuid.random(),
167 | title = "Ev16",
168 | description = Constants.EmptyDescription,
169 | startValue = 9.5F,
170 | endValue = 10.0F
171 | )
172 | )
173 | }
174 |
175 | @OptIn(ExperimentalUuidApi::class)
176 | private fun createEventsFor10(): List {
177 | return listOf(
178 | DecimalEvent(
179 | uuid = Uuid.random(),
180 | title = "Ev17",
181 | description = Constants.EmptyDescription,
182 | startValue = 10.0F,
183 | endValue = 11.5F
184 | ),
185 | DecimalEvent(
186 | uuid = Uuid.random(),
187 | title = "Ev18",
188 | description = Constants.EmptyDescription,
189 | startValue = 10.0F,
190 | endValue = 11.0F
191 | ),
192 | DecimalEvent(
193 | uuid = Uuid.random(),
194 | title = "Ev19",
195 | description = Constants.EmptyDescription,
196 | startValue = 10.0F,
197 | endValue = 10.5F
198 | )
199 | )
200 | }
201 |
202 | @OptIn(ExperimentalUuidApi::class)
203 | private fun createEventsFor10_5(): List {
204 | return listOf(
205 | DecimalEvent(
206 | uuid = Uuid.random(),
207 | title = "Ev20",
208 | description = Constants.EmptyDescription,
209 | startValue = 10.5F,
210 | endValue = 11.5F
211 | ),
212 | DecimalEvent(
213 | uuid = Uuid.random(),
214 | title = "Ev21",
215 | description = Constants.EmptyDescription,
216 | startValue = 10.5F,
217 | endValue = 11.0F
218 | )
219 | )
220 | }
221 |
222 | }
223 |
--------------------------------------------------------------------------------
/daily-agenda-view/src/commonMain/kotlin/com/macaosoftware/ui/dailyagenda/decimalslots/LayoutUtil.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.dailyagenda.decimalslots
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.platform.LocalDensity
5 | import androidx.compose.ui.unit.Dp
6 | import androidx.compose.ui.unit.dp
7 | import androidx.compose.ui.unit.max
8 |
9 | /**
10 | * Returns the Event Y axis offset in Dp from the slot start time.
11 | * */
12 | internal fun getEventTranslationInSlot(decimalEvent: DecimalEvent, eventSlot: Slot, config: Config): Dp {
13 | val fractionOfSlots = (decimalEvent.startValue - eventSlot.startValue) * config.slotScale
14 | return (fractionOfSlots * config.slotHeight).dp
15 | }
16 |
17 | /**
18 | * Returns the height in Dp for a given Event based on the amount of slots it touches.
19 | * */
20 | internal fun getEventHeight(decimalEvent: DecimalEvent, config: Config): Dp {
21 | val numberOfSlots = (decimalEvent.endValue - decimalEvent.startValue) * config.slotScale
22 | return (numberOfSlots * config.slotHeight).dp
23 | }
24 |
25 | /**
26 | * For a given event, it returns the maximum number of sibling events, across all the slots the
27 | * event touches.
28 | * */
29 | // TODO: This information can be computed in the data setup and saved in a
30 | // Map. Avoiding the iterations
31 | // during composition.
32 | private fun getMaximumNumberOfSiblingsInContainingSlots(
33 | decimalEvent: DecimalEvent,
34 | eventSlot: Slot,
35 | decimalSlotsBaseLayoutState: DecimalSlotsBaseLayoutState
36 | ): Int {
37 | val containingSlots = getSlotsIncludeStartSlot(decimalEvent, eventSlot, decimalSlotsBaseLayoutState.slots)
38 | val maxNumberOfEvents = containingSlots.fold(initial = 0) { maxNumberOfEvents, slot ->
39 | val numberOfEvents = decimalSlotsBaseLayoutState.slotInfoMap[slot]?.getTotalColumnSpans() ?: 0
40 | if (numberOfEvents > maxNumberOfEvents) {
41 | numberOfEvents
42 | } else maxNumberOfEvents
43 | }
44 | println("LayoutUtil: maxNumberOfEvents: $maxNumberOfEvents")
45 | return maxNumberOfEvents
46 | }
47 |
48 | /**
49 | * Returns a list of the slots touched by the given event. Including its own start slot.
50 | * */
51 | internal fun getSlotsIncludeStartSlot(
52 | decimalEvent: DecimalEvent,
53 | eventSlot: Slot,
54 | slots: List
55 | ): List {
56 | val slotIndex = slots.indexOf(eventSlot)
57 | val eventSlots = slots.subList(slotIndex, slots.size)
58 | val containingSlots = mutableListOf()
59 | eventSlots.forEach { slot ->
60 | if (decimalEvent.endValue > slot.startValue + 0.0001) {
61 | containingSlots.add(slot)
62 | }
63 | }
64 | return containingSlots
65 | }
66 |
67 | /**
68 | * Returns a list of the slots touched by the given event. Excluding its own start slot.
69 | * */
70 | internal fun getSlotsIgnoreStartSlot(
71 | decimalSlotsBaseLayoutState: DecimalSlotsBaseLayoutState,
72 | decimalEvent: DecimalEvent,
73 | eventSlot: Slot
74 | ): List {
75 | val slotIndex = decimalSlotsBaseLayoutState.slots.indexOf(eventSlot)
76 | val laterSlots = decimalSlotsBaseLayoutState.slots.subList(slotIndex + 1, decimalSlotsBaseLayoutState.slots.size)
77 | val containingSlots = mutableListOf()
78 | laterSlots.forEach { slot ->
79 | if (decimalEvent.endValue > slot.startValue + 0.0001) {
80 | containingSlots.add(slot)
81 | }
82 | }
83 | return containingSlots
84 | }
85 |
86 | internal fun updateEventOffsetX(
87 | decimalSlotsBaseLayoutState: DecimalSlotsBaseLayoutState,
88 | decimalEvent: DecimalEvent,
89 | eventSlot: Slot,
90 | slotOffsetInfoMap: Map,
91 | eventWidth: Dp,
92 | isLeft: Boolean
93 | ) {
94 |
95 | val eventSlops = getSlotsIgnoreStartSlot(
96 | decimalSlotsBaseLayoutState = decimalSlotsBaseLayoutState,
97 | decimalEvent = decimalEvent,
98 | eventSlot = eventSlot
99 | )
100 | val currentSlotOffsetInfo = slotOffsetInfoMap[eventSlot]!!
101 |
102 | if (isLeft) {
103 | currentSlotOffsetInfo.leftAccumulated += eventWidth
104 | } else {
105 | currentSlotOffsetInfo.rightAccumulated += eventWidth
106 | }
107 |
108 | eventSlops.forEach { slot ->
109 | val offsetInfo = slotOffsetInfoMap[slot]!!
110 | if (isLeft) {
111 | offsetInfo.leftStartOffset = currentSlotOffsetInfo.getTotalLeftOffset()
112 | println("LayoutUtil: slot: ${slot.title} has a new Left offset of: ${offsetInfo.leftAccumulated}")
113 | } else {
114 | offsetInfo.rightStartOffset = currentSlotOffsetInfo.getTotalRightOffset()
115 | println("LayoutUtil: slot: ${slot.title} has a new Right offset of: ${offsetInfo.rightAccumulated}")
116 | }
117 | }
118 | }
119 |
120 | internal fun DecimalEvent.isSingleSlot(eventSlot: Slot): Boolean {
121 | return eventSlot.endValue >= endValue
122 | }
123 |
124 | internal fun getEventWidthFromLeft(
125 | decimalSlotsBaseLayoutState: DecimalSlotsBaseLayoutState,
126 | decimalEvent: DecimalEvent,
127 | eventSlot: Slot,
128 | amountOfEventsInSameSlot: Int,
129 | currentEventIndex: Int,
130 | eventContainerWidth: Dp,
131 | offsetInfo: OffsetInfo,
132 | slotRemainingWidth: Dp,
133 | minimumWidth: Dp
134 | ): Dp {
135 | if (shouldReturnMinimumAllowedWidth(decimalSlotsBaseLayoutState.config, decimalEvent, eventSlot)) {
136 | return minimumWidth
137 | }
138 |
139 | var singleSlotWidth: Dp? = null
140 |
141 | val eventWidth = if (decimalEvent.isSingleSlot(eventSlot)) {
142 | singleSlotWidth ?: run {
143 | val amountOfSingleSlotEvents = (amountOfEventsInSameSlot - currentEventIndex)
144 | ((eventContainerWidth - offsetInfo.getTotalLeftOffset() - offsetInfo.rightStartOffset) / amountOfSingleSlotEvents).also {
145 | singleSlotWidth = it
146 | }
147 | }
148 | } else {
149 | val widthNumber = getMaximumNumberOfSiblingsInContainingSlots(
150 | decimalEvent,
151 | eventSlot,
152 | decimalSlotsBaseLayoutState
153 | )
154 | (slotRemainingWidth / widthNumber)
155 | }
156 |
157 | return max(minimumWidth, eventWidth)
158 | }
159 |
160 | internal fun getEventWidthFromRight(
161 | decimalSlotsBaseLayoutState: DecimalSlotsBaseLayoutState,
162 | decimalEvent: DecimalEvent,
163 | eventSlot: Slot,
164 | amountOfEventsInSameSlot: Int,
165 | currentEventIndex: Int,
166 | eventContainerWidth: Dp,
167 | offsetInfo: OffsetInfo,
168 | slotRemainingWidth: Dp,
169 | minimumWidth: Dp
170 | ): Dp {
171 | if (shouldReturnMinimumAllowedWidth(decimalSlotsBaseLayoutState.config, decimalEvent, eventSlot)) {
172 | return minimumWidth
173 | }
174 |
175 | var singleSlotWidth: Dp? = null
176 |
177 | val eventWidth: Dp = if (decimalEvent.isSingleSlot(eventSlot)) {
178 | singleSlotWidth ?: run {
179 | val amountOfSingleSlotEvents = (amountOfEventsInSameSlot - currentEventIndex)
180 | ((eventContainerWidth - offsetInfo.leftStartOffset - offsetInfo.getTotalRightOffset()) / amountOfSingleSlotEvents).also {
181 | singleSlotWidth = it
182 | }
183 | }
184 | } else {
185 | val widthNumber = getMaximumNumberOfSiblingsInContainingSlots(
186 | decimalEvent,
187 | eventSlot,
188 | decimalSlotsBaseLayoutState
189 | )
190 | (slotRemainingWidth / widthNumber)
191 | }
192 |
193 | return max(minimumWidth, eventWidth)
194 | }
195 |
196 | private fun shouldReturnMinimumAllowedWidth(
197 | config: Config,
198 | decimalEvent: DecimalEvent,
199 | eventSlot: Slot
200 | ): Boolean {
201 | when (val eventsArrangement = config.eventsArrangement) {
202 | is EventsArrangement.LeftToRight -> {
203 | if (!eventsArrangement.lastEventFillRow) {
204 | return true
205 | }
206 | if (!decimalEvent.isSingleSlot(eventSlot)) {
207 | return true
208 | }
209 | return false
210 | }
211 |
212 | is EventsArrangement.MixedDirections -> {
213 | return when (eventsArrangement.eventWidthType) {
214 | EventWidthType.MaxVariableSize -> false
215 | EventWidthType.FixedSize -> true
216 | EventWidthType.FixedSizeFillLastEvent -> !decimalEvent.isSingleSlot(eventSlot)
217 | }
218 |
219 | }
220 |
221 | is EventsArrangement.RightToLeft -> {
222 | if (!eventsArrangement.lastEventFillRow) {
223 | return true
224 | }
225 | if (!decimalEvent.isSingleSlot(eventSlot)) {
226 | return true
227 | }
228 | return false
229 | }
230 | }
231 | }
232 |
233 | @Composable
234 | internal fun Int.toDp(): Dp = with(receiver = LocalDensity.current) { toDp() }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH="\\\"\\\""
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | if ! command -v java >/dev/null 2>&1
137 | then
138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139 |
140 | Please set the JAVA_HOME variable in your environment to match the
141 | location of your Java installation."
142 | fi
143 | fi
144 |
145 | # Increase the maximum file descriptors if we can.
146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147 | case $MAX_FD in #(
148 | max*)
149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150 | # shellcheck disable=SC2039,SC3045
151 | MAX_FD=$( ulimit -H -n ) ||
152 | warn "Could not query maximum file descriptor limit"
153 | esac
154 | case $MAX_FD in #(
155 | '' | soft) :;; #(
156 | *)
157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158 | # shellcheck disable=SC2039,SC3045
159 | ulimit -n "$MAX_FD" ||
160 | warn "Could not set maximum file descriptor limit to $MAX_FD"
161 | esac
162 | fi
163 |
164 | # Collect all arguments for the java command, stacking in reverse order:
165 | # * args from the command line
166 | # * the main class name
167 | # * -classpath
168 | # * -D...appname settings
169 | # * --module-path (only if needed)
170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171 |
172 | # For Cygwin or MSYS, switch paths to Windows format before running java
173 | if "$cygwin" || "$msys" ; then
174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176 |
177 | JAVACMD=$( cygpath --unix "$JAVACMD" )
178 |
179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
180 | for arg do
181 | if
182 | case $arg in #(
183 | -*) false ;; # don't mess with options #(
184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185 | [ -e "$t" ] ;; #(
186 | *) false ;;
187 | esac
188 | then
189 | arg=$( cygpath --path --ignore --mixed "$arg" )
190 | fi
191 | # Roll the args list around exactly as many times as the number of
192 | # args, so each arg winds up back in the position where it started, but
193 | # possibly modified.
194 | #
195 | # NB: a `for` loop captures its iteration list before it begins, so
196 | # changing the positional parameters here affects neither the number of
197 | # iterations, nor the values presented in `arg`.
198 | shift # remove old arg
199 | set -- "$@" "$arg" # push replacement arg
200 | done
201 | fi
202 |
203 |
204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206 |
207 | # Collect all arguments for the java command:
208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209 | # and any embedded shellness will be escaped.
210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211 | # treated as '${Hostname}' itself on the command line.
212 |
213 | set -- \
214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
215 | -classpath "$CLASSPATH" \
216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217 | "$@"
218 |
219 | # Stop when "xargs" is not available.
220 | if ! command -v xargs >/dev/null 2>&1
221 | then
222 | die "xargs is not available"
223 | fi
224 |
225 | # Use "xargs" to parse quoted args.
226 | #
227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228 | #
229 | # In Bash we could simply go:
230 | #
231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232 | # set -- "${ARGS[@]}" "$@"
233 | #
234 | # but POSIX shell has neither arrays nor command substitution, so instead we
235 | # post-process each arg (as a line of input to sed) to backslash-escape any
236 | # character that might be a shell metacharacter, then use eval to reverse
237 | # that process (while maintaining the separation between arguments), and wrap
238 | # the whole thing up as a single "set" statement.
239 | #
240 | # This will of course break if any of these variables contains a newline or
241 | # an unmatched quote.
242 | #
243 |
244 | eval "set -- $(
245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246 | xargs -n1 |
247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248 | tr '\n' ' '
249 | )" '"$@"'
250 |
251 | exec "$JAVACMD" "$@"
252 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/ui/DayScheduleApp.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.ui
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.combinedClickable
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Scaffold
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotConfig
22 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsStateController
23 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsView
24 | import com.macaosoftware.ui.dailyagenda.decimalslots.EventWidthType
25 | import com.macaosoftware.ui.dailyagenda.decimalslots.EventsArrangement
26 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgChannelSlotConfig
27 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgSlotsStateController
28 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgSlotsView
29 | import com.macaosoftware.ui.dailyagenda.timeslots.TimeSlotConfig
30 | import com.macaosoftware.ui.dailyagenda.timeslots.TimeSlotsStateController
31 | import com.macaosoftware.ui.dailyagenda.timeslots.TimeSlotsView
32 | import com.macaosoftware.ui.data.DecimalSlotsDataSample
33 | import com.macaosoftware.ui.data.EpgSlotsDataSample
34 | import com.macaosoftware.ui.ui.DayScheduleAppViewModel.DecimalSlotsUiActionListener
35 | import com.macaosoftware.ui.ui.DayScheduleAppViewModel.EpgSlotsUiActionListener
36 | import com.macaosoftware.ui.ui.DayScheduleAppViewModel.TimeSlotsUiActionListener
37 | import com.macaosoftware.ui.ui.model.AllDayEvent
38 | import kotlinx.datetime.LocalTime
39 | import org.jetbrains.compose.ui.tooling.preview.Preview
40 | import kotlin.random.Random
41 | import kotlin.uuid.ExperimentalUuidApi
42 | import kotlin.uuid.Uuid
43 |
44 | @Composable
45 | @Preview
46 | fun DayScheduleApp() {
47 | val viewModel = remember { DayScheduleAppViewModel() }
48 | MaterialTheme {
49 | Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
50 | Box(
51 | modifier = Modifier
52 | .fillMaxSize()
53 | .padding(paddingValues = innerPadding)
54 | ) {
55 | when (viewModel.slotsViewType) {
56 | SlotsViewType.Decimal -> {
57 | DecimalSlotExample(
58 | decimalSlotsStateController = viewModel.decimalSlotsStateController,
59 | decimalSlotsUiActionListener = viewModel.decimalSlotsUiActionListener
60 | )
61 | }
62 |
63 | SlotsViewType.Timeline -> {
64 | TimeSlotExample(
65 | allDayEvents = viewModel.allDayEvents,
66 | timeSlotsStateController = viewModel.timeSlotsStateController,
67 | timeSlotsUiActionListener = viewModel.timeSlotsUiActionListener
68 | )
69 | }
70 |
71 | SlotsViewType.Epg -> {
72 | EpgSlotExample(
73 | epgSlotsStateController = viewModel.epgSlotsStateController,
74 | epgSlotsUiActionListener = viewModel.epgSlotsUiActionListener
75 | )
76 | }
77 | }
78 | DayScheduleAppActionsBottomView(
79 | slotsViewType = viewModel.slotsViewType,
80 | uiActionListener = viewModel.uiActionListener
81 | )
82 | DayScheduleAppBottomSheet(
83 | bottomSheetEventsState = viewModel.bottomSheetEventsState.value,
84 | timeSlotsUiActionListener = viewModel.timeSlotsUiActionListener,
85 | decimalSlotsUiActionListener = viewModel.decimalSlotsUiActionListener,
86 | epgSlotsUiActionListener = viewModel.epgSlotsUiActionListener,
87 | uiActionListener = viewModel.uiActionListener,
88 | alertDialogUiActionListener = viewModel.alertDialogUiActionListener
89 | )
90 | DayScheduleAppAlertDialog(
91 | alertDialogEventsState = viewModel.alertDialogEventsState.value,
92 | alertDialogUiActionListener = viewModel.alertDialogUiActionListener
93 | )
94 | }
95 | }
96 | }
97 | }
98 |
99 | @OptIn(ExperimentalUuidApi::class)
100 | @Composable
101 | private fun TimeSlotExample(
102 | allDayEvents: List,
103 | timeSlotsStateController: TimeSlotsStateController,
104 | timeSlotsUiActionListener: TimeSlotsUiActionListener
105 | ) {
106 | Column(modifier = Modifier.fillMaxSize()) {
107 | Box(
108 | modifier = Modifier.height(56.dp).fillMaxWidth().background(Color.LightGray)
109 | ) {
110 | Text(
111 | modifier = Modifier.align(Alignment.Center),
112 | text = "All Day Events = ${allDayEvents.size}"
113 | )
114 | }
115 | TimeSlotsView(
116 | timeSlotsStateController = timeSlotsStateController
117 | ) { localTimeEvent ->
118 | Box(
119 | modifier = Modifier.fillMaxSize()
120 | .padding(all = 1.dp)
121 | .background(color = generateRandomColor(localTimeEvent.uuid))
122 | .combinedClickable(
123 | onClick = {
124 | timeSlotsUiActionListener.onTimeEventClicked(localTimeEvent)
125 | },
126 | onDoubleClick = {
127 | timeSlotsUiActionListener.onTimeEventDoubleClicked(localTimeEvent)
128 | },
129 | onLongClick = {
130 | timeSlotsUiActionListener.onTimeEventLongClicked(localTimeEvent)
131 | }
132 | )
133 | ) {
134 | Text(
135 | text = "${localTimeEvent.title}: ${localTimeEvent.startTime}-${localTimeEvent.endTime}",
136 | fontSize = 12.sp
137 | )
138 | }
139 | }
140 | }
141 | }
142 |
143 | @OptIn(ExperimentalUuidApi::class)
144 | @Composable
145 | private fun DecimalSlotExample(
146 | decimalSlotsStateController: DecimalSlotsStateController,
147 | decimalSlotsUiActionListener: DecimalSlotsUiActionListener
148 | ) {
149 | Box(modifier = Modifier.fillMaxSize()) {
150 | DecimalSlotsView(
151 | decimalSlotsStateController = decimalSlotsStateController
152 | ) { decimalEvent ->
153 | Box(
154 | modifier = Modifier.fillMaxSize()
155 | .padding(all = 1.dp)
156 | .background(color = generateRandomColor(decimalEvent.uuid))
157 | .combinedClickable(
158 | onClick = {
159 | decimalSlotsUiActionListener.onDecimalEventClicked(decimalEvent)
160 | },
161 | onDoubleClick = {
162 | decimalSlotsUiActionListener.onDecimalEventDoubleClicked(decimalEvent)
163 | },
164 | onLongClick = {
165 | decimalSlotsUiActionListener.onDecimalEventLongClicked(decimalEvent)
166 | }
167 | )
168 | ) {
169 | Text(
170 | text = "${decimalEvent.title}: ${decimalEvent.startValue}-${decimalEvent.endValue}",
171 | fontSize = 12.sp
172 | )
173 | }
174 | }
175 |
176 | }
177 | }
178 |
179 | @OptIn(ExperimentalUuidApi::class)
180 | @Composable
181 | private fun EpgSlotExample(
182 | epgSlotsStateController: EpgSlotsStateController,
183 | epgSlotsUiActionListener: EpgSlotsUiActionListener
184 | ) {
185 | EpgSlotsView(
186 | epgSlotsStateController = epgSlotsStateController
187 | ) { localTimeEvent ->
188 | Box(
189 | modifier = Modifier.fillMaxSize().padding(1.dp)
190 | .background(generateRandomColor(localTimeEvent.uuid))
191 | .combinedClickable(
192 | onClick = {
193 | epgSlotsUiActionListener.onEpgEventClicked(localTimeEvent)
194 | },
195 | onDoubleClick = {
196 | epgSlotsUiActionListener.onEpgEventDoubleClicked(localTimeEvent)
197 | },
198 | onLongClick = {
199 | epgSlotsUiActionListener.onEpgEventLongClicked(localTimeEvent)
200 | }
201 | )
202 | ) {
203 | Text(
204 | text = "${localTimeEvent.title}: ${localTimeEvent.startTime}-${localTimeEvent.endTime}",
205 | fontSize = 12.sp
206 | )
207 | }
208 | }
209 | }
210 |
211 | @OptIn(ExperimentalUuidApi::class)
212 | private val colorPerEventMap = mutableMapOf()
213 |
214 | @OptIn(ExperimentalUuidApi::class)
215 | private fun generateRandomColor(uuid: Uuid): Color {
216 | colorPerEventMap[uuid]?.let { return it }
217 | val red = Random.nextInt(256)
218 | val green = Random.nextInt(256)
219 | val blue = Random.nextInt(256)
220 | return Color(red, green, blue).also { colorPerEventMap[uuid] = it }
221 | }
222 |
223 | @Preview(
224 | showBackground = true
225 | )
226 | @OptIn(ExperimentalUuidApi::class)
227 | @Composable
228 | fun DecimalSlotsViewPreview() {
229 | val decimalSlotsStateController = remember {
230 | DecimalSlotsStateController(
231 | decimalSlotConfig = DecimalSlotConfig(slotScale = 2),
232 | eventsArrangement = EventsArrangement.MixedDirections(EventWidthType.FixedSizeFillLastEvent)
233 | ).apply {
234 | // Prepare the initial data
235 | DecimalSlotsDataSample(decimalSlotsStateController = this)
236 | }
237 | }
238 | MaterialTheme {
239 | Box(modifier = Modifier.fillMaxSize()) {
240 | DecimalSlotsView(decimalSlotsStateController = decimalSlotsStateController) { event ->
241 | Box(
242 | modifier = Modifier.fillMaxSize()
243 | .padding(all = 1.dp)
244 | .background(color = generateRandomColor(event.uuid))
245 | ) {
246 | Text(
247 | text = "${event.title}: ${event.startValue}-${event.endValue}",
248 | fontSize = 12.sp
249 | )
250 | }
251 | }
252 | }
253 | }
254 | }
255 |
256 | @OptIn(ExperimentalUuidApi::class)
257 | @Preview(
258 | showBackground = true
259 | )
260 | @Composable
261 | fun EpgSlotsViewPreview() {
262 | val epgSlotsStateController = remember {
263 | EpgSlotsStateController(
264 | EpgChannelSlotConfig(
265 | timeSlotConfig = TimeSlotConfig(
266 | startSlotTime = LocalTime(6, 0),
267 | endSlotTime = LocalTime(23, 59)
268 | )
269 | )
270 | ).apply {
271 | EpgSlotsDataSample(epgSlotsStateController = this)
272 | }
273 | }
274 | MaterialTheme {
275 | Box(modifier = Modifier.fillMaxSize()) {
276 | EpgSlotsView(epgSlotsStateController = epgSlotsStateController) { localEvent ->
277 | Box(
278 | modifier = Modifier.fillMaxSize()
279 | .padding(all = 1.dp)
280 | .background(color = generateRandomColor(localEvent.uuid))
281 | ) {
282 | Text(
283 | text = "${localEvent.title}: ${localEvent.startTime}-${localEvent.endTime}",
284 | fontSize = 12.sp
285 | )
286 | }
287 | }
288 | }
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/com/macaosoftware/ui/ui/DayScheduleAppViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.macaosoftware.ui.ui
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalEvent
7 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotConfig
8 | import com.macaosoftware.ui.dailyagenda.decimalslots.DecimalSlotsStateController
9 | import com.macaosoftware.ui.dailyagenda.decimalslots.EventWidthType
10 | import com.macaosoftware.ui.dailyagenda.decimalslots.EventsArrangement
11 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgChannel
12 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgChannelSlotConfig
13 | import com.macaosoftware.ui.dailyagenda.epgslots.EpgSlotsStateController
14 | import com.macaosoftware.ui.dailyagenda.timeslots.LocalTimeEvent
15 | import com.macaosoftware.ui.dailyagenda.timeslots.TimeSlotConfig
16 | import com.macaosoftware.ui.dailyagenda.timeslots.TimeSlotsStateController
17 | import com.macaosoftware.ui.data.Constants
18 | import com.macaosoftware.ui.data.DecimalSlotsDataSample
19 | import com.macaosoftware.ui.data.EpgSlotsDataSample
20 | import com.macaosoftware.ui.data.TimeSlotsDataSample
21 | import com.macaosoftware.ui.ui.model.AllDayEvent
22 | import kotlinx.datetime.LocalTime
23 | import kotlin.uuid.ExperimentalUuidApi
24 | import kotlin.uuid.Uuid
25 |
26 | class DayScheduleAppViewModel {
27 |
28 | var slotsViewType by mutableStateOf(value = SlotsViewType.Timeline)
29 |
30 | // region: AllDayEvent
31 |
32 | val allDayEvents = mutableListOf()
33 |
34 | @OptIn(ExperimentalUuidApi::class)
35 | fun addAllDayEvent(
36 | uuid: Uuid = Uuid.random(),
37 | title: String,
38 | description: String
39 | ): Boolean {
40 | return addAllDayEvent(
41 | AllDayEvent(
42 | uuid = uuid,
43 | title = title,
44 | description = description
45 | )
46 | )
47 | }
48 |
49 | fun addAllDayEvent(allDayEvent: AllDayEvent): Boolean {
50 | return allDayEvents.add(allDayEvent)
51 | }
52 |
53 | // endregion
54 |
55 | // region: timeSlotsStateController
56 |
57 | val timeSlotsStateController by lazy {
58 | TimeSlotsStateController(
59 | timeSlotConfig = TimeSlotConfig(
60 | startSlotTime = LocalTime(7, 0), // 7:00 AM
61 | endSlotTime = LocalTime(19, 0), // 7:00 PM
62 | useAmPm = true,
63 | slotScale = 2,
64 | slotHeight = 48
65 | ),
66 | eventsArrangement = EventsArrangement.MixedDirections(EventWidthType.FixedSizeFillLastEvent)
67 | ).apply {
68 | TimeSlotsDataSample(timeSlotsStateController = this)
69 | }
70 | }
71 |
72 | internal interface TimeSlotsUiActionListener {
73 | fun confirmedAddTimeEvent(
74 | title: String,
75 | description: String,
76 | startLocalTime: LocalTime,
77 | endLocalTime: LocalTime
78 | )
79 |
80 | fun showRemoveTimeEventForm(timeEvent: LocalTimeEvent)
81 | fun confirmedRemoveTimeEvent(eventTitle: String)
82 | fun onTimeEventClicked(timeEvent: LocalTimeEvent)
83 | fun onTimeEventDoubleClicked(timeEvent: LocalTimeEvent)
84 | fun onTimeEventLongClicked(timeEvent: LocalTimeEvent)
85 | }
86 |
87 | @OptIn(ExperimentalUuidApi::class)
88 | internal val timeSlotsUiActionListener = object : TimeSlotsUiActionListener {
89 |
90 | override fun confirmedAddTimeEvent(
91 | title: String,
92 | description: String,
93 | startLocalTime: LocalTime,
94 | endLocalTime: LocalTime
95 | ) {
96 | uiActionListener.dismissInputForm()
97 | timeSlotsStateController.timeSlotsDataUpdater.postUpdate {
98 | addEvent(
99 | title = title,
100 | description = description,
101 | startTime = startLocalTime,
102 | endTime = endLocalTime
103 | )
104 | }
105 | }
106 |
107 | override fun showRemoveTimeEventForm(timeEvent: LocalTimeEvent) {
108 | bottomSheetEventsState.value =
109 | BottomSheetEventsState.RemoveTimedEventRequested(localTimeEvent = timeEvent)
110 | }
111 |
112 | override fun confirmedRemoveTimeEvent(eventTitle: String) {
113 | uiActionListener.dismissInputForm()
114 | timeSlotsStateController.timeSlotsDataUpdater.postUpdate {
115 | removeEventByTitle(eventTitle = eventTitle)
116 | }
117 | }
118 |
119 | override fun onTimeEventClicked(timeEvent: LocalTimeEvent) {
120 | bottomSheetEventsState.value =
121 | BottomSheetEventsState.ShowTimedEventRequested(localTimeEvent = timeEvent)
122 | }
123 |
124 | override fun onTimeEventDoubleClicked(timeEvent: LocalTimeEvent) {
125 | showRemoveTimeEventForm(timeEvent)
126 | }
127 |
128 | override fun onTimeEventLongClicked(timeEvent: LocalTimeEvent) {
129 | showRemoveTimeEventForm(timeEvent)
130 | }
131 | }
132 |
133 | // endregion
134 |
135 | // region: DecimalSlotsStateController
136 |
137 | val decimalSlotsStateController by lazy {
138 | DecimalSlotsStateController(
139 | decimalSlotConfig = DecimalSlotConfig(
140 | initialSlotValue = 7.0F,
141 | lastSlotValue = 19.0F,
142 | slotScale = 2,
143 | slotHeight = 48
144 | ),
145 | eventsArrangement = EventsArrangement.MixedDirections(EventWidthType.FixedSizeFillLastEvent)
146 | ).apply {
147 | DecimalSlotsDataSample(decimalSlotsStateController = this)
148 | }
149 | }
150 |
151 | internal interface DecimalSlotsUiActionListener {
152 | fun confirmedAddDecimalSegment(title: String, startValue: Float, endValue: Float)
153 | fun showRemoveDecimalEventForm(decimalEvent: DecimalEvent)
154 | fun confirmedRemoveDecimalEvent(eventTitle: String)
155 | fun onDecimalEventClicked(decimalEvent: DecimalEvent)
156 | fun onDecimalEventDoubleClicked(decimalEvent: DecimalEvent)
157 | fun onDecimalEventLongClicked(decimalEvent: DecimalEvent)
158 | }
159 |
160 | @OptIn(ExperimentalUuidApi::class)
161 | internal val decimalSlotsUiActionListener = object : DecimalSlotsUiActionListener {
162 |
163 | override fun confirmedAddDecimalSegment(
164 | title: String,
165 | startValue: Float,
166 | endValue: Float
167 | ) {
168 | uiActionListener.dismissInputForm()
169 | decimalSlotsStateController.decimalSlotsDataUpdater.postUpdate {
170 | addDecimalEvent(
171 | title = title,
172 | description = Constants.EmptyDescription,
173 | startValue = startValue,
174 | endValue = endValue
175 | )
176 | }
177 | }
178 |
179 | override fun showRemoveDecimalEventForm(decimalEvent: DecimalEvent) {
180 | bottomSheetEventsState.value =
181 | BottomSheetEventsState.RemoveDecimalEventRequested(decimalEvent)
182 | }
183 |
184 | override fun confirmedRemoveDecimalEvent(eventTitle: String) {
185 | uiActionListener.dismissInputForm()
186 | decimalSlotsStateController.decimalSlotsDataUpdater.postUpdate {
187 | removeDecimalEventByTittle(eventTitle = eventTitle)
188 | }
189 | }
190 |
191 | override fun onDecimalEventClicked(decimalEvent: DecimalEvent) {
192 | bottomSheetEventsState.value =
193 | BottomSheetEventsState.ShowDecimalEventRequested(decimalEvent)
194 | }
195 |
196 | override fun onDecimalEventDoubleClicked(decimalEvent: DecimalEvent) {
197 | showRemoveDecimalEventForm(decimalEvent)
198 | }
199 |
200 | override fun onDecimalEventLongClicked(decimalEvent: DecimalEvent) {
201 | showRemoveDecimalEventForm(decimalEvent)
202 | }
203 | }
204 |
205 | // endregion
206 |
207 | // region EpgSlotsStateController
208 |
209 | @OptIn(ExperimentalUuidApi::class)
210 | val epgSlotsStateController by lazy {
211 | EpgSlotsStateController(
212 | EpgChannelSlotConfig(
213 | topHeaderHeight = 48,
214 | channelWidth = 96,
215 | timeSlotConfig = TimeSlotConfig(
216 | startSlotTime = LocalTime(6, 0),
217 | endSlotTime = LocalTime(23, 59)
218 | )
219 | )
220 | ).apply {
221 | EpgSlotsDataSample(epgSlotsStateController = this)
222 | }
223 | }
224 |
225 | internal interface EpgSlotsUiActionListener {
226 | fun confirmedAddEpgEvent(
227 | title: String,
228 | description: String,
229 | startLocalTime: LocalTime,
230 | endLocalTime: LocalTime
231 | )
232 |
233 | fun confirmedRemoveEpgEvent(eventTitle: String)
234 | fun onEpgEventClicked(localTimeEvent: LocalTimeEvent)
235 | fun onEpgEventDoubleClicked(localTimeEvent: LocalTimeEvent)
236 | fun onEpgEventLongClicked(localTimeEvent: LocalTimeEvent)
237 | }
238 |
239 | @OptIn(ExperimentalUuidApi::class)
240 | internal val epgSlotsUiActionListener = object : EpgSlotsUiActionListener {
241 |
242 | override fun confirmedAddEpgEvent(
243 | title: String,
244 | description: String,
245 | startLocalTime: LocalTime,
246 | endLocalTime: LocalTime
247 | ) {
248 | uiActionListener.dismissInputForm()
249 | epgSlotsStateController.epgSlotsDataUpdater.postUpdate {
250 | addChannel(
251 | EpgChannel(
252 | name = "New Channel",
253 | events = listOf(
254 | LocalTimeEvent(
255 | uuid = Uuid.random(),
256 | title = title,
257 | description = description,
258 | startTime = startLocalTime,
259 | endTime = endLocalTime
260 | )
261 | )
262 | )
263 | )
264 | }
265 | }
266 |
267 | override fun confirmedRemoveEpgEvent(eventTitle: String) {
268 | TODO("Not yet implemented")
269 | }
270 |
271 | override fun onEpgEventClicked(localTimeEvent: LocalTimeEvent) {
272 | bottomSheetEventsState.value =
273 | BottomSheetEventsState.ShowEpgEventRequested(epgEvent = localTimeEvent)
274 | }
275 |
276 | override fun onEpgEventDoubleClicked(localTimeEvent: LocalTimeEvent) {
277 | bottomSheetEventsState.value =
278 | BottomSheetEventsState.RemoveEpgEventRequested(epgEvent = localTimeEvent)
279 | }
280 |
281 | override fun onEpgEventLongClicked(localTimeEvent: LocalTimeEvent) {
282 | bottomSheetEventsState.value =
283 | BottomSheetEventsState.RemoveEpgEventRequested(epgEvent = localTimeEvent)
284 | }
285 | }
286 |
287 | // endregion
288 |
289 | // region: Global screen events
290 |
291 | var bottomSheetEventsState =
292 | mutableStateOf(BottomSheetEventsState.Hidden)
293 |
294 | internal interface UiActionListener {
295 | fun showAddEventForm(slotsViewType: SlotsViewType)
296 | fun toggleAxisType(slotsViewType: SlotsViewType)
297 | fun dismissInputForm()
298 | }
299 |
300 | @OptIn(ExperimentalUuidApi::class)
301 | internal val uiActionListener = object : UiActionListener {
302 |
303 | override fun showAddEventForm(slotsViewType: SlotsViewType) {
304 | when (slotsViewType) {
305 | SlotsViewType.Decimal -> {
306 | bottomSheetEventsState.value =
307 | BottomSheetEventsState.AddDecimalEventRequested
308 | }
309 |
310 | SlotsViewType.Timeline -> {
311 | bottomSheetEventsState.value =
312 | BottomSheetEventsState.AddTimedEventRequested
313 | }
314 |
315 | SlotsViewType.Epg -> {
316 | bottomSheetEventsState.value =
317 | BottomSheetEventsState.AddEpgEventRequested
318 | }
319 | }
320 | }
321 |
322 | override fun toggleAxisType(slotsViewType: SlotsViewType) {
323 | if (this@DayScheduleAppViewModel.slotsViewType == slotsViewType) return
324 | this@DayScheduleAppViewModel.slotsViewType = slotsViewType
325 | }
326 |
327 | override fun dismissInputForm() {
328 | bottomSheetEventsState.value = BottomSheetEventsState.Hidden
329 | }
330 | }
331 |
332 | var alertDialogEventsState =
333 | mutableStateOf(value = AlertDialogEventsState.Hidden)
334 |
335 | internal interface AlertDialogUiActionListener {
336 | fun hideAlert()
337 | fun showAlert(text: String)
338 | }
339 |
340 | internal val alertDialogUiActionListener = object : AlertDialogUiActionListener {
341 | override fun hideAlert() {
342 | alertDialogEventsState.value = AlertDialogEventsState.Hidden
343 | }
344 |
345 | override fun showAlert(text: String) {
346 | alertDialogEventsState.value = AlertDialogEventsState.ShowingInfo(text = text)
347 | }
348 | }
349 |
350 | // endregion
351 |
352 | }
353 |
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 77;
7 | objects = {
8 |
9 | /* Begin PBXFileReference section */
10 | 2492D66DC8CF0569CAF0A973 /* DailyAgendaView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DailyAgendaView.app; sourceTree = BUILT_PRODUCTS_DIR; };
11 | /* End PBXFileReference section */
12 |
13 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
14 | BCE932F6EF25107CD5961435 /* Exceptions for "iosApp" folder in "iosApp" target */ = {
15 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
16 | membershipExceptions = (
17 | Info.plist,
18 | );
19 | target = F982EA1EC9ABF5958AC64DE4 /* iosApp */;
20 | };
21 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
22 |
23 | /* Begin PBXFileSystemSynchronizedRootGroup section */
24 | 1BC351C78DD103874EF9FF96 /* iosApp */ = {
25 | isa = PBXFileSystemSynchronizedRootGroup;
26 | exceptions = (
27 | BCE932F6EF25107CD5961435 /* Exceptions for "iosApp" folder in "iosApp" target */,
28 | );
29 | path = iosApp;
30 | sourceTree = "";
31 | };
32 | 801CDE462448F91F21DD7C45 /* Configuration */ = {
33 | isa = PBXFileSystemSynchronizedRootGroup;
34 | path = Configuration;
35 | sourceTree = "";
36 | };
37 | /* End PBXFileSystemSynchronizedRootGroup section */
38 |
39 | /* Begin PBXFrameworksBuildPhase section */
40 | 3E194DBDBBF44A91885822E2 /* Frameworks */ = {
41 | isa = PBXFrameworksBuildPhase;
42 | buildActionMask = 2147483647;
43 | files = (
44 | );
45 | runOnlyForDeploymentPostprocessing = 0;
46 | };
47 | /* End PBXFrameworksBuildPhase section */
48 |
49 | /* Begin PBXGroup section */
50 | 5FF200269E908102C3E747B8 /* Products */ = {
51 | isa = PBXGroup;
52 | children = (
53 | 2492D66DC8CF0569CAF0A973 /* DailyAgendaView.app */,
54 | );
55 | name = Products;
56 | sourceTree = "";
57 | };
58 | CF6DF17C606835B04744A5C5 = {
59 | isa = PBXGroup;
60 | children = (
61 | 801CDE462448F91F21DD7C45 /* Configuration */,
62 | 1BC351C78DD103874EF9FF96 /* iosApp */,
63 | 5FF200269E908102C3E747B8 /* Products */,
64 | );
65 | sourceTree = "";
66 | };
67 | /* End PBXGroup section */
68 |
69 | /* Begin PBXNativeTarget section */
70 | F982EA1EC9ABF5958AC64DE4 /* iosApp */ = {
71 | isa = PBXNativeTarget;
72 | buildConfigurationList = BD96A41A8776DBF81ECBC33B /* Build configuration list for PBXNativeTarget "iosApp" */;
73 | buildPhases = (
74 | EA8008CEF9B051BD19FB04B3 /* Compile Kotlin Framework */,
75 | CACF98B5141808CD9BBB0109 /* Sources */,
76 | 3E194DBDBBF44A91885822E2 /* Frameworks */,
77 | AB8044EEC2408D64EACBF532 /* Resources */,
78 | );
79 | buildRules = (
80 | );
81 | dependencies = (
82 | );
83 | fileSystemSynchronizedGroups = (
84 | 1BC351C78DD103874EF9FF96 /* iosApp */,
85 | );
86 | name = iosApp;
87 | packageProductDependencies = (
88 | );
89 | productName = iosApp;
90 | productReference = 2492D66DC8CF0569CAF0A973 /* DailyAgendaView.app */;
91 | productType = "com.apple.product-type.application";
92 | };
93 | /* End PBXNativeTarget section */
94 |
95 | /* Begin PBXProject section */
96 | D5F201B6F85CDD904C09510F /* Project object */ = {
97 | isa = PBXProject;
98 | attributes = {
99 | BuildIndependentTargetsInParallel = 1;
100 | LastSwiftUpdateCheck = 1620;
101 | LastUpgradeCheck = 1620;
102 | TargetAttributes = {
103 | F982EA1EC9ABF5958AC64DE4 = {
104 | CreatedOnToolsVersion = 16.2;
105 | };
106 | };
107 | };
108 | buildConfigurationList = D68920B7399172027C7782D8 /* Build configuration list for PBXProject "iosApp" */;
109 | developmentRegion = en;
110 | hasScannedForEncodings = 0;
111 | knownRegions = (
112 | en,
113 | Base,
114 | );
115 | mainGroup = CF6DF17C606835B04744A5C5;
116 | minimizedProjectReferenceProxies = 1;
117 | preferredProjectObjectVersion = 77;
118 | productRefGroup = 5FF200269E908102C3E747B8 /* Products */;
119 | projectDirPath = "";
120 | projectRoot = "";
121 | targets = (
122 | F982EA1EC9ABF5958AC64DE4 /* iosApp */,
123 | );
124 | };
125 | /* End PBXProject section */
126 |
127 | /* Begin PBXResourcesBuildPhase section */
128 | AB8044EEC2408D64EACBF532 /* Resources */ = {
129 | isa = PBXResourcesBuildPhase;
130 | buildActionMask = 2147483647;
131 | files = (
132 | );
133 | runOnlyForDeploymentPostprocessing = 0;
134 | };
135 | /* End PBXResourcesBuildPhase section */
136 |
137 | /* Begin PBXShellScriptBuildPhase section */
138 | EA8008CEF9B051BD19FB04B3 /* Compile Kotlin Framework */ = {
139 | isa = PBXShellScriptBuildPhase;
140 | alwaysOutOfDate = 1;
141 | buildActionMask = 2147483647;
142 | files = (
143 | );
144 | inputFileListPaths = (
145 | );
146 | inputPaths = (
147 | );
148 | name = "Compile Kotlin Framework";
149 | outputFileListPaths = (
150 | );
151 | outputPaths = (
152 | );
153 | runOnlyForDeploymentPostprocessing = 0;
154 | shellPath = /bin/sh;
155 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n";
156 | };
157 | /* End PBXShellScriptBuildPhase section */
158 |
159 | /* Begin PBXSourcesBuildPhase section */
160 | CACF98B5141808CD9BBB0109 /* Sources */ = {
161 | isa = PBXSourcesBuildPhase;
162 | buildActionMask = 2147483647;
163 | files = (
164 | );
165 | runOnlyForDeploymentPostprocessing = 0;
166 | };
167 | /* End PBXSourcesBuildPhase section */
168 |
169 | /* Begin XCBuildConfiguration section */
170 | 3BD7E2027F84D99E3F09838A /* Release */ = {
171 | isa = XCBuildConfiguration;
172 | buildSettings = {
173 | ARCHS = arm64;
174 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
175 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
176 | CODE_SIGN_IDENTITY = "Apple Development";
177 | CODE_SIGN_STYLE = Automatic;
178 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
179 | DEVELOPMENT_TEAM = "${TEAM_ID}";
180 | ENABLE_PREVIEWS = YES;
181 | GENERATE_INFOPLIST_FILE = YES;
182 | INFOPLIST_FILE = iosApp/Info.plist;
183 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
184 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
185 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
186 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
187 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
188 | LD_RUNPATH_SEARCH_PATHS = (
189 | "$(inherited)",
190 | "@executable_path/Frameworks",
191 | );
192 | SWIFT_EMIT_LOC_STRINGS = YES;
193 | SWIFT_VERSION = 5.0;
194 | TARGETED_DEVICE_FAMILY = "1,2";
195 | };
196 | name = Release;
197 | };
198 | 74592164CA65220A1D63E77A /* Debug */ = {
199 | isa = XCBuildConfiguration;
200 | baseConfigurationReferenceAnchor = 801CDE462448F91F21DD7C45 /* Configuration */;
201 | baseConfigurationReferenceRelativePath = Config.xcconfig;
202 | buildSettings = {
203 | ALWAYS_SEARCH_USER_PATHS = NO;
204 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
205 | CLANG_ANALYZER_NONNULL = YES;
206 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
208 | CLANG_ENABLE_MODULES = YES;
209 | CLANG_ENABLE_OBJC_ARC = YES;
210 | CLANG_ENABLE_OBJC_WEAK = YES;
211 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
212 | CLANG_WARN_BOOL_CONVERSION = YES;
213 | CLANG_WARN_COMMA = YES;
214 | CLANG_WARN_CONSTANT_CONVERSION = YES;
215 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
218 | CLANG_WARN_EMPTY_BODY = YES;
219 | CLANG_WARN_ENUM_CONVERSION = YES;
220 | CLANG_WARN_INFINITE_RECURSION = YES;
221 | CLANG_WARN_INT_CONVERSION = YES;
222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
223 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
224 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
225 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
226 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
227 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
228 | CLANG_WARN_STRICT_PROTOTYPES = YES;
229 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
230 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
231 | CLANG_WARN_UNREACHABLE_CODE = YES;
232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
233 | COPY_PHASE_STRIP = NO;
234 | DEBUG_INFORMATION_FORMAT = dwarf;
235 | ENABLE_STRICT_OBJC_MSGSEND = YES;
236 | ENABLE_TESTABILITY = YES;
237 | ENABLE_USER_SCRIPT_SANDBOXING = NO;
238 | GCC_C_LANGUAGE_STANDARD = gnu17;
239 | GCC_DYNAMIC_NO_PIC = NO;
240 | GCC_NO_COMMON_BLOCKS = YES;
241 | GCC_OPTIMIZATION_LEVEL = 0;
242 | GCC_PREPROCESSOR_DEFINITIONS = (
243 | "DEBUG=1",
244 | "$(inherited)",
245 | );
246 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
247 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
248 | GCC_WARN_UNDECLARED_SELECTOR = YES;
249 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
250 | GCC_WARN_UNUSED_FUNCTION = YES;
251 | GCC_WARN_UNUSED_VARIABLE = YES;
252 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
253 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
254 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
255 | MTL_FAST_MATH = YES;
256 | ONLY_ACTIVE_ARCH = YES;
257 | SDKROOT = iphoneos;
258 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
259 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
260 | };
261 | name = Debug;
262 | };
263 | A7E5ECB33D2A9216FE4B5806 /* Release */ = {
264 | isa = XCBuildConfiguration;
265 | baseConfigurationReferenceAnchor = 801CDE462448F91F21DD7C45 /* Configuration */;
266 | baseConfigurationReferenceRelativePath = Config.xcconfig;
267 | buildSettings = {
268 | ALWAYS_SEARCH_USER_PATHS = NO;
269 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
270 | CLANG_ANALYZER_NONNULL = YES;
271 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
272 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
273 | CLANG_ENABLE_MODULES = YES;
274 | CLANG_ENABLE_OBJC_ARC = YES;
275 | CLANG_ENABLE_OBJC_WEAK = YES;
276 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
277 | CLANG_WARN_BOOL_CONVERSION = YES;
278 | CLANG_WARN_COMMA = YES;
279 | CLANG_WARN_CONSTANT_CONVERSION = YES;
280 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
281 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
282 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
283 | CLANG_WARN_EMPTY_BODY = YES;
284 | CLANG_WARN_ENUM_CONVERSION = YES;
285 | CLANG_WARN_INFINITE_RECURSION = YES;
286 | CLANG_WARN_INT_CONVERSION = YES;
287 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
288 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
289 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
290 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
291 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
292 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
293 | CLANG_WARN_STRICT_PROTOTYPES = YES;
294 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
295 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
296 | CLANG_WARN_UNREACHABLE_CODE = YES;
297 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
298 | COPY_PHASE_STRIP = NO;
299 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
300 | ENABLE_NS_ASSERTIONS = NO;
301 | ENABLE_STRICT_OBJC_MSGSEND = YES;
302 | ENABLE_USER_SCRIPT_SANDBOXING = NO;
303 | GCC_C_LANGUAGE_STANDARD = gnu17;
304 | GCC_NO_COMMON_BLOCKS = YES;
305 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
306 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
307 | GCC_WARN_UNDECLARED_SELECTOR = YES;
308 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
309 | GCC_WARN_UNUSED_FUNCTION = YES;
310 | GCC_WARN_UNUSED_VARIABLE = YES;
311 | IPHONEOS_DEPLOYMENT_TARGET = 18.2;
312 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
313 | MTL_ENABLE_DEBUG_INFO = NO;
314 | MTL_FAST_MATH = YES;
315 | SDKROOT = iphoneos;
316 | SWIFT_COMPILATION_MODE = wholemodule;
317 | VALIDATE_PRODUCT = YES;
318 | };
319 | name = Release;
320 | };
321 | C6358A1BC15C2EEDB11E256D /* Debug */ = {
322 | isa = XCBuildConfiguration;
323 | buildSettings = {
324 | ARCHS = arm64;
325 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
326 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
327 | CODE_SIGN_IDENTITY = "Apple Development";
328 | CODE_SIGN_STYLE = Automatic;
329 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
330 | DEVELOPMENT_TEAM = JHRZ3PC8R8;
331 | ENABLE_PREVIEWS = YES;
332 | GENERATE_INFOPLIST_FILE = YES;
333 | INFOPLIST_FILE = iosApp/Info.plist;
334 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
335 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
336 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
337 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
338 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
339 | LD_RUNPATH_SEARCH_PATHS = (
340 | "$(inherited)",
341 | "@executable_path/Frameworks",
342 | );
343 | SWIFT_EMIT_LOC_STRINGS = YES;
344 | SWIFT_VERSION = 5.0;
345 | TARGETED_DEVICE_FAMILY = "1,2";
346 | };
347 | name = Debug;
348 | };
349 | /* End XCBuildConfiguration section */
350 |
351 | /* Begin XCConfigurationList section */
352 | BD96A41A8776DBF81ECBC33B /* Build configuration list for PBXNativeTarget "iosApp" */ = {
353 | isa = XCConfigurationList;
354 | buildConfigurations = (
355 | C6358A1BC15C2EEDB11E256D /* Debug */,
356 | 3BD7E2027F84D99E3F09838A /* Release */,
357 | );
358 | defaultConfigurationIsVisible = 0;
359 | defaultConfigurationName = Release;
360 | };
361 | D68920B7399172027C7782D8 /* Build configuration list for PBXProject "iosApp" */ = {
362 | isa = XCConfigurationList;
363 | buildConfigurations = (
364 | 74592164CA65220A1D63E77A /* Debug */,
365 | A7E5ECB33D2A9216FE4B5806 /* Release */,
366 | );
367 | defaultConfigurationIsVisible = 0;
368 | defaultConfigurationName = Release;
369 | };
370 | /* End XCConfigurationList section */
371 | };
372 | rootObject = D5F201B6F85CDD904C09510F /* Project object */;
373 | }
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Daily Agenda View
2 | Daily Agenda View is a clone of the calendar component seeing in the **Microsoft Outlook App** or **Microsoft Teams App**. The component layouts daily events across an hourly based timeline. It is great for task planner apps, appointment apps, event schedule apps, calendar apps.
3 |
4 | Live demo using kotlin-wasm: [Daily Agenda View](https://pablichjenkov.github.io/daily-agenda-view)
5 |
6 | ## Support and Compatibility
7 |
8 |
9 |
10 | Platform Support
11 | Kotlin Compatibility
12 |
13 |
14 |
15 |
16 | | Platform | Supported |
17 | |----------|:---------:|
18 | | Android | ✅ |
19 | | iOS | ✅ |
20 | | Desktop | ✅ |
21 | | JS | ✅ |
22 | | Wasm | ✅ |
23 |
24 |
25 |
26 |
27 | | Agenda Version | Kotlin Version | CMP Version |
28 | |---------------|--------|-------|
29 | | 1.5.1 | 2.2.21 | 1.9.3 |
30 | | 1.5.0 | 2.2.21 | 1.9.3 |
31 | | 1.4.0 | 2.2.21 | 1.9.3 |
32 |
33 |
34 |
35 |
36 |
37 | ## How to use it
38 |
39 | Add the gradle coordinates:
40 |
41 | ```kotlin
42 | sourceSets {
43 | commonMain.dependencies {
44 | implementation("io.github.pablichjenkov:daily-agenda-view:")
45 | }
46 | }
47 | ```
48 |
49 | In Android only projects which already use **java-time** or **joda-time**, it is necessary to include the **kotlinx-datetime** dependecy too. So gradle will look like bellow.
50 |
51 | ```kotlin
52 | dependencies {
53 | implementation("io.github.pablichjenkov:daily-agenda-view:")
54 | implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
55 | }
56 | ```
57 |
58 |
59 |
60 | The library API is based on the compose **StateController pattern**. A StateController is basically **a mini MVI store just for a specific UI component**.
61 | Instead of being coupled to a full screen single gigantic state. The state controller is only bound to a specific composable ui section of the screen.
62 | These are the options from the library:
63 | - [TimeSlotsStateController](#TimeSlotsStateController)
64 | - [DecimalSlotsStateController](#DecimalSlotsStateController)
65 | - [EpgSlotsStateController](#EpgSlotsStateController)
66 |
67 | ### TimeSlotsStateController
68 | The **TimeSlotsStateController** displays events in a hour and minute based timeline.
69 |
70 | ```kotlin
71 | val timeSlotsStateController = remember {
72 | TimeSlotsStateController(
73 | timeSlotConfig = TimeSlotConfig(slotScale = 2, slotHeight = 48),
74 | eventsArrangement =
75 | EventsArrangement.MixedDirections(EventWidthType.FixedSizeFillLastEvent)
76 | ).apply {
77 | timeSlotsDataUpdater.postUpdate {
78 | addEvent(
79 | uuid = Uuid.random(),
80 | startTime = LocalTime(hour = 8, minute = 0),
81 | endTime = LocalTime(hour = 8, minute = 30),
82 | title = "Event 0",
83 | description = "Description 0"
84 | )
85 | addEventList( // When adding a list all events must belong to the same slot.
86 | startTime = LocalTime(hour = 8, minute = 0),
87 | events =
88 | listOf(
89 | LocalTimeEvent(
90 | uuid = Uuid.random(),
91 | startTime = LocalTime(hour = 8, minute = 0),
92 | endTime = LocalTime(hour = 8, minute = 45),
93 | title = "Event 1",
94 | description = "Description 1"
95 | ),
96 | LocalTimeEvent(
97 | uuid = Uuid.random(),
98 | startTime = LocalTime(hour = 8, minute = 0),
99 | endTime = LocalTime(hour = 9, minute = 0),
100 | title = "Event 2",
101 | description = "Description 2"
102 | )
103 | )
104 | )
105 | }
106 | }
107 | }
108 |
109 | ```
110 |
111 |
112 |
113 | Now that you create a **TimeSlotsStateController** and added some events to it. Then add a **TimeSlotsView**
114 | in your Composable screen.
115 |
116 | ```kotlin
117 | @Composable
118 | fun MyDayScheduleView(modifier = Modifier.fillMaxSize()) {
119 |
120 | val timeSlotsStateController = remember { ... }
121 |
122 | TimeSlotsView(timeSlotsStateController = timeSlotsStateController) { localTimeEvent ->
123 | Box(modifier = Modifier.fillMaxSize().padding(all = 2.dp).background(color = Color.Gray)) {
124 | Text(
125 | text =
126 | "${localTimeEvent.title}: ${localTimeEvent.startTime}-${localTimeEvent.endTime}",
127 | fontSize = 12.sp
128 | )
129 | }
130 | }
131 | }
132 | ```
133 |
134 |
135 |
136 | ## Events Arrangement Options
137 |
138 | **1.** In this mode the agenda view will try to maximize the events witdh. It achieves that by mixing the rows layout direction. **Even rows** are rendered from left to right while **odd rows** are rendered from right to left. Since the events are order by duration, this mode leverage the maximum space available by laying out in the opposite direction from the previous road. It should be very effective in most data use cases.
139 |
140 | ```kotlin
141 | eventsArrangement = EventsArrangement.MixedDirections(eventWidthType = EventWidthType.VariableSize)
142 | ```
143 |
144 |
145 |
146 | ---
147 |
148 | **2.** Similar to above, this mode also mixes the direction of the layout, even rows do LTR and odd rows fo RTL. But in this mode all the events have the same with. This is for the case where maximum space wants to be coverred but at the same time esthetic is needed.
149 |
150 | ```kotlin
151 | eventsArrangement = EventsArrangement.MixedDirections(eventWidthType = EventWidthType.FixedSize)
152 | ```
153 |
154 |
155 |
156 | ---
157 |
158 | **3.** This mode is just like number 2 but expand the single slot events to occupy the full row available width. This is the default configuration if you don't specify any.
159 |
160 | ```kotlin
161 | eventsArrangement = EventsArrangement.MixedDirections(eventWidthType = EventWidthType.FixedSizeFillLastEvent)
162 | ```
163 |
164 |
165 |
166 |
167 | Events align with slot start/end time
168 |
169 |
170 | Events do not align with slot start/end time
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | ---
183 |
184 | **4.** Instead of maximizing space consumption, an App might want consistency laying out the daily calendar events. Bellow mode renders from left to right always and also expand the single slot events to occupy the full row available width.
185 |
186 | ```kotlin
187 | eventsArrangement = EventsArrangement.LeftToRight(lastEventFillRow = true)
188 | ```
189 |
190 |
191 |
192 | ---
193 |
194 | **5.** Similar to number 4 but in this case we want all the events to have the same width.
195 |
196 | ```kotlin
197 | eventsArrangement = EventsArrangement.LeftToRight(lastEventFillRow = false)
198 | ```
199 |
200 |
201 |
202 | ---
203 |
204 | **6.** The same as number 4 but from Right to left. Could be useful in countries where languages are written/read from right to left.
205 |
206 | ```kotlin
207 | eventsArrangement = EventsArrangement.RightToLeft(lastEventFillRow = true)
208 | ```
209 |
210 |
211 |
212 | ---
213 |
214 | **7.** The same as number 5 but from Right to left.
215 |
216 | ```kotlin
217 | eventsArrangement = EventsArrangement.RightToLeft(lastEventFillRow = false)
218 | ```
219 |
220 |
221 |
222 | ### DecimalSlotsStateController
223 | The **DecimalSlotsStateController** displays events in a vertical **decimal axis**. Although use cases for this type of data presentation are rare, the
224 | library includes it anyway in case someone needs it. This is actually the base StateController in which the TimeSlotsStateController builds upon.
225 |
226 | ```kotlin
227 | DecimalSlotsStateController(
228 | decimalSlotConfig =
229 | DecimalSlotConfig(
230 | initialSlotValue = 7.0F,
231 | lastSlotValue = 19.0F,
232 | slotScale = 2,
233 | slotHeight = 48
234 | ),
235 | eventsArrangement = EventsArrangement.MixedDirections(EventWidthType.FixedSizeFillLastEvent)
236 | )
237 | .apply {
238 | decimalSlotsDataUpdater.postUpdate {
239 | addDecimalEvent(
240 | DecimalEvent(
241 | uuid = Uuid.random(),
242 | title = "Ev0",
243 | description = Constants.EmptyDescription,
244 | startValue = 8.5F,
245 | endValue = 10.0F
246 | )
247 | )
248 | addDecimalEventList(
249 | startValue = 8.0F,
250 | segments = listOf(
251 | DecimalEvent(
252 | uuid = Uuid.random(),
253 | title = "Ev1",
254 | description = Constants.EmptyDescription,
255 | startValue = 8.0F,
256 | endValue = 10.0F
257 | ),
258 | DecimalEvent(
259 | uuid = Uuid.random(),
260 | title = "Ev2",
261 | description = Constants.EmptyDescription,
262 | startValue = 8.0F,
263 | endValue = 9.5F
264 | )
265 | )
266 | )
267 | }
268 | }
269 |
270 | ```
271 | Then use the **DecimalSlotsView** Composable like bellow:
272 |
273 | ```kotlin
274 | // Assuming the StateController is hosted in a ViewModel
275 | val decimalSlotsStateController = viewModel.decimalSlotsStateController
276 |
277 | DecimalSlotsView(decimalSlotsStateController = decimalSlotsStateController) { decimalEvent ->
278 | Text(text = "${decimalEvent.title}: ${decimalEvent.startValue}-${decimalEvent.endValue}", fontSize = 12.sp)
279 | }
280 | ```
281 | Above code should produce something like the image bellow. Notice the axis values are just decimal numbers, also this component doesn't have a current time line indicator.
282 |
283 |
284 |
285 | ### EpgSlotsStateController
286 | The library also includes an **EpgSlotsStateController**, EPG(Electronic Guide Program) is a very popular component in TV apps.
287 | Although other uses cases can leverage this type of component too.
288 |
289 | ```kotlin
290 | val epgSlotsStateController = remember {
291 | EpgSlotsStateController(
292 | EpgChannelSlotConfig(
293 | timeSlotConfig =
294 | TimeSlotConfig(startSlotTime = LocalTime(6, 0), endSlotTime = LocalTime(23, 59))
295 | )
296 | )
297 | .apply {
298 | epgSlotsDataUpdater.postUpdate {
299 | addChannel(
300 | EpgChannel(
301 | name = "Ch1",
302 | events =
303 | listOf(
304 | LocalTimeEvent(
305 | uuid = Uuid.random(),
306 | title = "Ev1",
307 | description = Constants.EmptyDescription,
308 | startTime = LocalTime(9, 0),
309 | endTime = LocalTime(10, 0)
310 | ),
311 | LocalTimeEvent(
312 | uuid = Uuid.random(),
313 | title = "Ev2",
314 | description = Constants.EmptyDescription,
315 | startTime = LocalTime(10, 0),
316 | endTime = LocalTime(11, 30)
317 | )
318 | )
319 | )
320 | )
321 | addChannel(
322 | EpgChannel(
323 | name = "Ch2",
324 | events =
325 | listOf(
326 | LocalTimeEvent(
327 | uuid = Uuid.random(),
328 | title = "Ev3",
329 | description = Constants.EmptyDescription,
330 | startTime = LocalTime(9, 30),
331 | endTime = LocalTime(10, 15)
332 | ),
333 | LocalTimeEvent(
334 | uuid = Uuid.random(),
335 | title = "Ev4",
336 | description = Constants.EmptyDescription,
337 | startTime = LocalTime(10, 30),
338 | endTime = LocalTime(11, 0)
339 | )
340 | )
341 | )
342 | )
343 | }
344 | }
345 | }
346 |
347 | ```
348 | Then use the **EpgSlotsView** to render the EpgSlotsStateController instance.
349 |
350 | ```kotlin
351 | @Composable
352 | fun MyTvScheduleView(modifier = Modifier.fillMaxSize()) {
353 |
354 | val epgSlotsStateController = remember { ... }
355 |
356 | EpgSlotsView(epgSlotsStateController = epgSlotsStateController) { localTimeEvent ->
357 | Text(
358 | text = "${localTimeEvent.title}: ${localTimeEvent.startTime}-${localTimeEvent.endTime}",
359 | fontSize = 12.sp
360 | )
361 | }
362 | }
363 | ```
364 |
365 |
366 |
367 |
368 |
369 | ## Contributions
370 |
371 | We welcome contributions from the community! If you have ideas for new features, bug fixes, or improvements, please open an issue or submit a pull request.
372 |
--------------------------------------------------------------------------------