├── .gitignore
├── LICENSE.txt
├── README.md
├── android
├── build.gradle.kts
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── kotlin
│ └── com
│ │ └── displayer
│ │ └── android
│ │ ├── MainActivity.kt
│ │ └── app
│ │ └── DisplayerApp.kt
│ └── res
│ ├── drawable-xhdpi
│ └── banner.png
│ ├── drawable
│ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── ic_launcher_background.xml
│ └── strings.xml
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
├── settings.gradle
└── src
│ └── main
│ └── kotlin
│ └── AndroidSdk.kt
├── common
├── build.gradle.kts
└── src
│ ├── androidMain
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ └── com
│ │ │ └── displayer
│ │ │ └── platform
│ │ │ └── Platform.kt
│ └── res
│ │ ├── drawable-nodpi
│ │ ├── social_facebook.png
│ │ ├── social_instagram.png
│ │ ├── social_snapchat.png
│ │ ├── social_tiktok.png
│ │ ├── social_youtube_dark.png
│ │ └── social_youtube_light.png
│ │ └── drawable
│ │ ├── logo_white.xml
│ │ ├── severity_error.xml
│ │ ├── severity_info.xml
│ │ ├── severity_warning.xml
│ │ ├── weather_cloudy.xml
│ │ ├── weather_cloudy_snowing.xml
│ │ ├── weather_fog.xml
│ │ ├── weather_partly_cloudy.xml
│ │ ├── weather_rainy.xml
│ │ ├── weather_sunny.xml
│ │ ├── weather_thunderstorm.xml
│ │ └── wind_direction.xml
│ ├── commonMain
│ ├── i18n
│ │ ├── Strings_en.properties
│ │ └── Strings_nl.properties
│ └── kotlin
│ │ └── com
│ │ └── displayer
│ │ ├── Ext.kt
│ │ ├── Koin.kt
│ │ ├── admin
│ │ ├── AdminPanel.kt
│ │ ├── AdminServer.kt
│ │ └── AdminState.kt
│ │ ├── app
│ │ ├── App.kt
│ │ ├── AppState.kt
│ │ ├── AppUi.kt
│ │ └── InitializingScreen.kt
│ │ ├── config
│ │ ├── Config.kt
│ │ ├── ConfigRepo.kt
│ │ ├── Parameters.kt
│ │ └── ParametersRepo.kt
│ │ ├── display
│ │ ├── DefaultDisplayFile.kt
│ │ ├── Display.kt
│ │ ├── DisplayRepo.kt
│ │ ├── DisplayState.kt
│ │ ├── DisplayUi.kt
│ │ ├── ErrorUi.kt
│ │ ├── MainUi.kt
│ │ ├── container
│ │ │ ├── ColumnLayout.kt
│ │ │ ├── Container.kt
│ │ │ ├── Empty.kt
│ │ │ ├── RowLayout.kt
│ │ │ └── Stacked.kt
│ │ ├── item
│ │ │ ├── Clock.kt
│ │ │ ├── Drink.kt
│ │ │ ├── Image.kt
│ │ │ ├── Item.kt
│ │ │ ├── RandomItem.kt
│ │ │ ├── Social.kt
│ │ │ ├── TextItem.kt
│ │ │ ├── UnknownItem.kt
│ │ │ └── Weather.kt
│ │ └── parser
│ │ │ ├── Dto.kt
│ │ │ ├── Message.kt
│ │ │ ├── Parser.kt
│ │ │ ├── Result.kt
│ │ │ └── Severity.kt
│ │ ├── platform
│ │ └── Platform.kt
│ │ ├── ui
│ │ ├── Icon.kt
│ │ ├── Message.kt
│ │ ├── Styling.kt
│ │ └── Text.kt
│ │ └── weather
│ │ ├── WeatherData.kt
│ │ ├── WeatherRepo.kt
│ │ ├── WeatherState.kt
│ │ └── api
│ │ └── Model.kt
│ ├── commonTest
│ └── kotlin
│ │ └── com
│ │ └── displayer
│ │ ├── ColorIsLightTest.kt
│ │ └── ColorParseTest.kt
│ ├── desktopMain
│ ├── kotlin
│ │ └── com
│ │ │ └── displayer
│ │ │ ├── app
│ │ │ └── DesktopApp.kt
│ │ │ └── platform
│ │ │ └── Platform.kt
│ └── resources
│ │ ├── logo_white.svg
│ │ ├── severity_error.svg
│ │ ├── severity_info.svg
│ │ ├── severity_warning.svg
│ │ ├── social_facebook.png
│ │ ├── social_instagram.png
│ │ ├── social_snapchat.png
│ │ ├── social_tiktok.png
│ │ ├── social_youtube_dark.png
│ │ ├── social_youtube_light.png
│ │ ├── weather_cloudy.svg
│ │ ├── weather_cloudy_snowing.svg
│ │ ├── weather_fog.svg
│ │ ├── weather_partly_cloudy.svg
│ │ ├── weather_rainy.svg
│ │ ├── weather_sunny.svg
│ │ ├── weather_thunderstorm.svg
│ │ └── wind_direction.svg
│ ├── jsMain
│ ├── kotlin
│ │ └── com
│ │ │ └── displayer
│ │ │ ├── JsKoin.kt
│ │ │ ├── admin
│ │ │ └── NoopAdminServer.kt
│ │ │ ├── browser
│ │ │ ├── BrowserViewportWindow.kt
│ │ │ └── Main.kt
│ │ │ └── platform
│ │ │ └── Platform.kt
│ └── resources
│ │ └── index.html
│ └── jvmMain
│ └── kotlin
│ └── com
│ └── displayer
│ ├── JvmKoin.kt
│ └── admin
│ └── HttpAdminServer.kt
├── desktop
├── build.gradle.kts
├── icon.png
└── src
│ └── jvmMain
│ └── kotlin
│ └── Main.kt
├── docs
├── graphics
│ └── feature-graphic.png
├── samples
│ ├── images.json
│ ├── images.png
│ ├── index.md
│ ├── locale_en_US.json
│ ├── locale_en_US.png
│ ├── locale_fi_FI.json
│ ├── locale_fi_FI.png
│ ├── locale_nl_BE.json
│ ├── locale_nl_BE.png
│ ├── not-json.png
│ ├── not-json.txt
│ ├── problems.json
│ ├── problems.png
│ ├── regions-alternative.json
│ ├── regions-alternative.png
│ ├── regions-images.json
│ ├── regions.json
│ ├── regions.png
│ ├── socials.json
│ ├── socials.png
│ ├── styles-items.json
│ ├── styles-items.png
│ ├── styles-regions.json
│ ├── styles-regions.png
│ ├── text.json
│ ├── text.png
│ ├── unknown-types.json
│ ├── unknown-types.png
│ ├── weather_betekom_BE.json
│ ├── weather_betekom_BE.png
│ ├── weather_rochester_MN.json
│ └── weather_rochester_MN.png
└── screenshots
│ ├── screenshot-1.png
│ └── screenshot-2.png
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .idea
3 | local.properties
4 | *.iml
5 | **/build
6 | captures
7 | /kotlin-js-store
8 | .cache
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Displayer
3 |
4 | [](http://www.apache.org/licenses/LICENSE-2.0)
5 | [](http://kotlinlang.org)
6 | [](http://kotlinlang.org)
7 |
8 | An open source multi-platform app for digital signage
9 |
10 | 
11 |
12 | ## Table of contents
13 |
14 | * [Introduction](#introduction)
15 | * [Status](#status)
16 | * [Screenshots](#screenshots)
17 | * [Build and run](#build-and-run)
18 | * [Android](#android)
19 | * [Desktop](#desktop)
20 | * [Display file structure](#display-file-structure)
21 | * [Admin server](#admin-server)
22 | * [Start the admin server](#start-the-admin-server)
23 | * [Load a remote display file](#load-a-remote-display-file)
24 | * [Upload a local display file](#upload-a-local-display-file)
25 | * [Set OpenWeather API key](#set-openweather-api-key)
26 | * [Stop the admin server](#stop-the-admin-server)
27 | * [Android intents](#android-intents)
28 | * [Load a remote display file](#load-a-remote-display-file-1)
29 | * [Set OpenWeather API key](#set-openweather-api-key-1)
30 | * [Stop the admin server](#stop-the-admin-server-1)
31 | * [Android TV launcher](#android-tv-launcher)
32 | * [Built with](#built-with)
33 | * [License](#license)
34 |
35 | ## Introduction
36 |
37 | The _Displayer_ app shows your content, described in a JSON file, in an endless loop, without any intervention, on the device of your choice.
38 |
39 | Some of its features are:
40 | * Works on multiple platforms:
41 | * Android, including Android TV
42 | * Linux
43 | * Any other platform supported by Kotlin Multiplatform (KMP)
44 | * Multiple _regions_
45 | * _Center_ region to show your main content
46 | * _Left_ and _Bottom_ region to show secondary (scrolling) content
47 | * Different types of content:
48 | * Text
49 | * Images
50 | * Clock
51 | * Live weather information (requires an OpenWeather API key)
52 | * Random content
53 | * Locale-aware (for time formatting, temperature units,...)
54 | * Is a _launcher_ on Android platforms (including Android TV)
55 |
56 | Displayer is being used successfully in the clubhouse of [korfbalclub KCBJ](https://www.kcbj.be/) to display information about upcoming events, sponsors, and live weather, without any intervention of the volunteer currently doing bar service.
57 |
58 | ## Status
59 |
60 | This project is currently under heavy development and should be considered _alpha-quality_ at best.
61 |
62 | ## Screenshots
63 |
64 | 
65 | 
66 |
67 | [](https://www.youtube.com/watch?v=r9NUL9Qbw-c "Watch screen recording")
68 |
69 | ## Build and run
70 |
71 | There are currently no binary builds available. You have to download/clone the source code and build the app to run it.
72 |
73 | Building Displayer requires at minimum Java 17.
74 |
75 | Optionally, you can open the project in IntelliJ IDEA (tested with version 2023.3.3 of the Community Edition running on Java 17)
76 |
77 | ### Android
78 |
79 | * Define `sdk.dir` in `local.properties` to point to your Android SDK location
80 | * Execute `./gradlew assemble` to build the Android app
81 | * The resulting APK files will be placed in `android/build/outputs/apk`
82 | * Use [ADB](https://developer.android.com/studio/command-line/adb) to install the app on your Android device: `adb install android/build/outputs/apk/debug/android-debug.apk`
83 |
84 | ### Desktop
85 |
86 | * Execute `./gradlew package` to build the desktop apps
87 | * The resulting binaries will be placed in `desktop/build/compose/binaries`
88 |
89 | ## Display file structure
90 |
91 | There are [sample display files](docs/samples/index.md) available showcasing different features.
92 |
93 | A detailed explanation of the display file structure is coming soon...
94 |
95 | ## Admin server
96 |
97 | Displayer has a built-in webserver that can be used to send commands and control the app from a remote device.
98 | This server accepts commands sent via `curl`, `wget` or any web browser.
99 |
100 | The admin server is *not* enabled by default.
101 |
102 | ### Start the admin server
103 |
104 | #### Android
105 |
106 | On Android, you'll need [ADB](https://developer.android.com/studio/command-line/adb) to start the admin server with the following command:
107 |
108 | ```
109 | adb shell am start -a com.displayer.action.CONFIG -e com.displayer.extra.ADMIN_PORT YOUR_PORT -e com.displayer.extra.ADMIN_SECRET YOUR_SECRET
110 | ```
111 |
112 | Replace YOUR_PORT with the TCP/IP port number to use for the admin server.
113 |
114 | Replace YOUR_SECRET with a secret that must be included in any request to the admin port.
115 |
116 | #### Desktop
117 |
118 | On desktop, you must start Displayer with extra parameters to enable the admin server.
119 |
120 | Example:
121 | ```
122 | /opt/displayer/bin/Displayer --admin-port=YOUR_PORT --admin-secret=YOUR_SECRET
123 | ```
124 |
125 | Replace YOUR_PORT with the TCP/IP port number to use for the admin server.
126 |
127 | Replace YOUR_SECRET with a secret that must be included in any request to the admin port.
128 |
129 | ### Load a remote display file
130 |
131 | ```
132 | curl "http://YOUR_HOST:YOUR_PORT/admin?secret=YOUR_SECRET&url=URL_OF_DISPLAY_FILE"
133 | ```
134 |
135 | Replace URL_OF_DISPLAY_FILE with the URL of your own display file, e.g. https://example.com/displayer.json.
136 | Make sure you URL-encode all special characters in the URL.
137 |
138 | ### Upload a local display file
139 |
140 | ```
141 | curl -F 'data=@PATH_OF_DISPLAY_FILE' "http://YOUR_HOST:YOUR_PORT/admin/display?secret=YOUR_SECRET"
142 | ```
143 |
144 | Replace PATH_OF_DISPLAY_FILE with the path of a local display file that will be uploaded.
145 |
146 | ### Set OpenWeather API key
147 |
148 | ```
149 | curl "http://YOUR_HOST:YOUR_PORT/admin?secret=YOUR_SECRET&open-weather-api-key=YOUR_API_KEY"
150 | ```
151 |
152 | Replace YOUR_API_KEY with your own OpenWeather API key obtained from the [OpenWeather API console](https://home.openweathermap.org/api_keys).
153 |
154 | ### Stop the admin server
155 |
156 | ```
157 | curl "http://YOUR_HOST:YOUR_PORT/admin?secret=YOUR_SECRET&kill-server"
158 | ```
159 |
160 | ## Android intents
161 |
162 | The Android app also accepts a number of _intents with extras_.
163 |
164 | You can use [ADB](https://developer.android.com/studio/command-line/adb) to launch these intents.
165 |
166 | ### Load a remote display file
167 | ```
168 | adb shell am start -a android.intent.action.VIEW -d "URL_OF_DISPLAY_FILE"
169 | ```
170 |
171 | Replace URL_OF_DISPLAY_FILE with the URL of your own display file, e.g. https://example.com/displayer.json
172 |
173 | ### Set OpenWeather API key
174 |
175 | ```
176 | adb shell am start -a com.displayer.action.CONFIG -e EXTRA_OPEN_WEATHER_API_KEY YOUR_API_KEY
177 | ```
178 |
179 | Replace YOUR_API_KEY with your own OpenWeather API key obtained from the [OpenWeather API console](https://home.openweathermap.org/api_keys).
180 |
181 | ### Stop the admin server
182 |
183 | ```
184 | adb shell am start -a com.displayer.action.KILL_SERVER
185 | ```
186 |
187 | ## Android TV launcher
188 |
189 | After installing Displayer on Android TV, you can disable the original Android TV launcher.
190 | This will make sure that Displayer automatically starts after rebooting your Android TV device.
191 |
192 | To disable the original Android TV launcher enter the following commands:
193 |
194 | ```
195 | adb shell pm disable-user --user 0 com.google.android.tvlauncher
196 | adb shell pm disable-user --user 0 com.google.android.tungsten.setupwraith
197 | ```
198 |
199 | You can always re-enable the original Android TV launcher by entering the following commands:
200 |
201 | ```
202 | adb shell pm enable com.google.android.tvlauncher
203 | adb shell pm enable com.google.android.tungsten.setupwraith
204 | ```
205 |
206 | ## Built with
207 |
208 | * [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html)
209 | * [Compose Desktop](https://www.jetbrains.com/lp/compose-desktop/)
210 | * [Kotlin Serialization](https://kotlinlang.org/docs/serialization.html)
211 | * [Kotlin Date/Time](https://github.com/Kotlin/kotlinx-datetime)
212 | * [Ktor](https://ktor.io/): HTTP server
213 | * [Koin](https://github.com/InsertKoinIO/koin): dependency injection framework
214 | * [Kamel](https://github.com/alialbaali/Kamel): asynchronous media loading library for Compose
215 | * [Multiplatform settings](https://github.com/russhwolf/multiplatform-settings): Kotlin Multiplatform library to persist key-value data
216 | * [Kermit](https://github.com/touchlab/Kermit): Kotlin Multiplatform logging
217 |
218 | # License
219 |
220 | Copyright 2022 Norio BV
221 |
222 | Licensed under the Apache License, Version 2.0 (the "License");
223 | you may not use this file except in compliance with the License.
224 | You may obtain a copy of the License at
225 |
226 | http://www.apache.org/licenses/LICENSE-2.0
227 |
228 | Unless required by applicable law or agreed to in writing, software
229 | distributed under the License is distributed on an "AS IS" BASIS,
230 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
231 | See the License for the specific language governing permissions and
232 | limitations under the License.
233 |
--------------------------------------------------------------------------------
/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | plugins {
4 | id("org.jetbrains.compose")
5 | id("com.android.application")
6 | kotlin("android")
7 | }
8 |
9 | repositories {
10 | jcenter()
11 | }
12 |
13 | dependencies {
14 | implementation(project(":common"))
15 | implementation(libs.androidx.activity.compose)
16 | }
17 |
18 | fun isBuildServer() = System.getenv().containsKey("CI") || System.getenv().containsKey("BUILD_NUMBER")
19 |
20 | android {
21 |
22 | compileSdk = AndroidSdk.compile
23 |
24 | defaultConfig {
25 | applicationId = "com.displayer"
26 | namespace = "com.displayer.android"
27 | minSdk = AndroidSdk.min
28 | targetSdk = AndroidSdk.target
29 | versionCode = 1
30 | versionName = "0.1"
31 |
32 | resourceConfigurations += "en"
33 |
34 | if (isBuildServer()) {
35 | setProperty("archivesBaseName", "$namespace-$versionName-$versionCode")
36 | }
37 | }
38 |
39 | buildTypes {
40 | named("debug").configure {
41 | isMinifyEnabled = false
42 | }
43 | named("release").configure {
44 | isMinifyEnabled = false
45 | }
46 | }
47 |
48 | compileOptions {
49 | sourceCompatibility = JavaVersion.VERSION_17
50 | targetCompatibility = JavaVersion.VERSION_17
51 | }
52 |
53 | kotlinOptions {
54 | jvmTarget = JavaVersion.VERSION_17.toString()
55 | freeCompilerArgs = listOf(
56 | "-Xallow-unstable-dependencies"
57 | )
58 | }
59 |
60 | lint {
61 | abortOnError = false
62 | }
63 | }
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
12 |
13 |
21 |
22 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/android/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/android/src/main/kotlin/com/displayer/android/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.android
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import android.view.WindowManager
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.compose.runtime.collectAsState
10 | import androidx.compose.runtime.getValue
11 | import androidx.lifecycle.lifecycleScope
12 | import co.touchlab.kermit.Logger
13 | import com.displayer.app.App
14 | import com.displayer.app.AppState
15 | import com.displayer.app.AppUi
16 | import org.koin.android.ext.android.inject
17 |
18 | class MainActivity : ComponentActivity() {
19 |
20 | private val app by inject()
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
25 | setContent {
26 | val display by app.observeState().collectAsState(AppState.Initializing)
27 | AppUi(display)
28 | }
29 | handleIntent(intent)
30 | }
31 |
32 | override fun onResume() {
33 | super.onResume()
34 | hideSystemUI()
35 | }
36 |
37 | override fun onNewIntent(intent: Intent?) {
38 | super.onNewIntent(intent)
39 | handleIntent(intent)
40 | }
41 |
42 | private fun handleIntent(intent: Intent?) {
43 | if (intent == null) {
44 | Logger.w { "Ignoring null Intent" }
45 | return
46 | }
47 | Logger.i { "Handling intent with action ${intent.action} and data ${intent.data}" }
48 | when (intent.action) {
49 | Intent.ACTION_MAIN -> lifecycleScope.launchWhenStarted { app.loadDisplayFromUrl(null) }
50 | Intent.ACTION_VIEW -> lifecycleScope.launchWhenStarted { app.loadDisplayFromUrl(intent.data?.toString()) }
51 | "com.displayer.action.KILL_SERVER" -> app.stopAdminServer()
52 | "com.displayer.action.CONFIG" -> intent.extras?.let { handleConfigIntent(it) }
53 | }
54 | }
55 |
56 | private fun handleConfigIntent(extras: Bundle) {
57 | Logger.d { "Handling config intent" }
58 | extras.getString("com.displayer.extra.OPEN_WEATHER_API_KEY")?.let { app.setOpenWeatherApiKey(it) }
59 |
60 | val extraPort: Int? = extras.getString("com.displayer.extra.ADMIN_PORT")?.toIntOrNull()
61 | val extraSecret: String? = extras.getString("com.displayer.extra.ADMIN_SECRET")
62 | app.setAdminParameters(extraPort, extraSecret)
63 | }
64 |
65 | private fun hideSystemUI() {
66 | window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
67 | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
68 | or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
69 | or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
70 | or View.SYSTEM_UI_FLAG_FULLSCREEN
71 | or View.SYSTEM_UI_FLAG_IMMERSIVE
72 | or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/android/src/main/kotlin/com/displayer/android/app/DisplayerApp.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.android.app
2 |
3 | import android.app.Application
4 | import com.displayer.coreModule
5 | import com.displayer.jvmModule
6 | import org.koin.android.ext.koin.androidContext
7 | import org.koin.android.ext.koin.androidLogger
8 | import org.koin.core.context.startKoin
9 | import org.koin.core.logger.Level
10 |
11 | class DisplayerApp : Application() {
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 | startKoin {
16 | androidContext(this@DisplayerApp)
17 | modules(listOf(coreModule, jvmModule))
18 | androidLogger(Level.INFO)
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/android/src/main/res/drawable-xhdpi/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/drawable-xhdpi/banner.png
--------------------------------------------------------------------------------
/android/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #1E9991
4 |
--------------------------------------------------------------------------------
/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Displayer
3 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
6 | maven("https://maven.universablockchain.com/")
7 | }
8 | }
9 |
10 | plugins {
11 | kotlin("multiplatform") apply false
12 | kotlin("plugin.serialization") apply false
13 | kotlin("android") apply false
14 | id("com.android.application") apply false
15 | id("com.android.library") apply false
16 | id("org.jetbrains.compose") apply false
17 | }
18 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 |
--------------------------------------------------------------------------------
/buildSrc/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | versionCatalogs {
3 | create("libs") {
4 | from(files("../gradle/libs.versions.toml"))
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/AndroidSdk.kt:
--------------------------------------------------------------------------------
1 |
2 | object AndroidSdk {
3 | const val min = 21
4 | const val compile = 34
5 | const val target = 34
6 | }
7 |
--------------------------------------------------------------------------------
/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
4 | import java.time.OffsetDateTime
5 | import java.time.format.DateTimeFormatter;
6 |
7 | plugins {
8 | kotlin("multiplatform")
9 | kotlin("plugin.serialization")
10 | id("org.jetbrains.compose")
11 | id("com.android.library")
12 | id("de.comahe.i18n4k") version "0.7.0"
13 | id("com.codingfeline.buildkonfig") version "0.13.3"
14 | }
15 |
16 | group = "com.displayer"
17 | version = "0.1"
18 |
19 | i18n4k {
20 | sourceCodeLocales = listOf("en", "nl")
21 | }
22 |
23 | buildkonfig {
24 | packageName = "com.displayer"
25 | defaultConfigs {
26 | buildConfigField(STRING, "APP_VERSON_NAME", version as String)
27 | buildConfigField(STRING, "BUILD_TIME", DateTimeFormatter.ISO_INSTANT.format(OffsetDateTime.now()) as String)
28 | }
29 | }
30 |
31 | kotlin {
32 | android()
33 | jvm("desktop")
34 |
35 | sourceSets {
36 | val commonMain by getting {
37 | dependencies {
38 |
39 | api(compose.runtime)
40 | api(compose.foundation)
41 |
42 | implementation(libs.kotlinx.serialization.json)
43 | implementation(libs.kotlinx.collections.immutable)
44 | implementation(libs.kotlinx.datetime)
45 |
46 | implementation(libs.kamel.image)
47 |
48 | implementation(libs.ktor.client.content.negotiation)
49 | implementation(libs.ktor.serialization.kotlinx.json)
50 |
51 | implementation(libs.multiplatform.settings.no.arg)
52 |
53 | implementation(libs.koin.core)
54 |
55 | api(libs.kermit)
56 |
57 | implementation(libs.i18n4k.core)
58 |
59 | implementation(libs.mp.stools)
60 |
61 | implementation(libs.okio)
62 | }
63 | }
64 | val commonTest by getting {
65 | dependencies {
66 | implementation(kotlin("test"))
67 | }
68 | }
69 | val androidMain by getting {
70 | kotlin.srcDirs("src/jvmMain/kotlin")
71 | dependencies {
72 | api(libs.koin.android)
73 | implementation(libs.ktor.client.cio)
74 | implementation(libs.ktor.server.core)
75 | implementation(libs.ktor.server.cio)
76 | implementation(libs.slf4j.slf4j.nop)
77 | }
78 | }
79 | val androidUnitTest by getting {
80 | dependencies {
81 | implementation(libs.junit)
82 | }
83 | }
84 | val desktopMain by getting {
85 | kotlin.srcDirs("src/jvmMain/kotlin")
86 | dependencies {
87 | api(compose.preview)
88 | implementation(libs.ktor.client.cio)
89 | implementation(libs.ktor.server.core)
90 | implementation(libs.ktor.server.cio)
91 | implementation(libs.slf4j.slf4j.nop)
92 | }
93 | }
94 | val desktopTest by getting
95 | }
96 | }
97 |
98 | android {
99 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
100 |
101 | compileSdk = AndroidSdk.compile
102 |
103 | defaultConfig {
104 | minSdk = AndroidSdk.min
105 | namespace = "com.displayer"
106 | }
107 |
108 | compileOptions {
109 | sourceCompatibility = JavaVersion.VERSION_17
110 | targetCompatibility = JavaVersion.VERSION_17
111 | }
112 |
113 | buildFeatures {
114 | buildConfig = true
115 | }
116 | }
117 |
118 | compose.experimental {
119 | web.application {}
120 | }
121 |
122 | tasks.withType {
123 | kotlinOptions {
124 | freeCompilerArgs = freeCompilerArgs + listOf(
125 | "-opt-in=kotlin.RequiresOptIn"
126 | )
127 | jvmTarget = JavaVersion.VERSION_17.toString()
128 | }
129 | }
130 |
131 | dependencies {
132 | implementation(libs.compose.ui)
133 | }
134 |
--------------------------------------------------------------------------------
/common/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/common/src/androidMain/kotlin/com/displayer/platform/Platform.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.platform
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.painter.Painter
6 | import androidx.compose.ui.res.painterResource
7 | import androidx.compose.ui.text.intl.Locale
8 | import com.displayer.R
9 | import com.displayer.ui.Icon
10 | import kotlinx.datetime.Instant
11 | import okio.FileSystem
12 | import okio.Path
13 | import okio.Path.Companion.toPath
14 | import org.koin.java.KoinJavaComponent.get
15 | import java.io.File
16 | import java.text.SimpleDateFormat
17 | import java.util.*
18 |
19 | @Composable
20 | actual fun getIcon(icon: Icon): Painter = painterResource(
21 | when (icon) {
22 | Icon.Empty -> 0
23 | Icon.Logo -> R.drawable.logo_white
24 | Icon.SeverityInfo -> R.drawable.severity_info
25 | Icon.SeverityWarning -> R.drawable.severity_warning
26 | Icon.SeverityError -> R.drawable.severity_error
27 | Icon.WindDirection -> R.drawable.wind_direction
28 | Icon.WeatherSunny -> R.drawable.weather_sunny
29 | Icon.WeatherPartlyCloudy -> R.drawable.weather_partly_cloudy
30 | Icon.WeatherCloudy -> R.drawable.weather_cloudy
31 | Icon.WeatherRainy -> R.drawable.weather_rainy
32 | Icon.WeatherThunderstorm -> R.drawable.weather_thunderstorm
33 | Icon.WeatherCloudySnowing -> R.drawable.weather_cloudy_snowing
34 | Icon.WeatherFog -> R.drawable.weather_fog
35 | Icon.Facebook -> R.drawable.social_facebook
36 | Icon.Instagram -> R.drawable.social_instagram
37 | Icon.TikTok -> R.drawable.social_tiktok
38 | Icon.YouTubeDark -> R.drawable.social_youtube_dark
39 | Icon.YouTubeLight -> R.drawable.social_youtube_light
40 | Icon.Snapchat -> R.drawable.social_snapchat
41 | }
42 | )
43 |
44 | actual fun formatTime(instant: Instant, locale: Locale): String {
45 | val df = SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT, java.util.Locale.forLanguageTag(locale.toLanguageTag()))
46 | return df.format(Date(instant.toEpochMilliseconds()))
47 | }
48 |
49 | actual fun getDefaultLanguage() = Locale.current.language
50 |
51 | actual fun getDefaultCountry() = Locale.current.region
52 |
53 | actual fun getFilesystem(): FileSystem = FileSystem.SYSTEM
54 |
55 | actual fun getDisplayCachePath(): Path = File(get(Context::class.java).cacheDir,"display-cache.json").absolutePath.toPath()
56 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable-nodpi/social_facebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/common/src/androidMain/res/drawable-nodpi/social_facebook.png
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable-nodpi/social_instagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/common/src/androidMain/res/drawable-nodpi/social_instagram.png
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable-nodpi/social_snapchat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/common/src/androidMain/res/drawable-nodpi/social_snapchat.png
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable-nodpi/social_tiktok.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/common/src/androidMain/res/drawable-nodpi/social_tiktok.png
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable-nodpi/social_youtube_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/common/src/androidMain/res/drawable-nodpi/social_youtube_dark.png
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable-nodpi/social_youtube_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/litrik/displayer/cad2f8a574911233d1a14f190c0b03a5ff4e8ed5/common/src/androidMain/res/drawable-nodpi/social_youtube_light.png
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/logo_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/severity_error.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/severity_info.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/severity_warning.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/weather_cloudy.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/weather_cloudy_snowing.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/weather_fog.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/weather_partly_cloudy.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/weather_rainy.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/weather_sunny.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/weather_thunderstorm.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/androidMain/res/drawable/wind_direction.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/common/src/commonMain/i18n/Strings_en.properties:
--------------------------------------------------------------------------------
1 | adminLabelStatus=Status
2 | adminLabelUrl=URL
3 | adminLabelVersion=Version
4 | adminSectionApp=App
5 | adminSectionAdmin=Admin server
6 | adminLabelStopped=Stopped
7 | adminLabelRunning=Running on port {0}
8 | adminSectionDisplay=Display
9 | adminSectionWeather=Weather
10 | adminWeatherApiKeyInvalid=Invalid API key
11 | adminWeatherApiKeyNotSet=No API key set
12 | adminStatusFailure=Problem
13 | adminStatusSuccess=OK
14 | errorTitle=Something went wrong
15 | loading=Loading...
16 | windSpeedImperial=mph
17 | windSpeedMetric=km/h
--------------------------------------------------------------------------------
/common/src/commonMain/i18n/Strings_nl.properties:
--------------------------------------------------------------------------------
1 | adminLabelStatus=Status
2 | adminLabelUrl=URL
3 | adminLabelVersion=Versie
4 | adminSectionApp=App
5 | adminSectionAdmin=Admin server
6 | adminLabelStopped=Gestopt
7 | adminLabelRunning=Actief op poort {0}
8 | adminSectionDisplay=Display
9 | adminSectionWeather=Weer
10 | adminWeatherApiKeyInvalid=Ongeldige API key
11 | adminWeatherApiKeyNotSet=API key niet ingesteld
12 | adminStatusFailure=Probleem
13 | adminStatusSuccess=OK
14 | errorTitle=Er ging iets fout
15 | loading=Laden...
16 | windSpeedImperial=mpu
17 | windSpeedMetric=km/u
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/Ext.kt:
--------------------------------------------------------------------------------
1 | package com.displayer
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.luminance
6 | import androidx.compose.ui.platform.LocalDensity
7 | import androidx.compose.ui.unit.Dp
8 |
9 | fun Color.Companion.parse(colorString: String): Color =
10 | with(colorString.removePrefix("#")) {
11 | try {
12 | when (length) {
13 | 8 -> {
14 | val color = (toLong(16) and 0xFFFFFF00) shr 8
15 | val alpha = toLong(16) and 0xFF
16 | Color(alpha shl 24 or color)
17 | }
18 | 6 -> Color(toLong(16) or 0x00000000FF000000)
19 | else -> Unspecified
20 | }
21 | } catch (e: Exception) {
22 | Unspecified
23 | }
24 | }
25 |
26 | fun Color.isLight(): Boolean = luminance() > 0.179f
27 |
28 | @Composable
29 | fun Dp.toSp() = with(LocalDensity.current) { this@toSp.toSp() }
30 |
31 | @Composable
32 | fun Dp.toPx(): Float = with(LocalDensity.current) { this@toPx.toPx() }
33 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/Koin.kt:
--------------------------------------------------------------------------------
1 | package com.displayer
2 |
3 | import com.displayer.app.App
4 | import com.displayer.config.ConfigRepo
5 | import com.displayer.config.ParametersRepo
6 | import com.displayer.display.DisplayRepo
7 | import com.displayer.display.parser.ContainerDto
8 | import com.displayer.display.parser.ItemDto
9 | import com.displayer.display.parser.StackDto
10 | import com.displayer.display.parser.UnknownDto
11 | import com.displayer.weather.WeatherRepo
12 | import com.russhwolf.settings.Settings
13 | import kotlinx.serialization.json.Json
14 | import kotlinx.serialization.modules.SerializersModule
15 | import kotlinx.serialization.modules.polymorphic
16 | import org.koin.dsl.module
17 |
18 | val coreModule = module {
19 | single {
20 | Json {
21 | prettyPrint = true
22 | isLenient = true
23 | ignoreUnknownKeys = true
24 | serializersModule = SerializersModule {
25 | polymorphic(ContainerDto::class) {
26 | defaultDeserializer { StackDto.serializer() }
27 | }
28 | polymorphic(ItemDto::class) {
29 | defaultDeserializer { UnknownDto.serializer() }
30 | }
31 | }
32 | }
33 | }
34 | single { Settings() }
35 | single { ParametersRepo() }
36 | single { ConfigRepo(get(), get()) }
37 | single { DisplayRepo(get(), get(), get(), get(), get()) }
38 | single { WeatherRepo(get(), get(), get()) }
39 | single { App(get(), get(), get(), get()) }
40 | }
41 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/admin/AdminPanel.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.admin
2 |
3 | import Strings
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.Arrangement.spacedBy
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.CompositionLocalProvider
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.text.TextStyle
19 | import androidx.compose.ui.text.font.FontWeight
20 | import com.displayer.BuildKonfig
21 | import com.displayer.app.AppState
22 | import com.displayer.display.DisplayState
23 | import com.displayer.platform.formatTime
24 | import com.displayer.platform.getIcon
25 | import com.displayer.toSp
26 | import com.displayer.ui.DisplayerColor
27 | import com.displayer.ui.Icon
28 | import com.displayer.ui.LocalDimensions
29 | import com.displayer.ui.LocalLocale
30 | import com.displayer.ui.LocalStyle
31 | import com.displayer.ui.Message
32 | import com.displayer.ui.Text
33 | import com.displayer.weather.WeatherState
34 |
35 | @Composable
36 | fun AdminPanel(state: AppState) {
37 | Column(
38 | modifier = Modifier
39 | .fillMaxHeight()
40 | .fillMaxWidth(0.5f)
41 | .background(Color.Black),
42 | ) {
43 | Row(
44 | modifier = Modifier.fillMaxWidth().background(DisplayerColor).padding(LocalDimensions.current.baseUnit),
45 | verticalAlignment = Alignment.CenterVertically,
46 | horizontalArrangement = spacedBy(LocalDimensions.current.baseUnit)
47 | ) {
48 | Image(getIcon(Icon.Logo), null, modifier = Modifier.size(LocalDimensions.current.baseUnit * 2))
49 | Text("Displayer")
50 | }
51 | Column(
52 | Modifier.padding(LocalDimensions.current.baseUnit),
53 | verticalArrangement = spacedBy(LocalDimensions.current.baseUnit * 0.5f)
54 | ) {
55 |
56 | SectionTitle(Strings.adminSectionApp.toString())
57 |
58 | Text("${Strings.adminLabelVersion}: ${BuildKonfig.APP_VERSON_NAME} (${BuildKonfig.BUILD_TIME})")
59 |
60 | if (state is AppState.Ready) {
61 |
62 | SectionTitle(Strings.adminSectionAdmin.toString())
63 | when (state.adminState) {
64 | is AdminState.Running -> Text("${Strings.adminLabelStatus}: ${Strings.adminLabelRunning(state.adminState.port)}")
65 | AdminState.Stopped -> Text("${Strings.adminLabelStatus}: ${Strings.adminLabelStopped}")
66 | }
67 |
68 | SectionTitle(Strings.adminSectionWeather.toString())
69 | Text("${Strings.adminLabelStatus}: ${getKeyLabel(state.weatherState)}")
70 |
71 | if (state.displayState !is DisplayState.NoDisplay) {
72 |
73 | SectionTitle(Strings.adminSectionDisplay.toString())
74 | Text("${Strings.adminLabelStatus}: ${getKeyLabel(state.displayState)}")
75 | state.displayState.url?.run { Text("${Strings.adminLabelUrl}: $this") }
76 | Column {
77 | state.displayState.messages.onEach {
78 | Message(
79 | message = it,
80 | )
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | @Composable
90 | fun getKeyLabel(state: WeatherState): String = when (state) {
91 | is WeatherState.ApiKeyInvalid -> Strings.adminWeatherApiKeyInvalid.toString()
92 | WeatherState.ApiKeyNotSet -> Strings.adminWeatherApiKeyNotSet.toString()
93 | is WeatherState.Failure -> "${Strings.adminStatusFailure} (${formatTime(state.instant, LocalLocale.current)})"
94 | is WeatherState.Success -> "${Strings.adminStatusSuccess} (${formatTime(state.instant, LocalLocale.current)})"
95 | }
96 |
97 | @Composable
98 | fun getKeyLabel(state: DisplayState): String = when (state) {
99 | is DisplayState.NoDisplay -> ""
100 | is DisplayState.Failure -> "${Strings.adminStatusFailure} (${formatTime(state.instant, LocalLocale.current)})"
101 | is DisplayState.Success -> "${Strings.adminStatusSuccess} (${formatTime(state.instant, LocalLocale.current)})"
102 | }
103 |
104 | @Composable
105 | fun SectionTitle(text: String) {
106 | CompositionLocalProvider(LocalStyle provides LocalStyle.current.copy(contentColor = DisplayerColor)) {
107 | Text(
108 | text = text,
109 | style = TextStyle.Default.copy(fontSize = LocalDimensions.current.fontSizeSmall.toSp(), fontWeight = FontWeight.Bold),
110 | )
111 | }
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/admin/AdminServer.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.admin
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface AdminServer {
6 |
7 | fun observeState() : Flow
8 | }
9 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/admin/AdminState.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.admin
2 |
3 | sealed class AdminState {
4 |
5 | object Stopped : AdminState()
6 |
7 | data class Running(
8 | val port: Int,
9 | ) : AdminState()
10 |
11 | }
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/app/App.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.app
2 |
3 | import co.touchlab.kermit.Logger
4 | import com.displayer.admin.AdminServer
5 | import com.displayer.config.AdminParameters
6 | import com.displayer.config.ConfigRepo
7 | import com.displayer.display.DisplayRepo
8 | import com.displayer.weather.WeatherRepo
9 | import kotlinx.coroutines.GlobalScope
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.combine
13 | import kotlinx.coroutines.launch
14 |
15 | class App(
16 | private val configRepo: ConfigRepo,
17 | private val displayRepo: DisplayRepo,
18 | private val weatherRepo: WeatherRepo,
19 | private val adminServer: AdminServer,
20 | ) {
21 |
22 | private val state = MutableStateFlow(AppState.Initializing)
23 |
24 | init {
25 | GlobalScope.launch {
26 | combine(
27 | displayRepo.observeState(),
28 | weatherRepo.observeState(),
29 | adminServer.observeState(),
30 | ) { displayState, weatherState, adminState ->
31 | AppState.Ready(
32 | displayState = displayState,
33 | weatherState = weatherState,
34 | adminState = adminState,
35 | )
36 | }.collect { state.value = it }
37 | }
38 |
39 | }
40 |
41 | fun observeState(): Flow = state
42 |
43 | fun setOpenWeatherApiKey(apiKey: String) = configRepo.setOpenWeatherApiKey(apiKey)
44 |
45 | suspend fun loadDisplayFromUrl(url: String?) = displayRepo.loadDisplayFromUrl(url)
46 |
47 | fun setAdminParameters(port: Int?, secret: String?) {
48 | if (port == null) {
49 | Logger.e("Port of admin server is missing")
50 | return
51 | }
52 | if (port <= 0 || port > 65535) {
53 | Logger.e("Port of admin server is invalid: $port")
54 | return
55 | }
56 | if (secret.isNullOrBlank()) {
57 | Logger.e("Secret of admin server is missing")
58 | return
59 | }
60 | configRepo.setAdminParameters(AdminParameters(port, secret))
61 | }
62 |
63 | fun stopAdminServer() {
64 | configRepo.setAdminParameters(null)
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/app/AppState.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.app
2 |
3 | import com.displayer.admin.AdminState
4 | import com.displayer.display.DisplayState
5 | import com.displayer.weather.WeatherState
6 |
7 | sealed class AppState {
8 |
9 | object Initializing : AppState()
10 |
11 | data class Ready(
12 | val weatherState: WeatherState,
13 | val displayState: DisplayState,
14 | val adminState: AdminState,
15 | ) : AppState()
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/app/AppUi.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalComposeUiApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
2 |
3 | package com.displayer.app
4 |
5 | import androidx.compose.animation.AnimatedVisibility
6 | import androidx.compose.animation.Crossfade
7 | import androidx.compose.animation.fadeIn
8 | import androidx.compose.animation.fadeOut
9 | import androidx.compose.animation.slideInHorizontally
10 | import androidx.compose.animation.slideOutHorizontally
11 | import androidx.compose.foundation.ExperimentalFoundationApi
12 | import androidx.compose.foundation.background
13 | import androidx.compose.foundation.focusable
14 | import androidx.compose.foundation.gestures.detectTapGestures
15 | import androidx.compose.foundation.layout.Box
16 | import androidx.compose.foundation.layout.BoxWithConstraints
17 | import androidx.compose.foundation.layout.fillMaxSize
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.CompositionLocalProvider
20 | import androidx.compose.runtime.LaunchedEffect
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.runtime.setValue
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.ExperimentalComposeUiApi
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.focus.FocusRequester
29 | import androidx.compose.ui.focus.focusRequester
30 | import androidx.compose.ui.focus.onFocusChanged
31 | import androidx.compose.ui.graphics.Color
32 | import androidx.compose.ui.input.key.Key
33 | import androidx.compose.ui.input.key.KeyEventType
34 | import androidx.compose.ui.input.key.key
35 | import androidx.compose.ui.input.key.onKeyEvent
36 | import androidx.compose.ui.input.key.type
37 | import androidx.compose.ui.input.pointer.pointerInput
38 | import com.displayer.admin.AdminPanel
39 | import com.displayer.display.DisplayScreen
40 | import com.displayer.ui.Dimensions
41 | import com.displayer.ui.LocalDimensions
42 | import com.displayer.ui.LocalStyle
43 |
44 | @Composable
45 | fun AppUi(state: AppState) {
46 | var showAdminPanel by remember { mutableStateOf(false) }
47 | val focusRequester = remember { FocusRequester() }
48 | var hasFocus by remember { mutableStateOf(false) }
49 |
50 | BoxWithConstraints(
51 | modifier = Modifier
52 | .fillMaxSize()
53 | .focusRequester(focusRequester)
54 | .onFocusChanged {
55 | hasFocus = it.hasFocus
56 | }
57 | .focusable()
58 | .background(LocalStyle.current.backgroundColor)
59 | .pointerInput(Unit) {
60 | detectTapGestures(onDoubleTap = { showAdminPanel = !showAdminPanel })
61 | }
62 | .onKeyEvent {
63 | if (it.type == KeyEventType.KeyUp) {
64 | if (it.key == Key.DirectionCenter) {
65 | showAdminPanel = !showAdminPanel
66 | return@onKeyEvent true
67 | }
68 | }
69 | return@onKeyEvent false
70 | },
71 | contentAlignment = Alignment.CenterEnd
72 | ) {
73 | CompositionLocalProvider(LocalDimensions provides Dimensions(maxWidth, maxHeight)) {
74 | Crossfade(targetState = state) {
75 | when (it) {
76 | AppState.Initializing -> InitializingScreen()
77 | is AppState.Ready -> DisplayScreen(it.displayState)
78 | }
79 | }
80 | AnimatedVisibility(showAdminPanel, enter = fadeIn(), exit = fadeOut()) {
81 | Box(modifier = Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.5f)))
82 | }
83 | AnimatedVisibility(showAdminPanel, enter = slideInHorizontally(initialOffsetX = { (it * 1.5f).toInt() }), exit = slideOutHorizontally(targetOffsetX = { (it * 1.5f).toInt() })) {
84 | AdminPanel(state)
85 | }
86 | }
87 | }
88 | LaunchedEffect(Unit) {
89 | focusRequester.requestFocus()
90 | }
91 | }
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/app/InitializingScreen.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.app
2 |
3 | import Strings
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.LaunchedEffect
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.text.TextStyle
17 | import com.displayer.toSp
18 | import com.displayer.ui.LocalDimensions
19 | import com.displayer.ui.Text
20 | import kotlinx.coroutines.delay
21 |
22 | @Composable
23 | fun InitializingScreen() {
24 | var showLoading by remember { mutableStateOf(false) }
25 | AnimatedVisibility(showLoading, enter = fadeIn()) {
26 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
27 | Text(Strings.loading.toString(), style = TextStyle.Default.copy(fontSize = LocalDimensions.current.fontSize.toSp()))
28 | }
29 | }
30 | LaunchedEffect(true) {
31 | delay(1000)
32 | showLoading = true
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/config/Config.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.config
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class Config(
7 | val displayUrl: String? = null,
8 | val openWeatherApiKey: String? = null,
9 | val adminParameters: AdminParameters? = null,
10 | )
11 |
12 | @Serializable
13 | data class AdminParameters(
14 | val port: Int,
15 | val secret: String,
16 | )
17 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/config/ConfigRepo.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.config
2 |
3 | import co.touchlab.kermit.Logger
4 | import com.russhwolf.settings.Settings
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.serialization.decodeFromString
8 | import kotlinx.serialization.encodeToString
9 | import kotlinx.serialization.json.Json
10 |
11 | /*
12 | Manages/persists the configuration of the app
13 | */
14 | class ConfigRepo(
15 | private val json: Json,
16 | private val settings: Settings,
17 | ) {
18 |
19 | private var configFlow = MutableStateFlow(json.decodeFromString(settings.getStringOrNull(KEY_CONFIG) ?: "{}"))
20 |
21 | fun observeConfig(): Flow = configFlow
22 |
23 | private fun updateConfig(config: Config) {
24 | configFlow.value = config
25 | settings.putString(KEY_CONFIG, json.encodeToString(config))
26 | }
27 |
28 | fun setOpenWeatherApiKey(apiKey: String) {
29 | Logger.d("Setting OpenWeather API key to $apiKey")
30 | updateConfig(configFlow.value.copy(openWeatherApiKey = apiKey))
31 | }
32 |
33 | fun getDisplayUrl(): String? = configFlow.value.displayUrl
34 |
35 | fun setDisplayUrl(url: String) {
36 | updateConfig(configFlow.value.copy(displayUrl = url))
37 | }
38 |
39 | fun setAdminParameters(params: AdminParameters?) {
40 | if (params == null) {
41 | Logger.d("Clearing admin parameters")
42 | } else {
43 | Logger.d("Setting admin parameters: port=${params.port} secret=XXXX")
44 | }
45 | updateConfig(configFlow.value.copy(adminParameters = params))
46 | }
47 |
48 | companion object {
49 | const val KEY_CONFIG = "Displayer.Config"
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/config/Parameters.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.config
2 |
3 | import com.displayer.display.parser.Units
4 |
5 | data class Parameters(
6 | val language: String,
7 | val country: String,
8 | val zip: String? = null,
9 | val units: Units = Units.Metric,
10 | )
11 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/config/ParametersRepo.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.config
2 |
3 | import co.touchlab.kermit.Logger
4 | import com.displayer.display.parser.Parser
5 | import de.comahe.i18n4k.config.I18n4kConfigDefault
6 | import de.comahe.i18n4k.i18n4k
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 |
10 | /*
11 | Manages the parameters specified in a display file
12 | */
13 | class ParametersRepo {
14 |
15 | private var parametersFlow = MutableStateFlow(Parser.parseParameters(null))
16 |
17 | fun observeParameters(): Flow = parametersFlow
18 |
19 | fun setParameters(parameters: Parameters) {
20 | parametersFlow.value = parameters
21 | activateLanguage(parameters.language)
22 | }
23 |
24 | private fun activateLanguage(language: String) {
25 | Logger.i("Setting language to $language")
26 | val i18n4kConfig = I18n4kConfigDefault()
27 | i18n4k = i18n4kConfig
28 | i18n4kConfig.locale = de.comahe.i18n4k.Locale(language)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/DefaultDisplayFile.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display
2 |
3 | import com.displayer.display.parser.ClockDto
4 | import com.displayer.display.parser.ColumnDto
5 | import com.displayer.display.parser.DisplayFile
6 | import com.displayer.display.parser.ImageDto
7 | import com.displayer.display.parser.ParametersDto
8 | import com.displayer.display.parser.RowDto
9 | import com.displayer.display.parser.ScaleDto
10 | import com.displayer.display.parser.StackDto
11 | import com.displayer.display.parser.StyleDto
12 | import com.displayer.display.parser.TextDto
13 |
14 | val defaultDisplayFile = DisplayFile(
15 | parameters = ParametersDto(
16 | language = "en",
17 | country = "BE",
18 | ),
19 | styleId = "light",
20 | center = StackDto(
21 | items = listOf(
22 | TextDto("Displayer Demo"),
23 | TextDto("Lorem ipsum dolor sit amet, consectetur adipiscing elit"),
24 | ImageDto(url = "https://picsum.photos/id/24/960/540", scale = ScaleDto.Crop),
25 | TextDto("Maecenas consectetur in erat sit amet condimentum."),
26 | ImageDto(url = "https://picsum.photos/id/56/960/540", scale = ScaleDto.Crop),
27 | TextDto("Etiam nec ipsum et massa accumsan pulvinar."),
28 | ),
29 | autoAdvanceInSeconds = 5,
30 | ),
31 | left = ColumnDto(
32 | styleId = "darker",
33 | items = listOf(
34 | ClockDto(),
35 | TextDto("⚝"),
36 | )
37 | ),
38 | bottom = RowDto(
39 | styleId = "dark",
40 | items = listOf(
41 | TextDto("Maecenas consectetur in erat sit amet condimentum."),
42 | ImageDto(url = "https://picsum.photos/id/106/640/480"),
43 | TextDto("Lorem ipsum dolor sit amet, consectetur adipiscing elit"),
44 | ImageDto(url = "https://picsum.photos/id/159/960/540"),
45 | TextDto("Etiam nec ipsum et massa accumsan pulvinar."),
46 | ),
47 | divider = TextDto("•"),
48 | ),
49 | styles = listOf(
50 | StyleDto(id = "light", backgroundColor = "#ffffff", contentColor = "#1E9991"),
51 | StyleDto(id = "dark", backgroundColor = "#1E9991", contentColor = "#ffffff"),
52 | StyleDto(id = "darker", backgroundColor = "#17756F", contentColor = "#ffffff"),
53 | )
54 | )
55 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/Display.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.text.intl.Locale
5 | import com.displayer.display.container.Container
6 |
7 | data class Display(
8 | val locale : Locale,
9 | val refreshInMinutes: Int = 0,
10 | val style: Style,
11 | val center: Container,
12 | val left: Container,
13 | val bottom: Container,
14 | val bottomHeight: Float,
15 | ) {
16 | companion object {
17 | const val DEFAULT_BOTTOM_HEIGHT: Float = 6f
18 | }
19 |
20 | }
21 |
22 | data class Style(
23 | val id: String,
24 | val backgroundColor: Color,
25 | val contentColor: Color,
26 | )
27 |
28 | val defaultStyle = Style(
29 | id = "displayer.style.dark",
30 | backgroundColor = Color.Black,
31 | contentColor = Color.White,
32 | )
33 |
34 | data class Padding(
35 | val horizontal: Float = 0f,
36 | val vertical: Float = 0f,
37 | )
38 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/DisplayState.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display
2 |
3 | import com.displayer.display.parser.Message
4 | import kotlinx.collections.immutable.ImmutableList
5 | import kotlinx.collections.immutable.persistentListOf
6 | import kotlinx.datetime.Clock
7 | import kotlinx.datetime.Instant
8 |
9 | sealed class DisplayState(
10 | open val instant: Instant,
11 | open val url: String? = null,
12 | open val messages: ImmutableList = persistentListOf()
13 | ) {
14 | class NoDisplay : DisplayState(Clock.System.now())
15 |
16 | data class Success(
17 | override val url: String? = null,
18 | override val messages: ImmutableList,
19 | val display: Display,
20 | ) : DisplayState(Clock.System.now(), url, messages)
21 |
22 | data class Failure(
23 | override val url: String?,
24 | override val messages: ImmutableList,
25 | ) : DisplayState(Clock.System.now(), url, messages)
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/DisplayUi.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display
2 |
3 | import androidx.compose.animation.Crossfade
4 | import androidx.compose.runtime.Composable
5 |
6 | @Composable
7 | fun DisplayScreen(state: DisplayState) {
8 | Crossfade(targetState = state) {
9 | when (it) {
10 | is DisplayState.Success -> MainUi(it.display)
11 | is DisplayState.Failure -> ErrorUi(it)
12 | is DisplayState.NoDisplay -> {}
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/ErrorUi.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display
2 |
3 | import Strings
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Arrangement.spacedBy
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.rememberScrollState
14 | import androidx.compose.foundation.text.BasicText
15 | import androidx.compose.foundation.verticalScroll
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.text.TextStyle
22 | import com.displayer.toSp
23 | import com.displayer.ui.LocalDimensions
24 | import com.displayer.ui.Message
25 | import io.kamel.image.KamelImage
26 | import io.kamel.image.lazyPainterResource
27 | import io.ktor.http.*
28 |
29 | @Composable
30 | fun ErrorUi(displayFailed: DisplayState.Failure) {
31 | Column(
32 | modifier = Modifier.fillMaxSize(),
33 | ) {
34 | BasicText(
35 | text = Strings.errorTitle.toString(),
36 | modifier = Modifier.fillMaxWidth().background(Color.Red).padding(horizontal = LocalDimensions.current.baseUnit * 2, vertical = LocalDimensions.current.baseUnit),
37 | style = TextStyle.Default.copy(color = Color.White, fontSize = LocalDimensions.current.fontSize.toSp())
38 | )
39 | Row(
40 | modifier = Modifier.fillMaxWidth(),
41 | ) {
42 | Column(
43 | modifier = Modifier.weight(1f, true).fillMaxHeight().padding(LocalDimensions.current.baseUnit * 2).verticalScroll(rememberScrollState()),
44 | verticalArrangement = spacedBy(LocalDimensions.current.baseUnit * 2)
45 | ) {
46 | val textStyle = TextStyle.Default.copy(fontSize = LocalDimensions.current.fontSize.toSp())
47 | displayFailed.messages.onEach {
48 | Message(it)
49 | }
50 | }
51 | displayFailed.url?.run {
52 | Box(
53 | modifier = Modifier.fillMaxHeight().fillMaxWidth(0.2f).padding(LocalDimensions.current.baseUnit),
54 | contentAlignment = Alignment.BottomCenter,
55 | ) {
56 | Column(
57 | horizontalAlignment = Alignment.CenterHorizontally,
58 | ) {
59 | BasicText(text = "Config URL")
60 | KamelImage(
61 | lazyPainterResource(data = "https://quickchart.io/qr?size=300&text=${encodeURLParameter()}"),
62 | contentDescription = null,
63 | modifier = Modifier.fillMaxWidth(),
64 | contentScale = ContentScale.FillWidth,
65 | )
66 | }
67 | }
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/MainUi.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.aspectRatio
7 | import androidx.compose.foundation.layout.fillMaxHeight
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.CompositionLocalProvider
13 | import androidx.compose.ui.Modifier
14 | import com.displayer.display.container.ContainerUi
15 | import com.displayer.ui.LocalDimensions
16 | import com.displayer.ui.LocalLocale
17 | import com.displayer.ui.LocalStyle
18 | import com.displayer.ui.StyledContent
19 |
20 | @Composable
21 | fun MainUi(display: Display) {
22 | with(display) {
23 | CompositionLocalProvider(LocalLocale provides (display.locale)) {
24 | StyledContent(style) {
25 | Column(
26 | modifier = Modifier.fillMaxSize().background(LocalStyle.current.backgroundColor)
27 | ) {
28 | val bottomHeight = LocalDimensions.current.baseUnit * display.bottomHeight
29 | Row(Modifier.height(LocalDimensions.current.screenHeight - bottomHeight)) {
30 | if (bottomHeight.value > 0f) {
31 | ContainerUi(left, Modifier.weight(1f, true).fillMaxSize())
32 | }
33 | ContainerUi(center, Modifier.fillMaxHeight().aspectRatio(LocalDimensions.current.screenAspectRatio))
34 | }
35 | if (bottomHeight.value > 0f) {
36 | ContainerUi(bottom, Modifier.height(bottomHeight).fillMaxWidth())
37 | }
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/container/ColumnLayout.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.container
2 |
3 | import androidx.compose.animation.core.LinearEasing
4 | import androidx.compose.animation.core.RepeatMode
5 | import androidx.compose.animation.core.infiniteRepeatable
6 | import androidx.compose.animation.core.tween
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.rememberScrollState
13 | import androidx.compose.foundation.verticalScroll
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.LaunchedEffect
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import com.displayer.display.Padding
19 | import com.displayer.display.Style
20 | import com.displayer.display.item.Item
21 | import com.displayer.display.item.ItemUi
22 | import com.displayer.toPx
23 | import com.displayer.ui.Direction
24 | import com.displayer.ui.LocalDimensions
25 | import kotlinx.collections.immutable.ImmutableList
26 | import kotlinx.coroutines.delay
27 |
28 | data class ColumnLayout(
29 | override val padding: Padding,
30 | override val style: Style? = null,
31 | override val items: ImmutableList- ,
32 | val spacing: Float? = null,
33 | val scrollSpeedSeconds: Float,
34 | ) : Container(
35 | direction = Direction.Vertical,
36 | ) {
37 |
38 | companion object {
39 | val DEFAULT_PADDING_SCROLLING: Padding = Padding(1f, 0f)
40 | val DEFAULT_PADDING_STATIC: Padding = Padding(1f, 1f)
41 | const val DEFAULT_SCROLL_SPEED: Float = 0f
42 | }
43 | }
44 |
45 | @Composable
46 | fun ColumnLayoutUi(container: ColumnLayout) {
47 | with(container) {
48 | if (scrollSpeedSeconds > 0f) {
49 | val scrollState = rememberScrollState()
50 | val scrollDistance = LocalDimensions.current.screenHeight
51 | Column(
52 | modifier = Modifier.fillMaxSize().verticalScroll(scrollState),
53 | horizontalAlignment = Alignment.CenterHorizontally,
54 | verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.baseUnit * (container.spacing ?: container.padding.horizontal)),
55 | ) {
56 | Spacer(Modifier.height(scrollDistance))
57 | items.forEach { ItemUi(it) }
58 | Spacer(Modifier.height(scrollDistance))
59 | }
60 |
61 | val durationFactor: Int = (scrollState.maxValue / scrollDistance.toPx()).toInt()
62 | LaunchedEffect(scrollState.maxValue) {
63 | delay(1000)
64 | scrollState.scrollTo(0)
65 | scrollState.animateScrollTo(
66 | scrollState.maxValue, infiniteRepeatable(
67 | animation = tween((scrollSpeedSeconds * 1000 * durationFactor).toInt(), easing = LinearEasing), repeatMode = RepeatMode.Restart
68 | )
69 | )
70 | }
71 | } else {
72 | Column(
73 | modifier = Modifier.fillMaxSize(),
74 | horizontalAlignment = Alignment.CenterHorizontally,
75 | verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.baseUnit * (container.spacing ?: container.padding.horizontal)),
76 | ) {
77 | items.forEach { ItemUi(it) }
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/container/Container.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.container
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.CompositionLocalProvider
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.text.style.TextAlign
11 | import com.displayer.display.Padding
12 | import com.displayer.display.Style
13 | import com.displayer.display.item.Item
14 | import com.displayer.ui.Direction
15 | import com.displayer.ui.LocalDimensions
16 | import com.displayer.ui.LocalDirection
17 | import com.displayer.ui.LocalStyle
18 | import com.displayer.ui.LocalTextAlign
19 | import com.displayer.ui.StyledContent
20 | import kotlinx.collections.immutable.ImmutableList
21 |
22 | sealed class Container(
23 | val direction: Direction = Direction.None,
24 | val textAlign: TextAlign = TextAlign.Center,
25 | ) {
26 | abstract val padding: Padding
27 | abstract val style: Style?
28 | abstract val items: ImmutableList
-
29 | }
30 |
31 | @Composable
32 | fun ContainerUi(container: Container, modifier: Modifier) {
33 | StyledContent(container.style) {
34 | CompositionLocalProvider(
35 | LocalDirection provides container.direction,
36 | LocalTextAlign provides container.textAlign,
37 | ) {
38 | val paddingHorizontal = LocalDimensions.current.baseUnit * container.padding.horizontal
39 | val paddingVertical = LocalDimensions.current.baseUnit * container.padding.vertical
40 | Box(modifier = modifier.background(LocalStyle.current.backgroundColor).fillMaxSize().padding(horizontal = paddingHorizontal, vertical = paddingVertical)) {
41 | when (container) {
42 | is Empty -> EmptyUi()
43 | is StackLayout -> StackLayoutUi(container)
44 | is ColumnLayout -> ColumnLayoutUi(container)
45 | is RowLayout -> RowLayoutUi(container)
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/container/Empty.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.container
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.displayer.display.Padding
5 | import com.displayer.display.Style
6 | import com.displayer.display.item.Item
7 | import kotlinx.collections.immutable.ImmutableList
8 | import kotlinx.collections.immutable.persistentListOf
9 |
10 | data class Empty(
11 | override val padding: Padding = Padding(0f, 0f),
12 | override val style: Style? = null,
13 | override val items: ImmutableList
- = persistentListOf(),
14 | ) : Container()
15 |
16 | @Composable
17 | fun EmptyUi() {
18 | }
19 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/container/RowLayout.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.container
2 |
3 | import androidx.compose.animation.core.LinearEasing
4 | import androidx.compose.animation.core.RepeatMode
5 | import androidx.compose.animation.core.infiniteRepeatable
6 | import androidx.compose.animation.core.tween
7 | import androidx.compose.foundation.horizontalScroll
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.width
13 | import androidx.compose.foundation.rememberScrollState
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.LaunchedEffect
16 | import androidx.compose.ui.Alignment.Companion.CenterVertically
17 | import androidx.compose.ui.Modifier
18 | import com.displayer.display.Padding
19 | import com.displayer.display.Style
20 | import com.displayer.display.item.Item
21 | import com.displayer.display.item.ItemUi
22 | import com.displayer.toPx
23 | import com.displayer.ui.Direction
24 | import com.displayer.ui.LocalDimensions
25 | import kotlinx.collections.immutable.ImmutableList
26 | import kotlinx.coroutines.delay
27 |
28 | data class RowLayout(
29 | override val padding: Padding,
30 | override val style: Style? = null,
31 | override val items: ImmutableList
- ,
32 | val spacing: Float? = null,
33 | val scrollSpeedSeconds: Float,
34 | ) : Container(
35 | direction = Direction.Horizontal,
36 | ) {
37 |
38 | companion object {
39 | val DEFAULT_PADDING_SCROLLING: Padding = Padding(0f, 1f)
40 | val DEFAULT_PADDING_STATIC: Padding = Padding(1f, 1f)
41 | const val DEFAULT_SCROLL_SPEED: Float = 12f
42 | }
43 | }
44 |
45 | @Composable
46 | fun RowLayoutUi(container: RowLayout) {
47 | with(container) {
48 | if (scrollSpeedSeconds > 0f) {
49 | val scrollState = rememberScrollState()
50 | val scrollDistance = LocalDimensions.current.screenWidth
51 | Row(
52 | modifier = Modifier.fillMaxSize().horizontalScroll(state = scrollState, enabled = false),
53 | horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.baseUnit * (container.spacing ?: container.padding.vertical)),
54 | ) {
55 | Spacer(Modifier.width(scrollDistance))
56 | items.forEach { ItemUi(it) }
57 | Spacer(Modifier.width(scrollDistance))
58 | }
59 |
60 | val durationFactor: Int = (scrollState.maxValue / scrollDistance.toPx()).toInt()
61 | LaunchedEffect(scrollState.maxValue) {
62 | delay(1000)
63 | scrollState.scrollTo(0)
64 | scrollState.animateScrollTo(
65 | scrollState.maxValue, infiniteRepeatable(
66 | animation = tween((scrollSpeedSeconds * 1000 * durationFactor).toInt(), easing = LinearEasing), repeatMode = RepeatMode.Restart
67 | )
68 | )
69 | }
70 | } else {
71 | Row(
72 | modifier = Modifier.fillMaxSize(),
73 | horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.baseUnit * (container.spacing ?: container.padding.vertical)),
74 | verticalAlignment = CenterVertically,
75 | ) {
76 | items.forEach { ItemUi(it) }
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/container/Stacked.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.container
2 |
3 | import androidx.compose.animation.AnimatedContent
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.compose.animation.with
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.mutableStateOf
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import com.displayer.display.Padding
18 | import com.displayer.display.Style
19 | import com.displayer.display.item.Item
20 | import com.displayer.display.item.ItemUi
21 | import kotlinx.collections.immutable.ImmutableList
22 | import kotlinx.coroutines.delay
23 |
24 | data class StackLayout(
25 | override val padding: Padding,
26 | override val style: Style? = null,
27 | override val items: ImmutableList
- ,
28 | val autoAdvanceInSeconds: Int,
29 | ) : Container() {
30 |
31 | companion object {
32 | const val DEFAULT_AUTO_ADVANCE_IN_SECONDS = 30
33 | val DEFAULT_PADDING: Padding = Padding(0f, 0f)
34 | }
35 | }
36 |
37 | @OptIn(ExperimentalAnimationApi::class)
38 | @Composable
39 | fun StackLayoutUi(container: StackLayout) {
40 | with(container) {
41 | val currentItem = remember { mutableStateOf(0) }
42 | if (items.isNotEmpty()) {
43 | LaunchedEffect(true) {
44 | while (true) {
45 | delay(autoAdvanceInSeconds * 1000L)
46 | currentItem.value = (currentItem.value + 1) % items.size
47 | }
48 | }
49 | }
50 | val animationDuration = 500
51 | AnimatedContent(
52 | targetState = currentItem.value,
53 | transitionSpec = { fadeIn(tween(animationDuration)) with fadeOut(tween(animationDuration)) }) { targetState ->
54 | Box(
55 | modifier = Modifier.fillMaxSize(),
56 | contentAlignment = Alignment.Center
57 | ) {
58 | items.getOrNull(targetState.coerceAtMost((items.size - 1).coerceAtLeast(0)))?.run {
59 | ItemUi(this)
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/Clock.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.setValue
9 | import com.displayer.display.Padding
10 | import com.displayer.display.Style
11 | import com.displayer.platform.formatTime
12 | import com.displayer.ui.LocalLocale
13 | import kotlinx.coroutines.delay
14 |
15 | data class Clock(
16 | override val style: Style? = null,
17 | override val padding: Padding = Padding(),
18 | ) : Item()
19 |
20 | @Composable
21 | fun ClockUi() {
22 | var timeString by remember { mutableStateOf("") }
23 | val locale = LocalLocale.current
24 | LaunchedEffect(true) {
25 | while (true) {
26 | timeString = formatTime(kotlinx.datetime.Clock.System.now(), locale)
27 | delay(200)
28 | }
29 | }
30 | AutoFitText(text = timeString)
31 | }
32 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/Drink.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.layout.ContentScale
11 | import androidx.compose.ui.text.TextStyle
12 | import com.displayer.display.Padding
13 | import com.displayer.display.Style
14 | import com.displayer.ui.LocalDimensions
15 | import com.displayer.ui.LocalStyle
16 | import com.displayer.ui.LocalTextAlign
17 | import io.kamel.image.KamelImage
18 | import io.kamel.image.lazyPainterResource
19 |
20 | data class DrinkItem(
21 | val image: String,
22 | val text: String,
23 | override val style: Style? = null,
24 | override val padding: Padding = Padding(),
25 | val spacing: Float? = DEFAULT_SPACING,
26 | ) : Item() {
27 |
28 | companion object {
29 | const val DEFAULT_SPACING = 1f
30 | }
31 | }
32 |
33 | @Composable
34 | fun DrinkItemUi(item: DrinkItem) {
35 | Column(
36 | modifier = Modifier.fillMaxSize(),
37 | horizontalAlignment = Alignment.CenterHorizontally,
38 | verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.baseUnit * (item.spacing ?: item.padding.horizontal)),
39 | ) {
40 | Box(
41 | modifier = Modifier.weight(1f),
42 | ) {
43 | KamelImage(
44 | lazyPainterResource(data = item.image),
45 | contentDescription = null,
46 | modifier = Modifier.fillMaxSize(),
47 | contentScale = ContentScale.Fit
48 | )
49 | }
50 | val actualStyle = TextStyle.Default.copy(
51 | color = LocalStyle.current.contentColor,
52 | textAlign = LocalTextAlign.current,
53 | )
54 | UnscaledText(item.text, actualStyle)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/Image.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.animation.core.FiniteAnimationSpec
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.layout.BoxScope
6 | import androidx.compose.foundation.layout.fillMaxHeight
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.ColorFilter
13 | import androidx.compose.ui.graphics.DefaultAlpha
14 | import androidx.compose.ui.graphics.painter.Painter
15 | import androidx.compose.ui.layout.ContentScale
16 | import com.displayer.display.Padding
17 | import com.displayer.display.Style
18 | import com.displayer.ui.Direction
19 | import com.displayer.ui.LocalDirection
20 | import io.kamel.core.ExperimentalKamelApi
21 | import io.kamel.core.Resource
22 | import io.kamel.image.KamelImageBox
23 | import io.kamel.image.lazyPainterResource
24 |
25 | data class Image(
26 | val url: String,
27 | override val style: Style? = null,
28 | override val padding: Padding = Padding(),
29 | val scale: ContentScale,
30 | ) : Item()
31 |
32 |
33 | @Composable
34 | fun ImageUi(item: Image) {
35 | BetterKamelImage(
36 | lazyPainterResource(data = item.url),
37 | contentDescription = null,
38 | modifier = when (LocalDirection.current) {
39 | Direction.None -> Modifier.fillMaxSize()
40 | Direction.Horizontal -> Modifier.fillMaxHeight()
41 | Direction.Vertical -> Modifier.fillMaxWidth()
42 | },
43 | contentScale = when (LocalDirection.current) {
44 | Direction.None -> item.scale
45 | Direction.Horizontal -> ContentScale.FillHeight
46 | Direction.Vertical -> ContentScale.FillWidth
47 | }
48 | )
49 | }
50 |
51 | @OptIn(ExperimentalKamelApi::class)
52 | @Composable
53 | public fun BetterKamelImage(
54 | resource: Resource,
55 | contentDescription: String?,
56 | modifier: Modifier = Modifier,
57 | alignment: Alignment = Alignment.Center,
58 | contentScale: ContentScale = ContentScale.Fit,
59 | alpha: Float = DefaultAlpha,
60 | colorFilter: ColorFilter? = null,
61 | onLoading: @Composable (BoxScope.(Float) -> Unit)? = null,
62 | onFailure: @Composable (BoxScope.(Throwable) -> Unit)? = null,
63 | contentAlignment: Alignment = Alignment.Center,
64 | animationSpec: FiniteAnimationSpec? = null,
65 | ) {
66 | val onSuccess: @Composable (BoxScope.(Painter) -> Unit) = { painter ->
67 | Image(
68 | painter,
69 | contentDescription,
70 | modifier,
71 | alignment,
72 | contentScale,
73 | alpha,
74 | colorFilter
75 | )
76 | }
77 | KamelImageBox(
78 | resource,
79 | modifier,
80 | contentAlignment,
81 | animationSpec,
82 | onLoading,
83 | onFailure,
84 | onSuccess,
85 | )
86 | }
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/Item.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import com.displayer.display.Padding
8 | import com.displayer.display.Style
9 | import com.displayer.ui.LocalDimensions
10 | import com.displayer.ui.StyledContent
11 |
12 | sealed class Item {
13 | abstract val style: Style?
14 | abstract val padding: Padding
15 | }
16 |
17 | @Composable
18 | fun ItemUi(item: Item) {
19 | StyledContent(item.style) {
20 | val paddingHorizontal = LocalDimensions.current.baseUnit * item.padding.horizontal
21 | val paddingVertical = LocalDimensions.current.baseUnit * item.padding.vertical
22 | Box(modifier = Modifier.padding(horizontal = paddingHorizontal, vertical = paddingVertical)) {
23 | when (item) {
24 | is UnknownItem -> UnknownItemUi()
25 | is Clock -> ClockUi()
26 | is Image -> ImageUi(item)
27 | is TextItem -> TextItemUi(item)
28 | is RandomItem -> RandomItemUi(item)
29 | is Weather -> WeatherUi(item)
30 | is DrinkItem -> DrinkItemUi(item)
31 | is SocialItem -> SocialItemUi(item)
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/RandomItem.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.runtime.Composable
4 | import co.touchlab.kermit.Logger
5 | import com.displayer.display.Padding
6 | import com.displayer.display.Style
7 | import kotlinx.collections.immutable.ImmutableList
8 | import kotlinx.collections.immutable.persistentListOf
9 | import kotlin.random.Random
10 |
11 | data class RandomItem(
12 | val items: ImmutableList
- = persistentListOf(),
13 | override val style: Style? = null,
14 | override val padding: Padding = Padding(),
15 | ) : Item()
16 |
17 | @Composable
18 | fun RandomItemUi(item: RandomItem) {
19 | if (item.items.isEmpty()) {
20 | return
21 | }
22 | val index = Random.nextInt(0, item.items.size)
23 | Logger.i { "Picked random item $index" }
24 | ItemUi(item.items[index])
25 | }
26 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/Social.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.aspectRatio
10 | import androidx.compose.foundation.layout.fillMaxHeight
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.layout.ContentScale
17 | import androidx.compose.ui.text.TextStyle
18 | import com.displayer.display.Padding
19 | import com.displayer.display.Style
20 | import com.displayer.display.parser.SocialApp
21 | import com.displayer.isLight
22 | import com.displayer.platform.getIcon
23 | import com.displayer.ui.Direction
24 | import com.displayer.ui.Icon
25 | import com.displayer.ui.LocalDimensions
26 | import com.displayer.ui.LocalDirection
27 | import com.displayer.ui.LocalStyle
28 | import com.displayer.ui.LocalTextAlign
29 |
30 | data class SocialItem(
31 | val app: SocialApp,
32 | val account: String,
33 | val text: String?,
34 | override val style: Style? = null,
35 | override val padding: Padding = Padding(),
36 | ) : Item()
37 |
38 | @Composable
39 | fun SocialItemUi(item: SocialItem) {
40 | when (LocalDirection.current) {
41 | Direction.None -> SocialItemOther(item)
42 | Direction.Horizontal -> SocialItemRow(item)
43 | Direction.Vertical -> SocialItemColumn(item)
44 | }
45 | }
46 |
47 | @Composable
48 | fun SocialItemOther(item: SocialItem) {
49 | Column(
50 | modifier = Modifier.fillMaxSize(),
51 | horizontalAlignment = Alignment.CenterHorizontally,
52 | verticalArrangement = Arrangement.SpaceEvenly,
53 | ) {
54 | val actualStyle = TextStyle.Default.copy(
55 | color = LocalStyle.current.contentColor,
56 | textAlign = LocalTextAlign.current,
57 | )
58 | Spacer(modifier = Modifier.weight(2f))
59 | item.text?.run {
60 | UnscaledText(this, actualStyle)
61 | }
62 | Spacer(modifier = Modifier.weight(1f))
63 | Box(
64 | modifier = Modifier.weight(6f).aspectRatio(2f),
65 | ) {
66 | Image(
67 | getIcon(item.app.asIcon()),
68 | contentDescription = null,
69 | modifier = Modifier.fillMaxSize(),
70 | contentScale = ContentScale.Fit
71 | )
72 | }
73 | Spacer(modifier = Modifier.weight(1f))
74 | UnscaledText(item.account, actualStyle)
75 | Spacer(modifier = Modifier.weight(2f))
76 | }
77 | }
78 |
79 | @Composable
80 | fun SocialItemRow(item: SocialItem) {
81 | Row(
82 | horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.baseUnit),
83 | verticalAlignment = Alignment.CenterVertically,
84 | ) {
85 | Image(
86 | getIcon(item.app.asIcon()),
87 | contentDescription = null,
88 | modifier = Modifier.fillMaxHeight(),
89 | contentScale = ContentScale.FillHeight
90 | )
91 | AutoFitText(listOfNotNull(item.text, item.account).joinToString(": "))
92 | }
93 | }
94 |
95 | @Composable
96 | fun SocialItemColumn(item: SocialItem) {
97 | Column(
98 | modifier = Modifier.fillMaxWidth(),
99 | horizontalAlignment = Alignment.CenterHorizontally,
100 | verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.baseUnit),
101 | ) {
102 | Image(
103 | getIcon(item.app.asIcon()),
104 | contentDescription = null,
105 | modifier = Modifier.fillMaxWidth(0.8f),
106 | contentScale = ContentScale.FillWidth
107 | )
108 | AutoFitText(item.account)
109 | }
110 | }
111 |
112 | @Composable
113 | fun SocialApp.asIcon() = when (this) {
114 | SocialApp.Facebook -> Icon.Facebook
115 | SocialApp.Instagram -> Icon.Instagram
116 | SocialApp.Tiktok -> Icon.TikTok
117 | SocialApp.YouTube -> if (LocalStyle.current.backgroundColor.isLight()) Icon.YouTubeLight else Icon.YouTubeDark
118 | SocialApp.Snapchat -> Icon.Snapchat
119 | }
120 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/TextItem.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.BoxWithConstraints
5 | import androidx.compose.foundation.layout.fillMaxHeight
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.text.BasicText
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.platform.LocalDensity
13 | import androidx.compose.ui.platform.LocalFontFamilyResolver
14 | import androidx.compose.ui.text.ParagraphIntrinsics
15 | import androidx.compose.ui.text.TextStyle
16 | import androidx.compose.ui.unit.sp
17 | import com.displayer.display.Padding
18 | import com.displayer.display.Style
19 | import com.displayer.toSp
20 | import com.displayer.ui.Direction
21 | import com.displayer.ui.LocalDirection
22 | import com.displayer.ui.LocalStyle
23 | import com.displayer.ui.LocalTextAlign
24 |
25 | data class TextItem(
26 | val text: String,
27 | override val style: Style? = null,
28 | override val padding: Padding = Padding(),
29 | ) : Item()
30 |
31 | @Composable
32 | fun TextItemUi(item: TextItem) {
33 | AutoFitText(text = item.text)
34 | }
35 |
36 | @Composable
37 | fun AutoFitText(
38 | text: String,
39 | modifier: Modifier = Modifier,
40 | style: TextStyle = TextStyle.Default,
41 | ) {
42 | val actualStyle = style.copy(
43 | color = LocalStyle.current.contentColor,
44 | textAlign = LocalTextAlign.current,
45 | // FIXME: Disable includeFontPadding. See https://medium.com/androiddevelopers/fixing-font-padding-in-compose-text-768cd232425b
46 | // platformStyle = PlatformTextStyle(
47 | // includeFontPadding = false
48 | // ),
49 | )
50 | when (LocalDirection.current) {
51 | Direction.None -> UnscaledText(text, actualStyle, modifier.fillMaxSize().background(LocalStyle.current.backgroundColor))
52 | Direction.Horizontal -> AutoFitTextVertically(text, actualStyle, modifier.fillMaxHeight().background(LocalStyle.current.backgroundColor))
53 | Direction.Vertical -> AutoFitTextHorizontally(text, actualStyle, modifier.fillMaxWidth().background(LocalStyle.current.backgroundColor))
54 | }
55 | }
56 |
57 | @Composable
58 | fun UnscaledText(
59 | text: String,
60 | style: TextStyle,
61 | modifier: Modifier = Modifier,
62 | ) {
63 | BoxWithConstraints(
64 | modifier = modifier,
65 | contentAlignment = Alignment.Center,
66 | ) {
67 | BasicText(
68 | text = text,
69 | style = style.copy(
70 | fontSize = maxHeight.toSp().times(0.1f)
71 | ),
72 | )
73 | }
74 | }
75 |
76 | @Composable
77 | fun AutoFitTextVertically(
78 | text: String,
79 | style: TextStyle,
80 | modifier: Modifier = Modifier,
81 | ) {
82 | BoxWithConstraints(modifier = modifier) {
83 | BasicText(
84 | text = text,
85 | modifier = Modifier.fillMaxHeight(),
86 | style = style.copy(
87 | fontSize = maxHeight.toSp().times(0.65f)
88 | ),
89 | maxLines = 1,
90 | )
91 | }
92 | }
93 |
94 | @Composable
95 | fun AutoFitTextHorizontally(
96 | text: String,
97 | style: TextStyle,
98 | modifier: Modifier = Modifier,
99 | ) {
100 | BoxWithConstraints(modifier = modifier) {
101 | var shrunkFontSize = 400.sp
102 | val calculateIntrinsics = @Composable {
103 | ParagraphIntrinsics(
104 | text = text,
105 | style = style.copy(fontSize = shrunkFontSize),
106 | density = LocalDensity.current,
107 | fontFamilyResolver = LocalFontFamilyResolver.current
108 | )
109 | }
110 |
111 | var intrinsics = calculateIntrinsics()
112 | with(LocalDensity.current) {
113 | while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
114 | shrunkFontSize *= 0.9f
115 | intrinsics = calculateIntrinsics()
116 | }
117 | }
118 |
119 | BasicText(
120 | text = text,
121 | modifier = Modifier.fillMaxWidth(),
122 | style = style.copy(fontSize = shrunkFontSize),
123 | maxLines = 1,
124 | )
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/UnknownItem.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.displayer.display.Padding
5 | import com.displayer.display.Style
6 |
7 | data class UnknownItem(
8 | override val style: Style? = null,
9 | override val padding: Padding,
10 | ) : Item()
11 |
12 | @Composable
13 | fun UnknownItemUi() {
14 | }
15 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/item/Weather.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.item
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Arrangement.spacedBy
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxHeight
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.CompositionLocalProvider
14 | import androidx.compose.runtime.collectAsState
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.rotate
19 | import androidx.compose.ui.graphics.ColorFilter
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.text.TextStyle
22 | import androidx.compose.ui.text.font.FontWeight
23 | import com.displayer.display.Padding
24 | import com.displayer.display.Style
25 | import com.displayer.platform.formatTime
26 | import com.displayer.platform.getIcon
27 | import com.displayer.toSp
28 | import com.displayer.ui.Direction
29 | import com.displayer.ui.Icon
30 | import com.displayer.ui.LocalDimensions
31 | import com.displayer.ui.LocalDirection
32 | import com.displayer.ui.LocalLocale
33 | import com.displayer.ui.LocalStyle
34 | import com.displayer.ui.Text
35 | import com.displayer.weather.WeatherData
36 | import kotlinx.coroutines.flow.Flow
37 | import kotlinx.datetime.Clock
38 | import kotlinx.datetime.Instant
39 | import net.sergeych.sprintf.format
40 | import kotlin.math.roundToInt
41 |
42 | data class Weather(
43 | val observeCurrentWeather: () -> Flow
>,
44 | override val style: Style? = null,
45 | override val padding: Padding,
46 | ) : Item()
47 |
48 | @Composable
49 | fun WeatherUi(item: Weather) {
50 | val weatherData by item.observeCurrentWeather().collectAsState(null)
51 | when (LocalDirection.current) {
52 | Direction.None -> weatherData?.run { WeatherItemOther(this) }
53 | Direction.Horizontal -> weatherData?.firstOrNull()?.run { WeatherItemRow(this) }
54 | Direction.Vertical -> weatherData?.firstOrNull()?.run { WeatherItemColumn(this, Arrangement.Top) }
55 | }
56 | }
57 |
58 | @Composable
59 | fun WeatherItemOther(data: List) {
60 | CompositionLocalProvider(
61 | LocalDirection provides Direction.Vertical,
62 | ) {
63 | val locale = LocalLocale.current
64 | Row {
65 | Spacer(modifier = Modifier.weight(10f))
66 | data.getOrNull(0)?.let { data ->
67 | Box(modifier = Modifier.weight(25f)) {
68 | WeatherItemColumn(data, title = formatTime(Clock.System.now(), locale))
69 | }
70 | }
71 | Spacer(modifier = Modifier.weight(15f))
72 | data.getOrNull(1)?.let { data ->
73 | Box(modifier = Modifier.weight(25f)) {
74 | WeatherItemColumn(data, title = formatTime(Instant.fromEpochSeconds(data.timeStamp), locale))
75 | }
76 | }
77 | Spacer(modifier = Modifier.weight(15f))
78 | data.getOrNull(2)?.let { data ->
79 | Box(modifier = Modifier.weight(25f)) {
80 | WeatherItemColumn(data, title = formatTime(Instant.fromEpochSeconds(data.timeStamp), locale))
81 | }
82 | }
83 | Spacer(modifier = Modifier.weight(10f))
84 | }
85 | }
86 | }
87 |
88 | @Composable
89 | fun WeatherItemRow(data: WeatherData) {
90 | Row(
91 | horizontalArrangement = spacedBy(LocalDimensions.current.baseUnit),
92 | verticalAlignment = Alignment.CenterVertically,
93 | ) {
94 | AutoFitText(text = "%d°".format(data.temperature.roundToInt()))
95 | Image(
96 | painter = data.icon,
97 | contentDescription = null,
98 | modifier = Modifier.fillMaxHeight(0.8f),
99 | colorFilter = ColorFilter.tint(LocalStyle.current.contentColor),
100 | contentScale = ContentScale.FillHeight,
101 | )
102 | data.description?.run {
103 | AutoFitText(text = this)
104 | }
105 | Image(
106 | painter = getIcon(Icon.WindDirection),
107 | contentDescription = null,
108 | modifier = Modifier.fillMaxHeight(0.7f).rotate(180f + (data.windDegrees)),
109 | colorFilter = ColorFilter.tint(LocalStyle.current.contentColor),
110 | contentScale = ContentScale.FillHeight,
111 | )
112 | AutoFitText(text = data.formatWindSpeed())
113 | }
114 | }
115 |
116 | @Composable
117 | fun WeatherItemColumn(data: WeatherData, verticalArrangement: Arrangement.Vertical = spacedBy(LocalDimensions.current.baseUnit), title: String? = null) {
118 | Column(
119 | modifier = Modifier.fillMaxWidth(),
120 | horizontalAlignment = Alignment.CenterHorizontally,
121 | verticalArrangement = verticalArrangement,
122 | ) {
123 | val textStyle = TextStyle.Default.copy(fontSize = (LocalDimensions.current.fontSize * 0.8f).toSp())
124 | val textStyleLarge = TextStyle.Default.copy(fontSize = (LocalDimensions.current.fontSize * 1.8f).toSp())
125 | title?.run {
126 | Text(this, style = TextStyle.Default.copy(fontWeight = FontWeight.Light, fontSize = (LocalDimensions.current.fontSize * 1.4f).toSp()))
127 | }
128 | Text(text = data.formatTemperature(), style = textStyleLarge)
129 | Image(
130 | painter = data.icon,
131 | contentDescription = null,
132 | modifier = Modifier.fillMaxWidth(0.5f),
133 | colorFilter = ColorFilter.tint(LocalStyle.current.contentColor),
134 | contentScale = ContentScale.FillWidth,
135 | )
136 | Row(
137 | verticalAlignment = Alignment.CenterVertically,
138 | ) {
139 | Image(
140 | painter = getIcon(Icon.WindDirection),
141 | contentDescription = null,
142 | modifier = Modifier.fillMaxWidth(0.2f).rotate(180f + (data.windDegrees)),
143 | colorFilter = ColorFilter.tint(LocalStyle.current.contentColor),
144 | contentScale = ContentScale.FillHeight,
145 | )
146 | Text(text = data.formatWindSpeed(), style = textStyle.copy(fontWeight = FontWeight.Light))
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/parser/Dto.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.parser
2 |
3 | import com.displayer.platform.getDefaultCountry
4 | import com.displayer.platform.getDefaultLanguage
5 | import com.displayer.weather.WeatherData
6 | import kotlinx.serialization.SerialName
7 | import kotlinx.serialization.Serializable
8 |
9 | @Serializable
10 | data class DisplayFile(
11 | val parameters: ParametersDto? = null,
12 | val styles: List = emptyList(),
13 | val styleId: String? = null,
14 | val center: ContainerDto,
15 | val left: ContainerDto? = null,
16 | val bottom: ContainerDto? = null,
17 | val bottomHeight: Float? = null,
18 | )
19 |
20 | /*
21 | CONTAINERS
22 | */
23 |
24 | @Serializable
25 | sealed class ContainerDto {
26 | abstract val padding: PaddingDto?
27 | abstract val styleId: String?
28 | abstract val items: List
29 | abstract val divider: ItemDto?
30 | }
31 |
32 | @Serializable
33 | @SerialName("stack")
34 | data class StackDto(
35 | override val padding: PaddingDto? = null,
36 | override val styleId: String? = null,
37 | override val items: List = emptyList(),
38 | override val divider: ItemDto? = null,
39 | val autoAdvanceInSeconds: Int? = null,
40 | ) : ContainerDto()
41 |
42 | @Serializable
43 | @SerialName("column")
44 | data class ColumnDto(
45 | override val padding: PaddingDto? = null,
46 | override val styleId: String? = null,
47 | override val items: List = emptyList(),
48 | override val divider: ItemDto? = null,
49 | val spacing: Float? = null,
50 | val scrollSpeedInSeconds: Float? = null,
51 | ) : ContainerDto()
52 |
53 | @Serializable
54 | @SerialName("row")
55 | data class RowDto(
56 | override val padding: PaddingDto? = null,
57 | override val styleId: String? = null,
58 | override val items: List = emptyList(),
59 | override val divider: ItemDto? = null,
60 | val spacing: Float? = null,
61 | val scrollSpeedInSeconds: Float? = null,
62 | ) : ContainerDto()
63 |
64 | /*
65 | ITEMS
66 | */
67 |
68 | @Serializable
69 | sealed class ItemDto {
70 | abstract val styleId: String?
71 | abstract val padding: PaddingDto?
72 | }
73 |
74 | @Serializable
75 | @SerialName("unknown")
76 | data class UnknownDto(
77 | override val styleId: String? = null,
78 | override val padding: PaddingDto? = null,
79 | ) : ItemDto()
80 |
81 | @Serializable
82 | @SerialName("clock")
83 | data class ClockDto(
84 | override val styleId: String? = null,
85 | override val padding: PaddingDto? = null,
86 | ) : ItemDto()
87 |
88 | @Serializable
89 | @SerialName("text")
90 | data class TextDto(
91 | val text: String,
92 | override val styleId: String? = null,
93 | override val padding: PaddingDto? = null,
94 | ) : ItemDto()
95 |
96 | @Serializable
97 | @SerialName("image")
98 | data class ImageDto(
99 | val url: String,
100 | override val styleId: String? = null,
101 | override val padding: PaddingDto? = null,
102 | val scale: ScaleDto? = null,
103 | ) : ItemDto()
104 |
105 | @Serializable
106 | @SerialName("random")
107 | data class RandomDto(
108 | override val styleId: String? = null,
109 | override val padding: PaddingDto? = null,
110 | val items: List = emptyList(),
111 | ) : ItemDto()
112 |
113 | @Serializable
114 | @SerialName("weather")
115 | data class WeatherDto(
116 | val weatherData: WeatherData? = null,
117 | override val styleId: String? = null,
118 | override val padding: PaddingDto? = null,
119 | ) : ItemDto()
120 |
121 | @Serializable
122 | @SerialName("drink")
123 | data class DrinkDto(
124 | val image: String,
125 | val text: String,
126 | override val styleId: String? = null,
127 | override val padding: PaddingDto? = null,
128 | val spacing: Float? = null,
129 | ) : ItemDto()
130 |
131 | @Serializable
132 | @SerialName("social")
133 | data class SocialDto(
134 | val app: SocialApp,
135 | val account: String,
136 | val text: String? = null,
137 | override val styleId: String? = null,
138 | override val padding: PaddingDto? = null,
139 | ) : ItemDto()
140 |
141 | @Serializable
142 | data class StyleDto(
143 | val id: String,
144 | val contentColor: String? = null,
145 | val backgroundColor: String? = null,
146 | )
147 |
148 | @Serializable
149 | data class PaddingDto(
150 | val horizontal: Float = 0f,
151 | val vertical: Float = 0f,
152 | )
153 |
154 | @Serializable
155 | data class ParametersDto(
156 | val refreshInMinutes: Int = 0,
157 | val language: String = getDefaultLanguage(), // ISO 639-1
158 | val country: String = getDefaultCountry(), // ISO 3166
159 | val zip: String? = null,
160 | val units: Units? = null,
161 | )
162 |
163 | enum class Units {
164 | @SerialName("metric")
165 | Metric,
166 |
167 | @SerialName("imperial")
168 | Imperial,
169 | }
170 |
171 | enum class ScaleDto {
172 | @SerialName("crop")
173 | Crop,
174 |
175 | @SerialName("fit")
176 | Fit,
177 | }
178 |
179 | enum class SocialApp {
180 | @SerialName("facebook")
181 | Facebook,
182 |
183 | @SerialName("instagram")
184 | Instagram,
185 |
186 | @SerialName("tiktok")
187 | Tiktok,
188 |
189 | @SerialName("youtube")
190 | YouTube,
191 |
192 | @SerialName("snapchat")
193 | Snapchat,
194 | }
195 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/parser/Message.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.parser
2 |
3 | data class Message(
4 | val severity: Severity,
5 | val message: String,
6 | )
7 |
--------------------------------------------------------------------------------
/common/src/commonMain/kotlin/com/displayer/display/parser/Parser.kt:
--------------------------------------------------------------------------------
1 | package com.displayer.display.parser
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.layout.ContentScale
5 | import com.displayer.config.Parameters
6 | import com.displayer.display.Padding
7 | import com.displayer.display.Style
8 | import com.displayer.display.container.ColumnLayout
9 | import com.displayer.display.container.Container
10 | import com.displayer.display.container.Empty
11 | import com.displayer.display.container.RowLayout
12 | import com.displayer.display.container.StackLayout
13 | import com.displayer.display.item.Clock
14 | import com.displayer.display.item.DrinkItem
15 | import com.displayer.display.item.Image
16 | import com.displayer.display.item.Item
17 | import com.displayer.display.item.RandomItem
18 | import com.displayer.display.item.SocialItem
19 | import com.displayer.display.item.TextItem
20 | import com.displayer.display.item.UnknownItem
21 | import com.displayer.display.item.Weather
22 | import com.displayer.parse
23 | import com.displayer.weather.WeatherData
24 | import kotlinx.collections.immutable.toImmutableList
25 | import kotlinx.coroutines.flow.Flow
26 |
27 | object Parser {
28 |
29 | fun parseStyle(dto: StyleDto): Result