├── .github
└── workflows
│ ├── publish.yml
│ └── static-analysis.yml
├── .gitignore
├── .idea
├── .gitignore
├── AndroidProjectSystem.xml
├── artifacts
│ ├── demo_jvm.xml
│ ├── desktopdemo_jvm.xml
│ └── programguide_jvm.xml
├── compiler.xml
├── gradle.xml
├── kotlinc.xml
├── migrations.xml
└── misc.xml
├── LICENSE.md
├── README.md
├── androiddemo
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── kotlin
│ └── eu
│ │ └── wewox
│ │ └── programguide
│ │ └── demo
│ │ └── MainActivity.kt
│ └── res
│ ├── drawable
│ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values-night
│ └── colors.xml
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
├── build-logic
├── .gitignore
├── convention
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ ├── JvmToolchainConventionPlugin.kt
│ │ └── utils
│ │ └── ProjectExtensions.kt
└── settings.gradle.kts
├── build.gradle.kts
├── config
└── detekt
│ └── detekt.yml
├── demo
├── .gitignore
├── build.gradle.kts
├── demo.podspec
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── eu
│ │ └── wewox
│ │ └── programguide
│ │ └── demo
│ │ ├── App.kt
│ │ ├── Example.kt
│ │ ├── RootScreen.kt
│ │ ├── data
│ │ └── Program.kt
│ │ ├── screens
│ │ ├── ProgramGuideConfigurationScreen.kt
│ │ ├── ProgramGuideSimpleScreen.kt
│ │ ├── ProgramGuideSizeScreen.kt
│ │ └── ProgramGuideStateScreen.kt
│ │ └── ui
│ │ ├── components
│ │ ├── ProgramGuideItems.kt
│ │ └── TopBar.kt
│ │ └── theme
│ │ ├── Color.kt
│ │ ├── Spacing.kt
│ │ └── Theme.kt
│ └── iosMain
│ └── kotlin
│ └── main.ios.kt
├── desktopdemo
├── .gitignore
├── build.gradle.kts
└── src
│ └── jvmMain
│ └── kotlin
│ └── Main.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── iosdemo
├── Configuration
│ └── Config.xcconfig
├── Podfile
├── iosApp.xcodeproj
│ └── project.pbxproj
├── iosApp.xcworkspace
│ └── contents.xcworkspacedata
└── iosApp
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ └── app-icon-512.png
│ └── Contents.json
│ ├── ContentView.swift
│ ├── Info.plist
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── iOSApp.swift
├── programguide
├── .gitignore
├── build.gradle.kts
└── src
│ ├── androidMain
│ └── AndroidManifest.xml
│ └── commonMain
│ └── kotlin
│ └── eu
│ └── wewox
│ └── programguide
│ ├── ProgramGuide.kt
│ ├── ProgramGuideDimensions.kt
│ ├── ProgramGuideIndexMapper.kt
│ ├── ProgramGuideItem.kt
│ ├── ProgramGuidePositionProvider.kt
│ ├── ProgramGuideScope.kt
│ └── ProgramGuideState.kt
├── settings.gradle.kts
└── wasmdemo
├── .gitignore
├── build.gradle.kts
└── src
└── wasmJsMain
├── kotlin
└── eu
│ └── wewox
│ └── programguide
│ └── demo
│ └── Main.kt
└── resources
└── index.html
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Publish To Maven Central
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow when tag is pushed
8 | push:
9 | tags:
10 | - 'v*'
11 |
12 | # Allows you to run this workflow manually from the Actions tab
13 | workflow_dispatch:
14 |
15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
16 | jobs:
17 | # This workflow contains a single job called "publish"
18 | publish:
19 | # The type of runner that the job will run on
20 | runs-on: macos-latest
21 |
22 | # Steps represent a sequence of tasks that will be executed as part of the job
23 | steps:
24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
25 | - uses: actions/checkout@v3
26 | - name: Set up JDK 17
27 | uses: actions/setup-java@v3
28 | with:
29 | java-version: '17'
30 | distribution: 'temurin'
31 |
32 | # Runs a single command using the runners shell
33 | - name: publish
34 | run: ./gradlew publish --no-daemon --no-parallel
35 | env:
36 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
37 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
38 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
39 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }}
40 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
41 |
--------------------------------------------------------------------------------
/.github/workflows/static-analysis.yml:
--------------------------------------------------------------------------------
1 | # This is a workflow to verify PRs with static code analysis tools
2 | name: Static Analysis
3 |
4 | # Controls when the workflow will run
5 | on:
6 | pull_request:
7 | branches: [main, develop]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | jobs:
13 | detekt:
14 | name: Detekt
15 | runs-on: macos-latest
16 |
17 | # Steps represent a sequence of tasks that will be executed as part of the job
18 | steps:
19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
20 | - uses: actions/checkout@v3
21 | - name: Set up JDK 17
22 | uses: actions/setup-java@v3
23 | with:
24 | java-version: '17'
25 | distribution: 'temurin'
26 |
27 | # Runs a single command using the runners shell
28 | - name: detekt
29 | run: ./gradlew detekt
30 |
31 | spotless:
32 | name: Spotless
33 | runs-on: macos-latest
34 |
35 | # Steps represent a sequence of tasks that will be executed as part of the job
36 | steps:
37 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
38 | - uses: actions/checkout@v3
39 | - name: Set up JDK 17
40 | uses: actions/setup-java@v3
41 | with:
42 | java-version: '17'
43 | distribution: 'temurin'
44 |
45 | # Runs a single command using the runners shell
46 | - name: spotless
47 | run: ./gradlew spotlessCheck
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/.name
5 | /.idea/artifacts
6 | /.idea/caches
7 | /.idea/libraries
8 | /.idea/modules.xml
9 | /.idea/workspace.xml
10 | /.idea/navEditor.xml
11 | /.idea/assetWizardSettings.xml
12 | /.idea/deploymentTargetDropDown.xml
13 | /.idea/other.xml
14 | /.idea/compiler.xml
15 | /.idea/kotlinc.xml
16 | /.idea/vcs.xml
17 | /.idea/misc.xml
18 | /.idea/inspectionProfiles
19 | /.idea/shelf
20 | /.idea/deploymentTargetSelector.xml
21 | /.idea/runConfigurations.xml
22 | /.idea/studiobot.xml
23 | /.kotlin/metadata/*
24 | /kotlin-js-store/
25 | .DS_Store
26 | /build
27 | /captures
28 | .externalNativeBuild
29 | .cxx
30 | local.properties
31 | iosdemo/Podfile.lock
32 | iosdemo/Pods/*
33 | iosdemo/iosdemo.xcworkspace/*
34 | iosdemo/iosdemo.xcodeproj/*
35 | !iosdemo/iosdemo.xcodeproj/project.pbxproj
36 | demo/shared.podspec
37 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/AndroidProjectSystem.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/artifacts/demo_jvm.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/demo/build/libs
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/artifacts/desktopdemo_jvm.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/desktopdemo/build/libs
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/artifacts/programguide_jvm.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | $PROJECT_DIR$/programguide/build/libs
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2023 Oleksandr Balan
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://central.sonatype.com/artifact/io.github.oleksandrbalan/programguide)
2 |
3 |
4 |
5 | # Program Guide
6 |
7 | Program Guide, aka EPG, library for Compose UI.
8 |
9 | Lazy layout to display program guide data on the two directional plane. It is build on the [MinaBox](https://github.com/oleksandrbalan/minabox) (which is build on `LazyLayout`) and provides methods to register item(s) and handles scrolling on the plane.
10 |
11 | ## Multiplatform
12 |
13 | Library supports Android, iOS, Desktop (Windows, MacOS, Linux) and Wasm targets.
14 |
15 | https://www.jetbrains.com/compose-multiplatform/
16 |
17 | ## Usage
18 |
19 | ### Get a dependency
20 |
21 | **Step 1.** Add the MavenCentral repository to your build file.
22 | Add it in your root `build.gradle.kts` at the end of repositories:
23 | ```kotlin
24 | allprojects {
25 | repositories {
26 | ...
27 | mavenCentral()
28 | }
29 | }
30 | ```
31 |
32 | Or in `settings.gradle.kts`:
33 | ```kotlin
34 | dependencyResolutionManagement {
35 | repositories {
36 | ...
37 | mavenCentral()
38 | }
39 | }
40 | ```
41 |
42 | **Step 2.** Add the dependency.
43 | Check latest version on the [releases page](https://github.com/oleksandrbalan/programguide/releases).
44 | ```kotlin
45 | dependencies {
46 | implementation("io.github.oleksandrbalan:programguide:$version")
47 | }
48 | ```
49 |
50 | ### Use in Composable
51 |
52 | The core element of the `ProgramGuide` layout is a `content` lambda, where program, channel and timeline items are registered in the similar manner as in `LazyColumn` or `LazyRow`.
53 |
54 | There are multiple types of items to register:
55 | * Programs - program item cells. Each program must define channel index, and where it starts and ends.
56 | * Channels - channel item cells, displayed to the left of the program guide. Each channel must define its index.
57 | * Timeline - timeline cells, displayed on the top. Each timeline item must define where it starts and ends, so it is not locked to per-hour granularity.
58 | * Current time - vertical line of the current time.
59 | * Top corner - place for content in the top left corner, above channels and timeline.
60 |
61 | **Note:** To be independent on date-time libraries, hours are defined as float numbers. For example: 9.5f represents 09:30 and 16.25f represents 16:15.
62 |
63 | The size of the items are defined via `dimensions` parameter.
64 |
65 | It is also possible to observe on the scroll state and change it programmatically using an instance of the `ProgramGuideState`.
66 |
67 | ```kotlin
68 | ProgramGuide {
69 | programs(
70 | // Count of programs
71 | count = ...,
72 | // Necessary layout info of the single program cell
73 | layoutInfo = { ProgramGuideItem.Program(...) }
74 | ) {
75 | // Composable for single program cell
76 | }
77 |
78 | channels(
79 | // Count of channels
80 | count = ...,
81 | // Necessary layout info of the single channel cell
82 | layoutInfo = { ProgramGuideItem.Channel(...) }
83 | ) {
84 | // Composable for single channel cell
85 | }
86 |
87 | timeline(
88 | // Count of timeline blocks
89 | count = ...,
90 | // Necessary layout info of the single timeline cell
91 | layoutInfo = { ProgramGuideItem.Timeline(...) }
92 | ) {
93 | // Composable for single timeline cell
94 | }
95 | }
96 | ```
97 |
98 | See Demo application and [examples](demo/src/commonMain/kotlin/eu/wewox/programguide/demo/screens) for more usage examples.
99 |
100 | ## Examples
101 |
102 | Simple EPG data.
103 |
104 | https://github.com/oleksandrbalan/programguide/assets/20944869/20fefbce-80cc-4e43-87d9-e359edf64c35
105 |
106 | Fully configurable layout.
107 |
108 | https://github.com/oleksandrbalan/programguide/assets/20944869/9ce2ecc6-7b80-470b-9254-77dcd5e670c1
109 |
110 | If you need further customization options, check [MinaBox](https://github.com/oleksandrbalan/minabox) library.
111 |
--------------------------------------------------------------------------------
/androiddemo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/androiddemo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin)
4 | alias(libs.plugins.compose.compiler)
5 | id("convention.jvm.toolchain")
6 | }
7 |
8 | android {
9 | namespace = "eu.wewox.programguide"
10 |
11 | compileSdk = libs.versions.sdk.compile.get().toInt()
12 |
13 | defaultConfig {
14 | applicationId = "eu.wewox.programguide"
15 |
16 | minSdk = libs.versions.sdk.min.get().toInt()
17 | targetSdk = libs.versions.sdk.target.get().toInt()
18 |
19 | versionCode = 1
20 | versionName = "1.0"
21 |
22 | vectorDrawables {
23 | useSupportLibrary = true
24 | }
25 | }
26 |
27 | buildTypes {
28 | release {
29 | isMinifyEnabled = false
30 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
31 | }
32 | }
33 | buildFeatures {
34 | compose = true
35 | }
36 | packaging {
37 | resources {
38 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
39 | }
40 | }
41 | }
42 |
43 | dependencies {
44 | implementation(project(":programguide"))
45 | implementation(project(":demo"))
46 |
47 | implementation(platform(libs.compose.bom))
48 | implementation(libs.compose.material3)
49 | implementation(libs.compose.ui)
50 | implementation(libs.compose.uitooling)
51 | implementation(libs.compose.uitoolingpreview)
52 | implementation(libs.androidx.activitycompose)
53 | implementation(libs.androidx.splashscreen)
54 | }
55 |
--------------------------------------------------------------------------------
/androiddemo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/androiddemo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/androiddemo/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/androiddemo/src/main/kotlin/eu/wewox/programguide/demo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.BackHandler
6 | import androidx.activity.compose.setContent
7 | import androidx.activity.enableEdgeToEdge
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.saveable.rememberSaveable
11 | import androidx.compose.runtime.setValue
12 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
13 |
14 | /**
15 | * Main activity for demo application.
16 | * Contains simple "Crossfade" based navigation to various examples.
17 | */
18 | class MainActivity : ComponentActivity() {
19 |
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | installSplashScreen()
22 | enableEdgeToEdge()
23 | super.onCreate(savedInstanceState)
24 |
25 | setContent {
26 | var example by rememberSaveable { mutableStateOf(null) }
27 | BackHandler(enabled = example != null) {
28 | example = null
29 | }
30 | App(
31 | example = example,
32 | onChangeExample = { example = it },
33 | )
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/androiddemo/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
12 |
13 |
18 |
23 |
28 |
29 |
30 |
33 |
36 |
39 |
42 |
45 |
48 |
51 |
54 |
57 |
60 |
63 |
64 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/androiddemo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androiddemo/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/black
4 |
5 |
--------------------------------------------------------------------------------
/androiddemo/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 | @android:color/white
5 |
6 |
--------------------------------------------------------------------------------
/androiddemo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ProgramGuide
3 |
--------------------------------------------------------------------------------
/androiddemo/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
15 |
16 |
--------------------------------------------------------------------------------
/build-logic/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/build-logic/convention/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/build-logic/convention/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | java {
6 | toolchain {
7 | val version = libs.versions.java.toolchain.get()
8 | languageVersion.set(JavaLanguageVersion.of(version))
9 | }
10 | }
11 |
12 | gradlePlugin {
13 | plugins {
14 | register("androidApplicationCompose") {
15 | id = "convention.jvm.toolchain"
16 | implementationClass = "JvmToolchainConventionPlugin"
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/JvmToolchainConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.api.plugins.JavaPluginExtension
4 | import org.gradle.jvm.toolchain.JavaLanguageVersion
5 | import org.gradle.kotlin.dsl.configure
6 | import utils.libs
7 |
8 | class JvmToolchainConventionPlugin : Plugin {
9 | override fun apply(target: Project) =
10 | with(target) {
11 | extensions.configure {
12 | toolchain {
13 | val version = libs.findVersion("java-toolchain").get().displayName
14 | languageVersion.set(JavaLanguageVersion.of(version))
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/utils/ProjectExtensions.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.VersionCatalog
5 | import org.gradle.api.artifacts.VersionCatalogsExtension
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | val Project.libs: VersionCatalog
9 | get() = extensions.getByType().named("libs")
10 |
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | versionCatalogs {
7 | create("libs") {
8 | from(files("../gradle/libs.versions.toml"))
9 | }
10 | }
11 | }
12 |
13 | plugins {
14 | id("org.gradle.toolchains.foojay-resolver-convention") version("0.7.0")
15 | }
16 |
17 |
18 | rootProject.name = "build-logic"
19 | include(":convention")
20 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.diffplug.gradle.spotless.SpotlessPlugin
2 | import io.gitlab.arturbosch.detekt.DetektPlugin
3 |
4 | plugins {
5 | alias(libs.plugins.android.application) apply false
6 | alias(libs.plugins.android.library) apply false
7 | alias(libs.plugins.kotlin) apply false
8 | alias(libs.plugins.kotlin.jvm) apply false
9 | alias(libs.plugins.kotlin.multiplatform) apply false
10 | alias(libs.plugins.jetbrains.cocoapods) apply false
11 | alias(libs.plugins.jetbrains.compose) apply false
12 | alias(libs.plugins.compose.compiler) apply false
13 | alias(libs.plugins.detekt)
14 | alias(libs.plugins.spotless)
15 | alias(libs.plugins.mavenpublish)
16 | }
17 |
18 | configure(subprojects) {
19 | apply()
20 | apply()
21 |
22 | spotless {
23 | kotlin {
24 | target("**/*.kt")
25 | ktlint("0.43.2")
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/config/detekt/detekt.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 0
3 | excludeCorrectable: false
4 | weights:
5 | # complexity: 2
6 | # LongParameterList: 1
7 | # style: 1
8 | # comments: 1
9 |
10 | config:
11 | validation: true
12 | warningsAsErrors: false
13 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
14 | excludes: ''
15 |
16 | processors:
17 | active: true
18 | exclude:
19 | - 'DetektProgressListener'
20 | # - 'KtFileCountProcessor'
21 | # - 'PackageCountProcessor'
22 | # - 'ClassCountProcessor'
23 | # - 'FunctionCountProcessor'
24 | # - 'PropertyCountProcessor'
25 | # - 'ProjectComplexityProcessor'
26 | # - 'ProjectCognitiveComplexityProcessor'
27 | # - 'ProjectLLOCProcessor'
28 | # - 'ProjectCLOCProcessor'
29 | # - 'ProjectLOCProcessor'
30 | # - 'ProjectSLOCProcessor'
31 | # - 'LicenseHeaderLoaderExtension'
32 |
33 | console-reports:
34 | active: true
35 | exclude:
36 | - 'ProjectStatisticsReport'
37 | - 'ComplexityReport'
38 | - 'NotificationReport'
39 | - 'FindingsReport'
40 | - 'FileBasedFindingsReport'
41 | # - 'LiteFindingsReport'
42 |
43 | output-reports:
44 | active: true
45 | exclude:
46 | # - 'TxtOutputReport'
47 | # - 'XmlOutputReport'
48 | # - 'HtmlOutputReport'
49 |
50 | comments:
51 | active: true
52 | AbsentOrWrongFileLicense:
53 | active: false
54 | licenseTemplateFile: 'license.template'
55 | licenseTemplateIsRegex: false
56 | CommentOverPrivateFunction:
57 | active: false
58 | CommentOverPrivateProperty:
59 | active: false
60 | DeprecatedBlockTag:
61 | active: false
62 | EndOfSentenceFormat:
63 | active: false
64 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
65 | OutdatedDocumentation:
66 | active: false
67 | matchTypeParameters: true
68 | matchDeclarationsOrder: true
69 | allowParamOnConstructorProperties: false
70 | UndocumentedPublicClass:
71 | active: true
72 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
73 | searchInNestedClass: true
74 | searchInInnerClass: true
75 | searchInInnerObject: true
76 | searchInInnerInterface: true
77 | UndocumentedPublicFunction:
78 | active: true
79 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
80 | UndocumentedPublicProperty:
81 | active: true
82 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
83 |
84 | complexity:
85 | active: true
86 | ComplexCondition:
87 | active: false
88 | threshold: 4
89 | ComplexInterface:
90 | active: false
91 | threshold: 10
92 | includeStaticDeclarations: false
93 | includePrivateDeclarations: false
94 | ComplexMethod:
95 | active: true
96 | threshold: 15
97 | ignoreSingleWhenExpression: false
98 | ignoreSimpleWhenEntries: false
99 | ignoreNestingFunctions: false
100 | nestingFunctions:
101 | - 'also'
102 | - 'apply'
103 | - 'forEach'
104 | - 'isNotNull'
105 | - 'ifNull'
106 | - 'let'
107 | - 'run'
108 | - 'use'
109 | - 'with'
110 | LabeledExpression:
111 | active: false
112 | ignoredLabels: []
113 | LargeClass:
114 | active: true
115 | threshold: 600
116 | LongMethod:
117 | active: true
118 | threshold: 60
119 | LongParameterList:
120 | active: true
121 | functionThreshold: 10
122 | constructorThreshold: 10
123 | ignoreDefaultParameters: false
124 | ignoreDataClasses: true
125 | ignoreAnnotated: [ 'Composable' ]
126 | ignoreAnnotatedParameter: []
127 | MethodOverloading:
128 | active: false
129 | threshold: 6
130 | NamedArguments:
131 | active: false
132 | threshold: 3
133 | ignoreArgumentsMatchingNames: false
134 | NestedBlockDepth:
135 | active: true
136 | threshold: 4
137 | ReplaceSafeCallChainWithRun:
138 | active: false
139 | StringLiteralDuplication:
140 | active: false
141 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
142 | threshold: 3
143 | ignoreAnnotation: true
144 | excludeStringsWithLessThan5Characters: true
145 | ignoreStringsRegex: '$^'
146 | TooManyFunctions:
147 | active: true
148 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
149 | thresholdInFiles: 11
150 | thresholdInClasses: 11
151 | thresholdInInterfaces: 11
152 | thresholdInObjects: 11
153 | thresholdInEnums: 11
154 | ignoreDeprecated: false
155 | ignorePrivate: false
156 | ignoreOverridden: false
157 |
158 | coroutines:
159 | active: true
160 | GlobalCoroutineUsage:
161 | active: false
162 | InjectDispatcher:
163 | active: false
164 | dispatcherNames:
165 | - 'IO'
166 | - 'Default'
167 | - 'Unconfined'
168 | RedundantSuspendModifier:
169 | active: false
170 | SleepInsteadOfDelay:
171 | active: false
172 | SuspendFunWithCoroutineScopeReceiver:
173 | active: false
174 | SuspendFunWithFlowReturnType:
175 | active: false
176 |
177 | empty-blocks:
178 | active: true
179 | EmptyCatchBlock:
180 | active: true
181 | allowedExceptionNameRegex: '_|(ignore|expected).*'
182 | EmptyClassBlock:
183 | active: true
184 | EmptyDefaultConstructor:
185 | active: true
186 | EmptyDoWhileBlock:
187 | active: true
188 | EmptyElseBlock:
189 | active: true
190 | EmptyFinallyBlock:
191 | active: true
192 | EmptyForBlock:
193 | active: true
194 | EmptyFunctionBlock:
195 | active: true
196 | ignoreOverridden: false
197 | EmptyIfBlock:
198 | active: true
199 | EmptyInitBlock:
200 | active: true
201 | EmptyKtFile:
202 | active: true
203 | EmptySecondaryConstructor:
204 | active: true
205 | EmptyTryBlock:
206 | active: true
207 | EmptyWhenBlock:
208 | active: true
209 | EmptyWhileBlock:
210 | active: true
211 |
212 | exceptions:
213 | active: true
214 | ExceptionRaisedInUnexpectedLocation:
215 | active: true
216 | methodNames:
217 | - 'equals'
218 | - 'finalize'
219 | - 'hashCode'
220 | - 'toString'
221 | InstanceOfCheckForException:
222 | active: false
223 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
224 | NotImplementedDeclaration:
225 | active: false
226 | ObjectExtendsThrowable:
227 | active: false
228 | PrintStackTrace:
229 | active: true
230 | RethrowCaughtException:
231 | active: true
232 | ReturnFromFinally:
233 | active: true
234 | ignoreLabeled: false
235 | SwallowedException:
236 | active: true
237 | ignoredExceptionTypes:
238 | - 'InterruptedException'
239 | - 'MalformedURLException'
240 | - 'NumberFormatException'
241 | - 'ParseException'
242 | allowedExceptionNameRegex: '_|(ignore|expected).*'
243 | ThrowingExceptionFromFinally:
244 | active: true
245 | ThrowingExceptionInMain:
246 | active: false
247 | ThrowingExceptionsWithoutMessageOrCause:
248 | active: true
249 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
250 | exceptions:
251 | - 'ArrayIndexOutOfBoundsException'
252 | - 'Exception'
253 | - 'IllegalArgumentException'
254 | - 'IllegalMonitorStateException'
255 | - 'IllegalStateException'
256 | - 'IndexOutOfBoundsException'
257 | - 'NullPointerException'
258 | - 'RuntimeException'
259 | - 'Throwable'
260 | ThrowingNewInstanceOfSameException:
261 | active: true
262 | TooGenericExceptionCaught:
263 | active: true
264 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
265 | exceptionNames:
266 | - 'ArrayIndexOutOfBoundsException'
267 | - 'Error'
268 | - 'Exception'
269 | - 'IllegalMonitorStateException'
270 | - 'IndexOutOfBoundsException'
271 | - 'NullPointerException'
272 | - 'RuntimeException'
273 | - 'Throwable'
274 | allowedExceptionNameRegex: '_|(ignore|expected).*'
275 | TooGenericExceptionThrown:
276 | active: true
277 | exceptionNames:
278 | - 'Error'
279 | - 'Exception'
280 | - 'RuntimeException'
281 | - 'Throwable'
282 |
283 | naming:
284 | active: true
285 | BooleanPropertyNaming:
286 | active: false
287 | allowedPattern: '^(is|has|are)'
288 | ignoreOverridden: true
289 | ClassNaming:
290 | active: true
291 | classPattern: '[A-Z][a-zA-Z0-9]*'
292 | ConstructorParameterNaming:
293 | active: true
294 | parameterPattern: '[a-z][A-Za-z0-9]*'
295 | privateParameterPattern: '[a-z][A-Za-z0-9]*'
296 | excludeClassPattern: '$^'
297 | ignoreOverridden: true
298 | EnumNaming:
299 | active: true
300 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
301 | ForbiddenClassName:
302 | active: false
303 | forbiddenName: []
304 | FunctionMaxLength:
305 | active: false
306 | maximumFunctionNameLength: 30
307 | FunctionMinLength:
308 | active: false
309 | minimumFunctionNameLength: 3
310 | FunctionNaming:
311 | active: true
312 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
313 | functionPattern: '[a-z][a-zA-Z0-9]*'
314 | excludeClassPattern: '$^'
315 | ignoreOverridden: true
316 | ignoreAnnotated: [ 'Composable' ]
317 | FunctionParameterNaming:
318 | active: true
319 | parameterPattern: '[a-z][A-Za-z0-9]*'
320 | excludeClassPattern: '$^'
321 | ignoreOverridden: true
322 | InvalidPackageDeclaration:
323 | active: false
324 | rootPackage: ''
325 | requireRootInDeclaration: false
326 | LambdaParameterNaming:
327 | active: false
328 | parameterPattern: '[a-z][A-Za-z0-9]*|_'
329 | MatchingDeclarationName:
330 | active: true
331 | mustBeFirst: true
332 | MemberNameEqualsClassName:
333 | active: true
334 | ignoreOverridden: true
335 | NoNameShadowing:
336 | active: false
337 | NonBooleanPropertyPrefixedWithIs:
338 | active: false
339 | ObjectPropertyNaming:
340 | active: true
341 | constantPattern: '[A-Za-z][_A-Za-z0-9]*'
342 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
343 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
344 | PackageNaming:
345 | active: true
346 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
347 | TopLevelPropertyNaming:
348 | active: true
349 | constantPattern: '[a-z][_A-Za-z0-9]*|[A-Z][_A-Z0-9]*'
350 | VariableMaxLength:
351 | active: false
352 | maximumVariableNameLength: 64
353 | VariableMinLength:
354 | active: false
355 | minimumVariableNameLength: 1
356 | VariableNaming:
357 | active: true
358 | variablePattern: '[a-z][A-Za-z0-9]*'
359 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
360 | excludeClassPattern: '$^'
361 | ignoreOverridden: true
362 |
363 | performance:
364 | active: true
365 | ArrayPrimitive:
366 | active: true
367 | ForEachOnRange:
368 | active: true
369 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
370 | SpreadOperator:
371 | active: false
372 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
373 | UnnecessaryTemporaryInstantiation:
374 | active: true
375 |
376 | potential-bugs:
377 | active: true
378 | AvoidReferentialEquality:
379 | active: false
380 | forbiddenTypePatterns:
381 | - 'kotlin.String'
382 | CastToNullableType:
383 | active: false
384 | Deprecation:
385 | active: false
386 | DontDowncastCollectionTypes:
387 | active: false
388 | DoubleMutabilityForCollection:
389 | active: false
390 | mutableTypes:
391 | - 'kotlin.collections.MutableList'
392 | - 'kotlin.collections.MutableMap'
393 | - 'kotlin.collections.MutableSet'
394 | - 'java.util.ArrayList'
395 | - 'java.util.LinkedHashSet'
396 | - 'java.util.HashSet'
397 | - 'java.util.LinkedHashMap'
398 | - 'java.util.HashMap'
399 | DuplicateCaseInWhenExpression:
400 | active: true
401 | ElseCaseInsteadOfExhaustiveWhen:
402 | active: false
403 | EqualsAlwaysReturnsTrueOrFalse:
404 | active: true
405 | EqualsWithHashCodeExist:
406 | active: true
407 | ExitOutsideMain:
408 | active: false
409 | ExplicitGarbageCollectionCall:
410 | active: true
411 | HasPlatformType:
412 | active: false
413 | IgnoredReturnValue:
414 | active: false
415 | restrictToAnnotatedMethods: true
416 | returnValueAnnotations:
417 | - '*.CheckResult'
418 | - '*.CheckReturnValue'
419 | ignoreReturnValueAnnotations:
420 | - '*.CanIgnoreReturnValue'
421 | ignoreFunctionCall: []
422 | ImplicitDefaultLocale:
423 | active: true
424 | ImplicitUnitReturnType:
425 | active: false
426 | allowExplicitReturnType: true
427 | InvalidRange:
428 | active: true
429 | IteratorHasNextCallsNextMethod:
430 | active: true
431 | IteratorNotThrowingNoSuchElementException:
432 | active: true
433 | LateinitUsage:
434 | active: false
435 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
436 | ignoreOnClassesPattern: ''
437 | MapGetWithNotNullAssertionOperator:
438 | active: false
439 | MissingPackageDeclaration:
440 | active: false
441 | excludes: ['**/*.kts']
442 | MissingWhenCase:
443 | active: true
444 | allowElseExpression: true
445 | NullCheckOnMutableProperty:
446 | active: false
447 | NullableToStringCall:
448 | active: false
449 | RedundantElseInWhen:
450 | active: true
451 | UnconditionalJumpStatementInLoop:
452 | active: false
453 | UnnecessaryNotNullOperator:
454 | active: true
455 | UnnecessarySafeCall:
456 | active: true
457 | UnreachableCatchBlock:
458 | active: false
459 | UnreachableCode:
460 | active: true
461 | UnsafeCallOnNullableType:
462 | active: true
463 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
464 | UnsafeCast:
465 | active: true
466 | UnusedUnaryOperator:
467 | active: false
468 | UselessPostfixExpression:
469 | active: false
470 | WrongEqualsTypeParameter:
471 | active: true
472 |
473 | style:
474 | active: true
475 | CanBeNonNullable:
476 | active: false
477 | ClassOrdering:
478 | active: false
479 | CollapsibleIfStatements:
480 | active: false
481 | DataClassContainsFunctions:
482 | active: false
483 | conversionFunctionPrefix: 'to'
484 | DataClassShouldBeImmutable:
485 | active: false
486 | DestructuringDeclarationWithTooManyEntries:
487 | active: false
488 | maxDestructuringEntries: 3
489 | EqualsNullCall:
490 | active: true
491 | EqualsOnSignatureLine:
492 | active: false
493 | ExplicitCollectionElementAccessMethod:
494 | active: false
495 | ExplicitItLambdaParameter:
496 | active: false
497 | ExpressionBodySyntax:
498 | active: false
499 | includeLineWrapping: false
500 | ForbiddenComment:
501 | active: false
502 | values:
503 | - 'FIXME:'
504 | - 'STOPSHIP:'
505 | - 'TODO:'
506 | allowedPatterns: ''
507 | customMessage: ''
508 | ForbiddenImport:
509 | active: false
510 | imports: []
511 | forbiddenPatterns: ''
512 | ForbiddenMethodCall:
513 | active: false
514 | methods:
515 | - 'kotlin.io.print'
516 | - 'kotlin.io.println'
517 | ForbiddenPublicDataClass:
518 | active: true
519 | excludes: ['**']
520 | ignorePackages:
521 | - '*.internal'
522 | - '*.internal.*'
523 | ForbiddenVoid:
524 | active: false
525 | ignoreOverridden: false
526 | ignoreUsageInGenerics: false
527 | FunctionOnlyReturningConstant:
528 | active: true
529 | ignoreOverridableFunction: true
530 | ignoreActualFunction: true
531 | excludedFunctions: ''
532 | LibraryCodeMustSpecifyReturnType:
533 | active: true
534 | excludes: ['**']
535 | LibraryEntitiesShouldNotBePublic:
536 | active: true
537 | excludes: ['**']
538 | LoopWithTooManyJumpStatements:
539 | active: true
540 | maxJumpCount: 1
541 | MagicNumber:
542 | active: false
543 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
544 | ignoreNumbers:
545 | - '-1'
546 | - '0'
547 | - '1'
548 | - '2'
549 | ignoreHashCodeFunction: true
550 | ignorePropertyDeclaration: false
551 | ignoreLocalVariableDeclaration: false
552 | ignoreConstantDeclaration: true
553 | ignoreCompanionObjectPropertyDeclaration: true
554 | ignoreAnnotation: false
555 | ignoreNamedArgument: true
556 | ignoreEnums: false
557 | ignoreRanges: false
558 | ignoreExtensionFunctions: true
559 | MandatoryBracesIfStatements:
560 | active: false
561 | MandatoryBracesLoops:
562 | active: false
563 | MaxLineLength:
564 | active: true
565 | maxLineLength: 120
566 | excludePackageStatements: true
567 | excludeImportStatements: true
568 | excludeCommentStatements: false
569 | MayBeConst:
570 | active: true
571 | ModifierOrder:
572 | active: true
573 | MultilineLambdaItParameter:
574 | active: false
575 | NestedClassesVisibility:
576 | active: true
577 | NewLineAtEndOfFile:
578 | active: true
579 | NoTabs:
580 | active: false
581 | ObjectLiteralToLambda:
582 | active: false
583 | OptionalAbstractKeyword:
584 | active: true
585 | OptionalUnit:
586 | active: false
587 | OptionalWhenBraces:
588 | active: false
589 | PreferToOverPairSyntax:
590 | active: false
591 | ProtectedMemberInFinalClass:
592 | active: true
593 | RedundantExplicitType:
594 | active: false
595 | RedundantHigherOrderMapUsage:
596 | active: false
597 | RedundantVisibilityModifierRule:
598 | active: false
599 | ReturnCount:
600 | active: true
601 | max: 2
602 | excludedFunctions: 'equals'
603 | excludeLabeled: false
604 | excludeReturnFromLambda: true
605 | excludeGuardClauses: false
606 | SafeCast:
607 | active: true
608 | SerialVersionUIDInSerializableClass:
609 | active: true
610 | SpacingBetweenPackageAndImports:
611 | active: false
612 | ThrowsCount:
613 | active: true
614 | max: 2
615 | excludeGuardClauses: false
616 | TrailingWhitespace:
617 | active: false
618 | UnderscoresInNumericLiterals:
619 | active: false
620 | acceptableLength: 4
621 | allowNonStandardGrouping: false
622 | UnnecessaryAbstractClass:
623 | active: true
624 | UnnecessaryAnnotationUseSiteTarget:
625 | active: false
626 | UnnecessaryApply:
627 | active: true
628 | UnnecessaryFilter:
629 | active: false
630 | UnnecessaryInheritance:
631 | active: true
632 | UnnecessaryInnerClass:
633 | active: false
634 | UnnecessaryLet:
635 | active: false
636 | UnnecessaryParentheses:
637 | active: false
638 | UntilInsteadOfRangeTo:
639 | active: false
640 | UnusedImports:
641 | active: false
642 | UnusedPrivateClass:
643 | active: true
644 | UnusedPrivateMember:
645 | active: true
646 | allowedNames: '(_|ignored|expected|serialVersionUID)'
647 | UseAnyOrNoneInsteadOfFind:
648 | active: false
649 | UseArrayLiteralsInAnnotations:
650 | active: false
651 | UseCheckNotNull:
652 | active: false
653 | UseCheckOrError:
654 | active: false
655 | UseDataClass:
656 | active: false
657 | allowVars: false
658 | UseEmptyCounterpart:
659 | active: false
660 | UseIfEmptyOrIfBlank:
661 | active: false
662 | UseIfInsteadOfWhen:
663 | active: false
664 | UseIsNullOrEmpty:
665 | active: false
666 | UseOrEmpty:
667 | active: false
668 | UseRequire:
669 | active: false
670 | UseRequireNotNull:
671 | active: false
672 | UselessCallOnNotNull:
673 | active: true
674 | UtilityClassWithPublicConstructor:
675 | active: true
676 | VarCouldBeVal:
677 | active: true
678 | WildcardImport:
679 | active: true
680 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
681 | excludeImports:
682 | - 'java.util.*'
683 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/demo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin.multiplatform)
5 | alias(libs.plugins.jetbrains.cocoapods)
6 | alias(libs.plugins.jetbrains.compose)
7 | alias(libs.plugins.android.library)
8 | alias(libs.plugins.compose.compiler)
9 | id("convention.jvm.toolchain")
10 | }
11 |
12 | kotlin {
13 | androidTarget()
14 |
15 | jvm()
16 |
17 | iosX64()
18 | iosArm64()
19 | iosSimulatorArm64()
20 |
21 | @OptIn(ExperimentalWasmDsl::class)
22 | wasmJs {
23 | browser()
24 | binaries.library()
25 | }
26 |
27 | applyDefaultHierarchyTemplate()
28 |
29 | cocoapods {
30 | version = "1.0.0"
31 | summary = "Demo Compose Multiplatform module"
32 | homepage = "---"
33 | ios.deploymentTarget = "14.1"
34 | podfile = project.file("../iosdemo/Podfile")
35 | framework {
36 | baseName = "demo"
37 | isStatic = true
38 | }
39 | }
40 |
41 | sourceSets {
42 | val commonMain by getting {
43 | dependencies {
44 | implementation(project(":programguide"))
45 |
46 | implementation(compose.material3)
47 | }
48 | }
49 |
50 | all {
51 | languageSettings.optIn("androidx.compose.ui.text.ExperimentalTextApi")
52 | languageSettings.optIn("androidx.compose.material3.ExperimentalMaterial3Api")
53 | }
54 | }
55 | }
56 |
57 | android {
58 | namespace = "eu.wewox.programguide.demo"
59 |
60 | compileSdk = libs.versions.sdk.compile.get().toInt()
61 | }
62 |
--------------------------------------------------------------------------------
/demo/demo.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'demo'
3 | spec.version = '1.0.0'
4 | spec.homepage = '---'
5 | spec.source = { :http=> ''}
6 | spec.authors = ''
7 | spec.license = ''
8 | spec.summary = 'Demo Compose Multiplatform module'
9 | spec.vendored_frameworks = 'build/cocoapods/framework/demo.framework'
10 | spec.libraries = 'c++'
11 | spec.ios.deployment_target = '14.1'
12 |
13 |
14 | if !Dir.exist?('build/cocoapods/framework/demo.framework') || Dir.empty?('build/cocoapods/framework/demo.framework')
15 | raise "
16 |
17 | Kotlin framework 'demo' doesn't exist yet, so a proper Xcode project can't be generated.
18 | 'pod install' should be executed after running ':generateDummyFramework' Gradle task:
19 |
20 | ./gradlew :demo:generateDummyFramework
21 |
22 | Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
23 | end
24 |
25 | spec.xcconfig = {
26 | 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
27 | }
28 |
29 | spec.pod_target_xcconfig = {
30 | 'KOTLIN_PROJECT_PATH' => ':demo',
31 | 'PRODUCT_MODULE_NAME' => 'demo',
32 | }
33 |
34 | spec.script_phases = [
35 | {
36 | :name => 'Build demo',
37 | :execution_position => :before_compile,
38 | :shell_path => '/bin/sh',
39 | :script => <<-SCRIPT
40 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
41 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
42 | exit 0
43 | fi
44 | set -ev
45 | REPO_ROOT="$PODS_TARGET_SRCROOT"
46 | "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
47 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
48 | -Pkotlin.native.cocoapods.archs="$ARCHS" \
49 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
50 | SCRIPT
51 | }
52 | ]
53 | spec.resources = ['build/compose/cocoapods/compose-resources']
54 | end
55 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/App.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo
2 |
3 | import androidx.compose.animation.Crossfade
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.saveable.rememberSaveable
8 | import androidx.compose.runtime.setValue
9 | import eu.wewox.programguide.demo.screens.ProgramGuideConfigurationScreen
10 | import eu.wewox.programguide.demo.screens.ProgramGuideSimpleScreen
11 | import eu.wewox.programguide.demo.screens.ProgramGuideSizeScreen
12 | import eu.wewox.programguide.demo.screens.ProgramGuideStateScreen
13 | import eu.wewox.programguide.demo.ui.theme.ProgramGuideDemoTheme
14 |
15 | @Composable
16 | fun App() {
17 | var example by rememberSaveable { mutableStateOf(null) }
18 | App(
19 | example = example,
20 | onChangeExample = { example = it },
21 | )
22 | }
23 |
24 | @Composable
25 | fun App(
26 | example: Example?,
27 | onChangeExample: (Example?) -> Unit,
28 | ) {
29 | ProgramGuideDemoTheme {
30 | val reset = { onChangeExample(null) }
31 |
32 | Crossfade(targetState = example) { selected ->
33 | when (selected) {
34 | null -> RootScreen(onExampleClick = onChangeExample)
35 | Example.ProgramGuideSimple -> ProgramGuideSimpleScreen(reset)
36 | Example.ProgramGuideConfiguration -> ProgramGuideConfigurationScreen(reset)
37 | Example.ProgramGuideState -> ProgramGuideStateScreen(reset)
38 | Example.ProgramGuideSize -> ProgramGuideSizeScreen(reset)
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/Example.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo
2 |
3 | /**
4 | * Enumeration of available demo examples.
5 | *
6 | * @param label Example name.
7 | * @param description Brief description.
8 | */
9 | enum class Example(
10 | val label: String,
11 | val description: String,
12 | ) {
13 | ProgramGuideSimple(
14 | "Simple Program Guide",
15 | "Basic program guide usage"
16 | ),
17 | ProgramGuideConfiguration(
18 | "Configurable Program Guide",
19 | "Example of different configuration options"
20 | ),
21 | ProgramGuideState(
22 | "Program Guide State",
23 | "Example how program guide state could be used"
24 | ),
25 | ProgramGuideSize(
26 | "Program Guide Size",
27 | "Example how to precalculate size for the layout"
28 | ),
29 | }
30 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/RootScreen.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.lazy.LazyColumn
9 | import androidx.compose.foundation.lazy.items
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Scaffold
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import eu.wewox.programguide.demo.ui.components.TopBar
20 | import eu.wewox.programguide.demo.ui.theme.SpacingMedium
21 |
22 | @Composable
23 | fun RootScreen(onExampleClick: (Example) -> Unit) {
24 | Scaffold(
25 | topBar = { TopBar("Program Guide Demo") }
26 | ) { padding ->
27 | LazyColumn(Modifier.padding(padding)) {
28 | items(Example.entries) {
29 | Row(
30 | verticalAlignment = Alignment.CenterVertically,
31 | modifier = Modifier
32 | .fillMaxWidth()
33 | .clickable { onExampleClick(it) }
34 | .padding(SpacingMedium)
35 | ) {
36 | Column(modifier = Modifier.weight(1f)) {
37 | Text(
38 | text = it.label,
39 | style = MaterialTheme.typography.titleMedium
40 | )
41 | Text(
42 | text = it.description,
43 | style = MaterialTheme.typography.bodyMedium
44 | )
45 | }
46 | Icon(
47 | imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
48 | contentDescription = null
49 | )
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/data/Program.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo.data
2 |
3 | /**
4 | * The demo domain object for program.
5 | * For simplicity has only channel index and start & end hours, real programs could be much more
6 | * complicated.
7 | *
8 | * @property channel The channel index.
9 | * @property start The start hour.
10 | * @property end The end hour.
11 | * @property title The simple title of the program.
12 | */
13 | data class Program(
14 | val channel: Int,
15 | val start: Float,
16 | val end: Float,
17 | val title: String,
18 | )
19 |
20 | /**
21 | * Formats time.
22 | */
23 | fun formatTime(from: Float, to: Float? = null): String {
24 | fun Float.hour(): String = toInt().formatWithPrefix(2)
25 | fun Float.minutes(): String = ((this % 1) * 60).toInt().formatWithPrefix(2)
26 |
27 | val fromFormatted = "${from.hour()}:${from.minutes()}"
28 | return if (to != null) {
29 | val toFormatted = "${to.hour()}:${to.minutes()}"
30 | "$fromFormatted - $toFormatted"
31 | } else {
32 | fromFormatted
33 | }
34 | }
35 |
36 | /**
37 | * Creates a list of programs to view in program guide.
38 | */
39 | fun createPrograms(
40 | channels: Int = CHANNELS_COUNT,
41 | timeline: IntRange = 0..HOURS_COUNT
42 | ): List {
43 | var channel = 0
44 | var hour = timeline.first + HOURS.random()
45 | return buildList {
46 | while (channel < channels) {
47 | while (true) {
48 | val end = hour + HOURS.random()
49 | if (end > timeline.last) {
50 | break
51 | }
52 |
53 | add(Program(channel, hour, end, "Program #$size"))
54 | hour = end
55 | }
56 | hour = timeline.first + HOURS.random()
57 | channel += 1
58 | }
59 | }
60 | }
61 |
62 | private fun Int.formatWithPrefix(length: Int, prefix: Char = '0'): String {
63 | val number = toString()
64 | val prefixLength = (length - number.length).coerceAtLeast(0)
65 | val prefixFull = List(prefixLength) { prefix }.joinToString(separator = "")
66 | return "$prefixFull$number"
67 | }
68 |
69 | private val HOURS = listOf(0.5f, 1f, 1.25f, 1.5f, 2f, 2.25f, 2.5f)
70 |
71 | private const val CHANNELS_COUNT = 30
72 | private const val HOURS_COUNT = 24
73 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/screens/ProgramGuideConfigurationScreen.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo.screens
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Switch
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 | import eu.wewox.programguide.ProgramGuide
21 | import eu.wewox.programguide.ProgramGuideDefaults
22 | import eu.wewox.programguide.ProgramGuideDimensions
23 | import eu.wewox.programguide.ProgramGuideItem
24 | import eu.wewox.programguide.demo.Example
25 | import eu.wewox.programguide.demo.data.Program
26 | import eu.wewox.programguide.demo.data.createPrograms
27 | import eu.wewox.programguide.demo.ui.components.ChannelCell
28 | import eu.wewox.programguide.demo.ui.components.CurrentTimeLine
29 | import eu.wewox.programguide.demo.ui.components.ProgramCell
30 | import eu.wewox.programguide.demo.ui.components.TimelineItemCell
31 | import eu.wewox.programguide.demo.ui.components.TopBar
32 | import eu.wewox.programguide.demo.ui.components.TopCornerCell
33 | import eu.wewox.programguide.demo.ui.theme.SpacingMedium
34 |
35 | /**
36 | * Example of different configuration options.
37 | */
38 | @Composable
39 | fun ProgramGuideConfigurationScreen(onBackClick: () -> Unit) {
40 | Scaffold(
41 | topBar = {
42 | TopBar(
43 | title = Example.ProgramGuideConfiguration.label,
44 | onBackClick = onBackClick,
45 | )
46 | }
47 | ) { padding ->
48 | val channels = 20
49 | val timeline = 8..22
50 | val programs = remember { createPrograms(channels, timeline) }
51 |
52 | Column(Modifier.padding(padding)) {
53 | var settings by remember { mutableStateOf(Settings()) }
54 |
55 | Settings(
56 | settings = settings,
57 | onChange = { settings = it },
58 | )
59 |
60 | ProgramGuide(
61 | channels = channels,
62 | timeline = timeline,
63 | programs = programs,
64 | settings = settings,
65 | )
66 | }
67 | }
68 | }
69 |
70 | @Composable
71 | private fun ProgramGuide(
72 | channels: Int,
73 | timeline: IntRange,
74 | programs: List,
75 | settings: Settings,
76 | modifier: Modifier = Modifier
77 | ) {
78 | ProgramGuide(
79 | dimensions = dimensions(settings.showChannels, settings.showTimeline),
80 | modifier = modifier.fillMaxWidth()
81 | ) {
82 | guideStartHour = timeline.first.toFloat()
83 |
84 | programs(
85 | items = programs,
86 | layoutInfo = {
87 | ProgramGuideItem.Program(
88 | channelIndex = it.channel,
89 | startHour = it.start,
90 | endHour = it.end,
91 | )
92 | },
93 | itemContent = { ProgramCell(it) },
94 | )
95 |
96 | if (settings.showChannels) {
97 | channels(
98 | count = channels,
99 | layoutInfo = {
100 | ProgramGuideItem.Channel(
101 | index = it
102 | )
103 | },
104 | itemContent = { ChannelCell(it) },
105 | )
106 | }
107 |
108 | if (settings.showTimeline) {
109 | timeline(
110 | count = timeline.count(),
111 | layoutInfo = {
112 | val start = timeline.toList()[it].toFloat()
113 | ProgramGuideItem.Timeline(
114 | startHour = start,
115 | endHour = start + 1f
116 | )
117 | },
118 | itemContent = { TimelineItemCell(timeline.toList()[it].toFloat()) },
119 | )
120 | }
121 |
122 | if (settings.showCurrentTime) {
123 | currentTime(
124 | layoutInfo = { ProgramGuideItem.CurrentTime(12.5f) },
125 | itemContent = { CurrentTimeLine() },
126 | )
127 | }
128 |
129 | if (settings.showCorner) {
130 | topCorner(
131 | itemContent = { TopCornerCell() },
132 | )
133 | }
134 | }
135 | }
136 |
137 | @Composable
138 | private fun Settings(
139 | settings: Settings,
140 | onChange: (Settings) -> Unit,
141 | ) {
142 | @Composable
143 | fun SettingsRow(
144 | text: String,
145 | value: Boolean,
146 | onChange: (Boolean) -> Unit,
147 | ) {
148 | Row(
149 | verticalAlignment = Alignment.CenterVertically,
150 | horizontalArrangement = Arrangement.spacedBy(SpacingMedium),
151 | modifier = Modifier
152 | .fillMaxWidth()
153 | .padding(horizontal = SpacingMedium),
154 | ) {
155 | Text(
156 | text = text,
157 | style = MaterialTheme.typography.titleMedium,
158 | modifier = Modifier.weight(1f)
159 | )
160 | Switch(
161 | checked = value,
162 | onCheckedChange = onChange
163 | )
164 | }
165 | }
166 |
167 | SettingsRow(
168 | text = "Show channels",
169 | value = settings.showChannels,
170 | onChange = { onChange(settings.copy(showChannels = it)) }
171 | )
172 |
173 | SettingsRow(
174 | text = "Show timeline",
175 | value = settings.showTimeline,
176 | onChange = { onChange(settings.copy(showTimeline = it)) }
177 | )
178 |
179 | SettingsRow(
180 | text = "Show current time",
181 | value = settings.showCurrentTime,
182 | onChange = { onChange(settings.copy(showCurrentTime = it)) }
183 | )
184 |
185 | SettingsRow(
186 | text = "Show corner",
187 | value = settings.showCorner,
188 | onChange = { onChange(settings.copy(showCorner = it)) }
189 | )
190 | }
191 |
192 | private fun dimensions(
193 | showChannels: Boolean,
194 | showTimeline: Boolean,
195 | ): ProgramGuideDimensions =
196 | ProgramGuideDefaults.dimensions.let {
197 | it.copy(
198 | channelWidth = if (showChannels) it.channelWidth else 0.dp,
199 | timelineHeight = if (showTimeline) it.timelineHeight else 0.dp,
200 | )
201 | }
202 |
203 | private data class Settings(
204 | val showChannels: Boolean = true,
205 | val showTimeline: Boolean = true,
206 | val showCurrentTime: Boolean = true,
207 | val showCorner: Boolean = true,
208 | )
209 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/screens/ProgramGuideSimpleScreen.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo.screens
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.Scaffold
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.Modifier
9 | import eu.wewox.programguide.ProgramGuide
10 | import eu.wewox.programguide.ProgramGuideItem
11 | import eu.wewox.programguide.demo.Example
12 | import eu.wewox.programguide.demo.data.createPrograms
13 | import eu.wewox.programguide.demo.ui.components.ChannelCell
14 | import eu.wewox.programguide.demo.ui.components.ProgramCell
15 | import eu.wewox.programguide.demo.ui.components.TimelineItemCell
16 | import eu.wewox.programguide.demo.ui.components.TopBar
17 |
18 | /**
19 | * Showcases the most simple usage of program guide.
20 | */
21 | @Composable
22 | fun ProgramGuideSimpleScreen(onBackClick: () -> Unit) {
23 | Scaffold(
24 | topBar = {
25 | TopBar(
26 | title = Example.ProgramGuideSimple.label,
27 | onBackClick = onBackClick,
28 | )
29 | }
30 | ) { padding ->
31 | val channels = 20
32 | val timeline = 8..22
33 | val programs = remember { createPrograms(channels, timeline) }
34 |
35 | ProgramGuide(
36 | modifier = Modifier
37 | .fillMaxWidth()
38 | .padding(padding)
39 | ) {
40 | guideStartHour = timeline.first.toFloat()
41 |
42 | programs(
43 | items = programs,
44 | layoutInfo = {
45 | ProgramGuideItem.Program(
46 | channelIndex = it.channel,
47 | startHour = it.start,
48 | endHour = it.end,
49 | )
50 | },
51 | itemContent = { ProgramCell(it) },
52 | )
53 |
54 | channels(
55 | count = channels,
56 | layoutInfo = {
57 | ProgramGuideItem.Channel(
58 | index = it
59 | )
60 | },
61 | itemContent = { ChannelCell(it) },
62 | )
63 |
64 | timeline(
65 | count = timeline.count(),
66 | layoutInfo = {
67 | val start = timeline.toList()[it].toFloat()
68 | ProgramGuideItem.Timeline(
69 | startHour = start,
70 | endHour = start + 1f
71 | )
72 | },
73 | itemContent = { TimelineItemCell(timeline.toList()[it].toFloat()) },
74 | )
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/screens/ProgramGuideSizeScreen.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo.screens
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.LocalTextStyle
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Scaffold
9 | import androidx.compose.material3.Slider
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.CompositionLocalProvider
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.Modifier
18 | import androidx.compose.ui.platform.LocalDensity
19 | import androidx.compose.ui.text.rememberTextMeasurer
20 | import androidx.compose.ui.unit.Density
21 | import androidx.compose.ui.unit.Dp
22 | import androidx.compose.ui.unit.dp
23 | import eu.wewox.programguide.ProgramGuide
24 | import eu.wewox.programguide.ProgramGuideDefaults
25 | import eu.wewox.programguide.ProgramGuideDimensions
26 | import eu.wewox.programguide.ProgramGuideItem
27 | import eu.wewox.programguide.demo.Example
28 | import eu.wewox.programguide.demo.data.createPrograms
29 | import eu.wewox.programguide.demo.ui.components.ChannelCell
30 | import eu.wewox.programguide.demo.ui.components.ProgramCell
31 | import eu.wewox.programguide.demo.ui.components.TimelineItemCell
32 | import eu.wewox.programguide.demo.ui.components.TopBar
33 | import eu.wewox.programguide.demo.ui.theme.SpacingMedium
34 |
35 | /**
36 | * Example how to precalculate size for the layout.
37 | */
38 | @Composable
39 | fun ProgramGuideSizeScreen(onBackClick: () -> Unit) {
40 | Scaffold(
41 | topBar = {
42 | TopBar(
43 | title = Example.ProgramGuideSize.label,
44 | onBackClick = onBackClick,
45 | )
46 | }
47 | ) { padding ->
48 | val channels = 20
49 | val timeline = 8..22
50 | val programs = remember { createPrograms(channels, timeline) }
51 |
52 | var fontScaling by remember { mutableStateOf(1f) }
53 |
54 | Column(
55 | modifier = Modifier
56 | .fillMaxWidth()
57 | .padding(padding)
58 | ) {
59 | Settings(fontScaling = fontScaling, onFontScalingChange = { fontScaling = it })
60 |
61 | val density = LocalDensity.current.density
62 | CompositionLocalProvider(LocalDensity provides Density(density, fontScaling)) {
63 | ProgramGuide(
64 | dimensions = calculateProgramGuideDimensions(),
65 | modifier = Modifier.fillMaxWidth()
66 | ) {
67 | guideStartHour = timeline.first.toFloat()
68 |
69 | programs(
70 | items = programs,
71 | layoutInfo = {
72 | ProgramGuideItem.Program(
73 | channelIndex = it.channel,
74 | startHour = it.start,
75 | endHour = it.end,
76 | )
77 | },
78 | itemContent = { ProgramCell(it) },
79 | )
80 |
81 | channels(
82 | count = channels,
83 | layoutInfo = {
84 | ProgramGuideItem.Channel(
85 | index = it
86 | )
87 | },
88 | itemContent = { ChannelCell(it) },
89 | )
90 |
91 | timeline(
92 | count = timeline.count(),
93 | layoutInfo = {
94 | val start = timeline.toList()[it].toFloat()
95 | ProgramGuideItem.Timeline(
96 | startHour = start,
97 | endHour = start + 1f
98 | )
99 | },
100 | itemContent = { TimelineItemCell(timeline.toList()[it].toFloat()) },
101 | )
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
108 | @Composable
109 | private fun Settings(
110 | fontScaling: Float,
111 | onFontScalingChange: (Float) -> Unit,
112 | ) {
113 | Column {
114 | Text(
115 | text = """
116 | Program guide dimensions should be pre-calculated to support dynamic font sizes.
117 | Keep in mind, that calculations should be tightly coupled with a displayed UI.
118 | """.trimIndent(),
119 | modifier = Modifier
120 | .padding(horizontal = SpacingMedium)
121 | .fillMaxWidth()
122 | )
123 |
124 | Slider(
125 | value = fontScaling,
126 | onValueChange = onFontScalingChange,
127 | valueRange = 1f..3f,
128 | modifier = Modifier
129 | .padding(SpacingMedium)
130 | .fillMaxWidth()
131 | )
132 | }
133 | }
134 |
135 | @Composable
136 | private fun calculateProgramGuideDimensions(): ProgramGuideDimensions =
137 | ProgramGuideDefaults.dimensions.copy(
138 | channelHeight = calculateProgramHeight(),
139 | channelWidth = calculateChannelWidth(),
140 | timelineHeight = calculateTimelineHeight(),
141 | )
142 |
143 | @Composable
144 | private fun calculateProgramHeight(): Dp {
145 | val textMeasurer = rememberTextMeasurer()
146 |
147 | val verticalPadding = 8.dp
148 | val title = textMeasurer.measure(
149 | text = "",
150 | maxLines = 1,
151 | style = LocalTextStyle.current
152 | )
153 | val label = textMeasurer.measure(
154 | text = "",
155 | maxLines = 1,
156 | style = MaterialTheme.typography.labelSmall
157 | )
158 |
159 | return with(LocalDensity.current) {
160 | title.size.height.toDp() + label.size.height.toDp() + verticalPadding
161 | }
162 | }
163 |
164 | @Composable
165 | private fun calculateChannelWidth(): Dp {
166 | val textMeasurer = rememberTextMeasurer()
167 |
168 | val horizontalPadding = 8.dp
169 | val label = textMeasurer.measure(
170 | text = "Ch #MM",
171 | maxLines = 1,
172 | style = MaterialTheme.typography.labelSmall
173 | )
174 |
175 | return with(LocalDensity.current) { label.size.width.toDp() + horizontalPadding }
176 | }
177 |
178 | @Composable
179 | private fun calculateTimelineHeight(): Dp {
180 | val textMeasurer = rememberTextMeasurer()
181 |
182 | val title = textMeasurer.measure(
183 | text = "",
184 | maxLines = 1,
185 | style = LocalTextStyle.current
186 | )
187 |
188 | return with(LocalDensity.current) { title.size.height.toDp() }
189 | }
190 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/screens/ProgramGuideStateScreen.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("LongMethod")
2 |
3 | package eu.wewox.programguide.demo.screens
4 |
5 | import androidx.compose.animation.AnimatedVisibility
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
13 | import androidx.compose.material.icons.automirrored.filled.ArrowForward
14 | import androidx.compose.material3.FilledIconButton
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.Scaffold
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.derivedStateOf
19 | import androidx.compose.runtime.getValue
20 | import androidx.compose.runtime.mutableStateOf
21 | import androidx.compose.runtime.remember
22 | import androidx.compose.runtime.rememberCoroutineScope
23 | import androidx.compose.runtime.setValue
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.geometry.Offset
27 | import androidx.compose.ui.graphics.vector.ImageVector
28 | import androidx.compose.ui.platform.LocalDensity
29 | import androidx.compose.ui.unit.dp
30 | import eu.wewox.programguide.ProgramGuide
31 | import eu.wewox.programguide.ProgramGuideItem
32 | import eu.wewox.programguide.demo.Example
33 | import eu.wewox.programguide.demo.data.createPrograms
34 | import eu.wewox.programguide.demo.ui.components.ChannelCell
35 | import eu.wewox.programguide.demo.ui.components.CurrentTimeLine
36 | import eu.wewox.programguide.demo.ui.components.ProgramCell
37 | import eu.wewox.programguide.demo.ui.components.TimelineItemCell
38 | import eu.wewox.programguide.demo.ui.components.TopBar
39 | import eu.wewox.programguide.demo.ui.theme.SpacingSmall
40 | import eu.wewox.programguide.rememberSaveableProgramGuideState
41 | import kotlinx.coroutines.launch
42 |
43 | /**
44 | * Example how program guide state could be used.
45 | */
46 | @Composable
47 | fun ProgramGuideStateScreen(onBackClick: () -> Unit) {
48 | Scaffold(
49 | topBar = {
50 | TopBar(
51 | title = Example.ProgramGuideState.label,
52 | onBackClick = onBackClick,
53 | )
54 | }
55 | ) { padding ->
56 | val channels = 20
57 | val timeline = 8..22
58 | val currentTime = 12.5f
59 | var programs by remember { mutableStateOf(createPrograms(channels, timeline)) }
60 |
61 | val scope = rememberCoroutineScope()
62 | val state = rememberSaveableProgramGuideState(
63 | initialOffset = {
64 | val x = getCurrentTimePosition()
65 | Offset(x, 0f)
66 | }
67 | )
68 |
69 | val buttonVisibilityThreshold = LocalDensity.current.run { 48.dp.toPx() }
70 | val showPrev by remember {
71 | derivedStateOf {
72 | val translate = state.minaBoxState.translate
73 | if (translate == null) {
74 | false
75 | } else {
76 | translate.x < buttonVisibilityThreshold
77 | }
78 | }
79 | }
80 | val showNext by remember {
81 | derivedStateOf {
82 | val translate = state.minaBoxState.translate
83 | if (translate == null) {
84 | false
85 | } else {
86 | translate.maxX - translate.x < buttonVisibilityThreshold
87 | }
88 | }
89 | }
90 |
91 | Box {
92 | ProgramGuide(
93 | state = state,
94 | modifier = Modifier
95 | .fillMaxWidth()
96 | .padding(padding)
97 | ) {
98 | guideStartHour = timeline.first.toFloat()
99 |
100 | programs(
101 | items = programs,
102 | layoutInfo = {
103 | ProgramGuideItem.Program(
104 | channelIndex = it.channel,
105 | startHour = it.start,
106 | endHour = it.end,
107 | )
108 | },
109 | itemContent = {
110 | ProgramCell(
111 | program = it,
112 | onClick = {
113 | val index = programs.indexOf(it)
114 | scope.launch { state.animateToProgram(index) }
115 | }
116 | )
117 | },
118 | )
119 |
120 | channels(
121 | count = channels,
122 | layoutInfo = {
123 | ProgramGuideItem.Channel(
124 | index = it
125 | )
126 | },
127 | itemContent = {
128 | ChannelCell(
129 | index = it,
130 | onClick = {
131 | scope.launch { state.animateToChannel(it) }
132 | }
133 | )
134 | },
135 | )
136 |
137 | timeline(
138 | count = timeline.count(),
139 | layoutInfo = {
140 | val start = timeline.toList()[it].toFloat()
141 | ProgramGuideItem.Timeline(
142 | startHour = start,
143 | endHour = start + 1f
144 | )
145 | },
146 | itemContent = {
147 | TimelineItemCell(
148 | hour = timeline.toList()[it].toFloat(),
149 | onClick = {
150 | scope.launch { state.animateToTimeline(it) }
151 | }
152 | )
153 | },
154 | )
155 |
156 | currentTime(
157 | layoutInfo = { ProgramGuideItem.CurrentTime(currentTime) },
158 | itemContent = { CurrentTimeLine() },
159 | )
160 | }
161 |
162 | ControlButton(
163 | visible = showPrev,
164 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
165 | onClick = { programs = createPrograms(channels, timeline) },
166 | modifier = Modifier.align(Alignment.CenterStart),
167 | )
168 |
169 | ControlButton(
170 | visible = showNext,
171 | imageVector = Icons.AutoMirrored.Filled.ArrowForward,
172 | onClick = { programs = createPrograms(channels, timeline) },
173 | modifier = Modifier.align(Alignment.CenterEnd),
174 | )
175 | }
176 | }
177 | }
178 |
179 | @Composable
180 | private fun ControlButton(
181 | visible: Boolean,
182 | imageVector: ImageVector,
183 | onClick: () -> Unit,
184 | modifier: Modifier = Modifier
185 | ) {
186 | AnimatedVisibility(
187 | visible = visible,
188 | enter = fadeIn(),
189 | exit = fadeOut(),
190 | modifier = modifier,
191 | ) {
192 | FilledIconButton(
193 | onClick = onClick,
194 | modifier = Modifier.padding(SpacingSmall),
195 | ) {
196 | Icon(
197 | imageVector = imageVector,
198 | contentDescription = null
199 | )
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/ui/components/ProgramGuideItems.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo.ui.components
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Surface
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.text.style.TextOverflow
14 | import androidx.compose.ui.unit.dp
15 | import eu.wewox.programguide.demo.data.Program
16 | import eu.wewox.programguide.demo.data.formatTime
17 |
18 | /**
19 | * Single program cell in program guide.
20 | *
21 | * @param program The program data.
22 | * @param modifier The modifier instance for the root composable.
23 | * @param onClick Callback to be called when the surface is clicked.
24 | */
25 | @Composable
26 | fun ProgramCell(program: Program, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null) {
27 | Surface(
28 | color = MaterialTheme.colorScheme.primary,
29 | border = BorderStroke(1.dp, MaterialTheme.colorScheme.surface),
30 | onClick = onClick ?: {},
31 | enabled = onClick != null,
32 | modifier = modifier,
33 | ) {
34 | Column(modifier = Modifier.padding(4.dp)) {
35 | Text(
36 | text = program.title,
37 | maxLines = 1,
38 | overflow = TextOverflow.Ellipsis,
39 | )
40 | Text(
41 | text = formatTime(program.start, program.end),
42 | maxLines = 1,
43 | overflow = TextOverflow.Ellipsis,
44 | style = MaterialTheme.typography.labelSmall,
45 | )
46 | }
47 | }
48 | }
49 |
50 | /**
51 | * Single channel cell in program guide.
52 | *
53 | * @param index The channel index.
54 | * @param modifier The modifier instance for the root composable.
55 | * @param onClick Callback to be called when the surface is clicked.
56 | */
57 | @Composable
58 | fun ChannelCell(index: Int, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null) {
59 | Surface(
60 | color = MaterialTheme.colorScheme.tertiary,
61 | border = BorderStroke(1.dp, MaterialTheme.colorScheme.surface),
62 | onClick = onClick ?: {},
63 | enabled = onClick != null,
64 | modifier = modifier,
65 | ) {
66 | Box(contentAlignment = Alignment.Center) {
67 | Text(
68 | text = "Ch #$index",
69 | style = MaterialTheme.typography.labelSmall,
70 | modifier = Modifier.padding(4.dp),
71 | )
72 | }
73 | }
74 | }
75 |
76 | /**
77 | * Single timeline item cell in program guide.
78 | *
79 | * @param hour The timeline item hour.
80 | * @param modifier The modifier instance for the root composable.
81 | * @param onClick Callback to be called when the surface is clicked.
82 | */
83 | @Composable
84 | fun TimelineItemCell(hour: Float, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null) {
85 | Surface(
86 | color = MaterialTheme.colorScheme.tertiary,
87 | border = BorderStroke(1.dp, MaterialTheme.colorScheme.surface),
88 | onClick = onClick ?: {},
89 | enabled = onClick != null,
90 | modifier = modifier,
91 | ) {
92 | Box(contentAlignment = Alignment.CenterStart) {
93 | Text(
94 | text = formatTime(hour),
95 | modifier = Modifier.padding(start = 4.dp)
96 | )
97 | }
98 | }
99 | }
100 |
101 | /**
102 | * Current time vertical line.
103 | *
104 | * @param modifier The modifier instance for the root composable.
105 | */
106 | @Composable
107 | fun CurrentTimeLine(modifier: Modifier = Modifier) {
108 | Surface(
109 | color = MaterialTheme.colorScheme.tertiary,
110 | modifier = modifier,
111 | ) {
112 | // Empty
113 | }
114 | }
115 |
116 | /**
117 | * Top corner cell item.
118 | *
119 | * @param modifier The modifier instance for the root composable.
120 | */
121 | @Composable
122 | fun TopCornerCell(modifier: Modifier = Modifier) {
123 | Surface(
124 | color = MaterialTheme.colorScheme.surface,
125 | modifier = modifier,
126 | ) {
127 | // Empty
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/ui/components/TopBar.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo.ui.components
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.IconButton
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.material3.TopAppBar
10 | import androidx.compose.material3.TopAppBarDefaults
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 |
15 | /**
16 | * The reusable component for top bar.
17 | *
18 | * @param title The text to show in top bar.
19 | * @param modifier The modifier instance for the root composable.
20 | */
21 | @Composable
22 | fun TopBar(
23 | title: String,
24 | modifier: Modifier = Modifier,
25 | onBackClick: (() -> Unit)? = null,
26 | ) {
27 | TopAppBar(
28 | title = {
29 | Text(
30 | text = title,
31 | style = MaterialTheme.typography.titleLarge,
32 | )
33 | },
34 | navigationIcon = {
35 | if (onBackClick != null) {
36 | IconButton(onClick = onBackClick) {
37 | Icon(
38 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
39 | contentDescription = "Back button"
40 | )
41 | }
42 | }
43 | },
44 | colors = TopAppBarDefaults.centerAlignedTopAppBarColors(Color.Transparent),
45 | modifier = modifier,
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UndocumentedPublicProperty")
2 |
3 | package eu.wewox.programguide.demo.ui.theme
4 |
5 | import androidx.compose.ui.graphics.Color
6 |
7 | val LightBlue = Color(0xFF6DD3FF)
8 | val LightYellow = Color(0xFFFFF281)
9 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/ui/theme/Spacing.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo.ui.theme
2 |
3 | import androidx.compose.ui.unit.dp
4 |
5 | /**
6 | * The small spacing.
7 | */
8 | val SpacingSmall = 8.dp
9 |
10 | /**
11 | * The medium spacing.
12 | */
13 | val SpacingMedium = 16.dp
14 |
--------------------------------------------------------------------------------
/demo/src/commonMain/kotlin/eu/wewox/programguide/demo/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Typography
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.lightColorScheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.graphics.Color
10 |
11 | private val DarkColorScheme = darkColorScheme(
12 | primary = LightBlue,
13 | secondary = LightBlue,
14 | tertiary = LightYellow,
15 |
16 | onPrimary = Color.Black,
17 | onSecondary = Color.Black,
18 | onTertiary = Color.Black,
19 |
20 | surface = Color.Black,
21 | background = Color.Black,
22 |
23 | onSurface = Color.White,
24 | onBackground = Color.White,
25 | )
26 |
27 | private val LightColorScheme = lightColorScheme(
28 | primary = LightBlue,
29 | secondary = LightBlue,
30 | tertiary = LightYellow,
31 |
32 | onPrimary = Color.Black,
33 | onSecondary = Color.Black,
34 | onTertiary = Color.Black,
35 |
36 | surface = Color.White,
37 | background = Color.White,
38 |
39 | onSurface = Color.Black,
40 | onBackground = Color.Black,
41 | )
42 |
43 | /**
44 | * The theme to use for demo application.
45 | */
46 | @Composable
47 | fun ProgramGuideDemoTheme(
48 | darkTheme: Boolean = isSystemInDarkTheme(),
49 | content: @Composable () -> Unit
50 | ) {
51 | val colorScheme = when {
52 | darkTheme -> DarkColorScheme
53 | else -> LightColorScheme
54 | }
55 |
56 | MaterialTheme(
57 | colorScheme = colorScheme,
58 | typography = Typography(),
59 | content = content
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/demo/src/iosMain/kotlin/main.ios.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.ComposeUIViewController
2 | import eu.wewox.programguide.demo.App
3 |
4 | fun MainViewController() = ComposeUIViewController { App() }
5 |
--------------------------------------------------------------------------------
/desktopdemo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/desktopdemo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin.multiplatform)
5 | alias(libs.plugins.jetbrains.compose)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | kotlin {
10 | jvm()
11 | sourceSets {
12 | val jvmMain by getting {
13 | dependencies {
14 | implementation(project(":demo"))
15 | implementation(compose.desktop.currentOs)
16 | }
17 | }
18 | }
19 | }
20 |
21 | compose.desktop {
22 | application {
23 | mainClass = "MainKt"
24 | nativeDistributions {
25 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
26 | packageName = "programguide"
27 | packageVersion = "1.0.0"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/desktopdemo/src/jvmMain/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.Window
2 | import androidx.compose.ui.window.application
3 | import eu.wewox.programguide.demo.App
4 |
5 | fun main() = application {
6 | Window(
7 | onCloseRequest = ::exitApplication,
8 | title = "ProgramGuide",
9 | ) {
10 | App()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 |
25 | # MPP
26 | kotlin.mpp.stability.nowarn=true
27 | kotlin.mpp.enableCInteropCommonization=true
28 | kotlin.mpp.androidSourceSetLayoutVersion=2
29 |
30 | # Compose
31 | org.jetbrains.compose.experimental.uikit.enabled=true
32 | org.jetbrains.compose.experimental.wasm.enabled=true
33 |
34 | # Maven setup with https://github.com/vanniktech/gradle-maven-publish-plugin
35 | SONATYPE_HOST=S01
36 | RELEASE_SIGNING_ENABLED=true
37 |
38 | GROUP=io.github.oleksandrbalan
39 | POM_ARTIFACT_ID=programguide
40 | VERSION_NAME=1.6.0
41 |
42 | POM_NAME=Program Guide
43 | POM_DESCRIPTION=Lazy layout for Jetpack Compose to display program guide data on the two directional plane.
44 | POM_INCEPTION_YEAR=2023
45 | POM_URL=https://github.com/oleksandrbalan/programguide
46 |
47 | POM_LICENSE_NAME=The Apache Software License, Version 2.0
48 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
49 | POM_LICENSE_DIST=repo
50 |
51 | POM_SCM_URL=https://github.com/oleksandrbalan/programguide
52 | POM_SCM_CONNECTION=scm:git:git://github.com/oleksandrbalan/programguide.git
53 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/oleksandrbalan/programguide.git
54 |
55 | POM_DEVELOPER_ID=oleksandrbalan
56 | POM_DEVELOPER_NAME=Oleksandr Balan
57 | POM_DEVELOPER_URL=https://github.com/oleksandrbalan
58 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | sdk-compile = "35"
3 | sdk-min = "21"
4 | sdk-target = "35"
5 |
6 | minabox = "1.10.0"
7 |
8 | # Must be updated along with compose-multiplatform
9 | compose-bom = "2025.01.00"
10 | compose-multiplatform = "1.7.3"
11 | activity-compose = "1.10.0"
12 | core-splashscreen = "1.0.1"
13 |
14 | plugin-android-gradle = "8.8.1"
15 | plugin-kotlin = "2.1.10"
16 | plugin-detekt = "1.23.5"
17 | plugin-spotless = "6.5.1"
18 | plugin-mavenpublish = "0.25.3"
19 |
20 | java-toolchain = "17"
21 |
22 | [libraries]
23 | minabox = { module = "io.github.oleksandrbalan:minabox", version.ref = "minabox" }
24 | compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
25 | compose-material3 = { module = "androidx.compose.material3:material3" }
26 | compose-ui = { module = "androidx.compose.ui:ui" }
27 | compose-uitooling = { module = "androidx.compose.ui:ui-tooling" }
28 | compose-uitoolingpreview = { module = "androidx.compose.ui:ui-tooling-preview" }
29 | androidx-activitycompose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
30 | androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "core-splashscreen" }
31 |
32 | [plugins]
33 | android-application = { id = "com.android.application", version.ref = "plugin-android-gradle" }
34 | android-library = { id = "com.android.library", version.ref = "plugin-android-gradle" }
35 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "plugin-kotlin" }
36 | jetbrains-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "plugin-kotlin" }
37 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
38 | kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "plugin-kotlin" }
39 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "plugin-kotlin" }
40 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "plugin-kotlin" }
41 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "plugin-detekt" }
42 | spotless = { id = "com.diffplug.spotless", version.ref = "plugin-spotless" }
43 | mavenpublish = { id = "com.vanniktech.maven.publish", version.ref = "plugin-mavenpublish" }
44 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Sep 05 21:54:15 CEST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or 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 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/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 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/iosdemo/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=eu.wewox.programguide
3 | APP_NAME=ProgramGuide
4 |
--------------------------------------------------------------------------------
/iosdemo/Podfile:
--------------------------------------------------------------------------------
1 | use_frameworks!
2 |
3 | platform :ios, '14.1'
4 |
5 | target 'iosApp' do
6 | pod 'demo', :path => '../demo'
7 | end
8 |
--------------------------------------------------------------------------------
/iosdemo/iosApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 51;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
13 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
14 | CFDB58B53BB94DE262B13C24 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
19 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
20 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
21 | 4FF3202A603A284706412EDC /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; };
22 | 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
23 | 7555FF7B242A565900829871 /* ProgramGuide.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ProgramGuide.app; sourceTree = BUILT_PRODUCTS_DIR; };
24 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
25 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
26 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
27 | FF8CA3F5360CEAB49D74065F /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; };
28 | /* End PBXFileReference section */
29 |
30 | /* Begin PBXFrameworksBuildPhase section */
31 | F85CB1118929364A9C6EFABC /* Frameworks */ = {
32 | isa = PBXFrameworksBuildPhase;
33 | buildActionMask = 2147483647;
34 | files = (
35 | CFDB58B53BB94DE262B13C24 /* Pods_iosApp.framework in Frameworks */,
36 | );
37 | runOnlyForDeploymentPostprocessing = 0;
38 | };
39 | /* End PBXFrameworksBuildPhase section */
40 |
41 | /* Begin PBXGroup section */
42 | 058557D7273AAEEB004C7B11 /* Preview Content */ = {
43 | isa = PBXGroup;
44 | children = (
45 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
46 | );
47 | path = "Preview Content";
48 | sourceTree = "";
49 | };
50 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = {
51 | isa = PBXGroup;
52 | children = (
53 | 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */,
54 | );
55 | name = Frameworks;
56 | sourceTree = "";
57 | };
58 | 7555FF72242A565900829871 = {
59 | isa = PBXGroup;
60 | children = (
61 | AB1DB47929225F7C00F7AF9C /* Configuration */,
62 | 7555FF7D242A565900829871 /* iosApp */,
63 | 7555FF7C242A565900829871 /* Products */,
64 | FEFF387C0A8D172AA4D59CAE /* Pods */,
65 | 42799AB246E5F90AF97AA0EF /* Frameworks */,
66 | );
67 | sourceTree = "";
68 | };
69 | 7555FF7C242A565900829871 /* Products */ = {
70 | isa = PBXGroup;
71 | children = (
72 | 7555FF7B242A565900829871 /* ProgramGuide.app */,
73 | );
74 | name = Products;
75 | sourceTree = "";
76 | };
77 | 7555FF7D242A565900829871 /* iosApp */ = {
78 | isa = PBXGroup;
79 | children = (
80 | 058557BA273AAA24004C7B11 /* Assets.xcassets */,
81 | 7555FF82242A565900829871 /* ContentView.swift */,
82 | 7555FF8C242A565B00829871 /* Info.plist */,
83 | 2152FB032600AC8F00CF470E /* iOSApp.swift */,
84 | 058557D7273AAEEB004C7B11 /* Preview Content */,
85 | );
86 | path = iosApp;
87 | sourceTree = "";
88 | };
89 | AB1DB47929225F7C00F7AF9C /* Configuration */ = {
90 | isa = PBXGroup;
91 | children = (
92 | AB3632DC29227652001CCB65 /* Config.xcconfig */,
93 | );
94 | path = Configuration;
95 | sourceTree = "";
96 | };
97 | FEFF387C0A8D172AA4D59CAE /* Pods */ = {
98 | isa = PBXGroup;
99 | children = (
100 | 4FF3202A603A284706412EDC /* Pods-iosApp.debug.xcconfig */,
101 | FF8CA3F5360CEAB49D74065F /* Pods-iosApp.release.xcconfig */,
102 | );
103 | path = Pods;
104 | sourceTree = "";
105 | };
106 | /* End PBXGroup section */
107 |
108 | /* Begin PBXNativeTarget section */
109 | 7555FF7A242A565900829871 /* iosApp */ = {
110 | isa = PBXNativeTarget;
111 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
112 | buildPhases = (
113 | 98D614C51D2DA07C614CC46E /* [CP] Check Pods Manifest.lock */,
114 | 7555FF77242A565900829871 /* Sources */,
115 | 7555FF79242A565900829871 /* Resources */,
116 | F85CB1118929364A9C6EFABC /* Frameworks */,
117 | 7EB96A24287E71B2A8F36112 /* [CP] Copy Pods Resources */,
118 | );
119 | buildRules = (
120 | );
121 | dependencies = (
122 | );
123 | name = iosApp;
124 | productName = iosApp;
125 | productReference = 7555FF7B242A565900829871 /* ProgramGuide.app */;
126 | productType = "com.apple.product-type.application";
127 | };
128 | /* End PBXNativeTarget section */
129 |
130 | /* Begin PBXProject section */
131 | 7555FF73242A565900829871 /* Project object */ = {
132 | isa = PBXProject;
133 | attributes = {
134 | LastSwiftUpdateCheck = 1130;
135 | LastUpgradeCheck = 1130;
136 | ORGANIZATIONNAME = orgName;
137 | TargetAttributes = {
138 | 7555FF7A242A565900829871 = {
139 | CreatedOnToolsVersion = 11.3.1;
140 | };
141 | };
142 | };
143 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
144 | compatibilityVersion = "Xcode 9.3";
145 | developmentRegion = en;
146 | hasScannedForEncodings = 0;
147 | knownRegions = (
148 | en,
149 | Base,
150 | );
151 | mainGroup = 7555FF72242A565900829871;
152 | productRefGroup = 7555FF7C242A565900829871 /* Products */;
153 | projectDirPath = "";
154 | projectRoot = "";
155 | targets = (
156 | 7555FF7A242A565900829871 /* iosApp */,
157 | );
158 | };
159 | /* End PBXProject section */
160 |
161 | /* Begin PBXResourcesBuildPhase section */
162 | 7555FF79242A565900829871 /* Resources */ = {
163 | isa = PBXResourcesBuildPhase;
164 | buildActionMask = 2147483647;
165 | files = (
166 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
167 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
168 | );
169 | runOnlyForDeploymentPostprocessing = 0;
170 | };
171 | /* End PBXResourcesBuildPhase section */
172 |
173 | /* Begin PBXShellScriptBuildPhase section */
174 | 7EB96A24287E71B2A8F36112 /* [CP] Copy Pods Resources */ = {
175 | isa = PBXShellScriptBuildPhase;
176 | buildActionMask = 2147483647;
177 | files = (
178 | );
179 | inputFileListPaths = (
180 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist",
181 | );
182 | name = "[CP] Copy Pods Resources";
183 | outputFileListPaths = (
184 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist",
185 | );
186 | runOnlyForDeploymentPostprocessing = 0;
187 | shellPath = /bin/sh;
188 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
189 | showEnvVarsInLog = 0;
190 | };
191 | 98D614C51D2DA07C614CC46E /* [CP] Check Pods Manifest.lock */ = {
192 | isa = PBXShellScriptBuildPhase;
193 | buildActionMask = 2147483647;
194 | files = (
195 | );
196 | inputFileListPaths = (
197 | );
198 | inputPaths = (
199 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
200 | "${PODS_ROOT}/Manifest.lock",
201 | );
202 | name = "[CP] Check Pods Manifest.lock";
203 | outputFileListPaths = (
204 | );
205 | outputPaths = (
206 | "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt",
207 | );
208 | runOnlyForDeploymentPostprocessing = 0;
209 | shellPath = /bin/sh;
210 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
211 | showEnvVarsInLog = 0;
212 | };
213 | /* End PBXShellScriptBuildPhase section */
214 |
215 | /* Begin PBXSourcesBuildPhase section */
216 | 7555FF77242A565900829871 /* Sources */ = {
217 | isa = PBXSourcesBuildPhase;
218 | buildActionMask = 2147483647;
219 | files = (
220 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
221 | 7555FF83242A565900829871 /* ContentView.swift in Sources */,
222 | );
223 | runOnlyForDeploymentPostprocessing = 0;
224 | };
225 | /* End PBXSourcesBuildPhase section */
226 |
227 | /* Begin XCBuildConfiguration section */
228 | 7555FFA3242A565B00829871 /* Debug */ = {
229 | isa = XCBuildConfiguration;
230 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
231 | buildSettings = {
232 | ALWAYS_SEARCH_USER_PATHS = NO;
233 | CLANG_ANALYZER_NONNULL = YES;
234 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
235 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
236 | CLANG_CXX_LIBRARY = "libc++";
237 | CLANG_ENABLE_MODULES = YES;
238 | CLANG_ENABLE_OBJC_ARC = YES;
239 | CLANG_ENABLE_OBJC_WEAK = YES;
240 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
241 | CLANG_WARN_BOOL_CONVERSION = YES;
242 | CLANG_WARN_COMMA = YES;
243 | CLANG_WARN_CONSTANT_CONVERSION = YES;
244 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
245 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
246 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
247 | CLANG_WARN_EMPTY_BODY = YES;
248 | CLANG_WARN_ENUM_CONVERSION = YES;
249 | CLANG_WARN_INFINITE_RECURSION = YES;
250 | CLANG_WARN_INT_CONVERSION = YES;
251 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
252 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
253 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
254 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
255 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
256 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
257 | CLANG_WARN_STRICT_PROTOTYPES = YES;
258 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
259 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
260 | CLANG_WARN_UNREACHABLE_CODE = YES;
261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
262 | COPY_PHASE_STRIP = NO;
263 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
264 | ENABLE_STRICT_OBJC_MSGSEND = YES;
265 | ENABLE_TESTABILITY = YES;
266 | GCC_C_LANGUAGE_STANDARD = gnu11;
267 | GCC_DYNAMIC_NO_PIC = NO;
268 | GCC_NO_COMMON_BLOCKS = YES;
269 | GCC_OPTIMIZATION_LEVEL = 0;
270 | GCC_PREPROCESSOR_DEFINITIONS = (
271 | "DEBUG=1",
272 | "$(inherited)",
273 | );
274 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
275 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
276 | GCC_WARN_UNDECLARED_SELECTOR = YES;
277 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
278 | GCC_WARN_UNUSED_FUNCTION = YES;
279 | GCC_WARN_UNUSED_VARIABLE = YES;
280 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
281 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
282 | MTL_FAST_MATH = YES;
283 | ONLY_ACTIVE_ARCH = YES;
284 | SDKROOT = iphoneos;
285 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
286 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
287 | };
288 | name = Debug;
289 | };
290 | 7555FFA4242A565B00829871 /* Release */ = {
291 | isa = XCBuildConfiguration;
292 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;
293 | buildSettings = {
294 | ALWAYS_SEARCH_USER_PATHS = NO;
295 | CLANG_ANALYZER_NONNULL = YES;
296 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
297 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
298 | CLANG_CXX_LIBRARY = "libc++";
299 | CLANG_ENABLE_MODULES = YES;
300 | CLANG_ENABLE_OBJC_ARC = YES;
301 | CLANG_ENABLE_OBJC_WEAK = YES;
302 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
303 | CLANG_WARN_BOOL_CONVERSION = YES;
304 | CLANG_WARN_COMMA = YES;
305 | CLANG_WARN_CONSTANT_CONVERSION = YES;
306 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
307 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
308 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
309 | CLANG_WARN_EMPTY_BODY = YES;
310 | CLANG_WARN_ENUM_CONVERSION = YES;
311 | CLANG_WARN_INFINITE_RECURSION = YES;
312 | CLANG_WARN_INT_CONVERSION = YES;
313 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
314 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
315 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
316 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
317 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
318 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
319 | CLANG_WARN_STRICT_PROTOTYPES = YES;
320 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
321 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
322 | CLANG_WARN_UNREACHABLE_CODE = YES;
323 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
324 | COPY_PHASE_STRIP = NO;
325 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
326 | ENABLE_NS_ASSERTIONS = NO;
327 | ENABLE_STRICT_OBJC_MSGSEND = YES;
328 | GCC_C_LANGUAGE_STANDARD = gnu11;
329 | GCC_NO_COMMON_BLOCKS = YES;
330 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
331 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
332 | GCC_WARN_UNDECLARED_SELECTOR = YES;
333 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
334 | GCC_WARN_UNUSED_FUNCTION = YES;
335 | GCC_WARN_UNUSED_VARIABLE = YES;
336 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
337 | MTL_ENABLE_DEBUG_INFO = NO;
338 | MTL_FAST_MATH = YES;
339 | SDKROOT = iphoneos;
340 | SWIFT_COMPILATION_MODE = wholemodule;
341 | SWIFT_OPTIMIZATION_LEVEL = "-O";
342 | VALIDATE_PRODUCT = YES;
343 | };
344 | name = Release;
345 | };
346 | 7555FFA6242A565B00829871 /* Debug */ = {
347 | isa = XCBuildConfiguration;
348 | baseConfigurationReference = 4FF3202A603A284706412EDC /* Pods-iosApp.debug.xcconfig */;
349 | buildSettings = {
350 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
351 | CODE_SIGN_IDENTITY = "Apple Development";
352 | CODE_SIGN_STYLE = Automatic;
353 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
354 | DEVELOPMENT_TEAM = "${TEAM_ID}";
355 | ENABLE_PREVIEWS = YES;
356 | INFOPLIST_FILE = iosApp/Info.plist;
357 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
358 | LD_RUNPATH_SEARCH_PATHS = (
359 | "$(inherited)",
360 | "@executable_path/Frameworks",
361 | );
362 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
363 | PRODUCT_NAME = "${APP_NAME}";
364 | PROVISIONING_PROFILE_SPECIFIER = "";
365 | SWIFT_VERSION = 5.0;
366 | TARGETED_DEVICE_FAMILY = "1,2";
367 | };
368 | name = Debug;
369 | };
370 | 7555FFA7242A565B00829871 /* Release */ = {
371 | isa = XCBuildConfiguration;
372 | baseConfigurationReference = FF8CA3F5360CEAB49D74065F /* Pods-iosApp.release.xcconfig */;
373 | buildSettings = {
374 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
375 | CODE_SIGN_IDENTITY = "Apple Development";
376 | CODE_SIGN_STYLE = Automatic;
377 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
378 | DEVELOPMENT_TEAM = "${TEAM_ID}";
379 | ENABLE_PREVIEWS = YES;
380 | INFOPLIST_FILE = iosApp/Info.plist;
381 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
382 | LD_RUNPATH_SEARCH_PATHS = (
383 | "$(inherited)",
384 | "@executable_path/Frameworks",
385 | );
386 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
387 | PRODUCT_NAME = "${APP_NAME}";
388 | PROVISIONING_PROFILE_SPECIFIER = "";
389 | SWIFT_VERSION = 5.0;
390 | TARGETED_DEVICE_FAMILY = "1,2";
391 | };
392 | name = Release;
393 | };
394 | /* End XCBuildConfiguration section */
395 |
396 | /* Begin XCConfigurationList section */
397 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
398 | isa = XCConfigurationList;
399 | buildConfigurations = (
400 | 7555FFA3242A565B00829871 /* Debug */,
401 | 7555FFA4242A565B00829871 /* Release */,
402 | );
403 | defaultConfigurationIsVisible = 0;
404 | defaultConfigurationName = Release;
405 | };
406 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
407 | isa = XCConfigurationList;
408 | buildConfigurations = (
409 | 7555FFA6242A565B00829871 /* Debug */,
410 | 7555FFA7242A565B00829871 /* Release */,
411 | );
412 | defaultConfigurationIsVisible = 0;
413 | defaultConfigurationName = Release;
414 | };
415 | /* End XCConfigurationList section */
416 | };
417 | rootObject = 7555FF73242A565900829871 /* Project object */;
418 | }
419 |
--------------------------------------------------------------------------------
/iosdemo/iosApp.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/iosdemo/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 | }
--------------------------------------------------------------------------------
/iosdemo/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-512.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "512x512"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosdemo/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oleksandrbalan/programguide/91a8c29e44935eda1899a34409b023da922d44f8/iosdemo/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-512.png
--------------------------------------------------------------------------------
/iosdemo/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/iosdemo/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import demo
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | Main_iosKt.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(.keyboard) // Compose has own keyboard handler
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/iosdemo/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | CADisableMinimumFrameDurationOnPhone
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/iosdemo/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/iosdemo/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 | }
--------------------------------------------------------------------------------
/programguide/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/programguide/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
2 |
3 | plugins {
4 | alias(libs.plugins.android.library)
5 | alias(libs.plugins.kotlin.multiplatform)
6 | alias(libs.plugins.jetbrains.compose)
7 | alias(libs.plugins.mavenpublish)
8 | alias(libs.plugins.compose.compiler)
9 | id("convention.jvm.toolchain")
10 | }
11 |
12 | kotlin {
13 | androidTarget()
14 |
15 | jvm()
16 |
17 | iosX64()
18 | iosArm64()
19 | iosSimulatorArm64()
20 |
21 | @OptIn(ExperimentalWasmDsl::class)
22 | wasmJs {
23 | browser()
24 | binaries.library()
25 | }
26 |
27 | applyDefaultHierarchyTemplate()
28 |
29 | sourceSets {
30 | val commonMain by getting {
31 | dependencies {
32 | api(compose.runtime)
33 | api(compose.foundation)
34 |
35 | api(libs.minabox)
36 | }
37 | }
38 | }
39 | }
40 |
41 | android {
42 | namespace = "eu.wewox.programguide"
43 |
44 | compileSdk = libs.versions.sdk.compile.get().toInt()
45 |
46 | defaultConfig {
47 | minSdk = libs.versions.sdk.min.get().toInt()
48 | }
49 | buildFeatures {
50 | compose = true
51 | }
52 | kotlin {
53 | explicitApi()
54 |
55 | androidTarget {
56 | publishLibraryVariants("release", "debug")
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/programguide/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/programguide/src/commonMain/kotlin/eu/wewox/programguide/ProgramGuide.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.platform.LocalDensity
7 | import androidx.compose.ui.unit.dp
8 | import eu.wewox.minabox.MinaBox
9 | import eu.wewox.minabox.MinaBoxItem
10 | import eu.wewox.minabox.MinaBoxScope
11 |
12 | /**
13 | * Lazy layout to display program guide data on the two directional plane.
14 | * Items should be provided with [content] lambda.
15 | *
16 | * @param modifier The modifier instance for the root composable.
17 | * @param state The state which could be used to observe and change translation offset.
18 | * @param dimensions The dimensions of the program guide.
19 | * @param contentPadding A padding around the whole content. This will add padding for the content
20 | * after it has been clipped, which is not possible via modifier param.
21 | * @param content The lambda block which describes the content. Inside this block you can use
22 | * [ProgramGuideScope.programs] / [ProgramGuideScope.channels] methods to add items.
23 | */
24 | @Composable
25 | public fun ProgramGuide(
26 | modifier: Modifier = Modifier,
27 | state: ProgramGuideState = rememberSaveableProgramGuideState(),
28 | dimensions: ProgramGuideDimensions = ProgramGuideDefaults.dimensions,
29 | contentPadding: PaddingValues = PaddingValues(0.dp),
30 | content: ProgramGuideScope.() -> Unit
31 | ) {
32 | val dimensionsPx = dimensions.roundToPx(LocalDensity.current)
33 | val scope = ProgramGuideScopeImpl().apply(content)
34 |
35 | state.dimensions = dimensionsPx
36 | state.indexMapper = ProgramGuideIndexMapper(
37 | programsCount = scope.programsContent?.count ?: 0,
38 | currentTimeCount = scope.currentTimeContent?.count ?: 0,
39 | channelsCount = scope.channelsContent?.count ?: 0,
40 | )
41 |
42 | MinaBox(
43 | state = state.minaBoxState,
44 | contentPadding = contentPadding,
45 | modifier = modifier
46 | ) {
47 | items(
48 | toMinaBoxItem = { toMinaBoxItem(scope.guideStartHour, dimensionsPx) },
49 | scope.programsContent,
50 | scope.currentTimeContent,
51 | scope.channelsContent,
52 | scope.timelinesContent,
53 | scope.topCornerContent
54 | )
55 | }
56 | }
57 |
58 | private fun MinaBoxScope.items(
59 | toMinaBoxItem: ProgramGuideItem.() -> MinaBoxItem,
60 | vararg items: ProgramGuideItemContent?,
61 | ) {
62 | if (items.isEmpty()) {
63 | return
64 | }
65 | items.filterNotNull().forEach { item ->
66 | items(
67 | count = item.count,
68 | layoutInfo = { item.layoutInfo(it).toMinaBoxItem() },
69 | key = item.key,
70 | contentType = item.contentType,
71 | itemContent = item.itemContent,
72 | )
73 | }
74 | }
75 |
76 | /**
77 | * Contains the default values for [ProgramGuide].
78 | */
79 | public object ProgramGuideDefaults {
80 |
81 | /**
82 | * The default dimensions of the program guide.
83 | */
84 | public val dimensions: ProgramGuideDimensions =
85 | ProgramGuideDimensions(
86 | timelineHourWidth = 128.dp,
87 | timelineHeight = 32.dp,
88 | channelWidth = 64.dp,
89 | channelHeight = 64.dp,
90 | currentTimeWidth = 2.dp,
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/programguide/src/commonMain/kotlin/eu/wewox/programguide/ProgramGuideDimensions.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide
2 |
3 | import androidx.compose.ui.unit.Density
4 | import androidx.compose.ui.unit.Dp
5 |
6 | /**
7 | * The dimensions of the program guide.
8 | *
9 | * Note: If for example channels are not added in the program guide the space for them will be
10 | * still occupied. In such case you should set [channelWidth] to zero to remove them from layout.
11 | * The same applies to the top timeline ([timelineHeight]).
12 | *
13 | * @property timelineHourWidth
14 | * @property timelineHeight
15 | * @property channelWidth
16 | * @property channelHeight
17 | * @property currentTimeWidth
18 | */
19 | public data class ProgramGuideDimensions(
20 | val timelineHourWidth: Dp,
21 | val timelineHeight: Dp,
22 | val channelWidth: Dp,
23 | val channelHeight: Dp,
24 | val currentTimeWidth: Dp,
25 | )
26 |
27 | /**
28 | * Converts [ProgramGuideDimensions] to [ProgramGuidePxDimensions].
29 | */
30 | internal fun ProgramGuideDimensions.roundToPx(density: Density): ProgramGuidePxDimensions =
31 | with(density) {
32 | ProgramGuidePxDimensions(
33 | timelineHourWidth = timelineHourWidth.roundToPx(),
34 | timelineHeight = timelineHeight.roundToPx(),
35 | channelWidth = channelWidth.roundToPx(),
36 | channelHeight = channelHeight.roundToPx(),
37 | currentTimeWidth = currentTimeWidth.roundToPx(),
38 | )
39 | }
40 |
41 | /**
42 | * The dimensions of the program guide in pixels.
43 | */
44 | internal data class ProgramGuidePxDimensions(
45 | val timelineHourWidth: Int,
46 | val timelineHeight: Int,
47 | val channelWidth: Int,
48 | val channelHeight: Int,
49 | val currentTimeWidth: Int,
50 | )
51 |
--------------------------------------------------------------------------------
/programguide/src/commonMain/kotlin/eu/wewox/programguide/ProgramGuideIndexMapper.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide
2 |
3 | /**
4 | * Used to map local index to global program guide items indices.
5 | *
6 | * @property programsCount The count of programs in the layout.
7 | * @property currentTimeCount The count of current time lines (0 or 1).
8 | * @property channelsCount The count of channels in the layout.
9 | */
10 | internal class ProgramGuideIndexMapper(
11 | private val programsCount: Int,
12 | private val currentTimeCount: Int,
13 | private val channelsCount: Int,
14 | ) {
15 | /**
16 | * Gets global index from program index.
17 | */
18 | fun getProgramIndex(index: Int): Int =
19 | index
20 |
21 | /**
22 | * Gets global index of the current time vertical line.
23 | */
24 | fun getCurrentTimeIndex(): Int =
25 | programsCount + 0
26 |
27 | /**
28 | * Gets global index from channel index.
29 | */
30 | fun getChannelIndex(index: Int): Int =
31 | programsCount + currentTimeCount + index
32 |
33 | /**
34 | * Gets global index from timeline item index.
35 | */
36 | fun getTimelinesIndex(index: Int): Int =
37 | programsCount + currentTimeCount + channelsCount + index
38 | }
39 |
--------------------------------------------------------------------------------
/programguide/src/commonMain/kotlin/eu/wewox/programguide/ProgramGuideItem.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide
2 |
3 | import eu.wewox.minabox.MinaBoxItem
4 |
5 | /**
6 | * The layout data for item inside program guide.
7 | *
8 | * Note: Hours used in data could contain decimal part to represent minutes. For example: 9.5f
9 | * represents 09:30 and 16.25f represents 16:15.
10 | */
11 | public sealed interface ProgramGuideItem {
12 |
13 | /**
14 | * The layout data for program.
15 | *
16 | * @property channelIndex The index of the channel.
17 | * @property startHour The starting hour.
18 | * @property endHour The end hour.
19 | */
20 | public class Program(
21 | public val channelIndex: Int,
22 | public val startHour: Float,
23 | public val endHour: Float,
24 | ) : ProgramGuideItem
25 |
26 | /**
27 | * The layout data for current time vertical line.
28 | *
29 | * @property hour The current hour.
30 | */
31 | public class CurrentTime(
32 | public val hour: Float
33 | ) : ProgramGuideItem
34 |
35 | /**
36 | * The layout data for channel.
37 | *
38 | * @property index The index of the channel.
39 | */
40 | public class Channel(
41 | public val index: Int,
42 | ) : ProgramGuideItem
43 |
44 | /**
45 | * The layout data for timeline item.
46 | * Typically [startHour] is a whole number in the selected range and [endHour] is a value
47 | * greater by one than [startHour].
48 | *
49 | * @property startHour The starting hour.
50 | * @property endHour The end hour.
51 | */
52 | public class Timeline(
53 | public val startHour: Float,
54 | public val endHour: Float,
55 | ) : ProgramGuideItem
56 |
57 | /**
58 | * The layout data for top corner where horizontal timeline row crosses vertical channels
59 | * column. Could be used to add a label or to simply clip the space.
60 | */
61 | @Suppress("CanSealedSubClassBeObject") // Object is not needed
62 | public class TopCorner : ProgramGuideItem
63 | }
64 |
65 | /**
66 | * Maps program guide layout data to [MinaBoxItem]s.
67 | */
68 | internal fun ProgramGuideItem.toMinaBoxItem(
69 | guideStartHour: Float,
70 | dimensions: ProgramGuidePxDimensions,
71 | ): MinaBoxItem = with(dimensions) {
72 | when (this@toMinaBoxItem) {
73 | is ProgramGuideItem.Program ->
74 | MinaBoxItem(
75 | x = (startHour - guideStartHour) * timelineHourWidth + channelWidth,
76 | y = channelIndex * channelHeight.toFloat() + timelineHeight,
77 | width = (endHour - startHour) * timelineHourWidth,
78 | height = channelHeight.toFloat(),
79 | )
80 |
81 | is ProgramGuideItem.CurrentTime ->
82 | MinaBoxItem(
83 | x = (hour - guideStartHour) * timelineHourWidth + channelWidth,
84 | y = 0f,
85 | width = MinaBoxItem.Value.Absolute(currentTimeWidth.toFloat()),
86 | height = MinaBoxItem.Value.MatchParent(1f),
87 | lockVertically = true,
88 | )
89 |
90 | is ProgramGuideItem.Channel ->
91 | MinaBoxItem(
92 | x = 0f,
93 | y = index * channelHeight.toFloat() + timelineHeight,
94 | width = channelWidth.toFloat(),
95 | height = channelHeight.toFloat(),
96 | lockHorizontally = true,
97 | )
98 |
99 | is ProgramGuideItem.Timeline ->
100 | MinaBoxItem(
101 | x = (startHour - guideStartHour) * timelineHourWidth + channelWidth,
102 | y = 0f,
103 | width = (endHour - startHour) * timelineHourWidth,
104 | height = timelineHeight.toFloat(),
105 | lockVertically = true,
106 | )
107 |
108 | is ProgramGuideItem.TopCorner ->
109 | MinaBoxItem(
110 | x = 0f,
111 | y = 0f,
112 | width = channelWidth.toFloat(),
113 | height = timelineHeight.toFloat(),
114 | lockHorizontally = true,
115 | lockVertically = true,
116 | )
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/programguide/src/commonMain/kotlin/eu/wewox/programguide/ProgramGuidePositionProvider.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide
2 |
3 | import androidx.compose.ui.Alignment
4 | import androidx.compose.ui.geometry.Offset
5 | import eu.wewox.minabox.MinaBoxPositionProvider
6 |
7 | /**
8 | * Interface to provide offset of the registered program guide items.
9 | */
10 | public interface ProgramGuidePositionProvider : MinaBoxPositionProvider {
11 | /**
12 | * Returns offset of the program item with local [index].
13 | *
14 | * @param index The global index of the item.
15 | * @param alignment The alignment to align item inside the [ProgramGuide].
16 | * @return An item offset.
17 | */
18 | public fun getProgramOffset(
19 | index: Int,
20 | alignment: Alignment = Alignment.Center
21 | ): Offset
22 |
23 | /**
24 | * Returns position on X axis of the current time line.
25 | *
26 | * @param alignment The alignment to align item inside the [ProgramGuide].
27 | * @return An item offset.
28 | */
29 | public fun getCurrentTimePosition(
30 | alignment: Alignment.Horizontal = Alignment.CenterHorizontally
31 | ): Float
32 |
33 | /**
34 | * Returns position on Y axis of the channel item with local [index].
35 | *
36 | * @param alignment The alignment to align item inside the [ProgramGuide].
37 | * @return An item offset.
38 | */
39 | public fun getChannelPosition(
40 | index: Int,
41 | alignment: Alignment.Vertical = Alignment.CenterVertically
42 | ): Float
43 |
44 | /**
45 | * Returns position on X axis of the timeline item with local [index].
46 | *
47 | * @param alignment The alignment to align item inside the [ProgramGuide].
48 | * @return An item offset.
49 | */
50 | public fun getTimelinePosition(
51 | index: Int,
52 | alignment: Alignment.Horizontal = Alignment.CenterHorizontally
53 | ): Float
54 | }
55 |
56 | /**
57 | * Implementation of the [ProgramGuidePositionProvider] with [MinaBoxPositionProvider].
58 | *
59 | * @property positionProvider The instance of the [MinaBoxPositionProvider].
60 | * @property dimensions The dimensions of the program guide in pixels.
61 | * @property indexMapper The local to global indices mapper.
62 | */
63 | internal class ProgramGuidePositionProviderImpl(
64 | private val positionProvider: MinaBoxPositionProvider,
65 | private val dimensions: ProgramGuidePxDimensions,
66 | private val indexMapper: ProgramGuideIndexMapper,
67 | ) : ProgramGuidePositionProvider, MinaBoxPositionProvider by positionProvider {
68 |
69 | override fun getProgramOffset(index: Int, alignment: Alignment): Offset =
70 | getOffset(
71 | index = indexMapper.getProgramIndex(index),
72 | alignment = alignment,
73 | paddingStart = dimensions.channelWidth.toFloat(),
74 | paddingTop = dimensions.timelineHeight.toFloat(),
75 | )
76 |
77 | override fun getCurrentTimePosition(alignment: Alignment.Horizontal): Float =
78 | getOffset(
79 | index = indexMapper.getCurrentTimeIndex(),
80 | alignment = alignment.toAlignment(),
81 | paddingStart = dimensions.channelWidth.toFloat(),
82 | ).x
83 |
84 | override fun getChannelPosition(index: Int, alignment: Alignment.Vertical): Float =
85 | getOffset(
86 | index = indexMapper.getChannelIndex(index),
87 | alignment = alignment.toAlignment(),
88 | paddingTop = dimensions.timelineHeight.toFloat(),
89 | ).y
90 |
91 | override fun getTimelinePosition(index: Int, alignment: Alignment.Horizontal): Float =
92 | getOffset(
93 | index = indexMapper.getTimelinesIndex(index),
94 | alignment = alignment.toAlignment(),
95 | paddingStart = dimensions.channelWidth.toFloat(),
96 | ).x
97 |
98 | private fun Alignment.Vertical.toAlignment(): Alignment =
99 | when (this) {
100 | Alignment.Top -> Alignment.TopCenter
101 | Alignment.CenterVertically -> Alignment.Center
102 | Alignment.Bottom -> Alignment.BottomCenter
103 | else -> Alignment.Center
104 | }
105 |
106 | private fun Alignment.Horizontal.toAlignment(): Alignment =
107 | when (this) {
108 | Alignment.Start -> Alignment.CenterStart
109 | Alignment.CenterHorizontally -> Alignment.Center
110 | Alignment.End -> Alignment.CenterEnd
111 | else -> Alignment.Center
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/programguide/src/commonMain/kotlin/eu/wewox/programguide/ProgramGuideScope.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide
2 |
3 | import androidx.compose.runtime.Composable
4 | import eu.wewox.minabox.MinaBoxScope
5 | import eu.wewox.programguide.ProgramGuideItem.Channel
6 | import eu.wewox.programguide.ProgramGuideItem.CurrentTime
7 | import eu.wewox.programguide.ProgramGuideItem.Program
8 | import eu.wewox.programguide.ProgramGuideItem.Timeline
9 | import eu.wewox.programguide.ProgramGuideItem.TopCorner
10 |
11 | /**
12 | * Receiver scope which is used by [ProgramGuide].
13 | *
14 | * Note: Multiple calls of the same method will override the previous call(s).
15 | */
16 | public interface ProgramGuideScope {
17 |
18 | /**
19 | * The start hour of the whole program guide. Default is 0:00.
20 | */
21 | public var guideStartHour: Float
22 |
23 | /**
24 | * Adds a [count] of programs.
25 | *
26 | * @param count The items count.
27 | * @param layoutInfo The lambda to provide layout information of the single item.
28 | * @param key A factory of stable and unique keys representing the item. Using the same key
29 | * for multiple items is not allowed. Type of the key should be saveable via Bundle on Android.
30 | * If null is passed the position in the list will represent the key.
31 | * @param contentType A factory of the content types for the item. The item compositions of
32 | * the same type could be reused more efficiently. Note that null is a valid type and items of
33 | * such type will be considered compatible.
34 | * @param itemContent The content displayed by a single item.
35 | */
36 | public fun programs(
37 | count: Int,
38 | layoutInfo: (index: Int) -> Program,
39 | key: ((index: Int) -> Any)? = null,
40 | contentType: (index: Int) -> Any? = { null },
41 | itemContent: @Composable (index: Int) -> Unit
42 | )
43 |
44 | /**
45 | * Adds a vertical current time line.
46 | *
47 | * @param layoutInfo The lambda to provide layout information of the item.
48 | * @param key A factory of stable and unique keys representing the item. Using the same key
49 | * for multiple items is not allowed. Type of the key should be saveable via Bundle on Android.
50 | * If null is passed the position in the list will represent the key.
51 | * @param contentType A factory of the content types for the item. The item compositions of
52 | * the same type could be reused more efficiently. Note that null is a valid type and items of
53 | * such type will be considered compatible.
54 | * @param itemContent The content displayed by an item.
55 | */
56 | public fun currentTime(
57 | layoutInfo: () -> CurrentTime,
58 | key: (() -> Any)? = null,
59 | contentType: () -> Any? = { null },
60 | itemContent: @Composable () -> Unit
61 | )
62 |
63 | /**
64 | * Adds a [count] of channels.
65 | *
66 | * @param count The items count.
67 | * @param layoutInfo The lambda to provide layout information of the single item.
68 | * @param key A factory of stable and unique keys representing the item. Using the same key
69 | * for multiple items is not allowed. Type of the key should be saveable via Bundle on Android.
70 | * If null is passed the position in the list will represent the key.
71 | * @param contentType A factory of the content types for the item. The item compositions of
72 | * the same type could be reused more efficiently. Note that null is a valid type and items of
73 | * such type will be considered compatible.
74 | * @param itemContent The content displayed by a single item.
75 | */
76 | public fun channels(
77 | count: Int,
78 | layoutInfo: (index: Int) -> Channel,
79 | key: ((index: Int) -> Any)? = null,
80 | contentType: (index: Int) -> Any? = { null },
81 | itemContent: @Composable (index: Int) -> Unit
82 | )
83 |
84 | /**
85 | * Adds a [count] of timeline items.
86 | *
87 | * @param count The items count.
88 | * @param layoutInfo The lambda to provide layout information of the single item.
89 | * @param key A factory of stable and unique keys representing the item. Using the same key
90 | * for multiple items is not allowed. Type of the key should be saveable via Bundle on Android.
91 | * If null is passed the position in the list will represent the key.
92 | * @param contentType A factory of the content types for the item. The item compositions of
93 | * the same type could be reused more efficiently. Note that null is a valid type and items of
94 | * such type will be considered compatible.
95 | * @param itemContent The content displayed by a single item.
96 | */
97 | public fun timeline(
98 | count: Int,
99 | layoutInfo: (index: Int) -> Timeline,
100 | key: ((index: Int) -> Any)? = null,
101 | contentType: (index: Int) -> Any? = { null },
102 | itemContent: @Composable (index: Int) -> Unit
103 | )
104 |
105 | /**
106 | * Adds a top corner.
107 | *
108 | * @param key A factory of stable and unique keys representing the item. Using the same key
109 | * for multiple items is not allowed. Type of the key should be saveable via Bundle on Android.
110 | * If null is passed the position in the list will represent the key.
111 | * @param contentType A factory of the content types for the item. The item compositions of
112 | * the same type could be reused more efficiently. Note that null is a valid type and items of
113 | * such type will be considered compatible.
114 | * @param itemContent The content displayed by an item.
115 | */
116 | public fun topCorner(
117 | key: (() -> Any)? = null,
118 | contentType: () -> Any? = { null },
119 | itemContent: @Composable () -> Unit
120 | )
121 |
122 | /**
123 | * Adds given [items] as programs.
124 | *
125 | * @param items The items to add to the [ProgramGuide].
126 | * @param layoutInfo The lambda to provide layout information of the single item.
127 | * @param key A factory of stable and unique keys representing the item. Using the same key
128 | * for multiple items is not allowed. Type of the key should be saveable via Bundle on Android.
129 | * If null is passed the position in the list will represent the key.
130 | * @param contentType A factory of the content types for the item. The item compositions of
131 | * the same type could be reused more efficiently. Note that null is a valid type and items of
132 | * such type will be considered compatible.
133 | * @param itemContent The content displayed by a single item.
134 | */
135 | public fun programs(
136 | items: List,
137 | layoutInfo: (item: T) -> Program,
138 | key: ((item: T) -> Any)? = null,
139 | contentType: (item: T) -> Any? = { null },
140 | itemContent: @Composable (item: T) -> Unit
141 | ): Unit = programs(
142 | count = items.size,
143 | layoutInfo = { index: Int -> layoutInfo(items[index]) },
144 | key = if (key != null) { index: Int -> key(items[index]) } else null,
145 | contentType = { index: Int -> contentType(items[index]) },
146 | itemContent = { index: Int -> itemContent(items[index]) },
147 | )
148 |
149 | /**
150 | * Adds given [items] as channels.
151 | *
152 | * @param items The items to add to the [ProgramGuide].
153 | * @param layoutInfo The lambda to provide layout information of the single item.
154 | * @param key A factory of stable and unique keys representing the item. Using the same key
155 | * for multiple items is not allowed. Type of the key should be saveable via Bundle on Android.
156 | * If null is passed the position in the list will represent the key.
157 | * @param contentType A factory of the content types for the item. The item compositions of
158 | * the same type could be reused more efficiently. Note that null is a valid type and items of
159 | * such type will be considered compatible.
160 | * @param itemContent The content displayed by a single item.
161 | */
162 | public fun channels(
163 | items: List,
164 | layoutInfo: (item: T) -> Channel,
165 | key: ((item: T) -> Any)? = null,
166 | contentType: (item: T) -> Any? = { null },
167 | itemContent: @Composable (item: T) -> Unit
168 | ): Unit = channels(
169 | count = items.size,
170 | layoutInfo = { index: Int -> layoutInfo(items[index]) },
171 | key = if (key != null) { index: Int -> key(items[index]) } else null,
172 | contentType = { index: Int -> contentType(items[index]) },
173 | itemContent = { index: Int -> itemContent(items[index]) },
174 | )
175 |
176 | /**
177 | * Adds given [items] as timeline items.
178 | *
179 | * @param items The items to add to the [ProgramGuide].
180 | * @param layoutInfo The lambda to provide layout information of the single item.
181 | * @param key A factory of stable and unique keys representing the item. Using the same key
182 | * for multiple items is not allowed. Type of the key should be saveable via Bundle on Android.
183 | * If null is passed the position in the list will represent the key.
184 | * @param contentType A factory of the content types for the item. The item compositions of
185 | * the same type could be reused more efficiently. Note that null is a valid type and items of
186 | * such type will be considered compatible.
187 | * @param itemContent The content displayed by a single item.
188 | */
189 | public fun timeline(
190 | items: List,
191 | layoutInfo: (item: T) -> Timeline,
192 | key: ((item: T) -> Any)? = null,
193 | contentType: (item: T) -> Any? = { null },
194 | itemContent: @Composable (item: T) -> Unit
195 | ): Unit = timeline(
196 | count = items.size,
197 | layoutInfo = { index: Int -> layoutInfo(items[index]) },
198 | key = if (key != null) { index: Int -> key(items[index]) } else null,
199 | contentType = { index: Int -> contentType(items[index]) },
200 | itemContent = { index: Int -> itemContent(items[index]) },
201 | )
202 | }
203 |
204 | /**
205 | * Implementation of the [MinaBoxScope] with [ProgramGuideItemContent] properties.
206 | */
207 | internal class ProgramGuideScopeImpl : ProgramGuideScope {
208 |
209 | override var guideStartHour: Float = 0f
210 |
211 | /**
212 | * Registered programs.
213 | */
214 | var programsContent: ProgramGuideItemContent? = null
215 | private set
216 |
217 | /**
218 | * Registered current time vertical line.
219 | */
220 | var currentTimeContent: ProgramGuideItemContent? = null
221 | private set
222 |
223 | /**
224 | * Registered channels.
225 | */
226 | var channelsContent: ProgramGuideItemContent? = null
227 | private set
228 |
229 | /**
230 | * Registered timeline items.
231 | */
232 | var timelinesContent: ProgramGuideItemContent? = null
233 | private set
234 |
235 | /**
236 | * Registered top corner.
237 | */
238 | var topCornerContent: ProgramGuideItemContent? = null
239 | private set
240 |
241 | override fun programs(
242 | count: Int,
243 | layoutInfo: (index: Int) -> Program,
244 | key: ((index: Int) -> Any)?,
245 | contentType: (index: Int) -> Any?,
246 | itemContent: @Composable (index: Int) -> Unit
247 | ) {
248 | programsContent = ProgramGuideItemContent(count, layoutInfo, key, contentType, itemContent)
249 | }
250 |
251 | override fun currentTime(
252 | layoutInfo: () -> CurrentTime,
253 | key: (() -> Any)?,
254 | contentType: () -> Any?,
255 | itemContent: @Composable () -> Unit
256 | ) {
257 | currentTimeContent = singleItemContent(layoutInfo, key, contentType, itemContent)
258 | }
259 |
260 | override fun channels(
261 | count: Int,
262 | layoutInfo: (index: Int) -> Channel,
263 | key: ((index: Int) -> Any)?,
264 | contentType: (index: Int) -> Any?,
265 | itemContent: @Composable (index: Int) -> Unit
266 | ) {
267 | channelsContent = ProgramGuideItemContent(count, layoutInfo, key, contentType, itemContent)
268 | }
269 |
270 | override fun timeline(
271 | count: Int,
272 | layoutInfo: (index: Int) -> Timeline,
273 | key: ((index: Int) -> Any)?,
274 | contentType: (index: Int) -> Any?,
275 | itemContent: @Composable (index: Int) -> Unit
276 | ) {
277 | timelinesContent = ProgramGuideItemContent(count, layoutInfo, key, contentType, itemContent)
278 | }
279 |
280 | override fun topCorner(
281 | key: (() -> Any)?,
282 | contentType: () -> Any?,
283 | itemContent: @Composable () -> Unit
284 | ) {
285 | topCornerContent = singleItemContent({ TopCorner() }, key, contentType, itemContent)
286 | }
287 |
288 | private fun singleItemContent(
289 | layoutInfo: () -> ProgramGuideItem,
290 | key: (() -> Any)?,
291 | contentType: () -> Any?,
292 | itemContent: @Composable () -> Unit
293 | ): ProgramGuideItemContent =
294 | ProgramGuideItemContent(
295 | count = 1,
296 | layoutInfo = { layoutInfo() },
297 | key = key?.let { { key.invoke() } },
298 | contentType = { contentType() },
299 | itemContent = { itemContent() }
300 | )
301 | }
302 |
303 | internal class ProgramGuideItemContent(
304 | val count: Int,
305 | val layoutInfo: (index: Int) -> ProgramGuideItem,
306 | val key: ((index: Int) -> Any)?,
307 | val contentType: (index: Int) -> Any?,
308 | val itemContent: @Composable (index: Int) -> Unit
309 | )
310 |
--------------------------------------------------------------------------------
/programguide/src/commonMain/kotlin/eu/wewox/programguide/ProgramGuideState.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Stable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.runtime.saveable.Saver
7 | import androidx.compose.runtime.saveable.listSaver
8 | import androidx.compose.runtime.saveable.rememberSaveable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.geometry.Offset
11 | import eu.wewox.minabox.MinaBoxState
12 |
13 | /**
14 | * Creates a [ProgramGuideState] that is remembered across compositions.
15 | *
16 | * @param initialOffset The lambda to provide initial offset on the plane.
17 | * @return Instance of the [ProgramGuideState].
18 | */
19 | @Deprecated(
20 | message = "Use rememberSaveableProgramGuideState() which uses rememberSaveable API.",
21 | replaceWith = ReplaceWith(
22 | "rememberSaveableProgramGuideState(initialOffset)",
23 | "eu.wewox.programguide.rememberSaveableProgramGuideState"
24 | )
25 | )
26 | @Composable
27 | public fun rememberProgramGuideState(
28 | initialOffset: ProgramGuidePositionProvider.() -> Offset = { Offset.Zero }
29 | ): ProgramGuideState {
30 | return remember { ProgramGuideState(initialOffset) }
31 | }
32 |
33 | /**
34 | * Creates a [ProgramGuideState] that is remembered across compositions and saved across activity or process recreation.
35 | *
36 | * @param initialOffset The lambda to provide initial offset on the plane.
37 | * @return Instance of the [ProgramGuideState].
38 | */
39 | @Composable
40 | public fun rememberSaveableProgramGuideState(
41 | initialOffset: ProgramGuidePositionProvider.() -> Offset = { Offset.Zero },
42 | ): ProgramGuideState {
43 | return rememberSaveable(
44 | saver = ProgramGuideState.Saver(),
45 | init = { ProgramGuideState(initialOffset) }
46 | )
47 | }
48 |
49 | /**
50 | * A state object that can be hoisted to control and observe scrolling.
51 | *
52 | * @property initialOffset The lambda to provide initial offset on the plane.
53 | */
54 | @Stable
55 | public class ProgramGuideState(
56 | internal val initialOffset: ProgramGuidePositionProvider.() -> Offset
57 | ) {
58 | internal lateinit var dimensions: ProgramGuidePxDimensions
59 | internal lateinit var indexMapper: ProgramGuideIndexMapper
60 |
61 | private val translateX: Float get() = minaBoxState.translate?.x ?: 0f
62 |
63 | private val translateY: Float get() = minaBoxState.translate?.y ?: 0f
64 |
65 | /**
66 | * The underlying [MinaBoxState] used to this state.
67 | */
68 | public val minaBoxState: MinaBoxState = MinaBoxState(
69 | initialOffset = {
70 | initialOffset(ProgramGuidePositionProviderImpl(this, dimensions, indexMapper))
71 | }
72 | )
73 |
74 | /**
75 | * The position provider used to get items offsets.
76 | */
77 | public val positionProvider: ProgramGuidePositionProvider
78 | get() = ProgramGuidePositionProviderImpl(
79 | positionProvider = minaBoxState.positionProvider,
80 | dimensions = dimensions,
81 | indexMapper = indexMapper
82 | )
83 |
84 | /**
85 | * Animates current offset to the program item with a given index.
86 | *
87 | * @param index The program index.
88 | * @param alignment The alignment to align item inside the [ProgramGuide].
89 | */
90 | public suspend fun animateToProgram(index: Int, alignment: Alignment = Alignment.Center) {
91 | val offset = positionProvider.getProgramOffset(index, alignment)
92 | minaBoxState.animateTo(offset.x, offset.y)
93 | }
94 |
95 | /**
96 | * Snaps current offset to the program item with a given index.
97 | *
98 | * @param index The program index.
99 | * @param alignment The alignment to align item inside the [ProgramGuide].
100 | */
101 | public suspend fun snapToProgram(index: Int, alignment: Alignment = Alignment.Center) {
102 | val offset = positionProvider.getProgramOffset(index, alignment)
103 | minaBoxState.snapTo(offset.x, offset.y)
104 | }
105 |
106 | /**
107 | * Animates current offset to the current time vertical line.
108 | *
109 | * @param alignment The alignment to align item inside the [ProgramGuide].
110 | */
111 | public suspend fun animateToCurrentTime(
112 | alignment: Alignment.Horizontal = Alignment.CenterHorizontally
113 | ) {
114 | val position = positionProvider.getCurrentTimePosition(alignment)
115 | minaBoxState.animateTo(position, translateY)
116 | }
117 |
118 | /**
119 | * Snaps current offset to the current time vertical line.
120 | *
121 | * @param alignment The alignment to align item inside the [ProgramGuide].
122 | */
123 | public suspend fun snapToCurrentTime(
124 | alignment: Alignment.Horizontal = Alignment.CenterHorizontally
125 | ) {
126 | val position = positionProvider.getCurrentTimePosition(alignment)
127 | minaBoxState.snapTo(position, translateY)
128 | }
129 |
130 | /**
131 | * Animates current offset to the channel item with a given index.
132 | *
133 | * @param index The program index.
134 | * @param alignment The alignment to align item inside the [ProgramGuide].
135 | */
136 | public suspend fun animateToChannel(
137 | index: Int,
138 | alignment: Alignment.Vertical = Alignment.CenterVertically
139 | ) {
140 | val position = positionProvider.getChannelPosition(index, alignment)
141 | minaBoxState.animateTo(translateX, position)
142 | }
143 |
144 | /**
145 | * Snaps current offset to the channel item with a given index.
146 | *
147 | * @param index The program index.
148 | * @param alignment The alignment to align item inside the [ProgramGuide].
149 | */
150 | public suspend fun snapToChannel(
151 | index: Int,
152 | alignment: Alignment.Vertical = Alignment.CenterVertically
153 | ) {
154 | val position = positionProvider.getChannelPosition(index, alignment)
155 | minaBoxState.snapTo(translateX, position)
156 | }
157 |
158 | /**
159 | * Animates current offset to the timeline item with a given index.
160 | *
161 | * @param index The program index.
162 | * @param alignment The alignment to align item inside the [ProgramGuide].
163 | */
164 | public suspend fun animateToTimeline(
165 | index: Int,
166 | alignment: Alignment.Horizontal = Alignment.CenterHorizontally
167 | ) {
168 | val position = positionProvider.getTimelinePosition(index, alignment)
169 | minaBoxState.animateTo(position, translateY)
170 | }
171 |
172 | /**
173 | * Snaps current offset to the timeline item with a given index.
174 | * Position on the Y axis is not changed.
175 | *
176 | * @param index The program index.
177 | * @param alignment The alignment to align item inside the [ProgramGuide].
178 | */
179 | public suspend fun snapToTimeline(
180 | index: Int,
181 | alignment: Alignment.Horizontal = Alignment.CenterHorizontally
182 | ) {
183 | val position = positionProvider.getTimelinePosition(index, alignment)
184 | minaBoxState.snapTo(position, translateY)
185 | }
186 |
187 | internal companion object {
188 |
189 | /**
190 | * Creates a [Saver] that can save and restore a [ProgramGuideState].
191 | *
192 | * @return A [Saver] instance for saving and restoring [ProgramGuideState].
193 | */
194 | fun Saver(): Saver = listSaver(
195 | save = {
196 | listOf(
197 | it.minaBoxState.translate?.x ?: 0f,
198 | it.minaBoxState.translate?.y ?: 0f,
199 | )
200 | },
201 | restore = {
202 | ProgramGuideState {
203 | Offset(it[0], it[1])
204 | }
205 | }
206 | )
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | includeBuild("build-logic")
3 | repositories {
4 | google()
5 | mavenCentral()
6 | gradlePluginPortal()
7 | }
8 | }
9 |
10 | plugins {
11 | id("org.gradle.toolchains.foojay-resolver-convention") version("0.7.0")
12 | }
13 |
14 | dependencyResolutionManagement {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | mavenLocal()
19 | }
20 | }
21 |
22 | rootProject.name = "ProgramGuide"
23 | include(":demo")
24 | include(":programguide")
25 | include(":desktopdemo")
26 | include(":androiddemo")
27 | include(":wasmdemo")
28 |
--------------------------------------------------------------------------------
/wasmdemo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/wasmdemo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
2 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
3 |
4 | plugins {
5 | alias(libs.plugins.kotlin.multiplatform)
6 | alias(libs.plugins.jetbrains.compose)
7 | alias(libs.plugins.compose.compiler)
8 | }
9 |
10 | kotlin {
11 | @OptIn(ExperimentalWasmDsl::class)
12 | wasmJs {
13 | moduleName = "programguide-demo"
14 | browser {
15 | commonWebpackConfig {
16 | outputFileName = "main.js"
17 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
18 | port = 8080
19 | static = (static ?: mutableListOf()).apply {
20 | add(project.rootDir.path)
21 | }
22 | }
23 | }
24 | }
25 | binaries.executable()
26 | }
27 | sourceSets {
28 | val wasmJsMain by getting {
29 | dependencies {
30 | implementation(project(":demo"))
31 | implementation(compose.ui)
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/wasmdemo/src/wasmJsMain/kotlin/eu/wewox/programguide/demo/Main.kt:
--------------------------------------------------------------------------------
1 | package eu.wewox.programguide.demo
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.window.CanvasBasedWindow
5 |
6 | @OptIn(ExperimentalComposeUiApi::class)
7 | fun main() {
8 | CanvasBasedWindow("Wasm Demo", canvasElementId = "canvas") {
9 | App()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/wasmdemo/src/wasmJsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Wasm Demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------