weather-demo: initial commit
6
.github/workflows/wasm_demos.yaml
vendored
|
@ -58,6 +58,11 @@ jobs:
|
|||
sed -i "s/#wasm# //" Cargo.toml
|
||||
wasm-pack build --release --target web --no-default-features --features slint/default,chrono
|
||||
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"
|
||||
if: ${{ inputs.build_artifacts }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
@ -75,6 +80,7 @@ jobs:
|
|||
examples/plotter/
|
||||
examples/opengl_underlay/
|
||||
examples/energy-monitor/
|
||||
examples/weather-demo/
|
||||
!/**/.gitignore
|
||||
- name: Clean cache # Otherwise the cache is much too big
|
||||
run: |
|
||||
|
|
24
.reuse/dep5
|
@ -127,3 +127,27 @@ License: CC-BY-4.0
|
|||
Files: examples/printerdemo_mcu/zephyr/VERSION
|
||||
Copyright: Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
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/mcu-board-support',
|
||||
'examples/uefi-demo',
|
||||
'examples/weather-demo',
|
||||
'helper_crates/const-field-offset',
|
||||
'helper_crates/vtable',
|
||||
'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
|
||||
|
||||
* [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>
|