weather-demo: initial commit
6
.github/workflows/wasm_demos.yaml
vendored
|
@ -58,6 +58,11 @@ jobs:
|
||||||
sed -i "s/#wasm# //" Cargo.toml
|
sed -i "s/#wasm# //" Cargo.toml
|
||||||
wasm-pack build --release --target web --no-default-features --features slint/default,chrono
|
wasm-pack build --release --target web --no-default-features --features slint/default,chrono
|
||||||
working-directory: examples/energy-monitor
|
working-directory: examples/energy-monitor
|
||||||
|
- name: Weather Demo example WASM build
|
||||||
|
run: |
|
||||||
|
sed -i "s/#wasm# //" Cargo.toml
|
||||||
|
wasm-pack build --release --target web --no-default-features --features slint/default,chrono
|
||||||
|
working-directory: examples/weather-demo
|
||||||
- name: "Upload Demo Artifacts"
|
- name: "Upload Demo Artifacts"
|
||||||
if: ${{ inputs.build_artifacts }}
|
if: ${{ inputs.build_artifacts }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
@ -75,6 +80,7 @@ jobs:
|
||||||
examples/plotter/
|
examples/plotter/
|
||||||
examples/opengl_underlay/
|
examples/opengl_underlay/
|
||||||
examples/energy-monitor/
|
examples/energy-monitor/
|
||||||
|
examples/weather-demo/
|
||||||
!/**/.gitignore
|
!/**/.gitignore
|
||||||
- name: Clean cache # Otherwise the cache is much too big
|
- name: Clean cache # Otherwise the cache is much too big
|
||||||
run: |
|
run: |
|
||||||
|
|
24
.reuse/dep5
|
@ -127,3 +127,27 @@ License: CC-BY-4.0
|
||||||
Files: examples/printerdemo_mcu/zephyr/VERSION
|
Files: examples/printerdemo_mcu/zephyr/VERSION
|
||||||
Copyright: Copyright © SixtyFPS GmbH <info@slint.dev>
|
Copyright: Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
License: MIT
|
License: MIT
|
||||||
|
|
||||||
|
Files: examples/weather-demo/wasm/index.html
|
||||||
|
Copyright: Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
License: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
||||||
|
|
||||||
|
Files: examples/weather-demo/docs/*.png
|
||||||
|
Copyright: Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
License: MIT
|
||||||
|
|
||||||
|
Files: examples/weather-demo/ui/assets/icons/*.svg
|
||||||
|
Copyright: Fontawesome project <https://fontawesome.com/license/free>
|
||||||
|
License: CC-BY-4.0
|
||||||
|
|
||||||
|
Files: examples/weather-demo/ui/assets/weathericons-font.ttf
|
||||||
|
Copyright: Weather Icons <http://erikflowers.github.io/weather-icons/>
|
||||||
|
License: OFL-1.1
|
||||||
|
|
||||||
|
Files: examples/weather-demo/android-res/*/ic_launcher.png
|
||||||
|
Copyright: Copyright © Felgo GmbH <contact@felgo.com>
|
||||||
|
License: CC-BY-ND-4.0
|
||||||
|
|
||||||
|
Files: examples/weather-demo/ui/assets/felgo-logo.svg
|
||||||
|
Copyright: Copyright © Felgo GmbH <contact@felgo.com>
|
||||||
|
License: CC-BY-ND-4.0
|
||||||
|
|
|
@ -32,6 +32,7 @@ members = [
|
||||||
'examples/energy-monitor',
|
'examples/energy-monitor',
|
||||||
'examples/mcu-board-support',
|
'examples/mcu-board-support',
|
||||||
'examples/uefi-demo',
|
'examples/uefi-demo',
|
||||||
|
'examples/weather-demo',
|
||||||
'helper_crates/const-field-offset',
|
'helper_crates/const-field-offset',
|
||||||
'helper_crates/vtable',
|
'helper_crates/vtable',
|
||||||
'helper_crates/vtable/macro',
|
'helper_crates/vtable/macro',
|
||||||
|
|
|
@ -176,6 +176,18 @@ Our implementations of the ["7GUIs"](https://7guis.github.io/7guis/) Tasks.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### [`weather-demo`](./weather-demo)
|
||||||
|
|
||||||
|
A simple, cross-platform (Desktop, Android, Wasm) weather application using real weather data from the [OpenWeather](https://openweathermap.org/) API.
|
||||||
|
|
||||||
|
| `.slint` Design | Rust Source (Desktop) | Rust Source (Android / Wasm) | Online wasm Preview | Open in SlintPad |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| [`main.slint`](./weather-demo/ui/main.slint) | [`main.rs`](./weather-demo/src/main.rs) | [`lib.rs`](./weather-demo/src/lib.rs) | [Online simulation](https://slint.dev/snapshots/master/demos/weather-demo/) | [Preview in Online Code Editor](https://slint.dev/snapshots/master/editor?load_url=https://raw.githubusercontent.com/slint-ui/slint/master/examples/weather-demo/ui/main.slint) |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### External examples
|
### External examples
|
||||||
|
|
||||||
* [Cargo UI](https://github.com/slint-ui/cargo-ui): A rust application that makes use of threads in the background.
|
* [Cargo UI](https://github.com/slint-ui/cargo-ui): A rust application that makes use of threads in the background.
|
||||||
|
|
67
examples/weather-demo/Cargo.toml
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "rusty-weather"
|
||||||
|
version = "1.7.0"
|
||||||
|
authors = ["FELGO GmbH <contact@felgo.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
build = "build.rs"
|
||||||
|
publish = false
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = "1.12.0"
|
||||||
|
chrono = { version = "0.4.38", optional = true, default-features = false, features = ["clock"] }
|
||||||
|
directories = "5.0.1"
|
||||||
|
log = "0.4.21"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0.115"
|
||||||
|
slint = { path = "../../api/rs/slint", features = [ "backend-android-activity-06" ] }
|
||||||
|
|
||||||
|
[target.'cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))'.dependencies]
|
||||||
|
env_logger = "0.11.3"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
android_logger = "0.14.1"
|
||||||
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
openweather_sdk = "0.1.8"
|
||||||
|
tokio = "1.37.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
console_log = "1.0"
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
slint-build = { path = "../../api/rs/build" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["chrono"]
|
||||||
|
|
||||||
|
# Android-activity / wasm support
|
||||||
|
[lib]
|
||||||
|
name="rusty_weather_lib"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
# Andoroid settings
|
||||||
|
# See more: https://github.com/rust-mobile/cargo-apk?tab=readme-ov-file#manifest
|
||||||
|
[package.metadata.android]
|
||||||
|
package = "dev.slint.examples.weatherdemo"
|
||||||
|
resources = "android-res"
|
||||||
|
build_targets = [ "aarch64-linux-android" ]
|
||||||
|
|
||||||
|
[package.metadata.android.sdk]
|
||||||
|
min_sdk_version = 29
|
||||||
|
target_sdk_version = 32
|
||||||
|
|
||||||
|
[package.metadata.android.application]
|
||||||
|
label = "Rusty Weather"
|
||||||
|
icon = "@mipmap/ic_launcher"
|
||||||
|
|
||||||
|
[[package.metadata.android.uses_permission]]
|
||||||
|
name = "android.permission.INTERNET"
|
77
examples/weather-demo/README.md
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
|
||||||
|
|
||||||
|
# Rusty Weather
|
||||||
|
|
||||||
|
Rusty Weather is a weather application made by [Felgo](https://felgo.com/).
|
||||||
|
|
||||||
|
The application retrieves weather data from the [OpenWeather](https://openweathermap.org/) API to provide:
|
||||||
|
* Real-time weather data,
|
||||||
|
* 8-day forecasts data,
|
||||||
|
* Temperatures at particular times of the day,
|
||||||
|
* Daily rain amount and rain probability,
|
||||||
|
* UV index level,
|
||||||
|
* support for various locations around the globe.
|
||||||
|
|
||||||
|
The project demonstrates how to write a cross-platform Rust GUI application using the [Slint](https://slint.dev/) toolkit.
|
||||||
|
It includes subjects like:
|
||||||
|
* responsive layouts and adaptions for different screen sizes and orientations,
|
||||||
|
* providing fully customized look and feel with custom widgets,
|
||||||
|
* integrating Slint code with the Rust backend code,
|
||||||
|
* using `async` features with a Slint-based application and combining it with the Tokio runtime.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
<img src="docs/img/desktop-preview.png" height="415px" />
|
||||||
|
<img src="docs/img/android-preview.png" height="415px" hspace="5" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Weather data
|
||||||
|
To enable real weather data from the [OpenWeather](https://openweathermap.org/) API, you must provide the `RUSTY_WEATHER_API_KEY` environment variable with your API key at build time. The [OpenCall API](https://openweathermap.org/price#onecall) subscription is required.
|
||||||
|
|
||||||
|
If you do not provide the key, the application loads the dummy data instead.
|
||||||
|
|
||||||
|
**Note:** You cannot use real weather data for the WebAssembly target.
|
||||||
|
|
||||||
|
# Supported platforms
|
||||||
|
|
||||||
|
## Desktop
|
||||||
|
The application runs on all desktop platforms (Windows, Linux and macOS).
|
||||||
|
|
||||||
|
To start the application, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Android
|
||||||
|
To be able to compile the application for Android, you must follow an initial setup. The instruction is available in [Slint's documentation](https://snapshots.slint.dev/master/docs/rust/slint/android/#building-and-deploying).
|
||||||
|
|
||||||
|
***Note:*** To build `openssl` for Android, you must ensure that the proper development libraries are available in the system and provided in the `PATH` variable.
|
||||||
|
|
||||||
|
To start the application, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo apk run --lib
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebAssembly
|
||||||
|
It is also possible to embed the application in a web page. You need to install the `wasm-pack` crate for this.
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo install wasm-pack
|
||||||
|
```
|
||||||
|
|
||||||
|
To build the application, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
wasm-pack build --target web --out-dir <output-dir>/pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
To run locally:
|
||||||
|
|
||||||
|
```
|
||||||
|
cp <source-dir>/wasm/index.html <output-dir>/ # you can also provide your HTML file
|
||||||
|
cd <output-dir> & python3 -m http.server
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, you can access the application at http://localhost:8000/.
|
BIN
examples/weather-demo/android-res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
examples/weather-demo/android-res/mipmap-ldpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
examples/weather-demo/android-res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
examples/weather-demo/android-res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6 KiB |
BIN
examples/weather-demo/android-res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
examples/weather-demo/android-res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 19 KiB |
10
examples/weather-demo/build.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env::set_var("SLINT_ENABLE_EXPERIMENTAL_FEATURES", "true");
|
||||||
|
|
||||||
|
slint_build::compile("ui/main.slint").unwrap();
|
||||||
|
}
|
BIN
examples/weather-demo/docs/img/android-preview.png
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
examples/weather-demo/docs/img/desktop-preview.png
Normal file
After Width: | Height: | Size: 87 KiB |
83
examples/weather-demo/src/app_main.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use crate::ui::*;
|
||||||
|
|
||||||
|
use crate::weather;
|
||||||
|
use weather::DummyWeatherController;
|
||||||
|
use weather::{WeatherControllerPointer, WeatherControllerSharedPointer, WeatherDisplayController};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use weather::OpenWeatherController;
|
||||||
|
|
||||||
|
pub struct AppHandler {
|
||||||
|
weather_controller: WeatherControllerSharedPointer,
|
||||||
|
weather_display_controller: WeatherDisplayController,
|
||||||
|
window: Option<AppWindow>,
|
||||||
|
support_add_city: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppHandler {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
|
||||||
|
let mut support_add_city = false;
|
||||||
|
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
|
||||||
|
let mut data_controller_opt: Option<WeatherControllerPointer> = None;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
if let Some(api_key) = std::option_env!("RUSTY_WEATHER_API_KEY") {
|
||||||
|
data_controller_opt = Some(Box::new(OpenWeatherController::new(api_key.into())));
|
||||||
|
support_add_city = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data_controller = match data_controller_opt {
|
||||||
|
Some(data_contoller_some) => data_contoller_some,
|
||||||
|
None => {
|
||||||
|
log::info!("Weather API key not provided. Using dummy data.");
|
||||||
|
Box::new(DummyWeatherController::new())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let data_controller: WeatherControllerSharedPointer = Arc::new(Mutex::new(data_controller));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
weather_controller: data_controller.clone(),
|
||||||
|
weather_display_controller: WeatherDisplayController::new(&data_controller),
|
||||||
|
window: None,
|
||||||
|
support_add_city,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) {
|
||||||
|
log::debug!("Saving state");
|
||||||
|
if let Err(e) = self.weather_controller.lock().unwrap().save() {
|
||||||
|
log::warn!("Error while saving state: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
|
||||||
|
pub fn reload(&self) {
|
||||||
|
log::debug!("Reloading state");
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
self.weather_display_controller.refresh(window); // load new weather data
|
||||||
|
} else {
|
||||||
|
log::warn!("Cannot reload state, window not available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize_ui(&mut self) {
|
||||||
|
let window = AppWindow::new().expect("Cannot create main window!");
|
||||||
|
self.weather_display_controller.initialize_ui(&window, self.support_add_city);
|
||||||
|
|
||||||
|
self.window = Some(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self) -> Result<(), slint::PlatformError> {
|
||||||
|
let window = self.window.as_ref().expect("Cannot access main window!");
|
||||||
|
self.weather_display_controller.load(window);
|
||||||
|
window.run()
|
||||||
|
}
|
||||||
|
}
|
90
examples/weather-demo/src/lib.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#![cfg(any(target_os = "android", target_arch = "wasm32"))]
|
||||||
|
|
||||||
|
pub mod ui {
|
||||||
|
slint::include_modules!();
|
||||||
|
}
|
||||||
|
|
||||||
|
mod app_main;
|
||||||
|
mod weather;
|
||||||
|
|
||||||
|
use crate::app_main::AppHandler;
|
||||||
|
|
||||||
|
// Android
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
use {
|
||||||
|
crate::android_activity::{MainEvent, PollEvent},
|
||||||
|
core::cell::RefCell,
|
||||||
|
slint::android::android_activity,
|
||||||
|
std::rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[no_mangle]
|
||||||
|
fn android_main(android_app: slint::android::AndroidApp) -> Result<(), slint::PlatformError> {
|
||||||
|
android_logger::init_once(android_logger::Config::default().with_max_level(
|
||||||
|
if cfg!(debug_assertions) { log::LevelFilter::Debug } else { log::LevelFilter::Info },
|
||||||
|
));
|
||||||
|
|
||||||
|
let app_handler = Rc::new(RefCell::new(AppHandler::new()));
|
||||||
|
|
||||||
|
// initialize android before creating main window
|
||||||
|
slint::android::init_with_event_listener(android_app, {
|
||||||
|
let app_handler = app_handler.clone();
|
||||||
|
move |event| match event {
|
||||||
|
PollEvent::Main(main_event) => match main_event {
|
||||||
|
MainEvent::Start => {
|
||||||
|
app_handler.borrow().reload();
|
||||||
|
}
|
||||||
|
MainEvent::Resume { .. } => {
|
||||||
|
app_handler.borrow().reload();
|
||||||
|
}
|
||||||
|
MainEvent::SaveState { .. } => {
|
||||||
|
app_handler.borrow().save();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
// create main window here
|
||||||
|
let mut app_handler = app_handler.borrow_mut();
|
||||||
|
app_handler.initialize_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_handler = app_handler.borrow();
|
||||||
|
app_handler.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wasm
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||||
|
pub fn main() {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
console_log::init_with_level(if cfg!(debug_assertions) {
|
||||||
|
log::Level::Debug
|
||||||
|
} else {
|
||||||
|
log::Level::Info
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let mut app_handler = AppHandler::new();
|
||||||
|
app_handler.initialize_ui();
|
||||||
|
|
||||||
|
let res = app_handler.run();
|
||||||
|
app_handler.save();
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Runtime error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
examples/weather-demo/src/main.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#![cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||||
|
|
||||||
|
use crate::app_main::AppHandler;
|
||||||
|
|
||||||
|
pub mod ui {
|
||||||
|
slint::include_modules!();
|
||||||
|
}
|
||||||
|
|
||||||
|
mod app_main;
|
||||||
|
mod weather;
|
||||||
|
|
||||||
|
fn main() -> Result<(), slint::PlatformError> {
|
||||||
|
env_logger::Builder::default()
|
||||||
|
.filter_level(if cfg!(debug_assertions) {
|
||||||
|
log::LevelFilter::Debug
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Info
|
||||||
|
})
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let mut app_handler = AppHandler::new();
|
||||||
|
app_handler.initialize_ui();
|
||||||
|
|
||||||
|
let res = app_handler.run();
|
||||||
|
app_handler.save();
|
||||||
|
res
|
||||||
|
}
|
620
examples/weather-demo/src/weather/dummyweather.json
Normal file
|
@ -0,0 +1,620 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"city_data": {
|
||||||
|
"lat": 52.51703643798828,
|
||||||
|
"lon": 13.388859748840332,
|
||||||
|
"city_name": "Berlin"
|
||||||
|
},
|
||||||
|
"weather_data": {
|
||||||
|
"current_data": {
|
||||||
|
"condition": "PartiallyCloudy",
|
||||||
|
"description": "broken clouds",
|
||||||
|
"current_temperature": 15.24,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 12.39,
|
||||||
|
"max": 19.27,
|
||||||
|
"morning": 14.01,
|
||||||
|
"day": 18.62,
|
||||||
|
"evening": 15.83,
|
||||||
|
"night": 13.8
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 1,
|
||||||
|
"rain_volume": 2.77,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 2.3
|
||||||
|
},
|
||||||
|
"forecast_data": [
|
||||||
|
{
|
||||||
|
"day_name": "d0",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "moderate rain",
|
||||||
|
"current_temperature": 16.18,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 9.56,
|
||||||
|
"max": 17.94,
|
||||||
|
"morning": 10.37,
|
||||||
|
"day": 16.18,
|
||||||
|
"evening": 16.71,
|
||||||
|
"night": 12.45
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 1,
|
||||||
|
"rain_volume": 2.77,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 3.82
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d1",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "moderate rain",
|
||||||
|
"current_temperature": 15.94,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 9.34,
|
||||||
|
"max": 18.96,
|
||||||
|
"morning": 10.31,
|
||||||
|
"day": 15.94,
|
||||||
|
"evening": 16.76,
|
||||||
|
"night": 12.56
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 1,
|
||||||
|
"rain_volume": 2.87,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 3.82
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d2",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Cloudy",
|
||||||
|
"description": "broken clouds",
|
||||||
|
"current_temperature": 15.73,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 9.17,
|
||||||
|
"max": 17.9,
|
||||||
|
"morning": 10.65,
|
||||||
|
"day": 15.73,
|
||||||
|
"evening": 17.14,
|
||||||
|
"night": 12.34
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 5.16
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d3",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Cloudy",
|
||||||
|
"description": "broken clouds",
|
||||||
|
"current_temperature": 16.99,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 9.47,
|
||||||
|
"max": 17.69,
|
||||||
|
"morning": 10.72,
|
||||||
|
"day": 16.99,
|
||||||
|
"evening": 16.7,
|
||||||
|
"night": 14.37
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 5.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d4",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "light rain",
|
||||||
|
"current_temperature": 22.95,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 13.23,
|
||||||
|
"max": 22.95,
|
||||||
|
"morning": 15.19,
|
||||||
|
"day": 22.95,
|
||||||
|
"evening": 20.97,
|
||||||
|
"night": 17.48
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0.2,
|
||||||
|
"rain_volume": 0.15,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 5.97
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d5",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "light rain",
|
||||||
|
"current_temperature": 24.07,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 16.06,
|
||||||
|
"max": 24.85,
|
||||||
|
"morning": 18.61,
|
||||||
|
"day": 24.07,
|
||||||
|
"evening": 21.87,
|
||||||
|
"night": 17.92
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0.2,
|
||||||
|
"rain_volume": 0.19,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d6",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Cloudy",
|
||||||
|
"description": "overcast clouds",
|
||||||
|
"current_temperature": 23.27,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 14.73,
|
||||||
|
"max": 23.27,
|
||||||
|
"morning": 16.66,
|
||||||
|
"day": 23.27,
|
||||||
|
"evening": 22.33,
|
||||||
|
"night": 18.94
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d7",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "light rain",
|
||||||
|
"current_temperature": 25.15,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 16.69,
|
||||||
|
"max": 25.29,
|
||||||
|
"morning": 19.84,
|
||||||
|
"day": 25.15,
|
||||||
|
"evening": 24.47,
|
||||||
|
"night": 20.54
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0.87,
|
||||||
|
"rain_volume": 0.69,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city_data": {
|
||||||
|
"lat": 48.20835494995117,
|
||||||
|
"lon": 16.37250328063965,
|
||||||
|
"city_name": "Vienna"
|
||||||
|
},
|
||||||
|
"weather_data": {
|
||||||
|
"current_data": {
|
||||||
|
"condition": "Rainy",
|
||||||
|
"description": "light intensity shower rain",
|
||||||
|
"current_temperature": 17.33,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 15.84,
|
||||||
|
"max": 18.96,
|
||||||
|
"morning": 17.5,
|
||||||
|
"day": 17.33,
|
||||||
|
"evening": 16.89,
|
||||||
|
"night": 17.5
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 1,
|
||||||
|
"rain_volume": 10.78,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 2.63
|
||||||
|
},
|
||||||
|
"forecast_data": [
|
||||||
|
{
|
||||||
|
"day_name": "d0",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "moderate rain",
|
||||||
|
"current_temperature": 17.74,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 15.84,
|
||||||
|
"max": 18.96,
|
||||||
|
"morning": 17.5,
|
||||||
|
"day": 17.74,
|
||||||
|
"evening": 16.89,
|
||||||
|
"night": 17.5
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 1,
|
||||||
|
"rain_volume": 10.78,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 2.63
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d1",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Cloudy",
|
||||||
|
"description": "overcast clouds",
|
||||||
|
"current_temperature": 18.14,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 15.06,
|
||||||
|
"max": 20.2,
|
||||||
|
"morning": 15.76,
|
||||||
|
"day": 18.14,
|
||||||
|
"evening": 19.76,
|
||||||
|
"night": 16.07
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0.11,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 4.81
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d2",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "light rain",
|
||||||
|
"current_temperature": 12.23,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 12.08,
|
||||||
|
"max": 15.59,
|
||||||
|
"morning": 13.42,
|
||||||
|
"day": 12.23,
|
||||||
|
"evening": 13.86,
|
||||||
|
"night": 12.08
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 1,
|
||||||
|
"rain_volume": 1.82,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 0.78
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d3",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "light rain",
|
||||||
|
"current_temperature": 18.83,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 11.93,
|
||||||
|
"max": 20.12,
|
||||||
|
"morning": 12.56,
|
||||||
|
"day": 18.83,
|
||||||
|
"evening": 19.76,
|
||||||
|
"night": 14.12
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 1,
|
||||||
|
"rain_volume": 1.05,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 2.35
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d4",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "MostlyCloudy",
|
||||||
|
"description": "scattered clouds",
|
||||||
|
"current_temperature": 21.13,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 11.73,
|
||||||
|
"max": 23.26,
|
||||||
|
"morning": 11.73,
|
||||||
|
"day": 21.13,
|
||||||
|
"evening": 23.24,
|
||||||
|
"night": 17.1
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 6.17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d5",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 23.55,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 14.71,
|
||||||
|
"max": 26,
|
||||||
|
"morning": 14.71,
|
||||||
|
"day": 23.55,
|
||||||
|
"evening": 25.7,
|
||||||
|
"night": 20.03
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d6",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "SunnyRainy",
|
||||||
|
"description": "light rain",
|
||||||
|
"current_temperature": 21.59,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 16.28,
|
||||||
|
"max": 23.22,
|
||||||
|
"morning": 16.28,
|
||||||
|
"day": 21.59,
|
||||||
|
"evening": 22.76,
|
||||||
|
"night": 17.24
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0.38,
|
||||||
|
"rain_volume": 0.27,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d7",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 24.89,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 14.89,
|
||||||
|
"max": 27.33,
|
||||||
|
"morning": 14.89,
|
||||||
|
"day": 24.89,
|
||||||
|
"evening": 27.33,
|
||||||
|
"night": 23.1
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"city_data": {
|
||||||
|
"lat": 25.774173736572266,
|
||||||
|
"lon": -80.19361877441406,
|
||||||
|
"city_name": "Miami"
|
||||||
|
},
|
||||||
|
"weather_data": {
|
||||||
|
"current_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 20.14,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 20.14,
|
||||||
|
"max": 30.88,
|
||||||
|
"morning": 20.83,
|
||||||
|
"day": 30.88,
|
||||||
|
"evening": 26.41,
|
||||||
|
"night": 23.2
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 11.85
|
||||||
|
},
|
||||||
|
"forecast_data": [
|
||||||
|
{
|
||||||
|
"day_name": "d0",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 30.88,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 20.14,
|
||||||
|
"max": 30.88,
|
||||||
|
"morning": 20.83,
|
||||||
|
"day": 30.88,
|
||||||
|
"evening": 26.41,
|
||||||
|
"night": 23.2
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 11.85
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d1",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 28.94,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 21.56,
|
||||||
|
"max": 29.68,
|
||||||
|
"morning": 21.61,
|
||||||
|
"day": 28.94,
|
||||||
|
"evening": 26.24,
|
||||||
|
"night": 23.22
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 11.62
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d2",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 29.32,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 21.09,
|
||||||
|
"max": 31.17,
|
||||||
|
"morning": 21.09,
|
||||||
|
"day": 29.32,
|
||||||
|
"evening": 26.99,
|
||||||
|
"night": 23.65
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 11.67
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d3",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 27.11,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 21.26,
|
||||||
|
"max": 30.37,
|
||||||
|
"morning": 21.26,
|
||||||
|
"day": 27.11,
|
||||||
|
"evening": 28.03,
|
||||||
|
"night": 22.17
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 11.53
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d4",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 25.7,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 19.67,
|
||||||
|
"max": 28.59,
|
||||||
|
"morning": 19.67,
|
||||||
|
"day": 25.7,
|
||||||
|
"evening": 27.08,
|
||||||
|
"night": 20.91
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d5",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 25.27,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 18.6,
|
||||||
|
"max": 27.95,
|
||||||
|
"morning": 18.6,
|
||||||
|
"day": 25.27,
|
||||||
|
"evening": 25.42,
|
||||||
|
"night": 20
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d6",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 26.84,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 19.13,
|
||||||
|
"max": 28.99,
|
||||||
|
"morning": 19.13,
|
||||||
|
"day": 26.84,
|
||||||
|
"evening": 27.56,
|
||||||
|
"night": 21.58
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day_name": "d7",
|
||||||
|
"weather_data": {
|
||||||
|
"condition": "Sunny",
|
||||||
|
"description": "clear sky",
|
||||||
|
"current_temperature": 28.99,
|
||||||
|
"detailed_temperature": {
|
||||||
|
"min": 20.87,
|
||||||
|
"max": 30.64,
|
||||||
|
"morning": 20.87,
|
||||||
|
"day": 28.99,
|
||||||
|
"evening": 29.22,
|
||||||
|
"night": 22.64
|
||||||
|
},
|
||||||
|
"precipitation": {
|
||||||
|
"probability": 0,
|
||||||
|
"rain_volume": 0,
|
||||||
|
"snow_volume": 0
|
||||||
|
},
|
||||||
|
"uv_index": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
93
examples/weather-demo/src/weather/dummyweathercontroller.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
|
||||||
|
use crate::weather::utils::*;
|
||||||
|
use crate::weather::weathercontroller::{
|
||||||
|
CityData, CityWeatherData, GeoLocationData, WeatherController,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct DummyWeatherController {
|
||||||
|
city_weather_data: Vec<CityWeatherData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DummyWeatherController {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { city_weather_data: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_dummy_data() -> Vec<CityWeatherData> {
|
||||||
|
let json_data = std::include_str!("./dummyweather.json");
|
||||||
|
|
||||||
|
match serde_json::from_str::<Vec<CityWeatherData>>(json_data) {
|
||||||
|
Ok(weather_data) => {
|
||||||
|
// fix day names
|
||||||
|
let mut weather_data = weather_data.clone();
|
||||||
|
for city_data in &mut weather_data {
|
||||||
|
let forecast_data = &mut (city_data.weather_data.forecast_data);
|
||||||
|
for (index, data) in forecast_data.iter_mut().enumerate() {
|
||||||
|
if index == 0 {
|
||||||
|
data.day_name = "Today".into();
|
||||||
|
} else {
|
||||||
|
data.day_name =
|
||||||
|
get_day_from_datetime(Utc::now() + Duration::days(index as i64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return weather_data;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Cannot read dummy weather data! Error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeatherController for DummyWeatherController {
|
||||||
|
fn load(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.city_weather_data = Self::generate_dummy_data();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_cities(&mut self) -> Result<Vec<CityWeatherData>, Box<dyn std::error::Error>> {
|
||||||
|
Ok(self.city_weather_data.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_city(
|
||||||
|
&mut self,
|
||||||
|
_city: CityData,
|
||||||
|
) -> Result<Option<CityWeatherData>, Box<dyn std::error::Error>> {
|
||||||
|
// not supported for the dummy data
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reorder_cities(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
new_index: usize,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.city_weather_data.swap(index, new_index);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_city(&mut self, index: usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.city_weather_data.remove(index);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_location(
|
||||||
|
&self,
|
||||||
|
_query: String,
|
||||||
|
) -> Result<Vec<GeoLocationData>, Box<dyn std::error::Error>> {
|
||||||
|
// not supported for the dummy data
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
}
|
21
examples/weather-demo/src/weather/mod.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
mod weathercontroller;
|
||||||
|
mod weatherdisplaycontroller;
|
||||||
|
|
||||||
|
mod dummyweathercontroller;
|
||||||
|
|
||||||
|
pub use weathercontroller::WeatherControllerPointer;
|
||||||
|
pub use weathercontroller::WeatherControllerSharedPointer;
|
||||||
|
pub use weatherdisplaycontroller::WeatherDisplayController;
|
||||||
|
|
||||||
|
pub use dummyweathercontroller::DummyWeatherController;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
mod openweathercontroller;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub use openweathercontroller::OpenWeatherController;
|
||||||
|
|
||||||
|
pub mod utils;
|
417
examples/weather-demo/src/weather/openweathercontroller.rs
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
|
use openweather_sdk::responses::{GeocodingResponse, OneCallResponse};
|
||||||
|
use openweather_sdk::{Language, OpenWeather, Units};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::io::{BufReader, BufWriter, Write};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::vec;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::weather::utils::*;
|
||||||
|
use crate::weather::weathercontroller::{
|
||||||
|
CityData, CityWeatherData, DayWeatherData, ForecastWeatherData, GeoLocationData,
|
||||||
|
PrecipitationData, TemperatureData, WeatherCondition, WeatherController, WeatherData,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
const CITIES_STORED_FILE_NAME: &str = "cities_data.json";
|
||||||
|
const ORGANIZATION_QUALIFIER: &str = "dev"; // have to match android app name in cargo.toml
|
||||||
|
const ORGANIZATION_NAME: &str = "slint.examples"; // have to match android app name in cargo.toml
|
||||||
|
const APPLICATION_NAME: &str = "weatherdemo"; // have to match app android name in cargo.toml
|
||||||
|
|
||||||
|
fn project_data_dir() -> Option<PathBuf> {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
match env::var("ANDROID_DATA") {
|
||||||
|
Ok(data_root) => {
|
||||||
|
if data_root.is_empty() {
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
let project_name = format!(
|
||||||
|
"{}.{}.{}",
|
||||||
|
ORGANIZATION_QUALIFIER, ORGANIZATION_NAME, APPLICATION_NAME
|
||||||
|
);
|
||||||
|
return Some(PathBuf::from(format!(
|
||||||
|
"{}/data/{}/files",
|
||||||
|
data_root, project_name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_e) => {
|
||||||
|
log::warn!("Cannot read ANDROID_DATA, persistence not avaialble.");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||||
|
{
|
||||||
|
if let Some(project_dir) = directories::ProjectDirs::from(
|
||||||
|
ORGANIZATION_QUALIFIER,
|
||||||
|
ORGANIZATION_NAME,
|
||||||
|
APPLICATION_NAME,
|
||||||
|
) {
|
||||||
|
return Some(project_dir.data_dir().to_path_buf());
|
||||||
|
};
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct WeatherClient {
|
||||||
|
pub city_data: CityData,
|
||||||
|
pub weather_data: Option<OneCallResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OpenWeatherController {
|
||||||
|
tokio_runtime: tokio::runtime::Runtime,
|
||||||
|
weather_api: OpenWeather,
|
||||||
|
city_clients: Arc<Mutex<Vec<WeatherClient>>>,
|
||||||
|
storage_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenWeatherController {
|
||||||
|
pub fn new(api_key: String) -> Self {
|
||||||
|
let mut weather_api = OpenWeather::new(api_key, Units::Metric, Language::English);
|
||||||
|
weather_api.one_call.fields.minutely = false;
|
||||||
|
weather_api.one_call.fields.hourly = false;
|
||||||
|
weather_api.one_call.fields.alerts = false;
|
||||||
|
|
||||||
|
let storage_path;
|
||||||
|
if let Some(project_dir) = project_data_dir() {
|
||||||
|
storage_path = Some(project_dir.as_path().join(CITIES_STORED_FILE_NAME));
|
||||||
|
} else {
|
||||||
|
storage_path = None;
|
||||||
|
log::error!("Failed to initialize project dir. Persistent data will not be loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
tokio_runtime: tokio::runtime::Runtime::new().unwrap(),
|
||||||
|
weather_api,
|
||||||
|
city_clients: Arc::new(Mutex::new(vec![])),
|
||||||
|
storage_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weather_condition_from_icon_icon_type(icon_type: &str) -> WeatherCondition {
|
||||||
|
match icon_type {
|
||||||
|
"01d" | "01n" => WeatherCondition::Sunny,
|
||||||
|
"02d" | "02n" => WeatherCondition::PartiallyCloudy,
|
||||||
|
"03d" | "03n" => WeatherCondition::MostlyCloudy,
|
||||||
|
"04d" | "04n" => WeatherCondition::Cloudy,
|
||||||
|
"10d" | "10n" => WeatherCondition::SunnyRainy,
|
||||||
|
"09d" | "09n" => WeatherCondition::Rainy,
|
||||||
|
"11d" | "11n" => WeatherCondition::Stormy,
|
||||||
|
"13d" | "13n" => WeatherCondition::Snowy,
|
||||||
|
"50d" | "50n" => WeatherCondition::Foggy,
|
||||||
|
_ => WeatherCondition::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_day_weather_data_from_response(
|
||||||
|
weather_response: &Option<OneCallResponse>,
|
||||||
|
) -> DayWeatherData {
|
||||||
|
if let Some(weather_data) = weather_response {
|
||||||
|
if let Some(current) = &weather_data.current {
|
||||||
|
let weather_details = ¤t.weather[0];
|
||||||
|
let today_weather_info =
|
||||||
|
weather_data.daily.as_ref().and_then(|daily| daily.first());
|
||||||
|
|
||||||
|
let detailed_temp = match today_weather_info {
|
||||||
|
Some(info) => {
|
||||||
|
let temp = info.temp;
|
||||||
|
TemperatureData {
|
||||||
|
min: temp.min,
|
||||||
|
max: temp.max,
|
||||||
|
|
||||||
|
morning: temp.morn,
|
||||||
|
day: temp.day,
|
||||||
|
evening: temp.eve,
|
||||||
|
night: temp.night,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => TemperatureData {
|
||||||
|
min: current.temp,
|
||||||
|
max: current.temp,
|
||||||
|
|
||||||
|
morning: current.temp,
|
||||||
|
day: current.temp,
|
||||||
|
evening: current.temp,
|
||||||
|
night: current.temp,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return DayWeatherData {
|
||||||
|
description: weather_details.description.clone(),
|
||||||
|
condition: Self::weather_condition_from_icon_icon_type(&weather_details.icon),
|
||||||
|
current_temperature: current.temp,
|
||||||
|
detailed_temperature: detailed_temp,
|
||||||
|
precipitation: PrecipitationData::default(),
|
||||||
|
uv_index: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DayWeatherData::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forecast_day_weather_data_from_response(
|
||||||
|
weather_response: &Option<OneCallResponse>,
|
||||||
|
) -> Vec<ForecastWeatherData> {
|
||||||
|
let mut forecast_weather_info: Vec<ForecastWeatherData> = vec![];
|
||||||
|
|
||||||
|
if let Some(weather_data) = weather_response {
|
||||||
|
if let Some(daily_weather_data) = &weather_data.daily {
|
||||||
|
for day_weather_data in daily_weather_data.iter() {
|
||||||
|
if let Some(datetime) = DateTime::from_timestamp(day_weather_data.datetime, 0) {
|
||||||
|
let weather_details = &day_weather_data.weather[0];
|
||||||
|
|
||||||
|
let detailed_temperature = TemperatureData {
|
||||||
|
min: day_weather_data.temp.min,
|
||||||
|
max: day_weather_data.temp.max,
|
||||||
|
|
||||||
|
morning: day_weather_data.temp.morn,
|
||||||
|
day: day_weather_data.temp.day,
|
||||||
|
evening: day_weather_data.temp.eve,
|
||||||
|
night: day_weather_data.temp.night,
|
||||||
|
};
|
||||||
|
|
||||||
|
let precipitation: PrecipitationData = PrecipitationData {
|
||||||
|
probability: day_weather_data.pop,
|
||||||
|
rain_volume: day_weather_data.rain.unwrap_or(0 as f64),
|
||||||
|
snow_volume: day_weather_data.snow.unwrap_or(0 as f64),
|
||||||
|
};
|
||||||
|
|
||||||
|
let day_weather_info = DayWeatherData {
|
||||||
|
description: weather_details.description.clone(),
|
||||||
|
condition: Self::weather_condition_from_icon_icon_type(
|
||||||
|
&weather_details.icon,
|
||||||
|
),
|
||||||
|
current_temperature: day_weather_data.temp.day,
|
||||||
|
detailed_temperature,
|
||||||
|
precipitation,
|
||||||
|
uv_index: day_weather_data.uvi,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: localization
|
||||||
|
forecast_weather_info.push(ForecastWeatherData {
|
||||||
|
day_name: get_day_from_datetime(datetime),
|
||||||
|
weather_data: day_weather_info,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forecast_weather_info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn city_weather_data_from_client(city_client: &WeatherClient) -> CityWeatherData {
|
||||||
|
let current_data = Self::current_day_weather_data_from_response(&city_client.weather_data);
|
||||||
|
let forecast_data =
|
||||||
|
Self::forecast_day_weather_data_from_response(&city_client.weather_data);
|
||||||
|
|
||||||
|
CityWeatherData {
|
||||||
|
city_data: city_client.city_data.clone(),
|
||||||
|
weather_data: WeatherData { current_data, forecast_data },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geo_location_data_from_response(response: &GeocodingResponse) -> GeoLocationData {
|
||||||
|
GeoLocationData {
|
||||||
|
name: response.name.clone(),
|
||||||
|
state: response.state.clone(),
|
||||||
|
country: response.country.clone(),
|
||||||
|
lat: response.lat,
|
||||||
|
lon: response.lon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeatherController for OpenWeatherController {
|
||||||
|
fn load(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if let Some(storage_path) = &self.storage_path {
|
||||||
|
log::debug!("Loading data from: {:?}", storage_path.to_str());
|
||||||
|
|
||||||
|
let file = File::open(storage_path.as_path())?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
let city_clients_data: Vec<WeatherClient> = serde_json::from_reader(reader)?;
|
||||||
|
log::debug!("Successfully loaded {} cities", city_clients_data.len());
|
||||||
|
|
||||||
|
let city_clients = self.city_clients.clone();
|
||||||
|
self.tokio_runtime.block_on(async move {
|
||||||
|
let mut city_clients = city_clients.lock().await;
|
||||||
|
*city_clients = city_clients_data;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(Box::new(io::Error::new(io::ErrorKind::NotFound, "Storage path not initialized")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if let Some(storage_path) = &self.storage_path {
|
||||||
|
log::debug!("Saving data to: {:?}", storage_path.to_str());
|
||||||
|
|
||||||
|
let file = File::create(storage_path)?;
|
||||||
|
let mut writer = BufWriter::new(file);
|
||||||
|
let city_clients = self.city_clients.clone();
|
||||||
|
|
||||||
|
self.tokio_runtime.block_on(async move {
|
||||||
|
let city_clients = city_clients.lock().await;
|
||||||
|
serde_json::to_writer(&mut writer, city_clients.deref())?;
|
||||||
|
writer.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(Box::new(io::Error::new(io::ErrorKind::NotFound, "Storage path not initialized")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_cities(&mut self) -> Result<Vec<CityWeatherData>, Box<dyn std::error::Error>> {
|
||||||
|
log::debug!("Refreshing all the clients!");
|
||||||
|
|
||||||
|
let city_clients_clone = self.city_clients.clone();
|
||||||
|
let weather_api = self.weather_api.clone();
|
||||||
|
|
||||||
|
self.tokio_runtime.block_on(async move {
|
||||||
|
let mut city_clients = city_clients_clone.lock().await;
|
||||||
|
|
||||||
|
let mut errors = vec![];
|
||||||
|
for client in city_clients.iter_mut() {
|
||||||
|
// TODO: Spawn all tasks at once and join them later.
|
||||||
|
if let Err(e) = client.refresh_weather(&weather_api).await {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!("Refreshing weather finished!");
|
||||||
|
|
||||||
|
if !errors.is_empty() && errors.len() == city_clients.len() {
|
||||||
|
return Err(errors.pop().unwrap());
|
||||||
|
}
|
||||||
|
Ok(city_clients.iter().map(Self::city_weather_data_from_client).collect())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_city(
|
||||||
|
&mut self,
|
||||||
|
city: CityData,
|
||||||
|
) -> Result<Option<CityWeatherData>, Box<dyn std::error::Error>> {
|
||||||
|
log::debug!("Adding new city: {city:?}");
|
||||||
|
let city_clients_clone = self.city_clients.clone();
|
||||||
|
let weather_api = self.weather_api.clone();
|
||||||
|
|
||||||
|
self.tokio_runtime.block_on(async move {
|
||||||
|
let mut city_clients = city_clients_clone.lock().await;
|
||||||
|
match city_clients.iter().position(|client| client.city_data == city) {
|
||||||
|
Some(_) => {
|
||||||
|
log::info!("City already present in list!");
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Add to list and refresh
|
||||||
|
let mut client = WeatherClient::new(city.lat, city.lon, &city.city_name);
|
||||||
|
client.refresh_weather(&weather_api).await?;
|
||||||
|
let city_weather_data = Self::city_weather_data_from_client(&client);
|
||||||
|
city_clients.push(client);
|
||||||
|
Ok(Some(city_weather_data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reorder_cities(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
new_index: usize,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let city_clients = self.city_clients.clone();
|
||||||
|
self.tokio_runtime.block_on(async move {
|
||||||
|
let mut city_clients = city_clients.lock().await;
|
||||||
|
city_clients.swap(index, new_index);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_city(&mut self, index: usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let city_clients = self.city_clients.clone();
|
||||||
|
self.tokio_runtime.block_on(async move {
|
||||||
|
let mut city_clients = city_clients.lock().await;
|
||||||
|
city_clients.remove(index);
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_location(
|
||||||
|
&self,
|
||||||
|
query: String,
|
||||||
|
) -> Result<Vec<GeoLocationData>, Box<dyn std::error::Error>> {
|
||||||
|
log::debug!("Searching for: {query}");
|
||||||
|
let weather_api = self.weather_api.clone();
|
||||||
|
|
||||||
|
if query.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tokio_runtime.block_on(async move {
|
||||||
|
let response_data = weather_api.geocoding.get_geocoding(&query, None, None, 0).await?;
|
||||||
|
|
||||||
|
log::debug!("Search result: {response_data:?}");
|
||||||
|
|
||||||
|
let mut unique_response_data: Vec<GeocodingResponse> = Vec::new();
|
||||||
|
for element in response_data {
|
||||||
|
if !unique_response_data.iter().any(|existing_element| {
|
||||||
|
if existing_element.name == element.name
|
||||||
|
&& existing_element.country == element.country
|
||||||
|
&& existing_element.state == element.state
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}) {
|
||||||
|
unique_response_data.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(unique_response_data.iter().map(Self::geo_location_data_from_response).collect())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeatherClient {
|
||||||
|
pub fn new(lat: f64, lon: f64, cname: &str) -> Self {
|
||||||
|
Self { city_data: CityData { lat, lon, city_name: cname.to_string() }, weather_data: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_weather(
|
||||||
|
&mut self,
|
||||||
|
weather_api: &OpenWeather,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let res = weather_api.one_call.call(self.city_data.lat, self.city_data.lon).await;
|
||||||
|
log::debug!("Weather response: {res:?}");
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(response_data) => {
|
||||||
|
self.weather_data = Some(response_data);
|
||||||
|
log::debug!("Response received at: {:?}", chrono::offset::Local::now().timestamp());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
examples/weather-demo/src/weather/utils.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use chrono::{DateTime, Datelike};
|
||||||
|
|
||||||
|
pub fn get_day_from_datetime(date: DateTime<chrono::offset::Utc>) -> String {
|
||||||
|
if date.day() == chrono::offset::Local::now().day() {
|
||||||
|
// TODO: translations
|
||||||
|
return "Today".to_string();
|
||||||
|
}
|
||||||
|
date.weekday().to_string()
|
||||||
|
}
|
115
examples/weather-demo/src/weather/weathercontroller.rs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct CityData {
|
||||||
|
pub lat: f64,
|
||||||
|
pub lon: f64,
|
||||||
|
pub city_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||||
|
pub enum WeatherCondition {
|
||||||
|
#[default]
|
||||||
|
Unknown,
|
||||||
|
Sunny,
|
||||||
|
PartiallyCloudy,
|
||||||
|
MostlyCloudy,
|
||||||
|
Cloudy,
|
||||||
|
SunnyRainy,
|
||||||
|
Rainy,
|
||||||
|
Stormy,
|
||||||
|
Snowy,
|
||||||
|
Foggy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||||
|
pub struct TemperatureData {
|
||||||
|
pub min: f64,
|
||||||
|
pub max: f64,
|
||||||
|
pub morning: f64,
|
||||||
|
pub day: f64,
|
||||||
|
pub evening: f64,
|
||||||
|
pub night: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||||
|
pub struct PrecipitationData {
|
||||||
|
pub probability: f64,
|
||||||
|
pub rain_volume: f64,
|
||||||
|
pub snow_volume: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||||
|
pub struct DayWeatherData {
|
||||||
|
pub condition: WeatherCondition,
|
||||||
|
pub description: String,
|
||||||
|
|
||||||
|
pub current_temperature: f64,
|
||||||
|
pub detailed_temperature: TemperatureData,
|
||||||
|
|
||||||
|
pub precipitation: PrecipitationData,
|
||||||
|
pub uv_index: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||||
|
pub struct ForecastWeatherData {
|
||||||
|
pub day_name: String,
|
||||||
|
pub weather_data: DayWeatherData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct WeatherData {
|
||||||
|
pub current_data: DayWeatherData,
|
||||||
|
pub forecast_data: Vec<ForecastWeatherData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct CityWeatherData {
|
||||||
|
pub city_data: CityData,
|
||||||
|
pub weather_data: WeatherData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct GeoLocationData {
|
||||||
|
pub name: String,
|
||||||
|
pub lat: f64,
|
||||||
|
pub lon: f64,
|
||||||
|
pub country: String,
|
||||||
|
pub state: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub type WeatherControllerPointer = Box<dyn WeatherController + Send>;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub type WeatherControllerPointer = Box<dyn WeatherController + Send + 'static>;
|
||||||
|
|
||||||
|
pub type WeatherControllerSharedPointer = Arc<Mutex<WeatherControllerPointer>>;
|
||||||
|
|
||||||
|
pub trait WeatherController {
|
||||||
|
fn load(&mut self) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
fn save(&self) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn refresh_cities(&mut self) -> Result<Vec<CityWeatherData>, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn add_city(
|
||||||
|
&mut self,
|
||||||
|
city: CityData,
|
||||||
|
) -> Result<Option<CityWeatherData>, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn reorder_cities(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
new_index: usize,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn remove_city(&mut self, index: usize) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn search_location(
|
||||||
|
&self,
|
||||||
|
query: String,
|
||||||
|
) -> Result<Vec<GeoLocationData>, Box<dyn std::error::Error>>;
|
||||||
|
}
|
472
examples/weather-demo/src/weather/weatherdisplaycontroller.rs
Normal file
|
@ -0,0 +1,472 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel, Weak};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::ui;
|
||||||
|
use ui::{
|
||||||
|
AppWindow, BusyLayerController, CityWeather, CityWeatherInfo, GeoLocation, GeoLocationEntry,
|
||||||
|
IconType, TemperatureInfo, WeatherForecastInfo, WeatherInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::weather::weathercontroller::{
|
||||||
|
CityData, CityWeatherData, DayWeatherData, ForecastWeatherData, GeoLocationData,
|
||||||
|
WeatherCondition, WeatherControllerSharedPointer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use async_std::task::spawn as spawn_task;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use wasm_bindgen_futures::spawn_local as spawn_task;
|
||||||
|
|
||||||
|
pub struct WeatherDisplayController {
|
||||||
|
data_controller: WeatherControllerSharedPointer,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forecast_graph_command(
|
||||||
|
model: ModelRc<WeatherForecastInfo>,
|
||||||
|
days_count: i32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
) -> SharedString {
|
||||||
|
if days_count == 0 || width == 0.0 || height == 0.0 {
|
||||||
|
return SharedString::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let temperatures: Vec<f32> = model
|
||||||
|
.clone()
|
||||||
|
.iter()
|
||||||
|
.take(days_count as usize)
|
||||||
|
.map(|info| info.weather_info.detailed_temp.day)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
const MIN_MAX_MARGIN: f32 = 5.0;
|
||||||
|
let min_temperature = match temperatures.iter().min_by(|a, b| a.total_cmp(b)) {
|
||||||
|
Some(min) => min - MIN_MAX_MARGIN,
|
||||||
|
None => 0.0,
|
||||||
|
};
|
||||||
|
let max_temperature = match temperatures.iter().max_by(|a, b| a.total_cmp(b)) {
|
||||||
|
Some(max) => max + MIN_MAX_MARGIN,
|
||||||
|
None => 50.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_temperature_value = max_temperature - min_temperature;
|
||||||
|
let temperature_ratio = height / max_temperature_value;
|
||||||
|
|
||||||
|
let day_width = width / days_count as f32;
|
||||||
|
let max_day_shift = days_count as f32 * day_width;
|
||||||
|
|
||||||
|
let border_command =
|
||||||
|
format!(
|
||||||
|
"M 0 0 M {max_width} 0 M {max_width} {max_temperature_value} M 0 {max_temperature_value} ",
|
||||||
|
max_width=max_day_shift, max_temperature_value=max_temperature_value * temperature_ratio);
|
||||||
|
|
||||||
|
let mut command = border_command;
|
||||||
|
|
||||||
|
let day_shift = |index: f32| -> f32 { index * day_width + 0.5 * day_width };
|
||||||
|
let day_temperature =
|
||||||
|
|temperature: f32| -> f32 { (max_temperature - temperature) * temperature_ratio };
|
||||||
|
|
||||||
|
for (index, &temperature) in temperatures.iter().enumerate() {
|
||||||
|
if index == 0 {
|
||||||
|
command += format!(
|
||||||
|
"M {x} {y} ",
|
||||||
|
x = day_shift(index as f32),
|
||||||
|
y = day_temperature(temperature)
|
||||||
|
)
|
||||||
|
.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(next_temperature) = temperatures.get(index + 1) {
|
||||||
|
let next_temperature = *next_temperature;
|
||||||
|
|
||||||
|
let day1 = day_shift(index as f32);
|
||||||
|
let day2 = day_shift(index as f32 + 1.0);
|
||||||
|
let temp1 = day_temperature(temperature);
|
||||||
|
let temp2 = day_temperature(next_temperature);
|
||||||
|
|
||||||
|
let day_mid = (day1 + day2) / 2.0;
|
||||||
|
let temp_mid = (temp1 + temp2) / 2.0;
|
||||||
|
|
||||||
|
let cp_day1 = (day_mid + day1) / 2.0;
|
||||||
|
let cp_day2 = (day_mid + day2) / 2.0;
|
||||||
|
|
||||||
|
command += format!(
|
||||||
|
"Q {x1} {y1} {cx1} {cy1} Q {x2} {y2} {cx2} {cy2} ",
|
||||||
|
x1 = cp_day1,
|
||||||
|
y1 = temp1,
|
||||||
|
cx1 = day_mid,
|
||||||
|
cy1 = temp_mid,
|
||||||
|
x2 = cp_day2,
|
||||||
|
y2 = temp2,
|
||||||
|
cx2 = day2,
|
||||||
|
cy2 = temp2
|
||||||
|
)
|
||||||
|
.as_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedString::from(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeatherDisplayController {
|
||||||
|
pub fn new(data_controller: &WeatherControllerSharedPointer) -> Self {
|
||||||
|
Self { data_controller: data_controller.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize_ui(&self, window: &AppWindow, support_add_city: bool) {
|
||||||
|
let city_weather = window.global::<CityWeather>();
|
||||||
|
let geo_location = window.global::<GeoLocation>();
|
||||||
|
|
||||||
|
// initialized models
|
||||||
|
city_weather
|
||||||
|
.set_city_weather(ModelRc::from(Rc::new(VecModel::<CityWeatherInfo>::from(vec![]))));
|
||||||
|
geo_location
|
||||||
|
.set_result_list(ModelRc::from(Rc::new(VecModel::<GeoLocationEntry>::from(vec![]))));
|
||||||
|
|
||||||
|
// initialize state
|
||||||
|
city_weather.set_can_add_city(support_add_city);
|
||||||
|
|
||||||
|
// handle callbacks
|
||||||
|
city_weather.on_get_forecast_graph_command(forecast_graph_command);
|
||||||
|
|
||||||
|
city_weather.on_refresh_all({
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
let data_controller = self.data_controller.clone();
|
||||||
|
|
||||||
|
move || Self::refresh_cities(&window_weak, &data_controller)
|
||||||
|
});
|
||||||
|
|
||||||
|
city_weather.on_reorder({
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
let data_controller = self.data_controller.clone();
|
||||||
|
|
||||||
|
move |index, new_index| {
|
||||||
|
if let Err(e) =
|
||||||
|
Self::reorder_cities(&window_weak, &data_controller, index, new_index)
|
||||||
|
{
|
||||||
|
log::warn!("Failed to reorder city from {} to {}: {}", index, new_index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
city_weather.on_delete({
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
let data_controller = self.data_controller.clone();
|
||||||
|
|
||||||
|
move |index| {
|
||||||
|
if let Err(e) = Self::remove_city(&window_weak, &data_controller, index) {
|
||||||
|
log::warn!("Failed to remove city from {}: {}", index, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
geo_location.on_search_location({
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
let data_controller = self.data_controller.clone();
|
||||||
|
|
||||||
|
move |location| Self::search_location(&window_weak, &data_controller, location)
|
||||||
|
});
|
||||||
|
|
||||||
|
geo_location.on_add_location({
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
let data_controller = self.data_controller.clone();
|
||||||
|
|
||||||
|
move |location| {
|
||||||
|
Self::add_city(&window_weak, &data_controller, location);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
|
||||||
|
pub fn refresh(&self, window: &AppWindow) {
|
||||||
|
Self::set_busy(window);
|
||||||
|
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
Self::refresh_cities(&window_weak, &self.data_controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self, window: &AppWindow) {
|
||||||
|
Self::set_busy(window);
|
||||||
|
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
let data_controller = self.data_controller.clone();
|
||||||
|
|
||||||
|
spawn_task(async move {
|
||||||
|
let city_data_res = async {
|
||||||
|
let mut data_controller = data_controller.lock().unwrap();
|
||||||
|
data_controller.load()?;
|
||||||
|
data_controller.refresh_cities()
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let city_data = match city_data_res {
|
||||||
|
Ok(city_data) => Some(city_data),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to load cities: {}.", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::check_update_error(window_weak.upgrade_in_event_loop(move |window| {
|
||||||
|
if let Some(city_data) = city_data {
|
||||||
|
WeatherDisplayController::update_displayed_cities(&window, city_data);
|
||||||
|
}
|
||||||
|
Self::unset_busy(&window);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_cities(
|
||||||
|
window_weak: &Weak<AppWindow>,
|
||||||
|
data_controller: &WeatherControllerSharedPointer,
|
||||||
|
) {
|
||||||
|
let window_weak = window_weak.clone();
|
||||||
|
let data_controller = data_controller.clone();
|
||||||
|
|
||||||
|
spawn_task(async move {
|
||||||
|
let city_data_res = async { data_controller.lock().unwrap().refresh_cities() }.await;
|
||||||
|
|
||||||
|
let city_data = match city_data_res {
|
||||||
|
Ok(city_data) => Some(city_data),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to update cities: {}.", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::check_update_error(window_weak.upgrade_in_event_loop(move |window| {
|
||||||
|
if let Some(city_data) = city_data {
|
||||||
|
WeatherDisplayController::update_displayed_cities(&window, city_data);
|
||||||
|
}
|
||||||
|
Self::unset_busy(&window);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_city(
|
||||||
|
window_weak: &Weak<AppWindow>,
|
||||||
|
data_controller: &WeatherControllerSharedPointer,
|
||||||
|
location: GeoLocationEntry,
|
||||||
|
) {
|
||||||
|
let city = CityData {
|
||||||
|
lat: location.lat as f64,
|
||||||
|
lon: location.lon as f64,
|
||||||
|
city_name: String::from(&location.name),
|
||||||
|
};
|
||||||
|
let city_data_res = data_controller.lock().unwrap().add_city(city);
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
let window = window_weak.upgrade().unwrap();
|
||||||
|
match city_data_res {
|
||||||
|
Ok(city_data) => {
|
||||||
|
if let Some(city_data) = city_data {
|
||||||
|
let city_weather = window.global::<CityWeather>();
|
||||||
|
let city_weather_list = city_weather.get_city_weather();
|
||||||
|
|
||||||
|
let city_weather = Self::city_weather_info_from_data(&city_data);
|
||||||
|
city_weather_list
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<slint::VecModel<CityWeatherInfo>>()
|
||||||
|
.unwrap()
|
||||||
|
.push(city_weather);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to add city: {}.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::unset_busy(&window);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reorder_cities(
|
||||||
|
window_weak: &Weak<AppWindow>,
|
||||||
|
data_controller: &WeatherControllerSharedPointer,
|
||||||
|
index: i32,
|
||||||
|
new_index: i32,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let pos: usize = index.try_into()?;
|
||||||
|
let new_pos: usize = new_index.try_into()?;
|
||||||
|
|
||||||
|
data_controller.lock().unwrap().reorder_cities(pos, new_pos)?;
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
let window = window_weak.upgrade().unwrap();
|
||||||
|
let city_weather = window.global::<CityWeather>();
|
||||||
|
let city_weather_list = city_weather.get_city_weather();
|
||||||
|
|
||||||
|
let pos_data = city_weather_list.row_data(pos).ok_or(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"Index out of bounds",
|
||||||
|
)))?;
|
||||||
|
let new_pos_data = city_weather_list.row_data(new_pos).ok_or(Box::new(
|
||||||
|
std::io::Error::new(std::io::ErrorKind::InvalidInput, "Index out of bounds"),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
city_weather_list.set_row_data(pos, new_pos_data);
|
||||||
|
city_weather_list.set_row_data(new_pos, pos_data);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_city(
|
||||||
|
window_weak: &Weak<AppWindow>,
|
||||||
|
data_controller: &WeatherControllerSharedPointer,
|
||||||
|
index: i32,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let pos: usize = index.try_into()?;
|
||||||
|
|
||||||
|
data_controller.lock().unwrap().remove_city(pos)?;
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
let window = window_weak.upgrade().unwrap();
|
||||||
|
let city_weather = window.global::<CityWeather>();
|
||||||
|
let city_weather_list = city_weather.get_city_weather();
|
||||||
|
|
||||||
|
let model = city_weather_list
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<slint::VecModel<CityWeatherInfo>>()
|
||||||
|
.expect("CityWeatherInfo model is not provided!");
|
||||||
|
|
||||||
|
model.remove(pos);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_location(
|
||||||
|
window_weak: &Weak<AppWindow>,
|
||||||
|
data_controller: &WeatherControllerSharedPointer,
|
||||||
|
query: slint::SharedString,
|
||||||
|
) {
|
||||||
|
let window_weak = window_weak.clone();
|
||||||
|
let data_controller = data_controller.clone();
|
||||||
|
let query = query.to_string();
|
||||||
|
|
||||||
|
spawn_task(async move {
|
||||||
|
let locations_res =
|
||||||
|
async { data_controller.lock().unwrap().search_location(query) }.await;
|
||||||
|
|
||||||
|
let locations = match locations_res {
|
||||||
|
Ok(locations) => Some(locations),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to search for location: {}.", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::check_update_error(window_weak.upgrade_in_event_loop(move |window| {
|
||||||
|
if let Some(locations) = locations {
|
||||||
|
WeatherDisplayController::update_location_search_results(&window, locations);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_displayed_cities(window: &AppWindow, data: Vec<CityWeatherData>) {
|
||||||
|
let display_vector: Vec<CityWeatherInfo> =
|
||||||
|
data.iter().map(Self::city_weather_info_from_data).collect();
|
||||||
|
|
||||||
|
let city_weather = window.global::<CityWeather>().get_city_weather();
|
||||||
|
let model = city_weather
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<VecModel<CityWeatherInfo>>()
|
||||||
|
.expect("City weather model not set.");
|
||||||
|
|
||||||
|
model.set_vec(display_vector);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_location_search_results(window: &AppWindow, result: Vec<GeoLocationData>) {
|
||||||
|
let display_vector: Vec<GeoLocationEntry> =
|
||||||
|
result.iter().map(Self::geo_location_entry_from_data).collect();
|
||||||
|
|
||||||
|
let geo_location = window.global::<GeoLocation>().get_result_list();
|
||||||
|
let model = geo_location
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<VecModel<GeoLocationEntry>>()
|
||||||
|
.expect("Geo location entry model not set.");
|
||||||
|
|
||||||
|
model.set_vec(display_vector);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_busy(window: &AppWindow) {
|
||||||
|
window.global::<BusyLayerController>().invoke_set_busy();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unset_busy(window: &AppWindow) {
|
||||||
|
window.global::<BusyLayerController>().invoke_unset_busy();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_update_error<E: std::fmt::Display>(result: Result<(), E>) {
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::error!("Error while updating UI: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_type_from_condition(condition: &WeatherCondition) -> IconType {
|
||||||
|
match condition {
|
||||||
|
WeatherCondition::Sunny => IconType::Sunny,
|
||||||
|
WeatherCondition::PartiallyCloudy => IconType::PartiallyCloudy,
|
||||||
|
WeatherCondition::MostlyCloudy => IconType::MostlyCloudy,
|
||||||
|
WeatherCondition::Cloudy => IconType::Cloudy,
|
||||||
|
WeatherCondition::SunnyRainy => IconType::SunnyRainy,
|
||||||
|
WeatherCondition::Rainy => IconType::Rainy,
|
||||||
|
WeatherCondition::Stormy => IconType::Stormy,
|
||||||
|
WeatherCondition::Snowy => IconType::Snowy,
|
||||||
|
WeatherCondition::Foggy => IconType::Foggy,
|
||||||
|
_ => IconType::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn weather_info_from_data(data: &DayWeatherData) -> WeatherInfo {
|
||||||
|
WeatherInfo {
|
||||||
|
description: SharedString::from(&data.description),
|
||||||
|
icon_type: Self::icon_type_from_condition(&data.condition),
|
||||||
|
current_temp: data.current_temperature as f32,
|
||||||
|
detailed_temp: TemperatureInfo {
|
||||||
|
min: data.detailed_temperature.min as f32,
|
||||||
|
max: data.detailed_temperature.max as f32,
|
||||||
|
|
||||||
|
morning: data.detailed_temperature.morning as f32,
|
||||||
|
day: data.detailed_temperature.day as f32,
|
||||||
|
evening: data.detailed_temperature.evening as f32,
|
||||||
|
night: data.detailed_temperature.night as f32,
|
||||||
|
},
|
||||||
|
uv: data.uv_index as i32,
|
||||||
|
precipitation_prob: data.precipitation.probability as f32,
|
||||||
|
rain: data.precipitation.rain_volume as f32,
|
||||||
|
snow: data.precipitation.snow_volume as f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forecast_weather_info_from_data(data: &[ForecastWeatherData]) -> Vec<WeatherForecastInfo> {
|
||||||
|
data.iter()
|
||||||
|
.map(|forecast_data| WeatherForecastInfo {
|
||||||
|
day_name: SharedString::from(&forecast_data.day_name),
|
||||||
|
weather_info: Self::weather_info_from_data(&forecast_data.weather_data),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn city_weather_info_from_data(data: &CityWeatherData) -> CityWeatherInfo {
|
||||||
|
let current_weather_info = Self::weather_info_from_data(&data.weather_data.current_data);
|
||||||
|
let forecast_weather_info =
|
||||||
|
Self::forecast_weather_info_from_data(&data.weather_data.forecast_data);
|
||||||
|
|
||||||
|
CityWeatherInfo {
|
||||||
|
city_name: SharedString::from(&data.city_data.city_name),
|
||||||
|
current_weather: current_weather_info,
|
||||||
|
forecast_weather: Rc::new(slint::VecModel::from(forecast_weather_info)).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geo_location_entry_from_data(data: &GeoLocationData) -> GeoLocationEntry {
|
||||||
|
GeoLocationEntry {
|
||||||
|
name: SharedString::from(&data.name),
|
||||||
|
state: SharedString::from(data.state.as_deref().unwrap_or_default()),
|
||||||
|
country: SharedString::from(&data.country),
|
||||||
|
lat: data.lat as f32,
|
||||||
|
lon: data.lon as f32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
examples/weather-demo/ui/about-box.slint
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { WindowInfo } from "./ui_utils.slint";
|
||||||
|
import { AppText } from "./controls/generic.slint";
|
||||||
|
import { AboutSlint } from "std-widgets.slint";
|
||||||
|
|
||||||
|
component AboutFelgo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: 5px;
|
||||||
|
|
||||||
|
width: 90% * parent.width;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
background: white;
|
||||||
|
border-radius: self.height / 2;
|
||||||
|
|
||||||
|
preferred-height: self.width * 45%;
|
||||||
|
|
||||||
|
logo-layout := VerticalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 2px;
|
||||||
|
|
||||||
|
made-text := AppText {
|
||||||
|
text: "MADE BY";
|
||||||
|
horizontal-alignment: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Image {
|
||||||
|
image-fit: contain;
|
||||||
|
width: 70% * parent.width;
|
||||||
|
source: @image-url("./assets/felgo-logo.svg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppText {
|
||||||
|
text: "https://felgo.com/";
|
||||||
|
horizontal-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AboutBox {
|
||||||
|
VerticalLayout {
|
||||||
|
if WindowInfo.is-portrait: VerticalLayout {
|
||||||
|
alignment: center;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
AboutFelgo {
|
||||||
|
width: Math.min(200px, 80% * parent.width);
|
||||||
|
min-height: self.preferred-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
AboutSlint {
|
||||||
|
width: Math.min(200px, 80% * parent.width);
|
||||||
|
min-height: self.preferred-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !WindowInfo.is-portrait: HorizontalLayout {
|
||||||
|
alignment: space-around;
|
||||||
|
|
||||||
|
// adjust AboutFelgo size to look the same as AboutSlint
|
||||||
|
Rectangle {
|
||||||
|
max-width: 200px;
|
||||||
|
|
||||||
|
AboutFelgo {
|
||||||
|
height: 84%;
|
||||||
|
preferred-width: 200px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AboutSlint {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
examples/weather-demo/ui/assets/felgo-logo.svg
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 3172 635" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||||
|
<g id="Logo-Vermillion-87-Saturation" serif:id="Logo Vermillion 87 Saturation" transform="matrix(1,0,0,1,-387.622,-203.331)">
|
||||||
|
<path id="segmented-line" serif:id="segmented line" d="M674.5,574.796L743.796,505.5L1044.3,806L905.704,806L674.5,574.796ZM605.204,505.5L427.342,327.639C408.219,308.516 408.219,277.465 427.342,258.342C446.465,239.219 477.516,239.219 496.639,258.342L674.5,436.204L605.204,505.5Z" style="fill:rgb(167,43,12);"/>
|
||||||
|
<g id="whole-line" serif:id="whole line" transform="matrix(0.699964,0.699964,-1.22244,1.22244,1092.04,-519.105)">
|
||||||
|
<path d="M511,331.651L412,388.339L412,779.656C412,795.3 434.18,808 461.5,808C488.82,808 511,795.3 511,779.656L511,331.651Z" style="fill:rgb(240,78,38);"/>
|
||||||
|
</g>
|
||||||
|
<g id="vertical" transform="matrix(0.989899,0,0,1,5.16162,-2)">
|
||||||
|
<path d="M511,295C511,267.956 488.82,246 461.5,246C434.18,246 412,267.956 412,295L412,759C412,786.044 434.18,808 461.5,808C488.82,808 511,786.044 511,759L511,295Z" style="fill:rgb(240,78,38);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.02897,0,0,1.02897,30.2111,266.834)">
|
||||||
|
<path d="M1080.69,-95.336L1080.69,-210.913L761.725,-210.913L761.725,314.437L881.805,314.437L881.805,121.558L1076.93,121.558L1076.93,5.981L881.805,5.981L881.805,-95.336L1080.69,-95.336Z" style="fill:rgb(34,38,46);fill-rule:nonzero;"/>
|
||||||
|
<g transform="matrix(1,0,0,1,5.8311,0)">
|
||||||
|
<path d="M1272.07,198.86L1272.07,105.798L1459.69,105.798L1459.69,-8.278L1272.07,-8.278L1272.07,-95.336L1478.45,-95.336L1478.45,-210.913L1151.99,-210.913L1151.99,314.437L1482.2,314.437L1482.2,198.86L1272.07,198.86Z" style="fill:rgb(34,38,46);fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,9.71849,0)">
|
||||||
|
<path d="M1669.83,198.86L1669.83,-210.913L1549.75,-210.913L1549.75,314.437L1857.45,314.437L1857.45,198.86L1669.83,198.86Z" style="fill:rgb(34,38,46);fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
<path d="M2430.09,21.742L2166.66,21.742L2166.66,126.812L2307,126.812C2288.24,177.095 2242.46,207.866 2172.66,207.866C2070.6,207.866 2007.56,142.572 2007.56,53.263C2007.56,-39.049 2073.6,-104.342 2162.16,-104.342C2221.45,-104.342 2268.73,-77.324 2290.49,-42.801L2392.56,-101.34C2348.28,-171.887 2263.47,-221.42 2162.91,-221.42C2008.31,-221.42 1887.48,-99.839 1887.48,52.512C1887.48,203.363 2006.05,324.944 2171.16,324.944C2318.26,324.944 2430.09,227.379 2430.09,66.772L2430.09,21.742Z" style="fill:rgb(34,38,46);fill-rule:nonzero;"/>
|
||||||
|
<path d="M2748.3,324.944C2899.15,324.944 3021.48,206.365 3021.48,51.762C3021.48,-102.841 2899.15,-221.42 2748.3,-221.42C2597.45,-221.42 2475.12,-102.841 2475.12,51.762C2475.12,206.365 2597.45,324.944 2748.3,324.944ZM2748.3,207.866C2662.74,207.866 2595.2,145.574 2595.2,51.762C2595.2,-42.051 2662.74,-104.342 2748.3,-104.342C2833.86,-104.342 2901.4,-42.051 2901.4,51.762C2901.4,145.574 2833.86,207.866 2748.3,207.866Z" style="fill:rgb(34,38,46);fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
1
examples/weather-demo/ui/assets/icons/arrow-down.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>
|
After Width: | Height: | Size: 400 B |
1
examples/weather-demo/ui/assets/icons/arrow-up.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>
|
After Width: | Height: | Size: 400 B |
1
examples/weather-demo/ui/assets/icons/plus.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M416 208H272V64c0-17.7-14.3-32-32-32h-32c-17.7 0-32 14.3-32 32v144H32c-17.7 0-32 14.3-32 32v32c0 17.7 14.3 32 32 32h144v144c0 17.7 14.3 32 32 32h32c17.7 0 32-14.3 32-32V304h144c17.7 0 32-14.3 32-32v-32c0-17.7-14.3-32-32-32z"/></svg>
|
After Width: | Height: | Size: 455 B |
1
examples/weather-demo/ui/assets/icons/refresh.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256.5 8c66.3 .1 126.4 26.2 170.9 68.7l35.7-35.7C478.1 25.9 504 36.6 504 57.9V192c0 13.3-10.7 24-24 24H345.9c-21.4 0-32.1-25.9-17-41l41.8-41.8c-30.9-28.9-70.8-44.9-113.2-45.3-92.4-.8-170.3 74-169.5 169.4C88.8 348 162.2 424 256 424c41.1 0 80-14.7 110.6-41.6 4.7-4.2 11.9-3.9 16.4 .6l39.7 39.7c4.9 4.9 4.6 12.8-.5 17.4C378.2 479.8 319.9 504 256 504 119 504 8 393 8 256 8 119.2 119.6 7.8 256.5 8z"/></svg>
|
After Width: | Height: | Size: 625 B |
1
examples/weather-demo/ui/assets/icons/search.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>
|
After Width: | Height: | Size: 464 B |
1
examples/weather-demo/ui/assets/icons/trash.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>
|
After Width: | Height: | Size: 486 B |
1
examples/weather-demo/ui/assets/icons/xmark.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M242.7 256l100.1-100.1c12.3-12.3 12.3-32.2 0-44.5l-22.2-22.2c-12.3-12.3-32.2-12.3-44.5 0L176 189.3 75.9 89.2c-12.3-12.3-32.2-12.3-44.5 0L9.2 111.5c-12.3 12.3-12.3 32.2 0 44.5L109.3 256 9.2 356.1c-12.3 12.3-12.3 32.2 0 44.5l22.2 22.2c12.3 12.3 32.2 12.3 44.5 0L176 322.7l100.1 100.1c12.3 12.3 32.2 12.3 44.5 0l22.2-22.2c12.3-12.3 12.3-32.2 0-44.5L242.7 256z"/></svg>
|
After Width: | Height: | Size: 588 B |
BIN
examples/weather-demo/ui/assets/weathericons-font.ttf
Normal file
218
examples/weather-demo/ui/city_weather.slint
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { WindowInfo } from "./ui_utils.slint";
|
||||||
|
import { PageBase } from "page-base.slint";
|
||||||
|
import { CityWeatherTile } from "city_weather_tile.slint";
|
||||||
|
import { ExpandedCityWeatherTile } from "expanded_city_weather_tile.slint";
|
||||||
|
import { CityWeather, CityWeatherInfo } from "weather_datatypes.slint";
|
||||||
|
import { AppPalette, AppImages } from "./style/styles.slint";
|
||||||
|
import { SlideButton } from "./controls/generic.slint";
|
||||||
|
import { AboutBox } from "about-box.slint";
|
||||||
|
|
||||||
|
component CitySlideArea inherits Rectangle {
|
||||||
|
in property<bool> can-move-up: true;
|
||||||
|
in property<bool> can-move-down: true;
|
||||||
|
|
||||||
|
callback opened;
|
||||||
|
callback closed;
|
||||||
|
|
||||||
|
callback up-clicked;
|
||||||
|
callback down-clicked;
|
||||||
|
callback delete-clicked;
|
||||||
|
callback content-clicked;
|
||||||
|
|
||||||
|
public function open() {
|
||||||
|
flickable.viewport-x = -buttons-layout.width;
|
||||||
|
root.opened();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close() {
|
||||||
|
flickable.viewport-x = 0;
|
||||||
|
root.closed();
|
||||||
|
}
|
||||||
|
|
||||||
|
height: content.preferred-height;
|
||||||
|
|
||||||
|
flickable := Flickable {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
viewport-width: slide-layout.preferred-width;
|
||||||
|
viewport-x: 0;
|
||||||
|
|
||||||
|
property<length> last-viewport-x: 0px;
|
||||||
|
flicked => {
|
||||||
|
if (self.last-viewport-x > self.viewport-x) {
|
||||||
|
root.open();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slide-layout := HorizontalLayout {
|
||||||
|
content := Rectangle {
|
||||||
|
width: root.width;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
@children
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
pointer-event(event) => {
|
||||||
|
if (event.kind == PointerEventKind.down) {
|
||||||
|
flickable.last-viewport-x = flickable.viewport-x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clicked => { root.content-clicked(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons-layout := VerticalLayout {
|
||||||
|
width: 50px;
|
||||||
|
spacing: 1px;
|
||||||
|
|
||||||
|
SlideButton {
|
||||||
|
icon-source: AppImages.arrow-up;
|
||||||
|
enabled: root.can-move-up;
|
||||||
|
|
||||||
|
clicked => { root.up-clicked(); }
|
||||||
|
}
|
||||||
|
SlideButton {
|
||||||
|
icon-source: AppImages.trash;
|
||||||
|
background-color: AppPalette.error-red;
|
||||||
|
|
||||||
|
clicked => { root.delete-clicked(); }
|
||||||
|
}
|
||||||
|
SlideButton {
|
||||||
|
icon-source: AppImages.arrow-down;
|
||||||
|
enabled: root.can-move-down;
|
||||||
|
|
||||||
|
clicked => { root.down-clicked(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
component CityWeatherList inherits Flickable {
|
||||||
|
property<int> opened-index: -1;
|
||||||
|
|
||||||
|
callback expand(int, Point, length, length);
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: start;
|
||||||
|
padding: 0px;
|
||||||
|
|
||||||
|
for city-weather-info[index] in CityWeather.city-weather:
|
||||||
|
CitySlideArea {
|
||||||
|
property<bool> is-opened: root.opened-index == index;
|
||||||
|
|
||||||
|
can-move-up: index > 0;
|
||||||
|
can-move-down: index < CityWeather.city-weather.length - 1;
|
||||||
|
|
||||||
|
changed is-opened => {
|
||||||
|
if (is-opened) {
|
||||||
|
self.open();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opened => {
|
||||||
|
root.opened-index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
content-clicked => {
|
||||||
|
root.opened-index = -1;
|
||||||
|
root.expand(index, self.absolute-position, self.width, self.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
up-clicked => {
|
||||||
|
root.opened-index = index - 1;
|
||||||
|
CityWeather.reorder(index, root.opened-index);
|
||||||
|
}
|
||||||
|
down-clicked => {
|
||||||
|
root.opened-index = index + 1;
|
||||||
|
CityWeather.reorder(index, root.opened-index);
|
||||||
|
}
|
||||||
|
delete-clicked => {
|
||||||
|
root.opened-index = -1;
|
||||||
|
CityWeather.delete(index);
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
CityWeatherTile {
|
||||||
|
city-weather-info: city-weather-info;
|
||||||
|
alternative-background: Math.mod(index, 2) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
// spacing
|
||||||
|
min-height: WindowInfo.is-portrait ? 25px : 10px;
|
||||||
|
}
|
||||||
|
AboutBox {
|
||||||
|
min-height: self.preferred-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TileInfo {
|
||||||
|
index: int,
|
||||||
|
absolute-position: Point,
|
||||||
|
size: { width: length, height: length }
|
||||||
|
}
|
||||||
|
|
||||||
|
export component CityListView inherits PageBase {
|
||||||
|
property<TileInfo> selected-tile;
|
||||||
|
|
||||||
|
CityWeatherList {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
expand(index, absolute-position, width, height) => {
|
||||||
|
root.selected-tile = {
|
||||||
|
index: index,
|
||||||
|
absolute-position: absolute-position,
|
||||||
|
size: { width: width, height: height }
|
||||||
|
};
|
||||||
|
expanded-tile.expand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PageBase {
|
||||||
|
opacity: 0.0;
|
||||||
|
|
||||||
|
states [
|
||||||
|
visible when expanded-tile.expanded: {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
out {
|
||||||
|
animate opacity { delay: expanded-tile.animation-duration; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded-tile := ExpandedCityWeatherTile {
|
||||||
|
city-weather-info: CityWeather.city-weather[root.selected-tile.index];
|
||||||
|
alternative-background: Math.mod(root.selected-tile.index, 2) == 0;
|
||||||
|
|
||||||
|
block-x: root.selected-tile.absolute-position.x - root.absolute-position.x;
|
||||||
|
block-y: root.selected-tile.absolute-position.y - root.absolute-position.y;
|
||||||
|
block-width: root.selected-tile.size.width;
|
||||||
|
block-height: root.selected-tile.size.height;
|
||||||
|
|
||||||
|
full-x: 0px;
|
||||||
|
full-y: 0px;
|
||||||
|
full-width: parent.width;
|
||||||
|
full-height: parent.height;
|
||||||
|
|
||||||
|
clicked => {
|
||||||
|
self.collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
examples/weather-demo/ui/city_weather_tile.slint
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { WeatherInfo, WeatherForecastInfo, CityWeatherInfo, CityWeather } from "weather_datatypes.slint";
|
||||||
|
import { WindowInfo } from "./ui_utils.slint";
|
||||||
|
import { AppPalette } from "./style/styles.slint";
|
||||||
|
import { AppText } from "./controls/generic.slint";
|
||||||
|
import { WeatherIcon } from "./controls/weather.slint";
|
||||||
|
import { DayForecastGraph } from "./forecast_with_graph.slint";
|
||||||
|
|
||||||
|
component TileBaseInfo inherits HorizontalLayout {
|
||||||
|
in property<string> city-name;
|
||||||
|
in property<WeatherInfo> current-weather;
|
||||||
|
|
||||||
|
spacing: 15px;
|
||||||
|
|
||||||
|
AppText {
|
||||||
|
font-size: 2.1rem;
|
||||||
|
text: root.city-name;
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
alignment: start;
|
||||||
|
|
||||||
|
AppText {
|
||||||
|
min-width: self.preferred-width;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
horizontal-alignment: right;
|
||||||
|
|
||||||
|
text: Math.round(root.current-weather.current_temp) + "°";
|
||||||
|
}
|
||||||
|
|
||||||
|
AppText {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
horizontal-alignment: right;
|
||||||
|
|
||||||
|
text: root.current-weather.description;
|
||||||
|
wrap: word-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WeatherIcon {
|
||||||
|
icon-type: root.current-weather.icon-type;
|
||||||
|
|
||||||
|
font-size: 3.5rem;
|
||||||
|
vertical-alignment: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component CityWeatherTile inherits TouchArea {
|
||||||
|
in property<CityWeatherInfo> city-weather-info;
|
||||||
|
in property<bool> alternative-background: false;
|
||||||
|
in property <bool> show-animations: true;
|
||||||
|
|
||||||
|
out property<string> city-name: city-weather-info.city-name;
|
||||||
|
out property<WeatherInfo> current-weather: city-weather-info.current-weather;
|
||||||
|
out property<[WeatherForecastInfo]> forecast-weather: city-weather-info.forecast-weather;
|
||||||
|
|
||||||
|
preferred-height: layout.preferred-height;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
background: root.alternative-background ? white.with-alpha(2.5%) : white.with-alpha(8.5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout := VerticalLayout {
|
||||||
|
padding: 15px;
|
||||||
|
spacing: 10px;
|
||||||
|
|
||||||
|
if WindowInfo.is-portrait: VerticalLayout {
|
||||||
|
vertical-stretch: 0;
|
||||||
|
spacing: parent.spacing;
|
||||||
|
|
||||||
|
TileBaseInfo {
|
||||||
|
city-name: root.city-name;
|
||||||
|
current-weather: root.current-weather;
|
||||||
|
}
|
||||||
|
|
||||||
|
DayForecastGraph {
|
||||||
|
min-height: self.preferred-height;
|
||||||
|
|
||||||
|
forecast-weather: root.forecast-weather;
|
||||||
|
show-animations: root.show-animations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !WindowInfo.is-portrait: HorizontalLayout {
|
||||||
|
spacing: 5% * self.width;
|
||||||
|
|
||||||
|
TileBaseInfo {
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
max-width: parent.width * 40%;
|
||||||
|
height: self.preferred-height;
|
||||||
|
|
||||||
|
city-name: root.city-name;
|
||||||
|
current-weather: root.current-weather;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
min-height: self.preferred-height;
|
||||||
|
|
||||||
|
DayForecastGraph {
|
||||||
|
forecast-weather: root.forecast-weather;
|
||||||
|
show-animations: root.show-animations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
examples/weather-demo/ui/controls/busy-layer.slint
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { AppFonts, AppImages } from "../style/styles.slint";
|
||||||
|
|
||||||
|
export global BusyLayerController {
|
||||||
|
out property<bool> is-busy: false;
|
||||||
|
|
||||||
|
property<int> busy-counter: 0;
|
||||||
|
|
||||||
|
public function set-busy() {
|
||||||
|
busy-counter += 1;
|
||||||
|
|
||||||
|
// updating only when real change happen to avoid:
|
||||||
|
// https://github.com/slint-ui/slint/issues/5209
|
||||||
|
if (!root.is-busy) {
|
||||||
|
root.is-busy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function unset-busy() {
|
||||||
|
busy-counter -= 1;
|
||||||
|
|
||||||
|
// updating only when real change happen to avoid:
|
||||||
|
// https://github.com/slint-ui/slint/issues/5209
|
||||||
|
if (root.is-busy && busy-counter == 0) {
|
||||||
|
root.is-busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component BusyLayer inherits Rectangle {
|
||||||
|
Rectangle {
|
||||||
|
background: black;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
width: 75px;
|
||||||
|
height: 75px;
|
||||||
|
image-fit: contain;
|
||||||
|
|
||||||
|
source: AppImages.refresh;
|
||||||
|
colorize: white.darker(15%);
|
||||||
|
|
||||||
|
rotation-angle: Math.mod(animation-tick() / 3.25ms, 360) * 1deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// touch blocker
|
||||||
|
TouchArea {}
|
||||||
|
}
|
123
examples/weather-demo/ui/controls/generic.slint
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { AppPalette, AppFonts, AppImages } from "../style/styles.slint";
|
||||||
|
|
||||||
|
export component AppText inherits Text {
|
||||||
|
color: AppPalette.foreground;
|
||||||
|
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AppIcon inherits Image {
|
||||||
|
image-fit: contain;
|
||||||
|
colorize: AppPalette.foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
export component FloatingTextButton inherits Rectangle {
|
||||||
|
in property icon-source <=> icon.source;
|
||||||
|
in property icon-color <=> icon.colorize;
|
||||||
|
|
||||||
|
callback clicked;
|
||||||
|
|
||||||
|
drop-shadow-color: self.background.darker(50%);
|
||||||
|
drop-shadow-blur: 5px;
|
||||||
|
drop-shadow-offset-x: 3px;
|
||||||
|
drop-shadow-offset-y: 2px;
|
||||||
|
|
||||||
|
background: AppPalette.background;
|
||||||
|
border-radius: Math.min(self.width, self.height) / 2;
|
||||||
|
|
||||||
|
padding: 12px;
|
||||||
|
padding-left: self.padding;
|
||||||
|
padding-right: self.padding;
|
||||||
|
padding-top: self.padding;
|
||||||
|
padding-bottom: self.padding;
|
||||||
|
|
||||||
|
preferred-width: 48px;
|
||||||
|
preferred-height: 48px;
|
||||||
|
|
||||||
|
width: self.preferred-width;
|
||||||
|
height: self.preferred-height;
|
||||||
|
|
||||||
|
icon := AppIcon {
|
||||||
|
width: parent.width - parent.padding-left - parent.padding-right;
|
||||||
|
height: parent.height - parent.padding-top - parent.padding-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
clicked => { root.clicked(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component SlideButton inherits Rectangle {
|
||||||
|
in-out property icon-source <=> icon.source;
|
||||||
|
in-out property<bool> enabled <=> touch-area.enabled;
|
||||||
|
in property<color> background-color;
|
||||||
|
callback clicked <=> touch-area.clicked;
|
||||||
|
|
||||||
|
background: touch-area.pressed ? self.background-color.darker(10%) : self.background-color;
|
||||||
|
opacity: root.enabled ? 1.0 : 0.5;
|
||||||
|
|
||||||
|
icon := AppIcon {
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
|
||||||
|
colorize: touch-area.pressed ? AppPalette.foreground.darker(10%) : AppPalette.foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
touch-area := TouchArea {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component TextField inherits Rectangle {
|
||||||
|
in property icon-source <=> icon.source;
|
||||||
|
in property<string> placeholder-text;
|
||||||
|
in-out property<string> text <=> text-input.text;
|
||||||
|
callback edited <=> text-input.edited;
|
||||||
|
|
||||||
|
forward-focus: text-input;
|
||||||
|
|
||||||
|
padding: 5px;
|
||||||
|
padding-top: self.padding;
|
||||||
|
padding-right: self.padding;
|
||||||
|
padding-bottom: self.padding;
|
||||||
|
padding-left: self.padding;
|
||||||
|
|
||||||
|
preferred-height: text-input.preferred-height + self.padding-top + self.padding-bottom;
|
||||||
|
height: self.preferred-height;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
background: white.with-alpha(15%);
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
x: root.padding-left;
|
||||||
|
width: parent.width - root.padding-left - root.padding-right;
|
||||||
|
|
||||||
|
y: root.padding-top;
|
||||||
|
height: parent.height - root.padding-top - root.padding-bottom;
|
||||||
|
|
||||||
|
spacing: icon.preferred-width > 0 ? 10px : 0px;
|
||||||
|
|
||||||
|
icon := AppIcon {
|
||||||
|
max-width: self.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppText {
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
|
||||||
|
horizontal-alignment: left;
|
||||||
|
font-size: text-input.font-size;
|
||||||
|
text: text-input.text == "" ? root.placeholder-text : "";
|
||||||
|
|
||||||
|
text-input := TextInput {
|
||||||
|
color: AppPalette.foreground;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppIcon {
|
||||||
|
source: AppImages.xmark;
|
||||||
|
max-width: self.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
examples/weather-demo/ui/controls/stackview.slint
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief The StackView is a component that can be used to simulate a stack view.
|
||||||
|
*
|
||||||
|
* \note: Due to the language limitation only the partial implementaion is possible.
|
||||||
|
* The component is meant to be a helper.
|
||||||
|
*
|
||||||
|
* \par Usage:
|
||||||
|
* The component can be used in two ways:
|
||||||
|
* - for smaller pages, where all pages are loaded at once,
|
||||||
|
* - for more complex pages, where pages are loaded dynamically.
|
||||||
|
*
|
||||||
|
* Static pages:
|
||||||
|
* \code{*.slint}
|
||||||
|
* stack := StackView {
|
||||||
|
* current-index: 0;
|
||||||
|
* min-index: 0;
|
||||||
|
*
|
||||||
|
* for color in [ Colors.red, Colors.green, Colors.blue ]:
|
||||||
|
* StackPage {
|
||||||
|
* is-current: self.check-is-current(stack.current-index);
|
||||||
|
* init => { self.page-index = stack.insert-page(); } // StackPage.count increased with insert-page function
|
||||||
|
*
|
||||||
|
* TestPage {
|
||||||
|
* background: color;
|
||||||
|
* push => { stack.push(); }
|
||||||
|
* pop => { stack.pop(); }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* \endcode
|
||||||
|
*
|
||||||
|
* Dynamic pages:
|
||||||
|
* \code{*.slint}
|
||||||
|
* stack := StackView {
|
||||||
|
* count: 2; // StackPage.count provided manually
|
||||||
|
* current-index: 0;
|
||||||
|
* min-index: 0;
|
||||||
|
*
|
||||||
|
* if (stack.current-index == 0): StackPage {
|
||||||
|
* page-index: 0; is-current: true;
|
||||||
|
*
|
||||||
|
* TestPage {
|
||||||
|
* background: Colors.red;
|
||||||
|
* push => { stack.push(); }
|
||||||
|
* pop => { stack.pop(); }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* if (stack.current-index == 1): StackPage {
|
||||||
|
* page-index: 1; is-current: true;
|
||||||
|
*
|
||||||
|
* TestPage {
|
||||||
|
* background: Colors.green;
|
||||||
|
* push => { stack.push(); }
|
||||||
|
* pop => { stack.pop(); }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* \endcode
|
||||||
|
*
|
||||||
|
* \sa StackPage
|
||||||
|
*/
|
||||||
|
export component StackView inherits Rectangle {
|
||||||
|
/// \brief This property states the number of items in the stack
|
||||||
|
in-out property<int> count: 0;
|
||||||
|
|
||||||
|
/// \brief This property states the index of the currently visible item
|
||||||
|
in-out property<int> current-index: -1;
|
||||||
|
|
||||||
|
/// \brief This property configures the minimum index the pop function can set (-1 by default)
|
||||||
|
in property<int> min-index: -1;
|
||||||
|
|
||||||
|
/// \brief This property configures the minimum index the push function can set (#count -1 by default)
|
||||||
|
in property<int> max-index: self.count - 1;
|
||||||
|
|
||||||
|
/// \brief This function increases the pages #count by one and returns new page index
|
||||||
|
public function insert-page() -> int {
|
||||||
|
self.count += 1;
|
||||||
|
return self.count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \brief This function increases the #current-index if possible
|
||||||
|
public function push() {
|
||||||
|
if (self.current-index < Math.min(self.max-index, self.count - 1)) {
|
||||||
|
self.current-index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \brief This function decreased the #current-index if possible
|
||||||
|
public function pop() {
|
||||||
|
if (self.current-index > Math.max(self.min-index, -1)) {
|
||||||
|
self.current-index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief The StackPage is a component to use in the StackView.
|
||||||
|
*
|
||||||
|
* The real content can either derive from the StackPage (which is Rectangle based) or can be contained
|
||||||
|
* as the page children.
|
||||||
|
*
|
||||||
|
* Inherits:
|
||||||
|
* \code{*.slint}
|
||||||
|
* export TestPage inherits StackPage {
|
||||||
|
* background: color;
|
||||||
|
* }
|
||||||
|
* \endcode
|
||||||
|
*
|
||||||
|
* Contains:
|
||||||
|
* \code{*.slint}
|
||||||
|
* StackPage {
|
||||||
|
* TestPage {
|
||||||
|
* background: Colors.red;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* \endcode
|
||||||
|
*
|
||||||
|
* \sa StackView
|
||||||
|
*/
|
||||||
|
export component StackPage inherits TouchArea {
|
||||||
|
/// \brief This property configures the page index
|
||||||
|
in property<int> page-index: -1;
|
||||||
|
|
||||||
|
/// \brief This property configures whether the page is a current page (if not, it is hidden)
|
||||||
|
in property<bool> is-current: false;
|
||||||
|
|
||||||
|
/// \brief This function is a helper function to use when setting the #is-current property
|
||||||
|
public pure function check-is-current(current-index: int) -> bool {
|
||||||
|
return current-index == self.page-index;
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: self.is-current;
|
||||||
|
}
|
158
examples/weather-demo/ui/controls/weather.slint
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { AppPalette, AppFonts } from "../style/styles.slint";
|
||||||
|
import { AppText } from "./generic.slint";
|
||||||
|
import { WindowInfo } from "../ui_utils.slint";
|
||||||
|
import { IconType } from "../weather_datatypes.slint";
|
||||||
|
|
||||||
|
export component WeatherIconBase inherits Text {
|
||||||
|
color: AppPalette.foreground;
|
||||||
|
font-family: AppFonts.weather-icons-font-name;
|
||||||
|
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
export component WeatherIcon inherits WeatherIconBase {
|
||||||
|
in property<IconType> icon-type;
|
||||||
|
|
||||||
|
pure function get-weather-icon-url(type: IconType) -> string {
|
||||||
|
if (type == IconType.Sunny) { return "\u{f00d}"; }
|
||||||
|
if (type == IconType.PartiallyCloudy) { return "\u{f002}"; }
|
||||||
|
if (type == IconType.MostlyCloudy) { return "\u{f041}"; }
|
||||||
|
if (type == IconType.Cloudy) { return "\u{f013}"; }
|
||||||
|
if (type == IconType.SunnyRainy) { return "\u{f008}"; }
|
||||||
|
if (type == IconType.Rainy) { return "\u{f015}"; }
|
||||||
|
if (type == IconType.Stormy) { return "\u{f01e}"; }
|
||||||
|
if (type == IconType.Snowy) { return "\u{f064}"; }
|
||||||
|
if (type == IconType.Foggy) { return "\u{f063}"; }
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
pure function get-weather-icon-color(type: IconType) -> color {
|
||||||
|
if (type == IconType.Sunny) { return AppPalette.sun-yellow; }
|
||||||
|
|
||||||
|
return AppPalette.foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
text: root.get-weather-icon-url(root.icon-type);
|
||||||
|
color: root.get-weather-icon-color(root.icon-type);
|
||||||
|
}
|
||||||
|
|
||||||
|
component DataText inherits AppText {
|
||||||
|
in property<bool> minimal: false;
|
||||||
|
|
||||||
|
font-size: root.minimal ? (WindowInfo.is-portrait ? 0.85rem : 0.9rem) : 1.1rem;
|
||||||
|
overflow: elide;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
export component RainInfo inherits Rectangle {
|
||||||
|
in property<float> precipitation-probability;
|
||||||
|
in property<float> rain-volume;
|
||||||
|
in property<float> snow-volume;
|
||||||
|
|
||||||
|
in property<bool> minimal: false;
|
||||||
|
|
||||||
|
property<bool> is-snow: root.snow-volume > root.rain-volume;
|
||||||
|
property<float> volume: Math.max(root.rain-volume, root.snow-volume);
|
||||||
|
property<float> probability: Math.round(root.precipitation-probability * 100);
|
||||||
|
|
||||||
|
property<string> volume-display: Math.round(volume * 10) / 10;
|
||||||
|
property<string> type-indicator: self.is-snow ? "\u{f076}" : "\u{f078}";
|
||||||
|
property<color> type-color: self.is-snow ? AppPalette.snow-white : AppPalette.rain-blue;
|
||||||
|
|
||||||
|
property<float> max-bar-volume: 10;
|
||||||
|
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
|
||||||
|
opacity: root.minimal ? (root.precipitation-probability * 30% + 70%) : 100%;
|
||||||
|
|
||||||
|
if !root.minimal: Rectangle {
|
||||||
|
x: parent.width - self.width;
|
||||||
|
y: parent.height - self.height;
|
||||||
|
|
||||||
|
width: 3px;
|
||||||
|
height: (parent.height - parent.padding-top - parent.padding-bottom) *
|
||||||
|
Math.min(root.volume, root.max-bar-volume) / root.max-bar-volume * 100%;
|
||||||
|
|
||||||
|
background: root.type-color;
|
||||||
|
opacity: root.precipitation-probability * 70% + 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 3px;
|
||||||
|
|
||||||
|
DataText {
|
||||||
|
minimal: root.minimal;
|
||||||
|
text: "\{root.probability}%";
|
||||||
|
|
||||||
|
color: root.type-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !root.minimal || !WindowInfo.is-portrait: WeatherIcon {
|
||||||
|
font-size: root.minimal ? 0.9rem : 1rem;
|
||||||
|
text: "\{root.type-indicator}";
|
||||||
|
color: root.type-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.minimal && WindowInfo.is-portrait: DataText {
|
||||||
|
minimal: true;
|
||||||
|
text: "/ \{root.volume-display}l";
|
||||||
|
|
||||||
|
color: root.type-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !root.minimal || !WindowInfo.is-portrait: DataText {
|
||||||
|
minimal: root.minimal;
|
||||||
|
text: "\{root.volume-display}l";
|
||||||
|
|
||||||
|
color: root.type-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component UvInfo inherits Rectangle {
|
||||||
|
in property<int> uv-index;
|
||||||
|
|
||||||
|
in property<bool> minimal: false;
|
||||||
|
|
||||||
|
property<float> uv-index-rate: (root.uv-index / 12.0);
|
||||||
|
|
||||||
|
opacity: root.minimal ? (root.uv-index-rate * 30% + 70%) : 100%;
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 3px;
|
||||||
|
|
||||||
|
WeatherIcon {
|
||||||
|
text: "\u{f06e}";
|
||||||
|
|
||||||
|
font-size: root.minimal ? 0.9rem : 1.3rem;
|
||||||
|
|
||||||
|
opacity: root.minimal ? 100% : (root.uv-index-rate * 70% + 30%);
|
||||||
|
color: AppPalette.sun-yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.minimal: DataText {
|
||||||
|
minimal: true;
|
||||||
|
text: "UV";
|
||||||
|
|
||||||
|
color: AppPalette.sun-yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataText {
|
||||||
|
minimal: root.minimal;
|
||||||
|
text: "\{root.uv-index}";
|
||||||
|
|
||||||
|
color: AppPalette.sun-yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
285
examples/weather-demo/ui/expanded_city_weather_tile.slint
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { VerticalBox, HorizontalBox } from "std-widgets.slint";
|
||||||
|
|
||||||
|
import { WindowInfo } from "./ui_utils.slint";
|
||||||
|
import { AppPalette } from "./style/styles.slint";
|
||||||
|
import { AppText } from "./controls/generic.slint";
|
||||||
|
import { WeatherIcon, RainInfo, UvInfo } from "./controls/weather.slint";
|
||||||
|
import { WeatherInfo, WeatherForecastInfo, CityWeatherInfo } from "weather_datatypes.slint";
|
||||||
|
import { CityWeather } from "weather_datatypes.slint";
|
||||||
|
|
||||||
|
import { CityWeatherTile } from "city_weather_tile.slint";
|
||||||
|
|
||||||
|
component ForecastDayLineBase inherits Rectangle {
|
||||||
|
out property<{temp: length, rain: length, uv: length}> fields-width: {
|
||||||
|
temp: 90px, rain: 65px, uv: 65px
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
component ForecastDataText inherits AppText {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
overflow: elide;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
component ForecastTitleText inherits ForecastDataText {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
component ForecastTitleLine inherits ForecastDayLineBase {
|
||||||
|
HorizontalLayout {
|
||||||
|
// spacer
|
||||||
|
Rectangle { horizontal-stretch: 1; }
|
||||||
|
|
||||||
|
ForecastTitleText {
|
||||||
|
preferred-width: root.fields-width.temp;
|
||||||
|
text: @tr("Max/Min");
|
||||||
|
}
|
||||||
|
ForecastTitleText {
|
||||||
|
preferred-width: root.fields-width.rain;
|
||||||
|
text: @tr("Rain");
|
||||||
|
}
|
||||||
|
ForecastTitleText {
|
||||||
|
preferred-width: root.fields-width.uv;
|
||||||
|
text: @tr("UV");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !WindowInfo.is-portrait: HorizontalLayout {
|
||||||
|
width: WindowInfo.is-portrait ? 0% : 50%;
|
||||||
|
|
||||||
|
for description[index] in [ @tr("Morning"), @tr("Day"), @tr("Evening"), @tr("Night") ]:
|
||||||
|
ForecastTitleText {
|
||||||
|
width: 25%;
|
||||||
|
text: description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ForecastDayLine inherits ForecastDayLineBase {
|
||||||
|
in property<string> day-name;
|
||||||
|
in property<WeatherInfo> day-weather;
|
||||||
|
|
||||||
|
height: 50px;
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
// spacer
|
||||||
|
Rectangle { width: 5px; }
|
||||||
|
|
||||||
|
name-text := ForecastDataText {
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
min-width: self.preferred-width;
|
||||||
|
|
||||||
|
horizontal-alignment: left;
|
||||||
|
text: root.day-name;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeatherIcon {
|
||||||
|
icon-type: root.day-weather.icon_type;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
|
||||||
|
visible: WindowInfo.window-width >= 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// spacer
|
||||||
|
Rectangle {
|
||||||
|
max-width: WindowInfo.window-width >= 380px ? self.preferred-width : 0;
|
||||||
|
preferred-width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ForecastDataText {
|
||||||
|
property<int> min-temp: Math.round(root.day-weather.detailed_temp.min);
|
||||||
|
property<int> max-temp: Math.round(root.day-weather.detailed_temp.max);
|
||||||
|
|
||||||
|
preferred-width: root.fields-width.temp;
|
||||||
|
text: "\{self.max-temp}° / \{self.min-temp}°";
|
||||||
|
}
|
||||||
|
|
||||||
|
RainInfo {
|
||||||
|
precipitation-probability: root.day-weather.precipitation_prob;
|
||||||
|
rain-volume: root.day-weather.rain;
|
||||||
|
snow-volume: root.day-weather.snow;
|
||||||
|
|
||||||
|
preferred-width: root.fields-width.rain;
|
||||||
|
}
|
||||||
|
|
||||||
|
UvInfo {
|
||||||
|
uv-index: root.day-weather.uv;
|
||||||
|
|
||||||
|
preferred-width: root.fields-width.uv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ForecastDayDetails inherits HorizontalLayout {
|
||||||
|
in property<WeatherInfo> day-weather;
|
||||||
|
|
||||||
|
property<[{ time: string, temp: float }]> temp-model: [
|
||||||
|
{ time: "\u{f051}", temp: Math.round(root.day-weather.detailed_temp.morning) },
|
||||||
|
{ time: "\u{f00d}", temp: Math.round(root.day-weather.detailed_temp.day) },
|
||||||
|
{ time: "\u{f052}", temp: Math.round(root.day-weather.detailed_temp.evening) },
|
||||||
|
{ time: "\u{f02e}", temp: Math.round(root.day-weather.detailed_temp.night) },
|
||||||
|
];
|
||||||
|
|
||||||
|
padding-top: WindowInfo.is-portrait ? 10px : 0;
|
||||||
|
padding-bottom: WindowInfo.is-portrait ? 10px : 0;
|
||||||
|
|
||||||
|
for time-temp in root.temp-model:
|
||||||
|
HorizontalLayout {
|
||||||
|
width: 25%;
|
||||||
|
alignment: center;
|
||||||
|
spacing: 5px;
|
||||||
|
|
||||||
|
WeatherIcon {
|
||||||
|
text: "\{time-temp.time}";
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ForecastDataText {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
text: "\{time-temp.temp}°";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ForecastDayDelegate inherits TouchArea {
|
||||||
|
in property<bool> expanded: false;
|
||||||
|
in property<bool> alternative-background: false;
|
||||||
|
|
||||||
|
in property<string> day-name;
|
||||||
|
in property<WeatherInfo> day-weather;
|
||||||
|
|
||||||
|
animate height { duration: 250ms; easing: ease-in-out-quad; }
|
||||||
|
|
||||||
|
height: root.expanded ? self.preferred-height : main-info-line.preferred-height;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
background: root.alternative-background ? Colors.white.transparentize(80%) : transparent;
|
||||||
|
clip: true;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
main-info-line := HorizontalLayout {
|
||||||
|
ForecastDayLine {
|
||||||
|
day-name: root.day-name;
|
||||||
|
day-weather: root.day-weather;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !WindowInfo.is-portrait: ForecastDayDetails {
|
||||||
|
width: 50%;
|
||||||
|
day-weather: root.day-weather;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if WindowInfo.is-portrait: ForecastDayDetails {
|
||||||
|
day-weather: root.day-weather;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component ExpandedCityWeatherTile inherits TouchArea {
|
||||||
|
in property<CityWeatherInfo> city-weather-info <=> base-tile.city-weather-info;
|
||||||
|
in property<bool> alternative-background <=> base-tile.alternative-background;
|
||||||
|
|
||||||
|
out property<bool> expanded: false;
|
||||||
|
in-out property<duration> animation-duration: 300ms;
|
||||||
|
|
||||||
|
in property<length> block-x;
|
||||||
|
in property<length> block-y;
|
||||||
|
in property<length> block-width;
|
||||||
|
in property<length> block-height;
|
||||||
|
|
||||||
|
in property<length> full-x;
|
||||||
|
in property<length> full-y;
|
||||||
|
in property<length> full-width;
|
||||||
|
in property<length> full-height;
|
||||||
|
|
||||||
|
public function expand() {
|
||||||
|
// Not working properly without the lines below. (A bug?)
|
||||||
|
// Seems the animation in transition using old values, and
|
||||||
|
// accessing the properties somehow forces the update.
|
||||||
|
root.x; root.y; root.width; root.height;
|
||||||
|
details-rect.height;
|
||||||
|
|
||||||
|
root.expanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collapse() {
|
||||||
|
root.expanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
x: self.block-x;
|
||||||
|
y: self.block-y;
|
||||||
|
width: self.block-width;
|
||||||
|
height: self.block-height;
|
||||||
|
|
||||||
|
visible: self.opacity > 0;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
states [
|
||||||
|
full-size when root.expanded: {
|
||||||
|
opacity: 1;
|
||||||
|
x: full-x;
|
||||||
|
y: full-y;
|
||||||
|
width: full-width;
|
||||||
|
height: full-height;
|
||||||
|
|
||||||
|
in {
|
||||||
|
animate x, y, width, height { duration: root.animation-duration; easing: ease-in-out-quad; }
|
||||||
|
}
|
||||||
|
out {
|
||||||
|
animate x, y, width, height { duration: root.animation-duration; easing: ease-in-out-quad; }
|
||||||
|
animate opacity { delay: root.animation-duration; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
base-tile := CityWeatherTile {
|
||||||
|
show-animations: false;
|
||||||
|
|
||||||
|
clicked => {
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details-rect := Rectangle {
|
||||||
|
vertical-stretch: 1;
|
||||||
|
|
||||||
|
clip: true;
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
|
||||||
|
y: self.padding-top;
|
||||||
|
height: parent.height - self.padding-top - self.padding-bottom;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: start;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
|
||||||
|
ForecastTitleLine {}
|
||||||
|
|
||||||
|
for day-forecast-weather[index] in root.city-weather-info.forecast-weather:
|
||||||
|
ForecastDayDelegate {
|
||||||
|
alternative-background: Math.mod(index, 2) == 0;
|
||||||
|
day-name: day-forecast-weather.day-name;
|
||||||
|
day-weather: day-forecast-weather.weather-info;
|
||||||
|
|
||||||
|
clicked => {
|
||||||
|
self.expanded = !self.expanded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
examples/weather-demo/ui/forecast_with_graph.slint
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { VerticalBox } from "std-widgets.slint";
|
||||||
|
import { AppPalette } from "./style/styles.slint";
|
||||||
|
import { AppText } from "./controls/generic.slint";
|
||||||
|
import { WeatherIcon, RainInfo, UvInfo } from "./controls/weather.slint";
|
||||||
|
import { WeatherInfo, WeatherForecastInfo, CityWeather } from "./weather_datatypes.slint";
|
||||||
|
|
||||||
|
component ForecastGraphText inherits AppText {
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
component DayForecastGraphEntry inherits VerticalLayout {
|
||||||
|
in property <string> day-name;
|
||||||
|
in property <WeatherInfo> day-weather;
|
||||||
|
in property <bool> detailed: true;
|
||||||
|
|
||||||
|
spacing: 5px;
|
||||||
|
|
||||||
|
ForecastGraphText {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
text: day-name;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeatherIcon {
|
||||||
|
icon-type: day-weather.icon-type;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
spacing: 5px;
|
||||||
|
|
||||||
|
ForecastGraphText {
|
||||||
|
text: Math.round(day-weather.detailed_temp.max) + "° / " + Math.round(day-weather.detailed_temp.min) + "°";
|
||||||
|
}
|
||||||
|
|
||||||
|
RainInfo {
|
||||||
|
precipitation-probability: root.day-weather.precipitation_prob;
|
||||||
|
rain-volume: root.day-weather.rain;
|
||||||
|
snow-volume: root.day-weather.snow;
|
||||||
|
|
||||||
|
minimal: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UvInfo {
|
||||||
|
uv-index: root.day-weather.uv;
|
||||||
|
|
||||||
|
minimal: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component DayForecastGraph inherits Rectangle {
|
||||||
|
in property <[WeatherForecastInfo]> forecast-weather;
|
||||||
|
in property <bool> show-animations: true;
|
||||||
|
|
||||||
|
property <length> preferred-day-width: 85px;
|
||||||
|
|
||||||
|
// max-days-count is not directly as a binding here, only when the value is actually changed.
|
||||||
|
// This is to avoid reevaluation of the conditional components that rely on it for every window size change.
|
||||||
|
// see: https://github.com/slint-ui/slint/issues/5209
|
||||||
|
property <int> max-days-count: 0;
|
||||||
|
property <int> days-count: Math.min(root.forecast-weather.length, root.max-days-count);
|
||||||
|
|
||||||
|
property <length> day-width: root.width / root.days-count;
|
||||||
|
|
||||||
|
function update-max-days-count() {
|
||||||
|
if (Math.floor(root.width / root.preferred-day-width) != root.max-days-count) {
|
||||||
|
root.max-days-count = Math.floor(root.width / root.preferred-day-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init => { root.update-max-days-count(); }
|
||||||
|
changed width => { root.update-max-days-count(); }
|
||||||
|
|
||||||
|
preferred-height: layout.preferred-height;
|
||||||
|
|
||||||
|
Path {
|
||||||
|
property <float> visible-part: 0%;
|
||||||
|
|
||||||
|
y: 0;
|
||||||
|
height: 50%;
|
||||||
|
|
||||||
|
stroke-width: 2px;
|
||||||
|
commands: CityWeather.get_forecast_graph_command(
|
||||||
|
root.forecast-weather, root.days-count, self.width, self.height);
|
||||||
|
|
||||||
|
stroke: @linear-gradient(90deg, AppPalette.foreground.with-alpha(25%) 0%,
|
||||||
|
AppPalette.foreground.with-alpha(25%) self.visible-part,
|
||||||
|
transparent self.visible-part,
|
||||||
|
transparent 100%);
|
||||||
|
|
||||||
|
opacity: 0.0;
|
||||||
|
animate opacity { duration: root.show-animations ? 1200ms : 0ms; easing: ease-in; }
|
||||||
|
animate visible-part { duration: root.show-animations ? 900ms : 0ms; easing: ease-in; }
|
||||||
|
|
||||||
|
init => {
|
||||||
|
self.opacity = 1.0;
|
||||||
|
self.visible-part = 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout := HorizontalLayout {
|
||||||
|
for index in root.days-count:
|
||||||
|
DayForecastGraphEntry {
|
||||||
|
property <WeatherForecastInfo> day-forecast-weather: root.forecast-weather[index];
|
||||||
|
property <duration> animation-duration: 0ms;
|
||||||
|
|
||||||
|
width: root.day-width;
|
||||||
|
day-name: day-forecast-weather.day-name;
|
||||||
|
day-weather: day-forecast-weather.weather-info;
|
||||||
|
|
||||||
|
opacity: 0.0;
|
||||||
|
animate opacity { duration: self.animation-duration; easing: ease-in-out-quad; }
|
||||||
|
|
||||||
|
init => {
|
||||||
|
if (root.show-animations) {
|
||||||
|
self.animation-duration = 600ms + (500ms - index * 50ms) * index;
|
||||||
|
}
|
||||||
|
self.opacity = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
examples/weather-demo/ui/location_datatypes.slint
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
export struct GeoLocationEntry {
|
||||||
|
name: string,
|
||||||
|
state: string,
|
||||||
|
country: string,
|
||||||
|
lat: float,
|
||||||
|
lon: float,
|
||||||
|
}
|
||||||
|
|
||||||
|
export global GeoLocation {
|
||||||
|
in property <[GeoLocationEntry]> result-list;
|
||||||
|
|
||||||
|
callback search-location(string);
|
||||||
|
callback add-location(GeoLocationEntry);
|
||||||
|
}
|
68
examples/weather-demo/ui/location_search.slint
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { PageBase } from "page-base.slint";
|
||||||
|
import { AppImages } from "./style/styles.slint";
|
||||||
|
import { AppText, TextField } from "./controls/generic.slint";
|
||||||
|
import { BusyLayerController } from "./controls/busy-layer.slint";
|
||||||
|
import { GeoLocation } from "./location_datatypes.slint";
|
||||||
|
|
||||||
|
import { Button } from "std-widgets.slint";
|
||||||
|
|
||||||
|
export component LocationSearchView inherits PageBase {
|
||||||
|
callback close-request;
|
||||||
|
|
||||||
|
public function clear() {
|
||||||
|
GeoLocation.search_location("");
|
||||||
|
text-field.text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
forward-focus: text-field;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
padding: 20px;
|
||||||
|
spacing: 10px;
|
||||||
|
|
||||||
|
text-field := TextField {
|
||||||
|
icon-source: AppImages.search;
|
||||||
|
placeholder-text: "Search";
|
||||||
|
|
||||||
|
edited => {
|
||||||
|
GeoLocation.search_location(self.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
VerticalLayout {
|
||||||
|
alignment: start;
|
||||||
|
|
||||||
|
for data[index] in GeoLocation.result-list : Rectangle {
|
||||||
|
preferred-height: layout.preferred-height + 20px;
|
||||||
|
min-height: self.preferred-height;
|
||||||
|
|
||||||
|
layout := VerticalLayout {
|
||||||
|
alignment: center;
|
||||||
|
spacing: 5px;
|
||||||
|
|
||||||
|
AppText {
|
||||||
|
text: data.name;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
AppText {
|
||||||
|
text: data.state == "" ? data.country : data.state + ", " + data.country;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
clicked => {
|
||||||
|
BusyLayerController.set-busy();
|
||||||
|
GeoLocation.add-location(data);
|
||||||
|
root.close-request();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
195
examples/weather-demo/ui/main.slint
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { Palette } from "std-widgets.slint";
|
||||||
|
import { WindowInfo, WindowInfoHelper } from "./ui_utils.slint";
|
||||||
|
import { StackView, StackPage } from "./controls/stackview.slint";
|
||||||
|
import { CityListView } from "./city_weather.slint";
|
||||||
|
import { CityWeather } from "./weather_datatypes.slint";
|
||||||
|
import { LocationSearchView } from "./location_search.slint";
|
||||||
|
import { GeoLocation } from "./location_datatypes.slint";
|
||||||
|
import { AppPalette, AppFonts, AppImages } from "./style/styles.slint";
|
||||||
|
import { FloatingTextButton } from "./controls/generic.slint";
|
||||||
|
import { BusyLayerController, BusyLayer } from "./controls/busy-layer.slint";
|
||||||
|
|
||||||
|
// Re export for native rust
|
||||||
|
export { WindowInfo, AppPalette, BusyLayerController, CityWeather, GeoLocation }
|
||||||
|
|
||||||
|
component EdgeFloatingTextButton inherits FloatingTextButton {
|
||||||
|
out property<length> edge-spacing: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
component AnimatedStackPage inherits StackPage {
|
||||||
|
// is-active and is-opened are not set as a binding here, only when the value is actually changed.
|
||||||
|
// This is to avoid redundant reevaluation of dependent properties and conditional elements.
|
||||||
|
// see: https://github.com/slint-ui/slint/issues/5209
|
||||||
|
out property<bool> is-active: false;
|
||||||
|
out property<bool> is-opened: false;
|
||||||
|
|
||||||
|
// using a helper int property to be able to use animate
|
||||||
|
property<int> is-active-value: 0;
|
||||||
|
|
||||||
|
property<duration> animation-duration: 250ms;
|
||||||
|
|
||||||
|
visible: root.is-active;
|
||||||
|
|
||||||
|
init => { root.is-active = (self.is-active_value == 1); }
|
||||||
|
changed is-active-value => { root.is-active = (self.is-active_value == 1); }
|
||||||
|
|
||||||
|
states [
|
||||||
|
active when self.is-current: {
|
||||||
|
is-active-value: 1;
|
||||||
|
|
||||||
|
out {
|
||||||
|
animate is-active-value { delay: root.animation-duration; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
content := Rectangle {
|
||||||
|
changed y => {
|
||||||
|
// First open animation is not working properly without the line below. (A bug?)
|
||||||
|
// Seems the animation in transition is using old values,
|
||||||
|
// and accessing the property somehow forces the update.
|
||||||
|
self.y;
|
||||||
|
|
||||||
|
if (root.is-opened != (self.y == 0)) {
|
||||||
|
root.is-opened = (self.y == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
y: root.is-current ? 0px : root.height;
|
||||||
|
|
||||||
|
animate y { duration: root.animation-duration; easing: ease-in-out-quad; }
|
||||||
|
|
||||||
|
@children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PageType {
|
||||||
|
Main,
|
||||||
|
AddLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AppWindow inherits Window {
|
||||||
|
background: AppPalette.background;
|
||||||
|
default-font-size: AppFonts.default-font-size;
|
||||||
|
|
||||||
|
preferred-width: 900px;
|
||||||
|
preferred-height: 600px;
|
||||||
|
|
||||||
|
WindowInfoHelper {
|
||||||
|
init => {
|
||||||
|
// no support for the different modes currently
|
||||||
|
// this is to display slint badge in proper colors
|
||||||
|
Palette.color-scheme = ColorScheme.dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this component is added to overcome a slint issue with android cursor never being hidden
|
||||||
|
// see: https://github.com/slint-ui/slint/issues/5233
|
||||||
|
invisible-input := TextInput {
|
||||||
|
y: -self.height * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
stack := StackView {
|
||||||
|
function show-page(pageType : PageType) {
|
||||||
|
if (pageType == PageType.Main) {
|
||||||
|
self.current-index = 0;
|
||||||
|
}
|
||||||
|
else if (pageType == PageType.AddLocation) {
|
||||||
|
self.current-index = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function back-to-main() {
|
||||||
|
self.show-page(PageType.Main);
|
||||||
|
}
|
||||||
|
|
||||||
|
current-index: 0;
|
||||||
|
min-index: 0;
|
||||||
|
|
||||||
|
StackPage {
|
||||||
|
is-current: self.check-is-current(stack.current-index);
|
||||||
|
init => { self.page-index = stack.insert-page(); }
|
||||||
|
visible: self.page-index <= stack.current-index;
|
||||||
|
|
||||||
|
CityListView {}
|
||||||
|
|
||||||
|
// right (refresh) button
|
||||||
|
EdgeFloatingTextButton {
|
||||||
|
x: parent.width - self.width - self.edge-spacing;
|
||||||
|
y: parent.height - self.height - self.edge-spacing;
|
||||||
|
|
||||||
|
icon-source: AppImages.refresh;
|
||||||
|
|
||||||
|
clicked => {
|
||||||
|
BusyLayerController.set-busy();
|
||||||
|
CityWeather.refresh-all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// left (add) button
|
||||||
|
EdgeFloatingTextButton {
|
||||||
|
x: self.edge-spacing;
|
||||||
|
y: parent.height - self.height - self.edge-spacing;
|
||||||
|
|
||||||
|
visible: CityWeather.can-add-city;
|
||||||
|
|
||||||
|
icon-source: AppImages.plus;
|
||||||
|
|
||||||
|
clicked => {
|
||||||
|
stack.show-page(PageType.AddLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedStackPage {
|
||||||
|
is-current: self.check-is-current(stack.current-index);
|
||||||
|
init => { self.page-index = stack.insert-page(); }
|
||||||
|
|
||||||
|
changed is-active => {
|
||||||
|
if (!self.is-active) {
|
||||||
|
// focusing invisible input so the cursor is drawn out of the window scope
|
||||||
|
invisible-input.focus();
|
||||||
|
// clearing focus right after to hide the keyboard
|
||||||
|
invisible-input.clear-focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location-search-view := LocationSearchView {
|
||||||
|
property<bool> is-active: parent.is-active;
|
||||||
|
property<bool> is-opened: parent.is-opened;
|
||||||
|
|
||||||
|
changed is-active => {
|
||||||
|
if (self.is-active) {
|
||||||
|
self.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changed is-opened => {
|
||||||
|
if (self.is-opened) {
|
||||||
|
// we cannot focus on init, because android cursor is then wrongly positioned
|
||||||
|
self.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close-request => {
|
||||||
|
self.clear-focus();
|
||||||
|
stack.back-to-main();
|
||||||
|
}
|
||||||
|
|
||||||
|
EdgeFloatingTextButton {
|
||||||
|
x: parent.width - self.width - self.edge-spacing;
|
||||||
|
y: parent.height - self.height - self.edge-spacing;
|
||||||
|
|
||||||
|
icon-source: AppImages.xmark;
|
||||||
|
|
||||||
|
clicked => { location-search-view.close-request(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if BusyLayerController.is-busy: BusyLayer {}
|
||||||
|
}
|
8
examples/weather-demo/ui/page-base.slint
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { AppPalette } from "style/styles.slint";
|
||||||
|
|
||||||
|
export component PageBase inherits Rectangle {
|
||||||
|
background: @linear-gradient(180deg, AppPalette.background 0%, AppPalette.alternate-background 100%);
|
||||||
|
}
|
34
examples/weather-demo/ui/style/styles.slint
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { Palette } from "std-widgets.slint";
|
||||||
|
|
||||||
|
import "../assets/weathericons-font.ttf";
|
||||||
|
|
||||||
|
export global AppPalette {
|
||||||
|
out property<brush> background: #1673b4;
|
||||||
|
out property<brush> alternate-background: #2296bc;
|
||||||
|
out property<brush> foreground: white;
|
||||||
|
|
||||||
|
out property<brush> sun-yellow: Colors.yellow;
|
||||||
|
out property<brush> snow-white: Colors.cornsilk;
|
||||||
|
out property<brush> rain-blue: #7DCDFF.brighter(15%);
|
||||||
|
|
||||||
|
out property<brush> error-red: Colors.red.darker(20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
export global AppFonts {
|
||||||
|
out property<length> default-font-size: 10pt;
|
||||||
|
|
||||||
|
out property<string> weather-icons-font-name: "Weather Icons";
|
||||||
|
}
|
||||||
|
|
||||||
|
export global AppImages {
|
||||||
|
out property <image> arrow-down: @image-url("../assets/icons/arrow-down.svg");
|
||||||
|
out property <image> arrow-up: @image-url("../assets/icons/arrow-up.svg");
|
||||||
|
out property <image> plus: @image-url("../assets/icons/plus.svg");
|
||||||
|
out property <image> refresh: @image-url("../assets/icons/refresh.svg");
|
||||||
|
out property <image> search: @image-url("../assets/icons/search.svg");
|
||||||
|
out property <image> trash: @image-url("../assets/icons/trash.svg");
|
||||||
|
out property <image> xmark: @image-url("../assets/icons/xmark.svg");
|
||||||
|
}
|
30
examples/weather-demo/ui/ui_utils.slint
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
export global WindowInfo {
|
||||||
|
// default values provided for the slint-viewer
|
||||||
|
in property<length> window-width: 400px;
|
||||||
|
in property<length> window-height: 700px;
|
||||||
|
|
||||||
|
// is-portrait is not set as a binding here, only when the value is actually changed.
|
||||||
|
// This is to avoid reevaluation of the conditional components that rely on it for every window size change.
|
||||||
|
// see: https://github.com/slint-ui/slint/issues/5209
|
||||||
|
in property<bool> is-portrait: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export component WindowInfoHelper inherits Rectangle {
|
||||||
|
function check-is-portrait() {
|
||||||
|
if (WindowInfo.is-portrait != (self.width < self.height)) {
|
||||||
|
WindowInfo.is-portrait = (self.width < self.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changed width => {
|
||||||
|
WindowInfo.window-width = self.width;
|
||||||
|
self.check-is-portrait();
|
||||||
|
}
|
||||||
|
changed height => {
|
||||||
|
WindowInfo.window-height = self.height;
|
||||||
|
self.check-is-portrait();
|
||||||
|
}
|
||||||
|
}
|
57
examples/weather-demo/ui/weather_datatypes.slint
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
export enum IconType {
|
||||||
|
Unknown,
|
||||||
|
Sunny,
|
||||||
|
PartiallyCloudy,
|
||||||
|
MostlyCloudy,
|
||||||
|
Cloudy,
|
||||||
|
SunnyRainy,
|
||||||
|
Rainy,
|
||||||
|
Stormy,
|
||||||
|
Snowy,
|
||||||
|
Foggy,
|
||||||
|
}
|
||||||
|
|
||||||
|
export struct TemperatureInfo {
|
||||||
|
min: float,
|
||||||
|
max:float,
|
||||||
|
|
||||||
|
morning: float,
|
||||||
|
day: float,
|
||||||
|
evening:float,
|
||||||
|
night:float,
|
||||||
|
}
|
||||||
|
|
||||||
|
export struct WeatherInfo {
|
||||||
|
description: string,
|
||||||
|
icon_type: IconType,
|
||||||
|
current_temp: float,
|
||||||
|
detailed_temp: TemperatureInfo,
|
||||||
|
uv: int,
|
||||||
|
precipitation_prob: float,
|
||||||
|
rain: float,
|
||||||
|
snow: float,
|
||||||
|
}
|
||||||
|
|
||||||
|
export struct WeatherForecastInfo {
|
||||||
|
day_name: string,
|
||||||
|
weather_info: WeatherInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
export struct CityWeatherInfo {
|
||||||
|
city_name: string,
|
||||||
|
current_weather: WeatherInfo,
|
||||||
|
forecast_weather: [WeatherForecastInfo],
|
||||||
|
}
|
||||||
|
|
||||||
|
export global CityWeather {
|
||||||
|
in property <[CityWeatherInfo]> city-weather;
|
||||||
|
in property <bool> can-add-city: false;
|
||||||
|
|
||||||
|
pure callback refresh-all();
|
||||||
|
pure callback delete(int);
|
||||||
|
pure callback reorder(int, int);
|
||||||
|
pure callback get_forecast_graph_command([WeatherForecastInfo], int, length, length) -> string;
|
||||||
|
}
|
38
examples/weather-demo/wasm/index.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
#loading {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loading">Loading...</div>
|
||||||
|
<div class="overlay">
|
||||||
|
<!-- canvas required by the Slint runtime -->
|
||||||
|
<canvas id="canvas" data-slint-auto-resize-to-preferred="true" unselectable="on"></canvas>
|
||||||
|
<script type="module">
|
||||||
|
// import the generated file.
|
||||||
|
import init from "./pkg/rusty_weather_lib.js";
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|